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 alongside the existing anchor and native variants, implementing two instructions — Create and Mint — dispatched by a single leading discriminator byte. Because no typed Pinocchio crate exists for mpl-token-metadata, both Metaplex CPIs are built by hand with correct discriminators, Borsh-encoded payloads, and account layouts verified against the Metaplex program's expected account ordering.

  • Create (discriminator 0): Uses InitializeMint2 (no rent sysvar needed) to set up a 0-decimal SPL mint, then constructs a 6-account CreateMetadataAccountV3 payload (discriminator 33) with is_mutable = false and no royalties/creators, matching the native and anchor examples.
  • Mint (discriminator 1): Idempotently creates the payer's ATA via pinocchio-associated-token-account, mints 1 token, then invokes CreateMasterEditionV3 (discriminator 17) with max_supply = Some(1), correctly transferring mint/freeze authority to the edition PDA.
  • Tests: Two solana-bankrun integration tests cover both code paths end-to-end, including assertion on the raw token-account amount field using readBigUInt64LE for full u64 precision.

Confidence Score: 5/5

The change adds a self-contained new example directory; it has no impact on existing programs or shared library code.

Both hand-rolled Metaplex CPI payloads (CreateMetadataAccountV3 discriminator 33, CreateMasterEditionV3 discriminator 17) use the correct account ordering and Borsh layout. The InitializeMint2 path correctly avoids the now-deprecated rent-sysvar requirement. The read_borsh_string parser bounds-checks both the length header and the payload slice before advancing the offset. Tests cover both instructions end-to-end with bankrun and verify on-chain ownership and state.

No files require special attention.

Important Files Changed

Filename Overview
tokens/nft-minter/pinocchio/program/src/instructions/create.rs Correctly hand-rolls CreateMetadataAccountV3 (discriminator 33): creates an SPL mint via InitializeMint2, then constructs a 6-account Borsh payload and invokes the Metaplex program via pinocchio CPI.
tokens/nft-minter/pinocchio/program/src/instructions/mint.rs Correctly creates an idempotent ATA, mints 1 token via MintTo, then hand-rolls CreateMasterEditionV3 (discriminator 17) with an 8-account slice matching the Metaplex account order; max_supply = Some(1) correctly caps the supply.
tokens/nft-minter/pinocchio/program/src/instructions/mod.rs Defines constants, shared types, and a robust read_borsh_string helper that bounds-checks both the 4-byte length header and the payload slice before advancing the offset.
tokens/nft-minter/pinocchio/tests/test.ts Two bankrun integration tests covering create and mint paths; uses readBigUInt64LE(64) correctly for token-amount comparison and verifies on-chain state (owner, metadata content, edition existence).
tokens/nft-minter/pinocchio/prepare.mjs Switches the Solana CLI to mainnet and dumps the Token Metadata program into tests/fixtures; errors are non-fatal so a clear test failure surfaces instead of a silent setup problem.

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 ATA as Associated Token Program
    participant Meta as Token Metadata Program

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

    Note over Client,Meta: Instruction 1 — Mint
    Client->>Program: Mint()
    Program->>ATA: "CreateIdempotent(payer -> ATA)"
    Program->>Token: "MintTo(amount=1, authority=payer)"
    Program->>Meta: "CreateMasterEditionV3(disc=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 ATA as Associated Token Program
    participant Meta as Token Metadata Program

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

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

Reviews (4): 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.

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 🙏

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