Created
January 27, 2025 19:47
-
-
Save Vectorized/7b6ff07667dececd90d29e2ac5698436 to your computer and use it in GitHub Desktop.
Solana Basic NFT Sale Program
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>, | |
} | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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