4

when i try to do cpi to program bubblegum to call burn instruction with anchor.

https://github.com/metaplex-foundation/metaplex-program-library/tree/master/bubblegum/program

i got an error :

"Program logged: "AnchorError thrown in src/lib.rs:1306. Error Code: LeafAuthorityMustSign. Error Number: 6025. Error Message: This transaction must be signed by either the leaf owner or leaf delegate.""

even though I have included the leafowner and leaf delegate accounts as signers.

here is my rust code :

    msg!("leaf_owner is signer: {:?}", owner.is_signer);
    msg!("leaf_delegate is signer: {:?}", delegate.is_signer);

    mpl_bubblegum::cpi::burn(
        CpiContext::new(
            ctx.accounts.bubblegum_program.to_account_info(),
            mpl_bubblegum::cpi::accounts::Burn {
                merkle_tree: ctx.accounts.merkle_tree.to_account_info(),
                tree_authority: ctx.accounts.tree_authority.to_account_info(),
                leaf_delegate: ctx.accounts.leaf_delegate.to_account_info(),
                leaf_owner: ctx.accounts.leaf_owner.to_account_info(),
                log_wrapper: ctx.accounts.log_wrapper.to_account_info(),
                compression_program: ctx.accounts.compression_program.to_account_info(),
                system_program: ctx.accounts.system_program.to_account_info(),
            },
        ),
        root,
        data_hash,
        creator_hash,
        nonce,
        index
    )?;

context struct :

#[instruction(
    _root: [u8; 32],
    _data_hash: [u8; 32],
    _creator_hash: [u8; 32],
    nonce: u64,
    _index: u32,
)]
pub struct ProcessRedeemRewardContext<'info> {
    #[account(mut)]
    pub leaf_owner: Signer<'info>,
    #[account(mut)]
    pub leaf_delegate: Signer<'info>,

    #[account(mut)]
    /// CHECK: checked in cpi
    pub merkle_tree: UncheckedAccount<'info>,
    #[account(mut)]
    /// CHECK: checked in cpi
    pub tree_authority: Account<'info, TreeConfig>,

    pub bubblegum_program: Program<'info, mpl_bubblegum::program::Bubblegum>,
    pub log_wrapper: Program<'info, Noop>,
    pub compression_program: Program<'info, SplAccountCompression>,
    pub system_program: Program<'info, System>,
}

my client side code :

const assetProof = await wrappedConnection.getAssetProof(new PublicKey(asset.id))
const leafNonce = asset.compression.leaf_id
const treeAuthority = getBubblegumAuthorityPDA(new PublicKey(assetProof.tree_id))
const leafDelegate = asset.ownership.delegate ? new PublicKey(asset.ownership.delegate) : new PublicKey(asset.ownership.owner)
let _tx = await (program.methods
    .processRedeemReward(
        bufferToArray(bs58.decode(assetProof.root)),
        bufferToArray(
        bs58.decode(asset.compression.data_hash.trim())
        ),
        bufferToArray(
        bs58.decode(asset.compression.creator_hash.trim())
        ),
        new anchor.BN(leafNonce),
        leafNonce,
    )
    .accounts({
        leafDelegate: leafDelegate,
        leafOwner: new PublicKey(asset.ownership.owner),
        treeAuthority: treeAuthority,
        merkleTree: new PublicKey(assetProof.tree_id),
        compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
        logWrapper: SPL_NOOP_PROGRAM_ID,
        bubblegumProgram: BGUM_PROGRAM_ID,
    })
    .instruction())

what's wrong with this? all the responses you will give will be very helpful thank you,

enter image description here

5
  • Do you mind sharing your frontend code?
    – Ademola
    Commented Apr 17, 2023 at 19:45
  • I've updated the post and included it above. i've done some cpi instructions in bubblegum like "create_tree" and "mintv1" and it worked, but not for this "burn" instruction. when I call directly to bubblegum (direct transaction) it works, but when I call via cpi it doesn't work. is there a special setup to tell the anchor that the signer like is either account A or B? thanks Commented Apr 17, 2023 at 21:23
  • Been having the same error with both burn and transfer, and only when called via CPI within an anchor program. Unsure why this check is throwing the error only when the instruction is called using a CPI: github.com/metaplex-foundation/metaplex-program-library/blob/…
    – john
    Commented Apr 18, 2023 at 15:36
  • did you have the same error john? how do you handle this? Commented Apr 18, 2023 at 16:28
  • getting the same error, haven't found solution yet
    – john
    Commented Apr 18, 2023 at 16:57

3 Answers 3

5

Update: https://github.com/metaplex-foundation/metaplex-program-library/issues/1129

Unfortunately, this is a limitation/bug/known issue with Anchor and using multipl UncheckedAccounts that are optional signers: see this issue coral-xyz/anchor#1899. There may be a way to fix this by creating a transfer_v2 with a different account structure, but otherwise I don't think we can fix transfer without a breaking change, so you'll need to use the manual cpi invoke method for now, sorry!

Credit to SolAndy's cnft-vault example.

It looks like the CPI works if we build it manually. For example:

use anchor_lang::prelude::*;
use mpl_bubblegum::program::Bubblegum;
use solana_program::pubkey::Pubkey;
use spl_account_compression::{program::SplAccountCompression, Noop};

declare_id!("ApT1qWmvuGbpjTyDXhB3U2yjxvb612xDRoeYqsUjUVgo");

#[program]
pub mod cnft_transfer {
   use super::*;

   pub fn transfer_compressed_nft<'a, 'b, 'c, 'info>(
       ctx: Context<'a, 'b, 'c, 'info, TransferCompressedNft<'info>>,
       root: [u8; 32],
       data_hash: [u8; 32],
       creator_hash: [u8; 32],
       nonce: u64,
       index: u32,
   ) -> Result<()> {
       // remaining_accounts are the accounts that make up the required proof
       let remaining_accounts_len = ctx.remaining_accounts.len();
       let mut accounts = Vec::with_capacity(
           8 // space for the 8 AccountMetas that are always included  (below)
           + remaining_accounts_len,
       );
       accounts.extend(vec![
           AccountMeta::new_readonly(ctx.accounts.tree_authority.key(), false),
           AccountMeta::new_readonly(ctx.accounts.leaf_owner.key(), true),
           AccountMeta::new_readonly(ctx.accounts.leaf_delegate.key(), false),
           AccountMeta::new_readonly(ctx.accounts.new_leaf_owner.key(), false),
           AccountMeta::new(ctx.accounts.merkle_tree.key(), false),
           AccountMeta::new_readonly(ctx.accounts.log_wrapper.key(), false),
           AccountMeta::new_readonly(ctx.accounts.compression_program.key(), false),
           AccountMeta::new_readonly(ctx.accounts.system_program.key(), false),
       ]);

       let transfer_discriminator: [u8; 8] = [163, 52, 200, 231, 140, 3, 69, 186];

       let mut data = Vec::with_capacity(
           8 // The length of transfer_discriminator,
           + root.len()
           + data_hash.len()
           + creator_hash.len()
           + 8 // The length of the nonce
           + 8, // The length of the index
       );
       data.extend(transfer_discriminator);
       data.extend(root);
       data.extend(data_hash);
       data.extend(creator_hash);
       data.extend(nonce.to_le_bytes());
       data.extend(index.to_le_bytes());

       let mut account_infos = Vec::with_capacity(
           8 // space for the 8 AccountInfos that are always included (below)
           + remaining_accounts_len,
       );
       account_infos.extend(vec![
           ctx.accounts.tree_authority.to_account_info(),
           ctx.accounts.leaf_owner.to_account_info(),
           ctx.accounts.leaf_delegate.to_account_info(),
           ctx.accounts.new_leaf_owner.to_account_info(),
           ctx.accounts.merkle_tree.to_account_info(),
           ctx.accounts.log_wrapper.to_account_info(),
           ctx.accounts.compression_program.to_account_info(),
           ctx.accounts.system_program.to_account_info(),
       ]);

       // Add "accounts" (hashes) that make up the merkle proof from the remaining accounts.
       for acc in ctx.remaining_accounts.iter() {
           accounts.push(AccountMeta::new_readonly(acc.key(), false));
           account_infos.push(acc.to_account_info());
       }

       let instruction = solana_program::instruction::Instruction {
           program_id: ctx.accounts.bubblegum_program.key(),
           accounts,
           data,
       };

       msg!("manual cpi call to bubblegum program transfer instruction");
       solana_program::program::invoke(&instruction, &account_infos[..])?;

       Ok(())
   }

   pub fn burn_compressed_nft<'a, 'b, 'c, 'info>(
       ctx: Context<'a, 'b, 'c, 'info, BurnCompressedNft<'info>>,
       root: [u8; 32],
       data_hash: [u8; 32],
       creator_hash: [u8; 32],
       nonce: u64,
       index: u32,
   ) -> Result<()> {
       // remaining_accounts are the accounts that make up the required proof
       let remaining_accounts_len = ctx.remaining_accounts.len();
       let mut accounts = Vec::with_capacity(
           7 // space for the 7 AccountMetas that are always included (below)
           + remaining_accounts_len,
       );
       accounts.extend(vec![
           AccountMeta::new_readonly(ctx.accounts.tree_authority.key(), false),
           AccountMeta::new_readonly(ctx.accounts.leaf_owner.key(), true),
           AccountMeta::new_readonly(ctx.accounts.leaf_delegate.key(), false),
           AccountMeta::new(ctx.accounts.merkle_tree.key(), false),
           AccountMeta::new_readonly(ctx.accounts.log_wrapper.key(), false),
           AccountMeta::new_readonly(ctx.accounts.compression_program.key(), false),
           AccountMeta::new_readonly(ctx.accounts.system_program.key(), false),
       ]);

       let burn_discriminator: [u8; 8] = [116, 110, 29, 56, 107, 219, 42, 93];

       let mut data = Vec::with_capacity(
           8 // The length of burn_discriminator,
           + root.len()
           + data_hash.len()
           + creator_hash.len()
           + 8 // The length of the nonce
           + 8, // The length of the index
       );
       data.extend(burn_discriminator);
       data.extend(root);
       data.extend(data_hash);
       data.extend(creator_hash);
       data.extend(nonce.to_le_bytes());
       data.extend(index.to_le_bytes());

       let mut account_infos = Vec::with_capacity(
           7 // space for the 7 AccountInfos that are always included (below)
           + remaining_accounts_len,
       );
       account_infos.extend(vec![
           ctx.accounts.tree_authority.to_account_info(),
           ctx.accounts.leaf_owner.to_account_info(),
           ctx.accounts.leaf_delegate.to_account_info(),
           ctx.accounts.merkle_tree.to_account_info(),
           ctx.accounts.log_wrapper.to_account_info(),
           ctx.accounts.compression_program.to_account_info(),
           ctx.accounts.system_program.to_account_info(),
       ]);

       // Add "accounts" (hashes) that make up the merkle proof from the remaining accounts.
       for acc in ctx.remaining_accounts.iter() {
           accounts.push(AccountMeta::new_readonly(acc.key(), false));
           account_infos.push(acc.to_account_info());
       }

       let instruction = solana_program::instruction::Instruction {
           program_id: ctx.accounts.bubblegum_program.key(),
           accounts,
           data,
       };

       msg!("manual cpi call to bubblegum program burn instruction");
       solana_program::program::invoke(&instruction, &account_infos[..])?;

       Ok(())
   }
}

#[derive(Accounts)]
pub struct TransferCompressedNft<'info> {
   #[account(mut)]
   pub leaf_owner: Signer<'info>,

   #[account(mut)]
   pub leaf_delegate: Signer<'info>,

   /// CHECK:
   #[account(
       mut,
       seeds = [merkle_tree.key().as_ref()],
       bump,
       seeds::program = bubblegum_program.key()
   )]
   pub tree_authority: UncheckedAccount<'info>,

   /// CHECK:
   #[account(mut)]
   pub merkle_tree: UncheckedAccount<'info>,

   /// CHECK:
   #[account(mut)]
   pub new_leaf_owner: UncheckedAccount<'info>,

   pub log_wrapper: Program<'info, Noop>,
   pub compression_program: Program<'info, SplAccountCompression>,
   pub bubblegum_program: Program<'info, Bubblegum>,
   pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct BurnCompressedNft<'info> {
   #[account(mut)]
   pub leaf_owner: Signer<'info>,

   #[account(mut)]
   pub leaf_delegate: Signer<'info>,

   /// CHECK:
   #[account(
       mut,
       seeds = [merkle_tree.key().as_ref()],
       bump,
       seeds::program = bubblegum_program.key()
   )]
   pub tree_authority: UncheckedAccount<'info>,

   /// CHECK:
   #[account(mut)]
   pub merkle_tree: UncheckedAccount<'info>,

   pub log_wrapper: Program<'info, Noop>,
   pub compression_program: Program<'info, SplAccountCompression>,
   pub bubblegum_program: Program<'info, Bubblegum>,
   pub system_program: Program<'info, System>,
}

The bubblegum transfer and burn instruction discriminators can be found here and here.

Then, you can built the transaction like this:

  const connection = new WrapperConnection(process.env.RPC_URL, "confirmed")
  ...

  const assetId = new PublicKey("...")
  const asset = await connection.getAsset(assetId)
  const assetProof = await connection.getAssetProof(assetId)

  // get tree account from tree address
  const treeAddress = new PublicKey(asset.compression.tree)
  const treeAccount = await ConcurrentMerkleTreeAccount.fromAccountAddress(
    connection,
    treeAddress
  )
  const treeAuthority = treeAccount.getAuthority()

  // get canopy depth from tree account
  const canopyDepth = treeAccount.getCanopyDepth()

  // get "proof path" from asset proof, these are the accounts that need to be passed to the program as remaining accounts
  // may also be empty if tree is small enough, and canopy depth is large enough
  const proofPath: AccountMeta[] = assetProof.proof
    .map((node: string) => ({
      pubkey: new PublicKey(node),
      isSigner: false,
      isWritable: false,
    }))
    .slice(0, assetProof.proof.length - (!!canopyDepth ? canopyDepth : 0))

  // get root, data hash, creator hash, nonce, and index from asset and asset proof
  const root = [...new PublicKey(assetProof.root.trim()).toBytes()]
  const dataHash = [
    ...new PublicKey(asset.compression.data_hash.trim()).toBytes(),
  ]
  const creatorHash = [
    ...new PublicKey(asset.compression.creator_hash.trim()).toBytes(),
  ]
  const nonce = asset.compression.leaf_id
  const index = asset.compression.leaf_id

 // test burn
  const tx = await program.methods
    .burnCompressedNft(root, dataHash, creatorHash, new BN(nonce), index)
    .accounts({
      leafOwner: payer.publicKey,
      leafDelegate: payer.publicKey,
      merkleTree: treeAddress,
      treeAuthority: treeAuthority,
      logWrapper: SPL_NOOP_PROGRAM_ID,
      bubblegumProgram: BUBBLEGUM_PROGRAM_ID,
      compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
    })
    .remainingAccounts(proofPath)
    .transaction()

  const txSignature = await sendAndConfirmTransaction(
    connection,
    tx,
    [payer]
  )

WrapperConnection is from this compressed nft demo repo: https://github.com/solana-developers/compressed-nfts. There's also a video walkthrough of the compressed-nfts demo repo here.

You'll need to use the Metaplex Read Api and a RPC provider that supports it to fetch the metadata of a compressed nft.

Transfer via CPI: https://explorer.solana.com/tx/2TiDMpwh34MB9cvaTyVN9RqEWBArmesAXzE3dckGqCXSHYRfbj6sigzyxmSAwxbpcdLqJrKFr5thaiDEPrLWSKan?cluster=devnet

Burn via CPI: https://explorer.solana.com/tx/2kJJxgMFJTEvJbupHpepwEV93VbneMqniJumdov7X7isk5a8LEwJDsFRMMjWYEN1uoZKtwNnXYXZnxntSLGZfi4C?cluster=devnet

Although still unsure why using mpl_bubblegum for the CPI causes the LeafAuthorityMustSign error. Anyone happen to know why building the instruction using mpl_bubblegum like below throws an error?

   pub fn transfer_compressed_nft<'a, 'b, 'c, 'info>(
        ctx: Context<'a, 'b, 'c, 'info, TransferCompressedNft<'info>>,
        root: [u8; 32],
        data_hash: [u8; 32],
        creator_hash: [u8; 32],
        nonce: u64,
        index: u32,
    ) -> Result<()> {
        let cpi_ctx = CpiContext::new(
            ctx.accounts.bubblegum_program.to_account_info(),
            Transfer {
                tree_authority: ctx.accounts.tree_authority.to_account_info(),
                leaf_owner: ctx.accounts.leaf_owner.to_account_info(),
                leaf_delegate: ctx.accounts.leaf_delegate.to_account_info(),
                new_leaf_owner: ctx.accounts.new_leaf_owner.to_account_info(),
                merkle_tree: ctx.accounts.merkle_tree.to_account_info(),
                log_wrapper: ctx.accounts.log_wrapper.to_account_info(),
                compression_program: ctx.accounts.compression_program.to_account_info(),
                system_program: ctx.accounts.system_program.to_account_info(),
            },
        )
        .with_remaining_accounts(ctx.remaining_accounts.to_vec());

        transfer(cpi_ctx, root, data_hash, creator_hash, nonce, index)?;

        Ok(())
    }
8
  • So from the research you've done, this is most likely an anchor problem? because when you try to do cpi manually without api anchor it works normally? Commented Apr 23, 2023 at 7:13
  • Still not entirely sure what's causing the error. You can use anchor (the example in the response is using anchor), but you'll need to construct the cpi manually. Its that making the CPI using mpl_bubblegum doesn't seem to work, but that could just be because it's not set up right.
    – john
    Commented Apr 23, 2023 at 8:07
  • you're right, I was thinking about calling the same function as bubblegum, but since calling spl compression requires a signer from bubblegum this doesn't solve the problem either. calling cpi manually without using the cpi anchor api seems to work for me too. thanks John for this info. don't you want to make an issue in the metaplex repo about this? Commented Apr 23, 2023 at 12:13
  • Good to see. Are you sure though that the solution is not simply down to the fact that you're now using payer.publicKey as your leafOwner and leafDelegate?
    – Ademola
    Commented Apr 23, 2023 at 23:05
  • Incidentally, in my case, the leaf owner is also a payer. I'm also not sure why cpi uses the anchor api to get the error, while cpi uses the native solana api program as above instead it works. even though all the account parameters are the same and I changed nothing, when the cpi used the cpi api from the solana program (invoke) it worked like the code above. Commented Apr 25, 2023 at 23:56
0

In your ProcessRedeemReward struct make sure the accounts in question are indicated as signers either by making them of type Signer or using the #[account(signer)] attribute.

In the case of a direct transaction it works because the client directly passes the signer privilege to the bubblegum program. In the second case though after the client passes it to your program, your program needs to maintain the flow and pass them on to the program it CPIs to.

6
  • I'm trying to update my post by giving my context struct, previously I've given the leaf owner and leaf delegate the signer attribute. do you think there is anything less than that? Thank You Commented Apr 18, 2023 at 10:17
  • I'll update my response
    – Ademola
    Commented Apr 18, 2023 at 12:49
  • I've tried it, using an account type Signer or using the attribute account (signer) it still seems to give the same error. then there are people who have the same problem, could this be an issue with the bubblegum program? because like my case, where the cpi to mint instructions, create_tree worked, but not for this burn and transfer process cpi. or did i miss something else do you think? Thank You. I've tried reading the original code from metaplex bubblegum, does this check sign line have a problem? Commented Apr 18, 2023 at 23:26
  • Is either one of the accounts you want to sign a PDA?
    – Ademola
    Commented Apr 19, 2023 at 8:52
0

I'm not sure it is possible to make a

CPI to mint_to_collection_v1() instruction, anyone have got this error before ?

Detail errors:

{ logs: [ 'Program ComputeBudget111111111111111111111111111111 invoke [1]', 'Program ComputeBudget111111111111111111111111111111 success', 'Program FB5RMfrn51PSzZHM8dXFe7zzWCwVbELohqGdRVXkne2J invoke [1]', 'Program log: Instruction: MintV3', 'Program log: collection_mint: FyisDuKY5PnFb2psKeoXywWFRMUUYjc4KFwSYdDGgunA collection_authority: Ao3EwHkp1HWuo4PM9CLhFuMwV39QRTxo8DfvjkwpKSX7 collection_update_authority: Ao3EwHkp1HWuo4PM9CLhFuMwV39QRTxo8DfvjkwpKSX7 collection_delegate_record: ARoZ2omK5WjVVr6epMjW1QmT5cy4mdpWbQ9uUA3qxhEt', 'Program log: ARoZ2omK5WjVVr6epMjW1QmT5cy4mdpWbQ9uUA3qxhEt', 'Program BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY invoke [2]', 'Program log: Instruction: MintToCollectionV1', 'Program log: mint: FyisDuKY5PnFb2psKeoXywWFRMUUYjc4KFwSYdDGgunA authority: Ao3EwHkp1HWuo4PM9CLhFuMwV39QRTxo8DfvjkwpKSX7 update_authority: Ao3EwHkp1HWuo4PM9CLhFuMwV39QRTxo8DfvjkwpKSX7 metadata_delegate_record: 5uPrMLCZXtGQ9gHbrnxcZcgAKHgeVeBB9DU3783E5G4p', 'Program log: AnchorError occurred. Error Code: InvalidDelegateRecord. Error Number: 6029. Error Message: Invalid delegate record pda derivation.', 'Program BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY consumed 76698 of 278599 compute units', 'Program BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY failed: custom program error: 0x178d', 'Program FB5RMfrn51PSzZHM8dXFe7zzWCwVbELohqGdRVXkne2J consumed 198099 of 400000 compute units', 'Program FB5RMfrn51PSzZHM8dXFe7zzWCwVbELohqGdRVXkne2J failed: custom program error: 0x178d' ] }

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.