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!
| } 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; | |
| } |
| "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" |
| # 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 |
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.