Skip to content

add pinocchio nft-minter example#611

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

add pinocchio nft-minter example#611
MarkFeder wants to merge 4 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 alongside the existing Anchor and native variants, with two on-chain instructions dispatched by a leading discriminator byte. Since there is no typed Pinocchio crate for mpl-token-metadata, both Metaplex CPIs (CreateMetadataAccountV3 and CreateMasterEditionV3) are constructed by hand with the correct discriminators (33 and 17) and tested via LiteSVM.

  • program/src/instructions/create.rs allocates and initialises a 0-decimal SPL mint, then invokes the Metaplex metadata CPI with hand-serialised Borsh data; mint.rs creates the payer's ATA idempotently, mints one token, and invokes CreateMasterEditionV3, transferring the mint/freeze authorities to the edition PDA.
  • program/src/processor.rs — dispatches on the first byte of instruction data, returning InvalidInstructionData for unknown variants, consistent with the native example.
  • tests/test.ts — LiteSVM suite covering both instructions; loads the mainnet Token Metadata program from tests/fixtures (dumped by prepare.mjs), asserts mint and metadata ownership after Create, and ATA balance plus edition account existence after Mint.

Confidence Score: 5/5

This PR is safe to merge; it adds a self-contained example with no shared state that could affect existing programs.

The implementation is a faithful port of the native example with well-documented hand-rolled Metaplex CPIs, correct discriminators, bounds-checked Borsh parsing, and a LiteSVM test suite that exercises both instructions end to end. No correctness issues were found after comparing against the native example and verifying account layouts and data serialisation.

No files require special attention.

Important Files Changed

Filename Overview
tokens/nft-minter/pinocchio/program/src/instructions/create.rs Implements CreateMetadataAccountV3 via hand-rolled CPI (discriminator 33); correctly duplicates mint_authority for both the mint-authority and update-authority roles, matching the native pattern.
tokens/nft-minter/pinocchio/program/src/instructions/mint.rs Implements idempotent ATA creation, MintTo, and CreateMasterEditionV3 CPI (discriminator 17); max_supply = Some(1) matches the native example exactly.
tokens/nft-minter/pinocchio/program/src/instructions/mod.rs Defines constants, CreateTokenArgs, and the read_borsh_string helper with bounds-checked slice parsing.
tokens/nft-minter/pinocchio/program/src/processor.rs Dispatcher using split_first for the discriminator byte; correctly routes to create_token (0) and mint_to (1), returning InvalidInstructionData for unknown variants.
tokens/nft-minter/pinocchio/tests/test.ts LiteSVM-based tests covering both instructions; uses readBigUInt64LE (already fixed per prior review) for u64 safety, and correctly derives metadata/edition PDAs.
tokens/nft-minter/pinocchio/prepare.mjs Dumps the mainnet Token Metadata program via solana config set -um; consistent with the pattern used in the anchor variant of this example.
tokens/nft-minter/pinocchio/program/Cargo.toml Declares workspace-pinned pinocchio dependencies; no unexpected additions beyond what the implementation requires.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant Program as NFT Minter (Pinocchio)
    participant System as System Program
    participant Token as SPL Token Program
    participant Meta as Token Metadata Program
    participant ATA as Associated Token Program

    Note over Client,Meta: Instruction 0 — Create
    Client->>Program: create_token(name, symbol, uri)
    Program->>System: "CreateAccount(mint, lamports, 82 bytes, owner=Token)"
    Program->>Token: "InitializeMint2(decimals=0, mint_authority=payer)"
    Program->>Meta: "CreateMetadataAccountV3(discriminator=33, DataV2, is_mutable=false)"

    Note over Client,Meta: Instruction 1 — Mint
    Client->>Program: mint_to()
    Program->>ATA: CreateIdempotent(payer ATA for mint)
    Program->>Token: "MintTo(amount=1, mint_authority=payer)"
    Program->>Meta: "CreateMasterEditionV3(discriminator=17, max_supply=Some(1))"
    Note over Meta: Transfers mint & freeze authority to edition PDA
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 Program as NFT Minter (Pinocchio)
    participant System as System Program
    participant Token as SPL Token Program
    participant Meta as Token Metadata Program
    participant ATA as Associated Token Program

    Note over Client,Meta: Instruction 0 — Create
    Client->>Program: create_token(name, symbol, uri)
    Program->>System: "CreateAccount(mint, lamports, 82 bytes, owner=Token)"
    Program->>Token: "InitializeMint2(decimals=0, mint_authority=payer)"
    Program->>Meta: "CreateMetadataAccountV3(discriminator=33, DataV2, is_mutable=false)"

    Note over Client,Meta: Instruction 1 — Mint
    Client->>Program: mint_to()
    Program->>ATA: CreateIdempotent(payer ATA for mint)
    Program->>Token: "MintTo(amount=1, mint_authority=payer)"
    Program->>Meta: "CreateMasterEditionV3(discriminator=17, max_supply=Some(1))"
    Note over Meta: Transfers mint & freeze authority to edition PDA
Loading

Reviews (6): Last reviewed commit: "nft-minter pinocchio: migrate test to ki..." | 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.

MarkFeder added a commit to MarkFeder/program-examples that referenced this pull request Jun 24, 2026
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.
MarkFeder added a commit to MarkFeder/program-examples that referenced this pull request Jul 8, 2026
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.
@MarkFeder
MarkFeder force-pushed the tokens-nft-minter-pinocchio branch from 0c168c4 to 4581d7c Compare July 8, 2026 21:13
MarkFeder added 2 commits July 9, 2026 09:38
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.
@MarkFeder
MarkFeder force-pushed the tokens-nft-minter-pinocchio branch from 4581d7c to 1f10648 Compare July 9, 2026 07:38
@MarkFeder

Copy link
Copy Markdown
Contributor Author

@Perelyn-sama @dev-jodee — rebased onto latest main (picks up the ASM sbpf/Solana pin from #625), CI is now fully green. Ready for review whenever you have a chance 🙏

Move the async bankrun setup out of the `describe` callback and into a
`before` hook so Mocha collects the `it` blocks (an async `describe` body
registers tests after the suite is already collected, so nothing ran).

With the test now executing, replace `Rent::try_minimum_balance` with the
integer rent formula: its floating-point exemption-threshold path emits an
opcode the bankrun VM rejects ("unsupported BPF instruction"). Matches the
create-token example.
@MarkFeder
MarkFeder requested a review from dev-jodee as a code owner July 15, 2026 21:22

@dev-jodee dev-jodee left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Will require changes based on #624 final version

Apply the kit + litesvm template from the mint-close-authority example:
build and sign transactions with @solana/kit and run them on litesvm,
dropping @solana/web3.js and solana-bankrun entirely. PDAs (metadata,
master edition, ATA) are derived with getProgramDerivedAddress; the Metaplex
Token Metadata program is still dumped from mainnet by prepare.mjs and loaded
into LiteSVM via addProgramFromFile (litesvm bundles only the SPL programs).

- deps: drop @solana/web3.js + solana-bankrun, add litesvm; pin @solana/kit
  to ^6.10.0 (litesvm's kit major) so there is a single kit in the tree
- tsconfig: bump typescript to ^5 and lib to es2022+dom (kit's types), add
  @types/node; the suite is type-clean under tsc --noEmit
@MarkFeder
MarkFeder requested a review from dev-jodee July 21, 2026 23:09
@MarkFeder

Copy link
Copy Markdown
Contributor Author

Applied the kit + litesvm template from #624 here as well. The test now builds and signs transactions with @solana/kit and runs them on litesvm, so @solana/web3.js and solana-bankrun are dropped entirely. Pinned @solana/kit to ^6.10.0 (litesvm's kit major) for a single kit in the tree, and bumped typescript/tsconfig (es2022 + dom, @types/node) so the suite is type-clean under tsc --noEmit. Verified locally: cargo build-sbf + ts-mocha2 passing, plus tsc --noEmit and biome clean. Commit is signed.

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.

2 participants