Skip to content

Latest commit

 

History

History
165 lines (122 loc) · 7.16 KB

README.md

File metadata and controls

165 lines (122 loc) · 7.16 KB

Ideally

IDL-first framework-less solana programming.

What's this?

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
  • 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

Account Resolvers

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.

Free Accounts

A free account is one that cannot be computed and is input by the user

Constrained Accounts

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.

Implication

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

Example

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.