Browse Source

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 c864cd5d4f019e6bd5f93641e01bd82fc74041d4.

* 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>
Sammy Harris 2 years ago
parent
commit
484628070c
52 changed files with 3794 additions and 2024 deletions
  1. 8 0
      .github/workflows/no-cashing-tests.yaml
  2. 8 0
      .github/workflows/tests.yaml
  3. 1 0
      client/example/Cargo.toml
  4. 3 1
      client/example/run-test.sh
  5. 64 0
      client/example/src/main.rs
  6. 1 0
      lang/Cargo.toml
  7. 1 0
      lang/derive/accounts/Cargo.toml
  8. 1 0
      lang/src/accounts/mod.rs
  9. 84 0
      lang/src/accounts/option.rs
  10. 3 0
      lang/src/error.rs
  11. 1 0
      lang/syn/Cargo.toml
  12. 22 5
      lang/syn/src/codegen/accounts/__client_accounts.rs
  13. 26 17
      lang/syn/src/codegen/accounts/__cpi_client_accounts.rs
  14. 396 123
      lang/syn/src/codegen/accounts/constraints.rs
  15. 12 4
      lang/syn/src/codegen/accounts/exit.rs
  16. 2 7
      lang/syn/src/codegen/accounts/to_account_infos.rs
  17. 15 5
      lang/syn/src/codegen/accounts/to_account_metas.rs
  18. 32 8
      lang/syn/src/codegen/accounts/try_accounts.rs
  19. 1 0
      lang/syn/src/idl/file.rs
  20. 2 0
      lang/syn/src/idl/mod.rs
  21. 46 3
      lang/syn/src/lib.rs
  22. 123 40
      lang/syn/src/parser/accounts/mod.rs
  23. 1 0
      tests/misc/Anchor.toml
  24. 22 0
      tests/misc/programs/misc-optional/Cargo.toml
  25. 2 0
      tests/misc/programs/misc-optional/Xargo.toml
  26. 75 0
      tests/misc/programs/misc-optional/src/account.rs
  27. 587 0
      tests/misc/programs/misc-optional/src/context.rs
  28. 55 0
      tests/misc/programs/misc-optional/src/event.rs
  29. 399 0
      tests/misc/programs/misc-optional/src/lib.rs
  30. 515 1606
      tests/misc/tests/misc/misc.ts
  31. 13 0
      tests/optional/Anchor.toml
  32. 4 0
      tests/optional/Cargo.toml
  33. 19 0
      tests/optional/package.json
  34. 16 0
      tests/optional/programs/allow-missing-optionals/Cargo.toml
  35. 2 0
      tests/optional/programs/allow-missing-optionals/Xargo.toml
  36. 38 0
      tests/optional/programs/allow-missing-optionals/src/lib.rs
  37. 16 0
      tests/optional/programs/optional/Cargo.toml
  38. 2 0
      tests/optional/programs/optional/Xargo.toml
  39. 20 0
      tests/optional/programs/optional/src/account.rs
  40. 50 0
      tests/optional/programs/optional/src/context.rs
  41. 71 0
      tests/optional/programs/optional/src/lib.rs
  42. 845 0
      tests/optional/tests/optional.ts
  43. 12 0
      tests/optional/tsconfig.json
  44. 6 1
      tests/package.json
  45. 25 180
      tests/yarn.lock
  46. 6 1
      ts/build-packages.sh
  47. 7 0
      ts/packages/anchor/src/idl.ts
  48. 71 7
      ts/packages/anchor/src/program/accounts-resolver.ts
  49. 3 1
      ts/packages/anchor/src/program/context.ts
  50. 12 5
      ts/packages/anchor/src/program/namespace/instruction.ts
  51. 47 10
      ts/packages/anchor/src/program/namespace/methods.ts
  52. 1 0
      ts/packages/anchor/src/program/namespace/state.ts

+ 8 - 0
.github/workflows/no-cashing-tests.yaml

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

+ 8 - 0
.github/workflows/tests.yaml

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

+ 1 - 0
client/example/Cargo.toml

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

+ 3 - 1
client/example/run-test.sh

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

+ 64 - 0
client/example/src/main.rs

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

+ 1 - 0
lang/Cargo.toml

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

+ 1 - 0
lang/derive/accounts/Cargo.toml

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

+ 1 - 0
lang/src/accounts/mod.rs

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

+ 84 - 0
lang/src/accounts/option.rs

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

+ 3 - 0
lang/src/error.rs

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

+ 1 - 0
lang/syn/Cargo.toml

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

+ 22 - 5
lang/syn/src/codegen/accounts/__client_accounts.rs

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

+ 26 - 17
lang/syn/src/codegen/accounts/__cpi_client_accounts.rs

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

+ 396 - 123
lang/syn/src/codegen/accounts/constraints.rs

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

+ 12 - 4
lang/syn/src/codegen/accounts/exit.rs

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

+ 2 - 7
lang/syn/src/codegen/accounts/to_account_infos.rs

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

+ 15 - 5
lang/syn/src/codegen/accounts/to_account_metas.rs

@@ -9,18 +9,28 @@ pub fn generate(accs: &AccountsStruct) -> proc_macro2::TokenStream {
         .fields
         .iter()
         .map(|f: &AccountField| {
-            let (name, is_signer) = match f {
-                AccountField::CompositeField(s) => (&s.ident, quote! {None}),
+            let (name, is_signer, is_optional) = match f {
+                AccountField::CompositeField(s) => (&s.ident, quote! {None}, false),
                 AccountField::Field(f) => {
                     let is_signer = match f.constraints.is_signer() {
                         false => quote! {None},
                         true => quote! {Some(true)},
                     };
-                    (&f.ident, is_signer)
+                    (&f.ident, is_signer, f.is_optional)
                 }
             };
-            quote! {
-                account_metas.extend(self.#name.to_account_metas(#is_signer));
+            if is_optional {
+                quote! {
+                    if let Some(#name) = &self.#name {
+                        account_metas.extend(#name.to_account_metas(#is_signer));
+                    } else {
+                        account_metas.push(AccountMeta::new_readonly(crate::ID, false));
+                    }
+                }
+            } else {
+                quote! {
+                    account_metas.extend(self.#name.to_account_metas(#is_signer));
+                }
             }
         })
         .collect();

+ 32 - 8
lang/syn/src/codegen/accounts/try_accounts.rs

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

+ 1 - 0
lang/syn/src/idl/file.rs

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

+ 2 - 0
lang/syn/src/idl/mod.rs

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

+ 46 - 3
lang/syn/src/lib.rs

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

+ 123 - 40
lang/syn/src/parser/accounts/mod.rs

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

+ 1 - 0
tests/misc/Anchor.toml

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

+ 22 - 0
tests/misc/programs/misc-optional/Cargo.toml

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

+ 2 - 0
tests/misc/programs/misc-optional/Xargo.toml

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

+ 75 - 0
tests/misc/programs/misc-optional/src/account.rs

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

+ 587 - 0
tests/misc/programs/misc-optional/src/context.rs

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

+ 55 - 0
tests/misc/programs/misc-optional/src/event.rs

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

+ 399 - 0
tests/misc/programs/misc-optional/src/lib.rs

@@ -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
+ 515 - 1606
tests/misc/tests/misc/misc.ts


+ 13 - 0
tests/optional/Anchor.toml

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

+ 4 - 0
tests/optional/Cargo.toml

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

+ 19 - 0
tests/optional/package.json

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

+ 16 - 0
tests/optional/programs/allow-missing-optionals/Cargo.toml

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

+ 2 - 0
tests/optional/programs/allow-missing-optionals/Xargo.toml

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

+ 38 - 0
tests/optional/programs/allow-missing-optionals/src/lib.rs

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

+ 16 - 0
tests/optional/programs/optional/Cargo.toml

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

+ 2 - 0
tests/optional/programs/optional/Xargo.toml

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

+ 20 - 0
tests/optional/programs/optional/src/account.rs

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

+ 50 - 0
tests/optional/programs/optional/src/context.rs

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

+ 71 - 0
tests/optional/programs/optional/src/lib.rs

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

+ 845 - 0
tests/optional/tests/optional.ts

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

+ 12 - 0
tests/optional/tsconfig.json

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

+ 6 - 1
tests/package.json

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

+ 25 - 180
tests/yarn.lock

@@ -2,7 +2,7 @@
 # yarn lockfile v1
 
 
-"@babel/runtime@^7.10.5", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5":
+"@babel/runtime@^7.10.5", "@babel/runtime@^7.12.5":
   version "7.16.3"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5"
   integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==
@@ -16,26 +16,13 @@
   dependencies:
     regenerator-runtime "^0.13.4"
 
-"@ethersproject/bytes@^5.5.0":
-  version "5.5.0"
-  resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.5.0.tgz#cb11c526de657e7b45d2e0f0246fb3b9d29a601c"
-  integrity sha512-ABvc7BHWhZU9PNM/tANm/Qx4ostPGadAuQzWTr3doklZOhDlmcBqclrQe/ZXUIj3K8wC28oYeuRa+A37tX9kog==
+"@coral-xyz/borsh@^0.2.6":
+  version "0.2.6"
+  resolved "https://registry.yarnpkg.com/@coral-xyz/borsh/-/borsh-0.2.6.tgz#0f11b223bf2967574310705afd3c53ce26688ada"
+  integrity sha512-y6nmHw1bFcJib7sMHsQPpC8r47xhqDZVvhUdna7NUPzpSbOZG6f46N21+aXsQ2w/tG8Ggls488J/ZmwbgVmyjg==
   dependencies:
-    "@ethersproject/logger" "^5.5.0"
-
-"@ethersproject/logger@^5.5.0":
-  version "5.5.0"
-  resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.5.0.tgz#0c2caebeff98e10aefa5aef27d7441c7fd18cf5d"
-  integrity sha512-rIY/6WPm7T8n3qS2vuHTUBPdXHl+rGxWxW5okDfo9J4Z0+gRRZT0msvUdIJkE4/HS29GUMziwGaaKO2bWONBrg==
-
-"@ethersproject/sha2@^5.5.0":
-  version "5.5.0"
-  resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.5.0.tgz#a40a054c61f98fd9eee99af2c3cc6ff57ec24db7"
-  integrity sha512-B5UBoglbCiHamRVPLA110J+2uqsifpZaTmid2/7W5rbtYVz6gus6/hSDieIU/6gaKIDcOj12WnOdiymEUHIAOA==
-  dependencies:
-    "@ethersproject/bytes" "^5.5.0"
-    "@ethersproject/logger" "^5.5.0"
-    hash.js "1.1.7"
+    bn.js "^5.1.2"
+    buffer-layout "^1.2.0"
 
 "@native-to-anchor/buffer-layout@=0.1.0":
   version "0.1.0"
@@ -63,8 +50,8 @@
 "@project-serum/anchor@=0.25.0", "@project-serum/anchor@file:../ts/packages/anchor":
   version "0.25.0"
   dependencies:
-    "@project-serum/borsh" "^0.2.5"
-    "@solana/web3.js" "^1.64.0"
+    "@coral-xyz/borsh" "^0.2.6"
+    "@solana/web3.js" "^1.68.0"
     base64-js "^1.5.1"
     bn.js "^5.1.2"
     bs58 "^4.0.1"
@@ -99,7 +86,7 @@
     snake-case "^3.0.4"
     toml "^3.0.0"
 
-"@project-serum/borsh@^0.2.2", "@project-serum/borsh@^0.2.5":
+"@project-serum/borsh@^0.2.2":
   version "0.2.5"
   resolved "https://registry.yarnpkg.com/@project-serum/borsh/-/borsh-0.2.5.tgz#6059287aa624ecebbfc0edd35e4c28ff987d8663"
   integrity sha512-UmeUkUoKdQ7rhx6Leve1SssMR/Ghv8qrEiyywyxSWg7ooV7StdpPBhciiy5eB3T0qU1BXvdRNC8TdrkxK7WC5Q==
@@ -156,13 +143,6 @@
   dependencies:
     buffer "~6.0.3"
 
-"@solana/buffer-layout@^3.0.0":
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/@solana/buffer-layout/-/buffer-layout-3.0.0.tgz#b9353caeb9a1589cb77a1b145bcb1a9a93114326"
-  integrity sha512-MVdgAKKL39tEs0l8je0hKaXLQFb7Rdfb0Xg2LjFZd8Lfdazkg6xiS98uAZrEKvaoF3i4M95ei9RydkGIDMeo3w==
-  dependencies:
-    buffer "~6.0.3"
-
 "@solana/spl-token@^0.1.6", "@solana/spl-token@^0.1.8":
   version "0.1.8"
   resolved "https://registry.yarnpkg.com/@solana/spl-token/-/spl-token-0.1.8.tgz#f06e746341ef8d04165e21fc7f555492a2a0faa6"
@@ -175,10 +155,10 @@
     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==
+"@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==
   dependencies:
     "@babel/runtime" "^7.12.5"
     "@noble/ed25519" "^1.7.0"
@@ -196,30 +176,10 @@
     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":
-  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==
+"@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"
@@ -237,10 +197,10 @@
     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==
+"@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==

+ 6 - 1
ts/build-packages.sh

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

+ 7 - 0
ts/packages/anchor/src/idl.ts

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

+ 71 - 7
ts/packages/anchor/src/program/accounts-resolver.ts

@@ -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";
-
-type Accounts = { [name: string]: PublicKey | Accounts };
+import { Program, translateAddress } from "./index.js";
+import {
+  flattenPartialAccounts,
+  isPartialAccounts,
+  PartialAccounts,
+} from "./namespace/methods";
+
+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;

+ 3 - 1
ts/packages/anchor/src/program/context.ts

@@ -1,7 +1,7 @@
 import {
   AccountMeta,
-  Signer,
   ConfirmOptions,
+  Signer,
   TransactionInstruction,
 } from "@solana/web3.js";
 import { Address } from "./common.js";
@@ -67,6 +67,8 @@ export type Accounts<A extends IdlAccountItem = IdlAccountItem> = {
 
 type Account<A extends IdlAccountItem> = A extends IdlAccounts
   ? Accounts<A["accounts"][number]>
+  : A extends { isOptional: true }
+  ? Address | null
   : Address;
 
 export function splitArgsAndCtx(

+ 12 - 5
ts/packages/anchor/src/program/namespace/instruction.ts

@@ -12,10 +12,10 @@ import {
 } from "../../idl.js";
 import { IdlError } from "../../error.js";
 import {
+  Address,
   toInstruction,
-  validateAccounts,
   translateAddress,
-  Address,
+  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,
           };
         }
       })

+ 47 - 10
ts/packages/anchor/src/program/namespace/methods.ts

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

+ 1 - 0
ts/packages/anchor/src/program/namespace/state.ts

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

Some files were not shown because too many files changed in this diff