Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Import a Foreign Program

Call another on-chain program from your own without hand-writing its wire format. pina import fetches a foreign program’s IDL, renders a standalone no_std CPI crate, and records what the crate was generated from.

Synopsis

pina import <NAME> --program-id <PUBKEY> [OPTIONS]
InputDefaultMeaning
<NAME>requiredCrate name; written to clients/cpi/<NAME>.
--program-idrequiredProgram ID the crate targets.
--idl <FILE>noneRead the IDL from a local file.
--url <URL>noneFetch the IDL over HTTPS. Plain HTTP is accepted only from localhost/127.0.0.1 for development.
--clustermainnet-betaFetch the on-chain canonical IDL for this cluster.
--output <DIR>clients/cpiDirectory to write the crate into.
--mode <MODE>autoauto, create, update, or complete overwrite.
--npx <COMMAND>npxRunner for Anchor IDL conversion.
--skip-unsupported-instructionsoffSkip instructions the renderer cannot express instead of failing; each skip is recorded in the generated instructions/mod.rs.

Give one of --idl, --url, or --cluster. Passing --idl with --url is rejected rather than silently preferring one.

# From a vendored IDL
pina import switchboard \
  --program-id SBondMDrcV3K4kxZR1HNVT7osZxAHVHgYXL5Ze1oMUv \
  --idl ./idls/on_demand.json

# From a URL
pina import metaplex \
  --program-id metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s \
  --url https://example.com/token_metadata.json

# From the program's published on-chain IDL
pina import squads \
  --program-id SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf \
  --cluster mainnet-beta

What the generated crate contains

Each instruction becomes a call struct holding its accounts in the target program’s own order plus a typed ix field for the arguments:

RandomnessReveal {
    randomness,
    oracle,
    queue,
    ix: RandomnessRevealIx { signature, recovery_id, value },
}
.invoke_signed(program, &[signer])?;

Each account in the IDL also gets a read-only parser:

use switchboard_cpi::accounts::randomness_account_data::RandomnessAccountData;

let state = RandomnessAccountData::parse(randomness.try_borrow()?.as_ref())
    .ok_or(ProgramError::InvalidAccountData)?;
if state.reveal_slot == clock.slot {
    let value = state.value;
}

The parser carries the account’s discriminator as a public constant, its encoded size as LEN (or MAX_LEN when the layout varies), a matches guard, and a parse that returns None on a short buffer or a foreign discriminator.

An account whose layout has a variable-width field still gets its struct, discriminator, and size constant, but no parse: a variable-width field leaves every field after it with no fixed offset, so a partial parser would read the wrong bytes. The generated crate records why in a PARSER_UNSUPPORTED constant so the omission is visible at the call site.

Provenance

pina import stamps the generated README with the IDL’s SHA-256, where it came from, and the generator version:

FieldValue
Program IDSBondMDrcV3K4kxZR1HNVT7osZxAHVHgYXL5Ze1oMUv
IDL sourcefile ./idls/on_demand.json
IDL SHA-256336e8714...
Generatorpina_cpi_renderer 0.18.0

The digest is the contract a reviewer checks. A CPI crate is a copy of another program’s interface, so without a recorded digest a reviewed crate can be regenerated from a different IDL with nothing in the diff to show it.

Re-running the same import against an unchanged IDL reports already up to date and rewrites nothing, so an import checked into CI produces no drift.

Binding the program ID

The target program address is compiled into the crate as an Address constant, and is_expected_program compares an address against it. Use it when the address arrives from caller input:

if !switchboard_cpi::is_expected_program(oracle_program.address()) {
    return Err(ProgramError::IncorrectProgramId);
}

The generated crate also ships a unit test that binds the compiled-in constant to the address spelled in the IDL. A swapped dependency could otherwise retarget every CPI in the crate without the source changing, so the expected address is asserted in full rather than only through the constant.

Supported argument shapes

Anchor routes most non-primitive arguments through definedTypes. The renderer resolves those links and declares the structs and enums it needs as Rust types with their own encoders, so nesting stays recursive instead of unrolling into the instruction body.

Supported:

  • little-endian integers u8-u128 and i8-i128, booleans, and public keys
  • fixed-width byte arrays
  • length-prefixed strings, byte slices, arrays, and maps
  • Option, including Anchor’s variable-length form
  • structs, tuples, and enums whose variants carry equally sized payloads
  • structs and enums referenced through definedTypeLinkNode

Rejected, with the reason in the error message:

ShapeWhy
shortU16Anchor encodes it as a 1-3 byte variable-length prefix reserved for account lengths.
f32 / f64A no_std crate has no float ABI conversion, so the writer would silently disagree.
Bare stringWithout a length prefix a reader cannot tell where the value ends.
Bare bytesSame; wrap it in sizePrefixTypeNode or fixedSizeTypeNode.
Big-endian intsSolana instruction data is little-endian.
Uneven enumsVariants with different payload widths have no single instruction-data layout.

Optional accounts

Anchor’s programId strategy — where an absent optional account becomes the program ID — is supported directly.

The omitted strategy, where an absent account is dropped from the list entirely, cannot be expressed by a fixed-size CPI account array — filling the slot with a placeholder would send the callee an account it does not expect. Instructions using this strategy therefore fail the import with the instruction named. Pass --skip-unsupported-instructions to import the rest of the program: the skipped instructions and the reasons are written into the generated instructions/mod.rs, so the gap is visible to every reviewer of the crate.

Verifying an import

The same gate that renders every checked-in fixture compiles the result for bpfel-unknown-none:

./scripts/verify-cpi-fixtures-sbf.sh

Checked-in fixtures cover Switchboard On-Demand randomness, Metaplex Token Metadata, Meteora DLMM, and Squads v4 multisig. The Switchboard fixture pins its instruction discriminators, account counts, and encoded instruction lengths against the hand-written reference crate in pina-rs/lootbox, and they match exactly.