Skip to content

add pinocchio create-token example#599

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

add pinocchio create-token example#599
MarkFeder wants to merge 3 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

Adds a complete Pinocchio implementation of the create-token example, continuing the Pinocchio lane after transfer-tokens and escrow. The program creates an SPL Token mint and attaches a Metaplex CreateMetadataAccountV3 metadata account, building the CPI payload by hand since no typed Pinocchio crate exists for Metaplex.

  • Core program (create_token.rs, util.rs): parses Borsh-encoded args, creates the mint via pinocchio-system and pinocchio-token CPIs, then hand-serializes the CreateMetadataAccountV3 payload and invokes the Metaplex program directly using InstructionView/cpi::invoke. Wire format is positionally identical to the native example.
  • Tests (test.ts, prepare.mjs): bankrun test loads the Metaplex program from a mainnet-dumped fixture (via a postinstall hook) and exercises both SPL token (decimals=9) and NFT (decimals=0) creation paths, verifying account owners and metadata content.
  • Workspace integration: Cargo.toml workspace member added; README.md updated with pinocchio link.

Confidence Score: 5/5

Safe to merge — new teaching example with no changes to existing logic.

All three CPIs use correct account order and privilege flags; the hand-built Borsh payload matches Metaplex wire format; parsing is bounds-checked; the bankrun test covers the full end-to-end flow for both SPL token and NFT paths.

No files require special attention.

Important Files Changed

Filename Overview
tokens/create-token/pinocchio/program/src/instructions/create_token.rs Core instruction handler — creates mint via CPI, then invokes Metaplex CreateMetadataAccountV3 with a manually-built Borsh payload; account order and flags are correct.
tokens/create-token/pinocchio/program/src/instructions/util.rs Helper utilities for Borsh string encode/decode; bounds-checked reads and correct 4-byte LE length prefix writes.
tokens/create-token/pinocchio/program/src/lib.rs Standard no_std entrypoint with bump allocator and panic handler; straightforward.
tokens/create-token/pinocchio/program/src/processor.rs Single-instruction dispatcher; no discriminator byte, passes raw instruction data directly to create_token — consistent with stated design.
tokens/create-token/pinocchio/tests/test.ts Bankrun test covers both SPL token and NFT (decimals=0) creation; verifies mint owner and metadata owner, and spot-checks metadata content.
tokens/create-token/pinocchio/prepare.mjs postinstall script that dumps the Metaplex program from mainnet; errors are caught and logged without aborting, so a missing fixture surfaces clearly in test.
tokens/create-token/pinocchio/program/Cargo.toml Workspace-relative dependency references; declares unused custom-heap/custom-panic features (convention consistent with other pinocchio examples).

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client as Client (Test)
    participant Program as create-token (Pinocchio)
    participant SysProg as System Program
    participant TokenProg as SPL Token Program
    participant MetaplexProg as Metaplex Token Metadata

    Client->>Program: create_token(mint, mint_authority, metadata, payer, ...)
    Program->>Program: parse CreateTokenArgs (name, symbol, uri, decimals)
    Program->>Program: Rent::get().try_minimum_balance(Mint::LEN)
    Program->>SysProg: "CreateAccount (mint, lamports, space, owner=TokenProgram)"
    SysProg-->>Program: ok
    Program->>TokenProg: InitializeMint2 (decimals, mint_authority, freeze_authority)
    TokenProg-->>Program: ok
    Program->>Program: "build_metadata_data() -> Vec<u8> (discriminator=33 + DataV2 borsh)"
    Program->>MetaplexProg: CreateMetadataAccountV3 CPI (InstructionView + invoke)
    MetaplexProg-->>Program: ok
    Program-->>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 Client (Test)
    participant Program as create-token (Pinocchio)
    participant SysProg as System Program
    participant TokenProg as SPL Token Program
    participant MetaplexProg as Metaplex Token Metadata

    Client->>Program: create_token(mint, mint_authority, metadata, payer, ...)
    Program->>Program: parse CreateTokenArgs (name, symbol, uri, decimals)
    Program->>Program: Rent::get().try_minimum_balance(Mint::LEN)
    Program->>SysProg: "CreateAccount (mint, lamports, space, owner=TokenProgram)"
    SysProg-->>Program: ok
    Program->>TokenProg: InitializeMint2 (decimals, mint_authority, freeze_authority)
    TokenProg-->>Program: ok
    Program->>Program: "build_metadata_data() -> Vec<u8> (discriminator=33 + DataV2 borsh)"
    Program->>MetaplexProg: CreateMetadataAccountV3 CPI (InstructionView + invoke)
    MetaplexProg-->>Program: ok
    Program-->>Client: Ok(())
Loading

Reviews (3): Last reviewed commit: "create-token pinocchio: address review f..." | 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.
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