add pinocchio nft-minter example#611
Conversation
Greptile SummaryThis PR adds a Pinocchio port of the
Confidence Score: 5/5The 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
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
%%{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
Reviews (4): 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.
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.
0c168c4 to
4581d7c
Compare
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.
4581d7c to
1f10648
Compare
|
@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 🙏 |
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).