Solana IDL to Rust client / CPI interface generator.
solita, light of my life, fire of my loins
This software is still in its early stages of development. USE AT YOUR OWN RISK. It's a codegen CLI, so you can always read and modify the generated code if you need to.
- solores
Table of contents generated with markdown-toc
cargo install solores
to install the CLI binary.
Lets say you had the following shank generated IDL, my_token_idl.json
:
{
"name": "my_token",
"instructions": [
{
"name": "transfer",
"accounts": [
{
"name": "src",
"isMut": true,
"isSigner": true
},
{
"name": "dest",
"isMut": true,
"isSigner": false
}
],
"args": [
{
"name": "transferArgs",
"type": {
"defined": "TransferArgs"
}
}
],
"discriminant": {
"type": "u8",
"value": 0
}
}
],
"types": [
{
"name": "TransferArgs",
"type": {
"kind": "struct",
"fields": [
{
"name": "amount",
"type": "u64"
}
]
}
}
]
}
Running solores my_token_idl.json
should generate a my_token_interface
rust crate that allows you to use it in an on-chain program as so:
use my_token_interface::{TransferAccounts, TransferArgs, TransferIxArgs, transfer_invoke_signed};
use solana_program::{account_info::{AccountInfo, next_account_info}, entrypoint::ProgramResult, program::invoke, pubkey::Pubkey};
pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let src = next_account_info(account_info_iter)?;
let dest = next_account_info(account_info_iter)?;
transfer_invoke_signed(
TransferAccounts { src, dest },
TransferIxArgs {
transfer_args: TransferArgs { amount: 1_000 },
},
&[&[&[0u8]]],
)
}
or in a client-side app:
use my_token_interface::{TransferKeys, TransferArgs, transfer_ix};
pub fn do_something_with_instruction() -> std::io::Result<()> {
...
let transfer_accounts = TransferKeys {
src: some_pubkey,
dest: another_pubkey,
};
let transfer_ix_args = TransferIxArgs {
transfer_args: TransferArgs { amount: 1_000 },
};
let ix = transfer_ix(transfer_accounts, transfer_ix_args)?;
...
}
The crate will also combine all instructions into a single borsh de/serializable ProgramIx
enum
use borsh::BorshSerialize;
use my_token_interface::{MyTokenProgramIx, TransferArgs, TransferIxArgs};
#[test]
pub fn test_borsh_serde_roundtrip_program_ix() {
let program_ix = MyTokenProgramIx::Transfer(
TransferIxArgs {
transfer_args: TransferArgs { amount: 1 },
}
);
// [0, 1, 0, 0, 0, 0, 0, 0, 0]
let serialized = program_ix.try_to_vec().unwrap();
// note that deserialize is an associated function/method
// rather than the BorshDeserialize trait impl,
// i.e. MyTokenProgramIx does NOT impl BorshDeserialize
// since it doesn't follow the borsh spec
let deserialized = MyTokenProgramIx::deserialize(&serialized).unwrap();
assert_eq!(program_ix, deserialized);
}
The crate will also export the instructions' discriminant as consts, and any error types defined in the IDL as an enum convertible to/from u32.
The usage for anchor IDLs is essentially the same as Shank IDL's. Additionally, the crate will also:
- export all accounts' discriminant as consts.
- create a
*Account
newtype that includes account discriminant checking in borsh serde operations - export event struct defs
For supporting older solana programs (system, stake), solores also supports a custom bincode IDL format identified by { "metadata": { "origin": "bincode" }}
.
The instructions must be declared in enum order to work with bincode.
No account definitions are supported, since system and stake program have their account defs in solana-program
already.
serde
is added as an optional dependency behind the serde
feature-flag to the generated crate to provide Serialize
and Deserialize
implementations for the various typedefs and onchain accounts.
Do note that since it's a simple derive, Pubkey
s are de/serialized as byte arrays instead of base-58 strings.
The various *Keys
struct also impl From<[Pubkey; *_IX_ACCOUNTS_LEN]>
to support indexing
use my_token_interface::{TRANSFER_IX_ACCOUNTS_LEN, TransferKeys};
use solana_program::{pubkey::Pubkey, sysvar::instructions::{BorrowedAccountMeta, BorrowedInstruction}};
use std::convert::TryInto;
fn index_instruction(ix: BorrowedInstruction) {
let metas: [BorrowedAccountMeta<'_>; TRANSFER_IX_ACCOUNTS_LEN] = ix.accounts.try_into().unwrap();
let pubkeys = metas.map(|meta| *meta.pubkey);
let transfer_keys: TransferKeys = pubkeys.into();
// Now you can do stuff like `transfer_keys.src` instead of
// having to keep track of the various account indices
//
// ...
}
The various *Accounts
also impl From<&[AccountInfo; *_IX_ACCOUNTS_LEN]>
to make unpacking from the program accounts slice more ergonomic.
use my_token_interface::{TRANSFER_IX_ACCOUNTS_LEN, TransferAccounts, TransferArgs, TransferIxArgs, transfer_invoke};
use solana_program::{account_info::{AccountInfo, next_account_info}, entrypoint::ProgramResult, program::invoke, pubkey::Pubkey};
pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult {
let transfer_accounts: &[AccountInfo; TRANSFER_IX_ACCOUNTS_LEN] = accounts[..TRANSFER_IX_ACCOUNTS_LEN].try_into().unwrap();
let accounts: TransferAccounts = transfer_accounts.into();
transfer_invoke(
accounts,
TransferIxArgs {
transfer_args: TransferArgs { amount: 1_000 },
}
)
}
A function to compare equality between the pubkeys of a instruction *Accounts
struct with a *Keys
struct is generated:
use my_token_interface::{TransferAccounts, TransferKeys, transfer_verify_account_keys};
use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey, program_error::ProgramError};
pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult {
let accounts: TransferAccounts = ...
let expected_keys: TransferKeys = ...
// transfer_verify_account_keys() returns the first non-matching pubkeys between accounts and expected_keys
if let Err((actual_pubkey, expected_pubkey)) = transfer_verify_account_keys(accounts, expected_keys) {
return Err(ProgramError::InvalidAccountData);
}
}
This function is not generated if the instruction has no account inputs.
A function to ensure writable signer privileges of a instruction *Accounts
struct is also generated:
use my_token_interface::{TransferAccounts, TransferKeys, transfer_verify_account_privileges};
use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey, program_error::ProgramError};
pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult {
let accounts: TransferAccounts = ...
if let Err((offending_acc, program_err)) = transfer_verify_account_privileges(accounts) {
solana_program::msg!("Writable/signer privilege escalation for {}: {}", offending_acc.key, program_err);
return Err(program_err);
}
}
This function is not generated if the instruction has no privileged account inputs (only non-signer and non-writable accounts).
Pass -z <name-of-type-or-account-in-idl>
to additionally derive Pod Zeroable Copy
for the generated types. Accepts multiple options. The correctness of the derive is not checked.
The following instructions that take a program ID pubkey as argument are also exported:
*_ix_with_program_id()
*_invoke_with_program_id()
*_invoke_signed_with_program_id()
They allow the creation of Instruction
s and invoking of programs of the same interface at a different program ID.
Compared to anchor-gen, solores:
-
Has no dependency on anchor. The generated crate's dependencies are:
- borsh solana-program
- thiserror num-derive num-traits if the idl contains error enum definitions.
- bytemuck if any
-z
types are provided
-
Produces human-readable rust code in a new, separate crate instead of using a proc-macro.
-
Exposes lower-level constructs such as functions for creating the
solana_program::instruction::Instruction
struct to allow for greater customizability.
Please check the repo's issues list for more.
- Does not check correctness of zero-copy/bytemuck accounts derives
- Does not handle account namespaces
- Does not handle the state instruction namespace