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
|
path: tests/ido-pool
|
||||||
- cmd: cd tests/cfo && anchor run test-with-build
|
- cmd: cd tests/cfo && anchor run test-with-build
|
||||||
path: tests/cfo
|
path: tests/cfo
|
||||||
|
- cmd: cd tests/auction-house && yarn && anchor test
|
||||||
|
path: tests/auction-house
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: ./.github/actions/setup/
|
- uses: ./.github/actions/setup/
|
||||||
|
|
|
@ -13,3 +13,6 @@
|
||||||
[submodule "examples/permissioned-markets/deps/serum-dex"]
|
[submodule "examples/permissioned-markets/deps/serum-dex"]
|
||||||
path = tests/permissioned-markets/deps/serum-dex
|
path = tests/permissioned-markets/deps/serum-dex
|
||||||
url = https://github.com/project-serum/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 cargo = Manifest::from_path(&path.join("Cargo.toml"))?;
|
||||||
let lib_name = cargo.lib_name()?;
|
let lib_name = cargo.lib_name()?;
|
||||||
let version = cargo.version();
|
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 {
|
r.push(Program {
|
||||||
lib_name,
|
lib_name,
|
||||||
path,
|
path,
|
||||||
|
@ -243,6 +247,7 @@ impl<T> std::ops::DerefMut for WithPath<T> {
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub anchor_version: Option<String>,
|
pub anchor_version: Option<String>,
|
||||||
pub solana_version: Option<String>,
|
pub solana_version: Option<String>,
|
||||||
|
pub features: FeaturesConfig,
|
||||||
pub registry: RegistryConfig,
|
pub registry: RegistryConfig,
|
||||||
pub provider: ProviderConfig,
|
pub provider: ProviderConfig,
|
||||||
pub programs: ProgramsConfig,
|
pub programs: ProgramsConfig,
|
||||||
|
@ -251,6 +256,11 @@ pub struct Config {
|
||||||
pub test: Option<Test>,
|
pub test: Option<Test>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct FeaturesConfig {
|
||||||
|
pub seeds: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct RegistryConfig {
|
pub struct RegistryConfig {
|
||||||
pub url: String,
|
pub url: String,
|
||||||
|
@ -362,6 +372,7 @@ impl Config {
|
||||||
struct _Config {
|
struct _Config {
|
||||||
anchor_version: Option<String>,
|
anchor_version: Option<String>,
|
||||||
solana_version: Option<String>,
|
solana_version: Option<String>,
|
||||||
|
features: Option<FeaturesConfig>,
|
||||||
programs: Option<BTreeMap<String, BTreeMap<String, serde_json::Value>>>,
|
programs: Option<BTreeMap<String, BTreeMap<String, serde_json::Value>>>,
|
||||||
registry: Option<RegistryConfig>,
|
registry: Option<RegistryConfig>,
|
||||||
provider: Provider,
|
provider: Provider,
|
||||||
|
@ -389,6 +400,7 @@ impl ToString for Config {
|
||||||
let cfg = _Config {
|
let cfg = _Config {
|
||||||
anchor_version: self.anchor_version.clone(),
|
anchor_version: self.anchor_version.clone(),
|
||||||
solana_version: self.solana_version.clone(),
|
solana_version: self.solana_version.clone(),
|
||||||
|
features: Some(self.features.clone()),
|
||||||
registry: Some(self.registry.clone()),
|
registry: Some(self.registry.clone()),
|
||||||
provider: Provider {
|
provider: Provider {
|
||||||
cluster: format!("{}", self.provider.cluster),
|
cluster: format!("{}", self.provider.cluster),
|
||||||
|
@ -417,6 +429,7 @@ impl FromStr for Config {
|
||||||
Ok(Config {
|
Ok(Config {
|
||||||
anchor_version: cfg.anchor_version,
|
anchor_version: cfg.anchor_version,
|
||||||
solana_version: cfg.solana_version,
|
solana_version: cfg.solana_version,
|
||||||
|
features: cfg.features.unwrap_or_default(),
|
||||||
registry: cfg.registry.unwrap_or_default(),
|
registry: cfg.registry.unwrap_or_default(),
|
||||||
provider: ProviderConfig {
|
provider: ProviderConfig {
|
||||||
cluster: cfg.provider.cluster.parse()?,
|
cluster: cfg.provider.cluster.parse()?,
|
||||||
|
|
|
@ -861,7 +861,7 @@ fn build_cwd_verifiable(
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// Build the idl.
|
// Build the idl.
|
||||||
println!("Extracting 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.
|
// Write out the JSON file.
|
||||||
println!("Writing the IDL file");
|
println!("Writing the IDL file");
|
||||||
let out_file = workspace_dir.join(format!("target/idl/{}.json", idl.name));
|
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.
|
// 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.
|
// JSON out path.
|
||||||
let out = match idl_out {
|
let out = match idl_out {
|
||||||
None => PathBuf::from(".").join(&idl.name).with_extension("json"),
|
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).
|
// 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 {
|
if bin_ver.state != BinVerificationState::Buffer {
|
||||||
let deployed_idl = fetch_idl(cfg_override, program_id)?;
|
let deployed_idl = fetch_idl(cfg_override, program_id)?;
|
||||||
if local_idl != deployed_idl {
|
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)
|
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 file = shellexpand::tilde(file);
|
||||||
let manifest_from_path = std::env::current_dir()?.join(PathBuf::from(&*file).parent().unwrap());
|
let manifest_from_path = std::env::current_dir()?.join(PathBuf::from(&*file).parent().unwrap());
|
||||||
let cargo = Manifest::discover_from_path(manifest_from_path)?
|
let cargo = Manifest::discover_from_path(manifest_from_path)?
|
||||||
.ok_or_else(|| anyhow!("Cargo.toml not found"))?;
|
.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<()> {
|
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),
|
} => idl_set_authority(cfg_override, program_id, address, new_authority),
|
||||||
IdlCommand::EraseAuthority { program_id } => idl_erase_authority(cfg_override, program_id),
|
IdlCommand::EraseAuthority { program_id } => idl_erase_authority(cfg_override, program_id),
|
||||||
IdlCommand::Authority { program_id } => idl_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),
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn idl_parse(file: String, out: Option<String>, out_ts: Option<String>) -> Result<()> {
|
fn idl_parse(
|
||||||
let idl = extract_idl(&file)?.ok_or_else(|| anyhow!("IDL not parsed"))?;
|
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 {
|
let out = match out {
|
||||||
None => OutFile::Stdout,
|
None => OutFile::Stdout,
|
||||||
Some(out) => OutFile::File(PathBuf::from(out)),
|
Some(out) => OutFile::File(PathBuf::from(out)),
|
||||||
|
|
|
@ -12,6 +12,7 @@ idl = []
|
||||||
hash = []
|
hash = []
|
||||||
default = []
|
default = []
|
||||||
anchor-debug = []
|
anchor-debug = []
|
||||||
|
seeds = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
proc-macro2 = "1.0"
|
proc-macro2 = "1.0"
|
||||||
|
|
|
@ -14,7 +14,11 @@ const DERIVE_NAME: &str = "Accounts";
|
||||||
const ERROR_CODE_OFFSET: u32 = 6000;
|
const ERROR_CODE_OFFSET: u32 = 6000;
|
||||||
|
|
||||||
// Parse an entire interface file.
|
// 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 ctx = CrateContext::parse(filename)?;
|
||||||
|
|
||||||
let program_mod = match parse_program_mod(&ctx) {
|
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<_>>();
|
.collect::<Vec<_>>();
|
||||||
let accounts_strct =
|
let accounts_strct =
|
||||||
accs.get(&method.anchor_ident.to_string()).unwrap();
|
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 {
|
IdlInstruction {
|
||||||
name,
|
name,
|
||||||
accounts,
|
accounts,
|
||||||
|
@ -91,7 +96,7 @@ pub fn parse(filename: impl AsRef<Path>, version: String) -> Result<Option<Idl>>
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let accounts_strct = accs.get(&anchor_ident.to_string()).unwrap();
|
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 {
|
IdlInstruction {
|
||||||
name,
|
name,
|
||||||
accounts,
|
accounts,
|
||||||
|
@ -159,7 +164,7 @@ pub fn parse(filename: impl AsRef<Path>, version: String) -> Result<Option<Idl>>
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
// todo: don't unwrap
|
// todo: don't unwrap
|
||||||
let accounts_strct = accs.get(&ix.anchor_ident.to_string()).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 {
|
IdlInstruction {
|
||||||
name: ix.ident.to_string().to_mixed_case(),
|
name: ix.ident.to_string().to_mixed_case(),
|
||||||
accounts,
|
accounts,
|
||||||
|
@ -494,8 +499,10 @@ fn to_idl_type(f: &syn::Field) -> IdlType {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn idl_accounts(
|
fn idl_accounts(
|
||||||
|
ctx: &CrateContext,
|
||||||
accounts: &AccountsStruct,
|
accounts: &AccountsStruct,
|
||||||
global_accs: &HashMap<String, AccountsStruct>,
|
global_accs: &HashMap<String, AccountsStruct>,
|
||||||
|
seeds_feature: bool,
|
||||||
) -> Vec<IdlAccountItem> {
|
) -> Vec<IdlAccountItem> {
|
||||||
accounts
|
accounts
|
||||||
.fields
|
.fields
|
||||||
|
@ -505,7 +512,7 @@ fn idl_accounts(
|
||||||
let accs_strct = global_accs
|
let accs_strct = global_accs
|
||||||
.get(&comp_f.symbol)
|
.get(&comp_f.symbol)
|
||||||
.expect("Could not resolve Accounts 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 {
|
IdlAccountItem::IdlAccounts(IdlAccounts {
|
||||||
name: comp_f.ident.to_string().to_mixed_case(),
|
name: comp_f.ident.to_string().to_mixed_case(),
|
||||||
accounts,
|
accounts,
|
||||||
|
@ -518,6 +525,7 @@ fn idl_accounts(
|
||||||
Ty::Signer => true,
|
Ty::Signer => true,
|
||||||
_ => acc.constraints.is_signer(),
|
_ => acc.constraints.is_signer(),
|
||||||
},
|
},
|
||||||
|
pda: pda::parse(ctx, accounts, acc, seeds_feature),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
|
|
|
@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value as JsonValue;
|
use serde_json::Value as JsonValue;
|
||||||
|
|
||||||
pub mod file;
|
pub mod file;
|
||||||
|
pub mod pda;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
pub struct Idl {
|
pub struct Idl {
|
||||||
|
@ -66,6 +67,52 @@ pub struct IdlAccount {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub is_mut: bool,
|
pub is_mut: bool,
|
||||||
pub is_signer: 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)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
@ -157,6 +204,7 @@ impl std::str::FromStr for IdlType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.retain(|c| !c.is_whitespace());
|
s.retain(|c| !c.is_whitespace());
|
||||||
|
|
||||||
let r = match s.as_str() {
|
let r = match s.as_str() {
|
||||||
"bool" => IdlType::Bool,
|
"bool" => IdlType::Bool,
|
||||||
"u8" => IdlType::U8,
|
"u8" => IdlType::U8,
|
||||||
|
@ -170,7 +218,7 @@ impl std::str::FromStr for IdlType {
|
||||||
"u128" => IdlType::U128,
|
"u128" => IdlType::U128,
|
||||||
"i128" => IdlType::I128,
|
"i128" => IdlType::I128,
|
||||||
"Vec<u8>" => IdlType::Bytes,
|
"Vec<u8>" => IdlType::Bytes,
|
||||||
"String" => IdlType::String,
|
"String" | "&str" => IdlType::String,
|
||||||
"Pubkey" => IdlType::PublicKey,
|
"Pubkey" => IdlType::PublicKey,
|
||||||
_ => match s.to_string().strip_prefix("Option<") {
|
_ => match s.to_string().strip_prefix("Option<") {
|
||||||
None => match s.to_string().strip_prefix("Vec<") {
|
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 proc_macro2::{Span, TokenStream};
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
use quote::ToTokens;
|
use quote::ToTokens;
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use syn::ext::IdentExt;
|
use syn::ext::IdentExt;
|
||||||
use syn::parse::{Error as ParseError, Parse, ParseStream, Result as ParseResult};
|
use syn::parse::{Error as ParseError, Parse, ParseStream, Result as ParseResult};
|
||||||
|
@ -145,6 +146,30 @@ impl AccountsStruct {
|
||||||
instruction_api,
|
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)]
|
#[allow(clippy::large_enum_variant)]
|
||||||
|
@ -161,6 +186,19 @@ impl AccountField {
|
||||||
AccountField::CompositeField(c_field) => &c_field.ident,
|
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)]
|
#[derive(Debug)]
|
||||||
|
@ -689,7 +727,7 @@ pub struct ConstraintSeedsGroup {
|
||||||
pub is_init: bool,
|
pub is_init: bool,
|
||||||
pub seeds: Punctuated<Expr, Token![,]>,
|
pub seeds: Punctuated<Expr, Token![,]>,
|
||||||
pub bump: Option<Expr>, // None => bump was given without a target.
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
**/target/types/*.ts
|
**/target/types/*.ts
|
||||||
cfo/deps/
|
cfo/deps/
|
||||||
|
auction-house/deps/
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit fea2d89c2b17ee39fcf0ebaadb0317b9e97206f4
|
|
@ -18,3 +18,4 @@ default = []
|
||||||
anchor-lang = { path = "../../../../lang" }
|
anchor-lang = { path = "../../../../lang" }
|
||||||
anchor-spl = { path = "../../../../spl" }
|
anchor-spl = { path = "../../../../spl" }
|
||||||
misc2 = { path = "../misc2", features = ["cpi"] }
|
misc2 = { path = "../misc2", features = ["cpi"] }
|
||||||
|
spl-associated-token-account = "=1.0.3"
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
const anchor = require("@project-serum/anchor");
|
const anchor = require("@project-serum/anchor");
|
||||||
const PublicKey = anchor.web3.PublicKey;
|
|
||||||
const assert = require("assert");
|
const assert = require("assert");
|
||||||
const {
|
const {
|
||||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||||
|
@ -7,7 +6,12 @@ const {
|
||||||
Token,
|
Token,
|
||||||
} = require("@solana/spl-token");
|
} = require("@solana/spl-token");
|
||||||
const miscIdl = require("../target/idl/misc.json");
|
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;
|
const utf8 = anchor.utils.bytes.utf8;
|
||||||
|
|
||||||
describe("misc", () => {
|
describe("misc", () => {
|
||||||
|
@ -1221,14 +1225,14 @@ describe("misc", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("init_if_needed throws if associated token exists but has the wrong owner", async () => {
|
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({
|
await program.rpc.testInitMint({
|
||||||
accounts: {
|
accounts: {
|
||||||
mint: mint.publicKey,
|
mint: mint.publicKey,
|
||||||
payer: program.provider.wallet.publicKey,
|
payer: program.provider.wallet.publicKey,
|
||||||
systemProgram: anchor.web3.SystemProgram.programId,
|
systemProgram: SystemProgram.programId,
|
||||||
tokenProgram: TOKEN_PROGRAM_ID,
|
tokenProgram: TOKEN_PROGRAM_ID,
|
||||||
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
|
rent: SYSVAR_RENT_PUBKEY,
|
||||||
},
|
},
|
||||||
signers: [mint],
|
signers: [mint],
|
||||||
});
|
});
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
"misc",
|
"misc",
|
||||||
"multisig",
|
"multisig",
|
||||||
"permissioned-markets",
|
"permissioned-markets",
|
||||||
|
"pda-derivation",
|
||||||
"pyth",
|
"pyth",
|
||||||
"spl/token-proxy",
|
"spl/token-proxy",
|
||||||
"swap",
|
"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 * as BufferLayout from "buffer-layout";
|
||||||
import { Layout } from "buffer-layout";
|
import { Layout } from "buffer-layout";
|
||||||
import { PublicKey } from "@solana/web3.js";
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import * as utils from "../../utils";
|
||||||
|
|
||||||
export function uint64(property?: string): Layout<u64 | null> {
|
export function uint64(property?: string): Layout<u64 | null> {
|
||||||
return new WrappedLayout(
|
return new WrappedLayout(
|
||||||
|
@ -74,30 +75,24 @@ export class COptionLayout<T> extends Layout<T | null> {
|
||||||
|
|
||||||
encode(src: T | null, b: Buffer, offset = 0): number {
|
encode(src: T | null, b: Buffer, offset = 0): number {
|
||||||
if (src === null || src === undefined) {
|
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);
|
this.discriminator.encode(1, b, offset);
|
||||||
return this.layout.encode(src, b, offset + 4) + 4;
|
return this.layout.encode(src, b, offset + 4) + 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
decode(b: Buffer, offset = 0): T | null {
|
decode(b: Buffer, offset = 0): T | null {
|
||||||
const discriminator = b[offset];
|
const discriminator = this.discriminator.decode(b, offset);
|
||||||
if (discriminator === 0) {
|
if (discriminator === 0) {
|
||||||
return null;
|
return null;
|
||||||
} else if (discriminator === 1) {
|
} else if (discriminator === 1) {
|
||||||
return this.layout.decode(b, offset + 4);
|
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 {
|
getSpan(b: Buffer, offset = 0): number {
|
||||||
const discriminator = b[offset];
|
return this.layout.getSpan(b, offset + 4) + 4;
|
||||||
if (discriminator === 0) {
|
|
||||||
return 1;
|
|
||||||
} else if (discriminator === 1) {
|
|
||||||
return this.layout.getSpan(b, offset + 4) + 4;
|
|
||||||
}
|
|
||||||
throw new Error("Invalid option " + this.property);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,8 +50,16 @@ export type IdlAccount = {
|
||||||
name: string;
|
name: string;
|
||||||
isMut: boolean;
|
isMut: boolean;
|
||||||
isSigner: boolean;
|
isSigner: boolean;
|
||||||
|
pda?: IdlPda;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type IdlPda = {
|
||||||
|
seeds: IdlSeed[];
|
||||||
|
programId?: IdlSeed;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IdlSeed = any; // TODO
|
||||||
|
|
||||||
// A nested/recursive version of IdlAccount.
|
// A nested/recursive version of IdlAccount.
|
||||||
export type IdlAccounts = {
|
export type IdlAccounts = {
|
||||||
name: string;
|
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 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);
|
const state = StateFactory.build(idl, coder, programId, provider);
|
||||||
|
|
||||||
idl.instructions.forEach(<I extends AllInstructions<IDL>>(idlIx: I) => {
|
idl.instructions.forEach(<I extends AllInstructions<IDL>>(idlIx: I) => {
|
||||||
|
@ -69,10 +73,14 @@ export default class NamespaceFactory {
|
||||||
idl
|
idl
|
||||||
);
|
);
|
||||||
const methodItem = MethodsBuilderFactory.build(
|
const methodItem = MethodsBuilderFactory.build(
|
||||||
|
provider,
|
||||||
|
programId,
|
||||||
|
idlIx,
|
||||||
ixItem,
|
ixItem,
|
||||||
txItem,
|
txItem,
|
||||||
rpcItem,
|
rpcItem,
|
||||||
simulateItem
|
simulateItem,
|
||||||
|
account
|
||||||
);
|
);
|
||||||
|
|
||||||
const name = camelCase(idlIx.name);
|
const name = camelCase(idlIx.name);
|
||||||
|
@ -84,10 +92,6 @@ export default class NamespaceFactory {
|
||||||
methods[name] = methodItem;
|
methods[name] = methodItem;
|
||||||
});
|
});
|
||||||
|
|
||||||
const account: AccountNamespace<IDL> = idl.accounts
|
|
||||||
? AccountFactory.build(idl, coder, programId, provider)
|
|
||||||
: ({} as AccountNamespace<IDL>);
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
rpc as RpcNamespace<IDL>,
|
rpc as RpcNamespace<IDL>,
|
||||||
instruction as InstructionNamespace<IDL>,
|
instruction as InstructionNamespace<IDL>,
|
||||||
|
|
|
@ -7,46 +7,78 @@ import {
|
||||||
TransactionSignature,
|
TransactionSignature,
|
||||||
PublicKey,
|
PublicKey,
|
||||||
} from "@solana/web3.js";
|
} from "@solana/web3.js";
|
||||||
import { SimulateResponse } from "./simulate";
|
import { SimulateResponse } from "./simulate.js";
|
||||||
import { TransactionFn } from "./transaction.js";
|
import { TransactionFn } from "./transaction.js";
|
||||||
import { Idl } from "../../idl.js";
|
import { Idl } from "../../idl.js";
|
||||||
import {
|
import { AllInstructions, MethodsFn, MakeMethodsNamespace } from "./types.js";
|
||||||
AllInstructions,
|
import { InstructionFn } from "./instruction.js";
|
||||||
InstructionContextFn,
|
import { RpcFn } from "./rpc.js";
|
||||||
MakeInstructionsNamespace,
|
import { SimulateFn } from "./simulate.js";
|
||||||
} from "./types";
|
import Provider from "../../provider.js";
|
||||||
import { InstructionFn } from "./instruction";
|
import { AccountNamespace } from "./account.js";
|
||||||
import { RpcFn } from "./rpc";
|
import { AccountsResolver } from "../accounts-resolver.js";
|
||||||
import { SimulateFn } from "./simulate";
|
|
||||||
|
export type MethodsNamespace<
|
||||||
|
IDL extends Idl = Idl,
|
||||||
|
I extends AllInstructions<IDL> = AllInstructions<IDL>
|
||||||
|
> = MakeMethodsNamespace<IDL, I>;
|
||||||
|
|
||||||
export class MethodsBuilderFactory {
|
export class MethodsBuilderFactory {
|
||||||
public static build<IDL extends Idl, I extends AllInstructions<IDL>>(
|
public static build<IDL extends Idl, I extends AllInstructions<IDL>>(
|
||||||
|
provider: Provider,
|
||||||
|
programId: PublicKey,
|
||||||
|
idlIx: AllInstructions<IDL>,
|
||||||
ixFn: InstructionFn<IDL>,
|
ixFn: InstructionFn<IDL>,
|
||||||
txFn: TransactionFn<IDL>,
|
txFn: TransactionFn<IDL>,
|
||||||
rpcFn: RpcFn<IDL>,
|
rpcFn: RpcFn<IDL>,
|
||||||
simulateFn: SimulateFn<IDL>
|
simulateFn: SimulateFn<IDL>,
|
||||||
): MethodFn {
|
accountNamespace: AccountNamespace<IDL>
|
||||||
const request: MethodFn<IDL, I> = (...args) => {
|
): MethodsFn<IDL, I, any> {
|
||||||
return new MethodsBuilder(args, ixFn, txFn, rpcFn, simulateFn);
|
const request: MethodsFn<IDL, I, any> = (...args) => {
|
||||||
|
return new MethodsBuilder(
|
||||||
|
args,
|
||||||
|
ixFn,
|
||||||
|
txFn,
|
||||||
|
rpcFn,
|
||||||
|
simulateFn,
|
||||||
|
provider,
|
||||||
|
programId,
|
||||||
|
idlIx,
|
||||||
|
accountNamespace
|
||||||
|
);
|
||||||
};
|
};
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MethodsBuilder<IDL extends Idl, I extends AllInstructions<IDL>> {
|
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 _remainingAccounts: Array<AccountMeta> = [];
|
||||||
private _signers: Array<Signer> = [];
|
private _signers: Array<Signer> = [];
|
||||||
private _preInstructions: Array<TransactionInstruction> = [];
|
private _preInstructions: Array<TransactionInstruction> = [];
|
||||||
private _postInstructions: Array<TransactionInstruction> = [];
|
private _postInstructions: Array<TransactionInstruction> = [];
|
||||||
|
private _accountsResolver: AccountsResolver<IDL, I>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private _args: Array<any>,
|
private _args: Array<any>,
|
||||||
private _ixFn: InstructionFn<IDL>,
|
private _ixFn: InstructionFn<IDL>,
|
||||||
private _txFn: TransactionFn<IDL>,
|
private _txFn: TransactionFn<IDL>,
|
||||||
private _rpcFn: RpcFn<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.
|
// TODO: don't use any.
|
||||||
public accounts(accounts: any): MethodsBuilder<IDL, I> {
|
public accounts(accounts: any): MethodsBuilder<IDL, I> {
|
||||||
|
@ -54,6 +86,11 @@ export class MethodsBuilder<IDL extends Idl, I extends AllInstructions<IDL>> {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public signers(signers: Array<Signer>): MethodsBuilder<IDL, I> {
|
||||||
|
this._signers = this._signers.concat(signers);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public remainingAccounts(
|
public remainingAccounts(
|
||||||
accounts: Array<AccountMeta>
|
accounts: Array<AccountMeta>
|
||||||
): MethodsBuilder<IDL, I> {
|
): MethodsBuilder<IDL, I> {
|
||||||
|
@ -76,7 +113,7 @@ export class MethodsBuilder<IDL extends Idl, I extends AllInstructions<IDL>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async rpc(options: ConfirmOptions): Promise<TransactionSignature> {
|
public async rpc(options: ConfirmOptions): Promise<TransactionSignature> {
|
||||||
await this.resolvePdas();
|
await this._accountsResolver.resolve();
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return this._rpcFn(...this._args, {
|
return this._rpcFn(...this._args, {
|
||||||
accounts: this._accounts,
|
accounts: this._accounts,
|
||||||
|
@ -91,7 +128,7 @@ export class MethodsBuilder<IDL extends Idl, I extends AllInstructions<IDL>> {
|
||||||
public async simulate(
|
public async simulate(
|
||||||
options: ConfirmOptions
|
options: ConfirmOptions
|
||||||
): Promise<SimulateResponse<any, any>> {
|
): Promise<SimulateResponse<any, any>> {
|
||||||
await this.resolvePdas();
|
await this._accountsResolver.resolve();
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return this._simulateFn(...this._args, {
|
return this._simulateFn(...this._args, {
|
||||||
accounts: this._accounts,
|
accounts: this._accounts,
|
||||||
|
@ -104,7 +141,7 @@ export class MethodsBuilder<IDL extends Idl, I extends AllInstructions<IDL>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async instruction(): Promise<TransactionInstruction> {
|
public async instruction(): Promise<TransactionInstruction> {
|
||||||
await this.resolvePdas();
|
await this._accountsResolver.resolve();
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return this._ixFn(...this._args, {
|
return this._ixFn(...this._args, {
|
||||||
accounts: this._accounts,
|
accounts: this._accounts,
|
||||||
|
@ -116,7 +153,7 @@ export class MethodsBuilder<IDL extends Idl, I extends AllInstructions<IDL>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async transaction(): Promise<Transaction> {
|
public async transaction(): Promise<Transaction> {
|
||||||
await this.resolvePdas();
|
await this._accountsResolver.resolve();
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return this._txFn(...this._args, {
|
return this._txFn(...this._args, {
|
||||||
accounts: this._accounts,
|
accounts: this._accounts,
|
||||||
|
@ -126,18 +163,4 @@ export class MethodsBuilder<IDL extends Idl, I extends AllInstructions<IDL>> {
|
||||||
postInstructions: this._postInstructions,
|
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];
|
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<
|
export type InstructionContextFn<
|
||||||
IDL extends Idl,
|
IDL extends Idl,
|
||||||
I extends AllInstructions<IDL>,
|
I extends AllInstructions<IDL>,
|
||||||
|
@ -79,6 +83,12 @@ export type InstructionContextFnArgs<
|
||||||
Context<Accounts<I["accounts"][number]>>
|
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 = {
|
type TypeMap = {
|
||||||
publicKey: PublicKey;
|
publicKey: PublicKey;
|
||||||
bool: boolean;
|
bool: boolean;
|
||||||
|
|
|
@ -8,12 +8,11 @@ const TOKEN_PROGRAM_ID = new PublicKey(
|
||||||
);
|
);
|
||||||
|
|
||||||
export function program(provider?: Provider): Program<SplToken> {
|
export function program(provider?: Provider): Program<SplToken> {
|
||||||
return new Program<SplToken>(
|
return new Program<SplToken>(IDL, TOKEN_PROGRAM_ID, provider, coder());
|
||||||
IDL,
|
}
|
||||||
TOKEN_PROGRAM_ID,
|
|
||||||
provider,
|
export function coder(): SplTokenCoder {
|
||||||
new SplTokenCoder(IDL)
|
return new SplTokenCoder(IDL);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { PublicKey } from "@solana/web3.js";
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
|
||||||
const TOKEN_PROGRAM_ID = new PublicKey(
|
export const TOKEN_PROGRAM_ID = new PublicKey(
|
||||||
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
|
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
|
||||||
);
|
);
|
||||||
const ASSOCIATED_PROGRAM_ID = new PublicKey(
|
export const ASSOCIATED_PROGRAM_ID = new PublicKey(
|
||||||
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
|
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue