I11N: Instruction Introspection made easy!

LE◎
Turbin3 Research
Published in
5 min readMay 2, 2024

This article provides a detailed overview of the newly developed i11n set of macros and helpers for Instruction Introspection, created by Turbin3. This advanced overview assumes prior knowledge of instruction introspection techniques. If you are unfamiliar with this topic, please review this other article before moving forward.

Turbin3 is the gold standard in developing premier Solana products while educating the upcoming wave of Solana developers.

What is i11n and why every developer should care about it

Let’s start with the basics. Any developer who has had to write “instruction introspection” understands the struggle of this name. In our quest to optimize and simplify the integration of introspection into your development workflow, we recognized the need for a more efficient terminology. That’s why Dean introduced the “i11n” concept (Introspection = I11N), with “11” representing the number of characters between ‘I’ and ‘N’.

Petition to call introspection: I-11Char-N == I11N.

With this context, let me tell you why you should care about it.

Solana Developers know about the complexities involved in integrating sophisticated programs, such as those from the DeFi sector, into their projects. Developers hate CPIing into complex programs but they still think that this is the best way to atomically execute composable actions in their smart contracts!

In our Devshop, we have shifted towards preferring introspection over CPI for these complex instructions. Introspection not only maintains atomicity but also leverages the usually pretty straightforward and available Typescript API to manage the heavy lifting like getting all the accounts and replicating the instructions, as demonstrated in our recent project where we integrated Jupiter.

The only tradeoff between CPI and Introspection is just that Introspection comes at a higher computational unit (CU) cost compared to traditional CPI methods.

Why Opt for I11N Over the Vanilla Sysvar Instruction Program?

I11N was born and designed primarily for internal use within our Devshop, to simplify and standardize the process of instruction introspection — a tool we frequently employ.

Let me show why this is needed.

If you are trying to access another instruction into the Transaction, as we saw in this previous writeup, you will use the Solana Native Sysvar Instruction Program that will give you an Instruction type:

pub struct Instruction {
pub program_id: Pubkey,
pub accounts: Vec<AccountMeta>,
pub data: Vec<u8>,
}

Although this method is relatively straightforward, it becomes cumbersome when dealing with complex instructions, such as a Jupiter Swap. Manually deserializing the data (passed as a Vec<u8>) and referencing account indices in an unclear manner can be quite challenging.

For example, consider this basic escrow scenario:

// Load the Instruction for introspection
let index = load_current_index_checked(&ctx.accounts.instructions.to_account_info())?;
let ix = load_instruction_at_checked(index as usize + 1, &ctx.accounts.instructions.to_account_info())?;

// The Discriminator Check
require_eq!(ix.data[0], 3u8, EscrowError::InvalidIx);

// The Data Check (Amount in this case)
require!(ix.data[1..9].eq(&escrow.take_amount.to_le_bytes()), EscrowError::InvalidAmount);

// The Account Check (Making sure that the Maker of the Escrow is the receiver)
let maker_ata = get_associated_token_address(&ctx.accounts.maker.key(), &escrow.mint_b);
require_keys_eq!(ix.accounts.get(1).unwrap().pubkey, maker_ata, EscrowError::InvalidMakerATA);

Now, observe the simplicity brought by using the Anchor Introspection SDK:

// Load the Instruction for introspection
let index = load_current_index_checked(&ctx.accounts.instructions.to_account_info())?;
let instruction = load_instruction_at_checked(index as usize + 1, &ctx.accounts.instructions.to_account_info())?;
let ix = MakeI11n::try_from(&instruction)?;

// The Data Check (Amount in this case)
require!(ix.args.receive.eq(&escrow.take_amount.to_le_bytes()), EscrowError::InvalidAmount);

// The Account Check (Making sure that the Maker of the Escrow is the receiver)
let maker_ata = get_associated_token_address(&ctx.accounts.maker.key(), &escrow.mint_b);
require_keys_eq!(ix.accounts.maker.pubkey, maker_ata, EscrowError::InvalidMakerATA);

Though it might appear similar at first glance, the use of the Anchor Introspection SDK transforms the process into one that is far more intuitive and user-friendly. It eliminates the need for manual deserialization and makes the entire process more readable and straightforward, enhancing both the efficiency and clarity of your checks.

So how do I use it?

Initially, the process of manually creating the Rust SDK, including the types necessary for the [TryFromInstruction] macro, was quite laborious. To simplify this process, Dean developed a tool that automates these tasks. So, let’s put this tool to use!

Your first step is to install the CLI from the source. You can do this by executing the following command:

cargo install - git https://github.com/deanmlittle/idlgen

Next, obtain the IDL of the program you want to introspect. You can find the IDL on Explorers like SolanaFm or Solana Explorer.

Search the program you need the IDL for, and copy it using the green-highlighted button.

After downloading, save it as a file, for instance jupiter.idl.json. Then, use the command below to generate the helper:

idlgen - filename jupiter.json - package crate

Note: idlgen is capable of creating both CPI and Introspection helpers. If you want just the Introspection helpers use the command

idlgen - filename jupiter.json - package crate --sdk i11n

Following these steps, you will have a directory containing your new SDK for i11n, ready for integration into your program.

To incorporate it into your repository, you should put the newly generated folder in the root of your anchor project and use it as an external crate to avoid deserialization issues and include the anchor-i11n repository in your project’s cargo.toml like this:

[dependencies]
...
<name-of-program>-sdk = { anchor-escrow-sdk = { path = "../../<name-of-program>" } }

This will include the [TryFromInstruction] macro, which handles the deserialization automatically.

#[derive(Accounts)]

...

#[account(address = instructions::ID)]
/// CHECK: InstructionsSysvar account
instructions: UncheckedAccount<'info>,

...

let index = load_current_index_checked(&self.instructions.to_account_info())?;
let ix = load_instruction_at_checked(index as usize - 1, &self.instructions.to_account_info())?;
let instruction = MakeI11n::try_from(&ix)?;

To make sure all the steps are clear, here’s a simplified example of how you might use it in a repository!

Congratulations! We hope that you just x100 your speed using introspection and that you found this resource helpful and insightful!

A special thanks to Dean who built this insane tool and Turbin3 Institute which is always developing cutting-edge tools on Solana.

If you want to follow my journey and you don’t want to miss out on my next tutorial, follow me on Twitter!

--

--