IDL-first framework-less solana programming.
Unlike Anchor, which provides a full library and DSL, Ideally is an approach to writing solana programs with the following characteristics:
- IDL-first development
- Using codegen to fully leverage the IDL definitions
- Maximal shared types and procedures between the on-chain program and off-chain clients
To achieve this, Ideally provides no mandatory runtime dependencies or libraries. Instead, the development procedure is to:
- First handwrite the program IDL
- Use solores to generate the interface crate
- Develop a library that uses the types and functions in the generated interface crate to implement common procedures that can be used by both the on-chain program and off-chain clients
- main content will likely be account resolvers for the various program instructions
- Develop the onchain program using both the interface crate and library
- the interface crate will provide the error type implementations, and accounts + instructions definitions and borsh serde that can simply be plugged in
As such, Ideally solana programs typically comprise 3 crates:
- interface
- library
- on-chain program
Name and concept stolen from Noah
The fact that the solana runtime requires users to explicitly pass in the accounts involved to the programs is one of the biggest sources of glass for devs:
- on the client side, consumers must know and compute which accounts to use for a given program instruction invocation
- in the on-chain program, the program must rigorously verify all accounts are indeed the ones expected
However, most of the time, these are really 2 sides of the same problem that can be solved with the same code.
To see how, let us introduce the concept of "free accounts" and "constrained accounts". In general, (sub)sets of account inputs to a program instruction can be split into these 2 mutually exclusive sets.
A free account is one that cannot be computed and is input by the user
Constrained accounts are accounts that must be computed from the free accounts' data and other arbitrary supporting data. This supporting data is usually the input instruction data and the account data of the free accounts.
For each instruction, we can define a struct that starts with only the free accounts. These structs will then define the method to compute the constrained accounts in order to resolve themselves into the next resulting struct. This resolution procedure may be multi-step, but the end struct should include the *Keys
struct generated by solores
. This is the full list of pubkeys of accounts to pass to the program instruction.
Off-chain clients can use this resolution procedure to require minimal account inputs from their users.
On-chain program uses this same resolution procedure to verify accounts:
- create the full
*Accounts
struct from the AccountInfo slice - create the starting free-accounts-only struct from a subslice of the AccountInfo slice
- complete this resolution procedure to end up with
*Keys
- compare
*Keys
against*Accounts
to make sure the pubkeys match
We'll use the create associated token account (ATA) instruction of the ATA program as an example. This instruction has:
Free accounts:
- Funding account
- Wallet address for the new associated token account
- The token mint for the new associated token account
Constrained accounts:
- SPL token program. This is the token mint's owner program.
- Associated token account address to be created. This is computed with PDA([wallet_address, token_program_id, token_mint_address], ATA program ID)
- System program. Constant, well-known accounts are constrained accounts.
Assuming code generated by solores 0.2.2 in the interface crate, we can do this in the library crate:
use solana_readonly_account::{KeyedAccount, ReadonlyAccountOwner};
pub struct CreateIxFreeAccounts<M: KeyedAccount + ReadonlyAccountOwner> {
pub funding_account: Pubkey,
pub wallet: Pubkey,
pub mint: M,
}
impl CreateIxFreeAccounts {
/// Returned u8 is the found bump seed for the ata
///
/// For more complex examples, you can pass supporting data
/// to this fn, and return more complex types. But `*Keys`
/// should be returned as the last step
pub fn resolve(&self) -> (CreateKeys, u8) {
let mint = *self.mint.key();
let token_program = *self.mint.owner();
let (ata, bump) = Pubkey::find_program_address(
&[
&self.wallet_address.to_bytes(),
&token_program.to_bytes(),
&mint.to_bytes(),
],
program_id,
);
(
CreateKeys {
funding_account: *self.funding_account,
wallet_address: *self.wallet_address,
mint,
token_program,
system_program: system_program::ID,
associated_token_account: ata,
},
bump
)
}
}
Off-chain clients can now do something like
// Now users only need to provide funding_account, wallet_address, token_mint, token_program, instead of all 6 addresses
let user_args: CreateIxFreeAccounts = ...; // assume computed from somewhere e.g. deserialized from CLI args
let keys: CreateKeys = user_args.resolve().0;
let ix = create_ix(&keys, CreateIxArgs {}).unwrap();
// ready to put ix into a Transaction and send it
Whereas the onchain instruction processor can do something like
pub fn process_create_associated_token_account(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult {
let funding_account = accounts[0];
let wallet = accounts[2];
let mint = accounts[3];
let free_accs = CreateIxFreeAccounts {
funding_account: *funding_account.pubkey,
wallet_address: *wallet.pubkey,
mint,
};
let (expected_keys, ata_bump) = free_accs.resolve();
let actual_accounts_slice: &[AccountInfo; CREATE_IX_ACCOUNTS_LEN] = accounts.try_into().unwrap();
let actual_accounts: CreateAccounts = actual_accounts_slice.into();
// program checks the identity of
// token_program, system_program, and associated_token_account
// with 1 function call
if let Err((actual_pubkey, expected_pubkey)) = create_verify_account_keys(&actual_accounts, &expected_keys) {
return Err(ProgramError::InvalidAccountData);
}
// this function generated by solores checks all writable and signer privileges
create_verify_account_privileges(&actual_accounts)?;
// other security checks and rest of program follows
// ...
//
}
For the full example, the ATA program is partially reimplemented as an Ideally program in examples/associated-token-account
.