Skip to content

add pinocchio nft-minter example#611

Open
MarkFeder wants to merge 2 commits into
solana-foundation:mainfrom
MarkFeder:tokens-nft-minter-pinocchio
Open

add pinocchio nft-minter example#611
MarkFeder wants to merge 2 commits into
solana-foundation:mainfrom
MarkFeder:tokens-nft-minter-pinocchio

Conversation

@MarkFeder

Copy link
Copy Markdown
Contributor

Adds a Pinocchio port of the tokens/nft-minter example, alongside the existing anchor and native versions.

What it does

Two instructions, dispatched by a leading discriminator byte (matching the native NftMinterInstruction enum):

  • Create (0) — creates a 0-decimal SPL mint and attaches a Metaplex metadata account via a hand-rolled CreateMetadataAccountV3 CPI (name, symbol, URI; immutable, no royalties).
  • Mint (1) — creates the payer's associated token account (idempotent), mints the single token, then creates the master edition via a hand-rolled CreateMasterEditionV3 CPI (max_supply = Some(1)). Creating the master edition hands the mint/freeze authorities to the edition PDA, making it a true non-fungible token.

Since there is no typed Pinocchio crate for mpl-token-metadata, both Metaplex instructions are built by hand (discriminators 33 and 17) and invoked through pinocchio::cpi::invoke. The mint authority is aliased to the payer (who signs the transaction) to satisfy the CPIs' signer requirements, mirroring the native example.

Tests

tests/test.ts runs under solana-bankrun, loading the program plus the Token Metadata program (dumped from mainnet into tests/fixtures by prepare.mjs). Two cases:

  • Create asserts the mint is owned by the Token program and the metadata account is owned by Token Metadata and contains the NFT name.
  • Mint asserts the ATA holds exactly 1 token and the master edition account exists and is owned by Token Metadata (proving the CreateMasterEditionV3 CPI succeeded).

@greptile-apps

greptile-apps Bot commented Jun 24, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a Pinocchio port of the tokens/nft-minter example, sitting alongside the existing anchor and native versions. Both Metaplex CPIs (CreateMetadataAccountV3 discriminator 33, CreateMasterEditionV3 discriminator 17) are hand-rolled because no typed Pinocchio crate exists for mpl-token-metadata, and the bankrun tests load the real on-chain program to verify end-to-end behavior.

  • create.rs — allocates the mint, initializes it at 0 decimals, then CPIs into Token Metadata to attach a DataV2 metadata account (immutable, no royalties); Borsh layout and account ordering match the Metaplex spec.
  • mint.rs — idempotently creates the ATA, mints one token, then CPIs CreateMasterEditionV3 with max_supply = Some(1), handing mint/freeze authority to the edition PDA to complete the NFT.
  • tests/test.ts — bankrun tests use readBigUInt64LE/BigInt for the token-amount assertion; both Create and Mint paths are covered with ownership and content checks.

Confidence Score: 5/5

Safe to merge; the program logic, CPI layouts, and tests are all correct.

The hand-rolled Metaplex CPIs use the correct discriminators (33 and 17), Borsh layouts, and account orderings as verified against the Metaplex spec. The bankrun tests load the real Token Metadata program and exercise both instructions end-to-end. The only finding is that prepare.mjs permanently changes the global Solana CLI cluster to mainnet-beta on pnpm install, which could disrupt a developer's active workflow but does not affect on-chain correctness.

No files require special attention for correctness; prepare.mjs has a minor developer-experience side effect worth addressing.

Important Files Changed

Filename Overview
tokens/nft-minter/pinocchio/program/src/instructions/create.rs Builds and invokes CreateMetadataAccountV3 by hand; discriminator (33), Borsh layout, and account ordering look correct.
tokens/nft-minter/pinocchio/program/src/instructions/mint.rs Idempotent ATA creation, MintTo, and CreateMasterEditionV3 CPI (discriminator 17, max_supply = Some(1)) are correctly ordered and Borsh-encoded.
tokens/nft-minter/pinocchio/program/src/instructions/mod.rs Defines constants, CreateTokenArgs, and a bounds-checked read_borsh_string helper; safe parsing logic.
tokens/nft-minter/pinocchio/tests/test.ts bankrun tests covering both Create and Mint paths; uses readBigUInt64LE/BigInt for token-amount assertion, asserting mint ownership, metadata content, ATA balance, and edition PDA existence.
tokens/nft-minter/pinocchio/prepare.mjs Dumps Token Metadata from mainnet into fixtures; silently and permanently sets the Solana CLI cluster to mainnet-beta via solana config set -um.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant NftMinterProgram
    participant SystemProgram
    participant TokenProgram
    participant TokenMetadataProgram
    participant ATAProgram

    Note over Client,TokenMetadataProgram: Instruction 0 — Create
    Client->>NftMinterProgram: Create(name, symbol, uri)
    NftMinterProgram->>SystemProgram: CreateAccount (mint, 82 bytes, rent-exempt)
    NftMinterProgram->>TokenProgram: "InitializeMint2 (0 decimals, mint_authority=payer)"
    NftMinterProgram->>TokenMetadataProgram: "CreateMetadataAccountV3 (DataV2, is_mutable=false)"

    Note over Client,TokenMetadataProgram: Instruction 1 — Mint
    Client->>NftMinterProgram: Mint
    NftMinterProgram->>ATAProgram: CreateIdempotent (payer ATA)
    NftMinterProgram->>TokenProgram: "MintTo (amount=1)"
    NftMinterProgram->>TokenMetadataProgram: "CreateMasterEditionV3 (max_supply=Some(1))"
    Note right of TokenMetadataProgram: Transfers mint+freeze authority to edition PDA - true NFT
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Client
    participant NftMinterProgram
    participant SystemProgram
    participant TokenProgram
    participant TokenMetadataProgram
    participant ATAProgram

    Note over Client,TokenMetadataProgram: Instruction 0 — Create
    Client->>NftMinterProgram: Create(name, symbol, uri)
    NftMinterProgram->>SystemProgram: CreateAccount (mint, 82 bytes, rent-exempt)
    NftMinterProgram->>TokenProgram: "InitializeMint2 (0 decimals, mint_authority=payer)"
    NftMinterProgram->>TokenMetadataProgram: "CreateMetadataAccountV3 (DataV2, is_mutable=false)"

    Note over Client,TokenMetadataProgram: Instruction 1 — Mint
    Client->>NftMinterProgram: Mint
    NftMinterProgram->>ATAProgram: CreateIdempotent (payer ATA)
    NftMinterProgram->>TokenProgram: "MintTo (amount=1)"
    NftMinterProgram->>TokenMetadataProgram: "CreateMasterEditionV3 (max_supply=Some(1))"
    Note right of TokenMetadataProgram: Transfers mint+freeze authority to edition PDA - true NFT
Loading

Reviews (2): Last reviewed commit: "nft-minter pinocchio: read NFT token amo..." | Re-trigger Greptile

Comment on lines +64 to +67
);
const client = context.banksClient;
const payer = context.payer;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 readTokenAmount uses Number for u64 arithmetic

buffer.readUInt32LE(68) * 4294967296 multiplies a 32-bit unsigned integer by 2³², which produces values up to ~18.4 quintillion. JavaScript's Number can only represent integers exactly up to Number.MAX_SAFE_INTEGER (2⁵³ − 1 ≈ 9 quadrillion), so any token amount whose high word is non-zero will silently lose precision. For an NFT the amount is always 1 so this is benign here, but readers learning from this example may copy the pattern for fungible tokens and get incorrect assertion results. Using buffer.readBigUInt64LE(64) and comparing against 1n avoids the issue entirely.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in 0c168c4. readTokenAmount now uses Buffer.readBigUInt64LE(64) and the assertion compares against 1n, so the full u64 range is represented exactly.

Comment on lines +86 to +89
invoke(
&instruction,
&[
metadata_account,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 No upper-bound validation on metadata string fields

read_borsh_string safely guards against reading beyond the input buffer, but name, symbol, and uri are forwarded to the Metaplex CreateMetadataAccountV3 CPI without checking Metaplex's field-length limits (name ≤ 32 chars, symbol ≤ 10 chars, uri ≤ 200 chars). An oversized value will silently pass on-chain parsing and only fail inside the CPI, producing a low-signal error. For a teaching example, an explicit length check before the CPI would make the constraint visible to learners.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving this as-is for consistency. This example is a direct Pinocchio port of the existing tokens/nft-minter/native (and anchor) variants, and neither of those validates name/symbol/uri lengths before the CPI — Metaplex itself enforces the limits (name ≤ 32, symbol ≤ 10, uri ≤ 200) and returns an error. Adding a length guard here would diverge from the canonical examples this one is meant to mirror, so I am keeping the behavior identical across the three variants.

Use Buffer.readBigUInt64LE so the full u64 range is represented exactly,
avoiding the silent precision loss of Number arithmetic above 2^53.
Addresses review feedback on PR solana-foundation#611.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant