Prechádzať zdrojové kódy

lang, ts: automatic client side pda derivation (#1331)

Armani Ferrante 3 rokov pred
rodič
commit
d8d720067d

+ 2 - 0
.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/

+ 3 - 0
.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

+ 14 - 1
cli/src/config.rs

@@ -162,7 +162,11 @@ impl WithPath<Config> {
             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<T> std::ops::DerefMut for WithPath<T> {
 pub struct Config {
     pub anchor_version: Option<String>,
     pub solana_version: Option<String>,
+    pub features: FeaturesConfig,
     pub registry: RegistryConfig,
     pub provider: ProviderConfig,
     pub programs: ProgramsConfig,
@@ -251,6 +256,11 @@ pub struct Config {
     pub test: Option<Test>,
 }
 
+#[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<String>,
     solana_version: Option<String>,
+    features: Option<FeaturesConfig>,
     programs: Option<BTreeMap<String, BTreeMap<String, serde_json::Value>>>,
     registry: Option<RegistryConfig>,
     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()?,

+ 14 - 8
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<Idl> {
     serde_json::from_slice(&s[..]).map_err(Into::into)
 }
 
-fn extract_idl(file: &str) -> Result<Option<Idl>> {
+fn extract_idl(cfg: &WithPath<Config>, file: &str) -> Result<Option<Idl>> {
     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<String>, out_ts: Option<String>) -> Result<()> {
-    let idl = extract_idl(&file)?.ok_or_else(|| anyhow!("IDL not parsed"))?;
+fn idl_parse(
+    cfg_override: &ConfigOverride,
+    file: String,
+    out: Option<String>,
+    out_ts: Option<String>,
+) -> 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)),

+ 1 - 0
lang/syn/Cargo.toml

@@ -12,6 +12,7 @@ idl = []
 hash = []
 default = []
 anchor-debug = []
+seeds = []
 
 [dependencies]
 proc-macro2 = "1.0"

+ 13 - 5
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<Path>, version: String) -> Result<Option<Idl>> {
+pub fn parse(
+    filename: impl AsRef<Path>,
+    version: String,
+    seeds_feature: bool,
+) -> Result<Option<Idl>> {
     let ctx = CrateContext::parse(filename)?;
 
     let program_mod = match parse_program_mod(&ctx) {
@@ -52,7 +56,8 @@ pub fn parse(filename: impl AsRef<Path>, version: String) -> Result<Option<Idl>>
                                     .collect::<Vec<_>>();
                                 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<Path>, version: String) -> Result<Option<Idl>>
                         })
                         .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<Path>, version: String) -> Result<Option<Idl>>
                 .collect::<Vec<_>>();
             // 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<String, AccountsStruct>,
+    seeds_feature: bool,
 ) -> Vec<IdlAccountItem> {
     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::<Vec<_>>()

+ 49 - 1
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<IdlPda>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
+#[serde(rename_all = "camelCase")]
+pub struct IdlPda {
+    pub seeds: Vec<IdlSeed>,
+    #[serde(skip_serializing_if = "Option::is_none", default)]
+    pub program_id: Option<IdlSeed>,
+}
+
+#[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<T>` type is used.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub account: Option<String>,
+    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<u8>" => 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<") {

+ 322 - 0
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<IdlPda> {
+    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<String, String>,
+    // Constants available in the crate.
+    const_names: Vec<String>,
+    // All field names of the accounts in the accounts context.
+    account_field_names: Vec<String>,
+}
+
+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<String> = 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<IdlPda> {
+        // Extract the idl seed types from the constraints.
+        let seeds = seeds_grp
+            .seeds
+            .iter()
+            .map(|s| self.parse_seed(s))
+            .collect::<Option<Vec<_>>>()?;
+
+        // 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<IdlSeed> {
+        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<IdlSeed> {
+        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<IdlSeed> {
+        // 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<IdlSeed> {
+        // 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<IdlSeed> {
+        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<String>);
+
+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<SeedPath> {
+    // 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)
+}

+ 39 - 1
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<HashMap<String, String>> {
+        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<String> {
+        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<String> {
+        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<Expr, Token![,]>,
     pub bump: Option<Expr>,         // None => bump was given without a target.
-    pub program_seed: Option<Expr>, // None => use the current program's program_id
+    pub program_seed: Option<Expr>, // None => use the current program's program_id.
 }
 
 #[derive(Debug, Clone)]

+ 1 - 0
tests/.prettierignore

@@ -1,2 +1,3 @@
 **/target/types/*.ts
 cfo/deps/
+auction-house/deps/

+ 1 - 0
tests/auction-house

@@ -0,0 +1 @@
+Subproject commit fea2d89c2b17ee39fcf0ebaadb0317b9e97206f4

+ 1 - 0
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"

+ 9 - 5
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],
     });

+ 1 - 0
tests/package.json

@@ -20,6 +20,7 @@
     "misc",
     "multisig",
     "permissioned-markets",
+    "pda-derivation",
     "pyth",
     "spl/token-proxy",
     "swap",

+ 15 - 0
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"

+ 4 - 0
tests/pda-derivation/Cargo.toml

@@ -0,0 +1,4 @@
+[workspace]
+members = [
+    "programs/*"
+]

+ 22 - 0
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<void> {
+    return new Promise((resolve) => {
+      setTimeout(() => {
+        console.log(exampleString);
+        resolve();
+      }, 2000);
+    });
+  }
+
+  await deployAsync("Typescript migration example complete.");
+};

+ 19 - 0
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"
+  }
+}

+ 18 - 0
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" }

+ 2 - 0
tests/pda-derivation/programs/pda-derivation/Xargo.toml

@@ -0,0 +1,2 @@
+[target.bpfel-unknown-unknown.dependencies.std]
+features = []

+ 83 - 0
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<InitBase>, 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<InitMyAccount>, 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,
+}

+ 34 - 0
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();
+  });
+});

+ 10 - 0
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
+  }
+}

+ 5 - 10
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<u64 | null> {
   return new WrappedLayout(
@@ -74,30 +75,24 @@ export class COptionLayout<T> extends Layout<T | null> {
 
   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;
   }
 }
 

+ 8 - 0
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;

+ 254 - 0
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<IDL extends Idl, I extends AllInstructions<IDL>> {
+  private _accountStore: AccountStore<IDL>;
+
+  constructor(
+    private _args: Array<any>,
+    private _accounts: { [name: string]: PublicKey },
+    private _provider: Provider,
+    private _programId: PublicKey,
+    private _idlIx: AllInstructions<IDL>,
+    _accountNamespace: AccountNamespace<IDL>
+  ) {
+    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<PublicKey> {
+    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<Buffer> {
+    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<Buffer> {
+    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<Buffer> {
+    const accountValue = await this.accountValue(seedDesc);
+    return this.toBufferValue(seedDesc.type, accountValue);
+  }
+
+  private async accountValue(seedDesc: IdlSeed): Promise<any> {
+    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<T = any>(account: T, path: Array<string>): 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<IDL extends Idl> {
+  private _cache = new Map<string, any>();
+
+  // todo: don't use the progrma use the account namespace.
+  constructor(
+    private _provider: Provider,
+    private _accounts: AccountNamespace<IDL>
+  ) {}
+
+  public async fetchAccount<T = any>(
+    name: string,
+    publicKey: PublicKey
+  ): Promise<T> {
+    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);
+  }
+}

+ 9 - 5
ts/src/program/namespace/index.ts

@@ -49,6 +49,10 @@ export default class NamespaceFactory {
 
     const idlErrors = parseIdlErrors(idl);
 
+    const account: AccountNamespace<IDL> = idl.accounts
+      ? AccountFactory.build(idl, coder, programId, provider)
+      : ({} as AccountNamespace<IDL>);
+
     const state = StateFactory.build(idl, coder, programId, provider);
 
     idl.instructions.forEach(<I extends AllInstructions<IDL>>(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> = idl.accounts
-      ? AccountFactory.build(idl, coder, programId, provider)
-      : ({} as AccountNamespace<IDL>);
-
     return [
       rpc as RpcNamespace<IDL>,
       instruction as InstructionNamespace<IDL>,

+ 57 - 34
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<IDL> = AllInstructions<IDL>
+> = MakeMethodsNamespace<IDL, I>;
 
 export class MethodsBuilderFactory {
   public static build<IDL extends Idl, I extends AllInstructions<IDL>>(
+    provider: Provider,
+    programId: PublicKey,
+    idlIx: AllInstructions<IDL>,
     ixFn: InstructionFn<IDL>,
     txFn: TransactionFn<IDL>,
     rpcFn: RpcFn<IDL>,
-    simulateFn: SimulateFn<IDL>
-  ): MethodFn {
-    const request: MethodFn<IDL, I> = (...args) => {
-      return new MethodsBuilder(args, ixFn, txFn, rpcFn, simulateFn);
+    simulateFn: SimulateFn<IDL>,
+    accountNamespace: AccountNamespace<IDL>
+  ): MethodsFn<IDL, I, any> {
+    const request: MethodsFn<IDL, I, any> = (...args) => {
+      return new MethodsBuilder(
+        args,
+        ixFn,
+        txFn,
+        rpcFn,
+        simulateFn,
+        provider,
+        programId,
+        idlIx,
+        accountNamespace
+      );
     };
     return request;
   }
 }
 
 export class MethodsBuilder<IDL extends Idl, I extends AllInstructions<IDL>> {
-  private _accounts: { [name: string]: PublicKey } = {};
+  readonly _accounts: { [name: string]: PublicKey } = {};
   private _remainingAccounts: Array<AccountMeta> = [];
   private _signers: Array<Signer> = [];
   private _preInstructions: Array<TransactionInstruction> = [];
   private _postInstructions: Array<TransactionInstruction> = [];
+  private _accountsResolver: AccountsResolver<IDL, I>;
 
   constructor(
     private _args: Array<any>,
     private _ixFn: InstructionFn<IDL>,
     private _txFn: TransactionFn<IDL>,
     private _rpcFn: RpcFn<IDL>,
-    private _simulateFn: SimulateFn<IDL>
-  ) {}
+    private _simulateFn: SimulateFn<IDL>,
+    _provider: Provider,
+    _programId: PublicKey,
+    _idlIx: AllInstructions<IDL>,
+    _accountNamespace: AccountNamespace<IDL>
+  ) {
+    this._accountsResolver = new AccountsResolver(
+      _args,
+      this._accounts,
+      _provider,
+      _programId,
+      _idlIx,
+      _accountNamespace
+    );
+  }
 
   // TODO: don't use any.
   public accounts(accounts: any): MethodsBuilder<IDL, I> {
@@ -54,6 +86,11 @@ export class MethodsBuilder<IDL extends Idl, I extends AllInstructions<IDL>> {
     return this;
   }
 
+  public signers(signers: Array<Signer>): MethodsBuilder<IDL, I> {
+    this._signers = this._signers.concat(signers);
+    return this;
+  }
+
   public remainingAccounts(
     accounts: Array<AccountMeta>
   ): MethodsBuilder<IDL, I> {
@@ -76,7 +113,7 @@ export class MethodsBuilder<IDL extends Idl, I extends AllInstructions<IDL>> {
   }
 
   public async rpc(options: ConfirmOptions): Promise<TransactionSignature> {
-    await this.resolvePdas();
+    await this._accountsResolver.resolve();
     // @ts-ignore
     return this._rpcFn(...this._args, {
       accounts: this._accounts,
@@ -91,7 +128,7 @@ export class MethodsBuilder<IDL extends Idl, I extends AllInstructions<IDL>> {
   public async simulate(
     options: ConfirmOptions
   ): Promise<SimulateResponse<any, any>> {
-    await this.resolvePdas();
+    await this._accountsResolver.resolve();
     // @ts-ignore
     return this._simulateFn(...this._args, {
       accounts: this._accounts,
@@ -104,7 +141,7 @@ export class MethodsBuilder<IDL extends Idl, I extends AllInstructions<IDL>> {
   }
 
   public async instruction(): Promise<TransactionInstruction> {
-    await this.resolvePdas();
+    await this._accountsResolver.resolve();
     // @ts-ignore
     return this._ixFn(...this._args, {
       accounts: this._accounts,
@@ -116,7 +153,7 @@ export class MethodsBuilder<IDL extends Idl, I extends AllInstructions<IDL>> {
   }
 
   public async transaction(): Promise<Transaction> {
-    await this.resolvePdas();
+    await this._accountsResolver.resolve();
     // @ts-ignore
     return this._txFn(...this._args, {
       accounts: this._accounts,
@@ -126,18 +163,4 @@ export class MethodsBuilder<IDL extends Idl, I extends AllInstructions<IDL>> {
       postInstructions: this._postInstructions,
     });
   }
-
-  private async resolvePdas() {
-    // TODO: resolve all PDAs and accounts not provided.
-  }
 }
-
-export type MethodsNamespace<
-  IDL extends Idl = Idl,
-  I extends AllInstructions<IDL> = AllInstructions<IDL>
-> = MakeInstructionsNamespace<IDL, I, any>; // TODO: don't use any.
-
-export type MethodFn<
-  IDL extends Idl = Idl,
-  I extends AllInstructions<IDL> = AllInstructions<IDL>
-> = InstructionContextFn<IDL, I, MethodsBuilder<IDL, I>>;

+ 10 - 0
ts/src/program/namespace/types.ts

@@ -65,6 +65,10 @@ export type MakeInstructionsNamespace<
     Mk[M];
 };
 
+export type MakeMethodsNamespace<IDL extends Idl, I extends IdlInstruction> = {
+  [M in keyof InstructionMap<I>]: MethodsFn<IDL, InstructionMap<I>[M], any>;
+};
+
 export type InstructionContextFn<
   IDL extends Idl,
   I extends AllInstructions<IDL>,
@@ -79,6 +83,12 @@ export type InstructionContextFnArgs<
   Context<Accounts<I["accounts"][number]>>
 ];
 
+export type MethodsFn<
+  IDL extends Idl,
+  I extends IDL["instructions"][number],
+  Ret
+> = (...args: ArgsTuple<I["args"], IdlTypes<IDL>>) => Ret;
+
 type TypeMap = {
   publicKey: PublicKey;
   bool: boolean;

+ 5 - 6
ts/src/spl/token.ts

@@ -8,12 +8,11 @@ const TOKEN_PROGRAM_ID = new PublicKey(
 );
 
 export function program(provider?: Provider): Program<SplToken> {
-  return new Program<SplToken>(
-    IDL,
-    TOKEN_PROGRAM_ID,
-    provider,
-    new SplTokenCoder(IDL)
-  );
+  return new Program<SplToken>(IDL, TOKEN_PROGRAM_ID, provider, coder());
+}
+
+export function coder(): SplTokenCoder {
+  return new SplTokenCoder(IDL);
 }
 
 /**

+ 2 - 2
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"
 );