Skip to content

add pinocchio nft-operations example#617

Open
MarkFeder wants to merge 1 commit into
solana-foundation:mainfrom
MarkFeder:tokens-nft-operations-pinocchio
Open

add pinocchio nft-operations example#617
MarkFeder wants to merge 1 commit into
solana-foundation:mainfrom
MarkFeder:tokens-nft-operations-pinocchio

Conversation

@MarkFeder

Copy link
Copy Markdown
Contributor

Description

Adds a Pinocchio port of the tokens/nft-operations example, alongside the existing anchor implementation. 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

  • CreateCollection — creates a 0-decimal mint (authority = the [b"authority"] PDA), mints one token to the user, and attaches Metaplex metadata marked as a sized collection (CollectionDetails::V1) plus a master edition.
  • MintNft — creates an NFT mint the same way, with metadata that references the collection mint (unverified) and a master edition.
  • VerifyCollection — invokes the Metaplex Verify instruction (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.ts runs against solana-bankrun (the Token Metadata program is dumped from mainnet into tests/fixtures via prepare.mjs): create collection → mint NFT into it → verify the NFT as part of the collection, asserting account ownership and token balances. The Metaplex Verify instruction performs strict on-chain checks, so a successful verification confirms the whole flow.

Notes

  • The [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.
  • Metadata names/symbols are hardcoded, matching the Anchor sources.

@greptile-apps

greptile-apps Bot commented Jun 28, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a Pinocchio version of the NFT operations example. The main changes are:

  • New on-chain Pinocchio program for collection creation, NFT minting, and collection verification.
  • Hand-rolled Token Metadata CPI data and account construction.
  • Bankrun tests and fixture preparation for the Token Metadata program.
  • Package, workspace, script, and README updates for the new example.

Confidence Score: 4/5

The new example has several fixable issues in its CPI packing and local setup scripts.

  • Token Metadata calls may be encoded with the wrong discriminator format.
  • Collection verification may pass a program account where an optional delegate record is expected.
  • Install-time fixture setup changes global Solana CLI state and can hide setup failures.
  • Deploy scripts point at an output name that the build does not create.

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

Security Review

The install hook switches the global Solana CLI config to mainnet, which can make later local deploy commands target the wrong cluster.

Important Files Changed

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]
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"}}}%%
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]
Loading

Reviews (1): Last reviewed commit: "add pinocchio nft-operations example" | Re-trigger Greptile

Comment on lines +35 to +39
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

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.

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.

Comment on lines +60 to +69
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()),
];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

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.

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" });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security 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!

Comment on lines +33 to +35
} catch (error) {
console.error(`Failed to prepare program fixtures: ${error.message}`);
}

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 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.

Suggested change
} 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"

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 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.

Suggested change
"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

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 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.

Suggested change
solana program deploy ./program/target/so/program.so
solana program deploy ./program/target/so/nft_operations_pinocchio_program.so

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