Skip to content

add pinocchio nft-minter example#611

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

add pinocchio nft-minter example#611
MarkFeder wants to merge 3 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 existing tokens/nft-minter example, placing it alongside the anchor and native variants. The two instructions (Create and Mint) are dispatched by a leading discriminator byte matching the native NftMinterInstruction enum; Metaplex CPIs are built by hand since no typed Pinocchio crate exists for mpl-token-metadata.

  • Create (0): allocates and initializes a 0-decimal SPL mint, then invokes CreateMetadataAccountV3 (discriminator 33) with hand-serialized Borsh data — the signer requirement on mint_authority is satisfied by aliasing it to the payer.
  • Mint (1): idempotently creates the payer's ATA, mints the single token via MintTo, then calls CreateMasterEditionV3 (discriminator 17) to transfer mint/freeze authorities to the edition PDA, making it a true NFT.
  • Tests run under solana-bankrun, loading the mainnet Token Metadata program from a fixture dumped by prepare.mjs; two cases cover creation (mint ownership, metadata content) and minting (ATA balance, edition existence).

Confidence Score: 5/5

Safe to merge — this is a self-contained new example that adds no breaking changes to existing code.

The Pinocchio port is a faithful translation of the existing native example. Both Metaplex CPI account orderings (CreateMetadataAccountV3 and CreateMasterEditionV3) are correct, the hand-rolled Borsh serialization matches the expected wire format, the rent calculation is accurate, and the read_borsh_string parser has proper bounds checks. The signer-aliasing pattern (mint_authority = payer) is intentional and works correctly under Solana's CPI signer rules. The bankrun tests exercise both instructions end-to-end.

No files require special attention.

Important Files Changed

Filename Overview
tokens/nft-minter/pinocchio/program/src/instructions/create.rs Correct account ordering for CreateMetadataAccountV3 CPI; rent calculation matches Solana's formula; borsh serialization layout verified against Metaplex schema
tokens/nft-minter/pinocchio/program/src/instructions/mint.rs Correct account ordering for CreateMasterEditionV3 CPI; idempotent ATA creation; max_supply=Some(1) mirrors the native example
tokens/nft-minter/pinocchio/program/src/instructions/mod.rs Defines CreateTokenArgs with safe bounds-checked Borsh string parser; TOKEN_METADATA_PROGRAM_ID const is correct
tokens/nft-minter/pinocchio/program/src/processor.rs Clean discriminator-based dispatch; gracefully rejects empty instruction data and unknown discriminators
tokens/nft-minter/pinocchio/tests/test.ts Bankrun tests cover both instructions; readTokenAmount uses BigInt for exact u64 arithmetic; account key setup correctly mirrors program expectations
tokens/nft-minter/pinocchio/prepare.mjs Dumps mainnet Token Metadata program into fixtures; uses the same solana config set -um pattern as all other examples in this repo
tokens/nft-minter/pinocchio/program/src/lib.rs Minimal entrypoint with nostd_panic_handler; correctly declares alloc for Vec usage in CPI data builders

Sequence Diagram

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

    Note over Client,Metadata: Instruction 0 — Create
    Client->>Program: Create(name, symbol, uri)
    Program->>SysProgram: "CreateAccount(mint, payer, space=82)"
    SysProgram-->>Program: OK
    Program->>TokenProgram: "InitializeMint2(mint, 0 decimals, mint_auth=payer)"
    TokenProgram-->>Program: OK
    Program->>Metadata: "CreateMetadataAccountV3(discriminator=33, DataV2)"
    Metadata-->>Program: OK (metadata PDA created)

    Note over Client,Metadata: Instruction 1 — Mint
    Client->>Program: Mint()
    Program->>ATA: "CreateIdempotent(funding=payer, wallet=payer, mint)"
    ATA-->>Program: OK (ATA created or already exists)
    Program->>TokenProgram: "MintTo(mint, ata, amount=1)"
    TokenProgram-->>Program: OK
    Program->>Metadata: "CreateMasterEditionV3(discriminator=17, max_supply=Some(1))"
    Note right of Metadata: Transfers mint+freeze authority to the edition PDA
    Metadata-->>Program: OK (master edition PDA created)
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 SysProgram as System Program
    participant TokenProgram as SPL Token Program
    participant ATA as Associated Token Program
    participant Metadata as Token Metadata Program

    Note over Client,Metadata: Instruction 0 — Create
    Client->>Program: Create(name, symbol, uri)
    Program->>SysProgram: "CreateAccount(mint, payer, space=82)"
    SysProgram-->>Program: OK
    Program->>TokenProgram: "InitializeMint2(mint, 0 decimals, mint_auth=payer)"
    TokenProgram-->>Program: OK
    Program->>Metadata: "CreateMetadataAccountV3(discriminator=33, DataV2)"
    Metadata-->>Program: OK (metadata PDA created)

    Note over Client,Metadata: Instruction 1 — Mint
    Client->>Program: Mint()
    Program->>ATA: "CreateIdempotent(funding=payer, wallet=payer, mint)"
    ATA-->>Program: OK (ATA created or already exists)
    Program->>TokenProgram: "MintTo(mint, ata, amount=1)"
    TokenProgram-->>Program: OK
    Program->>Metadata: "CreateMasterEditionV3(discriminator=17, max_supply=Some(1))"
    Note right of Metadata: Transfers mint+freeze authority to the edition PDA
    Metadata-->>Program: OK (master edition PDA created)
Loading

Reviews (5): Last reviewed commit: "nft-minter pinocchio: make bankrun test ..." | 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
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