lang, ts: automatic client side pda derivation (#1331)
This commit is contained in:
parent
1a2fd38451
commit
d8d720067d
|
@ -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/
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()?,
|
||||
|
|
|
@ -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)),
|
||||
|
|
|
@ -12,6 +12,7 @@ idl = []
|
|||
hash = []
|
||||
default = []
|
||||
anchor-debug = []
|
||||
seeds = []
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0"
|
||||
|
|
|
@ -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<_>>()
|
||||
|
|
|
@ -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<") {
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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,2 +1,3 @@
|
|||
**/target/types/*.ts
|
||||
cfo/deps/
|
||||
auction-house/deps/
|
|
@ -0,0 +1 @@
|
|||
Subproject commit fea2d89c2b17ee39fcf0ebaadb0317b9e97206f4
|
|
@ -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"
|
||||
|
|
|
@ -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],
|
||||
});
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
"misc",
|
||||
"multisig",
|
||||
"permissioned-markets",
|
||||
"pda-derivation",
|
||||
"pyth",
|
||||
"spl/token-proxy",
|
||||
"swap",
|
||||
|
|
|
@ -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"
|
|
@ -0,0 +1,4 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"programs/*"
|
||||
]
|
|
@ -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.");
|
||||
};
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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" }
|
|
@ -0,0 +1,2 @@
|
|||
[target.bpfel-unknown-unknown.dependencies.std]
|
||||
features = []
|
|
@ -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,
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"types": ["mocha", "chai"],
|
||||
"typeRoots": ["./node_modules/@types"],
|
||||
"lib": ["es2015"],
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"esModuleInterop": true
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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>,
|
||||
|
|
|
@ -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>>;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
|
||||
|
|
Loading…
Reference in New Issue