> For the complete documentation index, see [llms.txt](https://docs.envio.dev/llms.txt).

# Decoding Instructions

Solana instruction data is a packed [Borsh](https://borsh.io/) 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 {#idls}

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:

```yaml
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`](/docs/HyperIndex/solana/instruction-handlers) and read
`instruction.args` and `instruction.accounts` typed after codegen.

Names are used exactly as the IDL spells them, with no case conversion, so
check the file before you write a handler (an Anchor 0.30+ IDL is usually
snake_case, a legacy one camelCase). Accounts inside a nested group are
flattened and prefixed with the group name: `owner` inside the group
`depositAccounts` becomes `depositAccountsOwner`.

An instruction the IDL declares but HyperIndex can't decode or dispatch (it
references an undefined or recursive type, or has exactly the same
discriminator bytes as another instruction) is left out of the catalog, and
codegen prints the reason:

```text
Warning: program '<program>' will not index '<instruction>' from the IDL: <reason>
```

A discriminator that is only a prefix of another one is not a collision. Both
instructions stay in the catalog, and a call goes to every selected instruction
whose discriminator its data starts with (see [Discriminators](#discriminators)).

Add an `instructions` row only to override or extend the catalog:

- A row whose `name` the IDL declares **replaces** that instruction, and must spell out both `accounts` and `args`. This is also how you index an instruction codegen left out.
- A row with a new name **adds** an instruction the IDL didn't declare.

The [argument type table](#supported-argument-types) 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 {#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 `0x` prefix. Base58 and decimal are not accepted (base58 is only for `program_id`).
- **Native / non-Anchor programs:** whatever leading byte(s) the program uses (SPL Token `transfer` is `0x03`; Raydium AMM v4 `swap` is `0x09`).
- **Anchor programs:** the 8-byte Anchor sighash, which an `idl` carries for you.
- **The whole program:** `"0x"` - the empty prefix, carried by every call.

```yaml
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.

:::note Overlapping prefixes all fire
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 {#inline-layout-no-idl}

`args` is the Borsh argument list in order (after the discriminator); `accounts`
names the positional account slots.

```yaml
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](/docs/HyperIndex/solana/configuration#account-slots).

:::warning Verify positional layouts
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 {#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`. Selecting `"args"` in the handler's `fields.instruction` then throws at registration, since there is nothing to decode. |
| `[]` | 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. |

The layout rejects data that is too short, has **bytes left over** after the
last argument, carries an invalid `bool`, `option` or `enum` tag, holds invalid
UTF-8 in a `string`, or decodes to a NaN or infinite float. Bytes left over
count, so declare every argument the instruction takes: a layout that stops
early skips every call, with no error.

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 (an IDL tuple variant's fields are named `_0`, `_1`, …) |

```yaml
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 to `Uint8Array`, 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](/docs/HyperIndex/solana/configuration): where `idl`, `discriminator`, `args` and `accounts` live.
- [Instruction Handlers](/docs/HyperIndex/solana/instruction-handlers): using `args` and `accounts` in handlers.
