I'm converting my custom SPL token anchor program from using mint token as a newly generated anchor keypair, to using a PDA instead.
It seems that SPL can take either a keypair or PDA for the mint token account. However, I'm running into issues with my SPL setup.
Error:
Error in 'Create token!': AnchorError: AnchorError caused by account: mint_token. Error Code: AccountOwnedByWrongProgram. Error Number: 3007. Error Message: The given account is owned by a different program than expected.
Program log: Left:
Program log: 5qoJ4bwuvYbzKDbEhjfZbvsF8A8C6QrMT1LDomq2PMpA
Program log: Right:
Program log: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
at Function.parse (/Users/gavinmai/personal/portfolio/hermes/node_modules/@coral-xyz/anchor/src/error.ts:167:14)
at translateError (/Users/gavinmai/personal/portfolio/hermes/node_modules/@coral-xyz/anchor/src/error.ts:276:35)
at MethodsBuilder.rpc [as _rpcFn] (/Users/gavinmai/personal/portfolio/hermes/node_modules/@coral-xyz/anchor/src/program/namespace/rpc.ts:35:29)
at processTicksAndRejections (node:internal/process/task_queues:95:5) {
errorLogs: [
'Program log: AnchorError caused by account: mint_token. Error Code: AccountOwnedByWrongProgram. Error Number: 3007. Error Message: The given account is owned by a different program than expected.',
'Program log: Left:',
'Program log: 5qoJ4bwuvYbzKDbEhjfZbvsF8A8C6QrMT1LDomq2PMpA',
'Program log: Right:',
'Program log: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'
],
logs: [
'Program 5qoJ4bwuvYbzKDbEhjfZbvsF8A8C6QrMT1LDomq2PMpA invoke [1]',
'Program log: Instruction: CreateToken',
'Program 11111111111111111111111111111111 invoke [2]',
'Program 11111111111111111111111111111111 success',
'Program log: AnchorError caused by account: mint_token. Error Code: AccountOwnedByWrongProgram. Error Number: 3007. Error Message: The given account is owned by a different program than expected.',
'Program log: Left:',
'Program log: 5qoJ4bwuvYbzKDbEhjfZbvsF8A8C6QrMT1LDomq2PMpA',
'Program log: Right:',
'Program log: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
'Program 5qoJ4bwuvYbzKDbEhjfZbvsF8A8C6QrMT1LDomq2PMpA consumed 12806 of 200000 compute units',
'Program 5qoJ4bwuvYbzKDbEhjfZbvsF8A8C6QrMT1LDomq2PMpA failed: custom program error: 0xbbf'
],
error: {
errorCode: { code: 'AccountOwnedByWrongProgram', number: 3007 },
errorMessage: 'The given account is owned by a different program than expected',
comparedValues: [
[PublicKey [PublicKey(5qoJ4bwuvYbzKDbEhjfZbvsF8A8C6QrMT1LDomq2PMpA)]],
[PublicKey [PublicKey(TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA)]]
],
origin: 'mint_token'
},
_programErrorStack: ProgramErrorStack {
stack: [
[PublicKey [PublicKey(5qoJ4bwuvYbzKDbEhjfZbvsF8A8C6QrMT1LDomq2PMpA)]]
]
}
}
Clearly, this is because my PDA is owned by my local program, and NOT by the constant token program id TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
I've attached my anchor code and test case in the hopes that someone more experienced can help me out.
Anchor Lib.rs
use anchor_lang::prelude::*;
use anchor_spl::{token::{Token, TokenAccount, Mint}, associated_token::AssociatedToken};
use mpl_token_metadata::types::DataV2;
// Declare the program ID for the test token
declare_id!("5qoJ4bwuvYbzKDbEhjfZbvsF8A8C6QrMT1LDomq2PMpA");
// The main program module for the test token
#[program]
pub mod test_token {
// Import the system program and various token-related functions from the anchor_spl library
use anchor_lang::system_program;
use anchor_spl::{token::{initialize_mint, InitializeMint, mint_to, MintTo, transfer, Transfer, burn, Burn, freeze_account, FreezeAccount, close_account, CloseAccount, thaw_account, ThawAccount, set_authority, SetAuthority}, associated_token, metadata::{create_metadata_accounts_v3, create_master_edition_v3}};
// Import the parent module
use super::*;
// Function to create a new test token
// Takes in a context of CreateToken, the number of decimals for the token, and the initial amount of the token
pub fn create_token(ctx: Context<CreateToken>, decimals:u8, amount:u64) -> Result<()> {
// Create a new account for the mint token
system_program::create_account(
CpiContext::new(
// The system program account
ctx.accounts.system_program.to_account_info(),
// The CreateAccount instruction, specifying the signer and the mint token account
system_program::CreateAccount { from: ctx.accounts.signer.to_account_info(), to: ctx.accounts.mint_token.to_account_info() }
),
// The amount of lamports to fund the account with
10_000_000,
// The size of the account data
82,
// The program ID of the account
ctx.accounts.token_program.key
)?;
// Initialize the mint for the token
initialize_mint(
CpiContext::new(
// The token program account
ctx.accounts.token_program.to_account_info(),
// The InitializeMint instruction, specifying the mint token account and the rent account
InitializeMint{mint:ctx.accounts.mint_token.to_account_info(),rent:ctx.accounts.rent.to_account_info()}
),
// The number of decimals for the token
decimals,
// The mint authority for the token
ctx.accounts.signer.key,
// The freeze authority for the token
Some(ctx.accounts.signer.key)
)?;
// Create an associated token account for the token
associated_token::create(
CpiContext::new(
// The associated token program account
ctx.accounts.associate_token_program.to_account_info(),
// The Create instruction, specifying the payer, the associated token account, the authority, the mint token account, the system program account, and the token program account
associated_token::Create {
payer: ctx.accounts.signer.to_account_info(),
associated_token: ctx.accounts.token_account.to_account_info(),
authority: ctx.accounts.signer.to_account_info(),
mint: ctx.accounts.mint_token.to_account_info(),
system_program: ctx.accounts.system_program.to_account_info(),
token_program: ctx.accounts.token_program.to_account_info()
}
)
)?;
// Mint the initial amount of the token to the associated token account
mint_to(
CpiContext::new(
// The token account
ctx.accounts.token_account.to_account_info(),
// The MintTo instruction, specifying the authority, the mint token account, and the token account
MintTo{authority:ctx.accounts.signer.to_account_info(),mint:ctx.accounts.mint_token.to_account_info(),to:ctx.accounts.token_account.to_account_info()}
),
// The amount of the token to mint
amount
)?;
// Return success
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(())
}
// Function to set the metadata of a test token
// Takes in a context of CreateMetadata and the metadata data
pub fn set_token_metadata(ctx: Context<CreateMetadata>, data:MetadataData)->Result<()>{
// Find the program address for the metadata
let (metadata_address, _b1) = Pubkey::find_program_address(&[
b"metadata",
&ctx.accounts.metadata_program.key.to_bytes(),
&ctx.accounts.mint_token.key().to_bytes()
],
ctx.accounts.metadata_program.key
);
// The metadata account
let metadata_account = &ctx.accounts.metadata_account;
// The master account
let master_account = &ctx.accounts.master_account;
// Check if the metadata address matches the metadata account key
if metadata_address != *metadata_account.key{
// If not, return an error
return err!(ProgramErrors::PdaNotMatched)
}
// Find the program address for the master edition
let (master_address, _b2) = Pubkey::find_program_address(&[
b"metadata",
&ctx.accounts.metadata_program.key.to_bytes(),
&ctx.accounts.mint_token.key().to_bytes(),
b"edition"
],
ctx.accounts.metadata_program.key
);
// Check if the master address matches the master account key
if master_address != *master_account.key{
// If not, return an error
return err!(ProgramErrors::PdaNotMatched)
}
// Create the metadata accounts for the token
create_metadata_accounts_v3(
CpiContext::new(
// The metadata program account
ctx.accounts.metadata_program.to_account_info(),
// The CreateMetadataAccountsV3 instruction, specifying the metadata account, the mint token account, the mint authority, the update authority, the payer, the system program account, and the rent account
anchor_spl::metadata::CreateMetadataAccountsV3{
metadata:metadata_account.to_account_info(),
mint:ctx.accounts.mint_token.to_account_info(),
mint_authority:ctx.accounts.signer.to_account_info(),
update_authority:ctx.accounts.signer.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(),
}
),
// The metadata data
DataV2 {
name: data.name,
symbol: data.symbol,
uri: data.uri,
seller_fee_basis_points: data.seller_fee_basis_points,
creators: None,
collection: None,
uses: None
},
// Whether to update the metadata
true,
// Whether to update the authority
true,
// The new authority
None
)?;
// Create the master edition for the token
create_master_edition_v3(
CpiContext::new(
// The metadata program account
ctx.accounts.metadata_program.to_account_info(),
// The CreateMasterEditionV3 instruction, specifying the metadata account, the mint token account, the mint authority, the update authority, the payer, the system program account, the rent account, the edition account, and the token program account
anchor_spl::metadata::CreateMasterEditionV3 {
metadata:metadata_account.to_account_info(),
mint:ctx.accounts.mint_token.to_account_info(),
mint_authority:ctx.accounts.signer.to_account_info(),
update_authority:ctx.accounts.signer.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(),
edition:ctx.accounts.edition_account.to_account_info(),
token_program:ctx.accounts.token_program.to_account_info()
}
),
// The supply of the token
Some(data.supply)
)?;
// Return success
Ok(())
}
}
// Struct for the metadata data
#[derive(Debug,AnchorDeserialize,AnchorSerialize)]
pub struct MetadataData{
// The name of the token
pub name: String,
// The symbol of the token
pub symbol: String,
// The URI of the token
pub uri: String,
// The seller fee basis points of the token
pub seller_fee_basis_points: u16,
// The supply of the token
pub supply: u64
}
// Accounts for the CreateMetadata instruction
#[derive(Accounts)]
pub struct CreateMetadata<'info> {
// The mint token account
#[account(mut)]
pub mint_token:Account<'info,Mint>,
// The signer account
#[account(mut)]
pub signer:Signer<'info>,
// The metadata account
#[account(mut)]
/// CHECK: check for safety in future
pub metadata_account:AccountInfo<'info>,
// The master account
#[account(mut)]
/// CHECK: check for safety in future
pub master_account:AccountInfo<'info>,
// The edition account
#[account(mut)]
/// CHECK: check for safety in future
pub edition_account:AccountInfo<'info>,
// The system program account
pub system_program:Program<'info,System>,
// The token program account
pub token_program:Program<'info,Token>,
// The associated token program account
pub associate_token_program:Program<'info,AssociatedToken>,
// The metadata program account
#[account(mut)]
/// CHECK: check for safety in future
pub metadata_program:AccountInfo<'info>,
// The rent account
pub rent:Sysvar<'info,Rent>
}
// Accounts for the CreateToken instruction
#[derive(Accounts)]
pub struct CreateToken<'info> {
// The mint token account
#[account(init_if_needed, payer = signer, seeds = [b"mint-token", signer.key().as_ref()], bump, space=82)]
pub mint_token:Account<'info,Mint>,
// The signer account
#[account(mut)]
pub signer:Signer<'info>,
// The token account
#[account(mut)]
/// CHECK: check for safety in future
pub token_account:AccountInfo<'info>,
// The system program account
pub system_program:Program<'info,System>,
// The token program account
pub token_program:Program<'info,Token>,
// The associated token program account
pub associate_token_program:Program<'info,AssociatedToken>,
// 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", signer.key().as_ref()], bump, mint::decimals = 9, mint::authority = mint_token)]
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", signer.key().as_ref()], 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", signer.key().as_ref()], 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", signer.key().as_ref()], 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", signer.key().as_ref()], 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 testTokenProgram = anchor.workspace.TestToken as Program<TestToken>;
let mintTokenPDA, mintTokenPDABump, associatedTokenProgramKey, providerTokenAccountKey, tokenAccountKey
beforeEach(async () => {
try {
[mintTokenPDA, mintTokenPDABump] = await anchor.web3.PublicKey.findProgramAddress(
[Buffer.from(anchor.utils.bytes.utf8.encode("mint-token")), provider.wallet.publicKey.toBuffer()],
testTokenProgram.programId
);
associatedTokenProgramKey = new anchor.web3.PublicKey(
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
);
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<number> {
try {
const userTokenAccountKey = await getAssociatedTokenAddress(
mintKey,
userKey
);
const accountInfo = await getAccount(connection, userTokenAccountKey);
const balance = Number(accountInfo.amount) / Math.pow(10, DECIMALS);
return balance;
} catch (error) {
console.error(
`Failed to fetch user token balance: ${error}, mintKey=${mintKey}, userKey=${userKey}`
);
throw error;
}
}
it("Create 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"
);
try {
const transactionSignature = await testTokenProgram.methods
.createToken(DECIMALS, convertTokensToBN(100))
.accounts({
mintToken: mintTokenPDA,
tokenAccount: providerTokenAccountKey,
associateTokenProgram: associatedTokenProgramKey,
})
.signers([])
.rpc();
console.log("Transaction signature", transactionSignature);
const balance = await fetchUserTokenBalance(
provider.connection,
mintTokenPDA,
provider.wallet.publicKey
);
expect(balance).to.equal(
100,
`Balance for TestToken (TEST) does not match expected balance.`
);
} catch (error) {
console.log("Error in 'Create token!': ", error);
}
});
...