Development of this library has stopped and will likely not continue.
IDLs are the devil's work, and I will not contribute to their further proliferation.
Rust is all you need. You can write a minimal no-std
base sdk crate that is portable across onchain (both CPI and for the development of the program itself) and offchain (rust clients, js via wasm, python via pyo3) environments. Use the same codebase for everything instead of trying to sync changes across different languages using a lossy json format. Here's an incomplete example with only CPI integrations.
You can just do this instead of spending your time learning about how each of codama's "over 60 different types of (AST) nodes" work and debugging generated code.
The only major disadvantage of this approach is possibly large wasm binary sizes from including cryptographic operations not supported by the web Crypto interface, with find_pda
being the main culprit. But you can also structure it such that that part is delegated to javascript users.
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