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

This commit is contained in:
Jon Cinque 2023-02-27 05:55:08 -05:00 committed by GitHub
parent 9044b9b8cd
commit 777f2eace1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 2107 additions and 559 deletions

109
Cargo.lock generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
spl/src/token_2022.rs Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"
);
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,
TOKEN_PROGRAM_ID
);
mintA = await Token.createMint(
provider.connection,
payer,
mintAuthority.publicKey,
null,
0,
tokenProgramIdA
);
mintB = await Token.createMint(
provider.connection,
payer,
mintAuthority.publicKey,
null,
0,
TOKEN_PROGRAM_ID
);
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);
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);
initializerTokenAccountB = await mintB.createAccount(
provider.wallet.publicKey
);
takerTokenAccountB = await mintB.createAccount(
provider.wallet.publicKey
);
await mintA.mintTo(
initializerTokenAccountA,
mintAuthority.publicKey,
[mintAuthority],
initializerAmount
);
await mintA.mintTo(
initializerTokenAccountA,
mintAuthority.publicKey,
[mintAuthority],
initializerAmount
);
await mintB.mintTo(
takerTokenAccountB,
mintAuthority.publicKey,
[mintAuthority],
takerAmount
);
await mintB.mintTo(
takerTokenAccountB,
mintAuthority.publicKey,
[mintAuthority],
takerAmount
);
let _initializerTokenAccountA = await mintA.getAccountInfo(
initializerTokenAccountA
);
let _takerTokenAccountB = await mintB.getAccountInfo(takerTokenAccountB);
let _initializerTokenAccountA = await mintA.getAccountInfo(
initializerTokenAccountA
);
let _takerTokenAccountB = await mintB.getAccountInfo(
takerTokenAccountB
);
assert.strictEqual(
_initializerTokenAccountA.amount.toNumber(),
initializerAmount
);
assert.strictEqual(_takerTokenAccountB.amount.toNumber(), takerAmount);
});
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],
}
);
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
);
// 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;
pda = _pda;
let _initializerTokenAccountA = await mintA.getAccountInfo(
initializerTokenAccountA
);
let _initializerTokenAccountA = await mintA.getAccountInfo(
initializerTokenAccountA
);
let _escrowAccount: EscrowAccount =
await program.account.escrowAccount.fetch(escrowAccount.publicKey);
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 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
)
);
});
// 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,
},
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
);
});
});

View File

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

View File

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

View File

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

View File

@ -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)));
});
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
);
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("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,
},
});
});
it("Mints a token", async () => {
await program.rpc.proxyMintTo(new anchor.BN(1000), {
accounts: {
authority: provider.wallet.publicKey,
mint,
to: from,
tokenProgram: tokenProgram.programId,
},
});
const fromAccount = await getTokenAccount(provider, from);
assert.isTrue(fromAccount.amount.eq(new anchor.BN(1000)));
});
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);
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)
);
});
});
const fromAccount = await getTokenAccount(provider, from);
assert.isTrue(fromAccount.amount.eq(new anchor.BN(1000)));
});
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,
},
});
const fromAccount = await getTokenAccount(provider, from);
const toAccount = await getTokenAccount(provider, to);
assert.isTrue(fromAccount.amount.eq(new anchor.BN(600)));
assert.isTrue(toAccount.amount.eq(new anchor.BN(400)));
});
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,
},
});
const toAccount = await getTokenAccount(provider, to);
assert.isTrue(toAccount.amount.eq(new anchor.BN(1)));
});
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));
});
});
@ -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,
}),
];
}