Feat: Optional Positional Accounts (#2101)
* optional accounts initial implementation
* cargo fmt
* panic if Account related traits are run on none
* Allow empty accounts to deserialize to None for optional accounts
* implement constraints for optional accounts
* optional accounts to idl gen
* accountstruct helper method
* implemented to_account_metas and infos
* add test program
* Rename optional to is_optional
* added more traits
* added TryKey error
* fix has_one
* update prelude
* is_optional
* add is_optional helper method
* Add TryAccountInfos trait
* improve constraint parser
* initial work on TryToAccountInfo
* Rename to TryToAccountInfo
* finished implementing tryToAccountInfo
* Using program method
* Formatting
* Fix program function call
* Remove function return borrow
* Fix access to program field
* finished implementing tryToAccountInfo
* add exit try_to_account_infos
* descriptive ID path
* try_to_account_info
* fix close constraint
* update test files
* completed typescript optional accounts implementation
* fix try accounts for init
* update tests
* fix to_account_metas
* update tests
* fix linting
* remove types/node
* update yarn.lock maybe?
* update optional test
* update optional test
* update optional rust cli test
* fix linting and tests
* fix tests
* update try_accounts to pass in accs during constraint gen
* Add default impl for TryToAccountInfos
* Removed TryToAccountInfos trait
* Formatting
* remove unneccesary traits and improve constraint gen drastically
* fix exit generation
* clippy
* improve cross check error message
* improve comments
* more comments
* update constraints hopefully good now?
* add new errors to ts client
* add new errors to ts client
* update optional test
* update anchor ts client
* update misc crate
* linting
* temporarily comment out optional rs tests
* update ts
* remove local test files
* linting
* optional client tests
* fix other lints to make the test pass
* remove comments
* remove misc-optional for now
* update optional program
* update optional program and client tests again
* update optional program and client tests again again
* added initialize tests that should pass
* undo unrelated anchor.toml change
* update close on optional program and improve tests
* update optional program again.
* update optional program and optional tests
* fix has one error message
* fix client example tests
* update lockfile
* update lockfile
* regenerate lockfile
* reset lockfile
* reset ts yarn lockfile
* update no caching tests
* update exit codegen to use generate_optional_check
* remove `try_to_account_infos`
* update parser to ignore method calls in constraints
* refactor and improve optional checks in constraints
* add misc-optional program and tests
* enable cpi for optional tests
* Revert "enable cpi for optional tests"
This reverts commit c864cd5d4f
.
* simplify misc tests
* update version
* fix rust version and resolve merge conflicts
* prevent Option on composite accounts
* hopefully fixed ts stuff?
* hopefully fixed ts stuff?
* testing
* hopefully done?
* update misc test
* fix optional tests
* fix ts
* fix ts again!
* linting urg
* allow-missing-optionals feature
* fix client tests
* add bnjs types to tests
Co-authored-by: febo <febo@kent.ac.uk>
Co-authored-by: Henry-E <henry.elder@adaptcentre.ie>
This commit is contained in:
parent
d88a09dbb7
commit
484628070c
|
@ -81,6 +81,8 @@ jobs:
|
|||
fail-fast: false
|
||||
matrix:
|
||||
node:
|
||||
- path: tests/optional/
|
||||
name: optional.so
|
||||
- path: tests/events/
|
||||
name: events.so
|
||||
- path: examples/tutorial/basic-4/
|
||||
|
@ -124,6 +126,10 @@ jobs:
|
|||
- run: chmod +x ~/.cargo/bin/anchor
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: optional.so
|
||||
path: tests/optional/target/deploy/
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: events.so
|
||||
path: tests/events/target/deploy/
|
||||
|
@ -289,6 +295,8 @@ jobs:
|
|||
path: tests/cpi-returns
|
||||
- cmd: cd tests/multiple-suites && anchor test --skip-lint && npx tsc --noEmit
|
||||
path: tests/multiple-suites
|
||||
- cmd: cd tests/optional && anchor test --skip-lint && npx tsc --noEmit
|
||||
path: tests/optional
|
||||
- cmd: cd tests/multiple-suites-run-single && anchor test --skip-lint --run tests/should-run && npx tsc --noEmit
|
||||
path: tests/multiple-suites-run-single
|
||||
- cmd: cd tests/pda-derivation && anchor test --skip-lint && npx tsc --noEmit
|
||||
|
|
|
@ -139,6 +139,8 @@ jobs:
|
|||
fail-fast: false
|
||||
matrix:
|
||||
node:
|
||||
- path: tests/optional/
|
||||
name: optional.so
|
||||
- path: tests/events/
|
||||
name: events.so
|
||||
- path: examples/tutorial/basic-4/
|
||||
|
@ -182,6 +184,10 @@ jobs:
|
|||
- run: chmod +x ~/.cargo/bin/anchor
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: optional.so
|
||||
path: tests/optional/target/deploy/
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: events.so
|
||||
path: tests/events/target/deploy/
|
||||
|
@ -403,6 +409,8 @@ jobs:
|
|||
path: tests/multiple-suites
|
||||
- cmd: cd tests/multiple-suites-run-single && anchor test --skip-lint --run tests/should-run && npx tsc --noEmit
|
||||
path: tests/multiple-suites-run-single
|
||||
- cmd: cd tests/optional && anchor test --skip-lint && npx tsc --noEmit
|
||||
path: tests/optional
|
||||
- cmd: cd tests/pda-derivation && anchor test --skip-lint && npx tsc --noEmit
|
||||
path: tests/pda-derivation
|
||||
- cmd: cd tests/relations-derivation && anchor test --skip-lint && npx tsc --noEmit
|
||||
|
|
|
@ -12,6 +12,7 @@ anchor-client = { path = "../", features = ["debug"] }
|
|||
basic-2 = { path = "../../examples/tutorial/basic-2/programs/basic-2", features = ["no-entrypoint"] }
|
||||
basic-4 = { path = "../../examples/tutorial/basic-4/programs/basic-4", features = ["no-entrypoint"] }
|
||||
composite = { path = "../../tests/composite/programs/composite", features = ["no-entrypoint"] }
|
||||
optional = { path = "../../tests/optional/programs/optional", features = ["no-entrypoint"] }
|
||||
events = { path = "../../tests/events/programs/events", features = ["no-entrypoint"] }
|
||||
shellexpand = "2.1.0"
|
||||
anyhow = "1.0.32"
|
||||
|
|
|
@ -26,6 +26,7 @@ main() {
|
|||
local basic_2_pid="Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"
|
||||
local basic_4_pid="CwrqeMj2U8tFr1Rhkgwc84tpAsqbt9pTt2a4taoTADPr"
|
||||
local events_pid="2dhGsWUzy5YKUsjZdLHLmkNpUDAXkNa9MYWsPc4Ziqzy"
|
||||
local optional_pid="FNqz6pqLAwvMSds2FYjR4nKV3moVpPNtvkfGFrqLKrgG"
|
||||
|
||||
#
|
||||
# Bootup validator.
|
||||
|
@ -35,13 +36,14 @@ main() {
|
|||
--bpf-program $basic_2_pid ../../examples/tutorial/basic-2/target/deploy/basic_2.so \
|
||||
--bpf-program $basic_4_pid ../../examples/tutorial/basic-4/target/deploy/basic_4.so \
|
||||
--bpf-program $events_pid ../../tests/events/target/deploy/events.so \
|
||||
--bpf-program $optional_pid ../../tests/optional/target/deploy/optional.so \
|
||||
> test-validator.log &
|
||||
sleep 5
|
||||
|
||||
#
|
||||
# Run Test.
|
||||
#
|
||||
cargo run -- --composite-pid $composite_pid --basic-2-pid $basic_2_pid --basic-4-pid $basic_4_pid --events-pid $events_pid
|
||||
cargo run -- --composite-pid $composite_pid --basic-2-pid $basic_2_pid --basic-4-pid $basic_4_pid --events-pid $events_pid --optional-pid $optional_pid
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
|
|
|
@ -12,6 +12,8 @@ use basic_2::instruction as basic_2_instruction;
|
|||
use basic_2::Counter;
|
||||
use events::instruction as events_instruction;
|
||||
use events::MyEvent;
|
||||
use optional::accounts::Initialize as OptionalInitialize;
|
||||
use optional::instruction as optional_instruction;
|
||||
// The `accounts` and `instructions` modules are generated by the framework.
|
||||
use basic_4::accounts as basic_4_accounts;
|
||||
use basic_4::basic_4::Counter as CounterState;
|
||||
|
@ -21,6 +23,7 @@ use clap::Parser;
|
|||
use composite::accounts::{Bar, CompositeUpdate, Foo, Initialize};
|
||||
use composite::instruction as composite_instruction;
|
||||
use composite::{DummyA, DummyB};
|
||||
use optional::account::{DataAccount, DataPda};
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
|
@ -34,6 +37,8 @@ pub struct Opts {
|
|||
basic_4_pid: Pubkey,
|
||||
#[clap(long)]
|
||||
events_pid: Pubkey,
|
||||
#[clap(long)]
|
||||
optional_pid: Pubkey,
|
||||
}
|
||||
|
||||
// This example assumes a local validator is running with the programs
|
||||
|
@ -58,6 +63,7 @@ fn main() -> Result<()> {
|
|||
basic_2(&client, opts.basic_2_pid)?;
|
||||
basic_4(&client, opts.basic_4_pid)?;
|
||||
events(&client, opts.events_pid)?;
|
||||
optional(&client, opts.optional_pid)?;
|
||||
|
||||
// Success.
|
||||
Ok(())
|
||||
|
@ -226,3 +232,61 @@ pub fn basic_4(client: &Client, pid: Pubkey) -> Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Runs a client for tests/optional.
|
||||
//
|
||||
// Make sure to run a localnet with the program deploy to run this example.
|
||||
fn optional(client: &Client, pid: Pubkey) -> Result<()> {
|
||||
// Program client.
|
||||
let program = client.program(pid);
|
||||
|
||||
// `Initialize` parameters.
|
||||
let data_account_keypair = Keypair::new();
|
||||
|
||||
let data_account_key = data_account_keypair.pubkey();
|
||||
|
||||
let data_pda_seeds = &[DataPda::PREFIX.as_ref(), data_account_key.as_ref()];
|
||||
let data_pda_key = Pubkey::find_program_address(data_pda_seeds, &pid).0;
|
||||
let required_keypair = Keypair::new();
|
||||
let value: u64 = 10;
|
||||
|
||||
// Build and send a transaction.
|
||||
|
||||
program
|
||||
.request()
|
||||
.instruction(system_instruction::create_account(
|
||||
&program.payer(),
|
||||
&required_keypair.pubkey(),
|
||||
program
|
||||
.rpc()
|
||||
.get_minimum_balance_for_rent_exemption(DataAccount::LEN)?,
|
||||
DataAccount::LEN as u64,
|
||||
&program.id(),
|
||||
))
|
||||
.signer(&data_account_keypair)
|
||||
.signer(&required_keypair)
|
||||
.accounts(OptionalInitialize {
|
||||
payer: Some(program.payer()),
|
||||
required: required_keypair.pubkey(),
|
||||
system_program: Some(system_program::id()),
|
||||
optional_account: Some(data_account_keypair.pubkey()),
|
||||
optional_pda: None,
|
||||
})
|
||||
.args(optional_instruction::Initialize { value, key: pid })
|
||||
.send()
|
||||
.unwrap();
|
||||
|
||||
// Assert the transaction worked.
|
||||
let required: DataAccount = program.account(required_keypair.pubkey())?;
|
||||
assert_eq!(required.data, 0);
|
||||
|
||||
let optional_pda = program.account::<DataPda>(data_pda_key);
|
||||
assert!(optional_pda.is_err());
|
||||
|
||||
let optional_account: DataAccount = program.account(data_account_keypair.pubkey())?;
|
||||
assert_eq!(optional_account.data, value * 2);
|
||||
|
||||
println!("Optional success!");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ license = "Apache-2.0"
|
|||
description = "Solana Sealevel eDSL"
|
||||
|
||||
[features]
|
||||
allow-missing-optionals = ["anchor-derive-accounts/allow-missing-optionals"]
|
||||
init-if-needed = ["anchor-derive-accounts/init-if-needed"]
|
||||
derive = []
|
||||
default = []
|
||||
|
|
|
@ -12,6 +12,7 @@ edition = "2021"
|
|||
proc-macro = true
|
||||
|
||||
[features]
|
||||
allow-missing-optionals = ["anchor-syn/allow-missing-optionals"]
|
||||
init-if-needed = ["anchor-syn/init-if-needed"]
|
||||
default = []
|
||||
anchor-debug = ["anchor-syn/anchor-debug"]
|
||||
|
|
|
@ -13,6 +13,7 @@ pub mod cpi_state;
|
|||
#[doc(hidden)]
|
||||
#[allow(deprecated)]
|
||||
pub mod loader;
|
||||
pub mod option;
|
||||
pub mod program;
|
||||
#[doc(hidden)]
|
||||
#[allow(deprecated)]
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
//! Option<T> type for optional accounts.
|
||||
//!
|
||||
//! # Example
|
||||
//! ```ignore
|
||||
//! #[derive(Accounts)]
|
||||
//! pub struct Example {
|
||||
//! pub my_acc: Option<Account<'info, MyData>>
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use solana_program::account_info::AccountInfo;
|
||||
use solana_program::instruction::AccountMeta;
|
||||
use solana_program::pubkey::Pubkey;
|
||||
|
||||
use crate::{
|
||||
error::ErrorCode, Accounts, AccountsClose, AccountsExit, Result, ToAccountInfos, ToAccountMetas,
|
||||
};
|
||||
|
||||
impl<'info, T: Accounts<'info>> Accounts<'info> for Option<T> {
|
||||
fn try_accounts(
|
||||
program_id: &Pubkey,
|
||||
accounts: &mut &[AccountInfo<'info>],
|
||||
ix_data: &[u8],
|
||||
bumps: &mut BTreeMap<String, u8>,
|
||||
reallocs: &mut BTreeSet<Pubkey>,
|
||||
) -> Result<Self> {
|
||||
if accounts.is_empty() {
|
||||
return if cfg!(feature = "allow-missing-optionals") {
|
||||
// We don't care if accounts is empty (when this feature is active),
|
||||
// so if that's the case we return None. This allows adding optional
|
||||
// accounts at the end of the Accounts struct without causing a breaking
|
||||
// change. This is safe and will error out if a required account is then
|
||||
// added after the optional account and the accounts aren't passed in.
|
||||
Ok(None)
|
||||
} else {
|
||||
// If the feature is inactive (it is off by default), then we error out
|
||||
// like every other Account.
|
||||
Err(ErrorCode::AccountNotEnoughKeys.into())
|
||||
};
|
||||
}
|
||||
|
||||
// If there are enough accounts, it will check the program_id and return
|
||||
// None if it matches, popping the first account off the accounts vec.
|
||||
if accounts[0].key == program_id {
|
||||
*accounts = &accounts[1..];
|
||||
Ok(None)
|
||||
} else {
|
||||
// If the program_id doesn't equal the account key, we default to
|
||||
// the try_accounts implementation for the inner type and then wrap that with
|
||||
// Some. This should handle all possible valid cases.
|
||||
T::try_accounts(program_id, accounts, ix_data, bumps, reallocs).map(Some)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'info, T: ToAccountInfos<'info>> ToAccountInfos<'info> for Option<T> {
|
||||
fn to_account_infos(&self) -> Vec<AccountInfo<'info>> {
|
||||
self.as_ref()
|
||||
.map_or_else(Vec::new, |account| account.to_account_infos())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ToAccountMetas> ToAccountMetas for Option<T> {
|
||||
fn to_account_metas(&self, is_signer: Option<bool>) -> Vec<AccountMeta> {
|
||||
self.as_ref()
|
||||
.expect("Cannot run `to_account_metas` on None")
|
||||
.to_account_metas(is_signer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'info, T: AccountsClose<'info>> AccountsClose<'info> for Option<T> {
|
||||
fn close(&self, sol_destination: AccountInfo<'info>) -> Result<()> {
|
||||
self.as_ref()
|
||||
.map_or(Ok(()), |t| T::close(t, sol_destination))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'info, T: AccountsExit<'info>> AccountsExit<'info> for Option<T> {
|
||||
fn exit(&self, program_id: &Pubkey) -> Result<()> {
|
||||
self.as_ref().map_or(Ok(()), |t| t.exit(program_id))
|
||||
}
|
||||
}
|
|
@ -105,6 +105,9 @@ pub enum ErrorCode {
|
|||
/// 2019 - A space constraint was violated
|
||||
#[msg("A space constraint was violated")]
|
||||
ConstraintSpace,
|
||||
/// 2020 - A required account for the constraint is None
|
||||
#[msg("A required account for the constraint is None")]
|
||||
ConstraintAccountIsNone,
|
||||
|
||||
// Require
|
||||
/// 2500 - A require expression was violated
|
||||
|
|
|
@ -9,6 +9,7 @@ rust-version = "1.59"
|
|||
edition = "2021"
|
||||
|
||||
[features]
|
||||
allow-missing-optionals = []
|
||||
init-if-needed = []
|
||||
idl = []
|
||||
hash = []
|
||||
|
|
|
@ -61,9 +61,16 @@ pub fn generate(accs: &AccountsStruct) -> proc_macro2::TokenStream {
|
|||
} else {
|
||||
quote!()
|
||||
};
|
||||
quote! {
|
||||
#docs
|
||||
pub #name: anchor_lang::solana_program::pubkey::Pubkey
|
||||
if f.is_optional {
|
||||
quote! {
|
||||
#docs
|
||||
pub #name: Option<anchor_lang::solana_program::pubkey::Pubkey>
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
#docs
|
||||
pub #name: anchor_lang::solana_program::pubkey::Pubkey
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -93,8 +100,18 @@ pub fn generate(accs: &AccountsStruct) -> proc_macro2::TokenStream {
|
|||
true => quote! { anchor_lang::solana_program::instruction::AccountMeta::new },
|
||||
};
|
||||
let name = &f.ident;
|
||||
quote! {
|
||||
account_metas.push(#meta(self.#name, #is_signer));
|
||||
if f.is_optional {
|
||||
quote! {
|
||||
if let Some(#name) = &self.#name {
|
||||
account_metas.push(#meta(*#name, #is_signer));
|
||||
} else {
|
||||
account_metas.push(anchor_lang::solana_program::instruction::AccountMeta::new_readonly(crate::ID, false));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
account_metas.push(#meta(self.#name, #is_signer));
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -62,9 +62,16 @@ pub fn generate(accs: &AccountsStruct) -> proc_macro2::TokenStream {
|
|||
} else {
|
||||
quote!()
|
||||
};
|
||||
quote! {
|
||||
#docs
|
||||
pub #name: anchor_lang::solana_program::account_info::AccountInfo<'info>
|
||||
if f.is_optional {
|
||||
quote! {
|
||||
#docs
|
||||
pub #name: Option<anchor_lang::solana_program::account_info::AccountInfo<'info>>
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
#docs
|
||||
pub #name: anchor_lang::solana_program::account_info::AccountInfo<'info>
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -94,8 +101,18 @@ pub fn generate(accs: &AccountsStruct) -> proc_macro2::TokenStream {
|
|||
true => quote! { anchor_lang::solana_program::instruction::AccountMeta::new },
|
||||
};
|
||||
let name = &f.ident;
|
||||
quote! {
|
||||
account_metas.push(#meta(anchor_lang::Key::key(&self.#name), #is_signer));
|
||||
if f.is_optional {
|
||||
quote! {
|
||||
if let Some(#name) = &self.#name {
|
||||
account_metas.push(#meta(anchor_lang::Key::key(#name), #is_signer));
|
||||
} else {
|
||||
account_metas.push(anchor_lang::solana_program::instruction::AccountMeta::new_readonly(crate::ID, false));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
account_metas.push(#meta(anchor_lang::Key::key(&self.#name), #is_signer));
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -104,18 +121,10 @@ pub fn generate(accs: &AccountsStruct) -> proc_macro2::TokenStream {
|
|||
let account_struct_infos: Vec<proc_macro2::TokenStream> = accs
|
||||
.fields
|
||||
.iter()
|
||||
.map(|f: &AccountField| match f {
|
||||
AccountField::CompositeField(s) => {
|
||||
let name = &s.ident;
|
||||
quote! {
|
||||
account_infos.extend(anchor_lang::ToAccountInfos::to_account_infos(&self.#name));
|
||||
}
|
||||
}
|
||||
AccountField::Field(f) => {
|
||||
let name = &f.ident;
|
||||
quote! {
|
||||
account_infos.push(anchor_lang::ToAccountInfo::to_account_info(&self.#name));
|
||||
}
|
||||
.map(|f: &AccountField| {
|
||||
let name = &f.ident();
|
||||
quote! {
|
||||
account_infos.extend(anchor_lang::ToAccountInfos::to_account_infos(&self.#name));
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
use crate::*;
|
||||
use proc_macro2_diagnostics::SpanDiagnosticExt;
|
||||
use quote::quote;
|
||||
use std::collections::HashSet;
|
||||
use syn::Expr;
|
||||
|
||||
pub fn generate(f: &Field) -> proc_macro2::TokenStream {
|
||||
use crate::*;
|
||||
|
||||
pub fn generate(f: &Field, accs: &AccountsStruct) -> proc_macro2::TokenStream {
|
||||
let constraints = linearize(&f.constraints);
|
||||
|
||||
let rent = constraints
|
||||
|
@ -14,12 +16,41 @@ pub fn generate(f: &Field) -> proc_macro2::TokenStream {
|
|||
|
||||
let checks: Vec<proc_macro2::TokenStream> = constraints
|
||||
.iter()
|
||||
.map(|c| generate_constraint(f, c))
|
||||
.map(|c| generate_constraint(f, c, accs))
|
||||
.collect();
|
||||
|
||||
let mut all_checks = quote! {#(#checks)*};
|
||||
|
||||
// If the field is optional we do all the inner checks as if the account
|
||||
// wasn't optional. If the account is init we also need to return an Option
|
||||
// by wrapping the resulting value with Some or returning None if it doesn't exist.
|
||||
if f.is_optional && !constraints.is_empty() {
|
||||
let ident = &f.ident;
|
||||
let ty_decl = f.ty_decl(false);
|
||||
all_checks = match &constraints[0] {
|
||||
Constraint::Init(_) | Constraint::Zeroed(_) => {
|
||||
quote! {
|
||||
let #ident: #ty_decl = if let Some(#ident) = #ident {
|
||||
#all_checks
|
||||
Some(#ident)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
quote! {
|
||||
if let Some(#ident) = &#ident {
|
||||
#all_checks
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
quote! {
|
||||
#rent
|
||||
#(#checks)*
|
||||
#all_checks
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -115,12 +146,16 @@ pub fn linearize(c_group: &ConstraintGroup) -> Vec<Constraint> {
|
|||
constraints
|
||||
}
|
||||
|
||||
fn generate_constraint(f: &Field, c: &Constraint) -> proc_macro2::TokenStream {
|
||||
fn generate_constraint(
|
||||
f: &Field,
|
||||
c: &Constraint,
|
||||
accs: &AccountsStruct,
|
||||
) -> proc_macro2::TokenStream {
|
||||
match c {
|
||||
Constraint::Init(c) => generate_constraint_init(f, c),
|
||||
Constraint::Init(c) => generate_constraint_init(f, c, accs),
|
||||
Constraint::Zeroed(c) => generate_constraint_zeroed(f, c),
|
||||
Constraint::Mut(c) => generate_constraint_mut(f, c),
|
||||
Constraint::HasOne(c) => generate_constraint_has_one(f, c),
|
||||
Constraint::HasOne(c) => generate_constraint_has_one(f, c, accs),
|
||||
Constraint::Signer(c) => generate_constraint_signer(f, c),
|
||||
Constraint::Literal(c) => generate_constraint_literal(&f.ident, c),
|
||||
Constraint::Raw(c) => generate_constraint_raw(&f.ident, c),
|
||||
|
@ -128,13 +163,13 @@ fn generate_constraint(f: &Field, c: &Constraint) -> proc_macro2::TokenStream {
|
|||
Constraint::RentExempt(c) => generate_constraint_rent_exempt(f, c),
|
||||
Constraint::Seeds(c) => generate_constraint_seeds(f, c),
|
||||
Constraint::Executable(c) => generate_constraint_executable(f, c),
|
||||
Constraint::State(c) => generate_constraint_state(f, c),
|
||||
Constraint::Close(c) => generate_constraint_close(f, c),
|
||||
Constraint::State(c) => generate_constraint_state(f, c, accs),
|
||||
Constraint::Close(c) => generate_constraint_close(f, c, accs),
|
||||
Constraint::Address(c) => generate_constraint_address(f, c),
|
||||
Constraint::AssociatedToken(c) => generate_constraint_associated_token(f, c),
|
||||
Constraint::TokenAccount(c) => generate_constraint_token_account(f, c),
|
||||
Constraint::Mint(c) => generate_constraint_mint(f, c),
|
||||
Constraint::Realloc(c) => generate_constraint_realloc(f, c),
|
||||
Constraint::AssociatedToken(c) => generate_constraint_associated_token(f, c, accs),
|
||||
Constraint::TokenAccount(c) => generate_constraint_token_account(f, c, accs),
|
||||
Constraint::Mint(c) => generate_constraint_mint(f, c, accs),
|
||||
Constraint::Realloc(c) => generate_constraint_realloc(f, c, accs),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -166,14 +201,18 @@ fn generate_constraint_address(f: &Field, c: &ConstraintAddress) -> proc_macro2:
|
|||
}
|
||||
}
|
||||
|
||||
pub fn generate_constraint_init(f: &Field, c: &ConstraintInitGroup) -> proc_macro2::TokenStream {
|
||||
generate_constraint_init_group(f, c)
|
||||
pub fn generate_constraint_init(
|
||||
f: &Field,
|
||||
c: &ConstraintInitGroup,
|
||||
accs: &AccountsStruct,
|
||||
) -> proc_macro2::TokenStream {
|
||||
generate_constraint_init_group(f, c, accs)
|
||||
}
|
||||
|
||||
pub fn generate_constraint_zeroed(f: &Field, _c: &ConstraintZeroed) -> proc_macro2::TokenStream {
|
||||
let field = &f.ident;
|
||||
let name_str = field.to_string();
|
||||
let ty_decl = f.ty_decl();
|
||||
let ty_decl = f.ty_decl(true);
|
||||
let from_account_info = f.from_account_info(None, false);
|
||||
quote! {
|
||||
let #field: #ty_decl = {
|
||||
|
@ -189,13 +228,22 @@ pub fn generate_constraint_zeroed(f: &Field, _c: &ConstraintZeroed) -> proc_macr
|
|||
}
|
||||
}
|
||||
|
||||
pub fn generate_constraint_close(f: &Field, c: &ConstraintClose) -> proc_macro2::TokenStream {
|
||||
pub fn generate_constraint_close(
|
||||
f: &Field,
|
||||
c: &ConstraintClose,
|
||||
accs: &AccountsStruct,
|
||||
) -> proc_macro2::TokenStream {
|
||||
let field = &f.ident;
|
||||
let name_str = field.to_string();
|
||||
let target = &c.sol_dest;
|
||||
let target_optional_check =
|
||||
OptionalCheckScope::new_with_field(accs, field).generate_check(target);
|
||||
quote! {
|
||||
if #field.key() == #target.key() {
|
||||
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintClose).with_account_name(#name_str));
|
||||
{
|
||||
#target_optional_check
|
||||
if #field.key() == #target.key() {
|
||||
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintClose).with_account_name(#name_str));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -210,8 +258,12 @@ pub fn generate_constraint_mut(f: &Field, c: &ConstraintMut) -> proc_macro2::Tok
|
|||
}
|
||||
}
|
||||
|
||||
pub fn generate_constraint_has_one(f: &Field, c: &ConstraintHasOne) -> proc_macro2::TokenStream {
|
||||
let target = c.join_target.clone();
|
||||
pub fn generate_constraint_has_one(
|
||||
f: &Field,
|
||||
c: &ConstraintHasOne,
|
||||
accs: &AccountsStruct,
|
||||
) -> proc_macro2::TokenStream {
|
||||
let target = &c.join_target;
|
||||
let ident = &f.ident;
|
||||
let field = match &f.ty {
|
||||
Ty::Loader(_) => quote! {#ident.load()?},
|
||||
|
@ -224,8 +276,12 @@ pub fn generate_constraint_has_one(f: &Field, c: &ConstraintHasOne) -> proc_macr
|
|||
quote! { ConstraintHasOne },
|
||||
&Some(&(quote! { my_key }, quote! { target_key })),
|
||||
);
|
||||
let target_optional_check =
|
||||
OptionalCheckScope::new_with_field(accs, &field).generate_check(target);
|
||||
|
||||
quote! {
|
||||
{
|
||||
#target_optional_check
|
||||
let my_key = #field.#target;
|
||||
let target_key = #target.key();
|
||||
if my_key != target_key {
|
||||
|
@ -325,13 +381,22 @@ pub fn generate_constraint_rent_exempt(
|
|||
}
|
||||
}
|
||||
|
||||
fn generate_constraint_realloc(f: &Field, c: &ConstraintReallocGroup) -> proc_macro2::TokenStream {
|
||||
fn generate_constraint_realloc(
|
||||
f: &Field,
|
||||
c: &ConstraintReallocGroup,
|
||||
accs: &AccountsStruct,
|
||||
) -> proc_macro2::TokenStream {
|
||||
let field = &f.ident;
|
||||
let account_name = field.to_string();
|
||||
let new_space = &c.space;
|
||||
let payer = &c.payer;
|
||||
let zero = &c.zero;
|
||||
|
||||
let mut optional_check_scope = OptionalCheckScope::new_with_field(accs, field);
|
||||
let payer_optional_check = optional_check_scope.generate_check(payer);
|
||||
let system_program_optional_check =
|
||||
optional_check_scope.generate_check(quote! {system_program});
|
||||
|
||||
quote! {
|
||||
// Blocks duplicate account reallocs in a single instruction to prevent accidental account overwrites
|
||||
// and to ensure the calculation of the change in bytes is based on account size at program entry
|
||||
|
@ -349,7 +414,9 @@ fn generate_constraint_realloc(f: &Field, c: &ConstraintReallocGroup) -> proc_ma
|
|||
.unwrap();
|
||||
|
||||
if __delta_space != 0 {
|
||||
#payer_optional_check
|
||||
if __delta_space > 0 {
|
||||
#system_program_optional_check
|
||||
if ::std::convert::TryInto::<usize>::try_into(__delta_space).unwrap() > anchor_lang::solana_program::entrypoint::MAX_PERMITTED_DATA_INCREASE {
|
||||
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::AccountReallocExceedsLimit).with_account_name(#account_name));
|
||||
}
|
||||
|
@ -378,10 +445,14 @@ fn generate_constraint_realloc(f: &Field, c: &ConstraintReallocGroup) -> proc_ma
|
|||
}
|
||||
}
|
||||
|
||||
fn generate_constraint_init_group(f: &Field, c: &ConstraintInitGroup) -> proc_macro2::TokenStream {
|
||||
fn generate_constraint_init_group(
|
||||
f: &Field,
|
||||
c: &ConstraintInitGroup,
|
||||
accs: &AccountsStruct,
|
||||
) -> proc_macro2::TokenStream {
|
||||
let field = &f.ident;
|
||||
let name_str = f.ident.to_string();
|
||||
let ty_decl = f.ty_decl();
|
||||
let ty_decl = f.ty_decl(true);
|
||||
let if_needed = if c.if_needed {
|
||||
quote! {true}
|
||||
} else {
|
||||
|
@ -389,13 +460,7 @@ fn generate_constraint_init_group(f: &Field, c: &ConstraintInitGroup) -> proc_ma
|
|||
};
|
||||
let space = &c.space;
|
||||
|
||||
// Payer for rent exemption.
|
||||
let payer = {
|
||||
let p = &c.payer;
|
||||
quote! {
|
||||
let payer = #p.to_account_info();
|
||||
}
|
||||
};
|
||||
let payer = &c.payer;
|
||||
|
||||
// Convert from account info to account context wrapper type.
|
||||
let from_account_info = f.from_account_info(Some(&c.kind), true);
|
||||
|
@ -417,6 +482,36 @@ fn generate_constraint_init_group(f: &Field, c: &ConstraintInitGroup) -> proc_ma
|
|||
quote! { #seeds, }
|
||||
});
|
||||
|
||||
let validate_pda = {
|
||||
// If the bump is provided with init *and target*, then force it to be the
|
||||
// canonical bump.
|
||||
//
|
||||
// Note that for `#[account(init, seeds)]`, find_program_address has already
|
||||
// been run in the init constraint find_pda variable.
|
||||
if c.bump.is_some() {
|
||||
let b = c.bump.as_ref().unwrap();
|
||||
quote! {
|
||||
if #field.key() != __pda_address {
|
||||
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintSeeds).with_account_name(#name_str).with_pubkeys((#field.key(), __pda_address)));
|
||||
}
|
||||
if __bump != #b {
|
||||
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintSeeds).with_account_name(#name_str).with_values((__bump, #b)));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Init seeds but no bump. We already used the canonical to create bump so
|
||||
// just check the address.
|
||||
//
|
||||
// Note that for `#[account(init, seeds)]`, find_program_address has already
|
||||
// been run in the init constraint find_pda variable.
|
||||
quote! {
|
||||
if #field.key() != __pda_address {
|
||||
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintSeeds).with_account_name(#name_str).with_pubkeys((#field.key(), __pda_address)));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
(
|
||||
quote! {
|
||||
let (__pda_address, __bump) = Pubkey::find_program_address(
|
||||
|
@ -424,6 +519,7 @@ fn generate_constraint_init_group(f: &Field, c: &ConstraintInitGroup) -> proc_ma
|
|||
program_id,
|
||||
);
|
||||
__bumps.insert(#name_str.to_string(), __bump);
|
||||
#validate_pda
|
||||
},
|
||||
quote! {
|
||||
&[
|
||||
|
@ -435,22 +531,50 @@ fn generate_constraint_init_group(f: &Field, c: &ConstraintInitGroup) -> proc_ma
|
|||
}
|
||||
};
|
||||
|
||||
// Optional check idents
|
||||
let system_program = "e! {system_program};
|
||||
let token_program = "e! {token_program};
|
||||
let associated_token_program = "e! {associated_token_program};
|
||||
let rent = "e! {rent};
|
||||
|
||||
let mut check_scope = OptionalCheckScope::new_with_field(accs, field);
|
||||
match &c.kind {
|
||||
InitKind::Token { owner, mint } => {
|
||||
let owner_optional_check = check_scope.generate_check(owner);
|
||||
let mint_optional_check = check_scope.generate_check(mint);
|
||||
|
||||
let system_program_optional_check = check_scope.generate_check(system_program);
|
||||
let token_program_optional_check = check_scope.generate_check(token_program);
|
||||
let rent_optional_check = check_scope.generate_check(rent);
|
||||
|
||||
let optional_checks = quote! {
|
||||
#system_program_optional_check
|
||||
#token_program_optional_check
|
||||
#rent_optional_check
|
||||
#owner_optional_check
|
||||
#mint_optional_check
|
||||
};
|
||||
|
||||
let payer_optional_check = check_scope.generate_check(payer);
|
||||
|
||||
let create_account = generate_create_account(
|
||||
field,
|
||||
quote! {anchor_spl::token::TokenAccount::LEN},
|
||||
quote! {&token_program.key()},
|
||||
quote! {#payer},
|
||||
seeds_with_bump,
|
||||
);
|
||||
|
||||
quote! {
|
||||
// Define the bump and pda variable.
|
||||
#find_pda
|
||||
|
||||
let #field: #ty_decl = {
|
||||
// Checks that all the required accounts for this operation are present.
|
||||
#optional_checks
|
||||
|
||||
if !#if_needed || AsRef::<AccountInfo>::as_ref(&#field).owner == &anchor_lang::solana_program::system_program::ID {
|
||||
// Define payer variable.
|
||||
#payer
|
||||
#payer_optional_check
|
||||
|
||||
// Create the account with the system program.
|
||||
#create_account
|
||||
|
@ -480,17 +604,40 @@ fn generate_constraint_init_group(f: &Field, c: &ConstraintInitGroup) -> proc_ma
|
|||
}
|
||||
}
|
||||
InitKind::AssociatedToken { owner, mint } => {
|
||||
let owner_optional_check = check_scope.generate_check(owner);
|
||||
let mint_optional_check = check_scope.generate_check(mint);
|
||||
|
||||
let system_program_optional_check = check_scope.generate_check(system_program);
|
||||
let token_program_optional_check = check_scope.generate_check(token_program);
|
||||
let associated_token_program_optional_check =
|
||||
check_scope.generate_check(associated_token_program);
|
||||
let rent_optional_check = check_scope.generate_check(rent);
|
||||
|
||||
let optional_checks = quote! {
|
||||
#system_program_optional_check
|
||||
#token_program_optional_check
|
||||
#associated_token_program_optional_check
|
||||
#rent_optional_check
|
||||
#owner_optional_check
|
||||
#mint_optional_check
|
||||
};
|
||||
|
||||
let payer_optional_check = check_scope.generate_check(payer);
|
||||
|
||||
quote! {
|
||||
// Define the bump and pda variable.
|
||||
#find_pda
|
||||
|
||||
let #field: #ty_decl = {
|
||||
// Checks that all the required accounts for this operation are present.
|
||||
#optional_checks
|
||||
|
||||
if !#if_needed || AsRef::<AccountInfo>::as_ref(&#field).owner == &anchor_lang::solana_program::system_program::ID {
|
||||
#payer
|
||||
#payer_optional_check
|
||||
|
||||
let cpi_program = associated_token_program.to_account_info();
|
||||
let cpi_accounts = anchor_spl::associated_token::Create {
|
||||
payer: payer.to_account_info(),
|
||||
payer: #payer.to_account_info(),
|
||||
associated_token: #field.to_account_info(),
|
||||
authority: #owner.to_account_info(),
|
||||
mint: #mint.to_account_info(),
|
||||
|
@ -522,24 +669,50 @@ fn generate_constraint_init_group(f: &Field, c: &ConstraintInitGroup) -> proc_ma
|
|||
decimals,
|
||||
freeze_authority,
|
||||
} => {
|
||||
let owner_optional_check = check_scope.generate_check(owner);
|
||||
let freeze_authority_optional_check = match freeze_authority {
|
||||
Some(fa) => check_scope.generate_check(fa),
|
||||
None => quote! {},
|
||||
};
|
||||
|
||||
let system_program_optional_check = check_scope.generate_check(system_program);
|
||||
let token_program_optional_check = check_scope.generate_check(token_program);
|
||||
let rent_optional_check = check_scope.generate_check(rent);
|
||||
|
||||
let optional_checks = quote! {
|
||||
#system_program_optional_check
|
||||
#token_program_optional_check
|
||||
#rent_optional_check
|
||||
#owner_optional_check
|
||||
#freeze_authority_optional_check
|
||||
};
|
||||
|
||||
let payer_optional_check = check_scope.generate_check(payer);
|
||||
|
||||
let create_account = generate_create_account(
|
||||
field,
|
||||
quote! {anchor_spl::token::Mint::LEN},
|
||||
quote! {&token_program.key()},
|
||||
quote! {#payer},
|
||||
seeds_with_bump,
|
||||
);
|
||||
|
||||
let freeze_authority = match freeze_authority {
|
||||
Some(fa) => quote! { Option::<&anchor_lang::prelude::Pubkey>::Some(&#fa.key()) },
|
||||
None => quote! { Option::<&anchor_lang::prelude::Pubkey>::None },
|
||||
};
|
||||
|
||||
quote! {
|
||||
// Define the bump and pda variable.
|
||||
#find_pda
|
||||
|
||||
let #field: #ty_decl = {
|
||||
// Checks that all the required accounts for this operation are present.
|
||||
#optional_checks
|
||||
|
||||
if !#if_needed || AsRef::<AccountInfo>::as_ref(&#field).owner == &anchor_lang::solana_program::system_program::ID {
|
||||
// Define payer variable.
|
||||
#payer
|
||||
#payer_optional_check
|
||||
|
||||
// Create the account with the system program.
|
||||
#create_account
|
||||
|
@ -575,20 +748,45 @@ fn generate_constraint_init_group(f: &Field, c: &ConstraintInitGroup) -> proc_ma
|
|||
// Define the space variable.
|
||||
let space = quote! {let space = #space;};
|
||||
|
||||
let system_program_optional_check = check_scope.generate_check(system_program);
|
||||
|
||||
// Define the owner of the account being created. If not specified,
|
||||
// default to the currently executing program.
|
||||
let owner = match owner {
|
||||
None => quote! {
|
||||
program_id
|
||||
},
|
||||
Some(o) => quote! {
|
||||
&#o
|
||||
},
|
||||
let (owner, owner_optional_check) = match owner {
|
||||
None => (
|
||||
quote! {
|
||||
program_id
|
||||
},
|
||||
quote! {},
|
||||
),
|
||||
|
||||
Some(o) => {
|
||||
// We clone the `check_scope` here to avoid collisions with the
|
||||
// `payer_optional_check`, which is in a separate scope
|
||||
let owner_optional_check = check_scope.clone().generate_check(o);
|
||||
(
|
||||
quote! {
|
||||
&#o
|
||||
},
|
||||
owner_optional_check,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let payer_optional_check = check_scope.generate_check(payer);
|
||||
|
||||
let optional_checks = quote! {
|
||||
#system_program_optional_check
|
||||
};
|
||||
|
||||
// CPI to the system program to create the account.
|
||||
let create_account =
|
||||
generate_create_account(field, quote! {space}, owner.clone(), seeds_with_bump);
|
||||
let create_account = generate_create_account(
|
||||
field,
|
||||
quote! {space},
|
||||
owner.clone(),
|
||||
quote! {#payer},
|
||||
seeds_with_bump,
|
||||
);
|
||||
|
||||
// Put it all together.
|
||||
quote! {
|
||||
|
@ -596,6 +794,9 @@ fn generate_constraint_init_group(f: &Field, c: &ConstraintInitGroup) -> proc_ma
|
|||
#find_pda
|
||||
|
||||
let #field = {
|
||||
// Checks that all the required accounts for this operation are present.
|
||||
#optional_checks
|
||||
|
||||
let actual_field = #field.to_account_info();
|
||||
let actual_owner = actual_field.owner;
|
||||
|
||||
|
@ -605,8 +806,7 @@ fn generate_constraint_init_group(f: &Field, c: &ConstraintInitGroup) -> proc_ma
|
|||
// Create the account. Always do this in the event
|
||||
// if needed is not specified or the system program is the owner.
|
||||
let pa: #ty_decl = if !#if_needed || actual_owner == &anchor_lang::solana_program::system_program::ID {
|
||||
// Define the payer variable.
|
||||
#payer
|
||||
#payer_optional_check
|
||||
|
||||
// CPI to the system program to create.
|
||||
#create_account
|
||||
|
@ -620,6 +820,7 @@ fn generate_constraint_init_group(f: &Field, c: &ConstraintInitGroup) -> proc_ma
|
|||
|
||||
// Assert the account was created correctly.
|
||||
if #if_needed {
|
||||
#owner_optional_check
|
||||
if space != actual_field.data_len() {
|
||||
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintSpace).with_account_name(#name_str).with_values((space, actual_field.data_len())));
|
||||
}
|
||||
|
@ -645,59 +846,36 @@ fn generate_constraint_init_group(f: &Field, c: &ConstraintInitGroup) -> proc_ma
|
|||
}
|
||||
|
||||
fn generate_constraint_seeds(f: &Field, c: &ConstraintSeedsGroup) -> proc_macro2::TokenStream {
|
||||
let name = &f.ident;
|
||||
let name_str = name.to_string();
|
||||
if c.is_init {
|
||||
// Note that for `#[account(init, seeds)]`, the seed generation and checks is checked in
|
||||
// the init constraint find_pda/validate_pda block, so we don't do anything here and
|
||||
// return nothing!
|
||||
quote! {}
|
||||
} else {
|
||||
let name = &f.ident;
|
||||
let name_str = name.to_string();
|
||||
|
||||
let s = &mut c.seeds.clone();
|
||||
let s = &mut c.seeds.clone();
|
||||
|
||||
let deriving_program_id = c
|
||||
.program_seed
|
||||
.clone()
|
||||
// If they specified a seeds::program to use when deriving the PDA, use it.
|
||||
.map(|program_id| quote! { #program_id.key() })
|
||||
// Otherwise fall back to the current program's program_id.
|
||||
.unwrap_or(quote! { program_id });
|
||||
let deriving_program_id = c
|
||||
.program_seed
|
||||
.clone()
|
||||
// If they specified a seeds::program to use when deriving the PDA, use it.
|
||||
.map(|program_id| quote! { #program_id.key() })
|
||||
// Otherwise fall back to the current program's program_id.
|
||||
.unwrap_or(quote! { program_id });
|
||||
|
||||
// If the seeds came with a trailing comma, we need to chop it off
|
||||
// before we interpolate them below.
|
||||
if let Some(pair) = s.pop() {
|
||||
s.push_value(pair.into_value());
|
||||
}
|
||||
|
||||
// If the bump is provided with init *and target*, then force it to be the
|
||||
// canonical bump.
|
||||
//
|
||||
// Note that for `#[account(init, seeds)]`, find_program_address has already
|
||||
// been run in the init constraint.
|
||||
if c.is_init && c.bump.is_some() {
|
||||
let b = c.bump.as_ref().unwrap();
|
||||
quote! {
|
||||
if #name.key() != __pda_address {
|
||||
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintSeeds).with_account_name(#name_str).with_pubkeys((#name.key(), __pda_address)));
|
||||
}
|
||||
if __bump != #b {
|
||||
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintSeeds).with_account_name(#name_str).with_values((__bump, #b)));
|
||||
}
|
||||
// If the seeds came with a trailing comma, we need to chop it off
|
||||
// before we interpolate them below.
|
||||
if let Some(pair) = s.pop() {
|
||||
s.push_value(pair.into_value());
|
||||
}
|
||||
}
|
||||
// Init seeds but no bump. We already used the canonical to create bump so
|
||||
// just check the address.
|
||||
//
|
||||
// Note that for `#[account(init, seeds)]`, find_program_address has already
|
||||
// been run in the init constraint.
|
||||
else if c.is_init {
|
||||
quote! {
|
||||
if #name.key() != __pda_address {
|
||||
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintSeeds).with_account_name(#name_str).with_pubkeys((#name.key(), __pda_address)));
|
||||
}
|
||||
}
|
||||
}
|
||||
// No init. So we just check the address.
|
||||
else {
|
||||
|
||||
let maybe_seeds_plus_comma = (!s.is_empty()).then(|| {
|
||||
quote! { #s, }
|
||||
});
|
||||
|
||||
// Not init here, so do all the checks.
|
||||
let define_pda = match c.bump.as_ref() {
|
||||
// Bump target not given. Find it.
|
||||
None => quote! {
|
||||
|
@ -730,13 +908,25 @@ fn generate_constraint_seeds(f: &Field, c: &ConstraintSeedsGroup) -> proc_macro2
|
|||
fn generate_constraint_associated_token(
|
||||
f: &Field,
|
||||
c: &ConstraintAssociatedToken,
|
||||
accs: &AccountsStruct,
|
||||
) -> proc_macro2::TokenStream {
|
||||
let name = &f.ident;
|
||||
let name_str = name.to_string();
|
||||
let wallet_address = &c.wallet;
|
||||
let spl_token_mint_address = &c.mint;
|
||||
let mut optional_check_scope = OptionalCheckScope::new_with_field(accs, name);
|
||||
let wallet_address_optional_check = optional_check_scope.generate_check(wallet_address);
|
||||
let spl_token_mint_address_optional_check =
|
||||
optional_check_scope.generate_check(spl_token_mint_address);
|
||||
let optional_checks = quote! {
|
||||
#wallet_address_optional_check
|
||||
#spl_token_mint_address_optional_check
|
||||
};
|
||||
|
||||
quote! {
|
||||
{
|
||||
#optional_checks
|
||||
|
||||
let my_owner = #name.owner;
|
||||
let wallet_address = #wallet_address.key();
|
||||
if my_owner != wallet_address {
|
||||
|
@ -754,27 +944,43 @@ fn generate_constraint_associated_token(
|
|||
fn generate_constraint_token_account(
|
||||
f: &Field,
|
||||
c: &ConstraintTokenAccountGroup,
|
||||
accs: &AccountsStruct,
|
||||
) -> proc_macro2::TokenStream {
|
||||
let name = &f.ident;
|
||||
let mut optional_check_scope = OptionalCheckScope::new_with_field(accs, name);
|
||||
let authority_check = match &c.authority {
|
||||
Some(authority) => {
|
||||
quote! { if #name.owner != #authority.key() { return Err(anchor_lang::error::ErrorCode::ConstraintTokenOwner.into()); } }
|
||||
let authority_optional_check = optional_check_scope.generate_check(authority);
|
||||
quote! {
|
||||
#authority_optional_check
|
||||
if #name.owner != #authority.key() { return Err(anchor_lang::error::ErrorCode::ConstraintTokenOwner.into()); }
|
||||
}
|
||||
}
|
||||
None => quote! {},
|
||||
};
|
||||
let mint_check = match &c.mint {
|
||||
Some(mint) => {
|
||||
quote! { if #name.mint != #mint.key() { return Err(anchor_lang::error::ErrorCode::ConstraintTokenMint.into()); } }
|
||||
let mint_optional_check = optional_check_scope.generate_check(mint);
|
||||
quote! {
|
||||
#mint_optional_check
|
||||
if #name.mint != #mint.key() { return Err(anchor_lang::error::ErrorCode::ConstraintTokenMint.into()); }
|
||||
}
|
||||
}
|
||||
None => quote! {},
|
||||
};
|
||||
quote! {
|
||||
#authority_check
|
||||
#mint_check
|
||||
{
|
||||
#authority_check
|
||||
#mint_check
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_constraint_mint(f: &Field, c: &ConstraintTokenMintGroup) -> proc_macro2::TokenStream {
|
||||
fn generate_constraint_mint(
|
||||
f: &Field,
|
||||
c: &ConstraintTokenMintGroup,
|
||||
accs: &AccountsStruct,
|
||||
) -> proc_macro2::TokenStream {
|
||||
let name = &f.ident;
|
||||
|
||||
let decimal_check = match &c.decimals {
|
||||
|
@ -785,26 +991,77 @@ fn generate_constraint_mint(f: &Field, c: &ConstraintTokenMintGroup) -> proc_mac
|
|||
},
|
||||
None => quote! {},
|
||||
};
|
||||
let mut optional_check_scope = OptionalCheckScope::new_with_field(accs, name);
|
||||
let mint_authority_check = match &c.mint_authority {
|
||||
Some(mint_authority) => quote! {
|
||||
if #name.mint_authority != anchor_lang::solana_program::program_option::COption::Some(anchor_lang::Key::key(&#mint_authority)) {
|
||||
return Err(anchor_lang::error::ErrorCode::ConstraintMintMintAuthority.into());
|
||||
Some(mint_authority) => {
|
||||
let mint_authority_optional_check = optional_check_scope.generate_check(mint_authority);
|
||||
quote! {
|
||||
#mint_authority_optional_check
|
||||
if #name.mint_authority != anchor_lang::solana_program::program_option::COption::Some(#mint_authority.key()) {
|
||||
return Err(anchor_lang::error::ErrorCode::ConstraintMintMintAuthority.into());
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
None => quote! {},
|
||||
};
|
||||
let freeze_authority_check = match &c.freeze_authority {
|
||||
Some(freeze_authority) => quote! {
|
||||
if #name.freeze_authority != anchor_lang::solana_program::program_option::COption::Some(anchor_lang::Key::key(&#freeze_authority)) {
|
||||
return Err(anchor_lang::error::ErrorCode::ConstraintMintFreezeAuthority.into());
|
||||
Some(freeze_authority) => {
|
||||
let freeze_authority_optional_check =
|
||||
optional_check_scope.generate_check(freeze_authority);
|
||||
quote! {
|
||||
#freeze_authority_optional_check
|
||||
if #name.freeze_authority != anchor_lang::solana_program::program_option::COption::Some(#freeze_authority.key()) {
|
||||
return Err(anchor_lang::error::ErrorCode::ConstraintMintFreezeAuthority.into());
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
None => quote! {},
|
||||
};
|
||||
quote! {
|
||||
#decimal_check
|
||||
#mint_authority_check
|
||||
#freeze_authority_check
|
||||
{
|
||||
#decimal_check
|
||||
#mint_authority_check
|
||||
#freeze_authority_check
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OptionalCheckScope<'a> {
|
||||
seen: HashSet<String>,
|
||||
accounts: &'a AccountsStruct,
|
||||
}
|
||||
|
||||
impl<'a> OptionalCheckScope<'a> {
|
||||
pub fn new(accounts: &'a AccountsStruct) -> Self {
|
||||
Self {
|
||||
seen: HashSet::new(),
|
||||
accounts,
|
||||
}
|
||||
}
|
||||
pub fn new_with_field(accounts: &'a AccountsStruct, field: impl ToString) -> Self {
|
||||
let mut check_scope = Self::new(accounts);
|
||||
check_scope.seen.insert(field.to_string());
|
||||
check_scope
|
||||
}
|
||||
pub fn generate_check(&mut self, field: impl ToTokens) -> TokenStream {
|
||||
let field_name = tts_to_string(&field);
|
||||
if self.seen.contains(&field_name) {
|
||||
quote! {}
|
||||
} else {
|
||||
self.seen.insert(field_name.clone());
|
||||
if self.accounts.is_field_optional(&field) {
|
||||
quote! {
|
||||
let #field = if let Some(ref account) = #field {
|
||||
account
|
||||
} else {
|
||||
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintAccountIsNone).with_account_name(#field_name));
|
||||
};
|
||||
}
|
||||
} else {
|
||||
quote! {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -813,12 +1070,16 @@ fn generate_constraint_mint(f: &Field, c: &ConstraintTokenMintGroup) -> proc_mac
|
|||
//
|
||||
// `seeds_with_nonce` should be given for creating PDAs. Otherwise it's an
|
||||
// empty stream.
|
||||
pub fn generate_create_account(
|
||||
//
|
||||
// This should only be run within scopes where `system_program` is not Optional
|
||||
fn generate_create_account(
|
||||
field: &Ident,
|
||||
space: proc_macro2::TokenStream,
|
||||
owner: proc_macro2::TokenStream,
|
||||
payer: proc_macro2::TokenStream,
|
||||
seeds_with_nonce: proc_macro2::TokenStream,
|
||||
) -> proc_macro2::TokenStream {
|
||||
// Field, payer, and system program are already validated to not be an Option at this point
|
||||
quote! {
|
||||
// If the account being initialized already has lamports, then
|
||||
// return them all back to the payer so that the account has
|
||||
|
@ -829,13 +1090,13 @@ pub fn generate_create_account(
|
|||
// Create the token account with right amount of lamports and space, and the correct owner.
|
||||
let lamports = __anchor_rent.minimum_balance(#space);
|
||||
let cpi_accounts = anchor_lang::system_program::CreateAccount {
|
||||
from: payer.to_account_info(),
|
||||
from: #payer.to_account_info(),
|
||||
to: #field.to_account_info()
|
||||
};
|
||||
let cpi_context = anchor_lang::context::CpiContext::new(system_program.to_account_info(), cpi_accounts);
|
||||
anchor_lang::system_program::create_account(cpi_context.with_signer(&[#seeds_with_nonce]), lamports, #space as u64, #owner)?;
|
||||
} else {
|
||||
require_keys_neq!(payer.key(), #field.key(), anchor_lang::error::ErrorCode::TryingToInitPayerAsProgramAccount);
|
||||
require_keys_neq!(#payer.key(), #field.key(), anchor_lang::error::ErrorCode::TryingToInitPayerAsProgramAccount);
|
||||
// Fund the account for rent exemption.
|
||||
let required_lamports = __anchor_rent
|
||||
.minimum_balance(#space)
|
||||
|
@ -843,7 +1104,7 @@ pub fn generate_create_account(
|
|||
.saturating_sub(__current_lamports);
|
||||
if required_lamports > 0 {
|
||||
let cpi_accounts = anchor_lang::system_program::Transfer {
|
||||
from: payer.to_account_info(),
|
||||
from: #payer.to_account_info(),
|
||||
to: #field.to_account_info(),
|
||||
};
|
||||
let cpi_context = anchor_lang::context::CpiContext::new(system_program.to_account_info(), cpi_accounts);
|
||||
|
@ -871,6 +1132,9 @@ pub fn generate_constraint_executable(
|
|||
) -> proc_macro2::TokenStream {
|
||||
let name = &f.ident;
|
||||
let name_str = name.to_string();
|
||||
|
||||
// because we are only acting on the field, we know it isnt optional at this point
|
||||
// as it was unwrapped in `generate_constraint`
|
||||
quote! {
|
||||
if !#name.to_account_info().executable {
|
||||
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintExecutable).with_account_name(#name_str));
|
||||
|
@ -878,7 +1142,11 @@ pub fn generate_constraint_executable(
|
|||
}
|
||||
}
|
||||
|
||||
pub fn generate_constraint_state(f: &Field, c: &ConstraintState) -> proc_macro2::TokenStream {
|
||||
pub fn generate_constraint_state(
|
||||
f: &Field,
|
||||
c: &ConstraintState,
|
||||
accs: &AccountsStruct,
|
||||
) -> proc_macro2::TokenStream {
|
||||
let program_target = c.program_target.clone();
|
||||
let ident = &f.ident;
|
||||
let name_str = ident.to_string();
|
||||
|
@ -886,14 +1154,19 @@ pub fn generate_constraint_state(f: &Field, c: &ConstraintState) -> proc_macro2:
|
|||
Ty::CpiState(ty) => &ty.account_type_path,
|
||||
_ => panic!("Invalid state constraint"),
|
||||
};
|
||||
let program_target_optional_check =
|
||||
OptionalCheckScope::new_with_field(accs, ident).generate_check(quote! {#program_target});
|
||||
quote! {
|
||||
// Checks the given state account is the canonical state account for
|
||||
// the target program.
|
||||
if #ident.key() != anchor_lang::accounts::cpi_state::CpiState::<#account_ty>::address(&#program_target.key()) {
|
||||
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintState).with_account_name(#name_str));
|
||||
}
|
||||
if AsRef::<AccountInfo>::as_ref(&#ident).owner != &#program_target.key() {
|
||||
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintState).with_account_name(#name_str));
|
||||
{
|
||||
#program_target_optional_check
|
||||
// Checks the given state account is the canonical state account for
|
||||
// the target program.
|
||||
if #ident.key() != anchor_lang::accounts::cpi_state::CpiState::<#account_ty>::address(&#program_target.key()) {
|
||||
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintState).with_account_name(#name_str));
|
||||
}
|
||||
if AsRef::<AccountInfo>::as_ref(&#ident).owner != &#program_target.key() {
|
||||
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintState).with_account_name(#name_str));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use crate::accounts_codegen::constraints::OptionalCheckScope;
|
||||
use crate::codegen::accounts::{generics, ParsedGenerics};
|
||||
use crate::{AccountField, AccountsStruct};
|
||||
use quote::quote;
|
||||
|
@ -29,11 +30,18 @@ pub fn generate(accs: &AccountsStruct) -> proc_macro2::TokenStream {
|
|||
let name_str = ident.to_string();
|
||||
if f.constraints.is_close() {
|
||||
let close_target = &f.constraints.close.as_ref().unwrap().sol_dest;
|
||||
let close_target_optional_check =
|
||||
OptionalCheckScope::new(accs).generate_check(close_target);
|
||||
|
||||
quote! {
|
||||
anchor_lang::AccountsClose::close(
|
||||
&self.#ident,
|
||||
self.#close_target.to_account_info(),
|
||||
).map_err(|e| e.with_account_name(#name_str))?;
|
||||
{
|
||||
let #close_target = &self.#close_target;
|
||||
#close_target_optional_check
|
||||
anchor_lang::AccountsClose::close(
|
||||
&self.#ident,
|
||||
#close_target.to_account_info(),
|
||||
).map_err(|e| e.with_account_name(#name_str))?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match f.constraints.is_mutable() {
|
||||
|
|
|
@ -16,13 +16,8 @@ pub fn generate(accs: &AccountsStruct) -> proc_macro2::TokenStream {
|
|||
.fields
|
||||
.iter()
|
||||
.map(|f: &AccountField| {
|
||||
let name = match f {
|
||||
AccountField::CompositeField(s) => &s.ident,
|
||||
AccountField::Field(f) => &f.ident,
|
||||
};
|
||||
quote! {
|
||||
account_infos.extend(self.#name.to_account_infos());
|
||||
}
|
||||
let name = &f.ident();
|
||||
quote! { account_infos.extend(self.#name.to_account_infos()); }
|
||||
})
|
||||
.collect();
|
||||
quote! {
|
||||
|
|
|
@ -9,18 +9,28 @@ pub fn generate(accs: &AccountsStruct) -> proc_macro2::TokenStream {
|
|||
.fields
|
||||
.iter()
|
||||
.map(|f: &AccountField| {
|
||||
let (name, is_signer) = match f {
|
||||
AccountField::CompositeField(s) => (&s.ident, quote! {None}),
|
||||
let (name, is_signer, is_optional) = match f {
|
||||
AccountField::CompositeField(s) => (&s.ident, quote! {None}, false),
|
||||
AccountField::Field(f) => {
|
||||
let is_signer = match f.constraints.is_signer() {
|
||||
false => quote! {None},
|
||||
true => quote! {Some(true)},
|
||||
};
|
||||
(&f.ident, is_signer)
|
||||
(&f.ident, is_signer, f.is_optional)
|
||||
}
|
||||
};
|
||||
quote! {
|
||||
account_metas.extend(self.#name.to_account_metas(#is_signer));
|
||||
if is_optional {
|
||||
quote! {
|
||||
if let Some(#name) = &self.#name {
|
||||
account_metas.extend(#name.to_account_metas(#is_signer));
|
||||
} else {
|
||||
account_metas.push(AccountMeta::new_readonly(crate::ID, false));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
account_metas.extend(self.#name.to_account_metas(#is_signer));
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
|
|
@ -32,14 +32,38 @@ pub fn generate(accs: &AccountsStruct) -> proc_macro2::TokenStream {
|
|||
// `init` and `zero` acccounts are special cased as they are
|
||||
// deserialized by constraints. Here, we just take out the
|
||||
// AccountInfo for later use at constraint validation time.
|
||||
if is_init(af) || f.constraints.zeroed.is_some() {
|
||||
if is_init(af) || f.constraints.zeroed.is_some() {
|
||||
let name = &f.ident;
|
||||
quote!{
|
||||
if accounts.is_empty() {
|
||||
return Err(anchor_lang::error::ErrorCode::AccountNotEnoughKeys.into());
|
||||
// Optional accounts have slightly different behavior here and
|
||||
// we can't leverage the try_accounts implementation for zero and init.
|
||||
if f.is_optional {
|
||||
// Thus, this block essentially reimplements the try_accounts
|
||||
// behavior with optional accounts minus the deserialziation.
|
||||
let empty_behavior = if cfg!(feature = "allow-missing-optionals") {
|
||||
quote!{ None }
|
||||
} else {
|
||||
quote!{ return Err(anchor_lang::error::ErrorCode::AccountNotEnoughKeys.into()); }
|
||||
};
|
||||
quote! {
|
||||
let #name = if accounts.is_empty() {
|
||||
#empty_behavior
|
||||
} else if accounts[0].key == program_id {
|
||||
*accounts = &accounts[1..];
|
||||
None
|
||||
} else {
|
||||
let account = &accounts[0];
|
||||
*accounts = &accounts[1..];
|
||||
Some(account)
|
||||
};
|
||||
}
|
||||
} else {
|
||||
quote!{
|
||||
if accounts.is_empty() {
|
||||
return Err(anchor_lang::error::ErrorCode::AccountNotEnoughKeys.into());
|
||||
}
|
||||
let #name = &accounts[0];
|
||||
*accounts = &accounts[1..];
|
||||
}
|
||||
let #name = &accounts[0];
|
||||
*accounts = &accounts[1..];
|
||||
}
|
||||
} else {
|
||||
let name = f.ident.to_string();
|
||||
|
@ -129,14 +153,14 @@ pub fn generate_constraints(accs: &AccountsStruct) -> proc_macro2::TokenStream {
|
|||
true => Some(f),
|
||||
},
|
||||
})
|
||||
.map(constraints::generate)
|
||||
.map(|f| constraints::generate(f, accs))
|
||||
.collect();
|
||||
|
||||
// Constraint checks for each account fields.
|
||||
let access_checks: Vec<proc_macro2::TokenStream> = non_init_fields
|
||||
.iter()
|
||||
.map(|af: &&AccountField| match af {
|
||||
AccountField::Field(f) => constraints::generate(f),
|
||||
AccountField::Field(f) => constraints::generate(f, accs),
|
||||
AccountField::CompositeField(s) => constraints::generate_composite(s),
|
||||
})
|
||||
.collect();
|
||||
|
|
|
@ -674,6 +674,7 @@ fn idl_accounts(
|
|||
Ty::Signer => true,
|
||||
_ => acc.constraints.is_signer(),
|
||||
},
|
||||
is_optional: if acc.is_optional { Some(true) } else { None },
|
||||
docs: if !no_docs { acc.docs.clone() } else { None },
|
||||
pda: pda::parse(ctx, accounts, acc, seeds_feature),
|
||||
relations: relations::parse(acc, seeds_feature),
|
||||
|
|
|
@ -75,6 +75,8 @@ pub struct IdlAccount {
|
|||
pub is_mut: bool,
|
||||
pub is_signer: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub is_optional: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub docs: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
pub pda: Option<IdlPda>,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use crate::parser::tts_to_string;
|
||||
use codegen::accounts as accounts_codegen;
|
||||
use codegen::program as program_codegen;
|
||||
use parser::accounts as accounts_parser;
|
||||
|
@ -179,6 +180,29 @@ impl AccountsStruct {
|
|||
.map(|field| field.ident().to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn has_optional(&self) -> bool {
|
||||
for field in &self.fields {
|
||||
if let AccountField::Field(field) = field {
|
||||
if field.is_optional {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn is_field_optional<T: quote::ToTokens>(&self, field: &T) -> bool {
|
||||
let matching_field = self
|
||||
.fields
|
||||
.iter()
|
||||
.find(|f| *f.ident() == tts_to_string(field));
|
||||
if let Some(matching_field) = matching_field {
|
||||
matching_field.is_optional()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
|
@ -196,6 +220,13 @@ impl AccountField {
|
|||
}
|
||||
}
|
||||
|
||||
fn is_optional(&self) -> bool {
|
||||
match self {
|
||||
AccountField::Field(field) => field.is_optional,
|
||||
AccountField::CompositeField(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ty_name(&self) -> Option<String> {
|
||||
let qualified_ty_name = match self {
|
||||
AccountField::Field(field) => match &field.ty {
|
||||
|
@ -220,6 +251,7 @@ pub struct Field {
|
|||
pub ident: Ident,
|
||||
pub constraints: ConstraintGroup,
|
||||
pub ty: Ty,
|
||||
pub is_optional: bool,
|
||||
/// IDL Doc comment
|
||||
pub docs: Option<Vec<String>>,
|
||||
}
|
||||
|
@ -227,16 +259,16 @@ pub struct Field {
|
|||
impl Field {
|
||||
pub fn typed_ident(&self) -> proc_macro2::TokenStream {
|
||||
let name = &self.ident;
|
||||
let ty_decl = self.ty_decl();
|
||||
let ty_decl = self.ty_decl(false);
|
||||
quote! {
|
||||
#name: #ty_decl
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ty_decl(&self) -> proc_macro2::TokenStream {
|
||||
pub fn ty_decl(&self, ignore_option: bool) -> proc_macro2::TokenStream {
|
||||
let account_ty = self.account_ty();
|
||||
let container_ty = self.container_ty();
|
||||
match &self.ty {
|
||||
let inner_ty = match &self.ty {
|
||||
Ty::AccountInfo => quote! {
|
||||
AccountInfo
|
||||
},
|
||||
|
@ -283,11 +315,22 @@ impl Field {
|
|||
_ => quote! {
|
||||
#container_ty<#account_ty>
|
||||
},
|
||||
};
|
||||
if self.is_optional && !ignore_option {
|
||||
quote! {
|
||||
Option<#inner_ty>
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
#inner_ty
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: remove the option once `CpiAccount` is completely removed (not
|
||||
// just deprecated).
|
||||
// Ignores optional accounts. Optional account checks and handing should be done prior to this
|
||||
// function being called.
|
||||
pub fn from_account_info(
|
||||
&self,
|
||||
kind: Option<&InitKind>,
|
||||
|
|
|
@ -5,6 +5,7 @@ use syn::punctuated::Punctuated;
|
|||
use syn::spanned::Spanned;
|
||||
use syn::token::Comma;
|
||||
use syn::Expr;
|
||||
use syn::Path;
|
||||
|
||||
pub mod constraints;
|
||||
|
||||
|
@ -39,24 +40,51 @@ pub fn parse(strct: &syn::ItemStruct) -> ParseResult<AccountsStruct> {
|
|||
}
|
||||
|
||||
fn constraints_cross_checks(fields: &[AccountField]) -> ParseResult<()> {
|
||||
// COMMON ERROR MESSAGE
|
||||
let message = |constraint: &str, field: &str, required: bool| {
|
||||
if required {
|
||||
format! {
|
||||
"a non-optional {} constraint requires \
|
||||
a non-optional {} field to exist in the account \
|
||||
validation struct. Use the Program type to add \
|
||||
the {} field to your validation struct.", constraint, field, field
|
||||
}
|
||||
} else {
|
||||
format! {
|
||||
"an optional {} constraint requires \
|
||||
an optional or required {} field to exist \
|
||||
in the account validation struct. Use the Program type \
|
||||
to add the {} field to your validation struct.", constraint, field, field
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// INIT
|
||||
let mut required_init = false;
|
||||
let init_fields: Vec<&Field> = fields
|
||||
.iter()
|
||||
.filter_map(|f| match f {
|
||||
AccountField::Field(field) if field.constraints.init.is_some() => Some(field),
|
||||
AccountField::Field(field) if field.constraints.init.is_some() => {
|
||||
if !field.is_optional {
|
||||
required_init = true
|
||||
}
|
||||
Some(field)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !init_fields.is_empty() {
|
||||
// init needs system program.
|
||||
if fields.iter().all(|f| f.ident() != "system_program") {
|
||||
|
||||
if !fields
|
||||
.iter()
|
||||
// ensures that a non optional `system_program` is present with non optional `init`
|
||||
.any(|f| f.ident() == "system_program" && !(required_init && f.is_optional()))
|
||||
{
|
||||
return Err(ParseError::new(
|
||||
init_fields[0].ident.span(),
|
||||
"the init constraint requires \
|
||||
the system_program field to exist in the account \
|
||||
validation struct. Use the Program type to add \
|
||||
the system_program field to your validation struct.",
|
||||
message("init", "system_program", required_init),
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -65,29 +93,26 @@ fn constraints_cross_checks(fields: &[AccountField]) -> ParseResult<()> {
|
|||
match kind {
|
||||
InitKind::Program { .. } => (),
|
||||
InitKind::Token { .. } | InitKind::AssociatedToken { .. } | InitKind::Mint { .. } => {
|
||||
if fields.iter().all(|f| f.ident() != "token_program") {
|
||||
if !fields
|
||||
.iter()
|
||||
.any(|f| f.ident() == "token_program" && !(required_init && f.is_optional()))
|
||||
{
|
||||
return Err(ParseError::new(
|
||||
init_fields[0].ident.span(),
|
||||
"the init constraint requires \
|
||||
the token_program field to exist in the account \
|
||||
validation struct. Use the Program type to add \
|
||||
the token_program field to your validation struct.",
|
||||
message("init", "token_program", required_init),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// a_token needs associated token program.
|
||||
if let InitKind::AssociatedToken { .. } = kind {
|
||||
if fields
|
||||
.iter()
|
||||
.all(|f| f.ident() != "associated_token_program")
|
||||
{
|
||||
if !fields.iter().any(|f| {
|
||||
f.ident() == "associated_token_program" && !(required_init && f.is_optional())
|
||||
}) {
|
||||
return Err(ParseError::new(
|
||||
init_fields[0].ident.span(),
|
||||
"the init constraint requires \
|
||||
the associated_token_program field to exist in the account \
|
||||
validation struct. Use the Program type to add \
|
||||
the associated_token_program field to your validation struct.",
|
||||
message("init", "associated_token_program", required_init),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -97,6 +122,8 @@ fn constraints_cross_checks(fields: &[AccountField]) -> ParseResult<()> {
|
|||
let associated_payer_name = match field.constraints.init.clone().unwrap().payer {
|
||||
// composite payer, check not supported
|
||||
Expr::Field(_) => continue,
|
||||
// method call, check not supported
|
||||
Expr::MethodCall(_) => continue,
|
||||
field_name => field_name.to_token_stream().to_string(),
|
||||
};
|
||||
|
||||
|
@ -112,6 +139,11 @@ fn constraints_cross_checks(fields: &[AccountField]) -> ParseResult<()> {
|
|||
field.ident.span(),
|
||||
"the payer specified for an init constraint must be mutable.",
|
||||
));
|
||||
} else if associated_payer_field.is_optional && required_init {
|
||||
return Err(ParseError::new(
|
||||
field.ident.span(),
|
||||
"the payer specified for a required init constraint must be required.",
|
||||
));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
|
@ -143,23 +175,29 @@ fn constraints_cross_checks(fields: &[AccountField]) -> ParseResult<()> {
|
|||
}
|
||||
|
||||
// REALLOC
|
||||
let mut required_realloc = false;
|
||||
let realloc_fields: Vec<&Field> = fields
|
||||
.iter()
|
||||
.filter_map(|f| match f {
|
||||
AccountField::Field(field) if field.constraints.realloc.is_some() => Some(field),
|
||||
AccountField::Field(field) if field.constraints.realloc.is_some() => {
|
||||
if !field.is_optional {
|
||||
required_realloc = true
|
||||
}
|
||||
Some(field)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !realloc_fields.is_empty() {
|
||||
// realloc needs system program.
|
||||
if fields.iter().all(|f| f.ident() != "system_program") {
|
||||
if !fields
|
||||
.iter()
|
||||
.any(|f| f.ident() == "system_program" && !(required_realloc && f.is_optional()))
|
||||
{
|
||||
return Err(ParseError::new(
|
||||
realloc_fields[0].ident.span(),
|
||||
"the realloc constraint requires \
|
||||
the system_program field to exist in the account \
|
||||
validation struct. Use the Program type to add \
|
||||
the system_program field to your validation struct.",
|
||||
message("realloc", "system_program", required_realloc),
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -168,6 +206,8 @@ fn constraints_cross_checks(fields: &[AccountField]) -> ParseResult<()> {
|
|||
let associated_payer_name = match field.constraints.realloc.clone().unwrap().payer {
|
||||
// composite allocator, check not supported
|
||||
Expr::Field(_) => continue,
|
||||
// method call, check not supported
|
||||
Expr::MethodCall(_) => continue,
|
||||
field_name => field_name.to_token_stream().to_string(),
|
||||
};
|
||||
|
||||
|
@ -184,6 +224,11 @@ fn constraints_cross_checks(fields: &[AccountField]) -> ParseResult<()> {
|
|||
field.ident.span(),
|
||||
"the realloc::payer specified for an realloc constraint must be mutable.",
|
||||
));
|
||||
} else if associated_payer_field.is_optional && required_realloc {
|
||||
return Err(ParseError::new(
|
||||
field.ident.span(),
|
||||
"the realloc::payer specified for a required realloc constraint must be required.",
|
||||
));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
|
@ -204,21 +249,29 @@ pub fn parse_account_field(f: &syn::Field) -> ParseResult<AccountField> {
|
|||
let docs = docs::parse(&f.attrs);
|
||||
let account_field = match is_field_primitive(f)? {
|
||||
true => {
|
||||
let ty = parse_ty(f)?;
|
||||
let (ty, is_optional) = parse_ty(f)?;
|
||||
let account_constraints = constraints::parse(f, Some(&ty))?;
|
||||
AccountField::Field(Field {
|
||||
ident,
|
||||
ty,
|
||||
is_optional,
|
||||
constraints: account_constraints,
|
||||
docs,
|
||||
})
|
||||
}
|
||||
false => {
|
||||
let (_, optional, _) = ident_string(f)?;
|
||||
if optional {
|
||||
return Err(ParseError::new(
|
||||
f.ty.span(),
|
||||
"Cannot have Optional composite accounts",
|
||||
));
|
||||
}
|
||||
let account_constraints = constraints::parse(f, None)?;
|
||||
AccountField::CompositeField(CompositeField {
|
||||
ident,
|
||||
constraints: account_constraints,
|
||||
symbol: ident_string(f)?,
|
||||
symbol: ident_string(f)?.0,
|
||||
raw_field: f.clone(),
|
||||
docs,
|
||||
})
|
||||
|
@ -229,7 +282,7 @@ pub fn parse_account_field(f: &syn::Field) -> ParseResult<AccountField> {
|
|||
|
||||
fn is_field_primitive(f: &syn::Field) -> ParseResult<bool> {
|
||||
let r = matches!(
|
||||
ident_string(f)?.as_str(),
|
||||
ident_string(f)?.0.as_str(),
|
||||
"ProgramState"
|
||||
| "ProgramAccount"
|
||||
| "CpiAccount"
|
||||
|
@ -248,12 +301,9 @@ fn is_field_primitive(f: &syn::Field) -> ParseResult<bool> {
|
|||
Ok(r)
|
||||
}
|
||||
|
||||
fn parse_ty(f: &syn::Field) -> ParseResult<Ty> {
|
||||
let path = match &f.ty {
|
||||
syn::Type::Path(ty_path) => ty_path.path.clone(),
|
||||
_ => return Err(ParseError::new(f.ty.span(), "invalid account type given")),
|
||||
};
|
||||
let ty = match ident_string(f)?.as_str() {
|
||||
fn parse_ty(f: &syn::Field) -> ParseResult<(Ty, bool)> {
|
||||
let (ident, optional, path) = ident_string(f)?;
|
||||
let ty = match ident.as_str() {
|
||||
"ProgramState" => Ty::ProgramState(parse_program_state(&path)?),
|
||||
"CpiState" => Ty::CpiState(parse_cpi_state(&path)?),
|
||||
"ProgramAccount" => Ty::ProgramAccount(parse_program_account(&path)?),
|
||||
|
@ -271,19 +321,52 @@ fn parse_ty(f: &syn::Field) -> ParseResult<Ty> {
|
|||
_ => return Err(ParseError::new(f.ty.span(), "invalid account type given")),
|
||||
};
|
||||
|
||||
Ok(ty)
|
||||
Ok((ty, optional))
|
||||
}
|
||||
|
||||
fn ident_string(f: &syn::Field) -> ParseResult<String> {
|
||||
let path = match &f.ty {
|
||||
fn option_to_inner_path(path: &Path) -> ParseResult<Path> {
|
||||
let segment_0 = path.segments[0].clone();
|
||||
match segment_0.arguments {
|
||||
syn::PathArguments::AngleBracketed(args) => {
|
||||
if args.args.len() != 1 {
|
||||
return Err(ParseError::new(
|
||||
args.args.span(),
|
||||
"can only have one argument in option",
|
||||
));
|
||||
}
|
||||
match &args.args[0] {
|
||||
syn::GenericArgument::Type(syn::Type::Path(ty_path)) => Ok(ty_path.path.clone()),
|
||||
_ => Err(ParseError::new(
|
||||
args.args[1].span(),
|
||||
"first bracket argument must be a lifetime",
|
||||
)),
|
||||
}
|
||||
}
|
||||
_ => Err(ParseError::new(
|
||||
segment_0.arguments.span(),
|
||||
"expected angle brackets with a lifetime and type",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn ident_string(f: &syn::Field) -> ParseResult<(String, bool, Path)> {
|
||||
let mut path = match &f.ty {
|
||||
syn::Type::Path(ty_path) => ty_path.path.clone(),
|
||||
_ => return Err(ParseError::new(f.ty.span(), "invalid type")),
|
||||
_ => return Err(ParseError::new(f.ty.span(), "invalid account type given")),
|
||||
};
|
||||
let mut optional = false;
|
||||
if parser::tts_to_string(&path)
|
||||
.replace(' ', "")
|
||||
.starts_with("Option<")
|
||||
{
|
||||
path = option_to_inner_path(&path)?;
|
||||
optional = true;
|
||||
}
|
||||
if parser::tts_to_string(&path)
|
||||
.replace(' ', "")
|
||||
.starts_with("Box<Account<")
|
||||
{
|
||||
return Ok("Account".to_string());
|
||||
return Ok(("Account".to_string(), optional, path));
|
||||
}
|
||||
// TODO: allow segmented paths.
|
||||
if path.segments.len() != 1 {
|
||||
|
@ -294,7 +377,7 @@ fn ident_string(f: &syn::Field) -> ParseResult<String> {
|
|||
}
|
||||
|
||||
let segments = &path.segments[0];
|
||||
Ok(segments.ident.to_string())
|
||||
Ok((segments.ident.to_string(), optional, path))
|
||||
}
|
||||
|
||||
fn parse_program_state(path: &syn::Path) -> ParseResult<ProgramStateTy> {
|
||||
|
|
|
@ -5,6 +5,7 @@ wallet = "~/.config/solana/id.json"
|
|||
[programs.localnet]
|
||||
misc = "3TEqcc8xhrhdspwbvoamUJe2borm4Nr72JxL66k6rgrh"
|
||||
misc2 = "HmbTLCmaGvZhKnn1Zfa1JVnp7vkMV4DYVxPLWBVoN65L"
|
||||
misc_optional = "FNqz6pqLAwvMSds2FYjR4nKV3moVpPNtvkfGFrqLKrgG"
|
||||
idl_doc = "BqmKjZGVa8fqyWuojJzG16zaKSV1GjAisZToNuvEaz6m"
|
||||
init_if_needed = "BZoppwWi6jMnydnUBEJzotgEXHwLr3b3NramJgZtWeF2"
|
||||
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
[package]
|
||||
name = "misc-optional"
|
||||
version = "0.1.0"
|
||||
description = "Created with Anchor"
|
||||
rust-version = "1.56"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
name = "misc_optional"
|
||||
|
||||
[features]
|
||||
no-entrypoint = []
|
||||
no-idl = []
|
||||
cpi = ["no-entrypoint"]
|
||||
default = []
|
||||
|
||||
[dependencies]
|
||||
anchor-lang = { path = "../../../../lang", features = ["init-if-needed"] }
|
||||
anchor-spl = { path = "../../../../spl" }
|
||||
misc2 = { path = "../misc2", features = ["cpi"] }
|
||||
spl-associated-token-account = "1.1.1"
|
|
@ -0,0 +1,2 @@
|
|||
[target.bpfel-unknown-unknown.dependencies.std]
|
||||
features = []
|
|
@ -0,0 +1,75 @@
|
|||
use anchor_lang::prelude::*;
|
||||
|
||||
macro_rules! size {
|
||||
($name: ident, $size:expr) => {
|
||||
impl $name {
|
||||
pub const LEN: usize = $size;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub const MAX_SIZE: usize = 10;
|
||||
pub const MAX_SIZE_U8: u8 = 11;
|
||||
|
||||
#[account]
|
||||
pub struct Data {
|
||||
pub udata: u128, // 16
|
||||
pub idata: i128, // 16
|
||||
}
|
||||
size!(Data, 32);
|
||||
|
||||
#[account]
|
||||
pub struct DataU16 {
|
||||
pub data: u16, // 2
|
||||
}
|
||||
size!(DataU16, 32);
|
||||
|
||||
#[account]
|
||||
pub struct DataI8 {
|
||||
pub data: i8, // 1
|
||||
}
|
||||
size!(DataI8, 1);
|
||||
|
||||
#[account]
|
||||
pub struct DataI16 {
|
||||
pub data: i16, // 2
|
||||
}
|
||||
size!(DataI16, 2);
|
||||
|
||||
#[account(zero_copy)]
|
||||
pub struct DataZeroCopy {
|
||||
pub data: u16, // 2
|
||||
pub _padding: u8, // 1
|
||||
pub bump: u8, // 1
|
||||
}
|
||||
size!(DataZeroCopy, 4);
|
||||
|
||||
#[account]
|
||||
pub struct DataWithFilter {
|
||||
pub authority: Pubkey, // 32
|
||||
pub filterable: Pubkey, // 32
|
||||
}
|
||||
size!(DataWithFilter, 64);
|
||||
|
||||
#[account]
|
||||
pub struct DataMultidimensionalArray {
|
||||
pub data: [[u8; 10]; 10], // 100
|
||||
}
|
||||
size!(DataMultidimensionalArray, 100);
|
||||
|
||||
#[account]
|
||||
pub struct DataConstArraySize {
|
||||
pub data: [u8; MAX_SIZE], // 10
|
||||
}
|
||||
size!(DataConstArraySize, MAX_SIZE);
|
||||
|
||||
#[account]
|
||||
pub struct DataConstCastArraySize {
|
||||
pub data_one: [u8; MAX_SIZE as usize],
|
||||
pub data_two: [u8; MAX_SIZE_U8 as usize],
|
||||
}
|
||||
|
||||
#[account]
|
||||
pub struct DataMultidimensionalArrayConstSizes {
|
||||
pub data: [[u8; MAX_SIZE_U8 as usize]; MAX_SIZE],
|
||||
}
|
|
@ -0,0 +1,587 @@
|
|||
use crate::account::*;
|
||||
use anchor_lang::accounts::cpi_state::CpiState;
|
||||
use anchor_lang::accounts::loader::Loader;
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_spl::associated_token::AssociatedToken;
|
||||
use anchor_spl::token::{Mint, Token, TokenAccount};
|
||||
use misc2::misc2::MyState as Misc2State;
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestTokenSeedsInit<'info> {
|
||||
#[account(
|
||||
init,
|
||||
seeds = [b"my-mint-seed".as_ref()],
|
||||
bump,
|
||||
payer = authority,
|
||||
mint::decimals = 6,
|
||||
mint::authority = authority,
|
||||
)]
|
||||
pub mint: Option<Account<'info, Mint>>,
|
||||
#[account(
|
||||
init,
|
||||
seeds = [b"my-token-seed".as_ref()],
|
||||
bump,
|
||||
payer = authority,
|
||||
token::mint = mint,
|
||||
token::authority = authority,
|
||||
)]
|
||||
pub my_pda: Option<Account<'info, TokenAccount>>,
|
||||
#[account(mut)]
|
||||
/// CHECK:
|
||||
pub authority: Option<AccountInfo<'info>>,
|
||||
pub system_program: Option<Program<'info, System>>,
|
||||
pub token_program: Option<Program<'info, Token>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestInitAssociatedToken<'info> {
|
||||
#[account(
|
||||
init,
|
||||
associated_token::mint = mint,
|
||||
payer = payer,
|
||||
associated_token::authority = payer,
|
||||
)]
|
||||
pub token: Option<Account<'info, TokenAccount>>,
|
||||
pub mint: Option<Account<'info, Mint>>,
|
||||
#[account(mut)]
|
||||
pub payer: Option<Signer<'info>>,
|
||||
pub system_program: Option<Program<'info, System>>,
|
||||
pub token_program: Option<Program<'info, Token>>,
|
||||
pub associated_token_program: Option<Program<'info, AssociatedToken>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestValidateAssociatedToken<'info> {
|
||||
#[account(
|
||||
associated_token::mint = mint,
|
||||
associated_token::authority = wallet,
|
||||
)]
|
||||
pub token: Option<Account<'info, TokenAccount>>,
|
||||
pub mint: Option<Account<'info, Mint>>,
|
||||
/// CHECK:
|
||||
pub wallet: Option<AccountInfo<'info>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
#[instruction(nonce: u8)]
|
||||
pub struct TestInstructionConstraint<'info> {
|
||||
#[account(
|
||||
seeds = [b"my-seed", my_account.as_ref().unwrap().key.as_ref()],
|
||||
bump = nonce,
|
||||
)]
|
||||
/// CHECK:
|
||||
pub my_pda: Option<AccountInfo<'info>>,
|
||||
/// CHECK:
|
||||
pub my_account: Option<AccountInfo<'info>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
#[instruction(domain: String, seed: Vec<u8>, bump: u8)]
|
||||
pub struct TestPdaInit<'info> {
|
||||
#[account(
|
||||
init,
|
||||
seeds = [b"my-seed", domain.as_bytes(), foo.as_ref().unwrap().key.as_ref(), &seed],
|
||||
bump,
|
||||
payer = my_payer,
|
||||
space = DataU16::LEN + 8
|
||||
)]
|
||||
pub my_pda: Option<Account<'info, DataU16>>,
|
||||
#[account(mut)]
|
||||
pub my_payer: Option<Signer<'info>>,
|
||||
/// CHECK:
|
||||
pub foo: Option<AccountInfo<'info>>,
|
||||
pub system_program: Option<Program<'info, System>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestPdaInitZeroCopy<'info> {
|
||||
#[account(
|
||||
init,
|
||||
seeds = [b"my-seed".as_ref()],
|
||||
bump,
|
||||
payer = my_payer,
|
||||
space = DataZeroCopy::LEN + 8
|
||||
)]
|
||||
pub my_pda: Option<AccountLoader<'info, DataZeroCopy>>,
|
||||
#[account(mut)]
|
||||
pub my_payer: Option<Signer<'info>>,
|
||||
pub system_program: Option<Program<'info, System>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestPdaMutZeroCopy<'info> {
|
||||
#[account(
|
||||
mut,
|
||||
seeds = [b"my-seed".as_ref()],
|
||||
bump = my_pda.load()?.bump,
|
||||
)]
|
||||
pub my_pda: Option<AccountLoader<'info, DataZeroCopy>>,
|
||||
/// CHECK:
|
||||
pub my_payer: Option<AccountInfo<'info>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct Ctor {}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct RemainingAccounts {}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct Initialize<'info> {
|
||||
#[account(zero)]
|
||||
pub data: Option<Account<'info, Data>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct InitializeSkipRentExempt<'info> {
|
||||
#[account(zero, rent_exempt = skip)]
|
||||
pub data: Option<Account<'info, Data>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct InitializeNoRentExempt<'info> {
|
||||
/// CHECK:
|
||||
pub data: Option<AccountInfo<'info>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestOwner<'info> {
|
||||
#[account(owner = *misc.key)]
|
||||
/// CHECK:
|
||||
pub data: Option<AccountInfo<'info>>,
|
||||
/// CHECK:
|
||||
pub misc: AccountInfo<'info>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestExecutable<'info> {
|
||||
#[account(executable)]
|
||||
/// CHECK:
|
||||
pub program: Option<AccountInfo<'info>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestStateCpi<'info> {
|
||||
#[account(signer)]
|
||||
/// CHECK:
|
||||
pub authority: Option<AccountInfo<'info>>,
|
||||
#[account(mut, state = misc2_program)]
|
||||
pub cpi_state: Option<CpiState<'info, Misc2State>>,
|
||||
#[account(executable)]
|
||||
/// CHECK:
|
||||
pub misc2_program: Option<AccountInfo<'info>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestClose<'info> {
|
||||
#[account(mut, close = sol_dest)]
|
||||
pub data: Option<Account<'info, Data>>,
|
||||
/// CHECK:
|
||||
sol_dest: Option<AccountInfo<'info>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestCloseTwice<'info> {
|
||||
#[account(mut, close = sol_dest)]
|
||||
pub data: Option<Account<'info, Data>>,
|
||||
/// CHECK:
|
||||
pub sol_dest: Option<AccountInfo<'info>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestCloseMut<'info> {
|
||||
#[account(mut)]
|
||||
pub data: Option<Account<'info, Data>>,
|
||||
/// CHECK:
|
||||
pub sol_dest: Option<AccountInfo<'info>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestU16<'info> {
|
||||
#[account(zero)]
|
||||
pub my_account: Option<Account<'info, DataU16>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestI16<'info> {
|
||||
#[account(zero)]
|
||||
pub data: Option<Account<'info, DataI16>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestSimulate {}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestI8<'info> {
|
||||
#[account(zero)]
|
||||
pub data: Option<Account<'info, DataI8>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestCompositePayer<'info> {
|
||||
pub composite: TestInit<'info>,
|
||||
#[account(init, payer = payer.as_ref().unwrap(), space = Data::LEN + 8)]
|
||||
pub data: Option<Account<'info, Data>>,
|
||||
pub payer: Option<Signer<'info>>,
|
||||
pub system_program: Option<Program<'info, System>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestInit<'info> {
|
||||
#[account(init, payer = payer, space = DataI8::LEN + 8)]
|
||||
pub data: Option<Account<'info, DataI8>>,
|
||||
#[account(mut)]
|
||||
pub payer: Option<Signer<'info>>,
|
||||
pub system_program: Option<Program<'info, System>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestInitZeroCopy<'info> {
|
||||
#[account(init, payer = payer, space = DataZeroCopy::LEN + 8)]
|
||||
pub data: Option<Loader<'info, DataZeroCopy>>,
|
||||
#[account(mut)]
|
||||
pub payer: Option<Signer<'info>>,
|
||||
pub system_program: Option<Program<'info, System>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestInitMint<'info> {
|
||||
#[account(init, mint::decimals = 6, mint::authority = payer, mint::freeze_authority = payer, payer = payer, )]
|
||||
pub mint: Option<Account<'info, Mint>>,
|
||||
#[account(mut)]
|
||||
pub payer: Option<Signer<'info>>,
|
||||
pub system_program: Option<Program<'info, System>>,
|
||||
pub token_program: Option<Program<'info, Token>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestInitToken<'info> {
|
||||
#[account(init, token::mint = mint, token::authority = payer, payer = payer, )]
|
||||
pub token: Option<Account<'info, TokenAccount>>,
|
||||
pub mint: Option<Account<'info, Mint>>,
|
||||
#[account(mut)]
|
||||
pub payer: Option<Signer<'info>>,
|
||||
pub system_program: Option<Program<'info, System>>,
|
||||
pub token_program: Option<Program<'info, Token>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestFetchAll<'info> {
|
||||
#[account(init, payer = authority, space = DataWithFilter::LEN + 8)]
|
||||
pub data: Option<Account<'info, DataWithFilter>>,
|
||||
#[account(mut)]
|
||||
pub authority: Option<Signer<'info>>,
|
||||
pub system_program: Option<Program<'info, System>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestInitWithEmptySeeds<'info> {
|
||||
#[account(init, seeds = [], bump, payer = authority, space = Data::LEN + 8)]
|
||||
pub pda: Option<Account<'info, Data>>,
|
||||
#[account(mut)]
|
||||
pub authority: Option<Signer<'info>>,
|
||||
pub system_program: Option<Program<'info, System>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestEmptySeedsConstraint<'info> {
|
||||
#[account(seeds = [], bump)]
|
||||
/// CHECK:
|
||||
pub pda: Option<AccountInfo<'info>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct InitWithSpace<'info> {
|
||||
#[account(init, payer = payer, space = DataU16::LEN + 8)]
|
||||
pub data: Option<Account<'info, DataU16>>,
|
||||
#[account(mut)]
|
||||
pub payer: Option<Signer<'info>>,
|
||||
pub system_program: Option<Program<'info, System>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestInitIfNeeded<'info> {
|
||||
// intentionally using more space (+500) to check whether space is checked when using init_if_needed
|
||||
#[account(init_if_needed, payer = payer, space = DataU16::LEN + 8 + 500)]
|
||||
pub data: Option<Account<'info, DataU16>>,
|
||||
#[account(mut)]
|
||||
pub payer: Option<Signer<'info>>,
|
||||
pub system_program: Option<Program<'info, System>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestInitIfNeededChecksOwner<'info> {
|
||||
#[account(init_if_needed, payer = payer, space = 100, owner = *owner.key, seeds = [b"hello"], bump)]
|
||||
/// CHECK:
|
||||
pub data: Option<UncheckedAccount<'info>>,
|
||||
#[account(mut)]
|
||||
pub payer: Option<Signer<'info>>,
|
||||
pub system_program: Option<Program<'info, System>>,
|
||||
/// CHECK:
|
||||
pub owner: AccountInfo<'info>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
#[instruction(seed_data: String)]
|
||||
pub struct TestInitIfNeededChecksSeeds<'info> {
|
||||
#[account(init_if_needed, payer = payer, space = 100, seeds = [seed_data.as_bytes()], bump)]
|
||||
/// CHECK:
|
||||
pub data: Option<UncheckedAccount<'info>>,
|
||||
#[account(mut)]
|
||||
pub payer: Option<Signer<'info>>,
|
||||
pub system_program: Option<Program<'info, System>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
#[instruction(decimals: u8)]
|
||||
pub struct TestInitMintIfNeeded<'info> {
|
||||
#[account(init_if_needed, mint::decimals = decimals, mint::authority = mint_authority, mint::freeze_authority = freeze_authority, payer = payer)]
|
||||
pub mint: Option<Account<'info, Mint>>,
|
||||
#[account(mut)]
|
||||
pub payer: Option<Signer<'info>>,
|
||||
pub system_program: Option<Program<'info, System>>,
|
||||
pub token_program: Option<Program<'info, Token>>,
|
||||
/// CHECK:
|
||||
pub mint_authority: Option<AccountInfo<'info>>,
|
||||
/// CHECK:
|
||||
pub freeze_authority: Option<AccountInfo<'info>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestInitTokenIfNeeded<'info> {
|
||||
#[account(init_if_needed, token::mint = mint, token::authority = authority, payer = payer, )]
|
||||
pub token: Option<Account<'info, TokenAccount>>,
|
||||
pub mint: Option<Account<'info, Mint>>,
|
||||
#[account(mut)]
|
||||
pub payer: Option<Signer<'info>>,
|
||||
pub system_program: Option<Program<'info, System>>,
|
||||
pub token_program: Option<Program<'info, Token>>,
|
||||
/// CHECK:
|
||||
pub authority: Option<AccountInfo<'info>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestInitAssociatedTokenIfNeeded<'info> {
|
||||
#[account(
|
||||
init_if_needed,
|
||||
payer = payer,
|
||||
associated_token::mint = mint,
|
||||
associated_token::authority = authority
|
||||
)]
|
||||
pub token: Option<Account<'info, TokenAccount>>,
|
||||
pub mint: Option<Account<'info, Mint>>,
|
||||
#[account(mut)]
|
||||
pub payer: Option<Signer<'info>>,
|
||||
pub system_program: Option<Program<'info, System>>,
|
||||
pub token_program: Option<Program<'info, Token>>,
|
||||
pub associated_token_program: Option<Program<'info, AssociatedToken>>,
|
||||
/// CHECK:
|
||||
pub authority: Option<AccountInfo<'info>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestMultidimensionalArray<'info> {
|
||||
#[account(zero)]
|
||||
pub data: Option<Account<'info, DataMultidimensionalArray>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestConstArraySize<'info> {
|
||||
#[account(zero)]
|
||||
pub data: Option<Account<'info, DataConstArraySize>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestConstIxDataSize<'info> {
|
||||
#[account(zero)]
|
||||
pub data: Option<Account<'info, DataConstArraySize>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestMultidimensionalArrayConstSizes<'info> {
|
||||
#[account(zero)]
|
||||
pub data: Option<Account<'info, DataMultidimensionalArrayConstSizes>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct NoRentExempt<'info> {
|
||||
/// CHECK:
|
||||
pub data: Option<AccountInfo<'info>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct EnforceRentExempt<'info> {
|
||||
#[account(rent_exempt = enforce)]
|
||||
/// CHECK:
|
||||
pub data: Option<AccountInfo<'info>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct InitDecreaseLamports<'info> {
|
||||
#[account(init, payer = user, space = 1000)]
|
||||
/// CHECK:
|
||||
pub data: Option<AccountInfo<'info>>,
|
||||
#[account(mut)]
|
||||
pub user: Option<Signer<'info>>,
|
||||
pub system_program: Option<Program<'info, System>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct InitIfNeededChecksRentExemption<'info> {
|
||||
#[account(init_if_needed, payer = user, space = 1000)]
|
||||
/// CHECK:
|
||||
pub data: Option<AccountInfo<'info>>,
|
||||
#[account(mut)]
|
||||
pub user: Option<Signer<'info>>,
|
||||
pub system_program: Option<Program<'info, System>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
#[instruction(bump: u8, second_bump: u8)]
|
||||
pub struct TestProgramIdConstraint<'info> {
|
||||
// not a real associated token account
|
||||
// just deriving like this for testing purposes
|
||||
#[account(seeds = [b"seed"], bump = bump, seeds::program = anchor_spl::associated_token::ID)]
|
||||
/// CHECK:
|
||||
first: Option<AccountInfo<'info>>,
|
||||
|
||||
#[account(seeds = [b"seed"], bump = second_bump, seeds::program = crate::ID)]
|
||||
/// CHECK:
|
||||
second: Option<AccountInfo<'info>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestProgramIdConstraintUsingFindPda<'info> {
|
||||
// not a real associated token account
|
||||
// just deriving like this for testing purposes
|
||||
#[account(seeds = [b"seed"], bump, seeds::program = anchor_spl::associated_token::ID)]
|
||||
/// CHECK:
|
||||
first: Option<AccountInfo<'info>>,
|
||||
|
||||
#[account(seeds = [b"seed"], bump, seeds::program = crate::ID)]
|
||||
/// CHECK:
|
||||
second: Option<AccountInfo<'info>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestUnsafeFieldSafetyErrors<'info> {
|
||||
#[doc = "test"]
|
||||
/// CHECK:
|
||||
pub data: Option<UncheckedAccount<'info>>,
|
||||
#[account(mut)]
|
||||
/// CHECK:
|
||||
pub data_two: Option<UncheckedAccount<'info>>,
|
||||
#[account(
|
||||
seeds = [b"my-seed", signer.as_ref().unwrap().key.as_ref()],
|
||||
bump
|
||||
)]
|
||||
/// CHECK:
|
||||
pub data_three: Option<UncheckedAccount<'info>>,
|
||||
/// CHECK:
|
||||
pub data_four: Option<UncheckedAccount<'info>>,
|
||||
pub signer: Option<Signer<'info>>,
|
||||
pub system_program: Option<Program<'info, System>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestConstraintToken<'info> {
|
||||
#[account(
|
||||
token::mint = mint,
|
||||
token::authority = payer
|
||||
)]
|
||||
pub token: Option<Account<'info, TokenAccount>>,
|
||||
pub mint: Option<Account<'info, Mint>>,
|
||||
pub payer: Option<Signer<'info>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestAuthorityConstraint<'info> {
|
||||
#[account(
|
||||
token::mint = mint,
|
||||
token::authority = fake_authority
|
||||
)]
|
||||
pub token: Option<Account<'info, TokenAccount>>,
|
||||
pub mint: Option<Account<'info, Mint>>,
|
||||
pub fake_authority: Option<AccountInfo<'info>>,
|
||||
}
|
||||
#[derive(Accounts)]
|
||||
pub struct TestOnlyAuthorityConstraint<'info> {
|
||||
#[account(
|
||||
token::authority = payer
|
||||
)]
|
||||
pub token: Option<Account<'info, TokenAccount>>,
|
||||
pub mint: Option<Account<'info, Mint>>,
|
||||
pub payer: Option<Signer<'info>>,
|
||||
}
|
||||
#[derive(Accounts)]
|
||||
pub struct TestOnlyMintConstraint<'info> {
|
||||
#[account(
|
||||
token::mint = mint,
|
||||
)]
|
||||
pub token: Option<Account<'info, TokenAccount>>,
|
||||
pub mint: Option<Account<'info, Mint>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
#[instruction(decimals: u8)]
|
||||
pub struct TestMintConstraint<'info> {
|
||||
#[account(
|
||||
mint::decimals = decimals,
|
||||
mint::authority = mint_authority,
|
||||
mint::freeze_authority = freeze_authority
|
||||
)]
|
||||
pub mint: Option<Account<'info, Mint>>,
|
||||
pub mint_authority: Option<AccountInfo<'info>>,
|
||||
pub freeze_authority: Option<AccountInfo<'info>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
#[instruction(decimals: u8)]
|
||||
pub struct TestMintOnlyDecimalsConstraint<'info> {
|
||||
#[account(
|
||||
mint::decimals = decimals,
|
||||
)]
|
||||
pub mint: Option<Account<'info, Mint>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestMintAuthorityConstraint<'info> {
|
||||
#[account(
|
||||
mint::authority = mint_authority,
|
||||
mint::freeze_authority = freeze_authority
|
||||
)]
|
||||
pub mint: Option<Account<'info, Mint>>,
|
||||
pub mint_authority: Option<AccountInfo<'info>>,
|
||||
pub freeze_authority: Option<AccountInfo<'info>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestMintOneAuthorityConstraint<'info> {
|
||||
#[account(
|
||||
mint::authority = mint_authority,
|
||||
)]
|
||||
pub mint: Option<Account<'info, Mint>>,
|
||||
pub mint_authority: Option<AccountInfo<'info>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
#[instruction(decimals: u8)]
|
||||
pub struct TestMintMissMintAuthConstraint<'info> {
|
||||
#[account(
|
||||
mint::decimals = decimals,
|
||||
mint::freeze_authority = freeze_authority,
|
||||
)]
|
||||
pub mint: Option<Account<'info, Mint>>,
|
||||
pub freeze_authority: Option<AccountInfo<'info>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TestAssociatedToken<'info> {
|
||||
#[account(
|
||||
associated_token::mint = mint,
|
||||
associated_token::authority = authority,
|
||||
)]
|
||||
pub token: Option<Account<'info, TokenAccount>>,
|
||||
pub mint: Option<Account<'info, Mint>>,
|
||||
pub authority: Option<AccountInfo<'info>>,
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
use anchor_lang::prelude::*;
|
||||
|
||||
pub const MAX_EVENT_SIZE: usize = 10;
|
||||
pub const MAX_EVENT_SIZE_U8: u8 = 11;
|
||||
|
||||
#[event]
|
||||
pub struct E1 {
|
||||
pub data: u32,
|
||||
}
|
||||
|
||||
#[event]
|
||||
pub struct E2 {
|
||||
pub data: u32,
|
||||
}
|
||||
|
||||
#[event]
|
||||
pub struct E3 {
|
||||
pub data: u32,
|
||||
}
|
||||
|
||||
#[event]
|
||||
pub struct E4 {
|
||||
pub data: Pubkey,
|
||||
}
|
||||
|
||||
#[event]
|
||||
pub struct E5 {
|
||||
pub data: [u8; MAX_EVENT_SIZE],
|
||||
}
|
||||
|
||||
#[event]
|
||||
pub struct E6 {
|
||||
pub data: [u8; MAX_EVENT_SIZE_U8 as usize],
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, AnchorSerialize, AnchorDeserialize, PartialEq, Eq)]
|
||||
pub struct TestStruct {
|
||||
pub data1: u8,
|
||||
pub data2: u16,
|
||||
pub data3: u32,
|
||||
pub data4: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, AnchorSerialize, AnchorDeserialize, PartialEq, Eq)]
|
||||
pub enum TestEnum {
|
||||
First,
|
||||
Second { x: u64, y: u64 },
|
||||
TupleTest(u8, u8, u16, u16),
|
||||
TupleStructTest(TestStruct),
|
||||
}
|
||||
|
||||
#[event]
|
||||
pub struct E7 {
|
||||
pub data: TestEnum,
|
||||
}
|
|
@ -0,0 +1,399 @@
|
|||
//! Misc optional example is a catchall program for testing unrelated features.
|
||||
//! It's not too instructive/coherent by itself, so please see other examples.
|
||||
|
||||
use account::MAX_SIZE;
|
||||
use anchor_lang::prelude::*;
|
||||
use context::*;
|
||||
use event::*;
|
||||
use misc2::Auth;
|
||||
|
||||
mod account;
|
||||
mod context;
|
||||
mod event;
|
||||
|
||||
declare_id!("FNqz6pqLAwvMSds2FYjR4nKV3moVpPNtvkfGFrqLKrgG");
|
||||
|
||||
#[constant]
|
||||
pub const BASE: u128 = 1_000_000;
|
||||
#[constant]
|
||||
pub const DECIMALS: u8 = 6;
|
||||
pub const NO_IDL: u16 = 55;
|
||||
|
||||
#[program]
|
||||
pub mod misc_optional {
|
||||
use super::*;
|
||||
|
||||
pub const SIZE: u64 = 99;
|
||||
|
||||
#[state(SIZE)]
|
||||
pub struct MyState {
|
||||
pub v: Vec<u8>,
|
||||
}
|
||||
|
||||
impl MyState {
|
||||
pub fn new(_ctx: Context<Ctor>) -> Result<Self> {
|
||||
Ok(Self { v: vec![] })
|
||||
}
|
||||
|
||||
pub fn remaining_accounts(&mut self, ctx: Context<RemainingAccounts>) -> Result<()> {
|
||||
if ctx.remaining_accounts.len() != 1 {
|
||||
return Err(ProgramError::Custom(1).into()); // Arbitrary error.
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initialize(ctx: Context<Initialize>, udata: u128, idata: i128) -> Result<()> {
|
||||
ctx.accounts.data.as_mut().unwrap().udata = udata;
|
||||
ctx.accounts.data.as_mut().unwrap().idata = idata;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn initialize_no_rent_exempt(_ctx: Context<InitializeNoRentExempt>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn initialize_skip_rent_exempt(_ctx: Context<InitializeSkipRentExempt>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_owner(_ctx: Context<TestOwner>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_executable(_ctx: Context<TestExecutable>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_state_cpi(ctx: Context<TestStateCpi>, data: u64) -> Result<()> {
|
||||
let cpi_program = ctx.accounts.misc2_program.as_ref().unwrap().clone();
|
||||
let cpi_accounts = Auth {
|
||||
authority: ctx.accounts.authority.as_ref().unwrap().clone(),
|
||||
};
|
||||
let ctx = ctx
|
||||
.accounts
|
||||
.cpi_state
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.context(cpi_program, cpi_accounts);
|
||||
misc2::cpi::state::set_data(ctx, data)
|
||||
}
|
||||
|
||||
pub fn test_u16(ctx: Context<TestU16>, data: u16) -> Result<()> {
|
||||
ctx.accounts.my_account.as_mut().unwrap().data = data;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_simulate(_ctx: Context<TestSimulate>, data: u32) -> Result<()> {
|
||||
emit!(E1 { data });
|
||||
emit!(E2 { data: 1234 });
|
||||
emit!(E3 { data: 9 });
|
||||
emit!(E5 {
|
||||
data: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||
});
|
||||
emit!(E6 {
|
||||
data: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_input_enum(ctx: Context<TestSimulate>, data: TestEnum) -> Result<()> {
|
||||
emit!(E7 { data: data });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_i8(ctx: Context<TestI8>, data: i8) -> Result<()> {
|
||||
ctx.accounts.data.as_mut().unwrap().data = data;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_i16(ctx: Context<TestI16>, data: i16) -> Result<()> {
|
||||
ctx.accounts.data.as_mut().unwrap().data = data;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_const_array_size(ctx: Context<TestConstArraySize>, data: u8) -> Result<()> {
|
||||
ctx.accounts.data.as_mut().unwrap().data[0] = data;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_const_ix_data_size(
|
||||
ctx: Context<TestConstIxDataSize>,
|
||||
data: [u8; MAX_SIZE],
|
||||
) -> Result<()> {
|
||||
ctx.accounts.data.as_mut().unwrap().data = data;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_close(_ctx: Context<TestClose>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_close_twice(ctx: Context<TestCloseTwice>) -> Result<()> {
|
||||
let data_account = &ctx.accounts.data.as_ref().unwrap();
|
||||
let sol_dest_info = ctx.accounts.sol_dest.as_ref().unwrap().to_account_info();
|
||||
data_account.close(sol_dest_info)?;
|
||||
let data_account_info: &AccountInfo = data_account.as_ref();
|
||||
require_keys_eq!(*data_account_info.owner, System::id());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_close_mut(ctx: Context<TestCloseMut>) -> Result<()> {
|
||||
let data_account = &ctx.accounts.data.as_ref().unwrap();
|
||||
let sol_dest_info = ctx.accounts.sol_dest.as_ref().unwrap().to_account_info();
|
||||
data_account.close(sol_dest_info)?;
|
||||
let data_account_info: &AccountInfo = data_account.as_ref();
|
||||
require_keys_eq!(*data_account_info.owner, System::id());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_instruction_constraint(
|
||||
_ctx: Context<TestInstructionConstraint>,
|
||||
_nonce: u8,
|
||||
) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_pda_init(
|
||||
ctx: Context<TestPdaInit>,
|
||||
_domain: String,
|
||||
_seed: Vec<u8>,
|
||||
_bump: u8,
|
||||
) -> Result<()> {
|
||||
ctx.accounts.my_pda.as_mut().unwrap().data = 6;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_pda_init_zero_copy(ctx: Context<TestPdaInitZeroCopy>) -> Result<()> {
|
||||
let mut acc = ctx.accounts.my_pda.as_ref().unwrap().load_init()?;
|
||||
acc.data = 9;
|
||||
acc.bump = *ctx.bumps.get("my_pda").unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_pda_mut_zero_copy(ctx: Context<TestPdaMutZeroCopy>) -> Result<()> {
|
||||
let mut acc = ctx.accounts.my_pda.as_mut().unwrap().load_mut()?;
|
||||
acc.data = 1234;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_token_seeds_init(_ctx: Context<TestTokenSeedsInit>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn default<'info>(
|
||||
_program_id: &Pubkey,
|
||||
_accounts: &[AccountInfo<'info>],
|
||||
_data: &[u8],
|
||||
) -> Result<()> {
|
||||
Err(ProgramError::Custom(1234).into())
|
||||
}
|
||||
|
||||
pub fn test_init(ctx: Context<TestInit>) -> Result<()> {
|
||||
ctx.accounts.data.as_mut().unwrap().data = 3;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_init_zero_copy(ctx: Context<TestInitZeroCopy>) -> Result<()> {
|
||||
let mut data = ctx.accounts.data.as_ref().unwrap().load_init()?;
|
||||
data.data = 10;
|
||||
data.bump = 2;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_init_mint(ctx: Context<TestInitMint>) -> Result<()> {
|
||||
assert!(ctx.accounts.mint.as_ref().unwrap().decimals == 6);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_init_token(ctx: Context<TestInitToken>) -> Result<()> {
|
||||
assert!(
|
||||
ctx.accounts.token.as_ref().unwrap().mint == ctx.accounts.mint.as_ref().unwrap().key()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_composite_payer(ctx: Context<TestCompositePayer>) -> Result<()> {
|
||||
ctx.accounts.composite.data.as_mut().unwrap().data = 1;
|
||||
ctx.accounts.data.as_mut().unwrap().udata = 2;
|
||||
ctx.accounts.data.as_mut().unwrap().idata = 3;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_init_associated_token(ctx: Context<TestInitAssociatedToken>) -> Result<()> {
|
||||
assert!(
|
||||
ctx.accounts.token.as_ref().unwrap().mint == ctx.accounts.mint.as_ref().unwrap().key()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_validate_associated_token(
|
||||
_ctx: Context<TestValidateAssociatedToken>,
|
||||
) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_fetch_all(ctx: Context<TestFetchAll>, filterable: Pubkey) -> Result<()> {
|
||||
ctx.accounts.data.as_mut().unwrap().authority =
|
||||
ctx.accounts.authority.as_ref().unwrap().key();
|
||||
ctx.accounts.data.as_mut().unwrap().filterable = filterable;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_init_with_empty_seeds(_ctx: Context<TestInitWithEmptySeeds>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_empty_seeds_constraint(_ctx: Context<TestEmptySeedsConstraint>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_init_if_needed(ctx: Context<TestInitIfNeeded>, data: u16) -> Result<()> {
|
||||
ctx.accounts.data.as_mut().unwrap().data = data;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_init_if_needed_checks_owner(
|
||||
_ctx: Context<TestInitIfNeededChecksOwner>,
|
||||
) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_init_if_needed_checks_seeds(
|
||||
_ctx: Context<TestInitIfNeededChecksSeeds>,
|
||||
_seed_data: String,
|
||||
) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_init_mint_if_needed(
|
||||
_ctx: Context<TestInitMintIfNeeded>,
|
||||
_decimals: u8,
|
||||
) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_init_token_if_needed(_ctx: Context<TestInitTokenIfNeeded>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_init_associated_token_if_needed(
|
||||
_ctx: Context<TestInitAssociatedTokenIfNeeded>,
|
||||
) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn init_with_space(_ctx: Context<InitWithSpace>, data: u16) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_multidimensional_array(
|
||||
ctx: Context<TestMultidimensionalArray>,
|
||||
data: [[u8; 10]; 10],
|
||||
) -> Result<()> {
|
||||
ctx.accounts.data.as_mut().unwrap().data = data;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_multidimensional_array_const_sizes(
|
||||
ctx: Context<TestMultidimensionalArrayConstSizes>,
|
||||
data: [[u8; 11]; 10],
|
||||
) -> Result<()> {
|
||||
ctx.accounts.data.as_mut().unwrap().data = data;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_no_rent_exempt(_ctx: Context<NoRentExempt>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_enforce_rent_exempt(_ctx: Context<EnforceRentExempt>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn init_decrease_lamports(ctx: Context<InitDecreaseLamports>) -> Result<()> {
|
||||
**ctx
|
||||
.accounts
|
||||
.data
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.try_borrow_mut_lamports()? -= 1;
|
||||
**ctx
|
||||
.accounts
|
||||
.user
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.try_borrow_mut_lamports()? += 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn init_if_needed_checks_rent_exemption(
|
||||
_ctx: Context<InitIfNeededChecksRentExemption>,
|
||||
) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_program_id_constraint(
|
||||
_ctx: Context<TestProgramIdConstraint>,
|
||||
_bump: u8,
|
||||
_second_bump: u8,
|
||||
) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_program_id_constraint_find_pda(
|
||||
_ctx: Context<TestProgramIdConstraintUsingFindPda>,
|
||||
) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_token_constraint(_ctx: Context<TestConstraintToken>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_token_auth_constraint(_ctx: Context<TestAuthorityConstraint>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_only_auth_constraint(_ctx: Context<TestOnlyAuthorityConstraint>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_only_mint_constraint(_ctx: Context<TestOnlyMintConstraint>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_mint_constraint(_ctx: Context<TestMintConstraint>, _decimals: u8) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_mint_only_decimals_constraint(
|
||||
_ctx: Context<TestMintOnlyDecimalsConstraint>,
|
||||
_decimals: u8,
|
||||
) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_mint_only_auth_constraint(
|
||||
_ctx: Context<TestMintAuthorityConstraint>,
|
||||
) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_mint_only_one_auth_constraint(
|
||||
_ctx: Context<TestMintOneAuthorityConstraint>,
|
||||
) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_mint_miss_mint_auth_constraint(
|
||||
_ctx: Context<TestMintMissMintAuthConstraint>,
|
||||
_decimals: u8,
|
||||
) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_associated_constraint(_ctx: Context<TestAssociatedToken>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,13 @@
|
|||
[provider]
|
||||
cluster = "localnet"
|
||||
wallet = "~/.config/solana/id.json"
|
||||
|
||||
[programs.localnet]
|
||||
optional = "FNqz6pqLAwvMSds2FYjR4nKV3moVpPNtvkfGFrqLKrgG"
|
||||
allow_missing_optionals = "ErjUjtqKE5AGWUsjseSJCVLtddM6rhaMbDqmhzraF9h6"
|
||||
|
||||
[workspace]
|
||||
members = ["programs/optional", "programs/allow-missing-optionals"]
|
||||
|
||||
[scripts]
|
||||
test = "yarn run ts-mocha -t 1000000 -p ./tsconfig.json -t 1000000 tests/**/*.ts"
|
|
@ -0,0 +1,4 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"programs/*"
|
||||
]
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "optional",
|
||||
"version": "0.25.0",
|
||||
"license": "(MIT OR Apache-2.0)",
|
||||
"homepage": "https://github.com/coral-xyz/anchor#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/coral-xyz/anchor/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/coral-xyz/anchor.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=11"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "anchor test"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
[package]
|
||||
name = "allow-missing-optionals"
|
||||
version = "0.1.0"
|
||||
description = "Created with Anchor"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
name = "allow_missing_optionals"
|
||||
|
||||
[features]
|
||||
no-entrypoint = []
|
||||
cpi = ["no-entrypoint"]
|
||||
|
||||
[dependencies]
|
||||
anchor-lang = { path = "../../../../lang", features = ["allow-missing-optionals"] }
|
|
@ -0,0 +1,2 @@
|
|||
[target.bpfel-unknown-unknown.dependencies.std]
|
||||
features = []
|
|
@ -0,0 +1,38 @@
|
|||
//! This tests that the `allow-missing-optionals` feature works
|
||||
|
||||
use anchor_lang::prelude::*;
|
||||
|
||||
declare_id!("ErjUjtqKE5AGWUsjseSJCVLtddM6rhaMbDqmhzraF9h6");
|
||||
|
||||
#[program]
|
||||
mod allow_missing_optionals {
|
||||
use super::*;
|
||||
|
||||
pub fn do_stuff(ctx: Context<DoStuff>) -> Result<()> {
|
||||
msg!("Doing stuff...");
|
||||
let optional_2 = &mut ctx.accounts.optional_2;
|
||||
if let Some(data_account) = optional_2 {
|
||||
data_account.data = 42;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[account]
|
||||
pub struct DataAccount {
|
||||
pub data: u64,
|
||||
}
|
||||
|
||||
impl DataAccount {
|
||||
pub const LEN: usize = 8 + 8;
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct DoStuff<'info> {
|
||||
#[account(mut)]
|
||||
pub payer: Signer<'info>,
|
||||
pub system_program: Option<Program<'info, System>>,
|
||||
#[account(init, payer = payer, space = DataAccount::LEN)]
|
||||
pub optional_2: Option<Account<'info, DataAccount>>,
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
[package]
|
||||
name = "optional"
|
||||
version = "0.1.0"
|
||||
description = "Created with Anchor"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
name = "optional"
|
||||
|
||||
[features]
|
||||
no-entrypoint = []
|
||||
cpi = ["no-entrypoint"]
|
||||
|
||||
[dependencies]
|
||||
anchor-lang = { path = "../../../../lang" }
|
|
@ -0,0 +1,2 @@
|
|||
[target.bpfel-unknown-unknown.dependencies.std]
|
||||
features = []
|
|
@ -0,0 +1,20 @@
|
|||
use anchor_lang::prelude::*;
|
||||
|
||||
#[account]
|
||||
pub struct DataPda {
|
||||
pub data_account: Pubkey,
|
||||
}
|
||||
|
||||
impl DataPda {
|
||||
pub const LEN: usize = 8 + 32;
|
||||
pub const PREFIX: &'static str = "data_pda";
|
||||
}
|
||||
|
||||
#[account]
|
||||
pub struct DataAccount {
|
||||
pub data: u64,
|
||||
}
|
||||
|
||||
impl DataAccount {
|
||||
pub const LEN: usize = 8 + 8;
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
use crate::account::*;
|
||||
use anchor_lang::prelude::*;
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct Initialize<'info> {
|
||||
#[account(mut)]
|
||||
pub payer: Option<Signer<'info>>,
|
||||
#[account(init, payer = payer, space = DataAccount::LEN, constraint = payer.is_some())]
|
||||
pub optional_account: Option<Account<'info, DataAccount>>,
|
||||
pub system_program: Option<Program<'info, System>>,
|
||||
#[account(zero)]
|
||||
pub required: Account<'info, DataAccount>,
|
||||
#[account(init, seeds=[DataPda::PREFIX.as_ref(), optional_account.as_ref().unwrap().key().as_ref()], bump, payer=payer, space=DataPda::LEN)]
|
||||
pub optional_pda: Option<Account<'info, DataPda>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
#[instruction(value: u64, key: Pubkey, pda_bump: u8)]
|
||||
pub struct Update<'info> {
|
||||
#[account(mut)]
|
||||
pub payer: Option<Signer<'info>>,
|
||||
#[account(mut, seeds=[DataPda::PREFIX.as_ref(), optional_account.as_ref().unwrap().key().as_ref()], bump = pda_bump)]
|
||||
pub optional_pda: Option<Account<'info, DataPda>>,
|
||||
#[account(mut, signer, constraint = payer.is_some())]
|
||||
pub optional_account: Option<Account<'info, DataAccount>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
#[instruction(new_size: usize)]
|
||||
pub struct Realloc<'info> {
|
||||
#[account(mut)]
|
||||
pub payer: Option<Signer<'info>>,
|
||||
#[account(mut, realloc = new_size, realloc::payer = payer, realloc::zero = false)]
|
||||
pub optional_pda: Option<Account<'info, DataPda>>,
|
||||
pub required: Account<'info, DataAccount>,
|
||||
pub system_program: Option<Program<'info, System>>,
|
||||
#[account(mut, signer, realloc = new_size, realloc::payer = payer, realloc::zero = true)]
|
||||
pub optional_account: Option<Account<'info, DataAccount>>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct Close<'info> {
|
||||
#[account(mut)]
|
||||
pub payer: Option<Signer<'info>>,
|
||||
#[account(mut, close = payer, has_one = data_account)]
|
||||
pub optional_pda: Option<Account<'info, DataPda>>,
|
||||
#[account(mut, signer, close = payer)]
|
||||
pub data_account: Option<Account<'info, DataAccount>>,
|
||||
pub system_program: Option<Program<'info, System>>,
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
//! This example demonstrates the ability to use optional accounts in
|
||||
//! structs deriving `Accounts`.
|
||||
|
||||
use anchor_lang::prelude::*;
|
||||
pub use context::*;
|
||||
|
||||
pub mod account;
|
||||
pub mod context;
|
||||
declare_id!("FNqz6pqLAwvMSds2FYjR4nKV3moVpPNtvkfGFrqLKrgG");
|
||||
|
||||
#[program]
|
||||
mod optional {
|
||||
use super::*;
|
||||
|
||||
pub fn initialize(ctx: Context<Initialize>, value: u64, key: Pubkey) -> Result<()> {
|
||||
let optional_pda = &mut ctx.accounts.optional_pda;
|
||||
let optional_account = &mut ctx.accounts.optional_account;
|
||||
let required = &mut ctx.accounts.required;
|
||||
|
||||
required.data = 0;
|
||||
|
||||
if let Some(data_account) = optional_account {
|
||||
if let Some(data_pda) = optional_pda {
|
||||
data_pda.data_account = key;
|
||||
data_account.data = value;
|
||||
} else {
|
||||
data_account.data = value * 2;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update(ctx: Context<Update>, value: u64, key: Pubkey, _pda_bump: u8) -> Result<()> {
|
||||
if let Some(data_account) = &mut ctx.accounts.optional_account {
|
||||
data_account.data = value;
|
||||
}
|
||||
if let Some(data_account) = &mut ctx.accounts.optional_pda {
|
||||
data_account.data_account = key;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn realloc(ctx: Context<Realloc>, new_size: u64) -> Result<()> {
|
||||
let optional_pda = &ctx.accounts.optional_pda;
|
||||
let optional_account = &ctx.accounts.optional_account;
|
||||
if let Some(data_pda) = optional_pda {
|
||||
let len = data_pda.to_account_info().data_len();
|
||||
if len != new_size as usize {
|
||||
return err!(OptionalErrors::ReallocFailed);
|
||||
}
|
||||
}
|
||||
if let Some(data_account) = optional_account {
|
||||
let len = data_account.to_account_info().data_len();
|
||||
if len != new_size as usize {
|
||||
return err!(OptionalErrors::ReallocFailed);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn close(_ctx: Context<Close>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[error_code]
|
||||
pub enum OptionalErrors {
|
||||
#[msg("Failed realloc")]
|
||||
ReallocFailed,
|
||||
}
|
|
@ -0,0 +1,845 @@
|
|||
import * as anchor from "@project-serum/anchor";
|
||||
import {
|
||||
Program,
|
||||
web3,
|
||||
BN,
|
||||
AnchorError,
|
||||
LangErrorCode,
|
||||
LangErrorMessage,
|
||||
translateError,
|
||||
parseIdlErrors,
|
||||
} from "@project-serum/anchor";
|
||||
import { Optional } from "../target/types/optional";
|
||||
import { AllowMissingOptionals } from "../target/types/allow_missing_optionals";
|
||||
import { assert, expect } from "chai";
|
||||
|
||||
describe("Optional", () => {
|
||||
// configure the client to use the local cluster
|
||||
anchor.setProvider(anchor.AnchorProvider.env());
|
||||
const anchorProvider = anchor.AnchorProvider.env();
|
||||
const program = anchor.workspace.Optional as Program<Optional>;
|
||||
|
||||
const DATA_PDA_PREFIX = "data_pda";
|
||||
|
||||
const makeDataPdaSeeds = (dataAccount: web3.PublicKey) => {
|
||||
return [Buffer.from(DATA_PDA_PREFIX), dataAccount.toBuffer()];
|
||||
};
|
||||
|
||||
const findDataPda = (
|
||||
dataAccount: web3.PublicKey
|
||||
): [web3.PublicKey, number] => {
|
||||
return web3.PublicKey.findProgramAddressSync(
|
||||
makeDataPdaSeeds(dataAccount),
|
||||
program.programId
|
||||
);
|
||||
};
|
||||
|
||||
// payer of the transactions
|
||||
const payerWallet = (program.provider as anchor.AnchorProvider).wallet;
|
||||
const payer = payerWallet.publicKey;
|
||||
const systemProgram = web3.SystemProgram.programId;
|
||||
|
||||
const requiredKeypair1 = web3.Keypair.generate();
|
||||
const requiredKeypair2 = web3.Keypair.generate();
|
||||
|
||||
let createRequiredIx1: web3.TransactionInstruction;
|
||||
let createRequiredIx2: web3.TransactionInstruction;
|
||||
|
||||
const dataAccountKeypair1 = web3.Keypair.generate();
|
||||
const dataAccountKeypair2 = web3.Keypair.generate();
|
||||
|
||||
const dataPda1 = findDataPda(dataAccountKeypair1.publicKey);
|
||||
const dataPda2 = findDataPda(dataAccountKeypair2.publicKey);
|
||||
|
||||
const initializeValue1 = new BN(10);
|
||||
const initializeValue2 = new BN(100);
|
||||
const initializeKey = web3.PublicKey.default;
|
||||
|
||||
const createRequired = async (
|
||||
requiredKeypair?: web3.Keypair
|
||||
): Promise<[web3.Keypair, web3.TransactionInstruction]> => {
|
||||
const keypair = requiredKeypair ?? new web3.Keypair();
|
||||
const createIx = await program.account.dataAccount.createInstruction(
|
||||
keypair
|
||||
);
|
||||
return [keypair, createIx];
|
||||
};
|
||||
|
||||
before("Setup async stuff", async () => {
|
||||
createRequiredIx1 = (await createRequired(requiredKeypair1))[1];
|
||||
createRequiredIx2 = (await createRequired(requiredKeypair2))[1];
|
||||
});
|
||||
|
||||
describe("Missing optionals feature tests", async () => {
|
||||
it("Fails with missing optional accounts at the end by default", async () => {
|
||||
const [requiredKeypair, createRequiredIx] = await createRequired();
|
||||
const initializeIx = await program.methods
|
||||
.initialize(initializeValue1, initializeKey)
|
||||
.accounts({
|
||||
payer: null,
|
||||
optionalAccount: null,
|
||||
systemProgram,
|
||||
required: requiredKeypair.publicKey,
|
||||
optionalPda: null,
|
||||
})
|
||||
.signers([requiredKeypair])
|
||||
.instruction();
|
||||
initializeIx.keys.pop();
|
||||
const initializeTxn = new web3.Transaction()
|
||||
.add(createRequiredIx)
|
||||
.add(initializeIx);
|
||||
try {
|
||||
await anchorProvider
|
||||
.sendAndConfirm(initializeTxn, [requiredKeypair])
|
||||
.catch((e) => {
|
||||
throw translateError(e, parseIdlErrors(program.idl));
|
||||
});
|
||||
assert.fail(
|
||||
"Unexpected success in creating a transaction that should have failed with `AccountNotEnoughKeys` error"
|
||||
);
|
||||
} catch (e) {
|
||||
// @ts-ignore
|
||||
assert.isTrue(e instanceof AnchorError, e.toString());
|
||||
const err: AnchorError = <AnchorError>e;
|
||||
const errorCode = LangErrorCode.AccountNotEnoughKeys;
|
||||
assert.strictEqual(
|
||||
err.error.errorMessage,
|
||||
LangErrorMessage.get(errorCode)
|
||||
);
|
||||
assert.strictEqual(err.error.errorCode.number, errorCode);
|
||||
}
|
||||
});
|
||||
|
||||
it("Succeeds with missing optional accounts at the end with the feature on", async () => {
|
||||
const allowMissingOptionals = anchor.workspace
|
||||
.AllowMissingOptionals as Program<AllowMissingOptionals>;
|
||||
const doStuffIx = await allowMissingOptionals.methods
|
||||
.doStuff()
|
||||
.accounts({
|
||||
payer,
|
||||
systemProgram,
|
||||
optional2: null,
|
||||
})
|
||||
.instruction();
|
||||
doStuffIx.keys.pop();
|
||||
doStuffIx.keys.pop();
|
||||
const doStuffTxn = new web3.Transaction().add(doStuffIx);
|
||||
await anchorProvider.sendAndConfirm(doStuffTxn);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Initialize tests", async () => {
|
||||
it("Initialize with required null fails anchor-ts validation", async () => {
|
||||
const [requiredKeypair, createRequiredIx] = await createRequired();
|
||||
try {
|
||||
await program.methods
|
||||
.initialize(initializeValue1, initializeKey)
|
||||
.preInstructions([createRequiredIx])
|
||||
.accounts({
|
||||
payer,
|
||||
systemProgram,
|
||||
// @ts-ignore
|
||||
required: null, //requiredKeypair.publicKey,
|
||||
optionalPda: null,
|
||||
optionalAccount: null,
|
||||
})
|
||||
.signers([requiredKeypair])
|
||||
.rpc();
|
||||
assert.fail(
|
||||
"Unexpected success in creating a transaction that should have failed at the client level"
|
||||
);
|
||||
} catch (e) {
|
||||
const errMsg = "Invalid arguments: required not provided";
|
||||
// @ts-ignore
|
||||
let error: string = e.toString();
|
||||
assert(error.includes(errMsg), `Unexpected error: ${e}`);
|
||||
}
|
||||
});
|
||||
|
||||
it("Can initialize with no payer and no optionals", async () => {
|
||||
const [requiredKeypair, createRequiredIx] = await createRequired();
|
||||
await program.methods
|
||||
.initialize(initializeValue1, initializeKey)
|
||||
.preInstructions([createRequiredIx])
|
||||
.accounts({
|
||||
payer: null,
|
||||
systemProgram,
|
||||
required: requiredKeypair.publicKey,
|
||||
optionalPda: null,
|
||||
optionalAccount: null,
|
||||
})
|
||||
.signers([requiredKeypair])
|
||||
.rpc();
|
||||
|
||||
let required = await program.account.dataAccount.fetch(
|
||||
requiredKeypair.publicKey
|
||||
);
|
||||
expect(required.data.toNumber()).to.equal(0);
|
||||
});
|
||||
|
||||
it("Can initialize with no optionals", async () => {
|
||||
const [requiredKeypair, createRequiredIx] = await createRequired();
|
||||
await program.methods
|
||||
.initialize(initializeValue1, initializeKey)
|
||||
.preInstructions([createRequiredIx])
|
||||
.accounts({
|
||||
payer: null,
|
||||
systemProgram: null,
|
||||
required: requiredKeypair.publicKey,
|
||||
optionalPda: null,
|
||||
optionalAccount: null,
|
||||
})
|
||||
.signers([requiredKeypair])
|
||||
.rpc();
|
||||
|
||||
let required = await program.account.dataAccount.fetch(
|
||||
requiredKeypair.publicKey
|
||||
);
|
||||
expect(required.data.toNumber()).to.equal(0);
|
||||
});
|
||||
|
||||
it("Initialize with optionals and missing system program fails optional checks", async () => {
|
||||
const [requiredKeypair, createRequiredIx] = await createRequired();
|
||||
const dataAccount = new web3.Keypair();
|
||||
try {
|
||||
await program.methods
|
||||
.initialize(initializeValue1, initializeKey)
|
||||
.preInstructions([createRequiredIx])
|
||||
.accounts({
|
||||
payer,
|
||||
systemProgram: null,
|
||||
required: requiredKeypair.publicKey,
|
||||
optionalPda: null,
|
||||
optionalAccount: dataAccount.publicKey,
|
||||
})
|
||||
.signers([requiredKeypair, dataAccount])
|
||||
.rpc();
|
||||
assert.fail(
|
||||
"Unexpected success in creating a transaction that should have failed with `ConstraintAccountIsNone` error"
|
||||
);
|
||||
} catch (e) {
|
||||
// @ts-ignore
|
||||
assert.isTrue(e instanceof AnchorError, e.toString());
|
||||
const err: AnchorError = <AnchorError>e;
|
||||
const errorCode = LangErrorCode.ConstraintAccountIsNone;
|
||||
assert.strictEqual(
|
||||
err.error.errorMessage,
|
||||
LangErrorMessage.get(errorCode)
|
||||
);
|
||||
assert.strictEqual(err.error.errorCode.number, errorCode);
|
||||
}
|
||||
});
|
||||
|
||||
it("Unwrapping None account in constraint panics", async () => {
|
||||
const [requiredKeypair, createRequiredIx] = await createRequired();
|
||||
const dataAccount = new web3.Keypair();
|
||||
const [dataPda] = findDataPda(dataAccount.publicKey);
|
||||
try {
|
||||
await program.methods
|
||||
.initialize(initializeValue1, initializeKey)
|
||||
.preInstructions([createRequiredIx])
|
||||
.accounts({
|
||||
payer,
|
||||
systemProgram,
|
||||
required: requiredKeypair.publicKey,
|
||||
optionalPda: dataPda,
|
||||
optionalAccount: null,
|
||||
})
|
||||
.signers([requiredKeypair])
|
||||
.rpc();
|
||||
assert.fail(
|
||||
"Unexpected success in creating a transaction that should have failed with `ProgramFailedToComplete` error"
|
||||
);
|
||||
} catch (e) {
|
||||
const errMsg = "Program failed to complete";
|
||||
// @ts-ignore
|
||||
let error: string = e.toString();
|
||||
assert(error.includes(errMsg), `Unexpected error: ${e}`);
|
||||
}
|
||||
});
|
||||
|
||||
it("Can initialize with required and optional account", async () => {
|
||||
await program.methods
|
||||
.initialize(initializeValue1, initializeKey)
|
||||
.preInstructions([createRequiredIx1])
|
||||
.accounts({
|
||||
payer,
|
||||
systemProgram,
|
||||
required: requiredKeypair1.publicKey,
|
||||
optionalPda: null,
|
||||
optionalAccount: dataAccountKeypair1.publicKey,
|
||||
})
|
||||
.signers([requiredKeypair1, dataAccountKeypair1])
|
||||
.rpc();
|
||||
|
||||
const requiredDataAccount = await program.account.dataAccount.fetch(
|
||||
requiredKeypair1.publicKey
|
||||
);
|
||||
expect(requiredDataAccount.data.toNumber()).to.equal(0);
|
||||
|
||||
const optionalDataAccount = await program.account.dataAccount.fetch(
|
||||
dataAccountKeypair1.publicKey
|
||||
);
|
||||
expect(optionalDataAccount.data.toNumber()).to.equal(
|
||||
initializeValue1.muln(2).toNumber()
|
||||
);
|
||||
});
|
||||
|
||||
it("Invalid seeds with all accounts provided fails", async () => {
|
||||
try {
|
||||
await program.methods
|
||||
.initialize(initializeValue2, initializeKey)
|
||||
.preInstructions([createRequiredIx2])
|
||||
.accounts({
|
||||
payer,
|
||||
systemProgram,
|
||||
required: requiredKeypair2.publicKey,
|
||||
optionalPda: dataPda1[0],
|
||||
optionalAccount: dataAccountKeypair2.publicKey,
|
||||
})
|
||||
.signers([requiredKeypair2, dataAccountKeypair2])
|
||||
.rpc();
|
||||
assert.fail(
|
||||
"Unexpected success in creating a transaction that should have failed with `ConstraintSeeds` error"
|
||||
);
|
||||
} catch (e) {
|
||||
// @ts-ignore
|
||||
assert.isTrue(e instanceof AnchorError, e.toString());
|
||||
const err: AnchorError = <AnchorError>e;
|
||||
const errorCode = LangErrorCode.ConstraintSeeds;
|
||||
assert.strictEqual(
|
||||
err.error.errorMessage,
|
||||
LangErrorMessage.get(errorCode)
|
||||
);
|
||||
assert.strictEqual(err.error.errorCode.number, errorCode);
|
||||
}
|
||||
});
|
||||
|
||||
it("Can initialize with all accounts provided", async () => {
|
||||
await program.methods
|
||||
.initialize(initializeValue2, initializeKey)
|
||||
.preInstructions([createRequiredIx2])
|
||||
.accounts({
|
||||
payer,
|
||||
systemProgram,
|
||||
required: requiredKeypair2.publicKey,
|
||||
optionalPda: dataPda2[0],
|
||||
optionalAccount: dataAccountKeypair2.publicKey,
|
||||
})
|
||||
.signers([requiredKeypair2, dataAccountKeypair2])
|
||||
.rpc();
|
||||
|
||||
const requiredDataAccount = await program.account.dataAccount.fetch(
|
||||
requiredKeypair2.publicKey
|
||||
);
|
||||
expect(requiredDataAccount.data.toNumber()).to.equal(0);
|
||||
|
||||
const optionalDataAccount = await program.account.dataAccount.fetch(
|
||||
dataAccountKeypair2.publicKey
|
||||
);
|
||||
expect(optionalDataAccount.data.toNumber()).to.equal(
|
||||
initializeValue2.toNumber()
|
||||
);
|
||||
|
||||
const optionalDataPda = await program.account.dataPda.fetch(dataPda2[0]);
|
||||
expect(optionalDataPda.dataAccount.toString()).to.equal(
|
||||
initializeKey.toString()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Update tests", async () => {
|
||||
it("Can update with invalid explicit pda bump with no pda", async () => {
|
||||
await program.methods
|
||||
.update(initializeValue2, initializeKey, dataPda2[1] - 1)
|
||||
.accounts({
|
||||
payer,
|
||||
optionalPda: null,
|
||||
optionalAccount: null,
|
||||
})
|
||||
.rpc();
|
||||
});
|
||||
|
||||
it("Errors with invalid explicit pda bump with pda included", async () => {
|
||||
try {
|
||||
await program.methods
|
||||
.update(initializeValue2, initializeKey, dataPda2[1] - 1)
|
||||
.accounts({
|
||||
payer,
|
||||
optionalPda: dataPda2[0],
|
||||
optionalAccount: dataAccountKeypair2.publicKey,
|
||||
})
|
||||
.signers([dataAccountKeypair2])
|
||||
.rpc();
|
||||
assert.fail(
|
||||
"Unexpected success in creating a transaction that should have failed with `ConstraintSeeds` error"
|
||||
);
|
||||
} catch (e) {
|
||||
// @ts-ignore
|
||||
assert.isTrue(e instanceof AnchorError, e.toString());
|
||||
const err: AnchorError = <AnchorError>e;
|
||||
const errorCode = LangErrorCode.ConstraintSeeds;
|
||||
assert.strictEqual(
|
||||
err.error.errorMessage,
|
||||
LangErrorMessage.get(errorCode)
|
||||
);
|
||||
assert.strictEqual(err.error.errorCode.number, errorCode);
|
||||
}
|
||||
});
|
||||
|
||||
it("Fails with a missing signer", async () => {
|
||||
try {
|
||||
let txn = await program.methods
|
||||
.update(initializeValue2, initializeKey, dataPda2[1])
|
||||
.accounts({
|
||||
payer,
|
||||
optionalPda: dataPda2[0],
|
||||
optionalAccount: dataAccountKeypair2.publicKey,
|
||||
})
|
||||
.transaction();
|
||||
txn.instructions[0].keys.forEach((meta) => {
|
||||
if (meta.pubkey.equals(dataAccountKeypair2.publicKey)) {
|
||||
meta.isSigner = false;
|
||||
}
|
||||
});
|
||||
await anchorProvider.sendAndConfirm(txn);
|
||||
assert.fail(
|
||||
"Unexpected success in creating a transaction that should have failed with `ConstraintSigner` error"
|
||||
);
|
||||
} catch (e) {
|
||||
// @ts-ignore
|
||||
assert.isTrue(e instanceof web3.SendTransactionError, e.toString());
|
||||
const err: web3.SendTransactionError = <web3.SendTransactionError>e;
|
||||
const anchorError = AnchorError.parse(err.logs!)!;
|
||||
const errorCode = LangErrorCode.ConstraintSigner;
|
||||
assert.strictEqual(
|
||||
anchorError.error.errorMessage,
|
||||
LangErrorMessage.get(errorCode)
|
||||
);
|
||||
assert.strictEqual(anchorError.error.errorCode.number, errorCode);
|
||||
}
|
||||
});
|
||||
|
||||
it("Can trigger raw constraint violations with references to optional accounts", async () => {
|
||||
try {
|
||||
await program.methods
|
||||
.update(initializeValue2, initializeKey, dataPda2[1])
|
||||
.accounts({
|
||||
payer: null,
|
||||
optionalPda: dataPda2[0],
|
||||
optionalAccount: dataAccountKeypair2.publicKey,
|
||||
})
|
||||
.signers([dataAccountKeypair2])
|
||||
.rpc();
|
||||
assert.fail(
|
||||
"Unexpected success in creating a transaction that should have failed with `ConstraintRaw` error"
|
||||
);
|
||||
} catch (e) {
|
||||
// @ts-ignore
|
||||
assert.isTrue(e instanceof AnchorError, e.toString());
|
||||
const err: AnchorError = <AnchorError>e;
|
||||
const errorCode = LangErrorCode.ConstraintRaw;
|
||||
assert.strictEqual(
|
||||
err.error.errorMessage,
|
||||
LangErrorMessage.get(errorCode)
|
||||
);
|
||||
assert.strictEqual(err.error.errorCode.number, errorCode);
|
||||
}
|
||||
});
|
||||
|
||||
it("Can update an optional account", async () => {
|
||||
await program.methods
|
||||
.update(initializeValue2.muln(3), initializeKey, dataPda2[1])
|
||||
.accounts({
|
||||
payer,
|
||||
optionalPda: null,
|
||||
optionalAccount: dataAccountKeypair2.publicKey,
|
||||
})
|
||||
.signers([dataAccountKeypair2])
|
||||
.rpc();
|
||||
|
||||
const dataAccount = await program.account.dataAccount.fetch(
|
||||
dataAccountKeypair2.publicKey
|
||||
);
|
||||
expect(dataAccount.data.toNumber()).to.equal(
|
||||
initializeValue2.muln(3).toNumber()
|
||||
);
|
||||
});
|
||||
|
||||
it("Can update both accounts", async () => {
|
||||
const newKey = web3.PublicKey.unique();
|
||||
await program.methods
|
||||
.update(initializeValue2, newKey, dataPda2[1])
|
||||
.accounts({
|
||||
payer,
|
||||
optionalPda: dataPda2[0],
|
||||
optionalAccount: dataAccountKeypair2.publicKey,
|
||||
})
|
||||
.signers([dataAccountKeypair2])
|
||||
.rpc();
|
||||
|
||||
const dataPda = await program.account.dataPda.fetch(dataPda2[0]);
|
||||
expect(dataPda.dataAccount.toString()).to.equal(newKey.toString());
|
||||
|
||||
const dataAccount = await program.account.dataAccount.fetch(
|
||||
dataAccountKeypair2.publicKey
|
||||
);
|
||||
expect(dataAccount.data.toNumber()).to.equal(initializeValue2.toNumber());
|
||||
});
|
||||
});
|
||||
|
||||
describe("Realloc tests", async () => {
|
||||
it("Realloc with no payer fails", async () => {
|
||||
try {
|
||||
await program.methods
|
||||
.realloc(new BN(100))
|
||||
.accounts({
|
||||
payer: null,
|
||||
required: dataAccountKeypair1.publicKey,
|
||||
optionalPda: null,
|
||||
optionalAccount: dataAccountKeypair2.publicKey,
|
||||
systemProgram,
|
||||
})
|
||||
.signers([dataAccountKeypair2])
|
||||
.rpc();
|
||||
assert.fail(
|
||||
"Unexpected success in creating a transaction that should have failed with `ConstraintAccountIsNone` error"
|
||||
);
|
||||
} catch (e) {
|
||||
// @ts-ignore
|
||||
assert.isTrue(e instanceof AnchorError, e.toString());
|
||||
const err: AnchorError = <AnchorError>e;
|
||||
const errorCode = LangErrorCode.ConstraintAccountIsNone;
|
||||
assert.strictEqual(
|
||||
err.error.errorMessage,
|
||||
LangErrorMessage.get(errorCode)
|
||||
);
|
||||
assert.strictEqual(err.error.errorCode.number, errorCode);
|
||||
}
|
||||
});
|
||||
|
||||
it("Realloc with no system program fails", async () => {
|
||||
try {
|
||||
await program.methods
|
||||
.realloc(new BN(100))
|
||||
.accounts({
|
||||
payer,
|
||||
required: dataAccountKeypair1.publicKey,
|
||||
optionalPda: null,
|
||||
optionalAccount: dataAccountKeypair2.publicKey,
|
||||
systemProgram: null,
|
||||
})
|
||||
.signers([dataAccountKeypair2])
|
||||
.rpc();
|
||||
assert.fail(
|
||||
"Unexpected success in creating a transaction that should have failed with `ConstraintAccountIsNone` error"
|
||||
);
|
||||
} catch (e) {
|
||||
// @ts-ignore
|
||||
assert.isTrue(e instanceof AnchorError, e.toString());
|
||||
const err: AnchorError = <AnchorError>e;
|
||||
const errorCode = LangErrorCode.ConstraintAccountIsNone;
|
||||
assert.strictEqual(
|
||||
err.error.errorMessage,
|
||||
LangErrorMessage.get(errorCode)
|
||||
);
|
||||
assert.strictEqual(err.error.errorCode.number, errorCode);
|
||||
}
|
||||
});
|
||||
|
||||
it("Wrong type of account is caught for optional accounts", async () => {
|
||||
try {
|
||||
await program.methods
|
||||
.realloc(new BN(100))
|
||||
.accounts({
|
||||
payer,
|
||||
required: dataAccountKeypair1.publicKey,
|
||||
optionalPda: dataAccountKeypair2.publicKey,
|
||||
optionalAccount: null,
|
||||
systemProgram,
|
||||
})
|
||||
.rpc();
|
||||
assert.fail(
|
||||
"Unexpected success in creating a transaction that should have failed with `AccountDiscriminatorMismatch` error"
|
||||
);
|
||||
} catch (e) {
|
||||
// @ts-ignore
|
||||
assert.isTrue(e instanceof AnchorError, e.toString());
|
||||
const err: AnchorError = <AnchorError>e;
|
||||
const errorCode = LangErrorCode.AccountDiscriminatorMismatch;
|
||||
assert.strictEqual(
|
||||
err.error.errorMessage,
|
||||
LangErrorMessage.get(errorCode)
|
||||
);
|
||||
assert.strictEqual(err.error.errorCode.number, errorCode);
|
||||
}
|
||||
});
|
||||
|
||||
it("Can realloc with optional accounts", async () => {
|
||||
const newLength = 100;
|
||||
await program.methods
|
||||
.realloc(new BN(newLength))
|
||||
.accounts({
|
||||
payer,
|
||||
required: dataAccountKeypair1.publicKey,
|
||||
optionalPda: null,
|
||||
optionalAccount: dataAccountKeypair2.publicKey,
|
||||
systemProgram,
|
||||
})
|
||||
.signers([dataAccountKeypair2])
|
||||
.rpc();
|
||||
const dataAccount = await program.provider.connection.getAccountInfo(
|
||||
dataAccountKeypair2.publicKey
|
||||
);
|
||||
assert.exists(dataAccount);
|
||||
expect(dataAccount!.data.length).to.equal(newLength);
|
||||
});
|
||||
|
||||
it("Can realloc back to original size with optional accounts", async () => {
|
||||
const newLength = program.account.dataAccount.size;
|
||||
await program.methods
|
||||
.realloc(new BN(newLength))
|
||||
.accounts({
|
||||
payer,
|
||||
required: dataAccountKeypair1.publicKey,
|
||||
optionalPda: null,
|
||||
optionalAccount: dataAccountKeypair2.publicKey,
|
||||
systemProgram,
|
||||
})
|
||||
.signers([dataAccountKeypair2])
|
||||
.rpc();
|
||||
const dataAccount = await program.provider.connection.getAccountInfo(
|
||||
dataAccountKeypair2.publicKey
|
||||
);
|
||||
assert.exists(dataAccount);
|
||||
expect(dataAccount!.data.length).to.equal(newLength);
|
||||
});
|
||||
|
||||
it("Can realloc multiple optional accounts", async () => {
|
||||
const newLength = 100;
|
||||
await program.methods
|
||||
.realloc(new BN(newLength))
|
||||
.accounts({
|
||||
payer,
|
||||
required: dataAccountKeypair1.publicKey,
|
||||
optionalPda: dataPda2[0],
|
||||
optionalAccount: dataAccountKeypair2.publicKey,
|
||||
systemProgram,
|
||||
})
|
||||
.signers([dataAccountKeypair2])
|
||||
.rpc();
|
||||
const dataAccount = await program.provider.connection.getAccountInfo(
|
||||
dataAccountKeypair2.publicKey
|
||||
);
|
||||
assert.exists(dataAccount);
|
||||
expect(dataAccount!.data.length).to.equal(newLength);
|
||||
|
||||
const dataPda = await program.provider.connection.getAccountInfo(
|
||||
dataPda2[0]
|
||||
);
|
||||
assert.exists(dataPda);
|
||||
expect(dataPda!.data.length).to.equal(newLength);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Close tests", async () => {
|
||||
const requiredKeypair3 = web3.Keypair.generate();
|
||||
const requiredKeypair4 = web3.Keypair.generate();
|
||||
|
||||
let createRequiredIx3: web3.TransactionInstruction;
|
||||
let createRequiredIx4: web3.TransactionInstruction;
|
||||
|
||||
const dataAccountKeypair3 = web3.Keypair.generate();
|
||||
const dataAccountKeypair4 = web3.Keypair.generate();
|
||||
|
||||
const dataPda3 = findDataPda(dataAccountKeypair3.publicKey);
|
||||
const dataPda4 = findDataPda(dataAccountKeypair4.publicKey);
|
||||
|
||||
const initializeValue3 = new BN(50);
|
||||
const initializeValue4 = new BN(1000);
|
||||
|
||||
before("Setup additional accounts", async () => {
|
||||
createRequiredIx3 = (await createRequired(requiredKeypair3))[1];
|
||||
createRequiredIx4 = (await createRequired(requiredKeypair4))[1];
|
||||
const assertInitSuccess = async (
|
||||
requiredPubkey: web3.PublicKey,
|
||||
dataPdaPubkey: web3.PublicKey,
|
||||
dataAccountPubkey: web3.PublicKey,
|
||||
initializeValue: BN
|
||||
) => {
|
||||
const requiredDataAccount = await program.account.dataAccount.fetch(
|
||||
requiredPubkey
|
||||
);
|
||||
expect(requiredDataAccount.data.toNumber()).to.equal(0);
|
||||
|
||||
const optionalDataAccount = await program.account.dataAccount.fetch(
|
||||
dataAccountPubkey
|
||||
);
|
||||
expect(optionalDataAccount.data.toNumber()).to.equal(
|
||||
initializeValue.toNumber()
|
||||
);
|
||||
|
||||
const optionalDataPda = await program.account.dataPda.fetch(
|
||||
dataPdaPubkey
|
||||
);
|
||||
expect(optionalDataPda.dataAccount.toString()).to.equal(
|
||||
initializeKey.toString()
|
||||
);
|
||||
};
|
||||
|
||||
await program.methods
|
||||
.initialize(initializeValue3, initializeKey)
|
||||
.preInstructions([createRequiredIx3])
|
||||
.accounts({
|
||||
payer,
|
||||
systemProgram,
|
||||
required: requiredKeypair3.publicKey,
|
||||
optionalPda: dataPda3[0],
|
||||
optionalAccount: dataAccountKeypair3.publicKey,
|
||||
})
|
||||
.signers([requiredKeypair3, dataAccountKeypair3])
|
||||
.rpc();
|
||||
await assertInitSuccess(
|
||||
requiredKeypair3.publicKey,
|
||||
dataPda3[0],
|
||||
dataAccountKeypair3.publicKey,
|
||||
initializeValue3
|
||||
);
|
||||
await program.methods
|
||||
.initialize(initializeValue4, initializeKey)
|
||||
.preInstructions([createRequiredIx4])
|
||||
.accounts({
|
||||
payer,
|
||||
systemProgram,
|
||||
required: requiredKeypair4.publicKey,
|
||||
optionalPda: dataPda4[0],
|
||||
optionalAccount: dataAccountKeypair4.publicKey,
|
||||
})
|
||||
.signers([requiredKeypair4, dataAccountKeypair4])
|
||||
.rpc();
|
||||
await assertInitSuccess(
|
||||
requiredKeypair4.publicKey,
|
||||
dataPda4[0],
|
||||
dataAccountKeypair4.publicKey,
|
||||
initializeValue4
|
||||
);
|
||||
|
||||
await program.methods
|
||||
.update(initializeValue3, dataAccountKeypair3.publicKey, dataPda3[1])
|
||||
.accounts({
|
||||
payer,
|
||||
optionalPda: dataPda3[0],
|
||||
optionalAccount: dataAccountKeypair3.publicKey,
|
||||
})
|
||||
.signers([dataAccountKeypair3])
|
||||
.rpc();
|
||||
const optionalPda3 = await program.account.dataPda.fetch(dataPda3[0]);
|
||||
expect(optionalPda3.dataAccount.toString()).to.equal(
|
||||
dataAccountKeypair3.publicKey.toString()
|
||||
);
|
||||
await program.methods
|
||||
.update(initializeValue4, dataAccountKeypair4.publicKey, dataPda4[1])
|
||||
.accounts({
|
||||
payer,
|
||||
optionalPda: dataPda4[0],
|
||||
optionalAccount: dataAccountKeypair4.publicKey,
|
||||
})
|
||||
.signers([dataAccountKeypair4])
|
||||
.rpc();
|
||||
const optionalPda4 = await program.account.dataPda.fetch(dataPda4[0]);
|
||||
expect(optionalPda4.dataAccount.toString()).to.equal(
|
||||
dataAccountKeypair4.publicKey.toString()
|
||||
);
|
||||
});
|
||||
|
||||
it("Close with no close target fails", async () => {
|
||||
try {
|
||||
await program.methods
|
||||
.close()
|
||||
.accounts({
|
||||
payer: null,
|
||||
optionalPda: null,
|
||||
dataAccount: dataAccountKeypair3.publicKey,
|
||||
systemProgram,
|
||||
})
|
||||
.signers([dataAccountKeypair3])
|
||||
.rpc();
|
||||
assert.fail(
|
||||
"Unexpected success in creating a transaction that should have failed with `ConstraintRaw` error"
|
||||
);
|
||||
} catch (e) {
|
||||
// @ts-ignore
|
||||
assert.isTrue(e instanceof AnchorError, e.toString());
|
||||
const err: AnchorError = <AnchorError>e;
|
||||
const errorCode = LangErrorCode.ConstraintAccountIsNone;
|
||||
assert.strictEqual(
|
||||
err.error.errorMessage,
|
||||
LangErrorMessage.get(errorCode)
|
||||
);
|
||||
assert.strictEqual(err.error.errorCode.number, errorCode);
|
||||
}
|
||||
});
|
||||
|
||||
it("Has one constraints are caught with optional accounts", async () => {
|
||||
try {
|
||||
await program.methods
|
||||
.close()
|
||||
.accounts({
|
||||
payer,
|
||||
optionalPda: dataPda4[0],
|
||||
dataAccount: dataAccountKeypair3.publicKey,
|
||||
systemProgram,
|
||||
})
|
||||
.signers([dataAccountKeypair3])
|
||||
.rpc();
|
||||
assert.fail(
|
||||
"Unexpected success in creating a transaction that should have failed with `ConstraintHasOne` error"
|
||||
);
|
||||
} catch (e) {
|
||||
// @ts-ignore
|
||||
assert.isTrue(e instanceof AnchorError, e.toString());
|
||||
const err: AnchorError = <AnchorError>e;
|
||||
const errorCode = LangErrorCode.ConstraintHasOne;
|
||||
assert.strictEqual(
|
||||
err.error.errorMessage,
|
||||
LangErrorMessage.get(errorCode)
|
||||
);
|
||||
assert.strictEqual(err.error.errorCode.number, errorCode);
|
||||
}
|
||||
});
|
||||
|
||||
it("Can close an optional account", async () => {
|
||||
await program.methods
|
||||
.close()
|
||||
.accounts({
|
||||
payer,
|
||||
optionalPda: null,
|
||||
dataAccount: dataAccountKeypair3.publicKey,
|
||||
systemProgram,
|
||||
})
|
||||
.signers([dataAccountKeypair3])
|
||||
.rpc();
|
||||
const dataAccount = await program.provider.connection.getAccountInfo(
|
||||
dataAccountKeypair3.publicKey
|
||||
);
|
||||
assert.isNull(dataAccount);
|
||||
});
|
||||
|
||||
it("Can close multiple optional accounts", async () => {
|
||||
await program.methods
|
||||
.close()
|
||||
.accounts({
|
||||
payer,
|
||||
optionalPda: dataPda4[0],
|
||||
dataAccount: dataAccountKeypair4.publicKey,
|
||||
systemProgram,
|
||||
})
|
||||
.signers([dataAccountKeypair4])
|
||||
.rpc();
|
||||
const dataAccount = await program.provider.connection.getAccountInfo(
|
||||
dataAccountKeypair4.publicKey
|
||||
);
|
||||
assert.isNull(dataAccount);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"types": ["mocha", "chai"],
|
||||
"typeRoots": ["./node_modules/@types"],
|
||||
"lib": ["es2015"],
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true
|
||||
}
|
||||
}
|
|
@ -21,6 +21,7 @@
|
|||
"lockup",
|
||||
"misc",
|
||||
"multisig",
|
||||
"optional",
|
||||
"permissioned-markets",
|
||||
"pda-derivation",
|
||||
"relations-derivation",
|
||||
|
@ -47,12 +48,16 @@
|
|||
"@project-serum/common": "^0.0.1-beta.3",
|
||||
"@project-serum/serum": "^0.13.60",
|
||||
"@solana/spl-token": "^0.1.8",
|
||||
"@solana/web3.js": "^1.64.0"
|
||||
"@solana/web3.js": "^1.68.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"@project-serum/anchor/@solana/web3.js": "^1.68.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.3.0",
|
||||
"@types/mocha": "^9.1.0",
|
||||
"@types/node": "^14.14.37",
|
||||
"@types/bn.js": "^5.1.1",
|
||||
"chai": "^4.3.4",
|
||||
"mocha": "^10.0.0",
|
||||
"prettier": "^2.5.1",
|
||||
|
|
233
tests/yarn.lock
233
tests/yarn.lock
|
@ -2,7 +2,7 @@
|
|||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@babel/runtime@^7.10.5", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5":
|
||||
"@babel/runtime@^7.10.5", "@babel/runtime@^7.12.5":
|
||||
version "7.16.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5"
|
||||
integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==
|
||||
|
@ -16,26 +16,13 @@
|
|||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@ethersproject/bytes@^5.5.0":
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.5.0.tgz#cb11c526de657e7b45d2e0f0246fb3b9d29a601c"
|
||||
integrity sha512-ABvc7BHWhZU9PNM/tANm/Qx4ostPGadAuQzWTr3doklZOhDlmcBqclrQe/ZXUIj3K8wC28oYeuRa+A37tX9kog==
|
||||
"@coral-xyz/borsh@^0.2.6":
|
||||
version "0.2.6"
|
||||
resolved "https://registry.yarnpkg.com/@coral-xyz/borsh/-/borsh-0.2.6.tgz#0f11b223bf2967574310705afd3c53ce26688ada"
|
||||
integrity sha512-y6nmHw1bFcJib7sMHsQPpC8r47xhqDZVvhUdna7NUPzpSbOZG6f46N21+aXsQ2w/tG8Ggls488J/ZmwbgVmyjg==
|
||||
dependencies:
|
||||
"@ethersproject/logger" "^5.5.0"
|
||||
|
||||
"@ethersproject/logger@^5.5.0":
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.5.0.tgz#0c2caebeff98e10aefa5aef27d7441c7fd18cf5d"
|
||||
integrity sha512-rIY/6WPm7T8n3qS2vuHTUBPdXHl+rGxWxW5okDfo9J4Z0+gRRZT0msvUdIJkE4/HS29GUMziwGaaKO2bWONBrg==
|
||||
|
||||
"@ethersproject/sha2@^5.5.0":
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.5.0.tgz#a40a054c61f98fd9eee99af2c3cc6ff57ec24db7"
|
||||
integrity sha512-B5UBoglbCiHamRVPLA110J+2uqsifpZaTmid2/7W5rbtYVz6gus6/hSDieIU/6gaKIDcOj12WnOdiymEUHIAOA==
|
||||
dependencies:
|
||||
"@ethersproject/bytes" "^5.5.0"
|
||||
"@ethersproject/logger" "^5.5.0"
|
||||
hash.js "1.1.7"
|
||||
bn.js "^5.1.2"
|
||||
buffer-layout "^1.2.0"
|
||||
|
||||
"@native-to-anchor/buffer-layout@=0.1.0":
|
||||
version "0.1.0"
|
||||
|
@ -63,8 +50,8 @@
|
|||
"@project-serum/anchor@=0.25.0", "@project-serum/anchor@file:../ts/packages/anchor":
|
||||
version "0.25.0"
|
||||
dependencies:
|
||||
"@project-serum/borsh" "^0.2.5"
|
||||
"@solana/web3.js" "^1.64.0"
|
||||
"@coral-xyz/borsh" "^0.2.6"
|
||||
"@solana/web3.js" "^1.68.0"
|
||||
base64-js "^1.5.1"
|
||||
bn.js "^5.1.2"
|
||||
bs58 "^4.0.1"
|
||||
|
@ -99,7 +86,7 @@
|
|||
snake-case "^3.0.4"
|
||||
toml "^3.0.0"
|
||||
|
||||
"@project-serum/borsh@^0.2.2", "@project-serum/borsh@^0.2.5":
|
||||
"@project-serum/borsh@^0.2.2":
|
||||
version "0.2.5"
|
||||
resolved "https://registry.yarnpkg.com/@project-serum/borsh/-/borsh-0.2.5.tgz#6059287aa624ecebbfc0edd35e4c28ff987d8663"
|
||||
integrity sha512-UmeUkUoKdQ7rhx6Leve1SssMR/Ghv8qrEiyywyxSWg7ooV7StdpPBhciiy5eB3T0qU1BXvdRNC8TdrkxK7WC5Q==
|
||||
|
@ -156,13 +143,6 @@
|
|||
dependencies:
|
||||
buffer "~6.0.3"
|
||||
|
||||
"@solana/buffer-layout@^3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@solana/buffer-layout/-/buffer-layout-3.0.0.tgz#b9353caeb9a1589cb77a1b145bcb1a9a93114326"
|
||||
integrity sha512-MVdgAKKL39tEs0l8je0hKaXLQFb7Rdfb0Xg2LjFZd8Lfdazkg6xiS98uAZrEKvaoF3i4M95ei9RydkGIDMeo3w==
|
||||
dependencies:
|
||||
buffer "~6.0.3"
|
||||
|
||||
"@solana/spl-token@^0.1.6", "@solana/spl-token@^0.1.8":
|
||||
version "0.1.8"
|
||||
resolved "https://registry.yarnpkg.com/@solana/spl-token/-/spl-token-0.1.8.tgz#f06e746341ef8d04165e21fc7f555492a2a0faa6"
|
||||
|
@ -175,48 +155,7 @@
|
|||
buffer-layout "^1.2.0"
|
||||
dotenv "10.0.0"
|
||||
|
||||
"@solana/web3.js@^1.17.0", "@solana/web3.js@^1.32.0":
|
||||
version "1.66.2"
|
||||
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.66.2.tgz#80b43c5868b846124fe3ebac7d3943930c3fa60c"
|
||||
integrity sha512-RyaHMR2jGmaesnYP045VLeBGfR/gAW3cvZHzMFGg7bkO+WOYOYp1nEllf0/la4U4qsYGKCsO9eEevR5fhHiVHg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
"@noble/ed25519" "^1.7.0"
|
||||
"@noble/hashes" "^1.1.2"
|
||||
"@noble/secp256k1" "^1.6.3"
|
||||
"@solana/buffer-layout" "^4.0.0"
|
||||
bigint-buffer "^1.1.5"
|
||||
bn.js "^5.0.0"
|
||||
borsh "^0.7.0"
|
||||
bs58 "^4.0.1"
|
||||
buffer "6.0.1"
|
||||
fast-stable-stringify "^1.0.0"
|
||||
jayson "^3.4.4"
|
||||
node-fetch "2"
|
||||
rpc-websockets "^7.5.0"
|
||||
superstruct "^0.14.2"
|
||||
|
||||
"@solana/web3.js@^1.21.0":
|
||||
version "1.30.2"
|
||||
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.30.2.tgz#e85da75e0825dc64f53eb64a1ff0115b27bec135"
|
||||
integrity sha512-hznCj+rkfvM5taRP3Z+l5lumB7IQnDrB4l55Wpsg4kDU9Zds8pE5YOH5Z9bbF/pUzZJKQjyBjnY/6kScBm3Ugg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
"@ethersproject/sha2" "^5.5.0"
|
||||
"@solana/buffer-layout" "^3.0.0"
|
||||
bn.js "^5.0.0"
|
||||
borsh "^0.4.0"
|
||||
bs58 "^4.0.1"
|
||||
buffer "6.0.1"
|
||||
cross-fetch "^3.1.4"
|
||||
jayson "^3.4.4"
|
||||
js-sha3 "^0.8.0"
|
||||
rpc-websockets "^7.4.2"
|
||||
secp256k1 "^4.0.2"
|
||||
superstruct "^0.14.2"
|
||||
tweetnacl "^1.0.0"
|
||||
|
||||
"@solana/web3.js@^1.64.0":
|
||||
"@solana/web3.js@^1.17.0", "@solana/web3.js@^1.21.0", "@solana/web3.js@^1.32.0":
|
||||
version "1.64.0"
|
||||
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.64.0.tgz#b7f5a976976039a0161242e94d6e1224ab5d30f9"
|
||||
integrity sha512-AcFaoy48GxSmzBryVwB88C/UPJd/UQa+nFrO/uPc8ww6RCjanZY2vEZxdfTZub+q1NMUckwXpPwF32jJLe7SPA==
|
||||
|
@ -237,10 +176,31 @@
|
|||
rpc-websockets "^7.5.0"
|
||||
superstruct "^0.14.2"
|
||||
|
||||
"@types/bn.js@^4.11.5":
|
||||
version "4.11.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-4.11.6.tgz#c306c70d9358aaea33cd4eda092a742b9505967c"
|
||||
integrity sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==
|
||||
"@solana/web3.js@^1.68.0":
|
||||
version "1.70.0"
|
||||
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.70.0.tgz#14ad207f431861397db85921aad8df4e8374e7c8"
|
||||
integrity sha512-HwdI9LaHaszfpzgxJI44iP68mJWUeqK1TeSheKQsGkH5zlVyGWGmim50MyDWu2vXiuL8Akf2xEMSrDYyLordgg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
"@noble/ed25519" "^1.7.0"
|
||||
"@noble/hashes" "^1.1.2"
|
||||
"@noble/secp256k1" "^1.6.3"
|
||||
"@solana/buffer-layout" "^4.0.0"
|
||||
bigint-buffer "^1.1.5"
|
||||
bn.js "^5.0.0"
|
||||
borsh "^0.7.0"
|
||||
bs58 "^4.0.1"
|
||||
buffer "6.0.1"
|
||||
fast-stable-stringify "^1.0.0"
|
||||
jayson "^3.4.4"
|
||||
node-fetch "2"
|
||||
rpc-websockets "^7.5.0"
|
||||
superstruct "^0.14.2"
|
||||
|
||||
"@types/bn.js@^5.1.1":
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.1.tgz#b51e1b55920a4ca26e9285ff79936bbdec910682"
|
||||
integrity sha512-qNrYbZqMx0uJAfKnKclPh+dTwK33KfLHYqtyODwd5HnXOjnkhc4qgn3BrK6RWyGZm5+sIFE7Q7Vz6QQtJB7w7g==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
|
@ -406,11 +366,6 @@ bindings@^1.3.0:
|
|||
dependencies:
|
||||
file-uri-to-path "1.0.0"
|
||||
|
||||
bn.js@^4.11.9:
|
||||
version "4.12.0"
|
||||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
|
||||
integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
|
||||
|
||||
bn.js@^5.0.0, bn.js@^5.1.0, bn.js@^5.1.2:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.0.tgz#358860674396c6997771a9d051fcc1b57d4ae002"
|
||||
|
@ -421,16 +376,6 @@ bn.js@^5.2.0:
|
|||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70"
|
||||
integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==
|
||||
|
||||
borsh@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/borsh/-/borsh-0.4.0.tgz#9dd6defe741627f1315eac2a73df61421f6ddb9f"
|
||||
integrity sha512-aX6qtLya3K0AkT66CmYWCCDr77qsE9arV05OmdFpmat9qu8Pg9J5tBUPDztAW5fNh/d/MyVG/OYziP52Ndzx1g==
|
||||
dependencies:
|
||||
"@types/bn.js" "^4.11.5"
|
||||
bn.js "^5.0.0"
|
||||
bs58 "^4.0.0"
|
||||
text-encoding-utf-8 "^1.0.2"
|
||||
|
||||
borsh@^0.7.0:
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/borsh/-/borsh-0.7.0.tgz#6e9560d719d86d90dc589bca60ffc8a6c51fec2a"
|
||||
|
@ -462,11 +407,6 @@ braces@~3.0.2:
|
|||
dependencies:
|
||||
fill-range "^7.0.1"
|
||||
|
||||
brorand@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
|
||||
integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
|
||||
|
||||
browser-stdout@1.3.1:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60"
|
||||
|
@ -582,11 +522,6 @@ chokidar@3.5.3:
|
|||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
circular-json@^0.5.9:
|
||||
version "0.5.9"
|
||||
resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.5.9.tgz#932763ae88f4f7dead7a0d09c8a51a4743a53b1d"
|
||||
integrity sha512-4ivwqHpIFJZBuhN3g/pEcdbnGUywkBblloGbkglyloVjjR3uT6tieI89MVOfbP2tHX5sgb01FuLgAOzebNlJNQ==
|
||||
|
||||
cliui@^7.0.2:
|
||||
version "7.0.4"
|
||||
resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
|
||||
|
@ -618,13 +553,6 @@ concat-map@0.0.1:
|
|||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
|
||||
|
||||
cross-fetch@^3.1.4:
|
||||
version "3.1.4"
|
||||
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.4.tgz#9723f3a3a247bf8b89039f3a380a9244e8fa2f39"
|
||||
integrity sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ==
|
||||
dependencies:
|
||||
node-fetch "2.6.1"
|
||||
|
||||
cross-fetch@^3.1.5:
|
||||
version "3.1.5"
|
||||
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f"
|
||||
|
@ -691,19 +619,6 @@ dotenv@10.0.0:
|
|||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81"
|
||||
integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==
|
||||
|
||||
elliptic@^6.5.2:
|
||||
version "6.5.4"
|
||||
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb"
|
||||
integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==
|
||||
dependencies:
|
||||
bn.js "^4.11.9"
|
||||
brorand "^1.1.0"
|
||||
hash.js "^1.0.0"
|
||||
hmac-drbg "^1.0.1"
|
||||
inherits "^2.0.4"
|
||||
minimalistic-assert "^1.0.1"
|
||||
minimalistic-crypto-utils "^1.0.1"
|
||||
|
||||
emoji-regex@^8.0.0:
|
||||
version "8.0.0"
|
||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
|
||||
|
@ -839,28 +754,11 @@ has-flag@^4.0.0:
|
|||
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
|
||||
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
|
||||
|
||||
hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3:
|
||||
version "1.1.7"
|
||||
resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42"
|
||||
integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==
|
||||
dependencies:
|
||||
inherits "^2.0.3"
|
||||
minimalistic-assert "^1.0.1"
|
||||
|
||||
he@1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
|
||||
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
|
||||
|
||||
hmac-drbg@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
|
||||
integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=
|
||||
dependencies:
|
||||
hash.js "^1.0.3"
|
||||
minimalistic-assert "^1.0.0"
|
||||
minimalistic-crypto-utils "^1.0.1"
|
||||
|
||||
ieee754@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
||||
|
@ -874,7 +772,7 @@ inflight@^1.0.4:
|
|||
once "^1.3.0"
|
||||
wrappy "1"
|
||||
|
||||
inherits@2, inherits@^2.0.3, inherits@^2.0.4:
|
||||
inherits@2:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
|
@ -954,11 +852,6 @@ js-sha256@^0.9.0:
|
|||
resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.9.0.tgz#0b89ac166583e91ef9123644bd3c5334ce9d0966"
|
||||
integrity sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==
|
||||
|
||||
js-sha3@^0.8.0:
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840"
|
||||
integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==
|
||||
|
||||
js-yaml@4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
|
||||
|
@ -1020,16 +913,6 @@ make-error@^1.1.1:
|
|||
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
|
||||
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
|
||||
|
||||
minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
|
||||
integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
|
||||
|
||||
minimalistic-crypto-utils@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
|
||||
integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
|
||||
|
||||
minimatch@3.0.4, minimatch@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
||||
|
@ -1142,11 +1025,6 @@ no-case@^3.0.4:
|
|||
lower-case "^2.0.2"
|
||||
tslib "^2.0.3"
|
||||
|
||||
node-addon-api@^2.0.0:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32"
|
||||
integrity sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==
|
||||
|
||||
node-fetch@2, node-fetch@2.6.7:
|
||||
version "2.6.7"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
|
||||
|
@ -1154,12 +1032,7 @@ node-fetch@2, node-fetch@2.6.7:
|
|||
dependencies:
|
||||
whatwg-url "^5.0.0"
|
||||
|
||||
node-fetch@2.6.1:
|
||||
version "2.6.1"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
|
||||
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
|
||||
|
||||
node-gyp-build@^4.2.0, node-gyp-build@^4.3.0:
|
||||
node-gyp-build@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3"
|
||||
integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q==
|
||||
|
@ -1244,20 +1117,6 @@ require-directory@^2.1.1:
|
|||
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
|
||||
integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
|
||||
|
||||
rpc-websockets@^7.4.2:
|
||||
version "7.4.16"
|
||||
resolved "https://registry.yarnpkg.com/rpc-websockets/-/rpc-websockets-7.4.16.tgz#eb701cdef577d4357ba5f526d50e25f370396fac"
|
||||
integrity sha512-0b7OVhutzwRIaYAtJo5tqtaQTWKfwAsKnaThOSOy+VkhVdleNUgb8eZnWSdWITRZZEigV5uPEIDr5KZe4DBrdQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.11.2"
|
||||
circular-json "^0.5.9"
|
||||
eventemitter3 "^4.0.7"
|
||||
uuid "^8.3.0"
|
||||
ws "^7.4.5"
|
||||
optionalDependencies:
|
||||
bufferutil "^4.0.1"
|
||||
utf-8-validate "^5.0.2"
|
||||
|
||||
rpc-websockets@^7.5.0:
|
||||
version "7.5.0"
|
||||
resolved "https://registry.yarnpkg.com/rpc-websockets/-/rpc-websockets-7.5.0.tgz#bbeb87572e66703ff151e50af1658f98098e2748"
|
||||
|
@ -1276,15 +1135,6 @@ safe-buffer@^5.0.1, safe-buffer@^5.1.0:
|
|||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
||||
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
||||
|
||||
secp256k1@^4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-4.0.2.tgz#15dd57d0f0b9fdb54ac1fa1694f40e5e9a54f4a1"
|
||||
integrity sha512-UDar4sKvWAksIlfX3xIaQReADn+WFnHvbVujpcbr+9Sf/69odMwy2MUsz5CKLQgX9nsIyrjuxL2imVyoNHa3fg==
|
||||
dependencies:
|
||||
elliptic "^6.5.2"
|
||||
node-addon-api "^2.0.0"
|
||||
node-gyp-build "^4.2.0"
|
||||
|
||||
serialize-javascript@6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8"
|
||||
|
@ -1451,11 +1301,6 @@ tslib@^2.0.3:
|
|||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
|
||||
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
|
||||
|
||||
tweetnacl@^1.0.0:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596"
|
||||
integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==
|
||||
|
||||
type-detect@^4.0.0, type-detect@^4.0.5:
|
||||
version "4.0.8"
|
||||
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
|
||||
|
@ -1478,7 +1323,7 @@ uuid@^3.4.0:
|
|||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
|
||||
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
|
||||
|
||||
uuid@^8.3.0, uuid@^8.3.2:
|
||||
uuid@^8.3.2:
|
||||
version "8.3.2"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
||||
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
||||
|
|
|
@ -337,6 +337,7 @@ export const LangErrorCode = {
|
|||
ConstraintMintFreezeAuthority: 2017,
|
||||
ConstraintMintDecimals: 2018,
|
||||
ConstraintSpace: 2019,
|
||||
ConstraintAccountIsNone: 2020,
|
||||
|
||||
// Require.
|
||||
RequireViolated: 2500,
|
||||
|
@ -408,7 +409,7 @@ export const LangErrorMessage = new Map([
|
|||
|
||||
// Constraints.
|
||||
[LangErrorCode.ConstraintMut, "A mut constraint was violated"],
|
||||
[LangErrorCode.ConstraintHasOne, "A has_one constraint was violated"],
|
||||
[LangErrorCode.ConstraintHasOne, "A has one constraint was violated"],
|
||||
[LangErrorCode.ConstraintSigner, "A signer constraint was violated"],
|
||||
[LangErrorCode.ConstraintRaw, "A raw constraint was violated"],
|
||||
[LangErrorCode.ConstraintOwner, "An owner constraint was violated"],
|
||||
|
@ -442,6 +443,10 @@ export const LangErrorMessage = new Map([
|
|||
"A mint decimals constraint was violated",
|
||||
],
|
||||
[LangErrorCode.ConstraintSpace, "A space constraint was violated"],
|
||||
[
|
||||
LangErrorCode.ConstraintAccountIsNone,
|
||||
"A required account for the constraint is None",
|
||||
],
|
||||
|
||||
// Require.
|
||||
[LangErrorCode.RequireViolated, "A require expression was violated"],
|
||||
|
|
|
@ -52,10 +52,17 @@ export type IdlStateMethod = IdlInstruction;
|
|||
|
||||
export type IdlAccountItem = IdlAccount | IdlAccounts;
|
||||
|
||||
export function isIdlAccounts(
|
||||
accountItem: IdlAccountItem
|
||||
): accountItem is IdlAccounts {
|
||||
return "accounts" in accountItem;
|
||||
}
|
||||
|
||||
export type IdlAccount = {
|
||||
name: string;
|
||||
isMut: boolean;
|
||||
isSigner: boolean;
|
||||
isOptional?: boolean;
|
||||
docs?: string[];
|
||||
relations?: string[];
|
||||
pda?: IdlPda;
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
IdlTypeDef,
|
||||
IdlTypeDefTyStruct,
|
||||
IdlType,
|
||||
isIdlAccounts,
|
||||
} from "../idl.js";
|
||||
import * as utf8 from "../utils/bytes/utf8.js";
|
||||
import { TOKEN_PROGRAM_ID, ASSOCIATED_PROGRAM_ID } from "../utils/token.js";
|
||||
|
@ -22,17 +23,30 @@ import Provider from "../provider.js";
|
|||
import { AccountNamespace } from "./namespace/account.js";
|
||||
import { BorshAccountsCoder } from "src/coder/index.js";
|
||||
import { decodeTokenAccount } from "./token-account-layout";
|
||||
import { Program } from "./index.js";
|
||||
import { Program, translateAddress } from "./index.js";
|
||||
import {
|
||||
flattenPartialAccounts,
|
||||
isPartialAccounts,
|
||||
PartialAccounts,
|
||||
} from "./namespace/methods";
|
||||
|
||||
type Accounts = { [name: string]: PublicKey | Accounts };
|
||||
export type AccountsGeneric = {
|
||||
[name: string]: PublicKey | AccountsGeneric;
|
||||
};
|
||||
|
||||
export function isAccountsGeneric(
|
||||
accounts: PublicKey | AccountsGeneric
|
||||
): accounts is AccountsGeneric {
|
||||
return !(accounts instanceof PublicKey);
|
||||
}
|
||||
|
||||
export type CustomAccountResolver<IDL extends Idl> = (params: {
|
||||
args: Array<any>;
|
||||
accounts: Accounts;
|
||||
accounts: AccountsGeneric;
|
||||
provider: Provider;
|
||||
programId: PublicKey;
|
||||
idlIx: AllInstructions<IDL>;
|
||||
}) => Promise<{ accounts: Accounts; resolved: number }>;
|
||||
}) => Promise<{ accounts: AccountsGeneric; resolved: number }>;
|
||||
|
||||
// Populates a given accounts context with PDAs and common missing accounts.
|
||||
export class AccountsResolver<IDL extends Idl> {
|
||||
|
@ -49,7 +63,7 @@ export class AccountsResolver<IDL extends Idl> {
|
|||
|
||||
constructor(
|
||||
_args: Array<any>,
|
||||
private _accounts: Accounts,
|
||||
private _accounts: AccountsGeneric,
|
||||
private _provider: Provider,
|
||||
private _programId: PublicKey,
|
||||
private _idlIx: AllInstructions<IDL>,
|
||||
|
@ -99,6 +113,51 @@ export class AccountsResolver<IDL extends Idl> {
|
|||
return 0;
|
||||
}
|
||||
|
||||
private resolveOptionalsHelper(
|
||||
partialAccounts: PartialAccounts,
|
||||
accountItems: IdlAccountItem[]
|
||||
): AccountsGeneric {
|
||||
const nestedAccountsGeneric: AccountsGeneric = {};
|
||||
// Looping through accountItem array instead of on partialAccounts, so
|
||||
// we only traverse array once
|
||||
for (const accountItem of accountItems) {
|
||||
const accountName = accountItem.name;
|
||||
const partialAccount = partialAccounts[accountName];
|
||||
// Skip if the account isn't included (thus would be undefined)
|
||||
if (partialAccount === undefined) continue;
|
||||
if (isPartialAccounts(partialAccount)) {
|
||||
// is compound accounts, recurse one level deeper
|
||||
if (isIdlAccounts(accountItem)) {
|
||||
nestedAccountsGeneric[accountName] = this.resolveOptionalsHelper(
|
||||
partialAccount,
|
||||
accountItem["accounts"] as IdlAccountItem[]
|
||||
);
|
||||
} else {
|
||||
// Here we try our best to recover gracefully. If there are optionals we can't check, we will fail then.
|
||||
nestedAccountsGeneric[accountName] = flattenPartialAccounts(
|
||||
partialAccount,
|
||||
true
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// if not compound accounts, do null/optional check and proceed
|
||||
if (partialAccount !== null) {
|
||||
nestedAccountsGeneric[accountName] = translateAddress(partialAccount);
|
||||
} else if (accountItem["isOptional"]) {
|
||||
nestedAccountsGeneric[accountName] = this._programId;
|
||||
}
|
||||
}
|
||||
}
|
||||
return nestedAccountsGeneric;
|
||||
}
|
||||
|
||||
public resolveOptionals(accounts: PartialAccounts) {
|
||||
Object.assign(
|
||||
this._accounts,
|
||||
this.resolveOptionalsHelper(accounts, this._idlIx.accounts)
|
||||
);
|
||||
}
|
||||
|
||||
private get(path: string[]): PublicKey | undefined {
|
||||
// Only return if pubkey
|
||||
const ret = path.reduce(
|
||||
|
@ -120,7 +179,7 @@ export class AccountsResolver<IDL extends Idl> {
|
|||
}
|
||||
|
||||
curr[p] = curr[p] || {};
|
||||
curr = curr[p] as Accounts;
|
||||
curr = curr[p] as AccountsGeneric;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -183,6 +242,7 @@ export class AccountsResolver<IDL extends Idl> {
|
|||
|
||||
const accountDescCasted: IdlAccount = accountDesc as IdlAccount;
|
||||
const accountDescName = camelCase(accountDesc.name);
|
||||
|
||||
// PDA derived from IDL seeds.
|
||||
if (
|
||||
accountDescCasted.pda &&
|
||||
|
@ -381,6 +441,10 @@ export class AccountsResolver<IDL extends Idl> {
|
|||
const fieldName = pathComponents[0];
|
||||
const fieldPubkey = this.get([...path, camelCase(fieldName)]);
|
||||
|
||||
if (fieldPubkey === null) {
|
||||
throw new Error(`fieldPubkey is null`);
|
||||
}
|
||||
|
||||
// The seed is a pubkey of the account.
|
||||
if (pathComponents.length === 1) {
|
||||
return fieldPubkey;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {
|
||||
AccountMeta,
|
||||
Signer,
|
||||
ConfirmOptions,
|
||||
Signer,
|
||||
TransactionInstruction,
|
||||
} from "@solana/web3.js";
|
||||
import { Address } from "./common.js";
|
||||
|
@ -67,6 +67,8 @@ export type Accounts<A extends IdlAccountItem = IdlAccountItem> = {
|
|||
|
||||
type Account<A extends IdlAccountItem> = A extends IdlAccounts
|
||||
? Accounts<A["accounts"][number]>
|
||||
: A extends { isOptional: true }
|
||||
? Address | null
|
||||
: Address;
|
||||
|
||||
export function splitArgsAndCtx(
|
||||
|
|
|
@ -12,10 +12,10 @@ import {
|
|||
} from "../../idl.js";
|
||||
import { IdlError } from "../../error.js";
|
||||
import {
|
||||
toInstruction,
|
||||
validateAccounts,
|
||||
translateAddress,
|
||||
Address,
|
||||
toInstruction,
|
||||
translateAddress,
|
||||
validateAccounts,
|
||||
} from "../common.js";
|
||||
import { Accounts, splitArgsAndCtx } from "../context.js";
|
||||
import * as features from "../../utils/features.js";
|
||||
|
@ -66,6 +66,7 @@ export default class InstructionNamespaceFactory {
|
|||
return InstructionNamespaceFactory.accountsArray(
|
||||
accs,
|
||||
idlIx.accounts,
|
||||
programId,
|
||||
idlIx.name
|
||||
);
|
||||
};
|
||||
|
@ -76,6 +77,7 @@ export default class InstructionNamespaceFactory {
|
|||
public static accountsArray(
|
||||
ctx: Accounts | undefined,
|
||||
accounts: readonly IdlAccountItem[],
|
||||
programId: PublicKey,
|
||||
ixName?: string
|
||||
): AccountMeta[] {
|
||||
if (!ctx) {
|
||||
|
@ -92,11 +94,12 @@ export default class InstructionNamespaceFactory {
|
|||
return InstructionNamespaceFactory.accountsArray(
|
||||
rpcAccs,
|
||||
(acc as IdlAccounts).accounts,
|
||||
programId,
|
||||
ixName
|
||||
).flat();
|
||||
} else {
|
||||
const account: IdlAccount = acc as IdlAccount;
|
||||
let pubkey;
|
||||
let pubkey: PublicKey;
|
||||
try {
|
||||
pubkey = translateAddress(ctx[acc.name] as Address);
|
||||
} catch (err) {
|
||||
|
@ -108,10 +111,14 @@ export default class InstructionNamespaceFactory {
|
|||
}. Expected PublicKey or string.`
|
||||
);
|
||||
}
|
||||
|
||||
const optional = account.isOptional && pubkey.equals(programId);
|
||||
const isWritable = account.isMut && !optional;
|
||||
const isSigner = account.isSigner && !optional;
|
||||
return {
|
||||
pubkey,
|
||||
isWritable: account.isMut,
|
||||
isSigner: account.isSigner,
|
||||
isWritable,
|
||||
isSigner,
|
||||
};
|
||||
}
|
||||
})
|
||||
|
|
|
@ -10,10 +10,11 @@ import {
|
|||
import { Idl, IdlAccountItem, IdlAccounts, IdlTypeDef } from "../../idl.js";
|
||||
import Provider from "../../provider.js";
|
||||
import {
|
||||
AccountsGeneric,
|
||||
AccountsResolver,
|
||||
CustomAccountResolver,
|
||||
} from "../accounts-resolver.js";
|
||||
import { Address } from "../common.js";
|
||||
import { Address, translateAddress } from "../common.js";
|
||||
import { Accounts } from "../context.js";
|
||||
import { AccountNamespace } from "./account.js";
|
||||
import { InstructionFn } from "./instruction.js";
|
||||
|
@ -65,16 +66,50 @@ export class MethodsBuilderFactory {
|
|||
}
|
||||
}
|
||||
|
||||
type PartialAccounts<A extends IdlAccountItem = IdlAccountItem> = Partial<{
|
||||
[N in A["name"]]: PartialAccount<A & { name: N }>;
|
||||
}>;
|
||||
export type PartialAccounts<A extends IdlAccountItem = IdlAccountItem> =
|
||||
Partial<{
|
||||
[N in A["name"]]: PartialAccount<A & { name: N }>;
|
||||
}>;
|
||||
|
||||
type PartialAccount<A extends IdlAccountItem> = A extends IdlAccounts
|
||||
? Partial<Accounts<A["accounts"][number]>>
|
||||
? PartialAccounts<A["accounts"][number]>
|
||||
: A extends { isOptional: true }
|
||||
? Address | null
|
||||
: Address;
|
||||
|
||||
export function isPartialAccounts(
|
||||
partialAccount: PartialAccount<IdlAccountItem>
|
||||
): partialAccount is PartialAccounts {
|
||||
return (
|
||||
typeof partialAccount === "object" &&
|
||||
partialAccount !== null &&
|
||||
!("_bn" in partialAccount) // Ensures not a pubkey
|
||||
);
|
||||
}
|
||||
|
||||
export function flattenPartialAccounts<A extends IdlAccountItem>(
|
||||
partialAccounts: PartialAccounts<A>,
|
||||
throwOnNull: boolean
|
||||
): AccountsGeneric {
|
||||
const toReturn: AccountsGeneric = {};
|
||||
for (const accountName in partialAccounts) {
|
||||
const account = partialAccounts[accountName];
|
||||
if (account === null) {
|
||||
if (throwOnNull)
|
||||
throw new Error(
|
||||
"Failed to resolve optionals due to IDL type mismatch with input accounts!"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
toReturn[accountName] = isPartialAccounts(account)
|
||||
? flattenPartialAccounts(account, true)
|
||||
: translateAddress(account);
|
||||
}
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
export class MethodsBuilder<IDL extends Idl, I extends AllInstructions<IDL>> {
|
||||
private readonly _accounts: { [name: string]: PublicKey } = {};
|
||||
private readonly _accounts: AccountsGeneric = {};
|
||||
private _remainingAccounts: Array<AccountMeta> = [];
|
||||
private _signers: Array<Signer> = [];
|
||||
private _preInstructions: Array<TransactionInstruction> = [];
|
||||
|
@ -91,7 +126,7 @@ export class MethodsBuilder<IDL extends Idl, I extends AllInstructions<IDL>> {
|
|||
private _simulateFn: SimulateFn<IDL>,
|
||||
private _viewFn: ViewFn<IDL> | undefined,
|
||||
_provider: Provider,
|
||||
_programId: PublicKey,
|
||||
private _programId: PublicKey,
|
||||
_idlIx: AllInstructions<IDL>,
|
||||
_accountNamespace: AccountNamespace<IDL>,
|
||||
_idlTypes: IdlTypeDef[],
|
||||
|
@ -126,9 +161,11 @@ export class MethodsBuilder<IDL extends Idl, I extends AllInstructions<IDL>> {
|
|||
>;
|
||||
}
|
||||
|
||||
public accounts(accounts: PartialAccounts): MethodsBuilder<IDL, I> {
|
||||
public accounts(
|
||||
accounts: PartialAccounts<I["accounts"][number]>
|
||||
): MethodsBuilder<IDL, I> {
|
||||
this._autoResolveAccounts = true;
|
||||
Object.assign(this._accounts, accounts);
|
||||
this._accountsResolver.resolveOptionals(accounts);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -136,7 +173,7 @@ export class MethodsBuilder<IDL extends Idl, I extends AllInstructions<IDL>> {
|
|||
accounts: Accounts<I["accounts"][number]>
|
||||
): MethodsBuilder<IDL, I> {
|
||||
this._autoResolveAccounts = false;
|
||||
Object.assign(this._accounts, accounts);
|
||||
this._accountsResolver.resolveOptionals(accounts);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
|
@ -118,6 +118,7 @@ export class StateClient<IDL extends Idl> {
|
|||
InstructionNamespaceFactory.accountsArray(
|
||||
accounts,
|
||||
m.accounts,
|
||||
programId,
|
||||
m.name
|
||||
)
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue