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!

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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"?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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"?

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

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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"

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"

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

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

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.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

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.

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.

@MarkFeder

Copy link
Copy Markdown
Contributor Author

Hi @Perelyn-sama — thanks again for merging my earlier pinocchio examples! This one adds the nft-operations pinocchio port (create collection / mint NFT / verify collection — the first VerifyCollectionV1 CPI in the series). It's fully green (SBF build + bankrun validate all three instructions on-chain) and MERGEABLE. I've replied to all of Greptile's review comments: the two P1 program-logic findings are false positives (legacy Token Metadata uses single-byte discriminators, and the delegate_record slot is filled exactly as the canonical VerifyCollectionV1Cpi does), and the four scaffolding notes mirror the canonical anchor nft-operations/prepare.mjs and the merged escrow/transfer-tokens deploy scripts. Would appreciate a review whenever you get a chance. Thanks!

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