Skip to content

add pinocchio create-token example#599

Open
MarkFeder wants to merge 4 commits into
solana-foundation:mainfrom
MarkFeder:tokens-create-token-pinocchio
Open

add pinocchio create-token example#599
MarkFeder wants to merge 4 commits into
solana-foundation:mainfrom
MarkFeder:tokens-create-token-pinocchio

Conversation

@MarkFeder

Copy link
Copy Markdown
Contributor

Adds a Pinocchio implementation of tokens/create-token, continuing the Pinocchio token-examples lane after transfer-tokens (#596) and escrow (#598).

What it does

Creates an SPL Token mint and attaches an on-chain Metaplex metadata account (name, symbol, URI) — the same flow as the existing anchor and native options.

Notes

  • The Metaplex Token Metadata program has no typed Pinocchio crate, so the CreateMetadataAccountV3 CPI is built by hand (raw InstructionView / cpi::invoke), matching the on-chain wire format.
  • Single instruction, no discriminator byte — the instruction data is Borsh [name, symbol, uri, decimals], identical to the native example's wire format.
  • The bankrun test loads the Metaplex program from a mainnet-dumped token_metadata.so fixture via a postinstall script, the same approach as the anchor example.
  • The mint authority is passed as a non-signer and aliased to the payer (which signs), mirroring the native example.

Files

  • New: tokens/create-token/pinocchio/ (program + bankrun test)
  • Edited: root Cargo.toml (workspace member), README.md (pinocchio link), Cargo.lock

@greptile-apps

greptile-apps Bot commented Jul 8, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a Pinocchio implementation of the tokens/create-token example, joining the existing anchor and native variants. It creates an SPL Token mint and a Metaplex metadata account via a hand-built CreateMetadataAccountV3 CPI using Pinocchio's InstructionView, with a postinstall-dumped mainnet fixture for bankrun testing.

  • Core instruction (create_token.rs): a single no-discriminator instruction that parses Borsh [name, symbol, uri, decimals], creates and initializes a mint with InitializeMint2, then builds and invokes the Metaplex CPI entirely on a fixed-size stack buffer — matching the native wire format exactly.
  • Test (test.ts): two bankrun scenarios (SPL token at 9 decimals and NFT at 0 decimals) that load the real mainnet Metaplex binary via prepare.mjs and assert ownership of both the mint and metadata accounts.

Confidence Score: 5/5

Safe to merge — entirely additive change in a new subdirectory with no modifications to existing programs.

The hand-built Metaplex CPI wire format is verified byte-for-byte against the native example, and the fixed-size stack buffer exactly accommodates the Metaplex field maxima with overflow caught by slice.get() bounds checks. Both test scenarios exercise the full on-chain flow against the real mainnet Metaplex binary.

No files require special attention.

Important Files Changed

Filename Overview
tokens/create-token/pinocchio/program/src/instructions/create_token.rs Core instruction handler; Borsh parsing, mint creation, and manual Metaplex CPI are all correct. Buffer size (METADATA_DATA_MAX = 262) exactly fits maximum valid Metaplex inputs. is_mutable: false and all None optional fields match the native example's wire format.
tokens/create-token/pinocchio/program/src/instructions/util.rs Borsh string read/write helpers with proper bounds checking via slice.get() — safe against malformed length prefixes without allocation.
tokens/create-token/pinocchio/tests/test.ts Two bankrun test cases exercise the full on-chain flow. Both verify mint ownership (token program) and metadata ownership (Metaplex). Uses context.lastBlockhash across tests, which is safe in bankrun's simulated environment.
tokens/create-token/pinocchio/prepare.mjs Downloads the Metaplex binary from mainnet via solana program dump -um. Errors are caught and logged non-fatally, surfacing as a clear bankrun fixture-not-found failure. Consistent with the anchor example's approach.
tokens/create-token/pinocchio/program/src/lib.rs Minimal entrypoint using no_allocator! and nostd_panic_handler! — necessary to avoid heap-dependent codegen that the bankrun BPF VM rejects.
tokens/create-token/pinocchio/program/Cargo.toml Workspace-inheriting dependencies; no private features are leaked. custom-heap and custom-panic feature flags match the Pinocchio convention used in other token examples.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client as Test Client
    participant Prog as Pinocchio Program
    participant Sys as System Program
    participant Tok as Token Program
    participant Mpl as Metaplex Program

    Client->>Prog: CreateToken(name, symbol, uri, decimals)
    Note over Prog: Parse Borsh args

    Prog->>Sys: "CreateAccount(mint, space=Mint::LEN, owner=TokenProgram)"
    Sys-->>Prog: OK

    Prog->>Tok: InitializeMint2(mint, decimals, mint_authority, freeze_authority)
    Tok-->>Prog: OK

    Note over Prog: build_metadata_data() on stack buffer
    Prog->>Mpl: CreateMetadataAccountV3(metadata_pda, mint, mint_authority, payer, update_authority, system_program)
    Mpl->>Sys: CreateAccount(metadata_pda)
    Sys-->>Mpl: OK
    Mpl-->>Prog: OK

    Prog-->>Client: OK
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"}}}%%
sequenceDiagram
    participant Client as Test Client
    participant Prog as Pinocchio Program
    participant Sys as System Program
    participant Tok as Token Program
    participant Mpl as Metaplex Program

    Client->>Prog: CreateToken(name, symbol, uri, decimals)
    Note over Prog: Parse Borsh args

    Prog->>Sys: "CreateAccount(mint, space=Mint::LEN, owner=TokenProgram)"
    Sys-->>Prog: OK

    Prog->>Tok: InitializeMint2(mint, decimals, mint_authority, freeze_authority)
    Tok-->>Prog: OK

    Note over Prog: build_metadata_data() on stack buffer
    Prog->>Mpl: CreateMetadataAccountV3(metadata_pda, mint, mint_authority, payer, update_authority, system_program)
    Mpl->>Sys: CreateAccount(metadata_pda)
    Sys-->>Mpl: OK
    Mpl-->>Prog: OK

    Prog-->>Client: OK
Loading

Reviews (4): Last reviewed commit: "create-token pinocchio: rustfmt write_by..." | Re-trigger Greptile

Comment thread tokens/create-token/pinocchio/tests/test.ts
@MarkFeder MarkFeder force-pushed the tokens-create-token-pinocchio branch from e995793 to ac589b7 Compare July 9, 2026 07:38
@MarkFeder

Copy link
Copy Markdown
Contributor Author

@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 🙏

@dev-jodee dev-jodee left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

thanks for the work, couple of comments !

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
Collaborator

Choose a reason for hiding this comment

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

don't think we should change the config of the user's cli, program dump can take the url as a parameter, better do it then

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.

Good call — dropped the solana config set -um and now pass the cluster straight to the dump: solana program dump -um <id> <file>. No more touching the user's CLI config.


/// Reads a Borsh `string` (a 4-byte little-endian length prefix followed by that
/// many UTF-8 bytes) starting at `*offset`, advancing `offset` past it.
fn read_borsh_string<'a>(data: &'a [u8], offset: &mut usize) -> Result<&'a [u8], ProgramError> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this shoudl probably be in a util file instead of the mod.rs, would be cleaner

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.

Done — the borsh string helpers (read_borsh_string/push_borsh_string) now live in a new util.rs, so mod.rs is just module wiring.

}

impl<'a> CreateTokenArgs<'a> {
/// Parses the instruction data: three Borsh strings followed by a `u8`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

think this should be in create_token.rs file as well since its related to that specific ix

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.

Agreed — moved CreateTokenArgs (struct + parse) into create_token.rs since it's specific to that instruction. The Metaplex program-id const went with it.

pub use create_token::*;

/// Size (in bytes) of an SPL Token mint account.
pub const MINT_SIZE: usize = 82;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Does pinocchio have that as a const already? might be able to reuse

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.

Yes it does — switched to pinocchio_token::state::Mint::LEN and removed the hand-rolled MINT_SIZE const.

)[0];
}

describe("Create Token (Pinocchio)", async () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think this runs 0 test, since describe is async, it doesn't wait for anything ? All the setup (async) should be in before()

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.

Nice catch — you're right, the async describe callback registered the it blocks too late. Moved the async bankrun setup into a before() hook and kept describe synchronous.

- prepare.mjs: dump program via 'solana program dump -um' instead of
  mutating the user's Solana CLI config
- reuse pinocchio_token Mint::LEN instead of a hand-rolled MINT_SIZE const
- move CreateTokenArgs into create_token.rs (ix-specific) and the borsh
  string helpers into a util module; slim mod.rs to module wiring
- tests: run async bankrun setup in a before() hook so the it() blocks
  actually register (a describe callback runs synchronously)
@MarkFeder

Copy link
Copy Markdown
Contributor Author

@dev-jodee thanks for the review! Pushed a commit addressing all five points:

  • prepare.mjs dumps via solana program dump -um instead of mutating the CLI config
  • reuse pinocchio_token::state::Mint::LEN instead of a custom MINT_SIZE
  • CreateTokenArgs moved into create_token.rs; borsh string helpers moved to a util.rs; mod.rs is now just wiring
  • test setup moved into a before() hook so the it blocks actually run

Ready for another look when you have a moment.

…g tests

The tests were previously registered inside an async describe callback, so
mocha ran 0 of them and CI passed without ever executing the program. With
the before() hook they actually run — and surfaced a real failure:

  Program log: Instruction: CreateToken
  failed: unsupported BPF instruction

Root cause: Rent::try_minimum_balance() takes a floating-point path for the
exemption threshold (an f64), which the current platform-tools lower to a
float instruction the solana-bankrun VM rejects. Compute the rent-exempt
minimum with integer math using pinocchio's default rent constants instead
(DEFAULT_LAMPORTS_PER_BYTE already folds in the 2-year threshold).

While here, make the program alloc-free: build the Metaplex instruction data
in a fixed stack buffer and switch to program_entrypoint! + no_allocator!,
matching the other pinocchio examples.

Verified with cargo build-sbf (Solana 4.1.1 / platform-tools v1.54) + the
bankrun test: both cases pass.
@MarkFeder MarkFeder requested a review from dev-jodee July 13, 2026 19:47
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.

2 participants