From d8d720067dd6e2a3bec50207b84008276c914732 Mon Sep 17 00:00:00 2001 From: Armani Ferrante Date: Mon, 24 Jan 2022 14:44:24 -0500 Subject: [PATCH] lang, ts: automatic client side pda derivation (#1331) --- .github/workflows/tests.yaml | 2 + .gitmodules | 3 + cli/src/config.rs | 15 +- cli/src/lib.rs | 22 +- lang/syn/Cargo.toml | 1 + lang/syn/src/idl/file.rs | 18 +- lang/syn/src/idl/mod.rs | 50 ++- lang/syn/src/idl/pda.rs | 322 ++++++++++++++++++ lang/syn/src/lib.rs | 40 ++- tests/.prettierignore | 1 + tests/auction-house | 1 + tests/misc/programs/misc/Cargo.toml | 1 + tests/misc/tests/misc.js | 14 +- tests/package.json | 1 + tests/pda-derivation/Anchor.toml | 15 + tests/pda-derivation/Cargo.toml | 4 + tests/pda-derivation/migrations/deploy.ts | 22 ++ tests/pda-derivation/package.json | 19 ++ .../programs/pda-derivation/Cargo.toml | 18 + .../programs/pda-derivation/Xargo.toml | 2 + .../programs/pda-derivation/src/lib.rs | 83 +++++ tests/pda-derivation/tests/typescript.spec.ts | 34 ++ tests/pda-derivation/tsconfig.json | 10 + ts/src/coder/spl-token/buffer-layout.ts | 15 +- ts/src/idl.ts | 8 + ts/src/program/accounts-resolver.ts | 254 ++++++++++++++ ts/src/program/namespace/index.ts | 14 +- ts/src/program/namespace/methods.ts | 91 +++-- ts/src/program/namespace/types.ts | 10 + ts/src/spl/token.ts | 11 +- ts/src/utils/token.ts | 4 +- 31 files changed, 1027 insertions(+), 78 deletions(-) create mode 100644 lang/syn/src/idl/pda.rs create mode 160000 tests/auction-house create mode 100644 tests/pda-derivation/Anchor.toml create mode 100644 tests/pda-derivation/Cargo.toml create mode 100644 tests/pda-derivation/migrations/deploy.ts create mode 100644 tests/pda-derivation/package.json create mode 100644 tests/pda-derivation/programs/pda-derivation/Cargo.toml create mode 100644 tests/pda-derivation/programs/pda-derivation/Xargo.toml create mode 100644 tests/pda-derivation/programs/pda-derivation/src/lib.rs create mode 100644 tests/pda-derivation/tests/typescript.spec.ts create mode 100644 tests/pda-derivation/tsconfig.json create mode 100644 ts/src/program/accounts-resolver.ts diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 915f038a..fad42bcb 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -272,6 +272,8 @@ jobs: path: tests/ido-pool - cmd: cd tests/cfo && anchor run test-with-build path: tests/cfo + - cmd: cd tests/auction-house && yarn && anchor test + path: tests/auction-house steps: - uses: actions/checkout@v2 - uses: ./.github/actions/setup/ diff --git a/.gitmodules b/.gitmodules index ec506b73..5bb41c1e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,3 +13,6 @@ [submodule "examples/permissioned-markets/deps/serum-dex"] path = tests/permissioned-markets/deps/serum-dex url = https://github.com/project-serum/serum-dex +[submodule "tests/auction-house"] + path = tests/auction-house + url = https://github.com/armaniferrante/auction-house diff --git a/cli/src/config.rs b/cli/src/config.rs index 64b9c96a..16dd54e7 100644 --- a/cli/src/config.rs +++ b/cli/src/config.rs @@ -162,7 +162,11 @@ impl WithPath { let cargo = Manifest::from_path(&path.join("Cargo.toml"))?; let lib_name = cargo.lib_name()?; let version = cargo.version(); - let idl = anchor_syn::idl::file::parse(path.join("src/lib.rs"), version)?; + let idl = anchor_syn::idl::file::parse( + path.join("src/lib.rs"), + version, + self.features.seeds, + )?; r.push(Program { lib_name, path, @@ -243,6 +247,7 @@ impl std::ops::DerefMut for WithPath { pub struct Config { pub anchor_version: Option, pub solana_version: Option, + pub features: FeaturesConfig, pub registry: RegistryConfig, pub provider: ProviderConfig, pub programs: ProgramsConfig, @@ -251,6 +256,11 @@ pub struct Config { pub test: Option, } +#[derive(Default, Clone, Debug, Serialize, Deserialize)] +pub struct FeaturesConfig { + pub seeds: bool, +} + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct RegistryConfig { pub url: String, @@ -362,6 +372,7 @@ impl Config { struct _Config { anchor_version: Option, solana_version: Option, + features: Option, programs: Option>>, registry: Option, provider: Provider, @@ -389,6 +400,7 @@ impl ToString for Config { let cfg = _Config { anchor_version: self.anchor_version.clone(), solana_version: self.solana_version.clone(), + features: Some(self.features.clone()), registry: Some(self.registry.clone()), provider: Provider { cluster: format!("{}", self.provider.cluster), @@ -417,6 +429,7 @@ impl FromStr for Config { Ok(Config { anchor_version: cfg.anchor_version, solana_version: cfg.solana_version, + features: cfg.features.unwrap_or_default(), registry: cfg.registry.unwrap_or_default(), provider: ProviderConfig { cluster: cfg.provider.cluster.parse()?, diff --git a/cli/src/lib.rs b/cli/src/lib.rs index c56a468e..c5e93745 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -861,7 +861,7 @@ fn build_cwd_verifiable( Ok(_) => { // Build the idl. println!("Extracting the IDL"); - if let Ok(Some(idl)) = extract_idl("src/lib.rs") { + if let Ok(Some(idl)) = extract_idl(cfg, "src/lib.rs") { // Write out the JSON file. println!("Writing the IDL file"); let out_file = workspace_dir.join(format!("target/idl/{}.json", idl.name)); @@ -1135,7 +1135,7 @@ fn _build_cwd( } // Always assume idl is located at src/lib.rs. - if let Some(idl) = extract_idl("src/lib.rs")? { + if let Some(idl) = extract_idl(cfg, "src/lib.rs")? { // JSON out path. let out = match idl_out { None => PathBuf::from(".").join(&idl.name).with_extension("json"), @@ -1219,7 +1219,7 @@ fn verify( } // Verify IDL (only if it's not a buffer account). - if let Some(local_idl) = extract_idl("src/lib.rs")? { + if let Some(local_idl) = extract_idl(&cfg, "src/lib.rs")? { if bin_ver.state != BinVerificationState::Buffer { let deployed_idl = fetch_idl(cfg_override, program_id)?; if local_idl != deployed_idl { @@ -1383,12 +1383,12 @@ fn fetch_idl(cfg_override: &ConfigOverride, idl_addr: Pubkey) -> Result { serde_json::from_slice(&s[..]).map_err(Into::into) } -fn extract_idl(file: &str) -> Result> { +fn extract_idl(cfg: &WithPath, file: &str) -> Result> { let file = shellexpand::tilde(file); let manifest_from_path = std::env::current_dir()?.join(PathBuf::from(&*file).parent().unwrap()); let cargo = Manifest::discover_from_path(manifest_from_path)? .ok_or_else(|| anyhow!("Cargo.toml not found"))?; - anchor_syn::idl::file::parse(&*file, cargo.version()) + anchor_syn::idl::file::parse(&*file, cargo.version(), cfg.features.seeds) } fn idl(cfg_override: &ConfigOverride, subcmd: IdlCommand) -> Result<()> { @@ -1415,7 +1415,7 @@ fn idl(cfg_override: &ConfigOverride, subcmd: IdlCommand) -> Result<()> { } => idl_set_authority(cfg_override, program_id, address, new_authority), IdlCommand::EraseAuthority { program_id } => idl_erase_authority(cfg_override, program_id), IdlCommand::Authority { program_id } => idl_authority(cfg_override, program_id), - IdlCommand::Parse { file, out, out_ts } => idl_parse(file, out, out_ts), + IdlCommand::Parse { file, out, out_ts } => idl_parse(cfg_override, file, out, out_ts), IdlCommand::Fetch { address, out } => idl_fetch(cfg_override, address, out), } } @@ -1674,8 +1674,14 @@ fn idl_write(cfg: &Config, program_id: &Pubkey, idl: &Idl, idl_address: Pubkey) Ok(()) } -fn idl_parse(file: String, out: Option, out_ts: Option) -> Result<()> { - let idl = extract_idl(&file)?.ok_or_else(|| anyhow!("IDL not parsed"))?; +fn idl_parse( + cfg_override: &ConfigOverride, + file: String, + out: Option, + out_ts: Option, +) -> Result<()> { + let cfg = Config::discover(cfg_override)?.expect("Not in workspace."); + let idl = extract_idl(&cfg, &file)?.ok_or_else(|| anyhow!("IDL not parsed"))?; let out = match out { None => OutFile::Stdout, Some(out) => OutFile::File(PathBuf::from(out)), diff --git a/lang/syn/Cargo.toml b/lang/syn/Cargo.toml index bb8b61a4..89650a24 100644 --- a/lang/syn/Cargo.toml +++ b/lang/syn/Cargo.toml @@ -12,6 +12,7 @@ idl = [] hash = [] default = [] anchor-debug = [] +seeds = [] [dependencies] proc-macro2 = "1.0" diff --git a/lang/syn/src/idl/file.rs b/lang/syn/src/idl/file.rs index 9aa1f386..119f173a 100644 --- a/lang/syn/src/idl/file.rs +++ b/lang/syn/src/idl/file.rs @@ -14,7 +14,11 @@ const DERIVE_NAME: &str = "Accounts"; const ERROR_CODE_OFFSET: u32 = 6000; // Parse an entire interface file. -pub fn parse(filename: impl AsRef, version: String) -> Result> { +pub fn parse( + filename: impl AsRef, + version: String, + seeds_feature: bool, +) -> Result> { let ctx = CrateContext::parse(filename)?; let program_mod = match parse_program_mod(&ctx) { @@ -52,7 +56,8 @@ pub fn parse(filename: impl AsRef, version: String) -> Result> .collect::>(); let accounts_strct = accs.get(&method.anchor_ident.to_string()).unwrap(); - let accounts = idl_accounts(accounts_strct, &accs); + let accounts = + idl_accounts(&ctx, accounts_strct, &accs, seeds_feature); IdlInstruction { name, accounts, @@ -91,7 +96,7 @@ pub fn parse(filename: impl AsRef, version: String) -> Result> }) .collect(); let accounts_strct = accs.get(&anchor_ident.to_string()).unwrap(); - let accounts = idl_accounts(accounts_strct, &accs); + let accounts = idl_accounts(&ctx, accounts_strct, &accs, seeds_feature); IdlInstruction { name, accounts, @@ -159,7 +164,7 @@ pub fn parse(filename: impl AsRef, version: String) -> Result> .collect::>(); // todo: don't unwrap let accounts_strct = accs.get(&ix.anchor_ident.to_string()).unwrap(); - let accounts = idl_accounts(accounts_strct, &accs); + let accounts = idl_accounts(&ctx, accounts_strct, &accs, seeds_feature); IdlInstruction { name: ix.ident.to_string().to_mixed_case(), accounts, @@ -494,8 +499,10 @@ fn to_idl_type(f: &syn::Field) -> IdlType { } fn idl_accounts( + ctx: &CrateContext, accounts: &AccountsStruct, global_accs: &HashMap, + seeds_feature: bool, ) -> Vec { accounts .fields @@ -505,7 +512,7 @@ fn idl_accounts( let accs_strct = global_accs .get(&comp_f.symbol) .expect("Could not resolve Accounts symbol"); - let accounts = idl_accounts(accs_strct, global_accs); + let accounts = idl_accounts(ctx, accs_strct, global_accs, seeds_feature); IdlAccountItem::IdlAccounts(IdlAccounts { name: comp_f.ident.to_string().to_mixed_case(), accounts, @@ -518,6 +525,7 @@ fn idl_accounts( Ty::Signer => true, _ => acc.constraints.is_signer(), }, + pda: pda::parse(ctx, accounts, acc, seeds_feature), }), }) .collect::>() diff --git a/lang/syn/src/idl/mod.rs b/lang/syn/src/idl/mod.rs index 590c78aa..9482d367 100644 --- a/lang/syn/src/idl/mod.rs +++ b/lang/syn/src/idl/mod.rs @@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize}; use serde_json::Value as JsonValue; pub mod file; +pub mod pda; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct Idl { @@ -66,6 +67,52 @@ pub struct IdlAccount { pub name: String, pub is_mut: bool, pub is_signer: bool, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub pda: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct IdlPda { + pub seeds: Vec, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub program_id: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase", tag = "kind")] +pub enum IdlSeed { + Const(IdlSeedConst), + Arg(IdlSeedArg), + Account(IdlSeedAccount), +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct IdlSeedAccount { + #[serde(rename = "type")] + pub ty: IdlType, + // account_ty points to the entry in the "accounts" section. + // Some only if the `Account` type is used. + #[serde(skip_serializing_if = "Option::is_none")] + pub account: Option, + pub path: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct IdlSeedArg { + #[serde(rename = "type")] + pub ty: IdlType, + pub path: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct IdlSeedConst { + #[serde(rename = "type")] + pub ty: IdlType, + pub value: serde_json::Value, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] @@ -157,6 +204,7 @@ impl std::str::FromStr for IdlType { } } s.retain(|c| !c.is_whitespace()); + let r = match s.as_str() { "bool" => IdlType::Bool, "u8" => IdlType::U8, @@ -170,7 +218,7 @@ impl std::str::FromStr for IdlType { "u128" => IdlType::U128, "i128" => IdlType::I128, "Vec" => IdlType::Bytes, - "String" => IdlType::String, + "String" | "&str" => IdlType::String, "Pubkey" => IdlType::PublicKey, _ => match s.to_string().strip_prefix("Option<") { None => match s.to_string().strip_prefix("Vec<") { diff --git a/lang/syn/src/idl/pda.rs b/lang/syn/src/idl/pda.rs new file mode 100644 index 00000000..537e58f5 --- /dev/null +++ b/lang/syn/src/idl/pda.rs @@ -0,0 +1,322 @@ +use crate::idl::*; +use crate::parser; +use crate::parser::context::CrateContext; +use crate::ConstraintSeedsGroup; +use crate::{AccountsStruct, Field}; +use std::collections::HashMap; +use std::str::FromStr; +use syn::Expr; + +// Parses a seeds constraint, extracting the IdlSeed types. +// +// Note: This implementation makes assumptions about the types that can be used +// (e.g., no program-defined function calls in seeds). +// +// This probably doesn't cover all cases. If you see a warning log, you +// can add a new case here. In the worst case, we miss a seed and +// the parser will treat the given seeds as empty and so clients will +// simply fail to automatically populate the PDA accounts. +// +// Seed Assumptions: Seeds must be of one of the following forms: +// +// - instruction argument. +// - account context field pubkey. +// - account data, where the account is defined in the current program. +// We make an exception for the SPL token program, since it is so common +// and sometimes convenient to use fields as a seed (e.g. Auction house +// program). In the case of nested structs/account data, all nested structs +// must be defined in the current program as well. +// - byte string literal (e.g. b"MY_SEED"). +// - byte string literal constant (e.g. `pub const MY_SEED: [u8; 2] = *b"hi";`). +// - array constants. +// +pub fn parse( + ctx: &CrateContext, + accounts: &AccountsStruct, + acc: &Field, + seeds_feature: bool, +) -> Option { + if !seeds_feature { + return None; + } + let pda_parser = PdaParser::new(ctx, accounts); + acc.constraints + .seeds + .as_ref() + .map(|s| pda_parser.parse(s)) + .unwrap_or(None) +} + +struct PdaParser<'a> { + ctx: &'a CrateContext, + // Accounts context. + accounts: &'a AccountsStruct, + // Maps var name to var type. These are the instruction arguments in a + // given accounts context. + ix_args: HashMap, + // Constants available in the crate. + const_names: Vec, + // All field names of the accounts in the accounts context. + account_field_names: Vec, +} + +impl<'a> PdaParser<'a> { + fn new(ctx: &'a CrateContext, accounts: &'a AccountsStruct) -> Self { + // All the available sources of seeds. + let ix_args = accounts.instruction_args().unwrap_or_default(); + let const_names: Vec = ctx.consts().map(|c| c.ident.to_string()).collect(); + let account_field_names = accounts.field_names(); + + Self { + ctx, + accounts, + ix_args, + const_names, + account_field_names, + } + } + + fn parse(&self, seeds_grp: &ConstraintSeedsGroup) -> Option { + // Extract the idl seed types from the constraints. + let seeds = seeds_grp + .seeds + .iter() + .map(|s| self.parse_seed(s)) + .collect::>>()?; + + // Parse the program id from the constraints. + let program_id = seeds_grp + .program_seed + .as_ref() + .map(|pid| self.parse_seed(pid)) + .unwrap_or_default(); + + // Done. + Some(IdlPda { seeds, program_id }) + } + + fn parse_seed(&self, seed: &Expr) -> Option { + match seed { + Expr::MethodCall(_) => { + let seed_path = parse_seed_path(seed)?; + + if self.is_instruction(&seed_path) { + self.parse_instruction(&seed_path) + } else if self.is_const(&seed_path) { + self.parse_const(&seed_path) + } else if self.is_account(&seed_path) { + self.parse_account(&seed_path) + } else if self.is_str_literal(&seed_path) { + self.parse_str_literal(&seed_path) + } else { + println!("WARNING: unexpected seed category for var: {:?}", seed_path); + None + } + } + Expr::Reference(expr_reference) => self.parse_seed(&expr_reference.expr), + Expr::Index(_) => { + println!("WARNING: auto pda derivation not currently supported for slice literals"); + None + } + // Unknown type. Please file an issue. + _ => { + println!("WARNING: unexpected seed: {:?}", seed); + None + } + } + } + + fn parse_instruction(&self, seed_path: &SeedPath) -> Option { + let idl_ty = IdlType::from_str(self.ix_args.get(&seed_path.name()).unwrap()).ok()?; + Some(IdlSeed::Arg(IdlSeedArg { + ty: idl_ty, + path: seed_path.path(), + })) + } + + fn parse_const(&self, seed_path: &SeedPath) -> Option { + // Pull in the constant value directly into the IDL. + assert!(seed_path.components().is_empty()); + let const_item = self + .ctx + .consts() + .find(|c| c.ident == seed_path.name()) + .unwrap(); + let idl_ty = IdlType::from_str(&parser::tts_to_string(&const_item.ty)).ok()?; + let mut idl_ty_value = parser::tts_to_string(&const_item.expr); + + if let IdlType::Array(_ty, _size) = &idl_ty { + // Convert str literal to array. + if idl_ty_value.contains("b\"") { + let components: Vec<&str> = idl_ty_value.split('b').collect(); + assert!(components.len() == 2); + let mut str_lit = components[1].to_string(); + str_lit.retain(|c| c != '"'); + idl_ty_value = format!("{:?}", str_lit.as_bytes()); + } + } + + Some(IdlSeed::Const(IdlSeedConst { + ty: idl_ty, + value: serde_json::from_str(&idl_ty_value).unwrap(), + })) + } + + fn parse_account(&self, seed_path: &SeedPath) -> Option { + // Get the anchor account field from the derive accounts struct. + let account_field = self + .accounts + .fields + .iter() + .find(|field| *field.ident() == seed_path.name()) + .unwrap(); + + // Follow the path to find the seed type. + let ty = { + let mut path = seed_path.components(); + match path.len() { + 0 => IdlType::PublicKey, + 1 => { + // Name of the account struct. + let account = account_field.ty_name()?; + if account == "TokenAccount" { + assert!(path.len() == 1); + match path[0].as_str() { + "mint" => IdlType::PublicKey, + "amount" => IdlType::U64, + "authority" => IdlType::PublicKey, + "delegated_amount" => IdlType::U64, + _ => { + println!("WARNING: token field isn't supported: {}", &path[0]); + return None; + } + } + } else { + // Get the rust representation of the field's struct. + let strct = self.ctx.structs().find(|s| s.ident == account).unwrap(); + parse_field_path(self.ctx, strct, &mut path) + } + } + _ => panic!("invariant violation"), + } + }; + + Some(IdlSeed::Account(IdlSeedAccount { + ty, + account: account_field.ty_name(), + path: seed_path.path(), + })) + } + + fn parse_str_literal(&self, seed_path: &SeedPath) -> Option { + let mut var_name = seed_path.name(); + // Remove the byte `b` prefix if the string is of the form `b"seed". + if var_name.starts_with("b\"") { + var_name.remove(0); + } + let value_string: String = var_name.chars().filter(|c| *c != '"').collect(); + Some(IdlSeed::Const(IdlSeedConst { + value: serde_json::Value::String(value_string), + ty: IdlType::String, + })) + } + + fn is_instruction(&self, seed_path: &SeedPath) -> bool { + self.ix_args.contains_key(&seed_path.name()) + } + + fn is_const(&self, seed_path: &SeedPath) -> bool { + self.const_names.contains(&seed_path.name()) + } + + fn is_account(&self, seed_path: &SeedPath) -> bool { + self.account_field_names.contains(&seed_path.name()) + } + + fn is_str_literal(&self, seed_path: &SeedPath) -> bool { + seed_path.components().is_empty() && seed_path.name().contains('"') + } +} + +// SeedPath represents the deconstructed syntax of a single pda seed, +// consisting of a variable name and a vec of all the sub fields accessed +// on that variable name. For example, if a seed is `my_field.my_data.as_ref()`, +// then the field name is `my_field` and the vec of sub fields is `[my_data]`. +#[derive(Debug)] +struct SeedPath(String, Vec); + +impl SeedPath { + fn name(&self) -> String { + self.0.clone() + } + + // Full path to the data this seed represents. + fn path(&self) -> String { + match self.1.len() { + 0 => self.0.clone(), + _ => format!("{}.{}", self.name(), self.components().join(".")), + } + } + + // All path components for the subfields accessed on this seed. + fn components(&self) -> &[String] { + &self.1 + } +} + +// Extracts the seed path from a single seed expression. +fn parse_seed_path(seed: &Expr) -> Option { + // Convert the seed into the raw string representation. + let seed_str = parser::tts_to_string(&seed); + + // Break up the seed into each sub field component. + let mut components: Vec<&str> = seed_str.split(" . ").collect(); + if components.len() <= 1 { + println!("WARNING: seeds are in an unexpected format: {:?}", seed); + return None; + } + + // The name of the variable (or field). + let name = components.remove(0).to_string(); + + // The path to the seed (only if the `name` type is a struct). + let mut path = Vec::new(); + while !components.is_empty() { + let c = components.remove(0); + if c.contains("()") { + break; + } + path.push(c.to_string()); + } + if path.len() == 1 && (path[0] == "key" || path[0] == "key()") { + path = Vec::new(); + } + + Some(SeedPath(name, path)) +} + +fn parse_field_path(ctx: &CrateContext, strct: &syn::ItemStruct, path: &mut &[String]) -> IdlType { + let field_name = &path[0]; + *path = &path[1..]; + + // Get the type name for the field. + let next_field = strct + .fields + .iter() + .find(|f| &f.ident.clone().unwrap().to_string() == field_name) + .unwrap(); + let next_field_ty_str = parser::tts_to_string(&next_field.ty); + + // The path is empty so this must be a primitive type. + if path.is_empty() { + return next_field_ty_str.parse().unwrap(); + } + + // Get the rust representation of hte field's struct. + let strct = ctx + .structs() + .find(|s| s.ident == next_field_ty_str) + .unwrap(); + + parse_field_path(ctx, strct, path) +} diff --git a/lang/syn/src/lib.rs b/lang/syn/src/lib.rs index eb3e6a72..7d29cc51 100644 --- a/lang/syn/src/lib.rs +++ b/lang/syn/src/lib.rs @@ -5,6 +5,7 @@ use parser::program as program_parser; use proc_macro2::{Span, TokenStream}; use quote::quote; use quote::ToTokens; +use std::collections::HashMap; use std::ops::Deref; use syn::ext::IdentExt; use syn::parse::{Error as ParseError, Parse, ParseStream, Result as ParseResult}; @@ -145,6 +146,30 @@ impl AccountsStruct { instruction_api, } } + + // Return value maps instruction name to type. + // E.g. if we have `#[instruction(data: u64)]` then returns + // { "data": "u64"}. + pub fn instruction_args(&self) -> Option> { + self.instruction_api.as_ref().map(|instruction_api| { + instruction_api + .iter() + .map(|expr| { + let arg = parser::tts_to_string(&expr); + let components: Vec<&str> = arg.split(" : ").collect(); + assert!(components.len() == 2); + (components[0].to_string(), components[1].to_string()) + }) + .collect() + }) + } + + pub fn field_names(&self) -> Vec { + self.fields + .iter() + .map(|field| field.ident().to_string()) + .collect() + } } #[allow(clippy::large_enum_variant)] @@ -161,6 +186,19 @@ impl AccountField { AccountField::CompositeField(c_field) => &c_field.ident, } } + + pub fn ty_name(&self) -> Option { + match self { + AccountField::Field(field) => match &field.ty { + Ty::Account(account) => Some(parser::tts_to_string(&account.account_type_path)), + Ty::ProgramAccount(account) => { + Some(parser::tts_to_string(&account.account_type_path)) + } + _ => None, + }, + AccountField::CompositeField(field) => Some(field.symbol.clone()), + } + } } #[derive(Debug)] @@ -689,7 +727,7 @@ pub struct ConstraintSeedsGroup { pub is_init: bool, pub seeds: Punctuated, pub bump: Option, // None => bump was given without a target. - pub program_seed: Option, // None => use the current program's program_id + pub program_seed: Option, // None => use the current program's program_id. } #[derive(Debug, Clone)] diff --git a/tests/.prettierignore b/tests/.prettierignore index 844b0808..08fc59ee 100644 --- a/tests/.prettierignore +++ b/tests/.prettierignore @@ -1,2 +1,3 @@ **/target/types/*.ts cfo/deps/ +auction-house/deps/ \ No newline at end of file diff --git a/tests/auction-house b/tests/auction-house new file mode 160000 index 00000000..fea2d89c --- /dev/null +++ b/tests/auction-house @@ -0,0 +1 @@ +Subproject commit fea2d89c2b17ee39fcf0ebaadb0317b9e97206f4 diff --git a/tests/misc/programs/misc/Cargo.toml b/tests/misc/programs/misc/Cargo.toml index 53f44966..dc3406c0 100644 --- a/tests/misc/programs/misc/Cargo.toml +++ b/tests/misc/programs/misc/Cargo.toml @@ -18,3 +18,4 @@ default = [] anchor-lang = { path = "../../../../lang" } anchor-spl = { path = "../../../../spl" } misc2 = { path = "../misc2", features = ["cpi"] } +spl-associated-token-account = "=1.0.3" diff --git a/tests/misc/tests/misc.js b/tests/misc/tests/misc.js index 854b43f1..96bf281e 100644 --- a/tests/misc/tests/misc.js +++ b/tests/misc/tests/misc.js @@ -1,5 +1,4 @@ const anchor = require("@project-serum/anchor"); -const PublicKey = anchor.web3.PublicKey; const assert = require("assert"); const { ASSOCIATED_TOKEN_PROGRAM_ID, @@ -7,7 +6,12 @@ const { Token, } = require("@solana/spl-token"); const miscIdl = require("../target/idl/misc.json"); -const { SystemProgram } = require("@solana/web3.js"); +const { + SystemProgram, + Keypair, + PublicKey, + SYSVAR_RENT_PUBKEY, +} = require("@solana/web3.js"); const utf8 = anchor.utils.bytes.utf8; describe("misc", () => { @@ -1221,14 +1225,14 @@ describe("misc", () => { }); it("init_if_needed throws if associated token exists but has the wrong owner", async () => { - const mint = anchor.web3.Keypair.generate(); + const mint = Keypair.generate(); await program.rpc.testInitMint({ accounts: { mint: mint.publicKey, payer: program.provider.wallet.publicKey, - systemProgram: anchor.web3.SystemProgram.programId, + systemProgram: SystemProgram.programId, tokenProgram: TOKEN_PROGRAM_ID, - rent: anchor.web3.SYSVAR_RENT_PUBKEY, + rent: SYSVAR_RENT_PUBKEY, }, signers: [mint], }); diff --git a/tests/package.json b/tests/package.json index a7c54b9a..c9da006c 100644 --- a/tests/package.json +++ b/tests/package.json @@ -20,6 +20,7 @@ "misc", "multisig", "permissioned-markets", + "pda-derivation", "pyth", "spl/token-proxy", "swap", diff --git a/tests/pda-derivation/Anchor.toml b/tests/pda-derivation/Anchor.toml new file mode 100644 index 00000000..78935651 --- /dev/null +++ b/tests/pda-derivation/Anchor.toml @@ -0,0 +1,15 @@ +[features] +seeds = true + +[provider] +cluster = "localnet" +wallet = "~/.config/solana/id.json" + +[programs.localnet] +pda_derivation = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS" + +[workspace] +members = ["programs/pda-derivation"] + +[scripts] +test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" diff --git a/tests/pda-derivation/Cargo.toml b/tests/pda-derivation/Cargo.toml new file mode 100644 index 00000000..a60de986 --- /dev/null +++ b/tests/pda-derivation/Cargo.toml @@ -0,0 +1,4 @@ +[workspace] +members = [ + "programs/*" +] diff --git a/tests/pda-derivation/migrations/deploy.ts b/tests/pda-derivation/migrations/deploy.ts new file mode 100644 index 00000000..53e1252d --- /dev/null +++ b/tests/pda-derivation/migrations/deploy.ts @@ -0,0 +1,22 @@ +// Migrations are an early feature. Currently, they're nothing more than this +// single deploy script that's invoked from the CLI, injecting a provider +// configured from the workspace's Anchor.toml. + +const anchor = require("@project-serum/anchor"); + +module.exports = async function (provider) { + // Configure client to use the provider. + anchor.setProvider(provider); + + // Add your deploy script here. + async function deployAsync(exampleString: string): Promise { + return new Promise((resolve) => { + setTimeout(() => { + console.log(exampleString); + resolve(); + }, 2000); + }); + } + + await deployAsync("Typescript migration example complete."); +}; diff --git a/tests/pda-derivation/package.json b/tests/pda-derivation/package.json new file mode 100644 index 00000000..d1c31583 --- /dev/null +++ b/tests/pda-derivation/package.json @@ -0,0 +1,19 @@ +{ + "name": "pda-derivation", + "version": "0.20.1", + "license": "(MIT OR Apache-2.0)", + "homepage": "https://github.com/project-serum/anchor#readme", + "bugs": { + "url": "https://github.com/project-serum/anchor/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/project-serum/anchor.git" + }, + "engines": { + "node": ">=11" + }, + "scripts": { + "test": "anchor test" + } +} diff --git a/tests/pda-derivation/programs/pda-derivation/Cargo.toml b/tests/pda-derivation/programs/pda-derivation/Cargo.toml new file mode 100644 index 00000000..df80579c --- /dev/null +++ b/tests/pda-derivation/programs/pda-derivation/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "pda-derivation" +version = "0.1.0" +description = "Created with Anchor" +edition = "2018" + +[lib] +crate-type = ["cdylib", "lib"] +name = "pda_derivation" + +[features] +no-entrypoint = [] +no-idl = [] +cpi = ["no-entrypoint"] +default = [] + +[dependencies] +anchor-lang = { path = "../../../../lang" } diff --git a/tests/pda-derivation/programs/pda-derivation/Xargo.toml b/tests/pda-derivation/programs/pda-derivation/Xargo.toml new file mode 100644 index 00000000..1744f098 --- /dev/null +++ b/tests/pda-derivation/programs/pda-derivation/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] \ No newline at end of file diff --git a/tests/pda-derivation/programs/pda-derivation/src/lib.rs b/tests/pda-derivation/programs/pda-derivation/src/lib.rs new file mode 100644 index 00000000..ce82597d --- /dev/null +++ b/tests/pda-derivation/programs/pda-derivation/src/lib.rs @@ -0,0 +1,83 @@ +//! The typescript example serves to show how one would setup an Anchor +//! workspace with TypeScript tests and migrations. + +use anchor_lang::prelude::*; + +declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); + +pub const MY_SEED: [u8; 2] = *b"hi"; +pub const MY_SEED_STR: &str = "hi"; +pub const MY_SEED_U8: u8 = 1; +pub const MY_SEED_U32: u32 = 2; +pub const MY_SEED_U64: u64 = 3; + +#[program] +pub mod pda_derivation { + use super::*; + + pub fn init_base(ctx: Context, data: u64, data_key: Pubkey) -> ProgramResult { + let base = &mut ctx.accounts.base; + base.base_data = data; + base.base_data_key = data_key; + Ok(()) + } + + pub fn init_my_account(ctx: Context, seed_a: u8) -> ProgramResult { + Ok(()) + } +} + +#[derive(Accounts)] +pub struct InitBase<'info> { + #[account( + init, + payer = payer, + space = 8+8+32, + )] + base: Account<'info, BaseAccount>, + #[account(mut)] + payer: Signer<'info>, + system_program: Program<'info, System>, +} + +#[derive(Accounts)] +#[instruction(seed_a: u8)] +pub struct InitMyAccount<'info> { + base: Account<'info, BaseAccount>, + base2: AccountInfo<'info>, + #[account( + init, + payer = payer, + space = 8+8, + seeds = [ + &seed_a.to_le_bytes(), + "another-seed".as_bytes(), + b"test".as_ref(), + base.key().as_ref(), + base2.key.as_ref(), + MY_SEED.as_ref(), + MY_SEED_STR.as_bytes(), + MY_SEED_U8.to_le_bytes().as_ref(), + &MY_SEED_U32.to_le_bytes(), + &MY_SEED_U64.to_le_bytes(), + base.base_data.to_le_bytes().as_ref(), + base.base_data_key.as_ref(), + ], + bump, + )] + account: Account<'info, MyAccount>, + #[account(mut)] + payer: Signer<'info>, + system_program: Program<'info, System>, +} + +#[account] +pub struct MyAccount { + data: u64, +} + +#[account] +pub struct BaseAccount { + base_data: u64, + base_data_key: Pubkey, +} diff --git a/tests/pda-derivation/tests/typescript.spec.ts b/tests/pda-derivation/tests/typescript.spec.ts new file mode 100644 index 00000000..1343644c --- /dev/null +++ b/tests/pda-derivation/tests/typescript.spec.ts @@ -0,0 +1,34 @@ +import * as anchor from "@project-serum/anchor"; +import BN from "bn.js"; +import { Keypair } from "@solana/web3.js"; + +describe("typescript", () => { + // Configure the client to use the local cluster. + anchor.setProvider(anchor.Provider.env()); + + const program = anchor.workspace.PdaDerivation; + const base = Keypair.generate(); + const dataKey = Keypair.generate(); + const data = new BN(1); + const seedA = 4; + + it("Inits the base account", async () => { + await program.methods + .initBase(data, dataKey.publicKey) + .accounts({ + base: base.publicKey, + }) + .signers([base]) + .rpc(); + }); + + it("Inits the derived accounts", async () => { + await program.methods + .initMyAccount(seedA) + .accounts({ + base: base.publicKey, + base2: base.publicKey, + }) + .rpc(); + }); +}); diff --git a/tests/pda-derivation/tsconfig.json b/tests/pda-derivation/tsconfig.json new file mode 100644 index 00000000..cd5d2e3d --- /dev/null +++ b/tests/pda-derivation/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "types": ["mocha", "chai"], + "typeRoots": ["./node_modules/@types"], + "lib": ["es2015"], + "module": "commonjs", + "target": "es6", + "esModuleInterop": true + } +} diff --git a/ts/src/coder/spl-token/buffer-layout.ts b/ts/src/coder/spl-token/buffer-layout.ts index 1abfb203..21a77186 100644 --- a/ts/src/coder/spl-token/buffer-layout.ts +++ b/ts/src/coder/spl-token/buffer-layout.ts @@ -2,6 +2,7 @@ import BN from "bn.js"; import * as BufferLayout from "buffer-layout"; import { Layout } from "buffer-layout"; import { PublicKey } from "@solana/web3.js"; +import * as utils from "../../utils"; export function uint64(property?: string): Layout { return new WrappedLayout( @@ -74,30 +75,24 @@ export class COptionLayout extends Layout { encode(src: T | null, b: Buffer, offset = 0): number { if (src === null || src === undefined) { - return this.discriminator.encode(0, b, offset); + return this.layout.span + this.discriminator.encode(0, b, offset); } this.discriminator.encode(1, b, offset); return this.layout.encode(src, b, offset + 4) + 4; } decode(b: Buffer, offset = 0): T | null { - const discriminator = b[offset]; + const discriminator = this.discriminator.decode(b, offset); if (discriminator === 0) { return null; } else if (discriminator === 1) { return this.layout.decode(b, offset + 4); } - throw new Error("Invalid option " + this.property); + throw new Error("Invalid coption " + this.property); } getSpan(b: Buffer, offset = 0): number { - const discriminator = b[offset]; - if (discriminator === 0) { - return 1; - } else if (discriminator === 1) { - return this.layout.getSpan(b, offset + 4) + 4; - } - throw new Error("Invalid option " + this.property); + return this.layout.getSpan(b, offset + 4) + 4; } } diff --git a/ts/src/idl.ts b/ts/src/idl.ts index 7bc65fc6..afe07ad4 100644 --- a/ts/src/idl.ts +++ b/ts/src/idl.ts @@ -50,8 +50,16 @@ export type IdlAccount = { name: string; isMut: boolean; isSigner: boolean; + pda?: IdlPda; }; +export type IdlPda = { + seeds: IdlSeed[]; + programId?: IdlSeed; +}; + +export type IdlSeed = any; // TODO + // A nested/recursive version of IdlAccount. export type IdlAccounts = { name: string; diff --git a/ts/src/program/accounts-resolver.ts b/ts/src/program/accounts-resolver.ts new file mode 100644 index 00000000..758d54a3 --- /dev/null +++ b/ts/src/program/accounts-resolver.ts @@ -0,0 +1,254 @@ +import camelCase from "camelcase"; +import { PublicKey, SystemProgram, SYSVAR_RENT_PUBKEY } from "@solana/web3.js"; +import { Idl, IdlSeed, IdlAccount } from "../idl.js"; +import * as utf8 from "../utils/bytes/utf8.js"; +import { TOKEN_PROGRAM_ID, ASSOCIATED_PROGRAM_ID } from "../utils/token.js"; +import { AllInstructions } from "./namespace/types.js"; +import Provider from "../provider.js"; +import { AccountNamespace } from "./namespace/account.js"; +import { coder } from "../spl/token"; + +// Populates a given accounts context with PDAs and common missing accounts. +export class AccountsResolver> { + private _accountStore: AccountStore; + + constructor( + private _args: Array, + private _accounts: { [name: string]: PublicKey }, + private _provider: Provider, + private _programId: PublicKey, + private _idlIx: AllInstructions, + _accountNamespace: AccountNamespace + ) { + this._accountStore = new AccountStore(_provider, _accountNamespace); + } + + // Note: We serially resolve PDAs one by one rather than doing them + // in parallel because there can be dependencies between + // addresses. That is, one PDA can be used as a seed in another. + // + // TODO: PDAs need to be resolved in topological order. For now, we + // require the developer to simply list the accounts in the + // correct order. But in future work, we should create the + // dependency graph and resolve automatically. + // + public async resolve() { + for (let k = 0; k < this._idlIx.accounts.length; k += 1) { + // Cast is ok because only a non-nested IdlAccount can have a seeds + // cosntraint. + const accountDesc = this._idlIx.accounts[k] as IdlAccount; + const accountDescName = camelCase(accountDesc.name); + + // PDA derived from IDL seeds. + if (accountDesc.pda && accountDesc.pda.seeds.length > 0) { + if (this._accounts[accountDescName] === undefined) { + await this.autoPopulatePda(accountDesc); + continue; + } + } + + // Signers default to the provider. + if ( + accountDesc.isSigner && + this._accounts[accountDescName] === undefined + ) { + this._accounts[accountDescName] = this._provider.wallet.publicKey; + continue; + } + + // Common accounts are auto populated with magic names by convention. + switch (accountDescName) { + case "systemProgram": + if (this._accounts[accountDescName] === undefined) { + this._accounts[accountDescName] = SystemProgram.programId; + } + case "rent": + if (this._accounts[accountDescName] === undefined) { + this._accounts[accountDescName] = SYSVAR_RENT_PUBKEY; + } + case "tokenProgram": + if (this._accounts[accountDescName] === undefined) { + this._accounts[accountDescName] = TOKEN_PROGRAM_ID; + } + case "associatedTokenProgram": + if (this._accounts[accountDescName] === undefined) { + this._accounts[accountDescName] = ASSOCIATED_PROGRAM_ID; + } + } + } + } + + private async autoPopulatePda(accountDesc: IdlAccount) { + if (!accountDesc.pda || !accountDesc.pda.seeds) + throw new Error("Must have seeds"); + + const seeds: Buffer[] = await Promise.all( + accountDesc.pda.seeds.map((seedDesc: IdlSeed) => this.toBuffer(seedDesc)) + ); + + const programId = await this.parseProgramId(accountDesc); + const [pubkey] = await PublicKey.findProgramAddress(seeds, programId); + + this._accounts[camelCase(accountDesc.name)] = pubkey; + } + + private async parseProgramId(accountDesc: IdlAccount): Promise { + if (!accountDesc.pda?.programId) { + return this._programId; + } + switch (accountDesc.pda.programId.kind) { + case "const": + return new PublicKey( + this.toBufferConst(accountDesc.pda.programId.value) + ); + case "arg": + return this.argValue(accountDesc.pda.programId); + case "account": + return await this.accountValue(accountDesc.pda.programId); + default: + throw new Error( + `Unexpected program seed kind: ${accountDesc.pda.programId.kind}` + ); + } + } + + private async toBuffer(seedDesc: IdlSeed): Promise { + switch (seedDesc.kind) { + case "const": + return this.toBufferConst(seedDesc); + case "arg": + return await this.toBufferArg(seedDesc); + case "account": + return await this.toBufferAccount(seedDesc); + default: + throw new Error(`Unexpected seed kind: ${seedDesc.kind}`); + } + } + + private toBufferConst(seedDesc: IdlSeed): Buffer { + return this.toBufferValue(seedDesc.type, seedDesc.value); + } + + private async toBufferArg(seedDesc: IdlSeed): Promise { + const argValue = this.argValue(seedDesc); + return this.toBufferValue(seedDesc.type, argValue); + } + + private argValue(seedDesc: IdlSeed): any { + const seedArgName = camelCase(seedDesc.path.split(".")[0]); + + const idlArgPosition = this._idlIx.args.findIndex( + (argDesc: any) => argDesc.name === seedArgName + ); + if (idlArgPosition === -1) { + throw new Error(`Unable to find argument for seed: ${seedArgName}`); + } + + return this._args[idlArgPosition]; + } + + private async toBufferAccount(seedDesc: IdlSeed): Promise { + const accountValue = await this.accountValue(seedDesc); + return this.toBufferValue(seedDesc.type, accountValue); + } + + private async accountValue(seedDesc: IdlSeed): Promise { + const pathComponents = seedDesc.path.split("."); + + const fieldName = pathComponents[0]; + const fieldPubkey = this._accounts[camelCase(fieldName)]; + + // The seed is a pubkey of the account. + if (pathComponents.length === 1) { + return fieldPubkey; + } + + // The key is account data. + // + // Fetch and deserialize it. + const account = await this._accountStore.fetchAccount( + seedDesc.account, + fieldPubkey + ); + + // Dereference all fields in the path to get the field value + // used in the seed. + const fieldValue = this.parseAccountValue(account, pathComponents.slice(1)); + return fieldValue; + } + + private parseAccountValue(account: T, path: Array): any { + let accountField: any; + while (path.length > 0) { + accountField = account[camelCase(path[0])]; + path = path.slice(1); + } + return accountField; + } + + // Converts the given idl valaue into a Buffer. The values here must be + // primitives. E.g. no structs. + // + // TODO: add more types here as needed. + private toBufferValue(type: string | any, value: any): Buffer { + switch (type) { + case "u8": + return Buffer.from([value]); + case "u16": + let b = Buffer.alloc(2); + b.writeUInt16LE(value); + return b; + case "u32": + let buf = Buffer.alloc(4); + buf.writeUInt32LE(value); + return buf; + case "u64": + let bU64 = Buffer.alloc(8); + bU64.writeBigUInt64LE(BigInt(value)); + return bU64; + case "string": + return Buffer.from(utf8.encode(value)); + case "publicKey": + return value.toBuffer(); + default: + if (type.array) { + return Buffer.from(value); + } + throw new Error(`Unexpected seed type: ${type}`); + } + } +} + +// TODO: this should be configureable to avoid unnecessary requests. +export class AccountStore { + private _cache = new Map(); + + // todo: don't use the progrma use the account namespace. + constructor( + private _provider: Provider, + private _accounts: AccountNamespace + ) {} + + public async fetchAccount( + name: string, + publicKey: PublicKey + ): Promise { + const address = publicKey.toString(); + if (this._cache.get(address) === undefined) { + if (name === "TokenAccount") { + const accountInfo = await this._provider.connection.getAccountInfo( + publicKey + ); + if (accountInfo === null) { + throw new Error(`invalid account info for ${address}`); + } + const data = coder().accounts.decode("Token", accountInfo.data); + this._cache.set(address, data); + } else { + const account = this._accounts[camelCase(name)].fetch(publicKey); + this._cache.set(address, account); + } + } + return this._cache.get(address); + } +} diff --git a/ts/src/program/namespace/index.ts b/ts/src/program/namespace/index.ts index 365b3cf1..f7e994df 100644 --- a/ts/src/program/namespace/index.ts +++ b/ts/src/program/namespace/index.ts @@ -49,6 +49,10 @@ export default class NamespaceFactory { const idlErrors = parseIdlErrors(idl); + const account: AccountNamespace = idl.accounts + ? AccountFactory.build(idl, coder, programId, provider) + : ({} as AccountNamespace); + const state = StateFactory.build(idl, coder, programId, provider); idl.instructions.forEach(>(idlIx: I) => { @@ -69,10 +73,14 @@ export default class NamespaceFactory { idl ); const methodItem = MethodsBuilderFactory.build( + provider, + programId, + idlIx, ixItem, txItem, rpcItem, - simulateItem + simulateItem, + account ); const name = camelCase(idlIx.name); @@ -84,10 +92,6 @@ export default class NamespaceFactory { methods[name] = methodItem; }); - const account: AccountNamespace = idl.accounts - ? AccountFactory.build(idl, coder, programId, provider) - : ({} as AccountNamespace); - return [ rpc as RpcNamespace, instruction as InstructionNamespace, diff --git a/ts/src/program/namespace/methods.ts b/ts/src/program/namespace/methods.ts index 7085ede3..bf28812a 100644 --- a/ts/src/program/namespace/methods.ts +++ b/ts/src/program/namespace/methods.ts @@ -7,46 +7,78 @@ import { TransactionSignature, PublicKey, } from "@solana/web3.js"; -import { SimulateResponse } from "./simulate"; +import { SimulateResponse } from "./simulate.js"; import { TransactionFn } from "./transaction.js"; import { Idl } from "../../idl.js"; -import { - AllInstructions, - InstructionContextFn, - MakeInstructionsNamespace, -} from "./types"; -import { InstructionFn } from "./instruction"; -import { RpcFn } from "./rpc"; -import { SimulateFn } from "./simulate"; +import { AllInstructions, MethodsFn, MakeMethodsNamespace } from "./types.js"; +import { InstructionFn } from "./instruction.js"; +import { RpcFn } from "./rpc.js"; +import { SimulateFn } from "./simulate.js"; +import Provider from "../../provider.js"; +import { AccountNamespace } from "./account.js"; +import { AccountsResolver } from "../accounts-resolver.js"; + +export type MethodsNamespace< + IDL extends Idl = Idl, + I extends AllInstructions = AllInstructions +> = MakeMethodsNamespace; export class MethodsBuilderFactory { public static build>( + provider: Provider, + programId: PublicKey, + idlIx: AllInstructions, ixFn: InstructionFn, txFn: TransactionFn, rpcFn: RpcFn, - simulateFn: SimulateFn - ): MethodFn { - const request: MethodFn = (...args) => { - return new MethodsBuilder(args, ixFn, txFn, rpcFn, simulateFn); + simulateFn: SimulateFn, + accountNamespace: AccountNamespace + ): MethodsFn { + const request: MethodsFn = (...args) => { + return new MethodsBuilder( + args, + ixFn, + txFn, + rpcFn, + simulateFn, + provider, + programId, + idlIx, + accountNamespace + ); }; return request; } } export class MethodsBuilder> { - private _accounts: { [name: string]: PublicKey } = {}; + readonly _accounts: { [name: string]: PublicKey } = {}; private _remainingAccounts: Array = []; private _signers: Array = []; private _preInstructions: Array = []; private _postInstructions: Array = []; + private _accountsResolver: AccountsResolver; constructor( private _args: Array, private _ixFn: InstructionFn, private _txFn: TransactionFn, private _rpcFn: RpcFn, - private _simulateFn: SimulateFn - ) {} + private _simulateFn: SimulateFn, + _provider: Provider, + _programId: PublicKey, + _idlIx: AllInstructions, + _accountNamespace: AccountNamespace + ) { + this._accountsResolver = new AccountsResolver( + _args, + this._accounts, + _provider, + _programId, + _idlIx, + _accountNamespace + ); + } // TODO: don't use any. public accounts(accounts: any): MethodsBuilder { @@ -54,6 +86,11 @@ export class MethodsBuilder> { return this; } + public signers(signers: Array): MethodsBuilder { + this._signers = this._signers.concat(signers); + return this; + } + public remainingAccounts( accounts: Array ): MethodsBuilder { @@ -76,7 +113,7 @@ export class MethodsBuilder> { } public async rpc(options: ConfirmOptions): Promise { - await this.resolvePdas(); + await this._accountsResolver.resolve(); // @ts-ignore return this._rpcFn(...this._args, { accounts: this._accounts, @@ -91,7 +128,7 @@ export class MethodsBuilder> { public async simulate( options: ConfirmOptions ): Promise> { - await this.resolvePdas(); + await this._accountsResolver.resolve(); // @ts-ignore return this._simulateFn(...this._args, { accounts: this._accounts, @@ -104,7 +141,7 @@ export class MethodsBuilder> { } public async instruction(): Promise { - await this.resolvePdas(); + await this._accountsResolver.resolve(); // @ts-ignore return this._ixFn(...this._args, { accounts: this._accounts, @@ -116,7 +153,7 @@ export class MethodsBuilder> { } public async transaction(): Promise { - await this.resolvePdas(); + await this._accountsResolver.resolve(); // @ts-ignore return this._txFn(...this._args, { accounts: this._accounts, @@ -126,18 +163,4 @@ export class MethodsBuilder> { postInstructions: this._postInstructions, }); } - - private async resolvePdas() { - // TODO: resolve all PDAs and accounts not provided. - } } - -export type MethodsNamespace< - IDL extends Idl = Idl, - I extends AllInstructions = AllInstructions -> = MakeInstructionsNamespace; // TODO: don't use any. - -export type MethodFn< - IDL extends Idl = Idl, - I extends AllInstructions = AllInstructions -> = InstructionContextFn>; diff --git a/ts/src/program/namespace/types.ts b/ts/src/program/namespace/types.ts index 7d4a3b00..0da0d940 100644 --- a/ts/src/program/namespace/types.ts +++ b/ts/src/program/namespace/types.ts @@ -65,6 +65,10 @@ export type MakeInstructionsNamespace< Mk[M]; }; +export type MakeMethodsNamespace = { + [M in keyof InstructionMap]: MethodsFn[M], any>; +}; + export type InstructionContextFn< IDL extends Idl, I extends AllInstructions, @@ -79,6 +83,12 @@ export type InstructionContextFnArgs< Context> ]; +export type MethodsFn< + IDL extends Idl, + I extends IDL["instructions"][number], + Ret +> = (...args: ArgsTuple>) => Ret; + type TypeMap = { publicKey: PublicKey; bool: boolean; diff --git a/ts/src/spl/token.ts b/ts/src/spl/token.ts index eefc8367..cb661251 100644 --- a/ts/src/spl/token.ts +++ b/ts/src/spl/token.ts @@ -8,12 +8,11 @@ const TOKEN_PROGRAM_ID = new PublicKey( ); export function program(provider?: Provider): Program { - return new Program( - IDL, - TOKEN_PROGRAM_ID, - provider, - new SplTokenCoder(IDL) - ); + return new Program(IDL, TOKEN_PROGRAM_ID, provider, coder()); +} + +export function coder(): SplTokenCoder { + return new SplTokenCoder(IDL); } /** diff --git a/ts/src/utils/token.ts b/ts/src/utils/token.ts index c4e609ff..0f4d087e 100644 --- a/ts/src/utils/token.ts +++ b/ts/src/utils/token.ts @@ -1,9 +1,9 @@ import { PublicKey } from "@solana/web3.js"; -const TOKEN_PROGRAM_ID = new PublicKey( +export const TOKEN_PROGRAM_ID = new PublicKey( "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" ); -const ASSOCIATED_PROGRAM_ID = new PublicKey( +export const ASSOCIATED_PROGRAM_ID = new PublicKey( "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" );