add pinocchio nft-operations example#617
Conversation
Greptile SummaryThis PR adds a Pinocchio version of the NFT operations example. The main changes are:
Confidence Score: 4/5The new example has several fixable issues in its CPI packing and local setup scripts.
tokens/nft-operations/pinocchio/program/src/instructions/mod.rs, tokens/nft-operations/pinocchio/program/src/instructions/verify_collection.rs, tokens/nft-operations/pinocchio/prepare.mjs, deploy scripts
|
| Filename | Overview |
|---|---|
| tokens/nft-operations/pinocchio/program/src/instructions/mod.rs | Adds hand-rolled Token Metadata serialization and CPI helpers, with likely format risk around instruction discriminators. |
| tokens/nft-operations/pinocchio/program/src/instructions/verify_collection.rs | Adds collection verification CPI construction, with a likely account-packing issue for the optional delegate record. |
| tokens/nft-operations/pinocchio/prepare.mjs | Adds fixture dumping, but it mutates global CLI config and hides setup failures. |
| tokens/nft-operations/pinocchio/package.json | Adds package scripts for build, test, and deploy, with a deploy path that does not match the built binary name. |
| tokens/nft-operations/pinocchio/cicd.sh | Adds a quick build/deploy script, but the deploy command points at a missing output file. |
| tokens/nft-operations/pinocchio/tests/test.ts | Adds bankrun coverage for the main collection and NFT flow. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Create collection] --> B[Create mint and ATA]
B --> C[Create metadata]
C --> D[Create master edition]
D --> E[Mint NFT with collection reference]
E --> F[Verify collection membership]
%%{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"}}}%%
flowchart TD
A[Create collection] --> B[Create mint and ATA]
B --> C[Create metadata]
C --> D[Create master edition]
D --> E[Mint NFT with collection reference]
E --> F[Verify collection membership]
Reviews (1): Last reviewed commit: "add pinocchio nft-operations example" | Re-trigger Greptile
| const CREATE_METADATA_ACCOUNT_V3: u8 = 33; | ||
| /// Discriminator of the Metaplex `CreateMasterEditionV3` instruction. | ||
| const CREATE_MASTER_EDITION_V3: u8 = 17; | ||
| /// Discriminator of the Metaplex `Verify` instruction. | ||
| const VERIFY: u8 = 52; |
There was a problem hiding this comment.
Metaplex Discriminator Width Mismatch
These Metaplex instruction discriminators are emitted as one byte, but the typed Token Metadata CPI used by the matching example serializes these discriminators as little-endian u32 values. If the loaded Token Metadata program expects that format, every hand-rolled CPI starts with the wrong instruction id and the collection, mint, and verify flows fail before account validation.
There was a problem hiding this comment.
The legacy Token Metadata instructions use a single-byte discriminator (the Borsh enum variant index): CreateMetadataAccountV3 = 33 and CreateMasterEditionV3 = 17 are one byte each, and the newer Verify instruction is [52, 1] — a u8 discriminator followed by the VerificationArgs::CollectionV1 variant index. This matches the on-chain mpl-token-metadata layout (and the repo's native NFT examples), and is exercised end-to-end by the bankrun test, which loads the real Token Metadata program. A u32-wide discriminator is the Anchor account discriminator convention, which the Token Metadata program does not use for instructions.
| let verify_accounts = [ | ||
| InstructionAccount::readonly_signer(mint_authority.address()), | ||
| InstructionAccount::readonly(token_metadata_program.address()), | ||
| InstructionAccount::writable(metadata.address()), | ||
| InstructionAccount::readonly(collection_mint.address()), | ||
| InstructionAccount::writable(collection_metadata.address()), | ||
| InstructionAccount::readonly(collection_master_edition.address()), | ||
| InstructionAccount::readonly(system_program.address()), | ||
| InstructionAccount::readonly(sysvar_instructions.address()), | ||
| ]; |
There was a problem hiding this comment.
Delegate Record Account Mispacked
The Verify CPI fills the optional delegate_record slot with the Token Metadata program account instead of omitting the account. When the Token Metadata program treats a present delegate record as a real record, it validates the program id account as delegate data and the collection verification fails for otherwise valid NFTs.
There was a problem hiding this comment.
This matches the canonical mpl-token-metadata CPI. When delegate_record is None, the generated VerifyCollectionV1Cpi fills that account slot with the Token Metadata program id itself — see verify_collection_v1.rs, whose else branch pushes crate::MPL_TOKEN_METADATA_ID as a non-signer, read-only account. The program treats its own id in that slot as 'no delegate record', so passing the token metadata program account here is correct rather than a real delegate record. The bankrun test verifies a real NFT through this path successfully.
| try { | ||
| mkdirSync(outputDir, { recursive: true }); | ||
| // Point the Solana CLI at mainnet, where the canonical program lives. | ||
| execSync("solana config set -um", { stdio: "inherit" }); |
There was a problem hiding this comment.
Install Changes Deploy Cluster
postinstall runs this script, so pnpm install permanently switches the user's Solana CLI config to mainnet. A normal local flow like pnpm install && pnpm deploy or bash cicd.sh can then deploy to the wrong cluster unless the developer manually resets the CLI config.
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.
This mirrors the canonical anchor example this port is based on — tokens/nft-operations/anchor/prepare.mjs on main runs the same solana config set -um inside its postinstall step to dump the mainnet Token Metadata program into tests/fixtures/. Keeping the same behavior here is intentional so the pinocchio port stays consistent with the accepted house pattern across the metaplex examples. The switch is only needed to fetch the canonical program from mainnet; the dumped token_metadata.so is then loaded by the local bankrun validator, and no deploy in this example targets mainnet.
There was a problem hiding this comment.
This mirrors the canonical anchor example this port is based on — tokens/nft-operations/anchor/prepare.mjs on main runs the same solana config set -um inside its postinstall step to dump the mainnet Token Metadata program into tests/fixtures/. Keeping the same behavior here is intentional so the pinocchio port stays consistent with the accepted house pattern across the metaplex examples. The switch is only needed to fetch the canonical program from mainnet; the dumped token_metadata.so is then loaded by the local bankrun validator, and no deploy in this example targets mainnet.
There was a problem hiding this comment.
Fair point — the anchor version does the same thing. The concern still stands as a real footgun (any pnpm install silently flips the global CLI config), but since this is an established house pattern and the behavior is intentional and consistent, I'll defer to your judgment here. The risk is well-understood and bounded to the dev workflow, not to any actual deployments.
Should I remember this as a rule: "do not flag solana config set -um inside prepare.mjs / postinstall scripts in this repo, as it's the accepted pattern for dumping mainnet programs into bankrun fixtures"?
There was a problem hiding this comment.
Fair point — the anchor example confirms this is an established pattern in the repo. My concern stands as a general best practice, but given that this is intentionally consistent with the accepted house pattern, I'll defer to your judgment here.
Would you like me to remember this as a rule: "do not flag solana config set -um in prepare.mjs scripts — it is the accepted pattern for dumping mainnet programs into test fixtures in this repo"?
| } catch (error) { | ||
| console.error(`Failed to prepare program fixtures: ${error.message}`); | ||
| } |
There was a problem hiding this comment.
Fixture Dump Failure Is Hidden
When solana program dump fails because the CLI or network is unavailable, this catch block logs the error and lets install succeed without tests/fixtures/token_metadata.so. The later bankrun test then fails with a missing program fixture instead of reporting the real setup error.
| } catch (error) { | |
| console.error(`Failed to prepare program fixtures: ${error.message}`); | |
| } | |
| } catch (error) { | |
| console.error(`Failed to prepare program fixtures: ${error.message}`); | |
| process.exitCode = 1; | |
| } |
There was a problem hiding this comment.
This is deliberate and documented in the file header: "Errors are logged but not fatal — a missing fixture will surface as a clear test failure when bankrun can't find token_metadata.so." The canonical anchor nft-operations/prepare.mjs on main takes the same approach (logs the dump error, does not fail the install). Hard-failing postinstall on a transient solana program dump network hiccup would be noisier than the clear, actionable bankrun failure that follows.
There was a problem hiding this comment.
This is deliberate and documented in the file header: "Errors are logged but not fatal — a missing fixture will surface as a clear test failure when bankrun can not find token_metadata.so." The canonical anchor nft-operations/prepare.mjs on main takes the same approach (logs the dump error, does not fail the install). Hard-failing postinstall on a transient solana program dump network hiccup would be noisier than the clear, actionable bankrun failure that follows.
There was a problem hiding this comment.
Fair point — the header documents the intent, and matching the anchor implementation's behavior is a strong consistency argument. A transient network failure hard-crashing npm install would be a worse developer experience than the clear "missing fixture" bankrun error. I'll defer to the established pattern here.
There was a problem hiding this comment.
Fair point — the explicit documentation in the file header and consistency with the anchor implementation make the intent clear. The soft-fail approach is reasonable here. I'll drop this concern.
| "test": "pnpm ts-mocha -p ./tsconfig.json -t 1000000 ./tests/test.ts", | ||
| "build-and-test": "cargo build-sbf --manifest-path=./program/Cargo.toml --sbf-out-dir=./tests/fixtures && pnpm test", | ||
| "build": "cargo build-sbf --manifest-path=./program/Cargo.toml --sbf-out-dir=./program/target/so", | ||
| "deploy": "solana program deploy ./program/target/so/program.so" |
There was a problem hiding this comment.
Deploy Script Uses Missing Binary
cargo build-sbf emits the shared object using the crate name, nft_operations_pinocchio_program.so, not program.so. Running pnpm deploy after pnpm build therefore points solana program deploy at a file that was not produced.
| "deploy": "solana program deploy ./program/target/so/program.so" | |
| "deploy": "solana program deploy ./program/target/so/nft_operations_pinocchio_program.so" |
There was a problem hiding this comment.
The deploy script here is a convenience/reference command and is not exercised by CI — CI runs build-and-test, which builds with --sbf-out-dir=./tests/fixtures and never invokes deploy. The program.so path matches the established convention across the merged pinocchio examples (e.g. tokens/escrow/pinocchio and tokens/transfer-tokens/pinocchio on main both use solana program deploy ./program/target/so/program.so). Keeping it identical here avoids making this one example diverge from its siblings.
There was a problem hiding this comment.
The deploy script here is a convenience/reference command and is not exercised by CI — CI runs build-and-test, which builds with --sbf-out-dir=./tests/fixtures and never invokes deploy. The program.so path matches the established convention across the merged pinocchio examples (e.g. tokens/escrow/pinocchio and tokens/transfer-tokens/pinocchio on main both use solana program deploy ./program/target/so/program.so). Keeping it identical here avoids making this one example diverge from its siblings.
There was a problem hiding this comment.
Fair point — if tokens/escrow/pinocchio and tokens/transfer-tokens/pinocchio both use the same program.so path convention, keeping this consistent is the right call. The deploy script is a non-CI reference command, so diverging from the established pattern would only create unnecessary inconsistency.
I'll withdraw the comment. Thanks for the context.
There was a problem hiding this comment.
Fair point — if the existing pinocchio examples (escrow, transfer-tokens) already use program.so in their deploy scripts under the same convention, keeping this example consistent is the right call. The deploy script being a non-CI reference command makes the inconsistency acceptable, and diverging from siblings would be the worse outcome.
I'll withdraw the concern. Thanks for the context.
Shall I remember this as a rule: "The deploy script in pinocchio examples uses program.so as a convention — do not flag it as a bug since it is a non-CI reference command intentionally consistent with sibling examples"?
| # Run this bad boy with "bash cicd.sh" or "./cicd.sh" | ||
|
|
||
| cargo build-sbf --manifest-path=./program/Cargo.toml --sbf-out-dir=./program/target/so | ||
| solana program deploy ./program/target/so/program.so |
There was a problem hiding this comment.
Deploy Path Skips Built Artifact
The build step writes the SBF output with the crate-derived name, nft_operations_pinocchio_program.so. This deploy command still looks for program.so, so bash cicd.sh fails at deploy time even after a successful build.
| solana program deploy ./program/target/so/program.so | |
| solana program deploy ./program/target/so/nft_operations_pinocchio_program.so |
There was a problem hiding this comment.
Same as the package.json deploy note: cicd.sh is a reference convenience script, not part of CI (CI builds to ./tests/fixtures via build-and-test). The program.so path is the shared convention used by the merged pinocchio examples on main (escrow, transfer-tokens), so this stays consistent with the rest of the series.
There was a problem hiding this comment.
Fair point — if escrow and transfer-tokens use the same program.so convention in their cicd.sh scripts, consistency across the series is the right call. I'll drop this comment.
There was a problem hiding this comment.
Same as the package.json deploy note: cicd.sh is a reference convenience script, not part of CI (CI builds to ./tests/fixtures via build-and-test). The program.so path is the shared convention used by the merged pinocchio examples on main (escrow, transfer-tokens), so this stays consistent with the rest of the series.
|
Hi @Perelyn-sama — thanks again for merging my earlier pinocchio examples! This one adds the |
Description
Adds a Pinocchio port of the
tokens/nft-operationsexample, alongside the existinganchorimplementation. It continues the series of Pinocchio token examples in this repo.The program demonstrates the full Metaplex collection workflow, all driven from on-chain CPIs signed by a program-derived
[b"authority"]mint authority:Instructions
[b"authority"]PDA), mints one token to the user, and attaches Metaplex metadata marked as a sized collection (CollectionDetails::V1) plus a master edition.Verifyinstruction (VerificationArgs::CollectionV1) signed by the PDA (the collection's update authority) to verify the NFT as a member of the collection.All Metaplex CPIs (
CreateMetadataAccountV3,CreateMasterEditionV3,Verify) are hand-rolled, mirroring the approach used in the other Pinocchio token examples.Tests
tests/test.tsruns againstsolana-bankrun(the Token Metadata program is dumped from mainnet intotests/fixturesviaprepare.mjs): create collection → mint NFT into it → verify the NFT as part of the collection, asserting account ownership and token balances. The MetaplexVerifyinstruction performs strict on-chain checks, so a successful verification confirms the whole flow.Notes
[b"authority"]PDA is never initialized; it exists only to sign CPIs. Its bump is passed in instruction data (and verified on-chain) rather than re-derived, consistent with the other Pinocchio examples.