浏览代码

interface: Support multiple programs with the same interface via token-2022 (#2386)

Jon Cinque 2 年之前
父节点
当前提交
777f2eace1

+ 55 - 54
Cargo.lock

@@ -261,6 +261,7 @@ dependencies = [
  "solana-program",
  "spl-associated-token-account",
  "spl-token",
+ "spl-token-2022",
 ]
 
 [[package]]
@@ -3144,9 +3145,9 @@ dependencies = [
 
 [[package]]
 name = "solana-account-decoder"
-version = "1.14.7"
+version = "1.14.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f301f42bfbb5fc3dae9068f485544d4260af768d34b773ae9bfc9faf09ea737a"
+checksum = "701ca0143761d40eb6e2933e8854d1c0a2918ede7419264b71bd142980c5fb32"
 dependencies = [
  "Inflector",
  "base64 0.13.1",
@@ -3169,9 +3170,9 @@ dependencies = [
 
 [[package]]
 name = "solana-address-lookup-table-program"
-version = "1.14.7"
+version = "1.14.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "28a3d1316bdac7ec880e2e73be90ccaaf13d1f868cbc15904d3e8030f6c65169"
+checksum = "03f403a837de4e5d6135bb8100b7aa982a1e5ecc166386258ce3583cd12e2d7c"
 dependencies = [
  "bincode",
  "bytemuck",
@@ -3190,9 +3191,9 @@ dependencies = [
 
 [[package]]
 name = "solana-clap-utils"
-version = "1.14.7"
+version = "1.14.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3dae89175548ab57598ec68051b99234cd4a92a92ea3dac90fa6c48c74a3af28"
+checksum = "94635c6ba33899361777993370090a027abcefda4463f0f51863e0508cc0cd8a"
 dependencies = [
  "chrono",
  "clap 2.34.0",
@@ -3208,9 +3209,9 @@ dependencies = [
 
 [[package]]
 name = "solana-cli-config"
-version = "1.14.7"
+version = "1.14.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4d75a1cd1e7f04b50a2df322721ba76dd1fafd94623ceb3eab64479bd21571a0"
+checksum = "7f3185e08728970d1cb67dbcd887180feef72d05b2c0a3a3c61af7f3df5383ed"
 dependencies = [
  "dirs-next",
  "lazy_static",
@@ -3224,9 +3225,9 @@ dependencies = [
 
 [[package]]
 name = "solana-client"
-version = "1.14.7"
+version = "1.14.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f420729f477b91581874613f917f4bba9d8e96bf778b22f866f2913fa8f4546d"
+checksum = "1263dd1bd7473cc367e703f5198396e11dc83be37d10fb3f12fceca0a1eec749"
 dependencies = [
  "async-mutex",
  "async-trait",
@@ -3278,9 +3279,9 @@ dependencies = [
 
 [[package]]
 name = "solana-config-program"
-version = "1.14.7"
+version = "1.14.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2afc57e9a42e836bfc5363293969f464106db22a2fd653968708e0313429dba8"
+checksum = "16219e0c1b2f0c919f238c8951078b45b9c6c00b18acec547eebe2821d2db916"
 dependencies = [
  "bincode",
  "chrono",
@@ -3292,9 +3293,9 @@ dependencies = [
 
 [[package]]
 name = "solana-faucet"
-version = "1.14.7"
+version = "1.14.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "80f0fb1e5de36e1828a4885deb5e06f299d9983c23ab81fdc890f4d03db844ea"
+checksum = "435cfeb35c5f1e67e7e2ad5ac4106f04edaca0609ad52dbbc7ac051d884d6eca"
 dependencies = [
  "bincode",
  "byteorder",
@@ -3316,9 +3317,9 @@ dependencies = [
 
 [[package]]
 name = "solana-frozen-abi"
-version = "1.14.7"
+version = "1.14.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e9d5107e663df4a87c658ee764e9f0e4d15adf8bc1d1c9088b45ed8eaaf4958"
+checksum = "6c5a383f43792311db749bbed4e7794222c9f118b609bc8252b4ea3ad88b4188"
 dependencies = [
  "ahash",
  "blake3",
@@ -3350,9 +3351,9 @@ dependencies = [
 
 [[package]]
 name = "solana-frozen-abi-macro"
-version = "1.14.7"
+version = "1.14.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e4600fe5ae28cec848debc4ea3b41f34d9d8fd088aca209fbb1e8205489d08d"
+checksum = "062e282539e770967500945cd2fdb78170a1ea45aff7ad1b4ce4e2cc0b557db8"
 dependencies = [
  "proc-macro2 1.0.47",
  "quote 1.0.21",
@@ -3362,9 +3363,9 @@ dependencies = [
 
 [[package]]
 name = "solana-logger"
-version = "1.14.7"
+version = "1.14.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f78a1849908659ed28696b92f030b1048b8ddafadfad0e95e79dcd21fe31072"
+checksum = "0c2bcbaba2c683e7bf80ff4f3a3cdcdaabdb0b21333e8d89aed06be136193d39"
 dependencies = [
  "env_logger",
  "lazy_static",
@@ -3373,9 +3374,9 @@ dependencies = [
 
 [[package]]
 name = "solana-measure"
-version = "1.14.7"
+version = "1.14.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7597f7bc74f86e77aad037369b5f5176dfb23fe01a58df350e4d19c12faf8f7f"
+checksum = "33bbb0e7ee37cdfd18f2636e687cfafcc2e85a7768e283941fd08da022bd0f66"
 dependencies = [
  "log",
  "solana-sdk",
@@ -3383,9 +3384,9 @@ dependencies = [
 
 [[package]]
 name = "solana-metrics"
-version = "1.14.7"
+version = "1.14.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7d280910a33496a33222f3a15f0537d33c55aadf16616ed02cc4731a2bf465f"
+checksum = "f77f7044d57975f001a2c8f3756e4a04f10ca886c69eb8ce0b1786aad52c663d"
 dependencies = [
  "crossbeam-channel",
  "gethostname",
@@ -3397,9 +3398,9 @@ dependencies = [
 
 [[package]]
 name = "solana-net-utils"
-version = "1.14.7"
+version = "1.14.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3bda47bc41e4e6f43de660786541325550f21383b44bf163a04ca4b214fec850"
+checksum = "96e4f0b106e881e087226056612ed06ad3c4ff6260d3f9a1c1d54649c127d34f"
 dependencies = [
  "bincode",
  "clap 3.2.23",
@@ -3419,9 +3420,9 @@ dependencies = [
 
 [[package]]
 name = "solana-perf"
-version = "1.14.7"
+version = "1.14.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fccf3d9e7f6f75727811a618ccd4016d11073023ae9e459ae35d46fb009bde1c"
+checksum = "c02d0782ecaf35dafc7a88c63ec1f265edf6051b55489180d95757d71a4d66d6"
 dependencies = [
  "ahash",
  "bincode",
@@ -3446,9 +3447,9 @@ dependencies = [
 
 [[package]]
 name = "solana-program"
-version = "1.14.7"
+version = "1.14.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "512475cccb7e13f96ba76ed091b2d79a8431a485c73be728cd2235f9adba5a4e"
+checksum = "75602376f2cea17ac301292a3ded6db73e968310ac482857237d95a34473b62a"
 dependencies = [
  "base64 0.13.1",
  "bincode",
@@ -3495,9 +3496,9 @@ dependencies = [
 
 [[package]]
 name = "solana-program-runtime"
-version = "1.14.7"
+version = "1.14.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0741578bbee3a0170a620a4185202d8a559ac4bbba4435fd3d56c8cf432e5ca4"
+checksum = "eb4a1b61c005eb9c0767b215e428c51adfa6e0023691d37f05653a4cd29bce2b"
 dependencies = [
  "base64 0.13.1",
  "bincode",
@@ -3522,9 +3523,9 @@ dependencies = [
 
 [[package]]
 name = "solana-rayon-threadlimit"
-version = "1.14.7"
+version = "1.14.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f5acea18b6b1270e8bbb5fb47b8fd943bd30d673bb12b74ae6b485a0ae87da50"
+checksum = "7091fe2ae498f482f549450e9c5c04e89867dd8622612c742e7c1586b11cc2c1"
 dependencies = [
  "lazy_static",
  "num_cpus",
@@ -3532,9 +3533,9 @@ dependencies = [
 
 [[package]]
 name = "solana-remote-wallet"
-version = "1.14.7"
+version = "1.14.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cdabea51bcec2773d7a65a37f6e9e9f497ffd6f0b2e8bdc719a9e17c8f711f64"
+checksum = "874c76b56601eaf7a91a4d119824b57625c638ce42c601166d1e44eef4b28fc6"
 dependencies = [
  "console",
  "dialoguer",
@@ -3551,9 +3552,9 @@ dependencies = [
 
 [[package]]
 name = "solana-sdk"
-version = "1.14.7"
+version = "1.14.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0cb45fd782d3793c3821dd961b9c7a28b675e187f7f22cff06e694c7743904ce"
+checksum = "a46085d2548bb943e7210b28b09378e361350577b391a94457ad78af1a9f75ef"
 dependencies = [
  "assert_matches",
  "base64 0.13.1",
@@ -3602,9 +3603,9 @@ dependencies = [
 
 [[package]]
 name = "solana-sdk-macro"
-version = "1.14.7"
+version = "1.14.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a08cc4804804ecb9eb07a16c7ff2d4a770fe0533298f36f867a5efc2e3284745"
+checksum = "faa38323e649c70b698e49f1ded17849a9b5da2e0821a38ad08327307009e274"
 dependencies = [
  "bs58 0.4.0",
  "proc-macro2 1.0.47",
@@ -3615,9 +3616,9 @@ dependencies = [
 
 [[package]]
 name = "solana-streamer"
-version = "1.14.7"
+version = "1.14.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "73ced03c13b40fb283782ddc302d8bb7343e3c5f0ce07eb5412a641875b0d4f9"
+checksum = "57ec79681ce38d1b80ffad5507a4b25f6fc9eba827a589fc789561a022a605cf"
 dependencies = [
  "crossbeam-channel",
  "futures-util",
@@ -3644,9 +3645,9 @@ dependencies = [
 
 [[package]]
 name = "solana-transaction-status"
-version = "1.14.7"
+version = "1.14.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89bd8aad27d2f4f9eabcb8979ec360a2cc2237845dfe9f9c53bb2a9bfd377029"
+checksum = "72d3da9fd5d3d7b7c0bc8c071e614c15f73d75612b1a724a4ebf3139458cbb24"
 dependencies = [
  "Inflector",
  "base64 0.13.1",
@@ -3673,9 +3674,9 @@ dependencies = [
 
 [[package]]
 name = "solana-version"
-version = "1.14.7"
+version = "1.14.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7079bd2671de7c61165e25b2002c5b7178f216b221fe2ba3c8fd8b2af96f6a93"
+checksum = "f9592a3fb652a0b84593e18935db930e5f7e9614efaf26e15f3cace1c6d47151"
 dependencies = [
  "log",
  "rustc_version 0.4.0",
@@ -3689,9 +3690,9 @@ dependencies = [
 
 [[package]]
 name = "solana-vote-program"
-version = "1.14.7"
+version = "1.14.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b83e383b6e1810ae9b3ececd1e9a620e19983e3959ed22866ece6fd6c39c0658"
+checksum = "1eddab05371499a937a222f101fd9e2b708b87c575ca3cf01e0c012e14aff79d"
 dependencies = [
  "bincode",
  "log",
@@ -3710,9 +3711,9 @@ dependencies = [
 
 [[package]]
 name = "solana-zk-token-sdk"
-version = "1.14.7"
+version = "1.14.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d4f9818b158e7a49266b83e0c06e551ba429d2395a55de5803eb6e2daa1260c"
+checksum = "d81faf1b8f5c550923f01e9b2c41aec8f646cceff7fd72ca6712d10a4022f163"
 dependencies = [
  "aes-gcm-siv",
  "arrayref",
@@ -3757,9 +3758,9 @@ dependencies = [
 
 [[package]]
 name = "spl-associated-token-account"
-version = "1.1.1"
+version = "1.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "16a33ecc83137583902c3e13c02f34151c8b2f2b74120f9c2b3ff841953e083d"
+checksum = "fbc000f0fdf1f12f99d77d398137c1751345b18c88258ce0f99b7872cf6c9bd6"
 dependencies = [
  "assert_matches",
  "borsh",
@@ -3797,9 +3798,9 @@ dependencies = [
 
 [[package]]
 name = "spl-token-2022"
-version = "0.4.2"
+version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0a97cbf60b91b610c846ccf8eecca96d92a24a19ffbf9fe06cd0c84e76ec45e"
+checksum = "0edb869dbe159b018f17fb9bfa67118c30f232d7f54a73742bc96794dff77ed8"
 dependencies = [
  "arrayref",
  "bytemuck",

+ 68 - 56
lang/src/accounts/account.rs

@@ -223,58 +223,47 @@ use std::ops::{Deref, DerefMut};
 /// ```
 /// to access mint accounts.
 #[derive(Clone)]
-pub struct Account<'info, T: AccountSerialize + AccountDeserialize + Owner + Clone> {
+pub struct Account<'info, T: AccountSerialize + AccountDeserialize + Clone> {
     account: T,
     info: AccountInfo<'info>,
 }
 
-impl<'info, T: AccountSerialize + AccountDeserialize + Owner + Clone + fmt::Debug> fmt::Debug
+impl<'info, T: AccountSerialize + AccountDeserialize + Clone + fmt::Debug> fmt::Debug
     for Account<'info, T>
 {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        f.debug_struct("Account")
+        self.fmt_with_name("Account", f)
+    }
+}
+
+impl<'info, T: AccountSerialize + AccountDeserialize + Clone + fmt::Debug> Account<'info, T> {
+    pub(crate) fn fmt_with_name(&self, name: &str, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_struct(name)
             .field("account", &self.account)
             .field("info", &self.info)
             .finish()
     }
 }
 
-impl<'a, T: AccountSerialize + AccountDeserialize + crate::Owner + Clone> Account<'a, T> {
-    fn new(info: AccountInfo<'a>, account: T) -> Account<'a, T> {
+impl<'a, T: AccountSerialize + AccountDeserialize + Clone> Account<'a, T> {
+    pub(crate) fn new(info: AccountInfo<'a>, account: T) -> Account<'a, T> {
         Self { info, account }
     }
 
-    /// Deserializes the given `info` into a `Account`.
-    #[inline(never)]
-    pub fn try_from(info: &AccountInfo<'a>) -> Result<Account<'a, T>> {
-        if info.owner == &system_program::ID && info.lamports() == 0 {
-            return Err(ErrorCode::AccountNotInitialized.into());
-        }
-        if info.owner != &T::owner() {
-            return Err(Error::from(ErrorCode::AccountOwnedByWrongProgram)
-                .with_pubkeys((*info.owner, T::owner())));
-        }
-        let mut data: &[u8] = &info.try_borrow_data()?;
-        Ok(Account::new(info.clone(), T::try_deserialize(&mut data)?))
-    }
-
-    /// Deserializes the given `info` into a `Account` without checking
-    /// the account discriminator. Be careful when using this and avoid it if
-    /// possible.
-    #[inline(never)]
-    pub fn try_from_unchecked(info: &AccountInfo<'a>) -> Result<Account<'a, T>> {
-        if info.owner == &system_program::ID && info.lamports() == 0 {
-            return Err(ErrorCode::AccountNotInitialized.into());
-        }
-        if info.owner != &T::owner() {
-            return Err(Error::from(ErrorCode::AccountOwnedByWrongProgram)
-                .with_pubkeys((*info.owner, T::owner())));
+    pub(crate) fn exit_with_expected_owner(
+        &self,
+        expected_owner: &Pubkey,
+        program_id: &Pubkey,
+    ) -> Result<()> {
+        // Only persist if the owner is the current program and the account is not closed.
+        if expected_owner == program_id && !crate::common::is_closed(&self.info) {
+            let info = self.to_account_info();
+            let mut data = info.try_borrow_mut_data()?;
+            let dst: &mut [u8] = &mut data;
+            let mut writer = BpfWriter::new(dst);
+            self.account.try_serialize(&mut writer)?;
         }
-        let mut data: &[u8] = &info.try_borrow_data()?;
-        Ok(Account::new(
-            info.clone(),
-            T::try_deserialize_unchecked(&mut data)?,
-        ))
+        Ok(())
     }
 
     /// Reloads the account from storage. This is useful, for example, when
@@ -310,6 +299,41 @@ impl<'a, T: AccountSerialize + AccountDeserialize + crate::Owner + Clone> Accoun
     }
 }
 
+impl<'a, T: AccountSerialize + AccountDeserialize + Owner + Clone> Account<'a, T> {
+    /// Deserializes the given `info` into a `Account`.
+    #[inline(never)]
+    pub fn try_from(info: &AccountInfo<'a>) -> Result<Account<'a, T>> {
+        if info.owner == &system_program::ID && info.lamports() == 0 {
+            return Err(ErrorCode::AccountNotInitialized.into());
+        }
+        if info.owner != &T::owner() {
+            return Err(Error::from(ErrorCode::AccountOwnedByWrongProgram)
+                .with_pubkeys((*info.owner, T::owner())));
+        }
+        let mut data: &[u8] = &info.try_borrow_data()?;
+        Ok(Account::new(info.clone(), T::try_deserialize(&mut data)?))
+    }
+
+    /// Deserializes the given `info` into a `Account` without checking
+    /// the account discriminator. Be careful when using this and avoid it if
+    /// possible.
+    #[inline(never)]
+    pub fn try_from_unchecked(info: &AccountInfo<'a>) -> Result<Account<'a, T>> {
+        if info.owner == &system_program::ID && info.lamports() == 0 {
+            return Err(ErrorCode::AccountNotInitialized.into());
+        }
+        if info.owner != &T::owner() {
+            return Err(Error::from(ErrorCode::AccountOwnedByWrongProgram)
+                .with_pubkeys((*info.owner, T::owner())));
+        }
+        let mut data: &[u8] = &info.try_borrow_data()?;
+        Ok(Account::new(
+            info.clone(),
+            T::try_deserialize_unchecked(&mut data)?,
+        ))
+    }
+}
+
 impl<'info, T: AccountSerialize + AccountDeserialize + Owner + Clone> Accounts<'info>
     for Account<'info, T>
 where
@@ -336,19 +360,11 @@ impl<'info, T: AccountSerialize + AccountDeserialize + Owner + Clone> AccountsEx
     for Account<'info, T>
 {
     fn exit(&self, program_id: &Pubkey) -> Result<()> {
-        // Only persist if the owner is the current program and the account is not closed.
-        if &T::owner() == program_id && !crate::common::is_closed(&self.info) {
-            let info = self.to_account_info();
-            let mut data = info.try_borrow_mut_data()?;
-            let dst: &mut [u8] = &mut data;
-            let mut writer = BpfWriter::new(dst);
-            self.account.try_serialize(&mut writer)?;
-        }
-        Ok(())
+        self.exit_with_expected_owner(&T::owner(), program_id)
     }
 }
 
-impl<'info, T: AccountSerialize + AccountDeserialize + Owner + Clone> AccountsClose<'info>
+impl<'info, T: AccountSerialize + AccountDeserialize + Clone> AccountsClose<'info>
     for Account<'info, T>
 {
     fn close(&self, sol_destination: AccountInfo<'info>) -> Result<()> {
@@ -356,9 +372,7 @@ impl<'info, T: AccountSerialize + AccountDeserialize + Owner + Clone> AccountsCl
     }
 }
 
-impl<'info, T: AccountSerialize + AccountDeserialize + Owner + Clone> ToAccountMetas
-    for Account<'info, T>
-{
+impl<'info, T: AccountSerialize + AccountDeserialize + Clone> ToAccountMetas for Account<'info, T> {
     fn to_account_metas(&self, is_signer: Option<bool>) -> Vec<AccountMeta> {
         let is_signer = is_signer.unwrap_or(self.info.is_signer);
         let meta = match self.info.is_writable {
@@ -369,7 +383,7 @@ impl<'info, T: AccountSerialize + AccountDeserialize + Owner + Clone> ToAccountM
     }
 }
 
-impl<'info, T: AccountSerialize + AccountDeserialize + Owner + Clone> ToAccountInfos<'info>
+impl<'info, T: AccountSerialize + AccountDeserialize + Clone> ToAccountInfos<'info>
     for Account<'info, T>
 {
     fn to_account_infos(&self) -> Vec<AccountInfo<'info>> {
@@ -377,7 +391,7 @@ impl<'info, T: AccountSerialize + AccountDeserialize + Owner + Clone> ToAccountI
     }
 }
 
-impl<'info, T: AccountSerialize + AccountDeserialize + Owner + Clone> AsRef<AccountInfo<'info>>
+impl<'info, T: AccountSerialize + AccountDeserialize + Clone> AsRef<AccountInfo<'info>>
     for Account<'info, T>
 {
     fn as_ref(&self) -> &AccountInfo<'info> {
@@ -385,15 +399,13 @@ impl<'info, T: AccountSerialize + AccountDeserialize + Owner + Clone> AsRef<Acco
     }
 }
 
-impl<'info, T: AccountSerialize + AccountDeserialize + Owner + Clone> AsRef<T>
-    for Account<'info, T>
-{
+impl<'info, T: AccountSerialize + AccountDeserialize + Clone> AsRef<T> for Account<'info, T> {
     fn as_ref(&self) -> &T {
         &self.account
     }
 }
 
-impl<'a, T: AccountSerialize + AccountDeserialize + Owner + Clone> Deref for Account<'a, T> {
+impl<'a, T: AccountSerialize + AccountDeserialize + Clone> Deref for Account<'a, T> {
     type Target = T;
 
     fn deref(&self) -> &Self::Target {
@@ -401,7 +413,7 @@ impl<'a, T: AccountSerialize + AccountDeserialize + Owner + Clone> Deref for Acc
     }
 }
 
-impl<'a, T: AccountSerialize + AccountDeserialize + Owner + Clone> DerefMut for Account<'a, T> {
+impl<'a, T: AccountSerialize + AccountDeserialize + Clone> DerefMut for Account<'a, T> {
     fn deref_mut(&mut self) -> &mut Self::Target {
         #[cfg(feature = "anchor-debug")]
         if !self.info.is_writable {
@@ -412,7 +424,7 @@ impl<'a, T: AccountSerialize + AccountDeserialize + Owner + Clone> DerefMut for
     }
 }
 
-impl<'info, T: AccountSerialize + AccountDeserialize + Owner + Clone> Key for Account<'info, T> {
+impl<'info, T: AccountSerialize + AccountDeserialize + Clone> Key for Account<'info, T> {
     fn key(&self) -> Pubkey {
         *self.info.key
     }

+ 144 - 0
lang/src/accounts/interface.rs

@@ -0,0 +1,144 @@
+//! Type validating that the account is one of a set of given Programs
+
+use crate::accounts::program::Program;
+use crate::error::{Error, ErrorCode};
+use crate::{
+    AccountDeserialize, Accounts, AccountsExit, CheckId, Key, Result, ToAccountInfos,
+    ToAccountMetas,
+};
+use solana_program::account_info::AccountInfo;
+use solana_program::instruction::AccountMeta;
+use solana_program::pubkey::Pubkey;
+use std::collections::{BTreeMap, BTreeSet};
+use std::ops::Deref;
+
+/// Type validating that the account is one of a set of given Programs
+///
+/// The `Interface` wraps over the [`Program`](crate::Program), allowing for
+/// multiple possible program ids. Useful for any program that implements an
+/// instruction interface. For example, spl-token and spl-token-2022 both implement
+/// the spl-token interface.
+///
+/// # Table of Contents
+/// - [Basic Functionality](#basic-functionality)
+/// - [Out of the Box Types](#out-of-the-box-types)
+///
+/// # Basic Functionality
+///
+/// Checks:
+///
+/// - `expected_programs.contains(account_info.key)`
+/// - `account_info.executable == true`
+///
+/// # Example
+/// ```ignore
+/// #[program]
+/// mod my_program {
+///     fn set_admin_settings(...){...}
+/// }
+///
+/// #[account]
+/// #[derive(Default)]
+/// pub struct AdminSettings {
+///     ...
+/// }
+///
+/// #[derive(Accounts)]
+/// pub struct SetAdminSettings<'info> {
+///     #[account(mut, seeds = [b"admin"], bump)]
+///     pub admin_settings: Account<'info, AdminSettings>,
+///     #[account(constraint = program.programdata_address()? == Some(program_data.key()))]
+///     pub program: Interface<'info, MyProgram>,
+///     #[account(constraint = program_data.upgrade_authority_address == Some(authority.key()))]
+///     pub program_data: Account<'info, ProgramData>,
+///     pub authority: Signer<'info>,
+/// }
+/// ```
+/// The given program has a function with which the upgrade authority can set admin settings.
+///
+/// The required constraints are as follows:
+///
+/// - `program` is the account of the program itself.
+/// Its constraint checks that `program_data` is the account that contains the program's upgrade authority.
+/// Implicitly, this checks that `program` is a BPFUpgradeable program (`program.programdata_address()?`
+/// will be `None` if it's not).
+/// - `program_data`'s constraint checks that its upgrade authority is the `authority` account.
+/// - Finally, `authority` needs to sign the transaction.
+///
+/// # Out of the Box Types
+///
+/// Between the [`anchor_lang`](https://docs.rs/anchor-lang/latest/anchor_lang) and [`anchor_spl`](https://docs.rs/anchor_spl/latest/anchor_spl) crates,
+/// the following `Interface` types are provided out of the box:
+///
+/// - [`TokenInterface`](https://docs.rs/anchor-spl/latest/anchor_spl/token_interface/struct.TokenInterface.html)
+///
+#[derive(Clone)]
+pub struct Interface<'info, T>(Program<'info, T>);
+impl<'a, T> Interface<'a, T> {
+    pub(crate) fn new(info: AccountInfo<'a>) -> Self {
+        Self(Program::new(info))
+    }
+    pub fn programdata_address(&self) -> Result<Option<Pubkey>> {
+        self.0.programdata_address()
+    }
+}
+impl<'a, T: CheckId> TryFrom<&AccountInfo<'a>> for Interface<'a, T> {
+    type Error = Error;
+    /// Deserializes the given `info` into a `Program`.
+    fn try_from(info: &AccountInfo<'a>) -> Result<Self> {
+        T::check_id(info.key)?;
+        if !info.executable {
+            return Err(ErrorCode::InvalidProgramExecutable.into());
+        }
+        Ok(Self::new(info.clone()))
+    }
+}
+impl<'info, T> Deref for Interface<'info, T> {
+    type Target = AccountInfo<'info>;
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+impl<'info, T> AsRef<AccountInfo<'info>> for Interface<'info, T> {
+    fn as_ref(&self) -> &AccountInfo<'info> {
+        &self.0
+    }
+}
+
+impl<'info, T: CheckId> Accounts<'info> for Interface<'info, T> {
+    #[inline(never)]
+    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 Err(ErrorCode::AccountNotEnoughKeys.into());
+        }
+        let account = &accounts[0];
+        *accounts = &accounts[1..];
+        Self::try_from(account)
+    }
+}
+
+impl<'info, T> ToAccountMetas for Interface<'info, T> {
+    fn to_account_metas(&self, is_signer: Option<bool>) -> Vec<AccountMeta> {
+        self.0.to_account_metas(is_signer)
+    }
+}
+
+impl<'info, T> ToAccountInfos<'info> for Interface<'info, T> {
+    fn to_account_infos(&self) -> Vec<AccountInfo<'info>> {
+        self.0.to_account_infos()
+    }
+}
+
+impl<'info, T: AccountDeserialize> AccountsExit<'info> for Interface<'info, T> {}
+
+impl<'info, T: AccountDeserialize> Key for Interface<'info, T> {
+    fn key(&self) -> Pubkey {
+        self.0.key()
+    }
+}

+ 331 - 0
lang/src/accounts/interface_account.rs

@@ -0,0 +1,331 @@
+//! Account container that checks ownership on deserialization.
+
+use crate::accounts::account::Account;
+use crate::error::ErrorCode;
+use crate::{
+    AccountDeserialize, AccountSerialize, Accounts, AccountsClose, AccountsExit, CheckOwner, Key,
+    Owners, Result, ToAccountInfos, ToAccountMetas,
+};
+use solana_program::account_info::AccountInfo;
+use solana_program::instruction::AccountMeta;
+use solana_program::pubkey::Pubkey;
+use solana_program::system_program;
+use std::collections::{BTreeMap, BTreeSet};
+use std::fmt;
+use std::ops::{Deref, DerefMut};
+
+/// Wrapper around [`AccountInfo`](crate::solana_program::account_info::AccountInfo)
+/// that verifies program ownership and deserializes underlying data into a Rust type.
+///
+/// # Table of Contents
+/// - [Basic Functionality](#basic-functionality)
+/// - [Using InterfaceAccount with non-anchor types](#using-interface-account-with-non-anchor-types)
+/// - [Out of the box wrapper types](#out-of-the-box-wrapper-types)
+///
+/// # Basic Functionality
+///
+/// InterfaceAccount checks that `T::owners().contains(Account.info.owner)`.
+/// This means that the data type that Accounts wraps around (`=T`) needs to
+/// implement the [Owners trait](crate::Owners).
+/// The `#[account]` attribute implements the Owners trait for
+/// a struct using multiple `crate::ID`s declared by [`declareId`](crate::declare_id)
+/// in the same program. It follows that InterfaceAccount can also be used
+/// with a `T` that comes from a different program.
+///
+/// Checks:
+///
+/// - `T::owners().contains(InterfaceAccount.info.owner)`
+/// - `!(InterfaceAccount.info.owner == SystemProgram && InterfaceAccount.info.lamports() == 0)`
+///
+/// # Example
+/// ```ignore
+/// use anchor_lang::prelude::*;
+/// use other_program::Auth;
+///
+/// declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
+///
+/// #[program]
+/// mod hello_anchor {
+///     use super::*;
+///     pub fn set_data(ctx: Context<SetData>, data: u64) -> Result<()> {
+///         if (*ctx.accounts.auth_account).authorized {
+///             (*ctx.accounts.my_account).data = data;
+///         }
+///         Ok(())
+///     }
+/// }
+///
+/// #[account]
+/// #[derive(Default)]
+/// pub struct MyData {
+///     pub data: u64
+/// }
+///
+/// #[derive(Accounts)]
+/// pub struct SetData<'info> {
+///     #[account(mut)]
+///     pub my_account: InterfaceAccount<'info, MyData> // checks that my_account.info.owner == Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS
+///     pub auth_account: InterfaceAccount<'info, Auth> // checks that auth_account.info.owner == FEZGUxNhZWpYPj9MJCrZJvUo1iF9ys34UHx52y4SzVW9
+/// }
+///
+/// // In a different program
+///
+/// ...
+/// declare_id!("FEZGUxNhZWpYPj9MJCrZJvUo1iF9ys34UHx52y4SzVW9");
+/// #[account]
+/// #[derive(Default)]
+/// pub struct Auth {
+///     pub authorized: bool
+/// }
+/// ...
+/// ```
+///
+/// # Using InterfaceAccount with non-anchor programs
+///
+/// InterfaceAccount can also be used with non-anchor programs. The data types from
+/// those programs are not annotated with `#[account]` so you have to
+/// - create a wrapper type around the structs you want to wrap with InterfaceAccount
+/// - implement the functions required by InterfaceAccount yourself
+/// instead of using `#[account]`. You only have to implement a fraction of the
+/// functions `#[account]` generates. See the example below for the code you have
+/// to write.
+///
+/// The mint wrapper type that Anchor provides out of the box for the token program ([source](https://github.com/coral-xyz/anchor/blob/master/spl/src/token.rs))
+/// ```ignore
+/// #[derive(Clone)]
+/// pub struct Mint(spl_token::state::Mint);
+///
+/// // This is necessary so we can use "anchor_spl::token::Mint::LEN"
+/// // because rust does not resolve "anchor_spl::token::Mint::LEN" to
+/// // "spl_token::state::Mint::LEN" automatically
+/// impl Mint {
+///     pub const LEN: usize = spl_token::state::Mint::LEN;
+/// }
+///
+/// // You don't have to implement the "try_deserialize" function
+/// // from this trait. It delegates to
+/// // "try_deserialize_unchecked" by default which is what we want here
+/// // because non-anchor accounts don't have a discriminator to check
+/// impl anchor_lang::AccountDeserialize for Mint {
+///     fn try_deserialize_unchecked(buf: &mut &[u8]) -> Result<Self> {
+///         spl_token::state::Mint::unpack(buf).map(Mint)
+///     }
+/// }
+/// // AccountSerialize defaults to a no-op which is what we want here
+/// // because it's a foreign program, so our program does not
+/// // have permission to write to the foreign program's accounts anyway
+/// impl anchor_lang::AccountSerialize for Mint {}
+///
+/// impl anchor_lang::Owner for Mint {
+///     fn owner() -> Pubkey {
+///         // pub use spl_token::ID is used at the top of the file
+///         ID
+///     }
+/// }
+///
+/// // Implement the "std::ops::Deref" trait for better user experience
+/// impl Deref for Mint {
+///     type Target = spl_token::state::Mint;
+///
+///     fn deref(&self) -> &Self::Target {
+///         &self.0
+///     }
+/// }
+/// ```
+///
+/// ## Out of the box wrapper types
+///
+/// ### SPL Types
+///
+/// Anchor provides wrapper types to access accounts owned by the token programs. Use
+/// ```ignore
+/// use anchor_spl::token_interface::TokenAccount;
+///
+/// #[derive(Accounts)]
+/// pub struct Example {
+///     pub my_acc: InterfaceAccount<'info, TokenAccount>
+/// }
+/// ```
+/// to access token accounts and
+/// ```ignore
+/// use anchor_spl::token_interface::Mint;
+///
+/// #[derive(Accounts)]
+/// pub struct Example {
+///     pub my_acc: InterfaceAccount<'info, Mint>
+/// }
+/// ```
+/// to access mint accounts.
+#[derive(Clone)]
+pub struct InterfaceAccount<'info, T: AccountSerialize + AccountDeserialize + Clone> {
+    account: Account<'info, T>,
+    // The owner here is used to make sure that changes aren't incorrectly propagated
+    // to an account with a modified owner
+    owner: Pubkey,
+}
+
+impl<'info, T: AccountSerialize + AccountDeserialize + Clone + fmt::Debug> fmt::Debug
+    for InterfaceAccount<'info, T>
+{
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        self.account.fmt_with_name("InterfaceAccount", f)
+    }
+}
+
+impl<'a, T: AccountSerialize + AccountDeserialize + Clone> InterfaceAccount<'a, T> {
+    fn new(info: AccountInfo<'a>, account: T) -> Self {
+        let owner = *info.owner;
+        Self {
+            account: Account::new(info, account),
+            owner,
+        }
+    }
+
+    /// Reloads the account from storage. This is useful, for example, when
+    /// observing side effects after CPI.
+    pub fn reload(&mut self) -> Result<()> {
+        self.account.reload()
+    }
+
+    pub fn into_inner(self) -> T {
+        self.account.into_inner()
+    }
+
+    /// Sets the inner account.
+    ///
+    /// Instead of this:
+    /// ```ignore
+    /// pub fn new_user(ctx: Context<CreateUser>, new_user:User) -> Result<()> {
+    ///     (*ctx.accounts.user_to_create).name = new_user.name;
+    ///     (*ctx.accounts.user_to_create).age = new_user.age;
+    ///     (*ctx.accounts.user_to_create).address = new_user.address;
+    /// }
+    /// ```
+    /// You can do this:
+    /// ```ignore
+    /// pub fn new_user(ctx: Context<CreateUser>, new_user:User) -> Result<()> {
+    ///     ctx.accounts.user_to_create.set_inner(new_user);
+    /// }
+    /// ```
+    pub fn set_inner(&mut self, inner: T) {
+        self.account.set_inner(inner);
+    }
+}
+
+impl<'a, T: AccountSerialize + AccountDeserialize + CheckOwner + Clone> InterfaceAccount<'a, T> {
+    /// Deserializes the given `info` into a `InterfaceAccount`.
+    #[inline(never)]
+    pub fn try_from(info: &AccountInfo<'a>) -> Result<Self> {
+        if info.owner == &system_program::ID && info.lamports() == 0 {
+            return Err(ErrorCode::AccountNotInitialized.into());
+        }
+        T::check_owner(info.owner)?;
+        let mut data: &[u8] = &info.try_borrow_data()?;
+        Ok(Self::new(info.clone(), T::try_deserialize(&mut data)?))
+    }
+
+    /// Deserializes the given `info` into a `InterfaceAccount` without checking
+    /// the account discriminator. Be careful when using this and avoid it if
+    /// possible.
+    #[inline(never)]
+    pub fn try_from_unchecked(info: &AccountInfo<'a>) -> Result<Self> {
+        if info.owner == &system_program::ID && info.lamports() == 0 {
+            return Err(ErrorCode::AccountNotInitialized.into());
+        }
+        T::check_owner(info.owner)?;
+        let mut data: &[u8] = &info.try_borrow_data()?;
+        Ok(Self::new(
+            info.clone(),
+            T::try_deserialize_unchecked(&mut data)?,
+        ))
+    }
+}
+
+impl<'info, T: AccountSerialize + AccountDeserialize + CheckOwner + Clone> Accounts<'info>
+    for InterfaceAccount<'info, T>
+{
+    #[inline(never)]
+    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 Err(ErrorCode::AccountNotEnoughKeys.into());
+        }
+        let account = &accounts[0];
+        *accounts = &accounts[1..];
+        Self::try_from(account)
+    }
+}
+
+impl<'info, T: AccountSerialize + AccountDeserialize + Owners + Clone> AccountsExit<'info>
+    for InterfaceAccount<'info, T>
+{
+    fn exit(&self, program_id: &Pubkey) -> Result<()> {
+        self.account
+            .exit_with_expected_owner(&self.owner, program_id)
+    }
+}
+
+impl<'info, T: AccountSerialize + AccountDeserialize + Clone> AccountsClose<'info>
+    for InterfaceAccount<'info, T>
+{
+    fn close(&self, sol_destination: AccountInfo<'info>) -> Result<()> {
+        self.account.close(sol_destination)
+    }
+}
+
+impl<'info, T: AccountSerialize + AccountDeserialize + Clone> ToAccountMetas
+    for InterfaceAccount<'info, T>
+{
+    fn to_account_metas(&self, is_signer: Option<bool>) -> Vec<AccountMeta> {
+        self.account.to_account_metas(is_signer)
+    }
+}
+
+impl<'info, T: AccountSerialize + AccountDeserialize + Clone> ToAccountInfos<'info>
+    for InterfaceAccount<'info, T>
+{
+    fn to_account_infos(&self) -> Vec<AccountInfo<'info>> {
+        self.account.to_account_infos()
+    }
+}
+
+impl<'info, T: AccountSerialize + AccountDeserialize + Clone> AsRef<AccountInfo<'info>>
+    for InterfaceAccount<'info, T>
+{
+    fn as_ref(&self) -> &AccountInfo<'info> {
+        self.account.as_ref()
+    }
+}
+
+impl<'info, T: AccountSerialize + AccountDeserialize + Clone> AsRef<T>
+    for InterfaceAccount<'info, T>
+{
+    fn as_ref(&self) -> &T {
+        self.account.as_ref()
+    }
+}
+
+impl<'a, T: AccountSerialize + AccountDeserialize + Clone> Deref for InterfaceAccount<'a, T> {
+    type Target = T;
+
+    fn deref(&self) -> &Self::Target {
+        self.account.deref()
+    }
+}
+
+impl<'a, T: AccountSerialize + AccountDeserialize + Clone> DerefMut for InterfaceAccount<'a, T> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        self.account.deref_mut()
+    }
+}
+
+impl<'info, T: AccountSerialize + AccountDeserialize + Clone> Key for InterfaceAccount<'info, T> {
+    fn key(&self) -> Pubkey {
+        self.account.key()
+    }
+}

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

@@ -4,6 +4,8 @@ pub mod account;
 pub mod account_info;
 pub mod account_loader;
 pub mod boxed;
+pub mod interface;
+pub mod interface_account;
 pub mod option;
 pub mod program;
 pub mod signer;

+ 27 - 27
lang/src/accounts/program.rs

@@ -9,6 +9,7 @@ use solana_program::bpf_loader_upgradeable::{self, UpgradeableLoaderState};
 use solana_program::instruction::AccountMeta;
 use solana_program::pubkey::Pubkey;
 use std::collections::{BTreeMap, BTreeSet};
+use std::convert::TryFrom;
 use std::fmt;
 use std::marker::PhantomData;
 use std::ops::Deref;
@@ -75,38 +76,25 @@ use std::ops::Deref;
 /// - [`Token`](https://docs.rs/anchor-spl/latest/anchor_spl/token/struct.Token.html)
 ///
 #[derive(Clone)]
-pub struct Program<'info, T: Id + Clone> {
+pub struct Program<'info, T> {
     info: AccountInfo<'info>,
     _phantom: PhantomData<T>,
 }
 
-impl<'info, T: Id + Clone + fmt::Debug> fmt::Debug for Program<'info, T> {
+impl<'info, T: fmt::Debug> fmt::Debug for Program<'info, T> {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         f.debug_struct("Program").field("info", &self.info).finish()
     }
 }
 
-impl<'a, T: Id + Clone> Program<'a, T> {
-    fn new(info: AccountInfo<'a>) -> Program<'a, T> {
+impl<'a, T> Program<'a, T> {
+    pub(crate) fn new(info: AccountInfo<'a>) -> Program<'a, T> {
         Self {
             info,
             _phantom: PhantomData,
         }
     }
 
-    /// Deserializes the given `info` into a `Program`.
-    #[inline(never)]
-    pub fn try_from(info: &AccountInfo<'a>) -> Result<Program<'a, T>> {
-        if info.key != &T::id() {
-            return Err(Error::from(ErrorCode::InvalidProgramId).with_pubkeys((*info.key, T::id())));
-        }
-        if !info.executable {
-            return Err(ErrorCode::InvalidProgramExecutable.into());
-        }
-
-        Ok(Program::new(info.clone()))
-    }
-
     pub fn programdata_address(&self) -> Result<Option<Pubkey>> {
         if *self.info.owner == bpf_loader_upgradeable::ID {
             let mut data: &[u8] = &self.info.try_borrow_data()?;
@@ -137,10 +125,22 @@ impl<'a, T: Id + Clone> Program<'a, T> {
     }
 }
 
-impl<'info, T> Accounts<'info> for Program<'info, T>
-where
-    T: Id + Clone,
-{
+impl<'a, T: Id> TryFrom<&AccountInfo<'a>> for Program<'a, T> {
+    type Error = Error;
+    /// Deserializes the given `info` into a `Program`.
+    fn try_from(info: &AccountInfo<'a>) -> Result<Self> {
+        if info.key != &T::id() {
+            return Err(Error::from(ErrorCode::InvalidProgramId).with_pubkeys((*info.key, T::id())));
+        }
+        if !info.executable {
+            return Err(ErrorCode::InvalidProgramExecutable.into());
+        }
+
+        Ok(Program::new(info.clone()))
+    }
+}
+
+impl<'info, T: Id> Accounts<'info> for Program<'info, T> {
     #[inline(never)]
     fn try_accounts(
         _program_id: &Pubkey,
@@ -158,7 +158,7 @@ where
     }
 }
 
-impl<'info, T: Id + Clone> ToAccountMetas for Program<'info, T> {
+impl<'info, T> ToAccountMetas for Program<'info, T> {
     fn to_account_metas(&self, is_signer: Option<bool>) -> Vec<AccountMeta> {
         let is_signer = is_signer.unwrap_or(self.info.is_signer);
         let meta = match self.info.is_writable {
@@ -169,19 +169,19 @@ impl<'info, T: Id + Clone> ToAccountMetas for Program<'info, T> {
     }
 }
 
-impl<'info, T: Id + Clone> ToAccountInfos<'info> for Program<'info, T> {
+impl<'info, T> ToAccountInfos<'info> for Program<'info, T> {
     fn to_account_infos(&self) -> Vec<AccountInfo<'info>> {
         vec![self.info.clone()]
     }
 }
 
-impl<'info, T: Id + Clone> AsRef<AccountInfo<'info>> for Program<'info, T> {
+impl<'info, T> AsRef<AccountInfo<'info>> for Program<'info, T> {
     fn as_ref(&self) -> &AccountInfo<'info> {
         &self.info
     }
 }
 
-impl<'info, T: Id + Clone> Deref for Program<'info, T> {
+impl<'info, T> Deref for Program<'info, T> {
     type Target = AccountInfo<'info>;
 
     fn deref(&self) -> &Self::Target {
@@ -189,9 +189,9 @@ impl<'info, T: Id + Clone> Deref for Program<'info, T> {
     }
 }
 
-impl<'info, T: AccountDeserialize + Id + Clone> AccountsExit<'info> for Program<'info, T> {}
+impl<'info, T: AccountDeserialize> AccountsExit<'info> for Program<'info, T> {}
 
-impl<'info, T: AccountDeserialize + Id + Clone> Key for Program<'info, T> {
+impl<'info, T: AccountDeserialize> Key for Program<'info, T> {
     fn key(&self) -> Pubkey {
         *self.info.key
     }

+ 45 - 1
lang/src/lib.rs

@@ -224,11 +224,54 @@ pub trait Owner {
     fn owner() -> Pubkey;
 }
 
+/// Defines a list of addresses expected to own an account.
+pub trait Owners {
+    fn owners() -> &'static [Pubkey];
+}
+
+/// Defines a trait for checking the owner of a program.
+pub trait CheckOwner {
+    fn check_owner(owner: &Pubkey) -> Result<()>;
+}
+
+impl<T: Owners> CheckOwner for T {
+    fn check_owner(owner: &Pubkey) -> Result<()> {
+        if !Self::owners().contains(owner) {
+            Err(
+                error::Error::from(error::ErrorCode::AccountOwnedByWrongProgram)
+                    .with_account_name(*owner),
+            )
+        } else {
+            Ok(())
+        }
+    }
+}
+
 /// Defines the id of a program.
 pub trait Id {
     fn id() -> Pubkey;
 }
 
+/// Defines the possible ids of a program.
+pub trait Ids {
+    fn ids() -> &'static [Pubkey];
+}
+
+/// Defines a trait for checking the id of a program.
+pub trait CheckId {
+    fn check_id(id: &Pubkey) -> Result<()>;
+}
+
+impl<T: Ids> CheckId for T {
+    fn check_id(id: &Pubkey) -> Result<()> {
+        if !Self::ids().contains(id) {
+            Err(error::Error::from(error::ErrorCode::InvalidProgramId).with_account_name(*id))
+        } else {
+            Ok(())
+        }
+    }
+}
+
 /// Defines the Pubkey of an account.
 pub trait Key {
     fn key(&self) -> Pubkey;
@@ -245,7 +288,8 @@ impl Key for Pubkey {
 pub mod prelude {
     pub use super::{
         access_control, account, accounts::account::Account,
-        accounts::account_loader::AccountLoader, accounts::program::Program,
+        accounts::account_loader::AccountLoader, accounts::interface::Interface,
+        accounts::interface_account::InterfaceAccount, accounts::program::Program,
         accounts::signer::Signer, accounts::system_account::SystemAccount,
         accounts::sysvar::Sysvar, accounts::unchecked_account::UncheckedAccount, constant,
         context::Context, context::CpiContext, declare_id, emit, err, error, event, program,

+ 36 - 13
lang/syn/src/codegen/accounts/constraints.rs

@@ -284,6 +284,7 @@ pub fn generate_constraint_signer(f: &Field, c: &ConstraintSigner) -> proc_macro
     let info = match f.ty {
         Ty::AccountInfo => quote! { #ident },
         Ty::Account(_) => quote! { #ident.to_account_info() },
+        Ty::InterfaceAccount(_) => quote! { #ident.to_account_info() },
         Ty::AccountLoader(_) => quote! { #ident.to_account_info() },
         _ => panic!("Invalid syntax: signer cannot be specified."),
     };
@@ -520,9 +521,11 @@ fn generate_constraint_init_group(
 
             let payer_optional_check = check_scope.generate_check(payer);
 
+            let token_account_space = generate_get_token_account_space(mint);
+
             let create_account = generate_create_account(
                 field,
-                quote! {anchor_spl::token::TokenAccount::LEN},
+                quote! {#token_account_space},
                 quote! {&token_program.key()},
                 quote! {#payer},
                 seeds_with_bump,
@@ -544,13 +547,13 @@ fn generate_constraint_init_group(
 
                         // Initialize the token account.
                         let cpi_program = token_program.to_account_info();
-                        let accounts = anchor_spl::token::InitializeAccount3 {
+                        let accounts = ::anchor_spl::token_interface::InitializeAccount3 {
                             account: #field.to_account_info(),
                             mint: #mint.to_account_info(),
                             authority: #owner.to_account_info(),
                         };
                         let cpi_ctx = anchor_lang::context::CpiContext::new(cpi_program, accounts);
-                        anchor_spl::token::initialize_account3(cpi_ctx)?;
+                        ::anchor_spl::token_interface::initialize_account3(cpi_ctx)?;
                     }
 
                     let pa: #ty_decl = #from_account_info_unchecked;
@@ -599,7 +602,7 @@ fn generate_constraint_init_group(
                         #payer_optional_check
 
                         let cpi_program = associated_token_program.to_account_info();
-                        let cpi_accounts = anchor_spl::associated_token::Create {
+                        let cpi_accounts = ::anchor_spl::associated_token::Create {
                             payer: #payer.to_account_info(),
                             associated_token: #field.to_account_info(),
                             authority: #owner.to_account_info(),
@@ -608,7 +611,7 @@ fn generate_constraint_init_group(
                             token_program: token_program.to_account_info(),
                         };
                         let cpi_ctx = anchor_lang::context::CpiContext::new(cpi_program, cpi_accounts);
-                        anchor_spl::associated_token::create(cpi_ctx)?;
+                        ::anchor_spl::associated_token::create(cpi_ctx)?;
                     }
                     let pa: #ty_decl = #from_account_info_unchecked;
                     if #if_needed {
@@ -619,7 +622,7 @@ fn generate_constraint_init_group(
                             return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintTokenOwner).with_account_name(#name_str).with_pubkeys((pa.owner, #owner.key())));
                         }
 
-                        if pa.key() != anchor_spl::associated_token::get_associated_token_address(&#owner.key(), &#mint.key()) {
+                        if pa.key() != ::anchor_spl::associated_token::get_associated_token_address(&#owner.key(), &#mint.key()) {
                             return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::AccountNotAssociatedTokenAccount).with_account_name(#name_str));
                         }
                     }
@@ -654,7 +657,7 @@ fn generate_constraint_init_group(
 
             let create_account = generate_create_account(
                 field,
-                quote! {anchor_spl::token::Mint::LEN},
+                quote! {::anchor_spl::token::Mint::LEN},
                 quote! {&token_program.key()},
                 quote! {#payer},
                 seeds_with_bump,
@@ -682,11 +685,11 @@ fn generate_constraint_init_group(
 
                         // Initialize the mint account.
                         let cpi_program = token_program.to_account_info();
-                        let accounts = anchor_spl::token::InitializeMint2 {
+                        let accounts = ::anchor_spl::token_interface::InitializeMint2 {
                             mint: #field.to_account_info(),
                         };
                         let cpi_ctx = anchor_lang::context::CpiContext::new(cpi_program, accounts);
-                        anchor_spl::token::initialize_mint2(cpi_ctx, #decimals, &#owner.key(), #freeze_authority)?;
+                        ::anchor_spl::token_interface::initialize_mint2(cpi_ctx, #decimals, &#owner.key(), #freeze_authority)?;
                     }
                     let pa: #ty_decl = #from_account_info_unchecked;
                     if #if_needed {
@@ -707,7 +710,7 @@ fn generate_constraint_init_group(
                 };
             }
         }
-        InitKind::Program { owner } => {
+        InitKind::Program { owner } | InitKind::Interface { owner } => {
             // Define the space variable.
             let space = quote! {let space = #space;};
 
@@ -895,7 +898,7 @@ fn generate_constraint_associated_token(
             if my_owner != wallet_address {
                 return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintTokenOwner).with_account_name(#name_str).with_pubkeys((my_owner, wallet_address)));
             }
-            let __associated_token_address = anchor_spl::associated_token::get_associated_token_address(&wallet_address, &#spl_token_mint_address.key());
+            let __associated_token_address = ::anchor_spl::associated_token::get_associated_token_address(&wallet_address, &#spl_token_mint_address.key());
             let my_key = #name.key();
             if my_key != __associated_token_address {
                 return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintAssociated).with_account_name(#name_str).with_pubkeys((my_key, __associated_token_address)));
@@ -1028,6 +1031,25 @@ impl<'a> OptionalCheckScope<'a> {
     }
 }
 
+fn generate_get_token_account_space(mint: &Expr) -> proc_macro2::TokenStream {
+    quote! {
+        {
+            let mint_info = #mint.to_account_info();
+            if *mint_info.owner == ::anchor_spl::token_2022::Token2022::id() {
+                use ::anchor_spl::token_2022::spl_token_2022::extension::{BaseStateWithExtensions, ExtensionType, StateWithExtensions};
+                use ::anchor_spl::token_2022::spl_token_2022::state::{Account, Mint};
+                let mint_data = mint_info.try_borrow_data()?;
+                let mint_state = StateWithExtensions::<Mint>::unpack(&mint_data)?;
+                let mint_extensions = mint_state.get_extension_types()?;
+                let required_extensions = ExtensionType::get_required_init_account_extensions(&mint_extensions);
+                ExtensionType::get_account_len::<Account>(&required_extensions)
+            } else {
+                ::anchor_spl::token::TokenAccount::LEN
+            }
+        }
+    }
+}
+
 // Generated code to create an account with with system program with the
 // given `space` amount of data, owned by `owner`.
 //
@@ -1051,13 +1073,14 @@ fn generate_create_account(
         let __current_lamports = #field.lamports();
         if __current_lamports == 0 {
             // Create the token account with right amount of lamports and space, and the correct owner.
-            let lamports = __anchor_rent.minimum_balance(#space);
+            let space = #space;
+            let lamports = __anchor_rent.minimum_balance(space);
             let cpi_accounts = anchor_lang::system_program::CreateAccount {
                 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)?;
+            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);
             // Fund the account for rent exemption.

+ 39 - 2
lang/syn/src/lib.rs

@@ -254,7 +254,8 @@ impl Field {
             Ty::SystemAccount => quote! {
                 SystemAccount
             },
-            Ty::Account(AccountTy { boxed, .. }) => {
+            Ty::Account(AccountTy { boxed, .. })
+            | Ty::InterfaceAccount(InterfaceAccountTy { boxed, .. }) => {
                 if *boxed {
                     quote! {
                         Box<#container_ty<#account_ty>>
@@ -321,7 +322,8 @@ impl Field {
             Ty::UncheckedAccount => {
                 quote! { UncheckedAccount::try_from(#field.to_account_info()) }
             }
-            Ty::Account(AccountTy { boxed, .. }) => {
+            Ty::Account(AccountTy { boxed, .. })
+            | Ty::InterfaceAccount(InterfaceAccountTy { boxed, .. }) => {
                 let stream = if checked {
                     quote! {
                         match #container_ty::try_from(&#field) {
@@ -392,6 +394,10 @@ impl Field {
             },
             Ty::Sysvar(_) => quote! { anchor_lang::accounts::sysvar::Sysvar },
             Ty::Program(_) => quote! { anchor_lang::accounts::program::Program },
+            Ty::Interface(_) => quote! { anchor_lang::accounts::interface::Interface },
+            Ty::InterfaceAccount(_) => {
+                quote! { anchor_lang::accounts::interface_account::InterfaceAccount }
+            }
             Ty::AccountInfo => quote! {},
             Ty::UncheckedAccount => quote! {},
             Ty::Signer => quote! {},
@@ -424,6 +430,12 @@ impl Field {
                     #ident
                 }
             }
+            Ty::InterfaceAccount(ty) => {
+                let ident = &ty.account_type_path;
+                quote! {
+                    #ident
+                }
+            }
             Ty::AccountLoader(ty) => {
                 let ident = &ty.account_type_path;
                 quote! {
@@ -448,6 +460,12 @@ impl Field {
                     #program
                 }
             }
+            Ty::Interface(ty) => {
+                let program = &ty.account_type_path;
+                quote! {
+                    #program
+                }
+            }
         }
     }
 }
@@ -471,6 +489,8 @@ pub enum Ty {
     Sysvar(SysvarTy),
     Account(AccountTy),
     Program(ProgramTy),
+    Interface(InterfaceTy),
+    InterfaceAccount(InterfaceAccountTy),
     Signer,
     SystemAccount,
     ProgramData,
@@ -504,12 +524,26 @@ pub struct AccountTy {
     pub boxed: bool,
 }
 
+#[derive(Debug, PartialEq, Eq)]
+pub struct InterfaceAccountTy {
+    // The struct type of the account.
+    pub account_type_path: TypePath,
+    // True if the account has been boxed via `Box<T>`.
+    pub boxed: bool,
+}
+
 #[derive(Debug, PartialEq, Eq)]
 pub struct ProgramTy {
     // The struct type of the account.
     pub account_type_path: TypePath,
 }
 
+#[derive(Debug, PartialEq, Eq)]
+pub struct InterfaceTy {
+    // The struct type of the account.
+    pub account_type_path: TypePath,
+}
+
 #[derive(Debug)]
 pub struct Error {
     pub name: String,
@@ -760,6 +794,9 @@ pub enum InitKind {
     Program {
         owner: Option<Expr>,
     },
+    Interface {
+        owner: Option<Expr>,
+    },
     // Owner for token and mint represents the authority. Not to be confused
     // with the owner of the AccountInfo.
     Token {

+ 29 - 5
lang/syn/src/parser/accounts/mod.rs

@@ -91,7 +91,7 @@ fn constraints_cross_checks(fields: &[AccountField]) -> ParseResult<()> {
         let kind = &init_fields[0].constraints.init.as_ref().unwrap().kind;
         // init token/a_token/mint needs token program.
         match kind {
-            InitKind::Program { .. } => (),
+            InitKind::Program { .. } | InitKind::Interface { .. } => (),
             InitKind::Token { .. } | InitKind::AssociatedToken { .. } | InitKind::Mint { .. } => {
                 if !fields
                     .iter()
@@ -289,6 +289,8 @@ fn is_field_primitive(f: &syn::Field) -> ParseResult<bool> {
             | "AccountLoader"
             | "Account"
             | "Program"
+            | "Interface"
+            | "InterfaceAccount"
             | "Signer"
             | "SystemAccount"
             | "ProgramData"
@@ -305,6 +307,8 @@ fn parse_ty(f: &syn::Field) -> ParseResult<(Ty, bool)> {
         "AccountLoader" => Ty::AccountLoader(parse_program_account_loader(&path)?),
         "Account" => Ty::Account(parse_account_ty(&path)?),
         "Program" => Ty::Program(parse_program_ty(&path)?),
+        "Interface" => Ty::Interface(parse_interface_ty(&path)?),
+        "InterfaceAccount" => Ty::InterfaceAccount(parse_interface_account_ty(&path)?),
         "Signer" => Ty::Signer,
         "SystemAccount" => Ty::SystemAccount,
         "ProgramData" => Ty::ProgramData,
@@ -358,6 +362,12 @@ fn ident_string(f: &syn::Field) -> ParseResult<(String, bool, Path)> {
     {
         return Ok(("Account".to_string(), optional, path));
     }
+    if parser::tts_to_string(&path)
+        .replace(' ', "")
+        .starts_with("Box<InterfaceAccount<")
+    {
+        return Ok(("InterfaceAccount".to_string(), optional, path));
+    }
     // TODO: allow segmented paths.
     if path.segments.len() != 1 {
         return Err(ParseError::new(
@@ -388,17 +398,31 @@ fn parse_account_ty(path: &syn::Path) -> ParseResult<AccountTy> {
     })
 }
 
+fn parse_interface_account_ty(path: &syn::Path) -> ParseResult<InterfaceAccountTy> {
+    let account_type_path = parse_account(path)?;
+    let boxed = parser::tts_to_string(path)
+        .replace(' ', "")
+        .starts_with("Box<InterfaceAccount<");
+    Ok(InterfaceAccountTy {
+        account_type_path,
+        boxed,
+    })
+}
+
 fn parse_program_ty(path: &syn::Path) -> ParseResult<ProgramTy> {
     let account_type_path = parse_account(path)?;
     Ok(ProgramTy { account_type_path })
 }
 
+fn parse_interface_ty(path: &syn::Path) -> ParseResult<InterfaceTy> {
+    let account_type_path = parse_account(path)?;
+    Ok(InterfaceTy { account_type_path })
+}
+
 // TODO: this whole method is a hack. Do something more idiomatic.
 fn parse_account(mut path: &syn::Path) -> ParseResult<syn::TypePath> {
-    if parser::tts_to_string(path)
-        .replace(' ', "")
-        .starts_with("Box<Account<")
-    {
+    let path_str = parser::tts_to_string(path).replace(' ', "");
+    if path_str.starts_with("Box<Account<") || path_str.starts_with("Box<InterfaceAccount<") {
         let segments = &path.segments[0];
         match &segments.arguments {
             syn::PathArguments::AngleBracketed(args) => {

+ 3 - 1
spl/Cargo.toml

@@ -8,9 +8,10 @@ license = "Apache-2.0"
 description = "CPI clients for SPL programs"
 
 [features]
-default = ["mint", "token", "associated_token"]
+default = ["mint", "token", "token_2022", "associated_token"]
 mint = []
 token = ["spl-token"]
+token_2022 = ["spl-token-2022"]
 associated_token = ["spl-associated-token-account"]
 governance = []
 shmem = []
@@ -25,5 +26,6 @@ borsh = { version = "^0.9", optional = true }
 serum_dex = { git = "https://github.com/openbook-dex/program/", rev = "1be91f2", version = "0.4.0", features = ["no-entrypoint"], optional = true }
 solana-program = "1.13.5"
 spl-token = { version = "3.5.0", features = ["no-entrypoint"], optional = true }
+spl-token-2022 = { version = "0.5.0", features = ["no-entrypoint"], optional = true }
 spl-associated-token-account = { version = "1.1.1", features = ["no-entrypoint"], optional = true }
 mpl-token-metadata = { version = "^1.4.3", optional = true, features = ["no-entrypoint"] }

+ 5 - 4
spl/src/associated_token.rs

@@ -2,16 +2,17 @@ use anchor_lang::solana_program::account_info::AccountInfo;
 use anchor_lang::solana_program::pubkey::Pubkey;
 use anchor_lang::Result;
 use anchor_lang::{context::CpiContext, Accounts};
-use spl_token;
 
-pub use spl_associated_token_account::{get_associated_token_address, ID};
+pub use spl_associated_token_account::{
+    get_associated_token_address, get_associated_token_address_with_program_id, ID,
+};
 
 pub fn create<'info>(ctx: CpiContext<'_, '_, '_, 'info, Create<'info>>) -> Result<()> {
     let ix = spl_associated_token_account::instruction::create_associated_token_account(
         ctx.accounts.payer.key,
         ctx.accounts.authority.key,
         ctx.accounts.mint.key,
-        &spl_token::ID,
+        ctx.accounts.token_program.key,
     );
     solana_program::program::invoke_signed(
         &ix,
@@ -35,7 +36,7 @@ pub fn create_idempotent<'info>(
         ctx.accounts.payer.key,
         ctx.accounts.authority.key,
         ctx.accounts.mint.key,
-        &spl_token::ID,
+        ctx.accounts.token_program.key,
     );
     solana_program::program::invoke_signed(
         &ix,

+ 6 - 0
spl/src/lib.rs

@@ -7,6 +7,12 @@ pub mod mint;
 #[cfg(feature = "token")]
 pub mod token;
 
+#[cfg(feature = "token_2022")]
+pub mod token_2022;
+
+#[cfg(feature = "token_2022")]
+pub mod token_interface;
+
 #[cfg(feature = "dex")]
 pub mod dex;
 

+ 547 - 0
spl/src/token_2022.rs

@@ -0,0 +1,547 @@
+use anchor_lang::solana_program::account_info::AccountInfo;
+
+use anchor_lang::solana_program::pubkey::Pubkey;
+use anchor_lang::{context::CpiContext, Accounts};
+use anchor_lang::{solana_program, Result};
+
+pub use spl_token_2022;
+pub use spl_token_2022::ID;
+
+#[deprecated(
+    since = "0.27.0",
+    note = "please use `transfer_checked` or `transfer_checked_with_fee` instead"
+)]
+pub fn transfer<'info>(
+    ctx: CpiContext<'_, '_, '_, 'info, Transfer<'info>>,
+    amount: u64,
+) -> Result<()> {
+    #[allow(deprecated)]
+    let ix = spl_token_2022::instruction::transfer(
+        ctx.program.key,
+        ctx.accounts.from.key,
+        ctx.accounts.to.key,
+        ctx.accounts.authority.key,
+        &[],
+        amount,
+    )?;
+    solana_program::program::invoke_signed(
+        &ix,
+        &[
+            ctx.accounts.from.clone(),
+            ctx.accounts.to.clone(),
+            ctx.accounts.authority.clone(),
+        ],
+        ctx.signer_seeds,
+    )
+    .map_err(Into::into)
+}
+
+pub fn transfer_checked<'info>(
+    ctx: CpiContext<'_, '_, '_, 'info, TransferChecked<'info>>,
+    amount: u64,
+    decimals: u8,
+) -> Result<()> {
+    let ix = spl_token_2022::instruction::transfer_checked(
+        ctx.program.key,
+        ctx.accounts.from.key,
+        ctx.accounts.mint.key,
+        ctx.accounts.to.key,
+        ctx.accounts.authority.key,
+        &[],
+        amount,
+        decimals,
+    )?;
+    solana_program::program::invoke_signed(
+        &ix,
+        &[
+            ctx.accounts.from.clone(),
+            ctx.accounts.mint.clone(),
+            ctx.accounts.to.clone(),
+            ctx.accounts.authority.clone(),
+        ],
+        ctx.signer_seeds,
+    )
+    .map_err(Into::into)
+}
+
+pub fn mint_to<'info>(
+    ctx: CpiContext<'_, '_, '_, 'info, MintTo<'info>>,
+    amount: u64,
+) -> Result<()> {
+    let ix = spl_token_2022::instruction::mint_to(
+        ctx.program.key,
+        ctx.accounts.mint.key,
+        ctx.accounts.to.key,
+        ctx.accounts.authority.key,
+        &[],
+        amount,
+    )?;
+    solana_program::program::invoke_signed(
+        &ix,
+        &[
+            ctx.accounts.to.clone(),
+            ctx.accounts.mint.clone(),
+            ctx.accounts.authority.clone(),
+        ],
+        ctx.signer_seeds,
+    )
+    .map_err(Into::into)
+}
+
+pub fn burn<'info>(ctx: CpiContext<'_, '_, '_, 'info, Burn<'info>>, amount: u64) -> Result<()> {
+    let ix = spl_token_2022::instruction::burn(
+        ctx.program.key,
+        ctx.accounts.from.key,
+        ctx.accounts.mint.key,
+        ctx.accounts.authority.key,
+        &[],
+        amount,
+    )?;
+    solana_program::program::invoke_signed(
+        &ix,
+        &[
+            ctx.accounts.from.clone(),
+            ctx.accounts.mint.clone(),
+            ctx.accounts.authority.clone(),
+        ],
+        ctx.signer_seeds,
+    )
+    .map_err(Into::into)
+}
+
+pub fn approve<'info>(
+    ctx: CpiContext<'_, '_, '_, 'info, Approve<'info>>,
+    amount: u64,
+) -> Result<()> {
+    let ix = spl_token_2022::instruction::approve(
+        ctx.program.key,
+        ctx.accounts.to.key,
+        ctx.accounts.delegate.key,
+        ctx.accounts.authority.key,
+        &[],
+        amount,
+    )?;
+    solana_program::program::invoke_signed(
+        &ix,
+        &[
+            ctx.accounts.to.clone(),
+            ctx.accounts.delegate.clone(),
+            ctx.accounts.authority.clone(),
+        ],
+        ctx.signer_seeds,
+    )
+    .map_err(Into::into)
+}
+
+pub fn revoke<'info>(ctx: CpiContext<'_, '_, '_, 'info, Revoke<'info>>) -> Result<()> {
+    let ix = spl_token_2022::instruction::revoke(
+        ctx.program.key,
+        ctx.accounts.source.key,
+        ctx.accounts.authority.key,
+        &[],
+    )?;
+    solana_program::program::invoke_signed(
+        &ix,
+        &[ctx.accounts.source.clone(), ctx.accounts.authority.clone()],
+        ctx.signer_seeds,
+    )
+    .map_err(Into::into)
+}
+
+pub fn initialize_account<'info>(
+    ctx: CpiContext<'_, '_, '_, 'info, InitializeAccount<'info>>,
+) -> Result<()> {
+    let ix = spl_token_2022::instruction::initialize_account(
+        ctx.program.key,
+        ctx.accounts.account.key,
+        ctx.accounts.mint.key,
+        ctx.accounts.authority.key,
+    )?;
+    solana_program::program::invoke(
+        &ix,
+        &[
+            ctx.accounts.account.clone(),
+            ctx.accounts.mint.clone(),
+            ctx.accounts.authority.clone(),
+            ctx.accounts.rent.clone(),
+        ],
+    )
+    .map_err(Into::into)
+}
+
+pub fn initialize_account3<'info>(
+    ctx: CpiContext<'_, '_, '_, 'info, InitializeAccount3<'info>>,
+) -> Result<()> {
+    let ix = spl_token_2022::instruction::initialize_account3(
+        ctx.program.key,
+        ctx.accounts.account.key,
+        ctx.accounts.mint.key,
+        ctx.accounts.authority.key,
+    )?;
+    solana_program::program::invoke(
+        &ix,
+        &[ctx.accounts.account.clone(), ctx.accounts.mint.clone()],
+    )
+    .map_err(Into::into)
+}
+
+pub fn close_account<'info>(ctx: CpiContext<'_, '_, '_, 'info, CloseAccount<'info>>) -> Result<()> {
+    let ix = spl_token_2022::instruction::close_account(
+        ctx.program.key,
+        ctx.accounts.account.key,
+        ctx.accounts.destination.key,
+        ctx.accounts.authority.key,
+        &[], // TODO: support multisig
+    )?;
+    solana_program::program::invoke_signed(
+        &ix,
+        &[
+            ctx.accounts.account.clone(),
+            ctx.accounts.destination.clone(),
+            ctx.accounts.authority.clone(),
+        ],
+        ctx.signer_seeds,
+    )
+    .map_err(Into::into)
+}
+
+pub fn freeze_account<'info>(
+    ctx: CpiContext<'_, '_, '_, 'info, FreezeAccount<'info>>,
+) -> Result<()> {
+    let ix = spl_token_2022::instruction::freeze_account(
+        ctx.program.key,
+        ctx.accounts.account.key,
+        ctx.accounts.mint.key,
+        ctx.accounts.authority.key,
+        &[], // TODO: Support multisig signers.
+    )?;
+    solana_program::program::invoke_signed(
+        &ix,
+        &[
+            ctx.accounts.account.clone(),
+            ctx.accounts.mint.clone(),
+            ctx.accounts.authority.clone(),
+        ],
+        ctx.signer_seeds,
+    )
+    .map_err(Into::into)
+}
+
+pub fn thaw_account<'info>(ctx: CpiContext<'_, '_, '_, 'info, ThawAccount<'info>>) -> Result<()> {
+    let ix = spl_token_2022::instruction::thaw_account(
+        ctx.program.key,
+        ctx.accounts.account.key,
+        ctx.accounts.mint.key,
+        ctx.accounts.authority.key,
+        &[], // TODO: Support multisig signers.
+    )?;
+    solana_program::program::invoke_signed(
+        &ix,
+        &[
+            ctx.accounts.account.clone(),
+            ctx.accounts.mint.clone(),
+            ctx.accounts.authority.clone(),
+        ],
+        ctx.signer_seeds,
+    )
+    .map_err(Into::into)
+}
+
+pub fn initialize_mint<'info>(
+    ctx: CpiContext<'_, '_, '_, 'info, InitializeMint<'info>>,
+    decimals: u8,
+    authority: &Pubkey,
+    freeze_authority: Option<&Pubkey>,
+) -> Result<()> {
+    let ix = spl_token_2022::instruction::initialize_mint(
+        ctx.program.key,
+        ctx.accounts.mint.key,
+        authority,
+        freeze_authority,
+        decimals,
+    )?;
+    solana_program::program::invoke(&ix, &[ctx.accounts.mint.clone(), ctx.accounts.rent.clone()])
+        .map_err(Into::into)
+}
+
+pub fn initialize_mint2<'info>(
+    ctx: CpiContext<'_, '_, '_, 'info, InitializeMint2<'info>>,
+    decimals: u8,
+    authority: &Pubkey,
+    freeze_authority: Option<&Pubkey>,
+) -> Result<()> {
+    let ix = spl_token_2022::instruction::initialize_mint2(
+        ctx.program.key,
+        ctx.accounts.mint.key,
+        authority,
+        freeze_authority,
+        decimals,
+    )?;
+    solana_program::program::invoke(&ix, &[ctx.accounts.mint.clone()]).map_err(Into::into)
+}
+
+pub fn set_authority<'info>(
+    ctx: CpiContext<'_, '_, '_, 'info, SetAuthority<'info>>,
+    authority_type: spl_token_2022::instruction::AuthorityType,
+    new_authority: Option<Pubkey>,
+) -> Result<()> {
+    let mut spl_new_authority: Option<&Pubkey> = None;
+    if new_authority.is_some() {
+        spl_new_authority = new_authority.as_ref()
+    }
+
+    let ix = spl_token_2022::instruction::set_authority(
+        ctx.program.key,
+        ctx.accounts.account_or_mint.key,
+        spl_new_authority,
+        authority_type,
+        ctx.accounts.current_authority.key,
+        &[], // TODO: Support multisig signers.
+    )?;
+    solana_program::program::invoke_signed(
+        &ix,
+        &[
+            ctx.accounts.account_or_mint.clone(),
+            ctx.accounts.current_authority.clone(),
+        ],
+        ctx.signer_seeds,
+    )
+    .map_err(Into::into)
+}
+
+pub fn sync_native<'info>(ctx: CpiContext<'_, '_, '_, 'info, SyncNative<'info>>) -> Result<()> {
+    let ix = spl_token_2022::instruction::sync_native(ctx.program.key, ctx.accounts.account.key)?;
+    solana_program::program::invoke(&ix, &[ctx.accounts.account.clone()]).map_err(Into::into)
+}
+
+pub fn get_account_data_size<'info>(
+    ctx: CpiContext<'_, '_, '_, 'info, GetAccountDataSize<'info>>,
+    extension_types: &[spl_token_2022::extension::ExtensionType],
+) -> Result<u64> {
+    let ix = spl_token_2022::instruction::get_account_data_size(
+        ctx.program.key,
+        ctx.accounts.mint.key,
+        extension_types,
+    )?;
+    solana_program::program::invoke(&ix, &[ctx.accounts.mint.clone()])?;
+    solana_program::program::get_return_data()
+        .ok_or(solana_program::program_error::ProgramError::InvalidInstructionData)
+        .and_then(|(key, data)| {
+            if key != *ctx.program.key {
+                Err(solana_program::program_error::ProgramError::IncorrectProgramId)
+            } else {
+                data.try_into().map(u64::from_le_bytes).map_err(|_| {
+                    solana_program::program_error::ProgramError::InvalidInstructionData
+                })
+            }
+        })
+        .map_err(Into::into)
+}
+
+pub fn initialize_mint_close_authority<'info>(
+    ctx: CpiContext<'_, '_, '_, 'info, InitializeMintCloseAuthority<'info>>,
+    close_authority: Option<&Pubkey>,
+) -> Result<()> {
+    let ix = spl_token_2022::instruction::initialize_mint_close_authority(
+        ctx.program.key,
+        ctx.accounts.mint.key,
+        close_authority,
+    )?;
+    solana_program::program::invoke(&ix, &[ctx.accounts.mint.clone()]).map_err(Into::into)
+}
+
+pub fn initialize_immutable_owner<'info>(
+    ctx: CpiContext<'_, '_, '_, 'info, InitializeImmutableOwner<'info>>,
+) -> Result<()> {
+    let ix = spl_token_2022::instruction::initialize_immutable_owner(
+        ctx.program.key,
+        ctx.accounts.account.key,
+    )?;
+    solana_program::program::invoke(&ix, &[ctx.accounts.account.clone()]).map_err(Into::into)
+}
+
+pub fn amount_to_ui_amount<'info>(
+    ctx: CpiContext<'_, '_, '_, 'info, AmountToUiAmount<'info>>,
+    amount: u64,
+) -> Result<String> {
+    let ix = spl_token_2022::instruction::amount_to_ui_amount(
+        ctx.program.key,
+        ctx.accounts.account.key,
+        amount,
+    )?;
+    solana_program::program::invoke(&ix, &[ctx.accounts.account.clone()])?;
+    solana_program::program::get_return_data()
+        .ok_or(solana_program::program_error::ProgramError::InvalidInstructionData)
+        .and_then(|(key, data)| {
+            if key != *ctx.program.key {
+                Err(solana_program::program_error::ProgramError::IncorrectProgramId)
+            } else {
+                String::from_utf8(data).map_err(|_| {
+                    solana_program::program_error::ProgramError::InvalidInstructionData
+                })
+            }
+        })
+        .map_err(Into::into)
+}
+
+pub fn ui_amount_to_amount<'info>(
+    ctx: CpiContext<'_, '_, '_, 'info, UiAmountToAmount<'info>>,
+    ui_amount: &str,
+) -> Result<u64> {
+    let ix = spl_token_2022::instruction::ui_amount_to_amount(
+        ctx.program.key,
+        ctx.accounts.account.key,
+        ui_amount,
+    )?;
+    solana_program::program::invoke(&ix, &[ctx.accounts.account.clone()])?;
+    solana_program::program::get_return_data()
+        .ok_or(solana_program::program_error::ProgramError::InvalidInstructionData)
+        .and_then(|(key, data)| {
+            if key != *ctx.program.key {
+                Err(solana_program::program_error::ProgramError::IncorrectProgramId)
+            } else {
+                data.try_into().map(u64::from_le_bytes).map_err(|_| {
+                    solana_program::program_error::ProgramError::InvalidInstructionData
+                })
+            }
+        })
+        .map_err(Into::into)
+}
+
+#[derive(Accounts)]
+pub struct Transfer<'info> {
+    pub from: AccountInfo<'info>,
+    pub to: AccountInfo<'info>,
+    pub authority: AccountInfo<'info>,
+}
+
+#[derive(Accounts)]
+pub struct TransferChecked<'info> {
+    pub from: AccountInfo<'info>,
+    pub mint: AccountInfo<'info>,
+    pub to: AccountInfo<'info>,
+    pub authority: AccountInfo<'info>,
+}
+
+#[derive(Accounts)]
+pub struct MintTo<'info> {
+    pub mint: AccountInfo<'info>,
+    pub to: AccountInfo<'info>,
+    pub authority: AccountInfo<'info>,
+}
+
+#[derive(Accounts)]
+pub struct Burn<'info> {
+    pub mint: AccountInfo<'info>,
+    pub from: AccountInfo<'info>,
+    pub authority: AccountInfo<'info>,
+}
+
+#[derive(Accounts)]
+pub struct Approve<'info> {
+    pub to: AccountInfo<'info>,
+    pub delegate: AccountInfo<'info>,
+    pub authority: AccountInfo<'info>,
+}
+
+#[derive(Accounts)]
+pub struct Revoke<'info> {
+    pub source: AccountInfo<'info>,
+    pub authority: AccountInfo<'info>,
+}
+
+#[derive(Accounts)]
+pub struct InitializeAccount<'info> {
+    pub account: AccountInfo<'info>,
+    pub mint: AccountInfo<'info>,
+    pub authority: AccountInfo<'info>,
+    pub rent: AccountInfo<'info>,
+}
+
+#[derive(Accounts)]
+pub struct InitializeAccount3<'info> {
+    pub account: AccountInfo<'info>,
+    pub mint: AccountInfo<'info>,
+    pub authority: AccountInfo<'info>,
+}
+
+#[derive(Accounts)]
+pub struct CloseAccount<'info> {
+    pub account: AccountInfo<'info>,
+    pub destination: AccountInfo<'info>,
+    pub authority: AccountInfo<'info>,
+}
+
+#[derive(Accounts)]
+pub struct FreezeAccount<'info> {
+    pub account: AccountInfo<'info>,
+    pub mint: AccountInfo<'info>,
+    pub authority: AccountInfo<'info>,
+}
+
+#[derive(Accounts)]
+pub struct ThawAccount<'info> {
+    pub account: AccountInfo<'info>,
+    pub mint: AccountInfo<'info>,
+    pub authority: AccountInfo<'info>,
+}
+
+#[derive(Accounts)]
+pub struct InitializeMint<'info> {
+    pub mint: AccountInfo<'info>,
+    pub rent: AccountInfo<'info>,
+}
+
+#[derive(Accounts)]
+pub struct InitializeMint2<'info> {
+    pub mint: AccountInfo<'info>,
+}
+
+#[derive(Accounts)]
+pub struct SetAuthority<'info> {
+    pub current_authority: AccountInfo<'info>,
+    pub account_or_mint: AccountInfo<'info>,
+}
+
+#[derive(Accounts)]
+pub struct SyncNative<'info> {
+    pub account: AccountInfo<'info>,
+}
+
+#[derive(Accounts)]
+pub struct GetAccountDataSize<'info> {
+    pub mint: AccountInfo<'info>,
+}
+
+#[derive(Accounts)]
+pub struct InitializeMintCloseAuthority<'info> {
+    pub mint: AccountInfo<'info>,
+}
+
+#[derive(Accounts)]
+pub struct InitializeImmutableOwner<'info> {
+    pub account: AccountInfo<'info>,
+}
+
+#[derive(Accounts)]
+pub struct AmountToUiAmount<'info> {
+    pub account: AccountInfo<'info>,
+}
+
+#[derive(Accounts)]
+pub struct UiAmountToAmount<'info> {
+    pub account: AccountInfo<'info>,
+}
+
+#[derive(Clone)]
+pub struct Token2022;
+
+impl anchor_lang::Id for Token2022 {
+    fn id() -> Pubkey {
+        ID
+    }
+}
+
+// Field parsers to save compute. All account validation is assumed to be done
+// outside of these methods.
+pub use crate::token::accessor;

+ 71 - 0
spl/src/token_interface.rs

@@ -0,0 +1,71 @@
+use anchor_lang::solana_program::pubkey::Pubkey;
+use std::ops::Deref;
+
+static IDS: [Pubkey; 2] = [spl_token::ID, spl_token_2022::ID];
+
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct TokenAccount(spl_token_2022::state::Account);
+
+impl anchor_lang::AccountDeserialize for TokenAccount {
+    fn try_deserialize_unchecked(buf: &mut &[u8]) -> anchor_lang::Result<Self> {
+        spl_token_2022::extension::StateWithExtensions::<spl_token_2022::state::Account>::unpack(
+            buf,
+        )
+        .map(|t| TokenAccount(t.base))
+        .map_err(Into::into)
+    }
+}
+
+impl anchor_lang::AccountSerialize for TokenAccount {}
+
+impl anchor_lang::Owners for TokenAccount {
+    fn owners() -> &'static [Pubkey] {
+        &IDS
+    }
+}
+
+impl Deref for TokenAccount {
+    type Target = spl_token_2022::state::Account;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct Mint(spl_token_2022::state::Mint);
+
+impl anchor_lang::AccountDeserialize for Mint {
+    fn try_deserialize_unchecked(buf: &mut &[u8]) -> anchor_lang::Result<Self> {
+        spl_token_2022::extension::StateWithExtensions::<spl_token_2022::state::Mint>::unpack(buf)
+            .map(|t| Mint(t.base))
+            .map_err(Into::into)
+    }
+}
+
+impl anchor_lang::AccountSerialize for Mint {}
+
+impl anchor_lang::Owners for Mint {
+    fn owners() -> &'static [Pubkey] {
+        &IDS
+    }
+}
+
+impl Deref for Mint {
+    type Target = spl_token_2022::state::Mint;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+#[derive(Clone)]
+pub struct TokenInterface;
+
+impl anchor_lang::Ids for TokenInterface {
+    fn ids() -> &'static [Pubkey] {
+        &IDS
+    }
+}
+
+pub use crate::token_2022::*;

+ 6 - 0
tests/escrow/Anchor.toml

@@ -9,3 +9,9 @@ escrow = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"
 test = "yarn run ts-mocha -t 1000000 tests/*.ts"
 
 [features]
+
+[test.validator]
+url = "https://api.mainnet-beta.solana.com"
+
+[[test.validator.clone]]
+address = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"

+ 1 - 1
tests/escrow/programs/escrow/Cargo.toml

@@ -18,4 +18,4 @@ default = []
 [dependencies]
 anchor-lang = { path = "../../../../lang" }
 anchor-spl = { path = "../../../../spl" }
-spl-token = { version = "3.1.1", features = ["no-entrypoint"] }
+spl-token-2022 = { version = "0.5.0", features = ["no-entrypoint"] }

+ 48 - 28
tests/escrow/programs/escrow/src/lib.rs

@@ -16,8 +16,10 @@
 //! - Initializer will get back ownership of their token X account
 
 use anchor_lang::prelude::*;
-use anchor_spl::token::{self, SetAuthority, Token, TokenAccount, Transfer};
-use spl_token::instruction::AuthorityType;
+use anchor_spl::token_interface::{
+    self, Mint, SetAuthority, TokenAccount, TokenInterface, TransferChecked,
+};
+use spl_token_2022::instruction::AuthorityType;
 
 declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
 
@@ -51,7 +53,11 @@ pub mod escrow {
         ctx.accounts.escrow_account.taker_amount = taker_amount;
 
         let (pda, _bump_seed) = Pubkey::find_program_address(&[ESCROW_PDA_SEED], ctx.program_id);
-        token::set_authority(ctx.accounts.into(), AuthorityType::AccountOwner, Some(pda))?;
+        token_interface::set_authority(
+            ctx.accounts.into(),
+            AuthorityType::AccountOwner,
+            Some(pda),
+        )?;
         Ok(())
     }
 
@@ -59,7 +65,7 @@ pub mod escrow {
         let (_pda, bump_seed) = Pubkey::find_program_address(&[ESCROW_PDA_SEED], ctx.program_id);
         let seeds = &[&ESCROW_PDA_SEED[..], &[bump_seed]];
 
-        token::set_authority(
+        token_interface::set_authority(
             ctx.accounts
                 .into_set_authority_context()
                 .with_signer(&[&seeds[..]]),
@@ -75,19 +81,21 @@ pub mod escrow {
         let (_pda, bump_seed) = Pubkey::find_program_address(&[ESCROW_PDA_SEED], ctx.program_id);
         let seeds = &[&ESCROW_PDA_SEED[..], &[bump_seed]];
 
-        token::transfer(
+        token_interface::transfer_checked(
             ctx.accounts
                 .into_transfer_to_taker_context()
                 .with_signer(&[&seeds[..]]),
             ctx.accounts.escrow_account.initializer_amount,
+            ctx.accounts.receive_mint.decimals,
         )?;
 
-        token::transfer(
+        token_interface::transfer_checked(
             ctx.accounts.into_transfer_to_initializer_context(),
             ctx.accounts.escrow_account.taker_amount,
+            ctx.accounts.deposit_mint.decimals,
         )?;
 
-        token::set_authority(
+        token_interface::set_authority(
             ctx.accounts
                 .into_set_authority_context()
                 .with_signer(&[&seeds[..]]),
@@ -108,27 +116,29 @@ pub struct InitializeEscrow<'info> {
         mut,
         constraint = initializer_deposit_token_account.amount >= initializer_amount
     )]
-    pub initializer_deposit_token_account: Account<'info, TokenAccount>,
-    pub initializer_receive_token_account: Account<'info, TokenAccount>,
+    pub initializer_deposit_token_account: InterfaceAccount<'info, TokenAccount>,
+    pub initializer_receive_token_account: InterfaceAccount<'info, TokenAccount>,
     #[account(init, payer = initializer, space = 8 + EscrowAccount::LEN)]
     pub escrow_account: Account<'info, EscrowAccount>,
     pub system_program: Program<'info, System>,
-    pub token_program: Program<'info, Token>,
+    pub token_program: Interface<'info, TokenInterface>,
 }
 
 #[derive(Accounts)]
 pub struct Exchange<'info> {
     #[account(signer)]
+    /// CHECK:
     pub taker: AccountInfo<'info>,
+    #[account(mut, token::mint = deposit_mint)]
+    pub taker_deposit_token_account: Box<InterfaceAccount<'info, TokenAccount>>,
+    #[account(mut, token::mint = receive_mint)]
+    pub taker_receive_token_account: Box<InterfaceAccount<'info, TokenAccount>>,
+    #[account(mut, token::mint = receive_mint)]
+    pub pda_deposit_token_account: Box<InterfaceAccount<'info, TokenAccount>>,
+    #[account(mut, token::mint = deposit_mint)]
+    pub initializer_receive_token_account: Box<InterfaceAccount<'info, TokenAccount>>,
     #[account(mut)]
-    pub taker_deposit_token_account: Account<'info, TokenAccount>,
-    #[account(mut)]
-    pub taker_receive_token_account: Account<'info, TokenAccount>,
-    #[account(mut)]
-    pub pda_deposit_token_account: Account<'info, TokenAccount>,
-    #[account(mut)]
-    pub initializer_receive_token_account: Account<'info, TokenAccount>,
-    #[account(mut)]
+    /// CHECK:
     pub initializer_main_account: AccountInfo<'info>,
     #[account(
         mut,
@@ -139,15 +149,21 @@ pub struct Exchange<'info> {
         close = initializer_main_account
     )]
     pub escrow_account: Account<'info, EscrowAccount>,
+    /// CHECK:
     pub pda_account: AccountInfo<'info>,
-    pub token_program: Program<'info, Token>,
+    pub deposit_mint: Box<InterfaceAccount<'info, Mint>>,
+    pub receive_mint: Box<InterfaceAccount<'info, Mint>>,
+    pub deposit_token_program: Interface<'info, TokenInterface>,
+    pub receive_token_program: Interface<'info, TokenInterface>,
 }
 
 #[derive(Accounts)]
 pub struct CancelEscrow<'info> {
+    /// CHECK:
     pub initializer: AccountInfo<'info>,
     #[account(mut)]
-    pub pda_deposit_token_account: Account<'info, TokenAccount>,
+    pub pda_deposit_token_account: InterfaceAccount<'info, TokenAccount>,
+    /// CHECK:
     pub pda_account: AccountInfo<'info>,
     #[account(
         mut,
@@ -156,7 +172,7 @@ pub struct CancelEscrow<'info> {
         close = initializer
     )]
     pub escrow_account: Account<'info, EscrowAccount>,
-    pub token_program: Program<'info, Token>,
+    pub token_program: Interface<'info, TokenInterface>,
 }
 
 #[account]
@@ -205,19 +221,22 @@ impl<'info> Exchange<'info> {
             account_or_mint: self.pda_deposit_token_account.to_account_info().clone(),
             current_authority: self.pda_account.clone(),
         };
-        let cpi_program = self.token_program.to_account_info();
+        let cpi_program = self.receive_token_program.to_account_info();
         CpiContext::new(cpi_program, cpi_accounts)
     }
 }
 
 impl<'info> Exchange<'info> {
-    fn into_transfer_to_taker_context(&self) -> CpiContext<'_, '_, '_, 'info, Transfer<'info>> {
-        let cpi_accounts = Transfer {
+    fn into_transfer_to_taker_context(
+        &self,
+    ) -> CpiContext<'_, '_, '_, 'info, TransferChecked<'info>> {
+        let cpi_accounts = TransferChecked {
             from: self.pda_deposit_token_account.to_account_info().clone(),
+            mint: self.receive_mint.to_account_info().clone(),
             to: self.taker_receive_token_account.to_account_info().clone(),
             authority: self.pda_account.clone(),
         };
-        let cpi_program = self.token_program.to_account_info();
+        let cpi_program = self.receive_token_program.to_account_info();
         CpiContext::new(cpi_program, cpi_accounts)
     }
 }
@@ -225,16 +244,17 @@ impl<'info> Exchange<'info> {
 impl<'info> Exchange<'info> {
     fn into_transfer_to_initializer_context(
         &self,
-    ) -> CpiContext<'_, '_, '_, 'info, Transfer<'info>> {
-        let cpi_accounts = Transfer {
+    ) -> CpiContext<'_, '_, '_, 'info, TransferChecked<'info>> {
+        let cpi_accounts = TransferChecked {
             from: self.taker_deposit_token_account.to_account_info().clone(),
+            mint: self.deposit_mint.to_account_info().clone(),
             to: self
                 .initializer_receive_token_account
                 .to_account_info()
                 .clone(),
             authority: self.taker.clone(),
         };
-        let cpi_program = self.token_program.to_account_info();
+        let cpi_program = self.deposit_token_program.to_account_info();
         CpiContext::new(cpi_program, cpi_accounts)
     }
 }

+ 251 - 214
tests/escrow/tests/escrow.ts

@@ -11,6 +11,14 @@ describe("escrow", () => {
   const provider = anchor.AnchorProvider.env();
   anchor.setProvider(provider);
 
+  const TOKEN_2022_PROGRAM_ID = new anchor.web3.PublicKey(
+    "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
+  );
+  const TEST_PROGRAM_IDS = [
+    [TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID],
+    [TOKEN_2022_PROGRAM_ID, TOKEN_2022_PROGRAM_ID],
+    [TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID],
+  ];
   const program = anchor.workspace.Escrow as Program<Escrow>;
 
   let mintA: Token = null;
@@ -24,223 +32,252 @@ describe("escrow", () => {
   const takerAmount = 1000;
   const initializerAmount = 500;
 
-  const escrowAccount = Keypair.generate();
   const payer = Keypair.generate();
   const mintAuthority = Keypair.generate();
 
-  it("Initialise escrow state", async () => {
-    // Airdropping tokens to a payer.
-    await provider.connection.confirmTransaction(
-      await provider.connection.requestAirdrop(payer.publicKey, 10000000000),
-      "confirmed"
-    );
-
-    mintA = await Token.createMint(
-      provider.connection,
-      payer,
-      mintAuthority.publicKey,
-      null,
-      0,
-      TOKEN_PROGRAM_ID
-    );
-
-    mintB = await Token.createMint(
-      provider.connection,
-      payer,
-      mintAuthority.publicKey,
-      null,
-      0,
-      TOKEN_PROGRAM_ID
-    );
-
-    initializerTokenAccountA = await mintA.createAccount(
-      provider.wallet.publicKey
-    );
-    takerTokenAccountA = await mintA.createAccount(provider.wallet.publicKey);
-
-    initializerTokenAccountB = await mintB.createAccount(
-      provider.wallet.publicKey
-    );
-    takerTokenAccountB = await mintB.createAccount(provider.wallet.publicKey);
-
-    await mintA.mintTo(
-      initializerTokenAccountA,
-      mintAuthority.publicKey,
-      [mintAuthority],
-      initializerAmount
-    );
-
-    await mintB.mintTo(
-      takerTokenAccountB,
-      mintAuthority.publicKey,
-      [mintAuthority],
-      takerAmount
-    );
-
-    let _initializerTokenAccountA = await mintA.getAccountInfo(
-      initializerTokenAccountA
-    );
-    let _takerTokenAccountB = await mintB.getAccountInfo(takerTokenAccountB);
-
-    assert.strictEqual(
-      _initializerTokenAccountA.amount.toNumber(),
-      initializerAmount
-    );
-    assert.strictEqual(_takerTokenAccountB.amount.toNumber(), takerAmount);
-  });
-
-  it("Initialize escrow", async () => {
-    await program.rpc.initializeEscrow(
-      new BN(initializerAmount),
-      new BN(takerAmount),
-      {
-        accounts: {
-          initializer: provider.wallet.publicKey,
-          initializerDepositTokenAccount: initializerTokenAccountA,
-          initializerReceiveTokenAccount: initializerTokenAccountB,
-          escrowAccount: escrowAccount.publicKey,
-          systemProgram: SystemProgram.programId,
-          tokenProgram: TOKEN_PROGRAM_ID,
-        },
-        signers: [escrowAccount],
-      }
-    );
-
-    // Get the PDA that is assigned authority to token account.
-    const [_pda, _nonce] = await PublicKey.findProgramAddress(
-      [Buffer.from(anchor.utils.bytes.utf8.encode("escrow"))],
-      program.programId
-    );
-
-    pda = _pda;
-
-    let _initializerTokenAccountA = await mintA.getAccountInfo(
-      initializerTokenAccountA
-    );
-
-    let _escrowAccount: EscrowAccount =
-      await program.account.escrowAccount.fetch(escrowAccount.publicKey);
-
-    // Check that the new owner is the PDA.
-    assert.isTrue(_initializerTokenAccountA.owner.equals(pda));
-
-    // Check that the values in the escrow account match what we expect.
-    assert.isTrue(
-      _escrowAccount.initializerKey.equals(provider.wallet.publicKey)
-    );
-    assert.strictEqual(
-      _escrowAccount.initializerAmount.toNumber(),
-      initializerAmount
-    );
-    assert.strictEqual(_escrowAccount.takerAmount.toNumber(), takerAmount);
-    assert.isTrue(
-      _escrowAccount.initializerDepositTokenAccount.equals(
-        initializerTokenAccountA
-      )
-    );
-    assert.isTrue(
-      _escrowAccount.initializerReceiveTokenAccount.equals(
-        initializerTokenAccountB
-      )
-    );
-  });
-
-  it("Exchange escrow", async () => {
-    await program.rpc.exchange({
-      accounts: {
-        taker: provider.wallet.publicKey,
-        takerDepositTokenAccount: takerTokenAccountB,
-        takerReceiveTokenAccount: takerTokenAccountA,
-        pdaDepositTokenAccount: initializerTokenAccountA,
-        initializerReceiveTokenAccount: initializerTokenAccountB,
-        initializerMainAccount: provider.wallet.publicKey,
-        escrowAccount: escrowAccount.publicKey,
-        pdaAccount: pda,
-        tokenProgram: TOKEN_PROGRAM_ID,
-      },
+  TEST_PROGRAM_IDS.forEach((tokenProgramIds) => {
+    const escrowAccount = Keypair.generate();
+    const [tokenProgramIdA, tokenProgramIdB] = tokenProgramIds;
+    let name;
+    if (tokenProgramIdA === tokenProgramIdB) {
+      name = tokenProgramIdA === TOKEN_PROGRAM_ID ? "token" : "token-2022";
+    } else {
+      name = "mixed";
+    }
+    describe(name, () => {
+      it("Initialise escrow state", async () => {
+        // Airdropping tokens to a payer.
+        await provider.connection.confirmTransaction(
+          await provider.connection.requestAirdrop(
+            payer.publicKey,
+            10000000000
+          ),
+          "confirmed"
+        );
+
+        mintA = await Token.createMint(
+          provider.connection,
+          payer,
+          mintAuthority.publicKey,
+          null,
+          0,
+          tokenProgramIdA
+        );
+
+        mintB = await Token.createMint(
+          provider.connection,
+          payer,
+          mintAuthority.publicKey,
+          null,
+          0,
+          tokenProgramIdB
+        );
+
+        initializerTokenAccountA = await mintA.createAccount(
+          provider.wallet.publicKey
+        );
+        takerTokenAccountA = await mintA.createAccount(
+          provider.wallet.publicKey
+        );
+
+        initializerTokenAccountB = await mintB.createAccount(
+          provider.wallet.publicKey
+        );
+        takerTokenAccountB = await mintB.createAccount(
+          provider.wallet.publicKey
+        );
+
+        await mintA.mintTo(
+          initializerTokenAccountA,
+          mintAuthority.publicKey,
+          [mintAuthority],
+          initializerAmount
+        );
+
+        await mintB.mintTo(
+          takerTokenAccountB,
+          mintAuthority.publicKey,
+          [mintAuthority],
+          takerAmount
+        );
+
+        let _initializerTokenAccountA = await mintA.getAccountInfo(
+          initializerTokenAccountA
+        );
+        let _takerTokenAccountB = await mintB.getAccountInfo(
+          takerTokenAccountB
+        );
+
+        assert.strictEqual(
+          _initializerTokenAccountA.amount.toNumber(),
+          initializerAmount
+        );
+        assert.strictEqual(_takerTokenAccountB.amount.toNumber(), takerAmount);
+      });
+
+      it("Initialize escrow", async () => {
+        await program.rpc.initializeEscrow(
+          new BN(initializerAmount),
+          new BN(takerAmount),
+          {
+            accounts: {
+              initializer: provider.wallet.publicKey,
+              initializerDepositTokenAccount: initializerTokenAccountA,
+              initializerReceiveTokenAccount: initializerTokenAccountB,
+              escrowAccount: escrowAccount.publicKey,
+              systemProgram: SystemProgram.programId,
+              tokenProgram: tokenProgramIdA,
+            },
+            signers: [escrowAccount],
+          }
+        );
+
+        // Get the PDA that is assigned authority to token account.
+        const [_pda, _nonce] = await PublicKey.findProgramAddress(
+          [Buffer.from(anchor.utils.bytes.utf8.encode("escrow"))],
+          program.programId
+        );
+
+        pda = _pda;
+
+        let _initializerTokenAccountA = await mintA.getAccountInfo(
+          initializerTokenAccountA
+        );
+
+        let _escrowAccount: EscrowAccount =
+          await program.account.escrowAccount.fetch(escrowAccount.publicKey);
+
+        // Check that the new owner is the PDA.
+        assert.isTrue(_initializerTokenAccountA.owner.equals(pda));
+
+        // Check that the values in the escrow account match what we expect.
+        assert.isTrue(
+          _escrowAccount.initializerKey.equals(provider.wallet.publicKey)
+        );
+        assert.strictEqual(
+          _escrowAccount.initializerAmount.toNumber(),
+          initializerAmount
+        );
+        assert.strictEqual(_escrowAccount.takerAmount.toNumber(), takerAmount);
+        assert.isTrue(
+          _escrowAccount.initializerDepositTokenAccount.equals(
+            initializerTokenAccountA
+          )
+        );
+        assert.isTrue(
+          _escrowAccount.initializerReceiveTokenAccount.equals(
+            initializerTokenAccountB
+          )
+        );
+      });
+
+      it("Exchange escrow", async () => {
+        await program.rpc.exchange({
+          accounts: {
+            taker: provider.wallet.publicKey,
+            takerDepositTokenAccount: takerTokenAccountB,
+            takerReceiveTokenAccount: takerTokenAccountA,
+            pdaDepositTokenAccount: initializerTokenAccountA,
+            initializerReceiveTokenAccount: initializerTokenAccountB,
+            initializerMainAccount: provider.wallet.publicKey,
+            escrowAccount: escrowAccount.publicKey,
+            pdaAccount: pda,
+            depositMint: mintB.publicKey,
+            receiveMint: mintA.publicKey,
+            depositTokenProgram: tokenProgramIdB,
+            receiveTokenProgram: tokenProgramIdA,
+          },
+        });
+
+        let _takerTokenAccountA = await mintA.getAccountInfo(
+          takerTokenAccountA
+        );
+        let _takerTokenAccountB = await mintB.getAccountInfo(
+          takerTokenAccountB
+        );
+        let _initializerTokenAccountA = await mintA.getAccountInfo(
+          initializerTokenAccountA
+        );
+        let _initializerTokenAccountB = await mintB.getAccountInfo(
+          initializerTokenAccountB
+        );
+
+        // Check that the initializer gets back ownership of their token account.
+        assert.isTrue(
+          _takerTokenAccountA.owner.equals(provider.wallet.publicKey)
+        );
+
+        assert.strictEqual(
+          _takerTokenAccountA.amount.toNumber(),
+          initializerAmount
+        );
+        assert.strictEqual(_initializerTokenAccountA.amount.toNumber(), 0);
+        assert.strictEqual(
+          _initializerTokenAccountB.amount.toNumber(),
+          takerAmount
+        );
+        assert.strictEqual(_takerTokenAccountB.amount.toNumber(), 0);
+      });
+
+      let newEscrow = Keypair.generate();
+
+      it("Initialize escrow and cancel escrow", async () => {
+        // Put back tokens into initializer token A account.
+        await mintA.mintTo(
+          initializerTokenAccountA,
+          mintAuthority.publicKey,
+          [mintAuthority],
+          initializerAmount
+        );
+
+        await program.rpc.initializeEscrow(
+          new BN(initializerAmount),
+          new BN(takerAmount),
+          {
+            accounts: {
+              initializer: provider.wallet.publicKey,
+              initializerDepositTokenAccount: initializerTokenAccountA,
+              initializerReceiveTokenAccount: initializerTokenAccountB,
+              escrowAccount: newEscrow.publicKey,
+              systemProgram: SystemProgram.programId,
+              tokenProgram: tokenProgramIdA,
+            },
+            signers: [newEscrow],
+          }
+        );
+
+        let _initializerTokenAccountA = await mintA.getAccountInfo(
+          initializerTokenAccountA
+        );
+
+        // Check that the new owner is the PDA.
+        assert.isTrue(_initializerTokenAccountA.owner.equals(pda));
+
+        // Cancel the escrow.
+        await program.rpc.cancelEscrow({
+          accounts: {
+            initializer: provider.wallet.publicKey,
+            pdaDepositTokenAccount: initializerTokenAccountA,
+            pdaAccount: pda,
+            escrowAccount: newEscrow.publicKey,
+            tokenProgram: tokenProgramIdA,
+          },
+        });
+
+        // Check the final owner should be the provider public key.
+        _initializerTokenAccountA = await mintA.getAccountInfo(
+          initializerTokenAccountA
+        );
+        assert.isTrue(
+          _initializerTokenAccountA.owner.equals(provider.wallet.publicKey)
+        );
+
+        // Check all the funds are still there.
+        assert.strictEqual(
+          _initializerTokenAccountA.amount.toNumber(),
+          initializerAmount
+        );
+      });
     });
-
-    let _takerTokenAccountA = await mintA.getAccountInfo(takerTokenAccountA);
-    let _takerTokenAccountB = await mintB.getAccountInfo(takerTokenAccountB);
-    let _initializerTokenAccountA = await mintA.getAccountInfo(
-      initializerTokenAccountA
-    );
-    let _initializerTokenAccountB = await mintB.getAccountInfo(
-      initializerTokenAccountB
-    );
-
-    // Check that the initializer gets back ownership of their token account.
-    assert.isTrue(_takerTokenAccountA.owner.equals(provider.wallet.publicKey));
-
-    assert.strictEqual(
-      _takerTokenAccountA.amount.toNumber(),
-      initializerAmount
-    );
-    assert.strictEqual(_initializerTokenAccountA.amount.toNumber(), 0);
-    assert.strictEqual(
-      _initializerTokenAccountB.amount.toNumber(),
-      takerAmount
-    );
-    assert.strictEqual(_takerTokenAccountB.amount.toNumber(), 0);
-  });
-
-  let newEscrow = Keypair.generate();
-
-  it("Initialize escrow and cancel escrow", async () => {
-    // Put back tokens into initializer token A account.
-    await mintA.mintTo(
-      initializerTokenAccountA,
-      mintAuthority.publicKey,
-      [mintAuthority],
-      initializerAmount
-    );
-
-    await program.rpc.initializeEscrow(
-      new BN(initializerAmount),
-      new BN(takerAmount),
-      {
-        accounts: {
-          initializer: provider.wallet.publicKey,
-          initializerDepositTokenAccount: initializerTokenAccountA,
-          initializerReceiveTokenAccount: initializerTokenAccountB,
-          escrowAccount: newEscrow.publicKey,
-          systemProgram: SystemProgram.programId,
-          tokenProgram: TOKEN_PROGRAM_ID,
-        },
-        signers: [newEscrow],
-      }
-    );
-
-    let _initializerTokenAccountA = await mintA.getAccountInfo(
-      initializerTokenAccountA
-    );
-
-    // Check that the new owner is the PDA.
-    assert.isTrue(_initializerTokenAccountA.owner.equals(pda));
-
-    // Cancel the escrow.
-    await program.rpc.cancelEscrow({
-      accounts: {
-        initializer: provider.wallet.publicKey,
-        pdaDepositTokenAccount: initializerTokenAccountA,
-        pdaAccount: pda,
-        escrowAccount: newEscrow.publicKey,
-        tokenProgram: TOKEN_PROGRAM_ID,
-      },
-    });
-
-    // Check the final owner should be the provider public key.
-    _initializerTokenAccountA = await mintA.getAccountInfo(
-      initializerTokenAccountA
-    );
-    assert.isTrue(
-      _initializerTokenAccountA.owner.equals(provider.wallet.publicKey)
-    );
-
-    // Check all the funds are still there.
-    assert.strictEqual(
-      _initializerTokenAccountA.amount.toNumber(),
-      initializerAmount
-    );
   });
 });

+ 6 - 0
tests/spl/token-proxy/Anchor.toml

@@ -9,3 +9,9 @@ token_proxy = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"
 test = "yarn run mocha -t 1000000 tests/"
 
 [features]
+
+[test.validator]
+url = "https://api.mainnet-beta.solana.com"
+
+[[test.validator.clone]]
+address = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"

+ 1 - 1
tests/spl/token-proxy/programs/token-proxy/Cargo.toml

@@ -16,4 +16,4 @@ cpi = ["no-entrypoint"]
 [dependencies]
 anchor-lang = { path = "../../../../../lang" }
 anchor-spl = { path = "../../../../../spl" }
-spl-token = { version = "3.1.1", features = ["no-entrypoint"] }
+spl-token-2022 = { version = "0.5.0", features = ["no-entrypoint"] }

+ 148 - 31
tests/spl/token-proxy/programs/token-proxy/src/lib.rs

@@ -1,7 +1,10 @@
 //! This example demonstrates the use of the `anchor_spl::token` CPI client.
 
 use anchor_lang::prelude::*;
-use anchor_spl::token::{self, Burn, MintTo, SetAuthority, Transfer};
+use anchor_spl::associated_token::AssociatedToken;
+use anchor_spl::token_interface::{
+    self, Burn, Mint, MintTo, SetAuthority, TokenAccount, TokenInterface, Transfer, TransferChecked,
+};
 
 declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
 
@@ -10,15 +13,44 @@ mod token_proxy {
     use super::*;
 
     pub fn proxy_transfer(ctx: Context<ProxyTransfer>, amount: u64) -> Result<()> {
-        token::transfer(ctx.accounts.into(), amount)
+        #[allow(deprecated)]
+        token_interface::transfer(ctx.accounts.into(), amount)
+    }
+
+    pub fn proxy_optional_transfer(ctx: Context<ProxyOptionalTransfer>, amount: u64) -> Result<()> {
+        if let Some(token_program) = &ctx.accounts.token_program {
+            if let Some(mint) = &ctx.accounts.mint {
+                let cpi_accounts = TransferChecked {
+                    from: ctx.accounts.from.to_account_info().clone(),
+                    mint: mint.to_account_info().clone(),
+                    to: ctx.accounts.to.to_account_info().clone(),
+                    authority: ctx.accounts.authority.clone(),
+                };
+                let cpi_program = token_program.to_account_info();
+                let cpi_context = CpiContext::new(cpi_program, cpi_accounts);
+                token_interface::transfer_checked(cpi_context, amount, mint.decimals)
+            } else {
+                let cpi_accounts = Transfer {
+                    from: ctx.accounts.from.to_account_info().clone(),
+                    to: ctx.accounts.to.to_account_info().clone(),
+                    authority: ctx.accounts.authority.clone(),
+                };
+                let cpi_program = token_program.to_account_info();
+                let cpi_context = CpiContext::new(cpi_program, cpi_accounts);
+                #[allow(deprecated)]
+                token_interface::transfer(cpi_context, amount)
+            }
+        } else {
+            Ok(())
+        }
     }
 
     pub fn proxy_mint_to(ctx: Context<ProxyMintTo>, amount: u64) -> Result<()> {
-        token::mint_to(ctx.accounts.into(), amount)
+        token_interface::mint_to(ctx.accounts.into(), amount)
     }
 
     pub fn proxy_burn(ctx: Context<ProxyBurn>, amount: u64) -> Result<()> {
-        token::burn(ctx.accounts.into(), amount)
+        token_interface::burn(ctx.accounts.into(), amount)
     }
 
     pub fn proxy_set_authority(
@@ -26,7 +58,21 @@ mod token_proxy {
         authority_type: AuthorityType,
         new_authority: Option<Pubkey>,
     ) -> Result<()> {
-        token::set_authority(ctx.accounts.into(), authority_type.into(), new_authority)
+        token_interface::set_authority(ctx.accounts.into(), authority_type.into(), new_authority)
+    }
+
+    pub fn proxy_create_token_account(_ctx: Context<ProxyCreateTokenAccount>) -> Result<()> {
+        Ok(())
+    }
+
+    pub fn proxy_create_associated_token_account(
+        _ctx: Context<ProxyCreateAssociatedTokenAccount>,
+    ) -> Result<()> {
+        Ok(())
+    }
+
+    pub fn proxy_create_mint(_ctx: Context<ProxyCreateMint>, _name: String) -> Result<()> {
+        Ok(())
     }
 }
 
@@ -45,43 +91,112 @@ pub enum AuthorityType {
 #[derive(Accounts)]
 pub struct ProxyTransfer<'info> {
     #[account(signer)]
+    /// CHECK:
     pub authority: AccountInfo<'info>,
     #[account(mut)]
-    pub from: AccountInfo<'info>,
+    pub from: InterfaceAccount<'info, TokenAccount>,
     #[account(mut)]
-    pub to: AccountInfo<'info>,
-    pub token_program: AccountInfo<'info>,
+    pub to: InterfaceAccount<'info, TokenAccount>,
+    pub token_program: Interface<'info, TokenInterface>,
+}
+
+#[derive(Accounts)]
+pub struct ProxyOptionalTransfer<'info> {
+    #[account(signer)]
+    /// CHECK:
+    pub authority: AccountInfo<'info>,
+    #[account(mut)]
+    pub from: InterfaceAccount<'info, TokenAccount>,
+    #[account(mut)]
+    pub to: InterfaceAccount<'info, TokenAccount>,
+    pub mint: Option<InterfaceAccount<'info, Mint>>,
+    pub token_program: Option<Interface<'info, TokenInterface>>,
 }
 
 #[derive(Accounts)]
 pub struct ProxyMintTo<'info> {
     #[account(signer)]
+    /// CHECK:
     pub authority: AccountInfo<'info>,
     #[account(mut)]
-    pub mint: AccountInfo<'info>,
+    pub mint: InterfaceAccount<'info, Mint>,
     #[account(mut)]
-    pub to: AccountInfo<'info>,
-    pub token_program: AccountInfo<'info>,
+    pub to: InterfaceAccount<'info, TokenAccount>,
+    pub token_program: Interface<'info, TokenInterface>,
 }
 
 #[derive(Accounts)]
 pub struct ProxyBurn<'info> {
     #[account(signer)]
+    /// CHECK:
     pub authority: AccountInfo<'info>,
     #[account(mut)]
-    pub mint: AccountInfo<'info>,
+    pub mint: InterfaceAccount<'info, Mint>,
     #[account(mut)]
-    pub from: AccountInfo<'info>,
-    pub token_program: AccountInfo<'info>,
+    pub from: InterfaceAccount<'info, TokenAccount>,
+    pub token_program: Interface<'info, TokenInterface>,
 }
 
 #[derive(Accounts)]
 pub struct ProxySetAuthority<'info> {
     #[account(signer)]
+    /// CHECK:
     pub current_authority: AccountInfo<'info>,
     #[account(mut)]
+    /// CHECK:
     pub account_or_mint: AccountInfo<'info>,
-    pub token_program: AccountInfo<'info>,
+    pub token_program: Interface<'info, TokenInterface>,
+}
+
+#[derive(Accounts)]
+pub struct ProxyCreateTokenAccount<'info> {
+    #[account(mut)]
+    pub authority: Signer<'info>,
+    pub mint: InterfaceAccount<'info, Mint>,
+    #[account(init,
+        token::mint = mint,
+        token::authority = authority,
+        seeds = [authority.key().as_ref(), mint.key().as_ref(), b"token-proxy-account"],
+        bump,
+        payer = authority
+    )]
+    pub token_account: InterfaceAccount<'info, TokenAccount>,
+    pub system_program: Program<'info, System>,
+    pub token_program: Interface<'info, TokenInterface>,
+}
+
+#[derive(Accounts)]
+pub struct ProxyCreateAssociatedTokenAccount<'info> {
+    #[account(mut)]
+    pub authority: Signer<'info>,
+    #[account(
+        init,
+        associated_token::mint = mint,
+        payer = authority,
+        associated_token::authority = authority,
+    )]
+    pub token_account: InterfaceAccount<'info, TokenAccount>,
+    pub mint: InterfaceAccount<'info, Mint>,
+    pub system_program: Program<'info, System>,
+    pub token_program: Interface<'info, TokenInterface>,
+    pub associated_token_program: Program<'info, AssociatedToken>,
+}
+
+#[derive(Accounts)]
+#[instruction(name: String)]
+pub struct ProxyCreateMint<'info> {
+    #[account(mut)]
+    pub authority: Signer<'info>,
+    #[account(init,
+        mint::decimals = 9,
+        mint::authority = authority,
+        seeds = [authority.key().as_ref(), name.as_bytes(), b"token-proxy-mint"],
+        bump,
+        payer = authority
+    )]
+    pub mint: InterfaceAccount<'info, Mint>,
+    pub system_program: Program<'info, System>,
+    pub token_program: Interface<'info, TokenInterface>,
 }
 
 impl<'a, 'b, 'c, 'info> From<&mut ProxyTransfer<'info>>
@@ -89,11 +204,11 @@ impl<'a, 'b, 'c, 'info> From<&mut ProxyTransfer<'info>>
 {
     fn from(accounts: &mut ProxyTransfer<'info>) -> CpiContext<'a, 'b, 'c, 'info, Transfer<'info>> {
         let cpi_accounts = Transfer {
-            from: accounts.from.clone(),
-            to: accounts.to.clone(),
+            from: accounts.from.to_account_info().clone(),
+            to: accounts.to.to_account_info().clone(),
             authority: accounts.authority.clone(),
         };
-        let cpi_program = accounts.token_program.clone();
+        let cpi_program = accounts.token_program.to_account_info();
         CpiContext::new(cpi_program, cpi_accounts)
     }
 }
@@ -103,11 +218,11 @@ impl<'a, 'b, 'c, 'info> From<&mut ProxyMintTo<'info>>
 {
     fn from(accounts: &mut ProxyMintTo<'info>) -> CpiContext<'a, 'b, 'c, 'info, MintTo<'info>> {
         let cpi_accounts = MintTo {
-            mint: accounts.mint.clone(),
-            to: accounts.to.clone(),
+            mint: accounts.mint.to_account_info().clone(),
+            to: accounts.to.to_account_info().clone(),
             authority: accounts.authority.clone(),
         };
-        let cpi_program = accounts.token_program.clone();
+        let cpi_program = accounts.token_program.to_account_info();
         CpiContext::new(cpi_program, cpi_accounts)
     }
 }
@@ -115,11 +230,11 @@ impl<'a, 'b, 'c, 'info> From<&mut ProxyMintTo<'info>>
 impl<'a, 'b, 'c, 'info> From<&mut ProxyBurn<'info>> for CpiContext<'a, 'b, 'c, 'info, Burn<'info>> {
     fn from(accounts: &mut ProxyBurn<'info>) -> CpiContext<'a, 'b, 'c, 'info, Burn<'info>> {
         let cpi_accounts = Burn {
-            mint: accounts.mint.clone(),
-            from: accounts.from.clone(),
+            mint: accounts.mint.to_account_info().clone(),
+            from: accounts.from.to_account_info().clone(),
             authority: accounts.authority.clone(),
         };
-        let cpi_program = accounts.token_program.clone();
+        let cpi_program = accounts.token_program.to_account_info();
         CpiContext::new(cpi_program, cpi_accounts)
     }
 }
@@ -134,18 +249,20 @@ impl<'a, 'b, 'c, 'info> From<&mut ProxySetAuthority<'info>>
             account_or_mint: accounts.account_or_mint.clone(),
             current_authority: accounts.current_authority.clone(),
         }; // TODO: Support multisig signers
-        let cpi_program = accounts.token_program.clone();
+        let cpi_program = accounts.token_program.to_account_info();
         CpiContext::new(cpi_program, cpi_accounts)
     }
 }
 
-impl From<AuthorityType> for spl_token::instruction::AuthorityType {
-    fn from(authority_ty: AuthorityType) -> spl_token::instruction::AuthorityType {
+impl From<AuthorityType> for spl_token_2022::instruction::AuthorityType {
+    fn from(authority_ty: AuthorityType) -> spl_token_2022::instruction::AuthorityType {
         match authority_ty {
-            AuthorityType::MintTokens => spl_token::instruction::AuthorityType::MintTokens,
-            AuthorityType::FreezeAccount => spl_token::instruction::AuthorityType::FreezeAccount,
-            AuthorityType::AccountOwner => spl_token::instruction::AuthorityType::AccountOwner,
-            AuthorityType::CloseAccount => spl_token::instruction::AuthorityType::CloseAccount,
+            AuthorityType::MintTokens => spl_token_2022::instruction::AuthorityType::MintTokens,
+            AuthorityType::FreezeAccount => {
+                spl_token_2022::instruction::AuthorityType::FreezeAccount
+            }
+            AuthorityType::AccountOwner => spl_token_2022::instruction::AuthorityType::AccountOwner,
+            AuthorityType::CloseAccount => spl_token_2022::instruction::AuthorityType::CloseAccount,
         }
     }
 }

+ 247 - 130
tests/spl/token-proxy/tests/token-proxy.js

@@ -1,86 +1,252 @@
 const anchor = require("@coral-xyz/anchor");
 const { assert } = require("chai");
+const {
+  splTokenProgram,
+  SPL_TOKEN_PROGRAM_ID,
+} = require("@coral-xyz/spl-token");
 
-describe("token", () => {
+describe("program", () => {
   const provider = anchor.AnchorProvider.local();
 
+  const TEST_PROGRAM_IDS = [
+    SPL_TOKEN_PROGRAM_ID,
+    new anchor.web3.PublicKey("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"),
+  ];
+  const TOKEN_PROGRAMS = TEST_PROGRAM_IDS.map((programId) =>
+    splTokenProgram({
+      provider,
+      programId,
+    })
+  );
+
   // Configure the client to use the local cluster.
   anchor.setProvider(provider);
 
   const program = anchor.workspace.TokenProxy;
 
-  let mint = null;
-  let from = null;
-  let to = null;
+  TOKEN_PROGRAMS.forEach((tokenProgram) => {
+    const name =
+      tokenProgram.programId === SPL_TOKEN_PROGRAM_ID ? "token" : "token-2022";
+    describe(name, () => {
+      let mint = null;
+      let from = null;
+      let to = null;
 
-  it("Initializes test state", async () => {
-    mint = await createMint(provider);
-    from = await createTokenAccount(provider, mint, provider.wallet.publicKey);
-    to = await createTokenAccount(provider, mint, provider.wallet.publicKey);
-  });
+      it("Initializes test state", async () => {
+        mint = await createMint(tokenProgram);
+        from = await createTokenAccount(
+          tokenProgram,
+          mint,
+          provider.wallet.publicKey
+        );
+        to = await createTokenAccount(
+          tokenProgram,
+          mint,
+          provider.wallet.publicKey
+        );
+      });
 
-  it("Mints a token", async () => {
-    await program.rpc.proxyMintTo(new anchor.BN(1000), {
-      accounts: {
-        authority: provider.wallet.publicKey,
-        mint,
-        to: from,
-        tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
-      },
-    });
+      it("Creates a token account", async () => {
+        const newMint = await createMint(tokenProgram);
+        const authority = provider.wallet.publicKey;
+        const [tokenAccount] = anchor.web3.PublicKey.findProgramAddressSync(
+          [
+            authority.toBytes(),
+            newMint.toBytes(),
+            Buffer.from("token-proxy-account"),
+          ],
+          program.programId
+        );
+        await program.rpc.proxyCreateTokenAccount({
+          accounts: {
+            authority,
+            mint: newMint,
+            tokenAccount,
+            systemProgram: anchor.web3.SystemProgram.programId,
+            tokenProgram: tokenProgram.programId,
+          },
+        });
+        const account = await getTokenAccount(provider, tokenAccount);
+        assert.isTrue(account.amount.eq(new anchor.BN(0)));
+      });
 
-    const fromAccount = await getTokenAccount(provider, from);
+      it("Creates an associated token account", async () => {
+        const newMint = await createMint(tokenProgram);
+        const authority = provider.wallet.publicKey;
+        const associatedTokenProgram = new anchor.web3.PublicKey(
+          "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
+        );
+        const [tokenAccount] = anchor.web3.PublicKey.findProgramAddressSync(
+          [
+            authority.toBytes(),
+            tokenProgram.programId.toBytes(),
+            newMint.toBytes(),
+          ],
+          associatedTokenProgram
+        );
 
-    assert.isTrue(fromAccount.amount.eq(new anchor.BN(1000)));
-  });
+        await program.rpc.proxyCreateAssociatedTokenAccount({
+          accounts: {
+            tokenAccount,
+            mint: newMint,
+            authority,
+            systemProgram: anchor.web3.SystemProgram.programId,
+            tokenProgram: tokenProgram.programId,
+            associatedTokenProgram,
+          },
+        });
+        const account = await getTokenAccount(provider, tokenAccount);
+        assert.isTrue(account.amount.eq(new anchor.BN(0)));
+      });
 
-  it("Transfers a token", async () => {
-    await program.rpc.proxyTransfer(new anchor.BN(400), {
-      accounts: {
-        authority: provider.wallet.publicKey,
-        to,
-        from,
-        tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
-      },
-    });
+      it("Creates a mint", async () => {
+        const authority = provider.wallet.publicKey;
+        const [newMint] = anchor.web3.PublicKey.findProgramAddressSync(
+          [
+            authority.toBytes(),
+            Buffer.from(name),
+            Buffer.from("token-proxy-mint"),
+          ],
+          program.programId
+        );
+        await program.rpc.proxyCreateMint(name, {
+          accounts: {
+            authority,
+            mint: newMint,
+            systemProgram: anchor.web3.SystemProgram.programId,
+            tokenProgram: tokenProgram.programId,
+          },
+        });
+      });
 
-    const fromAccount = await getTokenAccount(provider, from);
-    const toAccount = await getTokenAccount(provider, to);
+      it("Mints a token", async () => {
+        await program.rpc.proxyMintTo(new anchor.BN(1000), {
+          accounts: {
+            authority: provider.wallet.publicKey,
+            mint,
+            to: from,
+            tokenProgram: tokenProgram.programId,
+          },
+        });
 
-    assert.isTrue(fromAccount.amount.eq(new anchor.BN(600)));
-    assert.isTrue(toAccount.amount.eq(new anchor.BN(400)));
-  });
+        const fromAccount = await getTokenAccount(provider, from);
 
-  it("Burns a token", async () => {
-    await program.rpc.proxyBurn(new anchor.BN(399), {
-      accounts: {
-        authority: provider.wallet.publicKey,
-        mint,
-        from: to,
-        tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
-      },
-    });
+        assert.isTrue(fromAccount.amount.eq(new anchor.BN(1000)));
+      });
 
-    const toAccount = await getTokenAccount(provider, to);
-    assert.isTrue(toAccount.amount.eq(new anchor.BN(1)));
-  });
+      it("Transfers a token", async () => {
+        const preFromAccount = await getTokenAccount(provider, from);
+        const preToAccount = await getTokenAccount(provider, to);
+
+        const transferAmount = new anchor.BN(400);
+
+        await program.rpc.proxyTransfer(transferAmount, {
+          accounts: {
+            authority: provider.wallet.publicKey,
+            to,
+            from,
+            tokenProgram: tokenProgram.programId,
+          },
+        });
+
+        const postFromAccount = await getTokenAccount(provider, from);
+        const postToAccount = await getTokenAccount(provider, to);
+
+        assert.isTrue(
+          postFromAccount.amount.eq(preFromAccount.amount.sub(transferAmount))
+        );
+        assert.isTrue(
+          postToAccount.amount.eq(preToAccount.amount.add(transferAmount))
+        );
+      });
+
+      it("Transfers a token with optional accounts", async () => {
+        const preFromAccount = await getTokenAccount(provider, from);
+        const preToAccount = await getTokenAccount(provider, to);
+
+        const transferAmount = new anchor.BN(10);
+
+        await program.rpc.proxyOptionalTransfer(transferAmount, {
+          accounts: {
+            authority: provider.wallet.publicKey,
+            to,
+            from,
+            mint,
+            tokenProgram: tokenProgram.programId,
+          },
+        });
+
+        const postFromAccount = await getTokenAccount(provider, from);
+        const postToAccount = await getTokenAccount(provider, to);
 
-  it("Set new mint authority", async () => {
-    const newMintAuthority = anchor.web3.Keypair.generate();
-    await program.rpc.proxySetAuthority(
-      { mintTokens: {} },
-      newMintAuthority.publicKey,
-      {
-        accounts: {
-          accountOrMint: mint,
-          currentAuthority: provider.wallet.publicKey,
-          tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
-        },
-      }
-    );
-
-    const mintInfo = await getMintInfo(provider, mint);
-    assert.isTrue(mintInfo.mintAuthority.equals(newMintAuthority.publicKey));
+        assert.isTrue(
+          postFromAccount.amount.eq(preFromAccount.amount.sub(transferAmount))
+        );
+        assert.isTrue(
+          postToAccount.amount.eq(preToAccount.amount.add(transferAmount))
+        );
+      });
+
+      it("Does not transfer a token without optional accounts", async () => {
+        const preFromAccount = await getTokenAccount(provider, from);
+        const preToAccount = await getTokenAccount(provider, to);
+
+        const optionalTransferIx = await program.methods
+          .proxyOptionalTransfer(new anchor.BN(10))
+          .accounts({
+            authority: provider.wallet.publicKey,
+            to,
+            from,
+            mint: null,
+            tokenProgram: null,
+          })
+          .instruction();
+        const tx = new anchor.web3.Transaction().add(optionalTransferIx);
+        await provider.sendAndConfirm(tx);
+
+        const postFromAccount = await getTokenAccount(provider, from);
+        const postToAccount = await getTokenAccount(provider, to);
+
+        assert.isTrue(postFromAccount.amount.eq(preFromAccount.amount));
+        assert.isTrue(postToAccount.amount.eq(preToAccount.amount));
+      });
+
+      it("Burns a token", async () => {
+        const preAccount = await getTokenAccount(provider, to);
+        const burnAmount = new anchor.BN(300);
+        await program.rpc.proxyBurn(burnAmount, {
+          accounts: {
+            authority: provider.wallet.publicKey,
+            mint,
+            from: to,
+            tokenProgram: tokenProgram.programId,
+          },
+        });
+
+        const postAccount = await getTokenAccount(provider, to);
+        assert.isTrue(postAccount.amount.eq(preAccount.amount.sub(burnAmount)));
+      });
+
+      it("Set new mint authority", async () => {
+        const newMintAuthority = anchor.web3.Keypair.generate();
+        await program.rpc.proxySetAuthority(
+          { mintTokens: {} },
+          newMintAuthority.publicKey,
+          {
+            accounts: {
+              accountOrMint: mint,
+              currentAuthority: provider.wallet.publicKey,
+              tokenProgram: tokenProgram.programId,
+            },
+          }
+        );
+
+        const mintInfo = await getMintInfo(provider, mint);
+        assert.isTrue(
+          mintInfo.mintAuthority.equals(newMintAuthority.publicKey)
+        );
+      });
+    });
   });
 });
 
@@ -88,13 +254,6 @@ describe("token", () => {
 // mostly irrelevant to the point of the example.
 
 const serumCmn = require("@project-serum/common");
-const TokenInstructions = require("@project-serum/serum").TokenInstructions;
-
-// TODO: remove this constant once @project-serum/serum uses the same version
-//       of @solana/web3.js as anchor (or switch packages).
-const TOKEN_PROGRAM_ID = new anchor.web3.PublicKey(
-  TokenInstructions.TOKEN_PROGRAM_ID.toString()
-);
 
 async function getTokenAccount(provider, addr) {
   return await serumCmn.getTokenAccount(provider, addr);
@@ -104,75 +263,33 @@ async function getMintInfo(provider, mintAddr) {
   return await serumCmn.getMintInfo(provider, mintAddr);
 }
 
-async function createMint(provider, authority) {
-  if (authority === undefined) {
-    authority = provider.wallet.publicKey;
-  }
+async function createMint(tokenProgram) {
   const mint = anchor.web3.Keypair.generate();
-  const instructions = await createMintInstructions(
-    provider,
-    authority,
-    mint.publicKey
-  );
+  const authority = tokenProgram.provider.wallet.publicKey;
+  const createMintIx = await tokenProgram.account.mint.createInstruction(mint);
+  const initMintIx = await tokenProgram.methods
+    .initializeMint2(0, authority, null)
+    .accounts({ mint: mint.publicKey })
+    .instruction();
 
   const tx = new anchor.web3.Transaction();
-  tx.add(...instructions);
+  tx.add(createMintIx, initMintIx);
 
-  await provider.sendAndConfirm(tx, [mint]);
+  await tokenProgram.provider.sendAndConfirm(tx, [mint]);
 
   return mint.publicKey;
 }
 
-async function createMintInstructions(provider, authority, mint) {
-  let instructions = [
-    anchor.web3.SystemProgram.createAccount({
-      fromPubkey: provider.wallet.publicKey,
-      newAccountPubkey: mint,
-      space: 82,
-      lamports: await provider.connection.getMinimumBalanceForRentExemption(82),
-      programId: TOKEN_PROGRAM_ID,
-    }),
-    TokenInstructions.initializeMint({
-      mint,
-      decimals: 0,
-      mintAuthority: authority,
-    }),
-  ];
-  return instructions;
-}
-
-async function createTokenAccount(provider, mint, owner) {
+async function createTokenAccount(tokenProgram, mint, owner) {
   const vault = anchor.web3.Keypair.generate();
   const tx = new anchor.web3.Transaction();
-  tx.add(
-    ...(await createTokenAccountInstrs(provider, vault.publicKey, mint, owner))
-  );
-  await provider.sendAndConfirm(tx, [vault]);
+  const createTokenAccountIx =
+    await tokenProgram.account.account.createInstruction(vault);
+  const initTokenAccountIx = await tokenProgram.methods
+    .initializeAccount3(owner)
+    .accounts({ account: vault.publicKey, mint })
+    .instruction();
+  tx.add(createTokenAccountIx, initTokenAccountIx);
+  await tokenProgram.provider.sendAndConfirm(tx, [vault]);
   return vault.publicKey;
 }
-
-async function createTokenAccountInstrs(
-  provider,
-  newAccountPubkey,
-  mint,
-  owner,
-  lamports
-) {
-  if (lamports === undefined) {
-    lamports = await provider.connection.getMinimumBalanceForRentExemption(165);
-  }
-  return [
-    anchor.web3.SystemProgram.createAccount({
-      fromPubkey: provider.wallet.publicKey,
-      newAccountPubkey,
-      space: 165,
-      lamports,
-      programId: TOKEN_PROGRAM_ID,
-    }),
-    TokenInstructions.initializeAccount({
-      account: newAccountPubkey,
-      mint,
-      owner,
-    }),
-  ];
-}