Skip to content

add pinocchio nft-minter example - #611

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

add pinocchio nft-minter example#611
MarkFeder wants to merge 5 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 implementations. It faithfully mirrors the native example's two-instruction flow (Create + Mint) while hand-rolling the Metaplex CreateMetadataAccountV3 (discriminator 33) and CreateMasterEditionV3 (discriminator 17) CPIs since no typed Pinocchio crate exists for mpl-token-metadata.

  • Create instruction (create.rs): allocates and initializes a 0-decimal SPL mint, then invokes CreateMetadataAccountV3 with name/symbol/URI; account ordering matches the current on-chain Metaplex IDL.
  • Mint instruction (mint.rs): idempotently creates the payer's ATA, mints the single token, then invokes CreateMasterEditionV3 with max_supply = Some(1) — consistent with the native example and correctly omitting the deprecated rent sysvar.
  • Tests (tests/test.ts): use litesvm (not solana-bankrun as stated in the PR description) to load the program alongside a mainnet-dumped token_metadata.so; both test cases pass end-to-end assertions on account ownership and token balance.

Confidence Score: 5/5

The program logic is correct and the Metaplex CPI account orderings align with the current on-chain IDL. Safe to merge.

The hand-rolled Metaplex CPIs are wired correctly (account order, discriminators, Borsh layout), Borsh string parsing guards against out-of-bounds reads, and the tests exercise the full Create→Mint flow end-to-end. The only item worth noting is that prepare.mjs mutates the global Solana CLI config, which is a minor usability concern for developers running pnpm install in this directory.

No files require special attention. prepare.mjs has a minor global-config side effect but does not affect program correctness.

Important Files Changed

Filename Overview
tokens/nft-minter/pinocchio/program/src/instructions/create.rs Hand-rolls CreateMetadataAccountV3 CPI; account ordering and data layout match the Metaplex on-chain IDL; payer-as-mint-authority aliasing mirrors native example
tokens/nft-minter/pinocchio/program/src/instructions/mint.rs Hand-rolls CreateMasterEditionV3 CPI with max_supply=Some(1); account ordering aligns with current Metaplex program IDL (no deprecated rent sysvar); idempotent ATA creation is correct
tokens/nft-minter/pinocchio/program/src/instructions/mod.rs Defines shared constants, TOKEN_METADATA_PROGRAM_ID, and safe Borsh string parsing with correct bounds checking
tokens/nft-minter/pinocchio/program/src/processor.rs Clean discriminator dispatch; correctly returns InvalidInstructionData for empty input and unknown variants
tokens/nft-minter/pinocchio/tests/test.ts Uses litesvm (not solana-bankrun as PR description says); correctly uses getTokenDecoder for u64 amount comparison; PDA derivations match on-chain seeds
tokens/nft-minter/pinocchio/prepare.mjs Dumps token_metadata.so from mainnet via postinstall; permanently changes global Solana CLI config to mainnet instead of using per-command --url flag
tokens/nft-minter/pinocchio/program/Cargo.toml Correct workspace dependencies; cdylib+lib crate-type is standard for Solana programs

Sequence Diagram

sequenceDiagram
    participant Client
    participant Program as NFT Minter (Pinocchio)
    participant SystemProg as System Program
    participant TokenProg as SPL Token Program
    participant ATAProg as Associated Token Program
    participant MetaProg as Token Metadata Program

    Note over Client,MetaProg: Instruction 0 – Create
    Client->>Program: Create(name, symbol, uri)
    Program->>SystemProg: CreateAccount (mint, MINT_SIZE, token_program owner)
    SystemProg-->>Program: ok
    Program->>TokenProg: "InitializeMint2 (0 decimals, mint_authority=payer)"
    TokenProg-->>Program: ok
    Program->>MetaProg: CreateMetadataAccountV3 (discriminator 33)
    MetaProg-->>Program: metadata PDA created
    Program-->>Client: Ok

    Note over Client,MetaProg: Instruction 1 – Mint
    Client->>Program: Mint
    Program->>ATAProg: CreateIdempotent (payer ATA)
    ATAProg-->>Program: ok
    Program->>TokenProg: "MintTo (amount=1)"
    TokenProg-->>Program: ok
    Program->>MetaProg: "CreateMasterEditionV3 (discriminator 17, max_supply=Some(1))"
    Note right of MetaProg: Transfers mint & freeze authorities to edition PDA
    MetaProg-->>Program: edition PDA created
    Program-->>Client: Ok
Loading

Reviews (7): Last reviewed commit: "nft-minter pinocchio: use official packa..." | 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.

Applies dev-jodee's solana-foundation#624 review refinements on top of the kit + litesvm test:

- program: compute rent with Rent::get()?.try_minimum_balance(MINT_SIZE)?
  instead of the integer-math workaround (only needed to dodge the f64 opcode
  the old bankrun VM rejected; litesvm runs the real syscall).
- test: source the token, associated-token and system program ids from the
  official @solana-program/token and @solana-program/system packages, and read
  the minted amount with getTokenDecoder().decode(...).amount instead of a raw
  byte offset. Token Metadata has no official @solana-program client, so its id
  stays hand-rolled.
- tsconfig: moduleResolution bundler for the packages' subpath exports.

Verified locally: cargo build-sbf + ts-mocha -> 2 passing, tsc --noEmit and
biome clean, frozen-lockfile OK.
@MarkFeder

Copy link
Copy Markdown
Contributor Author

Applied the same refinements @dev-jodee asked for on #624 (now merged):

  • program: rent is computed with Rent::get()?.try_minimum_balance(MINT_SIZE)? (the integer-math workaround was only needed to dodge the f64 opcode the old bankrun VM rejected; litesvm runs the real syscall).
  • test: the token, associated-token and system program ids now come from the official @solana-program/token / @solana-program/system packages, and the minted amount is read with getTokenDecoder().decode(...).amount instead of a raw byte offset. Token Metadata has no official @solana-program client, so its id stays hand-rolled.
  • tsconfig: moduleResolution: bundler for the packages' subpath exports.

cargo build-sbf + ts-mocha2 passing; tsc, biome, fmt and clippy clean; frozen-lockfile OK. 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