Skip to content

Instantly share code, notes, and snippets.

@Vectorized
Created January 27, 2025 19:47
Show Gist options
  • Save Vectorized/7b6ff07667dececd90d29e2ac5698436 to your computer and use it in GitHub Desktop.
Save Vectorized/7b6ff07667dececd90d29e2ac5698436 to your computer and use it in GitHub Desktop.
Solana Basic NFT Sale Program
use anchor_lang::prelude::*;
use anchor_spl::associated_token::{self, AssociatedToken};
use anchor_spl::token::{self, InitializeMint, Mint, MintTo, Token, TokenAccount};
use mpl_token_metadata::instruction::{create_master_edition_v3, create_metadata_accounts_v3};
use mpl_token_metadata::state::{Creator, DataV2};
use solana_program::program::invoke_signed;
use solana_program::system_instruction;
declare_id!("FILL_IN_YOUR_PROGRAM_ID_HERE");
#[program]
pub mod nft_sale {
use super::*;
/// Buys a newly minted NFT if the user pays exactly 0.01 SOL.
/// - `uri` is the link to your JSON metadata file (e.g. "https://example.com/metadata.json").
/// - `title` is the NFT title (e.g. "My NFT").
/// - `symbol` is the NFT symbol (e.g. "MYNFT").
pub fn buy_nft(ctx: Context<BuyNft>, uri: String, title: String, symbol: String) -> Result<()> {
// 0.01 SOL in lamports
let lamports_required = 10_000_000; // 1 SOL = 1_000_000_000 lamports
// Transfer 0.01 SOL from the payer to the owner
let ix = system_instruction::transfer(
&ctx.accounts.payer.key(),
&ctx.accounts.owner.key(),
lamports_required,
);
invoke_signed(
&ix,
&[
ctx.accounts.payer.to_account_info(),
ctx.accounts.owner.to_account_info(),
ctx.accounts.system_program.to_account_info(),
],
&[],
)?;
// Initialize the mint (NFT)
let cpi_ctx = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
InitializeMint {
mint: ctx.accounts.mint.to_account_info(),
rent: ctx.accounts.rent.to_account_info(),
},
);
token::initialize_mint(cpi_ctx, 0, &ctx.accounts.mint_authority.key(), Some(&ctx.accounts.mint_authority.key()))?;
// Create the Associated Token Account for the payer (to hold the new NFT)
{
let cpi_ctx = CpiContext::new(
ctx.accounts.associated_token_program.to_account_info(),
associated_token::Create {
payer: ctx.accounts.payer.to_account_info(),
associated_token: ctx.accounts.payer_ata.to_account_info(),
authority: ctx.accounts.payer.to_account_info(),
mint: ctx.accounts.mint.to_account_info(),
system_program: ctx.accounts.system_program.to_account_info(),
token_program: ctx.accounts.token_program.to_account_info(),
rent: ctx.accounts.rent.to_account_info(),
},
);
associated_token::create(cpi_ctx)?;
}
// Mint 1 token (the NFT) into the payer's associated token account
{
let cpi_ctx = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
MintTo {
mint: ctx.accounts.mint.to_account_info(),
to: ctx.accounts.payer_ata.to_account_info(),
authority: ctx.accounts.mint_authority.to_account_info(),
},
);
token::mint_to(cpi_ctx, 1)?;
}
// Prepare the metadata
let creators = vec![Creator {
address: ctx.accounts.owner.key(),
verified: false,
share: 100,
}];
let data_v2 = DataV2 {
name: title.clone(),
symbol: symbol.clone(),
uri: uri.clone(),
seller_fee_basis_points: 500, // 5% royalty
creators: Some(creators),
collection: None,
uses: None,
};
// We'll sign metadata creation with the mint_authority PDA
let metadata_seeds = &[
b"mint_authority".as_ref(),
ctx.program_id.as_ref(),
&[ctx.bumps["mint_authority"]],
];
let signer = &[&metadata_seeds[..]];
// Create Metadata Account
let metadata_ix = create_metadata_accounts_v3(
ctx.accounts.token_metadata_program.key(),
ctx.accounts.metadata.key(),
ctx.accounts.mint.key(),
ctx.accounts.mint_authority.key(), // update authority
ctx.accounts.payer.key(), // payer
ctx.accounts.mint_authority.key(), // mint authority
title,
symbol,
uri,
None, // creators (handled by data_v2 below)
500, // seller_fee_basis_points
true, // update authority is signer
true, // is mutable
None, // collection
None, // uses
None, // collection details
);
invoke_signed(
&metadata_ix,
&[
ctx.accounts.metadata.to_account_info(),
ctx.accounts.mint.to_account_info(),
ctx.accounts.mint_authority.to_account_info(),
ctx.accounts.payer.to_account_info(),
ctx.accounts.mint_authority.to_account_info(),
ctx.accounts.token_metadata_program.to_account_info(),
ctx.accounts.system_program.to_account_info(),
ctx.accounts.rent.to_account_info(),
],
signer,
)?;
// Create Master Edition (so it’s recognized as a unique NFT)
let master_edition_ix = create_master_edition_v3(
ctx.accounts.token_metadata_program.key(),
ctx.accounts.master_edition.key(),
ctx.accounts.mint.key(),
ctx.accounts.mint_authority.key(),
ctx.accounts.payer.key(),
ctx.accounts.mint_authority.key(),
None, // max supply = unlimited
);
invoke_signed(
&master_edition_ix,
&[
ctx.accounts.master_edition.to_account_info(),
ctx.accounts.mint.to_account_info(),
ctx.accounts.mint_authority.to_account_info(),
ctx.accounts.payer.to_account_info(),
ctx.accounts.mint_authority.to_account_info(),
ctx.accounts.token_metadata_program.to_account_info(),
ctx.accounts.system_program.to_account_info(),
ctx.accounts.rent.to_account_info(),
],
signer,
)?;
Ok(())
}
}
#[derive(Accounts)]
#[instruction(uri: String, title: String, symbol: String)]
pub struct BuyNft<'info> {
/// The user paying 0.01 SOL to buy the NFT
#[account(mut)]
pub payer: Signer<'info>,
/// The owner (recipient of the 0.01 SOL). Could be the project's treasury, etc.
#[account(mut)]
pub owner: UncheckedAccount<'info>,
/// A PDA used as the mint authority (and also the update authority for metadata).
/// We'll create it (and fund its rent) on the fly.
#[account(
seeds = [b"mint_authority", program_id.as_ref()],
bump,
)]
/// CHECK: Only used as a program-derived signing authority
pub mint_authority: AccountInfo<'info>,
/// The new mint for the NFT
#[account(
init,
payer = payer,
space = 82, // Enough for a Mint account
)]
pub mint: Account<'info, Mint>,
/// The associated token account where the payer will receive the minted NFT
#[account(
init_if_needed,
payer = payer,
associated_token::mint = mint,
associated_token::authority = payer
)]
pub payer_ata: Account<'info, TokenAccount>,
/// The metadata PDA for this new mint:
/// pda = ["metadata", token_metadata_program_id, mint_id]
#[account(mut)]
/// CHECK: Metaplex will create/check this account
pub metadata: UncheckedAccount<'info>,
/// The master edition PDA for this mint:
/// pda = ["metadata", token_metadata_program_id, mint_id, "edition"]
#[account(mut)]
/// CHECK: Metaplex will create/check this account
pub master_edition: UncheckedAccount<'info>,
/// The Token Metadata program (Metaplex)
/// mainnet: metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s
pub token_metadata_program: UncheckedAccount<'info>,
/// The SPL Token program
pub token_program: Program<'info, Token>,
/// The Associated Token Program
pub associated_token_program: Program<'info, AssociatedToken>,
/// The System Program
#[account(address = system_program::ID)]
pub system_program: Program<'info, System>,
/// Rent sysvar
#[account(address = sysvar::rent::ID)]
pub rent: Sysvar<'info, Rent>,
/// We store bump seeds for PDAs here
#[account(mut)]
pub bumps: AccountInfo<'info>,
}
import * as anchor from "@project-serum/anchor";
import { Program } from "@project-serum/anchor";
import { NftSale } from "../target/types/nft_sale"; // <-- The generated IDL TS type
import { PublicKey, Keypair, SystemProgram } from "@solana/web3.js";
import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { assert } from "chai";
// Metaplex Token Metadata Program (mainnet)
const TOKEN_METADATA_PROGRAM_ID = new PublicKey(
"metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"
);
describe("nft_sale program", () => {
// Set up the local Anchor provider
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
// Load the compiled program
const program = anchor.workspace.NftSale as Program<NftSale>;
it("Buys an NFT for 0.01 SOL", async () => {
// The payer (buyer) is the wallet from the Anchor local provider
const payer = (provider.wallet as anchor.Wallet).payer;
const payerPubkey = payer.publicKey;
// The 'owner' (recipient of funds) can be a new Keypair or your treasury
const ownerKeypair = Keypair.generate();
// Derive the mintAuthority PDA
const [mintAuthorityPda, mintAuthorityBump] = await PublicKey.findProgramAddress(
[Buffer.from("mint_authority"), program.programId.toBuffer()],
program.programId
);
// We'll create the Mint account. We can let Anchor do "init" in the program
// but we still need to derive the address if we want a PDA for it.
// For simplicity, let's just create a new Keypair for the mint:
// (Alternatively, you could do a PDA-based mint as well.)
const mintKeypair = Keypair.generate();
// Derive the Metadata PDA
const [metadataPda] = await PublicKey.findProgramAddress(
[
Buffer.from("metadata"),
TOKEN_METADATA_PROGRAM_ID.toBuffer(),
mintKeypair.publicKey.toBuffer(),
],
TOKEN_METADATA_PROGRAM_ID
);
// Derive the Master Edition PDA
const [masterEditionPda] = await PublicKey.findProgramAddress(
[
Buffer.from("metadata"),
TOKEN_METADATA_PROGRAM_ID.toBuffer(),
mintKeypair.publicKey.toBuffer(),
Buffer.from("edition"),
],
TOKEN_METADATA_PROGRAM_ID
);
// We must pass a "bumps" account (or something similar) if your program expects it.
// This depends on how you structured your program’s PDAs. If you do not store them,
// you can remove that from the program. For demonstration:
const bumpsKeypair = Keypair.generate();
// (In many examples, "bumps" is a simple account storing the PDAs.
// If you don't need that, remove it from your program.)
// Let’s call the instruction
const tx = await program.methods
.buyNft(
"https://example.com/metadata.json", // uri
"My Demo NFT", // title
"DEMO" // symbol
)
.accounts({
payer: payerPubkey,
owner: ownerKeypair.publicKey,
mintAuthority: mintAuthorityPda,
mint: mintKeypair.publicKey,
payerAta: anchor.web3.Keypair.generate().publicKey, // anchor will create via `init_if_needed`
metadata: metadataPda,
masterEdition: masterEditionPda,
tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
tokenProgram: TOKEN_PROGRAM_ID,
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
systemProgram: SystemProgram.programId,
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
bumps: bumpsKeypair.publicKey, // only if your program expects it
})
.signers([payer, ownerKeypair, mintKeypair, bumpsKeypair]) // Signers that pay for creation
.rpc();
console.log("Transaction signature:", tx);
// You could fetch the new NFT's associated token account to verify
// or confirm the owner's SOL balance increased by 0.01, etc.
// For now, we just confirm the transaction went through.
assert.ok(tx, "Transaction should succeed");
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment