Decoding Instructions
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. HyperIndex needs to know which bytes identify an instruction and how to read the rest, and there are two ways to tell it:
- Point at an IDL. The usual path, and nothing else on this page applies.
- Declare the layout yourself. For programs with no IDL - most native ones.
Point at an IDL
Give the program an IDL JSON file (relative to config.yaml). Anchor 0.30+,
legacy Anchor, Shank and Codama IDLs all work through the same path:
programs:
- name: Jupiter
program_id: JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4
idl: ./idls/jupiter.json
That's the whole configuration. Every usable instruction the IDL declares is
indexable - names, discriminators, argument layouts, ordered account names
(including nested account groups) and the IDL's types registry all come from
the file - so you select what you index by name in
onInstruction and read
instruction.args and instruction.accounts typed after codegen.
Add an instructions row only to override or extend the catalog:
- A row whose
namethe IDL declares replaces that instruction, and must spell out bothaccountsandargs. - A row with a new name adds an instruction the IDL didn't declare.
The argument type table below describes what the IDL's types become in TypeScript. Everything else on this page is about programs without an IDL.
Programs without an IDL
For a program with no IDL - most native programs - you declare the pieces an IDL would have carried: which bytes select the instruction, what its arguments look like, and what to call its accounts.
Discriminators
The discriminator is a 0x-prefixed hex prefix of the instruction data, of any
whole number of bytes. Every instruction whose data carries the prefix is
dispatched to it.
- Format: hex only, with the
0xprefix. Base58 and decimal are not accepted (base58 is only forprogram_id). - Native / non-Anchor programs: whatever leading byte(s) the program uses (SPL Token
transferis0x03; Raydium AMM v4swapis0x09). - Anchor programs: the 8-byte Anchor sighash, which an
idlcarries for you. - The whole program:
"0x"- the empty prefix, carried by every call.
instructions:
- name: swap
discriminator: "0x09" # 1-byte native
- name: sharedAccountsRoute
discriminator: "0xc1209b3341d69c81" # 8-byte Anchor sighash
instruction.discriminator in a handler reads back the same string you wrote in
config.yaml, so a config value and a handler comparison always match.
Dispatch is by prefix, not by exclusive match, so an entry whose prefix a call
carries always receives it. A program-wide "0x" entry fires alongside a keyed
one, and two entries may deliberately share a prefix - for example the layouts
before and after a program upgrade. Each decodes with its own args, and one
whose layout rejects the data is skipped for that call.
Declaring the layout
args is the Borsh argument list in order (after the discriminator); accounts
names the positional account slots.
programs:
- name: Raydium
program_id: 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8
instructions:
- name: swap
discriminator: "0x09"
accounts: # positional: slot 0 is tokenProgram, etc.
- tokenProgram
- amm
- userSourceTokenAccount
- userDestTokenAccount
args:
- { name: amountIn, type: u64 }
- { name: minAmountOut, type: u64 }
Account slots also accept ?optional (the key is absent when the call omits the
slot or fills it with the program id) and _ (holds a position without naming
it). See account slots.
For native programs the account order is the program's canonical layout, not
something HyperIndex can verify. Check it against a real transaction. Accounts
beyond your named list still arrive, positionally, in
instruction.accountArguments.
args is also a filter
Setting args attaches a decoder, and a call whose data the layout rejects is
skipped rather than delivered undecoded. That makes the three states
meaningfully different:
args | Behavior |
|---|---|
| omitted | No decoder at all. Every matched call is indexed and the payload stays raw, reachable as instruction.data. |
[] | The instruction takes no arguments, so only calls carrying nothing past the discriminator are indexed. |
| a list | Only calls whose data decodes cleanly against the layout are indexed. |
An idl always declares the layout of the instructions it names, empty included.
Supported argument types
These are the types you can write in args, and also what HyperIndex
understands from an IDL. The right column is the TypeScript type each value has
in instruction.args after codegen.
type | Type in instruction.args |
|---|---|
bool | boolean |
u8 u16 u32, i8 i16 i32, f32 f64 | number |
u64 u128 i64 i128 | bigint |
string | string |
pubkey (alias publicKey) | string (base58) |
bytes | Uint8Array |
{ option: <type> } | the value, or null |
{ vec: <type> } | readonly T[] - but vec<u8> is a Uint8Array |
{ array: [<type>, <len>] } | a readonly N-tuple - but [u8, N] is a Uint8Array |
{ struct: [ {name,type}, … ] } | object |
{ enum: [ {name, fields?}, … ] } | a variant name as a string literal, or { VariantName: { …fields } } for a variant with fields |
args:
- { name: amount, type: u64 } # bigint
- { name: authority, type: pubkey } # base58 string
- { name: maybeOwner, type: { option: pubkey } } # string | null
- { name: seedHash, type: { array: [u8, 32] } } # Uint8Array
- { name: side, type: { enum: [{ name: Bid }, { name: Ask }] } } # "Bid" | "Ask"
Nominal types are declared inline at the field that uses them. There's no way to
name one in YAML and refer to it elsewhere - attach an idl to the program when
its types are shared between instructions.
Two things worth knowing about the decoded output:
- Byte arrays are
Uint8Array.bytes,vec<u8>and[u8; N]all decode toUint8Array, so a 32-byte hash doesn't come back as a base58 string. - Address lookup tables need no configuration. ALT-resolved addresses arrive in the instruction's account list and are mapped positionally like any other.
Related
- Configuration: where
idl,discriminator,argsandaccountslive. - Instruction Handlers: using
argsandaccountsin handlers.