add pinocchio nft-minter example#611
Conversation
Greptile SummaryThis PR adds a Pinocchio port of the
Confidence Score: 5/5Safe 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 No files require special attention for correctness; Important Files Changed
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
%%{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
Reviews (2): Last reviewed commit: "nft-minter pinocchio: read NFT token amo..." | Re-trigger Greptile |
| ); | ||
| const client = context.banksClient; | ||
| const payer = context.payer; | ||
|
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| invoke( | ||
| &instruction, | ||
| &[ | ||
| metadata_account, |
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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.
Adds a Pinocchio port of the
tokens/nft-minterexample, alongside the existinganchorandnativeversions.What it does
Two instructions, dispatched by a leading discriminator byte (matching the native
NftMinterInstructionenum):0) — creates a 0-decimal SPL mint and attaches a Metaplex metadata account via a hand-rolledCreateMetadataAccountV3CPI (name, symbol, URI; immutable, no royalties).1) — creates the payer's associated token account (idempotent), mints the single token, then creates the master edition via a hand-rolledCreateMasterEditionV3CPI (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 throughpinocchio::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.tsruns undersolana-bankrun, loading the program plus the Token Metadata program (dumped from mainnet intotests/fixturesbyprepare.mjs). Two cases:CreateMasterEditionV3CPI succeeded).