5

I've been struggling for a while with a transferV1 transaction from the mpl-token-metadata library using the Umi interface. Many examples here and there show how to do it with the classic spl-token protocol, but I have the feeling that when it comes to token-2022, things get complicated...

The bottom line is that I'm "simply" trying to make a transaction that involves a "payment" of a defined amount, but in spl-token-2022, which is why I turned to transferV1 but maybe I'm going about it the wrong way. Instruction 2: custom program error: 0x39

umi.use(walletAdapterIdentity(wallet))
const sign = createSignerFromKeypair(umi, upgAuth);
umi.use(signerIdentity(sign, false));

let sourceATA = findAssociatedTokenPda(umi, { mint: TOKEN_REWARD, owner: umi.payer.publicKey, tokenProgramId: TOKEN_2022_PROGRAM_ID });
    let destATA = findAssociatedTokenPda(umi, { mint: TOKEN_REWARD, owner: umi.identity.publicKey, tokenProgramId: TOKEN_2022_PROGRAM_ID });
    sourceATA = new PublicKey(sourceATA[0]);
    destATA = new PublicKey(destATA[0]);
    transactions = transactions.add(
        transferV1(umi, {
            mint: TOKEN_REWARD,
            authority: umi.payer,
            tokenOwner: sourceATA,
            destinationOwner: destATA,
            tokenStandard: TokenStandard.Fungible,
            splTokenProgram: TOKEN_2022_PROGRAM_ID,
            amount: 10,
        })
    );

Everytime it ends up with the following error :

Error: failed to send transaction: Transaction simulation failed: custom program error: 0x39 [...]
0x39 = Incorrect account owner 

What i'm i doing wrong ?

2 Answers 2

1

https://github.com/metaplex-foundation/mpl-token-metadata/blob/main/clients/js/src/generated/instructions/transferV1.ts

If token and destinationToken parameters are not passed, transferV1 will call findAssociatedTokenPda without passing in any tokenProgramId, which defaults to the original Token Program.

if (!resolvedAccounts.token.value) {
  resolvedAccounts.token.value = findAssociatedTokenPda(context, {
    mint: expectPublicKey(resolvedAccounts.mint.value),
    owner: expectPublicKey(resolvedAccounts.tokenOwner.value),
  });
}

if (!resolvedAccounts.destinationToken.value) {
  resolvedAccounts.destinationToken.value = findAssociatedTokenPda(context, {
    mint: expectPublicKey(resolvedAccounts.mint.value),
    owner: expectPublicKey(resolvedAccounts.destinationOwner.value),
  });
}

https://github.com/metaplex-foundation/mpl-toolbox/blob/7c252a1de09de9456f7e49dd71193df1357369f0/clients/js/src/hooked/AssociatedToken.ts#L4

const tokenProgramIdResolved =
  seeds.tokenProgramId ?? context.programs.getPublicKey('splToken');

You have to explicitly pass in token and destinationToken when transferring Token-2022 tokens, so that when findProgramAddressSync from umi is called internally, it creates the associated token accounts if it doesn't already exist.

const [token] = findAssociatedTokenPda(umi, {
  mint,
  owner: tokenOwner,
  tokenProgramId: SPL_TOKEN_2022_PROGRAM_ID,
});

const [destinationToken] = findAssociatedTokenPda(umi, {
  mint,
  owner: destinationOwner,
  tokenProgramId: SPL_TOKEN_2022_PROGRAM_ID,
});

await transferV1(umi, {
  mint,
  authority: umi.payer,
  token,
  destinationToken,
  tokenOwner,
  destinationOwner,
  tokenStandard: TokenStandard.Fungible,
  splTokenProgram: SPL_TOKEN_2022_PROGRAM_ID,
  amount: amount * ATOMIC_UNITS_PER_TOKEN,
}).sendAndConfirm(umi);
0

I ended up using createTransferCheckedInstruction from spl-token library. Then used adapter fromWeb3jsInstruction which returns Instruction type (can not be appended to umi native transactions/TransactionBuilder) because it expects WrappedInstruction type and not Instruction.

This is how to convert Instruction to Wrapped Instruction

  const transferFeeTx = createTransferCheckedInstruction(
    fungibleSourceTokenAccount,
    new Web3PublicKey(tokenAddress),
    fungibleTargetTokenAccount,
    new Web3PublicKey(umi.payer.publicKey),
    BigInt(amount * Math.pow(10, tokenDecimals)),
    tokenDecimals,
    undefined,
    TOKEN_2022_PROGRAM_ID
  );

  const umiFeeIx = fromWeb3JsInstruction(transferFeeTx);

  const wrapperUmiFeeIx: WrappedInstruction = {
    instruction: umiFeeIx,
    signers: [umi.payer],
    bytesCreatedOnChain: 0,
  };

I don't know if this is the right solution but it works just fine. In my case, I needed to append this instruction to a TransactionBuilder object.

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.