Update the initialize_token_mint
instruction to also create a token metadata account.
Here's a reference initialize
instruction that:
- initialize a mint
- creates a metadata account for the mint
- mints the token
You can remove the mint_to
cpi from the initialize
instruction for your use case.
use anchor_lang::{prelude::*, solana_program::program::invoke_signed};
use anchor_spl::{
associated_token::AssociatedToken,
token::{mint_to, Mint, MintTo, Token, TokenAccount},
};
use borsh::BorshDeserialize;
use mpl_token_metadata::{
instruction::create_metadata_accounts_v3, pda::find_metadata_account, ID as MetadataID,
};
declare_id!("8LgkDyXcYXtX6h78eNRydLr2tkWHxduREnpb5TZdgE8b");
#[program]
pub mod token_with_metadata {
use super::*;
pub fn initialize(
ctx: Context<InitializeMint>,
uri: String,
name: String,
symbol: String,
) -> Result<()> {
let seeds = &["mint".as_bytes(), &[*ctx.bumps.get("mint").unwrap()]];
let signer = [&seeds[..]];
let account_info = vec![
ctx.accounts.metadata.to_account_info(),
ctx.accounts.mint.to_account_info(),
ctx.accounts.user.to_account_info(),
ctx.accounts.token_metadata_program.to_account_info(),
ctx.accounts.token_program.to_account_info(),
ctx.accounts.system_program.to_account_info(),
ctx.accounts.rent.to_account_info(),
];
invoke_signed(
&create_metadata_accounts_v3(
ctx.accounts.token_metadata_program.key(), // token metadata program
ctx.accounts.metadata.key(), // metadata account PDA for mint
ctx.accounts.mint.key(), // mint account
ctx.accounts.mint.key(), // mint authority
ctx.accounts.user.key(), // payer for transaction
ctx.accounts.mint.key(), // update authority
name, // name
symbol, // symbol
uri, // uri (offchain metadata)
None, // (optional) creators
0, // seller free basis points
true, // (bool) update authority is signer
true, // (bool) is mutable
None, // (optional) collection
None, // (optional) uses
None, // (optional) collection details
),
account_info.as_slice(),
&signer,
)?;
mint_to(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
MintTo {
authority: ctx.accounts.mint.to_account_info(),
to: ctx.accounts.token_account.to_account_info(),
mint: ctx.accounts.mint.to_account_info(),
},
&signer,
),
1,
)?;
Ok(())
}
}
#[derive(Accounts)]
pub struct InitializeMint<'info> {
#[account(
init,
seeds = [b"mint"],
bump,
payer = user,
mint::decimals = 6,
mint::authority = mint,
)]
pub mint: Account<'info, Mint>,
/// CHECK: Using "address" constraint to validate metadata account address
#[account(
mut,
address=find_metadata_account(&mint.key()).0
)]
pub metadata: UncheckedAccount<'info>,
#[account(
init_if_needed,
payer = user,
associated_token::mint = mint,
associated_token::authority = user
)]
pub token_account: Account<'info, TokenAccount>,
#[account(mut)]
pub user: Signer<'info>,
pub token_program: Program<'info, Token>,
pub token_metadata_program: Program<'info, TokenMetaData>,
pub associated_token_program: Program<'info, AssociatedToken>,
pub rent: Sysvar<'info, Rent>,
pub system_program: Program<'info, System>,
}
#[derive(Clone)]
pub struct TokenMetaData;
impl anchor_lang::Id for TokenMetaData {
fn id() -> Pubkey {
MetadataID
}
}
here's how the test looks
import * as anchor from "@project-serum/anchor"
import { Program } from "@project-serum/anchor"
import { TokenWithMetadata } from "../target/types/token_with_metadata"
import { findMetadataPda } from "@metaplex-foundation/js"
import { getAccount, getAssociatedTokenAddressSync } from "@solana/spl-token"
describe("token-with-metadata", () => {
const provider = anchor.AnchorProvider.env()
anchor.setProvider(provider)
const program = anchor.workspace
.TokenWithMetadata as Program<TokenWithMetadata>
const TOKEN_METADATA_PROGRAM_ID = new anchor.web3.PublicKey(
"metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"
)
const nft = {
uri: "https://arweave.net/OwXDf7SM6nCVY2cvQ4svNjtV7WBTz3plbI4obN9JNkk",
name: "NAME",
symbol: "SYMBOL",
}
const [mintPDA] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("mint")],
program.programId
)
const tokenAccount = getAssociatedTokenAddressSync(
mintPDA,
provider.wallet.publicKey
)
it("Is initialized!", async () => {
// Add your test here.
const metadataPDA = await findMetadataPda(mintPDA)
const tx = await program.methods
.initialize(nft.uri, nft.name, nft.symbol)
.accounts({
mint: mintPDA,
metadata: metadataPDA,
tokenAccount: tokenAccount,
user: provider.wallet.publicKey,
tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
})
.rpc()
console.log("Your transaction signature", tx)
const account = await getAccount(provider.connection, tokenAccount)
console.log(account.amount)
})
})
Also update Cargo.toml
, you can also remove init-if-needed
if you're not using it
[dependencies]
anchor-lang = { version = "0.25.0", features = ["init-if-needed"] }
anchor-spl = "0.25.0"
mpl-token-metadata = {version = "1.5.0", features = ["no-entrypoint"]}
Check out the metaplex sdk to see how to create the uri for the off-chain metadata for your token
https://github.com/metaplex-foundation/js#readme
You'll need to:
- upload an image to a storage service to get the image uri (ex. arweave)
- then upload again with the other metadata you want for the token (name, symbol etc) and the include the image uri from the previous step
Here's an example of the BONK token metadata
image uri: https://arweave.net/hQiPZOsRZXGXBJd_82PhVdlM_hACsT_q6wqwf5cSY7I
token metadata uri (which points to the image uri): https://arweave.net/QPC6FYdUn-3V8ytFNuoCS85S2tHAuiDblh6u3CIZLsw
Also recommend looking through the metaplex docs, great diagrams for understanding relationship between accounts
https://docs.metaplex.com/programs/understanding-programs
https://docs.metaplex.com/programs/token-metadata/overview