1

i'm writing an SPL token in anchor for learning but am running into issues when transferring my test SPL token from a newly created anchor keypair.

the transfer works for provider.wallet signer (which is the default pair imported by anchor through anchor.toml).

guidance on why I'm getting this error: Error in 'Cannot transfer more tokens than available': Error: unknown signer: H9E5eYVNd8LmcnejxxethLxKJhZqYZUWv5HsEX3NXbvE would be helpful. If you have any comments on the rest of my SPL anchor code (like if it makes sense to include the burn, freeze, OR if my anchor constraints make sense or not) would be helpful.

lib.rs

use anchor_lang::prelude::*;

use anchor_spl::{
    associated_token::AssociatedToken,
    metadata::{
        create_metadata_accounts_v3,
        mpl_token_metadata::{accounts::Metadata as MetadataAccount, types::DataV2},
        CreateMetadataAccountsV3, Metadata
    },
    token::{
        transfer,
        Transfer,
        burn,
        Burn,
        freeze_account,
        FreezeAccount,
        close_account,
        CloseAccount,
        thaw_account,
        ThawAccount,
        set_authority,
        SetAuthority,
        Mint,
        mint_to,
        MintTo,
        Token,
        TokenAccount
    },
};

// Declare the program ID for the test token
declare_id!("HGskNhfn4jS5a3jhqbq6FcXCDyWk3YYLsH7kUQDyxPty");

// The main program module for the test token
#[program]
pub mod test_token {
    // Import the parent module
    use super::*;

    // Function to create a new test token
    // Takes in a context of Initialize, the number of decimals for the token, and the initial amount of the token
    pub fn initialize(
        ctx: Context<Initialize>,
        name: String,
        symbol: String,
        uri: String,
        initial_supply: u64,
    ) -> Result<()> {
        let signer_seeds: &[&[&[u8]]] = &[&[b"mint-token", &[ctx.bumps.mint_token]]];

        // Invoke the create_metadata_account_v3 instruction on the token metadata program
        create_metadata_accounts_v3(
            CpiContext::new(
                ctx.accounts.token_metadata_program.to_account_info(),
                CreateMetadataAccountsV3 {
                    metadata: ctx.accounts.metadata.to_account_info(),
                    mint: ctx.accounts.mint_token.to_account_info(),
                    mint_authority: ctx.accounts.mint_token.to_account_info(),
                    update_authority: ctx.accounts.mint_token.to_account_info(),
                    payer: ctx.accounts.signer.to_account_info(),
                    system_program: ctx.accounts.system_program.to_account_info(),
                    rent: ctx.accounts.rent.to_account_info(),
                },
            )
            .with_signer(signer_seeds),
            DataV2 {
                name: name,
                symbol: symbol,
                uri: uri,
                seller_fee_basis_points: 0,
                creators: None,
                collection: None,
                uses: None,
            },
            true, // is_mutable
            true, // update_authority_is_signer
            None, // collection_details
        )?;

        // Invoke the mint_to instruction on the token program
        mint_to(
            CpiContext::new(
                ctx.accounts.token_program.to_account_info(),
                MintTo {
                    mint: ctx.accounts.mint_token.to_account_info(),
                    to: ctx.accounts.token_account.to_account_info(),
                    authority: ctx.accounts.mint_token.to_account_info(),
                },
            )
            .with_signer(signer_seeds),
            initial_supply * 10u64.pow(ctx.accounts.mint_token.decimals as u32),
        )?;
        Ok(())
    }

    // Function to transfer test tokens from one account to another
    // Takes in a context of TransferToken and the amount of the token to transfer
    pub fn transfer_token(ctx: Context<TransferToken>, amount:u64)->Result<()>{

        // Log the start of the transfer
        msg!("Started {:} tokens transfer from account {:} to {:}",amount,ctx.accounts.from_account.key(),ctx.accounts.to_account.key());

        // Transfer the specified amount of the token from the from_account to the to_account
        transfer(
            CpiContext::new(
                // The token program account
                ctx.accounts.token_program.to_account_info(), 
                // The Transfer instruction, specifying the authority, the from_account, and the to_account
                Transfer{authority:ctx.accounts.signer.to_account_info(),from:ctx.accounts.from_account.to_account_info(),to:ctx.accounts.to_account.to_account_info()}
            ), 
            // The amount of the token to transfer
            amount
        )?;

        // Return success
        Ok(())
    }

    // Function to set the authority of a test token
    // Takes in a context of SetAuthorityToken and the value of the authority to set
    pub fn set_authority_token(ctx: Context<SetAuthorityToken>, authority_value:u8)->Result<()>{
        // Declare variables for the account or mint and the authority type
        let account_or_mint;
        let authority_type;
        // Match the authority_value to set the authority type and the account or mint
        match authority_value {
            // If the authority_value is 0, set the authority type to MintTokens and the account or mint to the mint token account
            0=> {
                authority_type = anchor_spl::token::spl_token::instruction::AuthorityType::MintTokens;
                account_or_mint=ctx.accounts.mint_token.to_account_info();
            },
            // If the authority_value is 1, set the authority type to FreezeAccount and the account or mint to the mint token account
            1=> {
                authority_type = anchor_spl::token::spl_token::instruction::AuthorityType::FreezeAccount;
                account_or_mint=ctx.accounts.mint_token.to_account_info();
            },
            // If the authority_value is 2, set the authority type to AccountOwner and the account or mint to the token account
            2 => {
                authority_type = anchor_spl::token::spl_token::instruction::AuthorityType::AccountOwner;
                account_or_mint = ctx.accounts.token_account.to_account_info();
            },
            // If the authority_value is anything else, set the authority type to CloseAccount and the account or mint to the token account
            _ => {
                authority_type = anchor_spl::token::spl_token::instruction::AuthorityType::CloseAccount;
                account_or_mint = ctx.accounts.token_account.to_account_info();
            }
        }
        // Set the authority of the account or mint
        set_authority(
            CpiContext::new(
                // The token program account
                ctx.accounts.token_program.to_account_info(), 
                // The SetAuthority instruction, specifying the account or mint and the current authority
                SetAuthority{
                    account_or_mint:account_or_mint,
                    current_authority:ctx.accounts.signer.to_account_info()
                }
            ), 
            // The authority type to set
            authority_type.clone(), 
            // The new authority
            Some(ctx.accounts.new_signer.key())
        )?;

        // Return success
        Ok(())
    }

    // Function to burn a specified amount of a test token
    // Takes in a context of BurnToken and the amount of the token to burn
    pub fn burn_token(ctx: Context<BurnToken>, amount:u64)->Result<()>{
        // Burn the specified amount of the token from the token account
        burn(
            CpiContext::new(
                // The token program account
                ctx.accounts.token_program.to_account_info(), 
                // The Burn instruction, specifying the authority, the from_account, and the mint token account
                Burn{
                    authority:ctx.accounts.signer.to_account_info(),
                    from:ctx.accounts.token_account.to_account_info(),
                    mint:ctx.accounts.mint_token.to_account_info()
                }
            ), 
            // The amount of the token to burn
            amount
        )?;
        // Return success
        Ok(())
    }

    // Function to freeze a test token
    // Takes in a context of FreezeToken
    pub fn freeze_token(ctx: Context<FreezeToken>)->Result<()>{
        
        // Freeze the token account
        freeze_account(
            CpiContext::new(
                // The token program account
                ctx.accounts.token_program.to_account_info(), 
                // The FreezeAccount instruction, specifying the account, the mint token account, and the authority
                FreezeAccount{
                    account:ctx.accounts.token_account.to_account_info(),
                    mint:ctx.accounts.mint_token.to_account_info(),
                    authority:ctx.accounts.signer.to_account_info(),
                }
            )
        )?;


        // Return success
        Ok(())
    }

    // Function to unfreeze a test token
    // Takes in a context of FreezeToken
    pub fn un_freeze_token(ctx: Context<FreezeToken>)->Result<()>{
        
        // Unfreeze the token account
        thaw_account(
            CpiContext::new(
                // The token program account
                ctx.accounts.token_program.to_account_info(), 
                // The ThawAccount instruction, specifying the account, the mint token account, and the authority
                ThawAccount{
                    account:ctx.accounts.token_account.to_account_info(),
                    mint:ctx.accounts.mint_token.to_account_info(),
                    authority:ctx.accounts.signer.to_account_info(),
                }
            )
        )?;


        // Return success
        Ok(())
    }

    // Function to close a test token
    // Takes in a context of CloseToken
    pub fn close_token(ctx: Context<CloseToken>)->Result<()>{
        
        // Close the token account
        close_account(
            CpiContext::new(
                // The token program account
                ctx.accounts.token_program.to_account_info(), 
                // The CloseAccount instruction, specifying the account, the destination, and the authority
                CloseAccount{
                    account:ctx.accounts.token_account.to_account_info(),
                    destination:ctx.accounts.signer.to_account_info(),
                    authority:ctx.accounts.signer.to_account_info(),
                }
            )
        )?;


        // Return success
        Ok(())
    }
}

// Accounts for the Initialize instruction
#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(mut)]
    pub signer:Signer<'info>,
    // The mint token account
    #[account(
        init,
        seeds = [b"mint-token"],
        bump,
        payer = signer,
        mint::decimals = 9,
        mint::authority = mint_token,    
    )]
    pub mint_token:Account<'info,Mint>,
    #[account(
        mut,
        address = MetadataAccount::find_pda(&mint_token.key()).0,
    )]
    /// CHECK: check for safety in future
    pub metadata: UncheckedAccount<'info>,
    pub token_metadata_program: Program<'info, Metadata>,
    #[account(
        init_if_needed,
        payer = signer,
        associated_token::mint = mint_token,
        associated_token::authority = signer
    )]
    pub token_account: Account<'info, TokenAccount>,
    pub token_program: Program<'info, Token>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    // The system program account
    pub system_program:Program<'info,System>,
    // The rent account
    pub rent:Sysvar<'info,Rent>
}

// Accounts for the TransferToken instruction
#[derive(Accounts)]
pub struct TransferToken<'info>{    
    // The mint token account
    #[account(mut, seeds = [b"mint-token"], bump)]
    pub mint_token:Account<'info,Mint>,
    #[account(mut)]
    pub from_account:Account<'info,TokenAccount>,
    #[account(mut)]
    pub to_account:Account<'info,TokenAccount>,
    #[account(mut)]
    pub signer:Signer<'info>,
    pub system_program:Program<'info,System>,
    pub token_program:Program<'info,Token>,
    pub associate_token_program:Program<'info,AssociatedToken>,
}

#[derive(Accounts)]
pub struct SetAuthorityToken<'info> {
    #[account(mut, seeds = [b"mint-token"], bump)]
    pub mint_token:Account<'info,Mint>,
    #[account(mut)]
    pub signer:Signer<'info>,
    #[account(mut)]
    pub new_signer:Signer<'info>,
    #[account(mut)]
    pub token_account:Account<'info,TokenAccount>,
    pub token_program:Program<'info,Token>,
}

#[derive(Accounts)]
pub struct BurnToken<'info> {
    #[account(mut, seeds = [b"mint-token"], bump)]
    pub mint_token:Account<'info,Mint>,
    #[account(mut)]
    pub signer:Signer<'info>,
    #[account(mut)]
    pub token_account:Account<'info,TokenAccount>,
    pub token_program:Program<'info,Token>,
}

#[derive(Accounts)]
pub struct FreezeToken<'info> {
    #[account(mut, seeds = [b"mint-token"], bump)]
    pub mint_token:Account<'info,Mint>,
    #[account(mut)]
    pub signer:Signer<'info>,
    #[account(mut)]
    pub token_account:Account<'info,TokenAccount>,
    pub token_program:Program<'info,Token>,
}

#[derive(Accounts)]
pub struct CloseToken<'info> {
    #[account(mut, seeds = [b"mint-token"], bump)]
    pub mint_token:Account<'info,Mint>,
    #[account(mut)]
    pub signer:Signer<'info>,
    #[account(mut)]
    pub token_account:Account<'info,TokenAccount>,
    pub token_program:Program<'info,Token>,
}

#[error_code]
pub enum ProgramErrors {
    #[msg("PDA account not matched")]
    PdaNotMatched
}

test.ts

describe("Token Operations", () => {
  const provider = anchor.AnchorProvider.env();
  anchor.setProvider(provider);
  const wallet = provider.wallet as anchor.Wallet;
  
  const testTokenProgram = anchor.workspace.TestToken as Program<TestToken>;

  let mintTokenPDA,
    mintTokenPDABump,
    associatedTokenProgramKey,
    providerTokenAccountKey,
    tokenAccountKey,
    metadataAccountAddress;

  // Metadata program id
  const METADATA_PROGRAM_ID = new PublicKey(
    "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"
  );

  // Function to airdrop tokens
  async function airdropTokens(publicKey: PublicKey, solAmount: number) {
    await provider.connection.requestAirdrop(
      publicKey,
      solAmount * anchor.web3.LAMPORTS_PER_SOL
    );
  }

  beforeEach(async () => {
    try {
      [mintTokenPDA, mintTokenPDABump] =
        await anchor.web3.PublicKey.findProgramAddress(
          [Buffer.from(anchor.utils.bytes.utf8.encode("mint-token"))],
          testTokenProgram.programId
        );

      associatedTokenProgramKey = new anchor.web3.PublicKey(
        "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
      );

      // Metadata account PDA for mint
      [metadataAccountAddress] = anchor.web3.PublicKey.findProgramAddressSync(
        [
          Buffer.from("metadata"),
          METADATA_PROGRAM_ID.toBuffer(),
          mintTokenPDA.toBuffer(),
        ],
        METADATA_PROGRAM_ID
      );

      providerTokenAccountKey = anchor.utils.token.associatedAddress({
        mint: mintTokenPDA,
        owner: provider.wallet.publicKey,
      });

      tokenAccountKey = anchor.web3.PublicKey.findProgramAddressSync(
        [
          provider.wallet.publicKey.toBuffer(),
          TOKEN_PROGRAM_ID.toBuffer(),
          mintTokenPDA.toBuffer(),
        ],
        associatedTokenProgramKey
      )[0];
    } catch (error) {
      console.error("Error calculating PDA: ", error);
    }
  });

  const DECIMALS = 9;

  function convertTokensToBN(tokens: number) {
    return new anchor.BN(tokens * Math.pow(10, DECIMALS));
  }

  async function fetchUserTokenBalance(
    connection: anchor.web3.Connection,
    mintKey: anchor.web3.PublicKey,
    userKey: anchor.web3.PublicKey
  ): Promise<anchor.BN> {
    try {
      const associatedTokenAccount = getAssociatedTokenAddressSync(
        mintKey,
        userKey
      );

      const balance = await connection.getTokenAccountBalance(
        associatedTokenAccount
      );

      return new anchor.BN(balance.value.uiAmountString);
    } catch (error) {
      console.error(
        `Failed to fetch user token balance: ${error}, mintKey=${mintKey}, userKey=${userKey}`
      );
      throw error;
    }
  }

  async function createTokenAccountIfNeeded(
    connection: anchor.web3.Connection,
    payer: anchor.web3.Keypair,
    mintTokenPDA: anchor.web3.PublicKey,
    walletPublicKey: anchor.web3.PublicKey
  ) {
    const receiverTokenAccountKey = await getAssociatedTokenAddress(
      mintTokenPDA,
      walletPublicKey
    );

    const receiverAccountInfo = await connection.getAccountInfo(
      receiverTokenAccountKey
    );

    if (!receiverAccountInfo) {
      await createAccount(connection, payer, mintTokenPDA, walletPublicKey);
    }
  }

  it("Initialize token!", async () => {
    console.log("Mint token key: ", mintTokenPDA.toBase58());
    console.log(
      "Provider token account key via anchor: ",
      providerTokenAccountKey.toBase58()
    );
    console.log("Token account key via PDA: ", tokenAccountKey.toBase58());

    expect(providerTokenAccountKey.toBase58()).to.equal(
      tokenAccountKey.toBase58(),
      "Token accounts must be same using different methods"
    );

    const associatedTokenAccount = getAssociatedTokenAddressSync(
      mintTokenPDA,
      wallet.publicKey
    );

    try {
      const initalSupply = 10000000000; // 10B
      const transaction = await testTokenProgram.methods
        .initialize(
          "Test",
          "TEST",
          "ipfs://",
          new anchor.BN(initalSupply)
        )
        .accounts({
          metadata: metadataAccountAddress,
          tokenMetadataProgram: METADATA_PROGRAM_ID,
          tokenAccount: associatedTokenAccount,
        })
        .transaction();

      const txSig = await sendAndConfirmTransaction(
        provider.connection,
        transaction,
        [wallet.payer],
        { commitment: "confirmed" }
      );

      console.log("Transaction signature", txSig);

      await createTokenAccountIfNeeded(
        provider.connection,
        wallet.payer,
        mintTokenPDA,
        wallet.publicKey
      );

      const balance = await fetchUserTokenBalance(
        provider.connection,
        mintTokenPDA,
        provider.wallet.publicKey
      );
      expect(new anchor.BN(balance)).to.eql(
        new anchor.BN(10000000000),
        `Balance for TestToken (TEST) does not match expected balance.`
      );
    } catch (error) {
      console.log("Error in 'Initialize token!': ", error);
    }
  });

  it("Token transfer", async () => {
    try {
      let receiverKeyPair = anchor.web3.Keypair.generate();

      const signature = await provider.connection.requestAirdrop(
        receiverKeyPair.publicKey,
        anchor.web3.LAMPORTS_PER_SOL
      );
      await provider.connection.confirmTransaction(signature);

      await createTokenAccountIfNeeded(
        provider.connection,
        receiverKeyPair,
        mintTokenPDA,
        receiverKeyPair.publicKey
      );

      const receiverTokenAccountKey = await getAssociatedTokenAddress(
        mintTokenPDA,
        receiverKeyPair.publicKey
      );

      const transactionSignature = await testTokenProgram.methods
        .transferToken(convertTokensToBN(90))
        .accounts({
          mintToken: mintTokenPDA,
          fromAccount: providerTokenAccountKey,
          toAccount: receiverTokenAccountKey,
          associateTokenProgram: associatedTokenProgramKey,
        })
        .signers([])
        .rpc();

      console.log("Transaction signature", transactionSignature);
      const senderBalance = await fetchUserTokenBalance(
        provider.connection,
        mintTokenPDA,
        provider.wallet.publicKey
      );
      const receiverBalance = await fetchUserTokenBalance(
        provider.connection,
        mintTokenPDA,
        receiverKeyPair.publicKey
      );
      expect(senderBalance).to.eql(
        new anchor.BN(9999999910),
        `Balance for TestToken (TEST) does not match expected balance of sender.`
      );
      expect(receiverBalance).to.eql(
        new anchor.BN(90),
        `Balance for TestToken (TEST) does not match expected balance of receiver.`
      );
    } catch (error) {
      console.log("Error in 'Token transfer': ", error);
    }
  });

  it("Cannot transfer more tokens than available", async () => {
    let receiverKeyPair = anchor.web3.Keypair.generate();

    const signature = await provider.connection.requestAirdrop(
      receiverKeyPair.publicKey,
      anchor.web3.LAMPORTS_PER_SOL
    );
    await provider.connection.confirmTransaction(signature);

    await createTokenAccountIfNeeded(
      provider.connection,
      receiverKeyPair,
      mintTokenPDA,
      receiverKeyPair.publicKey
    );

    const receiverTokenAccountKey = await getAssociatedTokenAddress(
      mintTokenPDA,
      receiverKeyPair.publicKey
    );

    // Transfer 10 tokens from wallet.payer to receiverKeyPair
    await testTokenProgram.methods
      .transferToken(convertTokensToBN(10))
      .accounts({
        mintToken: mintTokenPDA,
        fromAccount: providerTokenAccountKey,
        toAccount: receiverTokenAccountKey,
        associateTokenProgram: associatedTokenProgramKey,
      })
      .signers([])
      .rpc();

    // Attempt to transfer more tokens than receiverKeyPair has back to wallet.payer
    try {
      await testTokenProgram.methods
        .transferToken(convertTokensToBN(20))
        .accounts({
          mintToken: mintTokenPDA,
          fromAccount: receiverTokenAccountKey,
          toAccount: providerTokenAccountKey,
          associateTokenProgram: associatedTokenProgramKey,
        })
        .signers([receiverKeyPair])
        .rpc();
    } catch (error) {
      console.log("Error in 'Cannot transfer more tokens than available': ", error);
    }
  });
...

1 Answer 1

1

Rookie mistake, I thought I didn't need to pass "signer" as an account. I fixed this by changing:

// Attempt to transfer more tokens than receiverKeyPair has back to wallet.payer
    try {
      await testTokenProgram.methods
        .transferToken(convertTokensToBN(20))
        .accounts({
          signer: receiverKeyPair.publicKey, // <========= ADD THIS ******************
          mintToken: mintTokenPDA,
          fromAccount: receiverTokenAccountKey,
          toAccount: providerTokenAccountKey,
          associateTokenProgram: associatedTokenProgramKey,
        })
        .signers([receiverKeyPair])
        .rpc();
    } catch (error) {
      console.log("Error in 'Cannot transfer more tokens than available': ", error);
    }

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.