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:
Sammy Harris 2022-12-12 09:32:59 -06:00 committed by GitHub
parent d88a09dbb7
commit 484628070c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 5419 additions and 2514 deletions

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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() {

View File

@ -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(())
}

View File

@ -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 = []

View File

@ -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"]

View File

@ -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)]

View File

@ -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))
}
}

View File

@ -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

View File

@ -9,6 +9,7 @@ rust-version = "1.59"
edition = "2021"
[features]
allow-missing-optionals = []
init-if-needed = []
idl = []
hash = []

View File

@ -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));
}
}
}
})

View File

@ -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();

View File

@ -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 = &quote! {system_program};
let token_program = &quote! {token_program};
let associated_token_program = &quote! {associated_token_program};
let rent = &quote! {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));
}
}
}
}

View File

@ -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() {

View File

@ -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! {

View File

@ -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();

View File

@ -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();

View File

@ -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),

View File

@ -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>,

View File

@ -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>,

View File

@ -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> {

View File

@ -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"

View File

@ -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"

View File

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

View File

@ -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],
}

View File

@ -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>>,
}

View File

@ -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,
}

View File

@ -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

View File

@ -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"

View File

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

View File

@ -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"
}
}

View File

@ -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"] }

View File

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

View File

@ -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>>,
}

View File

@ -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" }

View File

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

View File

@ -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;
}

View File

@ -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>>,
}

View File

@ -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,
}

View File

@ -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);
});
});
});

View File

@ -0,0 +1,12 @@
{
"compilerOptions": {
"types": ["mocha", "chai"],
"typeRoots": ["./node_modules/@types"],
"lib": ["es2015"],
"module": "commonjs",
"target": "es6",
"esModuleInterop": true,
"skipLibCheck": true,
"strict": true
}
}

View File

@ -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",

View File

@ -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==

0
ts/build-packages.sh Normal file → Executable file
View File

View File

@ -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"],

View File

@ -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;

View File

@ -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;

View File

@ -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(

View File

@ -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,
};
}
})

View File

@ -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;
}

View File

@ -118,6 +118,7 @@ export class StateClient<IDL extends Idl> {
InstructionNamespaceFactory.accountsArray(
accounts,
m.accounts,
programId,
m.name
)
);