From 772edde073afef240daa7263e1747db872517ae7 Mon Sep 17 00:00:00 2001 From: Armani Ferrante Date: Fri, 29 Jan 2021 06:19:00 -0800 Subject: [PATCH] Store IDL at deterministic account address (#49) --- Cargo.lock | 3 + cli/Cargo.toml | 7 +- cli/src/config.rs | 2 +- cli/src/main.rs | 282 +++++++++++++++++++++++++++++++----- cli/src/template.rs | 2 + src/account_info.rs | 27 +++- src/ctor.rs | 14 ++ src/idl.rs | 73 ++++++++++ src/lib.rs | 1 + syn/src/codegen/accounts.rs | 20 +-- syn/src/codegen/program.rs | 140 ++++++++++++++++++ syn/src/idl.rs | 28 ++-- ts/package.json | 4 +- ts/src/idl.ts | 35 +++++ ts/src/program.ts | 32 +++- ts/yarn.lock | 10 ++ 16 files changed, 613 insertions(+), 67 deletions(-) create mode 100644 src/idl.rs diff --git a/Cargo.lock b/Cargo.lock index d5e2145c..c30662cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -101,10 +101,12 @@ dependencies = [ name = "anchor-cli" version = "0.1.0" dependencies = [ + "anchor-lang", "anchor-syn", "anyhow", "clap 3.0.0-beta.2", "dirs", + "flate2", "heck", "serde", "serde_json", @@ -112,6 +114,7 @@ dependencies = [ "serum-common", "shellexpand", "solana-client", + "solana-program", "solana-sdk", "syn 1.0.57", "toml", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 6e4ce177..2fb0c9d7 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -8,12 +8,11 @@ edition = "2018" name = "anchor" path = "src/main.rs" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] clap = "3.0.0-beta.1" anyhow = "1.0.32" syn = { version = "1.0.54", features = ["full", "extra-traits"] } +anchor-lang = { path = "../" } anchor-syn = { path = "../syn", features = ["idl"] } serde_json = "1.0" shellexpand = "2.1.0" @@ -21,7 +20,9 @@ serde_yaml = "0.8" toml = "0.5.8" serde = { version = "1.0", features = ["derive"] } solana-sdk = "1.5.0" +solana-program = "1.5.0" solana-client = "1.4.4" serum-common = { git = "https://github.com/project-serum/serum-dex", features = ["client"] } dirs = "3.0" -heck = "0.3.1" \ No newline at end of file +heck = "0.3.1" +flate2 = "1.0.19" diff --git a/cli/src/config.rs b/cli/src/config.rs index 1e384ed1..6bef16c3 100644 --- a/cli/src/config.rs +++ b/cli/src/config.rs @@ -148,7 +148,7 @@ pub fn extract_lib_name(path: impl AsRef) -> Result { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Program { pub lib_name: String, pub path: PathBuf, diff --git a/cli/src/main.rs b/cli/src/main.rs index a5d43a20..02b07648 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,10 +1,20 @@ use crate::config::{find_cargo_toml, read_all_programs, Config, Program}; +use anchor_lang::idl::IdlAccount; +use anchor_lang::{AnchorDeserialize, AnchorSerialize}; use anchor_syn::idl::Idl; -use anyhow::Result; +use anyhow::{anyhow, Result}; use clap::Clap; +use flate2::read::ZlibDecoder; +use flate2::write::ZlibEncoder; +use flate2::Compression; use serde::{Deserialize, Serialize}; use solana_client::rpc_client::RpcClient; +use solana_client::rpc_config::RpcSendTransactionConfig; +use solana_program::instruction::{AccountMeta, Instruction}; +use solana_sdk::commitment_config::CommitmentConfig; use solana_sdk::pubkey::Pubkey; +use solana_sdk::signature::Signer; +use solana_sdk::transaction::Transaction; use std::fs::{self, File}; use std::io::prelude::*; use std::path::{Path, PathBuf}; @@ -34,8 +44,39 @@ pub enum Command { Test, /// Creates a new program. New { name: String }, - /// Outputs an interface definition file. + /// Commands for interact with interface definitions. Idl { + #[clap(subcommand)] + subcmd: IdlCommand, + }, + /// Deploys the workspace, creates IDL accounts, and runs the migration + /// script. + Deploy { + #[clap(short, long)] + url: Option, + #[clap(short, long)] + keypair: Option, + }, + /// Runs the deploy migration script. + Migrate { + #[clap(short, long)] + url: String, + }, + /// Not yet implemented. Please use `solana program deploy` command to + /// upgrade your program. + Upgrade {}, +} + +#[derive(Debug, Clap)] +pub enum IdlCommand { + /// Initializes a program's IDL account. Can only be run once. + Init { + program_id: Pubkey, + #[clap(short, long)] + filepath: String, + }, + /// Parses an IDL from source. + Parse { /// Path to the program's interface definition. #[clap(short, long)] file: String, @@ -43,16 +84,13 @@ pub enum Command { #[clap(short, long)] out: Option, }, - /// Deploys the workspace to the configured cluster. - Deploy { + /// Fetches an IDL for the given program from a cluster. + Fetch { + program_id: Pubkey, + /// Output file for the idl (stdout if not specified). #[clap(short, long)] - url: Option, - #[clap(short, long)] - keypair: Option, + out: Option, }, - /// Not yet implemented. Please use `solana program deploy` command to - /// upgrade your program. - Upgrade {}, } fn main() -> Result<()> { @@ -63,17 +101,13 @@ fn main() -> Result<()> { Command::Build { idl } => build(idl), Command::Test => test(), Command::New { name } => new(name), - Command::Idl { file, out } => { - if out.is_none() { - return idl(file, None); - } - idl(file, Some(&PathBuf::from(out.unwrap()))) - } + Command::Idl { subcmd } => idl(subcmd), Command::Deploy { url, keypair } => deploy(url, keypair), Command::Upgrade {} => { println!("This command is not yet implemented. Please use `solana program deploy`."); Ok(()) } + Command::Migrate { url } => migrate(&url), } } @@ -176,7 +210,7 @@ fn build_ws( Some(idl) => Some(PathBuf::from(idl)), None => { let cfg_parent = match cfg_path.parent() { - None => return Err(anyhow::anyhow!("Invalid Anchor.toml")), + None => return Err(anyhow!("Invalid Anchor.toml")), Some(parent) => parent, }; fs::create_dir_all(cfg_parent.join("target/idl"))?; @@ -191,10 +225,7 @@ fn build_ws( fn build_all(_cfg: Config, cfg_path: PathBuf, idl_out: Option) -> Result<()> { match cfg_path.parent() { - None => Err(anyhow::anyhow!( - "Invalid Anchor.toml at {}", - cfg_path.display() - )), + None => Err(anyhow!("Invalid Anchor.toml at {}", cfg_path.display())), Some(parent) => { let files = fs::read_dir(parent.join("programs"))?; for f in files { @@ -219,7 +250,7 @@ fn build_cwd(idl_out: Option) -> Result<()> { // Runs the build command outside of a workspace. fn _build_cwd(cargo_toml: PathBuf, idl_out: Option) -> Result<()> { match cargo_toml.parent() { - None => return Err(anyhow::anyhow!("Unable to find parent")), + None => return Err(anyhow!("Unable to find parent")), Some(p) => std::env::set_current_dir(&p)?, }; @@ -241,12 +272,29 @@ fn _build_cwd(cargo_toml: PathBuf, idl_out: Option) -> Result<()> { Some(o) => PathBuf::from(&o.join(&idl.name).with_extension("json")), }; - write_idl(&idl, Some(&out)) + write_idl(&idl, OutFile::File(out)) } -fn idl(file: String, out: Option<&Path>) -> Result<()> { - let idl = extract_idl(&file)?; - write_idl(&idl, out) +// Fetches an IDL for the given program_id. +fn fetch_idl(program_id: Pubkey) -> Result { + let cfg = Config::discover()?.expect("Inside a workspace").0; + let client = RpcClient::new(cfg.cluster.url().to_string()); + + let idl_addr = IdlAccount::address(&program_id); + + let account = client + .get_account_with_commitment(&idl_addr, CommitmentConfig::recent())? + .value + .map_or(Err(anyhow!("Account not found")), Ok)?; + + // Cut off account discriminator. + let mut d: &[u8] = &account.data[8..]; + let idl_account: IdlAccount = AnchorDeserialize::deserialize(&mut d)?; + + let mut z = ZlibDecoder::new(&idl_account.data[..]); + let mut s = Vec::new(); + z.read_to_end(&mut s)?; + serde_json::from_slice(&s[..]).map_err(Into::into) } fn extract_idl(file: &str) -> Result { @@ -254,15 +302,61 @@ fn extract_idl(file: &str) -> Result { anchor_syn::parser::file::parse(&*file) } -fn write_idl(idl: &Idl, out: Option<&Path>) -> Result<()> { +fn idl(subcmd: IdlCommand) -> Result<()> { + match subcmd { + IdlCommand::Init { + program_id, + filepath, + } => idl_init(program_id, filepath), + IdlCommand::Parse { file, out } => idl_parse(file, out), + IdlCommand::Fetch { program_id, out } => idl_fetch(program_id, out), + } +} + +fn idl_init(program_id: Pubkey, idl_filepath: String) -> Result<()> { + let cfg = Config::discover()?.expect("Inside a workspace").0; + let keypair = cfg.wallet.to_string(); + + let bytes = std::fs::read(idl_filepath)?; + let idl: Idl = serde_json::from_reader(&*bytes)?; + let idl_address = create_idl_account(&cfg, &keypair, &program_id, &idl)?; + + println!("Idl account created: {:?}", idl_address); + Ok(()) +} + +fn idl_parse(file: String, out: Option) -> Result<()> { + let idl = extract_idl(&file)?; + let out = match out { + None => OutFile::Stdout, + Some(out) => OutFile::File(PathBuf::from(out)), + }; + write_idl(&idl, out) +} + +fn idl_fetch(program_id: Pubkey, out: Option) -> Result<()> { + let idl = fetch_idl(program_id)?; + let out = match out { + None => OutFile::Stdout, + Some(out) => OutFile::File(PathBuf::from(out)), + }; + write_idl(&idl, out) +} + +fn write_idl(idl: &Idl, out: OutFile) -> Result<()> { let idl_json = serde_json::to_string_pretty(idl)?; - match out.as_ref() { - None => println!("{}", idl_json), - Some(out) => std::fs::write(out, idl_json)?, + match out { + OutFile::Stdout => println!("{}", idl_json), + OutFile::File(out) => std::fs::write(out, idl_json)?, }; Ok(()) } +enum OutFile { + Stdout, + File(PathBuf), +} + // Builds, deploys, and tests all workspace programs in a single command. fn test() -> Result<()> { // Switch directories to top level workspace. @@ -297,7 +391,7 @@ fn test() -> Result<()> { let idl_out = PathBuf::from("target/idl") .join(&idl.name) .with_extension("json"); - write_idl(&idl, Some(&idl_out))?; + write_idl(&idl, OutFile::File(idl_out))?; } // Run the tests. @@ -386,6 +480,10 @@ fn deploy(url: Option, keypair: Option) -> Result<()> { // Add metadata to all IDLs. for (program, address) in deployment { + // Store the IDL on chain. + let idl_address = create_idl_account(&cfg, &keypair, &address, &program.idl)?; + println!("IDL account created: {}", idl_address.to_string()); + // Add metadata to the IDL. let mut idl = program.idl; idl.metadata = Some(serde_json::to_value(IdlTestMetadata { @@ -396,18 +494,130 @@ fn deploy(url: Option, keypair: Option) -> Result<()> { let idl_out = PathBuf::from("target/idl") .join(&idl.name) .with_extension("json"); - write_idl(&idl, Some(&idl_out))?; + write_idl(&idl, OutFile::File(idl_out))?; println!("Deployed {} at {}", idl.name, address.to_string()); } - run_hosted_deploy(&url)?; + // Run migration script. + migrate(&url)?; Ok(()) } -fn run_hosted_deploy(url: &str) -> Result<()> { - println!("Running deploy script"); +fn create_idl_account( + cfg: &Config, + keypair_path: &str, + program_id: &Pubkey, + idl: &Idl, +) -> Result { + // Misc. + let idl_address = IdlAccount::address(program_id); + let keypair = solana_sdk::signature::read_keypair_file(keypair_path) + .map_err(|_| anyhow!("Unable to read keypair file"))?; + let client = RpcClient::new(cfg.cluster.url().to_string()); + + // Serialize and compress the idl. + let idl_data = { + let json_bytes = serde_json::to_vec(idl)?; + let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); + e.write_all(&json_bytes)?; + e.finish()? + }; + + // Run `Create instruction. + { + let data = serialize_idl_ix(anchor_lang::idl::IdlInstruction::Create { + data_len: (idl_data.len() as u64) * 2, // Double for future growth. + })?; + let program_signer = Pubkey::find_program_address(&[], program_id).0; + let accounts = vec![ + AccountMeta::new_readonly(keypair.pubkey(), true), + AccountMeta::new(idl_address, false), + AccountMeta::new_readonly(program_signer, false), + AccountMeta::new_readonly(solana_program::system_program::ID, false), + AccountMeta::new_readonly(*program_id, false), + AccountMeta::new_readonly(solana_program::sysvar::rent::ID, false), + ]; + let ix = Instruction { + program_id: *program_id, + accounts, + data, + }; + let (recent_hash, _fee_calc) = client.get_recent_blockhash()?; + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&keypair.pubkey()), + &[&keypair], + recent_hash, + ); + client.send_and_confirm_transaction_with_spinner_and_config( + &tx, + CommitmentConfig::single(), + RpcSendTransactionConfig { + skip_preflight: true, + ..RpcSendTransactionConfig::default() + }, + )?; + } + + // Write the idl to the account buffer, chopping up the IDL into pieces + // and sending multiple transactions in the event the IDL doesn't fit into + // a single transaction. + { + const MAX_WRITE_SIZE: usize = 1000; + let mut offset = 0; + while offset < idl_data.len() { + // Instruction data. + let data = { + let start = offset; + let end = std::cmp::min(offset + MAX_WRITE_SIZE, idl_data.len()); + serialize_idl_ix(anchor_lang::idl::IdlInstruction::Write { + data: idl_data[start..end].to_vec(), + })? + }; + // Instruction accounts. + let accounts = vec![ + AccountMeta::new(idl_address, false), + AccountMeta::new_readonly(keypair.pubkey(), true), + ]; + // Instruction. + let ix = Instruction { + program_id: *program_id, + accounts, + data, + }; + // Send transaction. + let (recent_hash, _fee_calc) = client.get_recent_blockhash()?; + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&keypair.pubkey()), + &[&keypair], + recent_hash, + ); + client.send_and_confirm_transaction_with_spinner_and_config( + &tx, + CommitmentConfig::single(), + RpcSendTransactionConfig { + skip_preflight: true, + ..RpcSendTransactionConfig::default() + }, + )?; + offset += MAX_WRITE_SIZE; + } + } + + Ok(idl_address) +} + +fn serialize_idl_ix(ix_inner: anchor_lang::idl::IdlInstruction) -> Result> { + let mut data = anchor_lang::idl::IDL_IX_TAG.to_le_bytes().to_vec(); + data.append(&mut ix_inner.try_to_vec()?); + Ok(data) +} + +fn migrate(url: &str) -> Result<()> { + println!("Running migration deploy script"); let cur_dir = std::env::current_dir()?; let module_path = format!("{}/migrations/deploy.js", cur_dir.display()); @@ -463,7 +673,7 @@ fn deploy_ws(url: &str, keypair: &str) -> Result> { } #[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] +#[serde(rename_all = "PascalCase")] pub struct DeployStdout { program_id: String, } diff --git a/cli/src/template.rs b/cli/src/template.rs index 4a58f19d..bc27e317 100644 --- a/cli/src/template.rs +++ b/cli/src/template.rs @@ -24,7 +24,9 @@ name = "{1}" [features] no-entrypoint = [] +no-idl = [] cpi = ["no-entrypoint"] +default = [] [dependencies] anchor-lang = {{ git = "https://github.com/project-serum/anchor", features = ["derive"] }} diff --git a/src/account_info.rs b/src/account_info.rs index c8f8ff08..5bba8b40 100644 --- a/src/account_info.rs +++ b/src/account_info.rs @@ -1,4 +1,4 @@ -use crate::{Accounts, AccountsExit, ToAccountInfo, ToAccountInfos, ToAccountMetas}; +use crate::{Accounts, AccountsExit, AccountsInit, ToAccountInfo, ToAccountInfos, ToAccountMetas}; use solana_program::account_info::AccountInfo; use solana_program::entrypoint::ProgramResult; use solana_program::instruction::AccountMeta; @@ -19,6 +19,31 @@ impl<'info> Accounts<'info> for AccountInfo<'info> { } } +impl<'info> AccountsInit<'info> for AccountInfo<'info> { + fn try_accounts_init( + _program_id: &Pubkey, + accounts: &mut &[AccountInfo<'info>], + ) -> Result { + if accounts.len() == 0 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + let account = &accounts[0]; + *accounts = &accounts[1..]; + + // The discriminator should be zero, since we're initializing. + let data: &[u8] = &account.try_borrow_data()?; + let mut disc_bytes = [0u8; 8]; + disc_bytes.copy_from_slice(&data[..8]); + let discriminator = u64::from_le_bytes(disc_bytes); + if discriminator != 0 { + return Err(ProgramError::InvalidAccountData); + } + + Ok(account.clone()) + } +} + impl<'info> ToAccountMetas for AccountInfo<'info> { fn to_account_metas(&self, is_signer: Option) -> Vec { let is_signer = is_signer.unwrap_or(self.is_signer); diff --git a/src/ctor.rs b/src/ctor.rs index f55e790f..a43ef929 100644 --- a/src/ctor.rs +++ b/src/ctor.rs @@ -5,13 +5,27 @@ use solana_program::sysvar::rent::Rent; // Needed for the `Accounts` macro. use crate as anchor_lang; +// The Ctor accounts that can be used to create any account within the program +// itself (instead of creating the account on the client). +// +// This is used to create accounts at deterministic addresses, as a function of +// nothing but a program ID--for example, to create state global program +// structs and program IDL accounts. #[derive(Accounts)] pub struct Ctor<'info> { + // Payer of the transaction. pub from: AccountInfo<'info>, + // The deterministically defined "state" account being created via + // `create_account_with_seed`. #[account(mut)] pub to: AccountInfo<'info>, + // The program-derived-address signing off on the account creation. + // Seeds = &[] + bump seed. pub base: AccountInfo<'info>, + // The system program. pub system_program: AccountInfo<'info>, + // The program whose state is being constructed. pub program: AccountInfo<'info>, + // Rent sysvar. pub rent: Sysvar<'info, Rent>, } diff --git a/src/idl.rs b/src/idl.rs new file mode 100644 index 00000000..407e545e --- /dev/null +++ b/src/idl.rs @@ -0,0 +1,73 @@ +//! idl.rs defines the instructions and account state used to store a +//! program's IDL. +//! +//! Note that the transaction to store the IDL can be larger than the max +//! transaction size. As a reuslt, the transaction must be broken up into +//! several pieces and stored into the IDL account with multiple transactions +//! via the `Write` instruction to continuously append to the account's IDL data +//! buffer. +//! +//! To upgrade the IDL, first invoke the `Clear` instruction to reset the data. +//! And invoke `Write` once more. To eliminate the ability to change the IDL, +//! set the authority to a key for which you can't sign, e.g., the zero address +//! or the system program ID, or compile the program with the "no-idl" feature +//! and upgrade the program with the upgradeable BPF loader. + +use crate::prelude::*; +use solana_program::pubkey::Pubkey; + +// Needed for the `Accounts` macro. +use crate as anchor_lang; + +// The first 8 bytes of an instruction to create or modify the IDL account. This +// instruction is defined outside the main program's instruction enum, so that +// the enum variant tags can align with function source order. +// +// Sha256(anchor:idl)[..8]; +pub const IDL_IX_TAG: u64 = 0x0a69e9a778bcf440; + +#[derive(AnchorSerialize, AnchorDeserialize)] +pub enum IdlInstruction { + // One time initializer for creating the program's idl account. + Create { data_len: u64 }, + // Appends to the end of the idl account data. + Write { data: Vec }, + // Clear's the IdlInstruction data. Used to update the IDL. + Clear, + // Sets a new authority on the IdlAccount. + SetAuthority { new_authority: Pubkey }, +} + +// Accounts for the Create instuction. +pub type IdlCreateAccounts<'info> = crate::ctor::Ctor<'info>; + +// Accounts for Idl instructions. +#[derive(Accounts)] +pub struct IdlAccounts<'info> { + #[account(mut, has_one = authority)] + pub idl: ProgramAccount<'info, IdlAccount>, + #[account(signer)] + pub authority: AccountInfo<'info>, +} + +// The account holding a program's IDL. This is stored on chain so that clients +// can fetch it and generate a client with nothing but a program's ID. +#[account] +#[derive(Debug)] +pub struct IdlAccount { + // Address that can modify the IDL. + pub authority: Pubkey, + // Compressed idl bytes. + pub data: Vec, +} + +impl IdlAccount { + pub fn address(program_id: &Pubkey) -> Pubkey { + let program_signer = Pubkey::find_program_address(&[], program_id).0; + Pubkey::create_with_seed(&program_signer, IdlAccount::seed(), program_id) + .expect("Seed is always valid") + } + pub fn seed() -> &'static str { + "anchor:idl" + } +} diff --git a/src/lib.rs b/src/lib.rs index a7486ba1..eb50ff0e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,6 +33,7 @@ mod context; mod cpi_account; mod ctor; mod error; +pub mod idl; mod program_account; mod state; mod sysvar; diff --git a/syn/src/codegen/accounts.rs b/syn/src/codegen/accounts.rs index 551a53a5..aee9e858 100644 --- a/syn/src/codegen/accounts.rs +++ b/syn/src/codegen/accounts.rs @@ -15,17 +15,17 @@ pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream { let name = &s.ident; let ty = &s.raw_field.ty; quote! { - let #name: #ty = Accounts::try_accounts(program_id, accounts)?; + let #name: #ty = anchor_lang::Accounts::try_accounts(program_id, accounts)?; } } AccountField::Field(f) => { let name = f.typed_ident(); match f.is_init { false => quote! { - let #name = Accounts::try_accounts(program_id, accounts)?; + let #name = anchor_lang::Accounts::try_accounts(program_id, accounts)?; }, true => quote! { - let #name = AccountsInit::try_accounts_init(program_id, accounts)?; + let #name = anchor_lang::AccountsInit::try_accounts_init(program_id, accounts)?; }, } } @@ -217,7 +217,7 @@ pub fn generate_constraint_belongs_to( // todo: would be nice if target could be an account info object. quote! { if &#ident.#target != #target.to_account_info().key { - return Err(ProgramError::Custom(1)); // todo: error codes + return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(1)); // todo: error codes } } } @@ -237,7 +237,7 @@ pub fn generate_constraint_signer(f: &Field, _c: &ConstraintSigner) -> proc_macr // This check will be performed on the other end of the invocation. if cfg!(not(feature = "cpi")) { if !#info.is_signer { - return Err(ProgramError::MissingRequiredSignature); + return Err(anchor_lang::solana_program::program_error::ProgramError::MissingRequiredSignature); } } } @@ -247,7 +247,7 @@ pub fn generate_constraint_literal(c: &ConstraintLiteral) -> proc_macro2::TokenS let tokens = &c.tokens; quote! { if !(#tokens) { - return Err(ProgramError::Custom(1)); // todo: error codes + return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(1)); // todo: error codes } } } @@ -263,7 +263,7 @@ pub fn generate_constraint_owner(f: &Field, c: &ConstraintOwner) -> proc_macro2: ConstraintOwner::Skip => quote! {}, ConstraintOwner::Program => quote! { if #info.owner != program_id { - return Err(ProgramError::Custom(1)); // todo: error codes + return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(1)); // todo: error codes } }, } @@ -283,7 +283,7 @@ pub fn generate_constraint_rent_exempt( ConstraintRentExempt::Skip => quote! {}, ConstraintRentExempt::Enforce => quote! { if !rent.is_exempt(#info.lamports(), #info.try_data_len()?) { - return Err(ProgramError::Custom(2)); // todo: error codes + return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(2)); // todo: error codes } }, } @@ -296,9 +296,9 @@ pub fn generate_constraint_seeds(f: &Field, c: &ConstraintSeeds) -> proc_macro2: let program_signer = Pubkey::create_program_address( &#seeds, program_id, - ).map_err(|_| ProgramError::Custom(1))?; // todo + ).map_err(|_| anchor_lang::solana_program::program_error::ProgramError::Custom(1))?; // todo if #name.to_account_info().key != &program_signer { - return Err(ProgramError::Custom(1)); // todo + return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(1)); // todo } } } diff --git a/syn/src/codegen/program.rs b/syn/src/codegen/program.rs index 900b32d0..8e3dd13f 100644 --- a/syn/src/codegen/program.rs +++ b/syn/src/codegen/program.rs @@ -21,6 +21,13 @@ pub fn generate(program: Program) -> proc_macro2::TokenStream { anchor_lang::solana_program::entrypoint!(entry); #[cfg(not(feature = "no-entrypoint"))] fn entry(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult { + if cfg!(not(feature = "no-idl")) { + if instruction_data.len() >= 8 { + if anchor_lang::idl::IDL_IX_TAG.to_le_bytes() == instruction_data[..8] { + return __private::__idl(program_id, accounts, &instruction_data[8..]); + } + } + } let mut data: &[u8] = instruction_data; let ix = __private::instruction::#instruction_name::deserialize(&mut data) .map_err(|_| ProgramError::Custom(1))?; // todo: error code @@ -114,6 +121,136 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream { // so. pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStream { let program_name = &program.name; + let non_inlined_idl: proc_macro2::TokenStream = { + quote! { + // Entry for all IDL related instructions. Use the "no-idl" feature + // to eliminate this code, for example, if one wants to make the + // IDL no longer mutable or if one doesn't want to store the IDL + // on chain. + #[inline(never)] + #[cfg(not(feature = "no-idl"))] + pub fn __idl(program_id: &Pubkey, accounts: &[AccountInfo], idl_ix_data: &[u8]) -> ProgramResult { + let mut accounts = accounts; + let mut data: &[u8] = idl_ix_data; + + let ix = anchor_lang::idl::IdlInstruction::deserialize(&mut data) + .map_err(|_| ProgramError::Custom(1))?; // todo + + match ix { + anchor_lang::idl::IdlInstruction::Create { data_len } => { + let mut accounts = anchor_lang::idl::IdlCreateAccounts::try_accounts(program_id, &mut accounts)?; + __idl_create_account(program_id, &mut accounts, data_len)?; + accounts.exit(program_id)?; + }, + anchor_lang::idl::IdlInstruction::Write { data } => { + let mut accounts = anchor_lang::idl::IdlAccounts::try_accounts(program_id, &mut accounts)?; + __idl_write(program_id, &mut accounts, data)?; + accounts.exit(program_id)?; + }, + anchor_lang::idl::IdlInstruction::Clear => { + let mut accounts = anchor_lang::idl::IdlAccounts::try_accounts(program_id, &mut accounts)?; + __idl_clear(program_id, &mut accounts)?; + accounts.exit(program_id)?; + }, + anchor_lang::idl::IdlInstruction::SetAuthority { new_authority } => { + let mut accounts = anchor_lang::idl::IdlAccounts::try_accounts(program_id, &mut accounts)?; + __idl_set_authority(program_id, &mut accounts, new_authority)?; + accounts.exit(program_id)?; + } + } + Ok(()) + } + + // One time IDL account initializer. Will faill on subsequent + // invocations. + #[inline(never)] + pub fn __idl_create_account( + program_id: &Pubkey, + accounts: &mut anchor_lang::idl::IdlCreateAccounts, + data_len: u64, + ) -> ProgramResult { + // Create the IDL's account. + let from = accounts.from.key; + let (base, nonce) = Pubkey::find_program_address(&[], accounts.program.key); + let seed = anchor_lang::idl::IdlAccount::seed(); + let owner = accounts.program.key; + let to = Pubkey::create_with_seed(&base, seed, owner).unwrap(); + // Space: account discriminator || authority pubkey || vec len || vec data + let space = 8 + 32 + 4 + data_len as usize; + let lamports = accounts.rent.minimum_balance(space); + let seeds = &[&[nonce][..]]; + let ix = anchor_lang::solana_program::system_instruction::create_account_with_seed( + from, + &to, + &base, + seed, + lamports, + space as u64, + owner, + ); + anchor_lang::solana_program::program::invoke_signed( + &ix, + &[ + accounts.from.clone(), + accounts.to.clone(), + accounts.base.clone(), + accounts.system_program.clone(), + ], + &[seeds], + )?; + + // Deserialize the newly created account. + let mut idl_account = { + let mut account_data = accounts.to.try_borrow_data()?; + let mut account_data_slice: &[u8] = &account_data; + anchor_lang::idl::IdlAccount::try_deserialize_unchecked( + &mut account_data_slice, + )? + }; + + // Set the authority. + idl_account.authority = *accounts.from.key; + + // Store the new account data. + let mut data = accounts.to.try_borrow_mut_data()?; + let dst: &mut [u8] = &mut data; + let mut cursor = std::io::Cursor::new(dst); + idl_account.try_serialize(&mut cursor)?; + + Ok(()) + } + + #[inline(never)] + pub fn __idl_write( + program_id: &Pubkey, + accounts: &mut anchor_lang::idl::IdlAccounts, + idl_data: Vec, + ) -> ProgramResult { + let mut idl = &mut accounts.idl; + idl.data.extend(idl_data); + Ok(()) + } + + #[inline(never)] + pub fn __idl_clear( + program_id: &Pubkey, + accounts: &mut anchor_lang::idl::IdlAccounts, + ) -> ProgramResult { + accounts.idl.data = vec![]; + Ok(()) + } + + #[inline(never)] + pub fn __idl_set_authority( + program_id: &Pubkey, + accounts: &mut anchor_lang::idl::IdlAccounts, + new_authority: Pubkey, + ) -> ProgramResult { + accounts.idl.authority = new_authority; + Ok(()) + } + } + }; let non_inlined_ctor: proc_macro2::TokenStream = match &program.state { None => quote! {}, Some(state) => { @@ -123,6 +260,8 @@ pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStr let mod_name = &program.name; let anchor_ident = &state.ctor_anchor; quote! { + // One time state account initializer. Will faill on subsequent + // invocations. #[inline(never)] pub fn __ctor(program_id: &Pubkey, accounts: &[AccountInfo], #(#ctor_typed_args),*) -> ProgramResult { let mut remaining_accounts: &[AccountInfo] = accounts; @@ -277,6 +416,7 @@ pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStr .collect(); quote! { + #non_inlined_idl #non_inlined_ctor #(#non_inlined_state_handlers)* #(#non_inlined_handlers)* diff --git a/syn/src/idl.rs b/syn/src/idl.rs index e70282c4..f0ab9c21 100644 --- a/syn/src/idl.rs +++ b/syn/src/idl.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Idl { pub version: String, pub name: String, @@ -17,7 +17,7 @@ pub struct Idl { pub metadata: Option, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct IdlState { #[serde(rename = "struct")] pub strct: IdlTypeDef, @@ -26,7 +26,7 @@ pub struct IdlState { pub type IdlStateMethod = IdlInstruction; -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct IdlInstruction { pub name: String, pub accounts: Vec, @@ -34,14 +34,14 @@ pub struct IdlInstruction { } // A single struct deriving `Accounts`. -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct IdlAccounts { pub name: String, pub accounts: Vec, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum IdlAccountItem { IdlAccount(IdlAccount), @@ -49,7 +49,7 @@ pub enum IdlAccountItem { } // A single field in the accounts struct. -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct IdlAccount { pub name: String, @@ -57,42 +57,42 @@ pub struct IdlAccount { pub is_signer: bool, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct IdlField { pub name: String, #[serde(rename = "type")] pub ty: IdlType, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct IdlTypeDef { pub name: String, #[serde(rename = "type")] pub ty: IdlTypeDefTy, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "lowercase", tag = "kind")] pub enum IdlTypeDefTy { Struct { fields: Vec }, Enum { variants: Vec }, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct EnumVariant { pub name: String, #[serde(skip_serializing_if = "Option::is_none", default)] pub fields: Option, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum EnumFields { Named(Vec), Tuple(Vec), } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum IdlType { Bool, @@ -112,7 +112,7 @@ pub enum IdlType { Vec(Box), } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct IdlTypePublicKey; impl std::str::FromStr for IdlType { @@ -162,7 +162,7 @@ impl std::str::FromStr for IdlType { } } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct IdlErrorCode { pub code: u32, pub name: String, diff --git a/ts/package.json b/ts/package.json index 4a019c32..edded4ed 100644 --- a/ts/package.json +++ b/ts/package.json @@ -27,13 +27,15 @@ "@solana/web3.js": "^0.90.4", "@types/bn.js": "^4.11.6", "@types/bs58": "^4.0.1", + "@types/pako": "^1.0.1", "bn.js": "^5.1.2", "bs58": "^4.0.1", "buffer-layout": "^1.2.0", "camelcase": "^5.3.1", "crypto-hash": "^1.3.0", "eventemitter3": "^4.0.7", - "find": "^0.3.0" + "find": "^0.3.0", + "pako": "^2.0.3" }, "devDependencies": { "@commitlint/cli": "^8.2.0", diff --git a/ts/src/idl.ts b/ts/src/idl.ts index 6e44961f..375549c8 100644 --- a/ts/src/idl.ts +++ b/ts/src/idl.ts @@ -1,3 +1,6 @@ +import { PublicKey } from "@solana/web3.js"; +import * as borsh from "@project-serum/borsh"; + export type Idl = { version: string; name: string; @@ -101,3 +104,35 @@ type IdlErrorCode = { name: string; msg?: string; }; + +// Deterministic IDL address as a function of the program id. +export async function idlAddress(programId: PublicKey): Promise { + const base = (await PublicKey.findProgramAddress([], programId))[0]; + return await PublicKey.createWithSeed(base, seed(), programId); +} + +// Seed for generating the idlAddress. +export function seed(): string { + return "anchor:idl"; +} + +// The on-chain account of the IDL. +export interface IdlProgramAccount { + authority: PublicKey; + data: Buffer; +} + +const IDL_ACCOUNT_LAYOUT: borsh.Layout = borsh.struct([ + borsh.publicKey("authority"), + borsh.vecU8("data"), +]); + +export function decodeIdlAccount(data: Buffer): IdlProgramAccount { + return IDL_ACCOUNT_LAYOUT.decode(data); +} + +export function encodeIdlAccount(acc: IdlProgramAccount): Buffer { + const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer. + const len = IDL_ACCOUNT_LAYOUT.encode(acc, buffer); + return buffer.slice(0, len); +} diff --git a/ts/src/program.ts b/ts/src/program.ts index 5e8fc510..8ec7e135 100644 --- a/ts/src/program.ts +++ b/ts/src/program.ts @@ -1,7 +1,8 @@ import { PublicKey } from "@solana/web3.js"; +import { inflate } from "pako"; import Provider from "./provider"; import { RpcFactory } from "./rpc"; -import { Idl } from "./idl"; +import { Idl, idlAddress, decodeIdlAccount } from "./idl"; import Coder from "./coder"; import { Rpcs, Ixs, Txs, Accounts, State } from "./rpc"; import { getProvider } from "./"; @@ -78,4 +79,33 @@ export class Program { this.coder = coder; this.state = state; } + + /** + * Generates a Program client by fetching the IDL from chain. + */ + public static async at(programId: PublicKey, provider?: Provider) { + const idl = await Program.fetchIdl(programId, provider); + return new Program(idl, programId, provider); + } + + /** + * Fetches an idl from the blockchain. + */ + public static async fetchIdl(programId: PublicKey, provider?: Provider) { + provider = provider ?? getProvider(); + const address = await idlAddress(programId); + const accountInfo = await provider.connection.getAccountInfo(address); + // Chop off account discriminator. + let idlAccount = decodeIdlAccount(accountInfo.data.slice(8)); + const inflatedIdl = inflate(idlAccount.data); + return JSON.parse(decodeUtf8(inflatedIdl)); + } +} + +function decodeUtf8(array: Uint8Array): string { + const decoder = + typeof TextDecoder === "undefined" + ? new (require("util").TextDecoder)("utf-8") // Node. + : new TextDecoder("utf-8"); // Browser. + return decoder.decode(array); } diff --git a/ts/yarn.lock b/ts/yarn.lock index 54f68514..a0094eca 100644 --- a/ts/yarn.lock +++ b/ts/yarn.lock @@ -829,6 +829,11 @@ resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== +"@types/pako@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/pako/-/pako-1.0.1.tgz#33b237f3c9aff44d0f82fe63acffa4a365ef4a61" + integrity sha512-GdZbRSJ3Cv5fiwT6I0SQ3ckeN2PWNqxd26W9Z2fCK1tGrrasGy4puvNFtnddqH9UJFMQYXxEuuB7B8UK+LLwSg== + "@types/parse-json@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" @@ -4510,6 +4515,11 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +pako@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/pako/-/pako-2.0.3.tgz#cdf475e31b678565251406de9e759196a0ea7a43" + integrity sha512-WjR1hOeg+kki3ZIOjaf4b5WVcay1jaliKSYiEaB1XzwhMQZJxRdQRv0V31EKBYlxb4T7SK3hjfc/jxyU64BoSw== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"