Decoding & IDLs
Solana instruction data is a packed Borsh byte string with no
self-describing structure — unlike an EVM log, there's no ABI travelling with it.
To turn raw bytes into typed args and named accounts, HyperIndex needs two
things per instruction:
- A discriminator — the leading bytes that identify which instruction it is.
- A schema — the argument layout and account names, from an Anchor IDL, an inline declaration, or a bundled schema.
Discriminators
The discriminator is the first N bytes of the instruction data. HyperIndex matches the leading bytes of every instruction in the matched program against the discriminators you configured; the longest configured length is tried first, so a program can mix 8-byte Anchor and 1-byte native instructions unambiguously.
- Format: hex only, with optional
0xprefix. Base58 and decimal are not accepted here (base58 is only forprogram_id). - Length: exactly 1, 2, 4, or 8 bytes (2, 4, 8, or 16 hex digits).
- Anchor programs: the discriminator is the 8-byte Anchor sighash —
sha256("global:" + snake_case(instructionName))[0..8]. - Native / non-Anchor programs: whatever leading byte(s) the program uses
(e.g. SPL Token Transfer is
0x03; Raydium AMM v4 swap is0x09).
instructions:
- name: sharedAccountsRoute
discriminator: "0xc1209b3341d69c81" # 8-byte Anchor sighash
- name: swap
discriminator: "0x09" # 1-byte native
discriminator, even with an IDLHyperIndex reads the discriminator only from config.yaml. Modern Anchor IDLs
(0.30+) embed a discriminator array, but codegen ignores it, and it is the
configured bytes that the IDL's argument/account layout is looked up by.
Omit discriminator and two things happen at once: the instruction matches
every instruction of the program, and no layout resolves, so params is
undefined on every one of them. Neither failure raises an error.
Computing an Anchor discriminator
For an Anchor program, compute the 8-byte sighash from the instruction name:
import { createHash } from "node:crypto";
function anchorDiscriminator(name: string): string {
const snake = name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
const hash = createHash("sha256").update(`global:${snake}`).digest();
return "0x" + hash.subarray(0, 8).toString("hex");
}
anchorDiscriminator("route"); // 0xe517cb977ae3ad2a
anchorDiscriminator("sharedAccountsRoute"); // 0xc1209b3341d69c81
Where the schema comes from
HyperIndex resolves an instruction's argument/account layout in this order:
| Source | When | How to use |
|---|---|---|
| Inline schema | Instruction has both args and accounts | You declare the layout directly. Highest priority; mutually exclusive with the program's idl. |
| Anchor IDL | Program has idl: set | HyperIndex derives args + accounts from the IDL entry whose discriminator matches the instruction's configured discriminator. |
| Bundled | Program id matches a built-in schema | Currently only Metaplex Token Metadata: no idl/inline needed. Looked up by discriminator the same way. |
| None | No schema resolved | The instruction still matches and fires the handler, but params is undefined (you can read raw instruction.data / instruction.accounts). |
The order above is the resolution order: inline args+accounts win, then the
IDL or bundled schema keyed by discriminator, then nothing.
Anchor IDLs
Point a program at a standard Anchor IDL JSON file (relative to config.yaml).
Both legacy (no embedded discriminator) and modern (0.30+, with discriminator)
IDLs are supported through the same path. HyperIndex extracts instruction names,
argument layouts, ordered account names (including nested account groups), and the
IDL types registry.
experimental:
programs:
- name: Jupiter
program_id: JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4
idl: idls/jupiter.json
instructions:
- name: route
discriminator: "0xe517cb977ae3ad2a"
- name: sharedAccountsRoute
discriminator: "0xc1209b3341d69c81"
With an IDL set, list each instruction's name and discriminator; the args and
accounts come from the IDL, matched on those discriminator bytes. Don't add inline
args/accounts; they're mutually exclusive with idl. Supplying only one of
args/accounts is a config error: they must be given together or both omitted.
Inline schema (no IDL)
For programs without an IDL, declare the layout yourself. args is the Borsh
argument list (in order, after the discriminator); accounts is the ordered list
of account names. They must be provided together.
experimental:
programs:
- name: Raydium
program_id: 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8
instructions:
- name: swap
discriminator: "0x09"
args:
- { name: amountIn, type: u64 }
- { name: minAmountOut, type: u64 }
accounts: # positional: accounts[0] is tokenProgram, etc.
- tokenProgram
- amm
- userSourceTokenAccount
- userDestTokenAccount
For native programs the account order is the program's canonical layout, not
something HyperIndex can verify. Check it against a real transaction. Trailing
accounts beyond your named list still arrive — see extraAccounts.
Supported argument types
Use these in inline args (and they're what HyperIndex understands from IDLs).
The right column is how each value appears in params.args.
type | Rendered in params.args as |
|---|---|
bool | boolean |
u8 u16 u32, i8 i16 i32 | number |
u64 u128 i64 i128 | decimal string (precision-safe) |
f32 f64 | number |
string | string |
bytes | 0x-prefixed hex string |
pubkey (alias publicKey) | base58 string |
{ option: <type> } | the value, or null |
{ vec: <type> } | array |
{ array: [<type>, <len>] } | array — except [u8, 32], rendered base58 as a pubkey |
{ defined: "<TypeName>" } | resolved from the IDL's types registry |
{ struct: [ {name,type}, … ] } | object |
{ enum: [ {name, fields?}, … ] } | { VariantName: { …fields } } |
args:
- { name: amount, type: u64 }
- { name: authority, type: pubkey }
- { name: maybeOwner, type: { option: pubkey } }
- { name: route, type: { vec: { defined: "RoutePlanStep" } } }
- { name: seedHash, type: { array: [u8, 32] } } # → base58 string
Decoded output
When a schema matches, instruction.params is:
type SvmInstructionParams = {
name: string; // the instruction name
args: unknown; // object keyed by arg name (typed after codegen)
accounts: Record<string, string>; // schema account name -> base58 pubkey
extraAccounts: readonly string[]; // accounts beyond the named list (base58)
};
argsis keyed by your argument names. Aftercodegenit's typed per instruction; if you bypass that (see below) it'sunknown— narrow it with a local type and read defensively.accountsmaps each schema account name to its base58 pubkey, e.g.params.accounts.mint. Names come from the IDL or your inlineaccountslist.extraAccountscollects accounts present on-chain beyond your named list — Anchorremaining_accounts, optional accounts, or accounts resolved from an address lookup table.
Reading args safely
Because args is loosely typed (and u64+ values are strings), read each field
as possibly-absent so one missing field can't throw and kill the handler:
import { type SvmInstructionParams } from "envio";
interface JupiterRouteArgs {
inAmount: string;
quotedOutAmount: string;
}
const bi = (x: unknown) =>
x === undefined || x === null ? undefined : BigInt(x as string);
function mapRoute(params: SvmInstructionParams) {
const a = params.args as Partial<JupiterRouteArgs>;
return {
inAmount: bi(a.inAmount),
outAmount: bi(a.quotedOutAmount),
sourceMint: params.accounts.sourceMint,
destMint: params.accounts.destinationMint,
};
}
When params is undefined
Decoding returns undefined (rather than crashing) when:
- the instruction has no
discriminatorinconfig.yaml, so no layout could be looked up; - the configured discriminator matched no schema entry (a stale IDL, or the wrong sighash);
- there were too few accounts for the named list;
- the argument bytes didn't decode cleanly (wrong/partial layout).
None of these throws, and the indexer keeps running. params is optional in the
type system too, so always if (!params) return; before using it, or fall back to
the raw instruction.data and instruction.accounts.
Known limitations
[u8; 32]is always rendered as a base58 pubkey. A 32-byte hash or Merkle root will look like a pubkey string.- Account-count tolerance. Surplus accounts go to
extraAccounts; too few meansparamsisundefined. - Address lookup tables. ALT-resolved addresses are included in the instruction's account list and mapped positionally — there's no separate ALT handling to configure.
- Inline nominal types. Reference IDL
typesvia{ defined: "Name" }, or declare shapes inline with{ struct: … }/{ enum: … }. A standalone inline program-leveltypesregistry isn't available yet.
Related
- Configuration — where
idl,discriminator,args,accountslive. - Instruction Handlers — using
paramsin handlers.