Add `token_program` constraint to token, mint, and associated token accounts (#2460)

This commit is contained in:
Elliot Kennedy 2023-04-19 09:43:24 +01:00 committed by GitHub
parent b11d63861c
commit 670b4f5005
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 2180 additions and 36 deletions

View File

@ -377,6 +377,8 @@ jobs:
path: tests/errors
- cmd: cd tests/spl/token-proxy && anchor test --skip-lint
path: spl/token-proxy
- cmd: cd tests/spl/token-wrapper && anchor test --skip-lint
path: spl/token-wrapper
- cmd: cd tests/multisig && anchor test --skip-lint
path: tests/multisig
# - cmd: cd tests/lockup && anchor test --skip-lint

View File

@ -13,6 +13,7 @@ The minor version will be incremented upon a breaking change and the patch versi
### Features
- spl: Add metadata wrappers `approve_collection_authority`, `bubblegum_set_collection_size`, `burn_edition_nft`, `burn_nft`, `revoke_collection_authority`, `set_token_standard`, `utilize`, `unverify_sized_collection_item`, `unverify_collection` ([#2430](https://github.com/coral-xyz/anchor/pull/2430))
- spl: Add `token_program` constraint to `Token`, `Mint`, and `AssociatedToken` accounts in order to override required `token_program` fields and use different token interface implementations in the same instruction ([#2460](https://github.com/coral-xyz/anchor/pull/2460))
- cli: Add support for Solidity programs. `anchor init` and `anchor new` take an option `--solidity` which creates solidity code rather than rust. `anchor build` and `anchor test` work accordingly ([#2421](https://github.com/coral-xyz/anchor/pull/2421))
### Fixes

View File

@ -479,6 +479,8 @@ use syn::parse_macro_input;
/// <tr>
/// <td>
/// <code>#[account(token::mint = &lt;target_account&gt;, token::authority = &lt;target_account&gt;)]</code>
/// <br><br>
/// <code>#[account(token::mint = &lt;target_account&gt;, token::authority = &lt;target_account&gt;, token::token_program = &lt;target_account&gt;)]</code>
/// </td>
/// <td>
/// Can be used as a check or with <code>init</code> to create a token
@ -546,6 +548,8 @@ use syn::parse_macro_input;
/// <tr>
/// <td>
/// <code>#[account(associated_token::mint = &lt;target_account&gt;, associated_token::authority = &lt;target_account&gt;)]</code>
/// <br><br>
/// <code>#[account(associated_token::mint = &lt;target_account&gt;, associated_token::authority = &lt;target_account&gt;, associated_token::token_program = &lt;target_account&gt;)]</code>
/// </td>
/// <td>
/// Can be used as a standalone as a check or with <code>init</code> to create an associated token
@ -580,6 +584,48 @@ use syn::parse_macro_input;
/// pub system_program: Program<'info, System>
/// </pre>
/// </td>
/// </tr><tr>
/// <td>
/// <code>#[account(*::token_program = &lt;target_account&gt;)]</code>
/// </td>
/// <td>
/// The <code>token_program</code> can optionally be overridden.
/// <br><br>
/// Example:
/// <pre>
/// use anchor_spl::{mint, token::{TokenAccount, Mint, Token}};
/// ...&#10;
/// #[account(
/// mint::token_program = token_a_token_program,
/// )]
/// pub token_a_mint: Box<InterfaceAccount<'info, Mint>>,
/// #[account(
/// mint::token_program = token_b_token_program,
/// )]
/// pub token_b_mint: Box<InterfaceAccount<'info, Mint>>,
/// #[account(
/// init,
/// payer = payer,
/// token::mint = token_a_mint,
/// token::authority = payer,
/// token::token_program = token_a_token_program,
/// )]
/// pub token_a_account: Box<InterfaceAccount<'info, TokenAccount>>,
/// #[account(
/// init,
/// payer = payer,
/// token::mint = token_b_mint,
/// token::authority = payer,
/// token::token_program = token_b_token_program,
/// )]
/// pub token_b_account: Box<InterfaceAccount<'info, TokenAccount>>,
/// pub token_a_token_program: Interface<'info, TokenInterface>,
/// pub token_b_token_program: Interface<'info, TokenInterface>,
/// #[account(mut)]
/// pub payer: Signer<'info>,
/// pub system_program: Program<'info, System>
/// </pre>
/// </td>
/// </tr>
/// <tbody>
/// </table>

View File

@ -110,6 +110,17 @@ pub enum ErrorCode {
/// 2020 - A required account for the constraint is None
#[msg("A required account for the constraint is None")]
ConstraintAccountIsNone,
/// The token token is intentional -> a token program for the token account.
///
/// 2021 - A token account token program constraint was violated
#[msg("A token account token program constraint was violated")]
ConstraintTokenTokenProgram,
/// 2022 - A mint token program constraint was violated
#[msg("A mint token program constraint was violated")]
ConstraintMintTokenProgram,
/// 2023 - A mint token program constraint was violated
#[msg("An associated token account token program constraint was violated")]
ConstraintAssociatedTokenTokenProgram,
// Require
/// 2500 - A require expression was violated

View File

@ -497,18 +497,26 @@ fn generate_constraint_init_group(
// Optional check idents
let system_program = &quote! {system_program};
let token_program = &quote! {token_program};
let associated_token_program = &quote! {associated_token_program};
let rent = &quote! {rent};
let mut check_scope = OptionalCheckScope::new_with_field(accs, field);
match &c.kind {
InitKind::Token { owner, mint } => {
InitKind::Token {
owner,
mint,
token_program,
} => {
let token_program = match token_program {
Some(t) => t.to_token_stream(),
None => quote! {token_program},
};
let owner_optional_check = check_scope.generate_check(owner);
let mint_optional_check = check_scope.generate_check(mint);
let system_program_optional_check = check_scope.generate_check(system_program);
let token_program_optional_check = check_scope.generate_check(token_program);
let token_program_optional_check = check_scope.generate_check(&token_program);
let rent_optional_check = check_scope.generate_check(rent);
let optional_checks = quote! {
@ -526,7 +534,7 @@ fn generate_constraint_init_group(
let create_account = generate_create_account(
field,
quote! {#token_account_space},
quote! {&token_program.key()},
quote! {&#token_program.key()},
quote! {#payer},
seeds_with_bump,
);
@ -539,14 +547,15 @@ fn generate_constraint_init_group(
// Checks that all the required accounts for this operation are present.
#optional_checks
if !#if_needed || AsRef::<AccountInfo>::as_ref(&#field).owner == &anchor_lang::solana_program::system_program::ID {
let owner_program = AsRef::<AccountInfo>::as_ref(&#field).owner;
if !#if_needed || owner_program == &anchor_lang::solana_program::system_program::ID {
#payer_optional_check
// Create the account with the system program.
#create_account
// Initialize the token account.
let cpi_program = token_program.to_account_info();
let cpi_program = #token_program.to_account_info();
let accounts = ::anchor_spl::token_interface::InitializeAccount3 {
account: #field.to_account_info(),
mint: #mint.to_account_info(),
@ -564,17 +573,28 @@ fn generate_constraint_init_group(
if pa.owner != #owner.key() {
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintTokenOwner).with_account_name(#name_str).with_pubkeys((pa.owner, #owner.key())));
}
if owner_program != &#token_program.key() {
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintTokenTokenProgram).with_account_name(#name_str).with_pubkeys((*owner_program, #token_program.key())));
}
}
pa
};
}
}
InitKind::AssociatedToken { owner, mint } => {
InitKind::AssociatedToken {
owner,
mint,
token_program,
} => {
let token_program = match token_program {
Some(t) => t.to_token_stream(),
None => quote! {token_program},
};
let owner_optional_check = check_scope.generate_check(owner);
let mint_optional_check = check_scope.generate_check(mint);
let system_program_optional_check = check_scope.generate_check(system_program);
let token_program_optional_check = check_scope.generate_check(token_program);
let token_program_optional_check = check_scope.generate_check(&token_program);
let associated_token_program_optional_check =
check_scope.generate_check(associated_token_program);
let rent_optional_check = check_scope.generate_check(rent);
@ -598,7 +618,8 @@ fn generate_constraint_init_group(
// Checks that all the required accounts for this operation are present.
#optional_checks
if !#if_needed || AsRef::<AccountInfo>::as_ref(&#field).owner == &anchor_lang::solana_program::system_program::ID {
let owner_program = AsRef::<AccountInfo>::as_ref(&#field).owner;
if !#if_needed || owner_program == &anchor_lang::solana_program::system_program::ID {
#payer_optional_check
let cpi_program = associated_token_program.to_account_info();
@ -608,7 +629,7 @@ fn generate_constraint_init_group(
authority: #owner.to_account_info(),
mint: #mint.to_account_info(),
system_program: system_program.to_account_info(),
token_program: token_program.to_account_info(),
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)?;
@ -621,6 +642,9 @@ fn generate_constraint_init_group(
if pa.owner != #owner.key() {
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintTokenOwner).with_account_name(#name_str).with_pubkeys((pa.owner, #owner.key())));
}
if owner_program != &#token_program.key() {
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintAssociatedTokenTokenProgram).with_account_name(#name_str).with_pubkeys((*owner_program, #token_program.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));
@ -634,7 +658,12 @@ fn generate_constraint_init_group(
owner,
decimals,
freeze_authority,
token_program,
} => {
let token_program = match token_program {
Some(t) => t.to_token_stream(),
None => quote! {token_program},
};
let owner_optional_check = check_scope.generate_check(owner);
let freeze_authority_optional_check = match freeze_authority {
Some(fa) => check_scope.generate_check(fa),
@ -642,7 +671,7 @@ fn generate_constraint_init_group(
};
let system_program_optional_check = check_scope.generate_check(system_program);
let token_program_optional_check = check_scope.generate_check(token_program);
let token_program_optional_check = check_scope.generate_check(&token_program);
let rent_optional_check = check_scope.generate_check(rent);
let optional_checks = quote! {
@ -658,7 +687,7 @@ fn generate_constraint_init_group(
let create_account = generate_create_account(
field,
quote! {::anchor_spl::token::Mint::LEN},
quote! {&token_program.key()},
quote! {&#token_program.key()},
quote! {#payer},
seeds_with_bump,
);
@ -676,7 +705,8 @@ fn generate_constraint_init_group(
// Checks that all the required accounts for this operation are present.
#optional_checks
if !#if_needed || AsRef::<AccountInfo>::as_ref(&#field).owner == &anchor_lang::solana_program::system_program::ID {
let owner_program = AsRef::<AccountInfo>::as_ref(&#field).owner;
if !#if_needed || owner_program == &anchor_lang::solana_program::system_program::ID {
// Define payer variable.
#payer_optional_check
@ -684,7 +714,7 @@ fn generate_constraint_init_group(
#create_account
// Initialize the mint account.
let cpi_program = token_program.to_account_info();
let cpi_program = #token_program.to_account_info();
let accounts = ::anchor_spl::token_interface::InitializeMint2 {
mint: #field.to_account_info(),
};
@ -705,6 +735,9 @@ fn generate_constraint_init_group(
if pa.decimals != #decimals {
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintMintDecimals).with_account_name(#name_str).with_values((pa.decimals, #decimals)));
}
if owner_program != &#token_program.key() {
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintMintTokenProgram).with_account_name(#name_str).with_pubkeys((*owner_program, #token_program.key())));
}
}
pa
};
@ -888,10 +921,21 @@ fn generate_constraint_associated_token(
#wallet_address_optional_check
#spl_token_mint_address_optional_check
};
let token_program_check = match &c.token_program {
Some(token_program) => {
let token_program_optional_check = optional_check_scope.generate_check(token_program);
quote! {
#token_program_optional_check
if #name.to_account_info().owner != &#token_program.key() { return Err(anchor_lang::error::ErrorCode::ConstraintAssociatedTokenTokenProgram.into()); }
}
}
None => quote! {},
};
quote! {
{
#optional_checks
#token_program_check
let my_owner = #name.owner;
let wallet_address = #wallet_address.key();
@ -934,10 +978,21 @@ fn generate_constraint_token_account(
}
None => quote! {},
};
let token_program_check = match &c.token_program {
Some(token_program) => {
let token_program_optional_check = optional_check_scope.generate_check(token_program);
quote! {
#token_program_optional_check
if #name.to_account_info().owner != &#token_program.key() { return Err(anchor_lang::error::ErrorCode::ConstraintTokenTokenProgram.into()); }
}
}
None => quote! {},
};
quote! {
{
#authority_check
#mint_check
#token_program_check
}
}
}
@ -983,11 +1038,22 @@ fn generate_constraint_mint(
}
None => quote! {},
};
let token_program_check = match &c.token_program {
Some(token_program) => {
let token_program_optional_check = optional_check_scope.generate_check(token_program);
quote! {
#token_program_optional_check
if #name.to_account_info().owner != &#token_program.key() { return Err(anchor_lang::error::ErrorCode::ConstraintMintTokenProgram.into()); }
}
}
None => quote! {},
};
quote! {
{
#decimal_check
#mint_authority_check
#freeze_authority_check
#token_program_check
}
}
}

View File

@ -662,11 +662,14 @@ pub enum ConstraintToken {
Address(Context<ConstraintAddress>),
TokenMint(Context<ConstraintTokenMint>),
TokenAuthority(Context<ConstraintTokenAuthority>),
TokenTokenProgram(Context<ConstraintTokenProgram>),
AssociatedTokenMint(Context<ConstraintTokenMint>),
AssociatedTokenAuthority(Context<ConstraintTokenAuthority>),
AssociatedTokenTokenProgram(Context<ConstraintTokenProgram>),
MintAuthority(Context<ConstraintMintAuthority>),
MintFreezeAuthority(Context<ConstraintMintFreezeAuthority>),
MintDecimals(Context<ConstraintMintDecimals>),
MintTokenProgram(Context<ConstraintTokenProgram>),
Bump(Context<ConstraintTokenBump>),
ProgramSeed(Context<ConstraintProgramSeed>),
Realloc(Context<ConstraintRealloc>),
@ -802,15 +805,18 @@ pub enum InitKind {
Token {
owner: Expr,
mint: Expr,
token_program: Option<Expr>,
},
AssociatedToken {
owner: Expr,
mint: Expr,
token_program: Option<Expr>,
},
Mint {
owner: Expr,
freeze_authority: Option<Expr>,
decimals: Expr,
token_program: Option<Expr>,
},
}
@ -829,6 +835,11 @@ pub struct ConstraintTokenAuthority {
pub auth: Expr,
}
#[derive(Debug, Clone)]
pub struct ConstraintTokenProgram {
token_program: Expr,
}
#[derive(Debug, Clone)]
pub struct ConstraintMintAuthority {
pub mint_auth: Expr,
@ -858,12 +869,14 @@ pub struct ConstraintProgramSeed {
pub struct ConstraintAssociatedToken {
pub wallet: Expr,
pub mint: Expr,
pub token_program: Option<Expr>,
}
#[derive(Debug, Clone)]
pub struct ConstraintTokenAccountGroup {
pub mint: Option<Expr>,
pub authority: Option<Expr>,
pub token_program: Option<Expr>,
}
#[derive(Debug, Clone)]
@ -871,6 +884,7 @@ pub struct ConstraintTokenMintGroup {
pub decimals: Option<Expr>,
pub mint_authority: Option<Expr>,
pub freeze_authority: Option<Expr>,
pub token_program: Option<Expr>,
}
// Syntaxt context object for preserving metadata about the inner item.

View File

@ -84,6 +84,12 @@ pub fn parse_token(stream: ParseStream) -> ParseResult<ConstraintToken> {
decimals: stream.parse()?,
},
)),
"token_program" => ConstraintToken::MintTokenProgram(Context::new(
span,
ConstraintTokenProgram {
token_program: stream.parse()?,
},
)),
_ => return Err(ParseError::new(ident.span(), "Invalid attribute")),
}
}
@ -111,6 +117,12 @@ pub fn parse_token(stream: ParseStream) -> ParseResult<ConstraintToken> {
auth: stream.parse()?,
},
)),
"token_program" => ConstraintToken::TokenTokenProgram(Context::new(
span,
ConstraintTokenProgram {
token_program: stream.parse()?,
},
)),
_ => return Err(ParseError::new(ident.span(), "Invalid attribute")),
}
}
@ -138,6 +150,12 @@ pub fn parse_token(stream: ParseStream) -> ParseResult<ConstraintToken> {
auth: stream.parse()?,
},
)),
"token_program" => ConstraintToken::AssociatedTokenTokenProgram(Context::new(
span,
ConstraintTokenProgram {
token_program: stream.parse()?,
},
)),
_ => return Err(ParseError::new(ident.span(), "Invalid attribute")),
}
}
@ -332,11 +350,14 @@ pub struct ConstraintGroupBuilder<'ty> {
pub address: Option<Context<ConstraintAddress>>,
pub token_mint: Option<Context<ConstraintTokenMint>>,
pub token_authority: Option<Context<ConstraintTokenAuthority>>,
pub token_token_program: Option<Context<ConstraintTokenProgram>>,
pub associated_token_mint: Option<Context<ConstraintTokenMint>>,
pub associated_token_authority: Option<Context<ConstraintTokenAuthority>>,
pub associated_token_token_program: Option<Context<ConstraintTokenProgram>>,
pub mint_authority: Option<Context<ConstraintMintAuthority>>,
pub mint_freeze_authority: Option<Context<ConstraintMintFreezeAuthority>>,
pub mint_decimals: Option<Context<ConstraintMintDecimals>>,
pub mint_token_program: Option<Context<ConstraintTokenProgram>>,
pub bump: Option<Context<ConstraintTokenBump>>,
pub program_seed: Option<Context<ConstraintProgramSeed>>,
pub realloc: Option<Context<ConstraintRealloc>>,
@ -364,11 +385,14 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
address: None,
token_mint: None,
token_authority: None,
token_token_program: None,
associated_token_mint: None,
associated_token_authority: None,
associated_token_token_program: None,
mint_authority: None,
mint_freeze_authority: None,
mint_decimals: None,
mint_token_program: None,
bump: None,
program_seed: None,
realloc: None,
@ -563,11 +587,14 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
address,
token_mint,
token_authority,
token_token_program,
associated_token_mint,
associated_token_authority,
associated_token_token_program,
mint_authority,
mint_freeze_authority,
mint_decimals,
mint_token_program,
bump,
program_seed,
realloc,
@ -600,20 +627,33 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
.expect("bump must be provided with seeds"),
program_seed: into_inner!(program_seed).map(|id| id.program_seed),
});
let associated_token = match (associated_token_mint, associated_token_authority) {
(Some(mint), Some(auth)) => Some(ConstraintAssociatedToken {
let associated_token = match (
associated_token_mint,
associated_token_authority,
&associated_token_token_program,
) {
(Some(mint), Some(auth), _) => Some(ConstraintAssociatedToken {
wallet: auth.into_inner().auth,
mint: mint.into_inner().mint,
token_program: associated_token_token_program
.as_ref()
.map(|a| a.clone().into_inner().token_program),
}),
(Some(mint), None) => return Err(ParseError::new(
(Some(mint), None, _) => return Err(ParseError::new(
mint.span(),
"authority must be provided to specify an associated token program derived address",
)),
(None, Some(auth)) => {
(None, Some(auth), _) => {
return Err(ParseError::new(
auth.span(),
"mint must be provided to specify an associated token program derived address",
))
},
(None, None, Some(token_program)) => {
return Err(ParseError::new(
token_program.span(),
"mint and authority must be provided to specify an associated token program derived address",
))
}
_ => None,
};
@ -626,18 +666,26 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
}
}
let token_account = match (&token_mint, &token_authority) {
(None, None) => None,
let token_account = match (&token_mint, &token_authority, &token_token_program) {
(None, None, None) => None,
_ => Some(ConstraintTokenAccountGroup {
mint: token_mint.as_ref().map(|a| a.clone().into_inner().mint),
authority: token_authority
.as_ref()
.map(|a| a.clone().into_inner().auth),
token_program: token_token_program
.as_ref()
.map(|a| a.clone().into_inner().token_program),
}),
};
let mint = match (&mint_decimals, &mint_authority, &mint_freeze_authority) {
(None, None, None) => None,
let mint = match (
&mint_decimals,
&mint_authority,
&mint_freeze_authority,
&mint_token_program,
) {
(None, None, None, None) => None,
_ => Some(ConstraintTokenMintGroup {
decimals: mint_decimals
.as_ref()
@ -648,6 +696,9 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
freeze_authority: mint_freeze_authority
.as_ref()
.map(|a| a.clone().into_inner().mint_freeze_auth),
token_program: mint_token_program
.as_ref()
.map(|a| a.clone().into_inner().token_program),
}),
};
@ -667,11 +718,13 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
"authority must be provided to initialize a token program derived address"
)),
},
token_program: token_token_program.map(|tp| tp.into_inner().token_program),
}
} else if let Some(at) = &associated_token {
InitKind::AssociatedToken {
mint: at.mint.clone(),
owner: at.wallet.clone()
owner: at.wallet.clone(),
token_program: associated_token_token_program.map(|tp| tp.into_inner().token_program),
}
} else if let Some(d) = &mint_decimals {
InitKind::Mint {
@ -683,7 +736,8 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
"authority must be provided to initialize a mint program derived address"
))
},
freeze_authority: mint_freeze_authority.map(|fa| fa.into_inner().mint_freeze_auth)
freeze_authority: mint_freeze_authority.map(|fa| fa.into_inner().mint_freeze_auth),
token_program: mint_token_program.map(|tp| tp.into_inner().token_program),
}
} else {
InitKind::Program {
@ -731,11 +785,16 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
ConstraintToken::Address(c) => self.add_address(c),
ConstraintToken::TokenAuthority(c) => self.add_token_authority(c),
ConstraintToken::TokenMint(c) => self.add_token_mint(c),
ConstraintToken::TokenTokenProgram(c) => self.add_token_token_program(c),
ConstraintToken::AssociatedTokenAuthority(c) => self.add_associated_token_authority(c),
ConstraintToken::AssociatedTokenMint(c) => self.add_associated_token_mint(c),
ConstraintToken::AssociatedTokenTokenProgram(c) => {
self.add_associated_token_token_program(c)
}
ConstraintToken::MintAuthority(c) => self.add_mint_authority(c),
ConstraintToken::MintFreezeAuthority(c) => self.add_mint_freeze_authority(c),
ConstraintToken::MintDecimals(c) => self.add_mint_decimals(c),
ConstraintToken::MintTokenProgram(c) => self.add_mint_token_program(c),
ConstraintToken::Bump(c) => self.add_bump(c),
ConstraintToken::ProgramSeed(c) => self.add_program_seed(c),
ConstraintToken::Realloc(c) => self.add_realloc(c),
@ -763,6 +822,12 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
"init must be provided before token authority",
));
}
if self.token_token_program.is_some() {
return Err(ParseError::new(
c.span(),
"init must be provided before token account token program",
));
}
if self.mint_authority.is_some() {
return Err(ParseError::new(
c.span(),
@ -781,6 +846,12 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
"init must be provided before mint decimals",
));
}
if self.mint_token_program.is_some() {
return Err(ParseError::new(
c.span(),
"init must be provided before mint token program",
));
}
if self.associated_token_mint.is_some() {
return Err(ParseError::new(
c.span(),
@ -793,6 +864,12 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
"init must be provided before associated token authority",
));
}
if self.associated_token_token_program.is_some() {
return Err(ParseError::new(
c.span(),
"init must be provided before associated token account token program",
));
}
self.init.replace(c);
Ok(())
}
@ -988,6 +1065,31 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
Ok(())
}
fn add_token_token_program(&mut self, c: Context<ConstraintTokenProgram>) -> ParseResult<()> {
if self.token_token_program.is_some() {
return Err(ParseError::new(
c.span(),
"token token_program already provided",
));
}
self.token_token_program.replace(c);
Ok(())
}
fn add_associated_token_token_program(
&mut self,
c: Context<ConstraintTokenProgram>,
) -> ParseResult<()> {
if self.associated_token_token_program.is_some() {
return Err(ParseError::new(
c.span(),
"associated token token_program already provided",
));
}
self.associated_token_token_program.replace(c);
Ok(())
}
fn add_mint_authority(&mut self, c: Context<ConstraintMintAuthority>) -> ParseResult<()> {
if self.mint_authority.is_some() {
return Err(ParseError::new(c.span(), "mint authority already provided"));
@ -1018,6 +1120,17 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
Ok(())
}
fn add_mint_token_program(&mut self, c: Context<ConstraintTokenProgram>) -> ParseResult<()> {
if self.mint_token_program.is_some() {
return Err(ParseError::new(
c.span(),
"mint token_program already provided",
));
}
self.mint_token_program.replace(c);
Ok(())
}
fn add_mut(&mut self, c: Context<ConstraintMut>) -> ParseResult<()> {
if self.mutable.is_some() {
return Err(ParseError::new(c.span(), "mut already provided"));

View File

@ -92,14 +92,23 @@ fn constraints_cross_checks(fields: &[AccountField]) -> ParseResult<()> {
// init token/a_token/mint needs token program.
match kind {
InitKind::Program { .. } | InitKind::Interface { .. } => (),
InitKind::Token { .. } | InitKind::AssociatedToken { .. } | InitKind::Mint { .. } => {
if !fields
.iter()
.any(|f| f.ident() == "token_program" && !(required_init && f.is_optional()))
{
InitKind::Token { token_program, .. }
| InitKind::AssociatedToken { token_program, .. }
| InitKind::Mint { token_program, .. } => {
// is the token_program constraint specified?
let token_program_field = if let Some(token_program_id) = token_program {
// if so, is it present in the struct?
token_program_id.to_token_stream().to_string()
} else {
// if not, look for the token_program field
"token_program".to_string()
};
if !fields.iter().any(|f| {
f.ident() == &token_program_field && !(required_init && f.is_optional())
}) {
return Err(ParseError::new(
init_fields[0].ident.span(),
message("init", "token_program", required_init),
message("init", &token_program_field, required_init),
));
}
}

View File

@ -47,6 +47,24 @@ pub struct TestInitAssociatedToken<'info> {
pub associated_token_program: Option<Program<'info, AssociatedToken>>,
}
#[derive(Accounts)]
pub struct TestInitAssociatedTokenWithTokenProgram<'info> {
#[account(init,
payer = payer,
associated_token::mint = mint,
associated_token::authority = payer,
associated_token::token_program = associated_token_token_program,
)]
pub token: Option<Account<'info, TokenAccount>>,
pub mint: Option<Account<'info, Mint>>,
#[account(mut)]
pub payer: Option<Signer<'info>>,
pub system_program: Option<Program<'info, System>>,
/// CHECK: ignore
pub associated_token_token_program: Option<AccountInfo<'info>>,
pub associated_token_program: Option<Program<'info, AssociatedToken>>,
}
#[derive(Accounts)]
pub struct TestValidateAssociatedToken<'info> {
#[account(
@ -233,6 +251,23 @@ pub struct TestInitMint<'info> {
pub token_program: Option<Program<'info, Token>>,
}
#[derive(Accounts)]
pub struct TestInitMintWithTokenProgram<'info> {
#[account(init,
payer = payer,
mint::decimals = 6,
mint::authority = payer,
mint::freeze_authority = payer,
mint::token_program = mint_token_program,
)]
pub mint: Option<Account<'info, Mint>>,
#[account(mut)]
pub payer: Option<Signer<'info>>,
pub system_program: Option<Program<'info, System>>,
/// CHECK: ignore
pub mint_token_program: Option<AccountInfo<'info>>,
}
#[derive(Accounts)]
pub struct TestInitToken<'info> {
#[account(init, token::mint = mint, token::authority = payer, payer = payer, )]
@ -244,6 +279,23 @@ pub struct TestInitToken<'info> {
pub token_program: Option<Program<'info, Token>>,
}
#[derive(Accounts)]
pub struct TestInitTokenWithTokenProgram<'info> {
#[account(init,
payer = payer,
token::mint = mint,
token::authority = payer,
token::token_program = token_token_program,
)]
pub token: Option<Account<'info, TokenAccount>>,
pub mint: Option<Account<'info, Mint>>,
#[account(mut)]
pub payer: Option<Signer<'info>>,
pub system_program: Option<Program<'info, System>>,
/// CHECK: ignore
pub token_token_program: Option<AccountInfo<'info>>,
}
#[derive(Accounts)]
pub struct TestFetchAll<'info> {
#[account(init, payer = authority, space = DataWithFilter::LEN + 8)]
@ -326,6 +378,27 @@ pub struct TestInitMintIfNeeded<'info> {
pub freeze_authority: Option<AccountInfo<'info>>,
}
#[derive(Accounts)]
pub struct TestInitMintIfNeededWithTokenProgram<'info> {
#[account(init_if_needed,
payer = payer,
mint::decimals = 6,
mint::authority = mint_authority,
mint::freeze_authority = freeze_authority,
mint::token_program = mint_token_program,
)]
pub mint: Option<Account<'info, Mint>>,
#[account(mut)]
pub payer: Option<Signer<'info>>,
pub system_program: Option<Program<'info, System>>,
/// CHECK: ignore
pub mint_token_program: Option<AccountInfo<'info>>,
/// CHECK: ignore
pub mint_authority: Option<AccountInfo<'info>>,
/// CHECK: ignore
pub freeze_authority: Option<AccountInfo<'info>>,
}
#[derive(Accounts)]
pub struct TestInitTokenIfNeeded<'info> {
#[account(init_if_needed, token::mint = mint, token::authority = authority, payer = payer, )]
@ -339,10 +412,28 @@ pub struct TestInitTokenIfNeeded<'info> {
pub authority: Option<AccountInfo<'info>>,
}
#[derive(Accounts)]
pub struct TestInitTokenIfNeededWithTokenProgram<'info> {
#[account(init_if_needed,
payer = payer,
token::mint = mint,
token::authority = authority,
token::token_program = token_token_program
)]
pub token: Option<Account<'info, TokenAccount>>,
pub mint: Option<Account<'info, Mint>>,
#[account(mut)]
pub payer: Option<Signer<'info>>,
pub system_program: Option<Program<'info, System>>,
/// CHECK: ignore
pub token_token_program: Option<AccountInfo<'info>>,
/// CHECK:
pub authority: Option<AccountInfo<'info>>,
}
#[derive(Accounts)]
pub struct TestInitAssociatedTokenIfNeeded<'info> {
#[account(
init_if_needed,
#[account(init_if_needed,
payer = payer,
associated_token::mint = mint,
associated_token::authority = authority
@ -358,6 +449,26 @@ pub struct TestInitAssociatedTokenIfNeeded<'info> {
pub authority: Option<AccountInfo<'info>>,
}
#[derive(Accounts)]
pub struct TestInitAssociatedTokenIfNeededWithTokenProgram<'info> {
#[account(init_if_needed,
payer = payer,
associated_token::mint = mint,
associated_token::authority = authority,
associated_token::token_program = associated_token_token_program,
)]
pub token: Option<Account<'info, TokenAccount>>,
pub mint: Option<Account<'info, Mint>>,
#[account(mut)]
pub payer: Option<Signer<'info>>,
pub system_program: Option<Program<'info, System>>,
/// CHECK: ignore
pub associated_token_token_program: Option<AccountInfo<'info>>,
pub associated_token_program: Option<Program<'info, AssociatedToken>>,
/// CHECK: ignore
pub authority: Option<AccountInfo<'info>>,
}
#[derive(Accounts)]
pub struct TestMultidimensionalArray<'info> {
#[account(zero)]
@ -492,6 +603,16 @@ pub struct TestOnlyAuthorityConstraint<'info> {
pub mint: Option<Account<'info, Mint>>,
pub payer: Option<Signer<'info>>,
}
#[derive(Accounts)]
pub struct TestOnlyTokenProgramConstraint<'info> {
#[account(
token::token_program = token_token_program
)]
pub token: Option<Account<'info, TokenAccount>>,
pub token_token_program: Option<AccountInfo<'info>>,
}
#[derive(Accounts)]
pub struct TestOnlyMintConstraint<'info> {
#[account(
@ -554,6 +675,16 @@ pub struct TestMintMissMintAuthConstraint<'info> {
pub freeze_authority: Option<AccountInfo<'info>>,
}
#[derive(Accounts)]
pub struct TestMintOnlyTokenProgramConstraint<'info> {
#[account(
mint::token_program = mint_token_program,
)]
pub mint: Option<Account<'info, Mint>>,
/// CHECK: ignore
pub mint_token_program: Option<AccountInfo<'info>>,
}
#[derive(Accounts)]
pub struct TestAssociatedToken<'info> {
#[account(
@ -564,3 +695,18 @@ pub struct TestAssociatedToken<'info> {
pub mint: Option<Account<'info, Mint>>,
pub authority: Option<AccountInfo<'info>>,
}
#[derive(Accounts)]
pub struct TestAssociatedTokenWithTokenProgramConstraint<'info> {
#[account(
associated_token::mint = mint,
associated_token::authority = authority,
associated_token::token_program = associated_token_token_program,
)]
pub token: Option<Account<'info, TokenAccount>>,
pub mint: Account<'info, Mint>,
/// CHECK: ignore
pub authority: AccountInfo<'info>,
/// CHECK: ignore
pub associated_token_token_program: Option<AccountInfo<'info>>,
}

View File

@ -171,6 +171,10 @@ pub mod misc_optional {
Ok(())
}
pub fn test_init_mint_with_token_program(_ctx: Context<TestInitMintWithTokenProgram>) -> Result<()> {
Ok(())
}
pub fn test_init_token(ctx: Context<TestInitToken>) -> Result<()> {
assert!(
ctx.accounts.token.as_ref().unwrap().mint == ctx.accounts.mint.as_ref().unwrap().key()
@ -178,6 +182,11 @@ pub mod misc_optional {
Ok(())
}
pub fn test_init_token_with_token_program(_ctx: Context<TestInitTokenWithTokenProgram>) -> Result<()> {
Ok(())
}
pub fn test_composite_payer(ctx: Context<TestCompositePayer>) -> Result<()> {
ctx.accounts.composite.data.as_mut().unwrap().data = 1;
ctx.accounts.data.as_mut().unwrap().udata = 2;
@ -192,6 +201,10 @@ pub mod misc_optional {
Ok(())
}
pub fn test_init_associated_token_with_token_program(ctx: Context<TestInitAssociatedTokenWithTokenProgram>) -> Result<()> {
Ok(())
}
pub fn test_validate_associated_token(
_ctx: Context<TestValidateAssociatedToken>,
) -> Result<()> {
@ -238,16 +251,32 @@ pub mod misc_optional {
Ok(())
}
pub fn test_init_mint_if_needed_with_token_program(
_ctx: Context<TestInitMintIfNeededWithTokenProgram>,
) -> Result<()> {
Ok(())
}
pub fn test_init_token_if_needed(_ctx: Context<TestInitTokenIfNeeded>) -> Result<()> {
Ok(())
}
pub fn test_init_token_if_needed_with_token_program(_ctx: Context<TestInitTokenIfNeededWithTokenProgram>) -> Result<()> {
Ok(())
}
pub fn test_init_associated_token_if_needed(
_ctx: Context<TestInitAssociatedTokenIfNeeded>,
) -> Result<()> {
Ok(())
}
pub fn test_init_associated_token_if_needed_with_token_program(
_ctx: Context<TestInitAssociatedTokenIfNeededWithTokenProgram>,
) -> Result<()> {
Ok(())
}
pub fn init_with_space(_ctx: Context<InitWithSpace>, data: u16) -> Result<()> {
Ok(())
}
@ -328,6 +357,10 @@ pub mod misc_optional {
Ok(())
}
pub fn test_only_token_program_constraint(_ctx: Context<TestOnlyTokenProgramConstraint>) -> Result<()> {
Ok(())
}
pub fn test_mint_constraint(_ctx: Context<TestMintConstraint>, _decimals: u8) -> Result<()> {
Ok(())
}
@ -358,7 +391,17 @@ pub mod misc_optional {
Ok(())
}
pub fn test_mint_only_token_program_constraint(
_ctx: Context<TestMintOnlyTokenProgramConstraint>,
) -> Result<()> {
Ok(())
}
pub fn test_associated_constraint(_ctx: Context<TestAssociatedToken>) -> Result<()> {
Ok(())
}
pub fn test_associated_token_with_token_program_constraint(_ctx: Context<TestAssociatedTokenWithTokenProgramConstraint>) -> Result<()> {
Ok(())
}
}

View File

@ -34,8 +34,8 @@ pub struct TestTokenSeedsInit<'info> {
pub struct TestInitAssociatedToken<'info> {
#[account(
init,
associated_token::mint = mint,
payer = payer,
associated_token::mint = mint,
associated_token::authority = payer,
)]
pub token: Account<'info, TokenAccount>,
@ -47,6 +47,25 @@ pub struct TestInitAssociatedToken<'info> {
pub associated_token_program: Program<'info, AssociatedToken>,
}
#[derive(Accounts)]
pub struct TestInitAssociatedTokenWithTokenProgram<'info> {
#[account(
init,
payer = payer,
associated_token::mint = mint,
associated_token::authority = payer,
associated_token::token_program = associated_token_token_program,
)]
pub token: Account<'info, TokenAccount>,
pub mint: Account<'info, Mint>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
/// CHECK: ignore
pub associated_token_token_program: AccountInfo<'info>,
pub associated_token_program: Program<'info, AssociatedToken>,
}
#[derive(Accounts)]
pub struct TestValidateAssociatedToken<'info> {
#[account(
@ -230,6 +249,23 @@ pub struct TestInitMint<'info> {
pub token_program: Program<'info, Token>,
}
#[derive(Accounts)]
pub struct TestInitMintWithTokenProgram<'info> {
#[account(init,
payer = payer,
mint::decimals = 6,
mint::authority = payer,
mint::freeze_authority = payer,
mint::token_program = mint_token_program,
)]
pub mint: Account<'info, Mint>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
/// CHECK: ignore
pub mint_token_program: AccountInfo<'info>,
}
#[derive(Accounts)]
pub struct TestInitToken<'info> {
#[account(init, token::mint = mint, token::authority = payer, payer = payer, )]
@ -241,6 +277,23 @@ pub struct TestInitToken<'info> {
pub token_program: Program<'info, Token>,
}
#[derive(Accounts)]
pub struct TestInitTokenWithTokenProgram<'info> {
#[account(init,
payer = payer,
token::mint = mint,
token::authority = payer,
token::token_program = token_token_program,
)]
pub token: Account<'info, TokenAccount>,
pub mint: Account<'info, Mint>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
/// CHECK: ignore
pub token_token_program: AccountInfo<'info>,
}
#[derive(Accounts)]
pub struct TestCompositePayer<'info> {
pub composite: TestInit<'info>,
@ -331,6 +384,27 @@ pub struct TestInitMintIfNeeded<'info> {
pub freeze_authority: AccountInfo<'info>,
}
#[derive(Accounts)]
pub struct TestInitMintIfNeededWithTokenProgram<'info> {
#[account(init_if_needed,
payer = payer,
mint::decimals = 6,
mint::authority = mint_authority,
mint::token_program = mint_token_program,
mint::freeze_authority = freeze_authority,
)]
pub mint: Account<'info, Mint>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
/// CHECK: ignore
pub mint_token_program: AccountInfo<'info>,
/// CHECK: ignore
pub mint_authority: AccountInfo<'info>,
/// CHECK: ignore
pub freeze_authority: AccountInfo<'info>,
}
#[derive(Accounts)]
pub struct TestInitTokenIfNeeded<'info> {
#[account(init_if_needed, token::mint = mint, token::authority = authority, payer = payer, )]
@ -344,10 +418,28 @@ pub struct TestInitTokenIfNeeded<'info> {
pub authority: AccountInfo<'info>,
}
#[derive(Accounts)]
pub struct TestInitTokenIfNeededWithTokenProgram<'info> {
#[account(init_if_needed,
payer = payer,
token::mint = mint,
token::authority = authority,
token::token_program = token_token_program
)]
pub token: Account<'info, TokenAccount>,
pub mint: Account<'info, Mint>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
/// CHECK: ignore
pub token_token_program: AccountInfo<'info>,
/// CHECK:
pub authority: AccountInfo<'info>,
}
#[derive(Accounts)]
pub struct TestInitAssociatedTokenIfNeeded<'info> {
#[account(
init_if_needed,
#[account(init_if_needed,
payer = payer,
associated_token::mint = mint,
associated_token::authority = authority
@ -363,6 +455,26 @@ pub struct TestInitAssociatedTokenIfNeeded<'info> {
pub authority: AccountInfo<'info>,
}
#[derive(Accounts)]
pub struct TestInitAssociatedTokenIfNeededWithTokenProgram<'info> {
#[account(init_if_needed,
payer = payer,
associated_token::mint = mint,
associated_token::authority = authority,
associated_token::token_program = associated_token_token_program,
)]
pub token: Account<'info, TokenAccount>,
pub mint: Account<'info, Mint>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
/// CHECK: ignore
pub associated_token_token_program: AccountInfo<'info>,
pub associated_token_program: Program<'info, AssociatedToken>,
/// CHECK: ignore
pub authority: AccountInfo<'info>,
}
#[derive(Accounts)]
pub struct TestMultidimensionalArray<'info> {
#[account(zero)]
@ -489,6 +601,7 @@ pub struct TestAuthorityConstraint<'info> {
/// CHECK: ignore
pub fake_authority: AccountInfo<'info>,
}
#[derive(Accounts)]
pub struct TestOnlyAuthorityConstraint<'info> {
#[account(
@ -498,6 +611,16 @@ pub struct TestOnlyAuthorityConstraint<'info> {
pub mint: Account<'info, Mint>,
pub payer: Signer<'info>,
}
#[derive(Accounts)]
pub struct TestOnlyTokenProgramConstraint<'info> {
#[account(
token::token_program = token_token_program
)]
pub token: Account<'info, TokenAccount>,
pub token_token_program: AccountInfo<'info>,
}
#[derive(Accounts)]
pub struct TestOnlyMintConstraint<'info> {
#[account(
@ -566,6 +689,16 @@ pub struct TestMintMissMintAuthConstraint<'info> {
pub freeze_authority: AccountInfo<'info>,
}
#[derive(Accounts)]
pub struct TestMintOnlyTokenProgramConstraint<'info> {
#[account(
mint::token_program = mint_token_program,
)]
pub mint: Account<'info, Mint>,
/// CHECK: ignore
pub mint_token_program: AccountInfo<'info>,
}
#[derive(Accounts)]
pub struct TestAssociatedToken<'info> {
#[account(
@ -577,3 +710,18 @@ pub struct TestAssociatedToken<'info> {
/// CHECK: ignore
pub authority: AccountInfo<'info>,
}
#[derive(Accounts)]
pub struct TestAssociatedTokenWithTokenProgramConstraint<'info> {
#[account(
associated_token::mint = mint,
associated_token::authority = authority,
associated_token::token_program = associated_token_token_program,
)]
pub token: Account<'info, TokenAccount>,
pub mint: Account<'info, Mint>,
/// CHECK: ignore
pub authority: AccountInfo<'info>,
/// CHECK: ignore
pub associated_token_token_program: AccountInfo<'info>,
}

View File

@ -175,11 +175,19 @@ pub mod misc {
Ok(())
}
pub fn test_init_mint_with_token_program(_ctx: Context<TestInitMintWithTokenProgram>) -> Result<()> {
Ok(())
}
pub fn test_init_token(ctx: Context<TestInitToken>) -> Result<()> {
assert!(ctx.accounts.token.mint == ctx.accounts.mint.key());
Ok(())
}
pub fn test_init_token_with_token_program(_ctx: Context<TestInitTokenWithTokenProgram>) -> Result<()> {
Ok(())
}
pub fn test_composite_payer(ctx: Context<TestCompositePayer>) -> Result<()> {
ctx.accounts.composite.data.data = 1;
ctx.accounts.data.udata = 2;
@ -192,6 +200,10 @@ pub mod misc {
Ok(())
}
pub fn test_init_associated_token_with_token_program(_ctx: Context<TestInitAssociatedTokenWithTokenProgram>) -> Result<()> {
Ok(())
}
pub fn test_validate_associated_token(
_ctx: Context<TestValidateAssociatedToken>,
) -> Result<()> {
@ -237,16 +249,32 @@ pub mod misc {
Ok(())
}
pub fn test_init_mint_if_needed_with_token_program(
_ctx: Context<TestInitMintIfNeededWithTokenProgram>
) -> Result<()> {
Ok(())
}
pub fn test_init_token_if_needed(_ctx: Context<TestInitTokenIfNeeded>) -> Result<()> {
Ok(())
}
pub fn test_init_token_if_needed_with_token_program(_ctx: Context<TestInitTokenIfNeededWithTokenProgram>) -> Result<()> {
Ok(())
}
pub fn test_init_associated_token_if_needed(
_ctx: Context<TestInitAssociatedTokenIfNeeded>,
) -> Result<()> {
Ok(())
}
pub fn test_init_associated_token_if_needed_with_token_program(
_ctx: Context<TestInitAssociatedTokenIfNeededWithTokenProgram>,
) -> Result<()> {
Ok(())
}
pub fn init_with_space(_ctx: Context<InitWithSpace>, _data: u16) -> Result<()> {
Ok(())
}
@ -317,6 +345,10 @@ pub mod misc {
Ok(())
}
pub fn test_only_token_program_constraint(_ctx: Context<TestOnlyTokenProgramConstraint>) -> Result<()> {
Ok(())
}
pub fn test_mint_constraint(_ctx: Context<TestMintConstraint>, _decimals: u8) -> Result<()> {
Ok(())
}
@ -347,7 +379,17 @@ pub mod misc {
Ok(())
}
pub fn test_mint_only_token_program_constraint(
_ctx: Context<TestMintOnlyTokenProgramConstraint>,
) -> Result<()> {
Ok(())
}
pub fn test_associated_constraint(_ctx: Context<TestAssociatedToken>) -> Result<()> {
Ok(())
}
pub fn test_associated_token_with_token_program_constraint(_ctx: Context<TestAssociatedTokenWithTokenProgramConstraint>) -> Result<()> {
Ok(())
}
}

View File

@ -901,6 +901,68 @@ const miscTest = (
assert.strictEqual(account2.idata.toNumber(), 3);
});
it("Can create a random mint account with token program", async () => {
const newMint = anchor.web3.Keypair.generate();
await program.rpc.testInitMintWithTokenProgram({
accounts: {
mint: newMint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
mintTokenProgram: TOKEN_PROGRAM_ID,
},
signers: [newMint],
});
const client = new Token(
program.provider.connection,
newMint.publicKey,
TOKEN_PROGRAM_ID,
wallet.payer
);
const mintAccount = await client.getMintInfo();
assert.strictEqual(mintAccount.decimals, 6);
assert.isTrue(
mintAccount.mintAuthority.equals(provider.wallet.publicKey)
);
assert.isTrue(
mintAccount.freezeAuthority.equals(provider.wallet.publicKey)
);
const accInfo = await program.provider.connection.getAccountInfo(
newMint.publicKey
);
assert.strictEqual(accInfo.owner.toString(), TOKEN_PROGRAM_ID.toString());
});
it("Can create a random token account with token program", async () => {
const token = anchor.web3.Keypair.generate();
await program.rpc.testInitTokenWithTokenProgram({
accounts: {
token: token.publicKey,
mint: mint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
tokenTokenProgram: TOKEN_PROGRAM_ID,
},
signers: [token],
});
const client = new Token(
program.provider.connection,
mint.publicKey,
TOKEN_PROGRAM_ID,
wallet.payer
);
const account = await client.getAccountInfo(token.publicKey);
// @ts-expect-error
assert.strictEqual(account.state, 1);
assert.strictEqual(account.amount.toNumber(), 0);
assert.isTrue(account.isInitialized);
assert.strictEqual(
account.owner.toString(),
provider.wallet.publicKey.toString()
);
assert.strictEqual(account.mint.toString(), mint.publicKey.toString());
});
describe("associated_token constraints", () => {
let associatedToken = null;
// apparently cannot await here so doing it in the 'it' statements
@ -942,6 +1004,64 @@ const miscTest = (
assert.isTrue(account.mint.equals(localClient.publicKey));
});
it("Can create an associated token account with token program", async () => {
const newMint = anchor.web3.Keypair.generate();
await program.rpc.testInitMint({
accounts: {
mint: newMint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
},
signers: [newMint],
});
const associatedToken = await Token.getAssociatedTokenAddress(
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
newMint.publicKey,
provider.wallet.publicKey
);
await program.rpc.testInitAssociatedTokenWithTokenProgram({
accounts: {
token: associatedToken,
mint: newMint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
associatedTokenTokenProgram: TOKEN_PROGRAM_ID,
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
},
});
const token = new Token(
program.provider.connection,
newMint.publicKey,
TOKEN_PROGRAM_ID,
wallet.payer
);
const ataAccount = await token.getAccountInfo(associatedToken);
// @ts-expect-error
assert.strictEqual(ataAccount.state, 1);
assert.strictEqual(ataAccount.amount.toNumber(), 0);
assert.isTrue(ataAccount.isInitialized);
assert.strictEqual(
ataAccount.owner.toString(),
provider.wallet.publicKey.toString()
);
assert.strictEqual(
ataAccount.mint.toString(),
newMint.publicKey.toString()
);
const rawAta = await provider.connection.getAccountInfo(
associatedToken
);
assert.strictEqual(
rawAta.owner.toBase58(),
TOKEN_PROGRAM_ID.toBase58()
);
});
it("Can use fetchNullable() on accounts with only a balance", async () => {
const account = anchor.web3.Keypair.generate();
@ -1028,6 +1148,106 @@ const miscTest = (
}
);
});
it("associated_token constraints (no init) - Can make with associated_token::token_program", async () => {
const mint = anchor.web3.Keypair.generate();
await program.rpc.testInitMint({
accounts: {
mint: mint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
},
signers: [mint],
});
const associatedToken = await Token.getAssociatedTokenAddress(
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
mint.publicKey,
provider.wallet.publicKey
);
await program.rpc.testInitAssociatedToken({
accounts: {
token: associatedToken,
mint: mint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
},
signers: [],
});
await program.rpc.testAssociatedTokenWithTokenProgramConstraint({
accounts: {
token: associatedToken,
mint: mint.publicKey,
authority: provider.wallet.publicKey,
associatedTokenTokenProgram: TOKEN_PROGRAM_ID,
},
});
const account = await provider.connection.getAccountInfo(
associatedToken
);
assert.strictEqual(
account.owner.toString(),
TOKEN_PROGRAM_ID.toString()
);
});
it("associated_token constraints (no init) - throws if associated_token::token_program mismatch", async () => {
const mint = anchor.web3.Keypair.generate();
await program.rpc.testInitMint({
accounts: {
mint: mint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
},
signers: [mint],
});
const associatedToken = await Token.getAssociatedTokenAddress(
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
mint.publicKey,
provider.wallet.publicKey
);
await program.rpc.testInitAssociatedToken({
accounts: {
token: associatedToken,
mint: mint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
},
signers: [],
});
const fakeTokenProgram = Keypair.generate();
try {
await program.rpc.testAssociatedTokenWithTokenProgramConstraint({
accounts: {
token: associatedToken,
mint: mint.publicKey,
authority: provider.wallet.publicKey,
associatedTokenTokenProgram: fakeTokenProgram.publicKey,
},
});
assert.isTrue(false);
} catch (_err) {
assert.isTrue(_err instanceof AnchorError);
const err: AnchorError = _err;
assert.strictEqual(err.error.errorCode.number, 2023);
assert.strictEqual(
err.error.errorCode.code,
"ConstraintAssociatedTokenTokenProgram"
);
}
});
});
it("Can fetch all accounts of a given type", async () => {
@ -1253,6 +1473,300 @@ const miscTest = (
assert.isUndefined(miscIdl.constants.find((c) => c.name === "NO_IDL"));
});
it("init_if_needed creates mint account if not exists", async () => {
const newMint = anchor.web3.Keypair.generate();
await program.rpc.testInitMintIfNeeded(6, {
accounts: {
mint: newMint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
mintAuthority: provider.wallet.publicKey,
freezeAuthority: provider.wallet.publicKey,
},
signers: [newMint],
});
const mintClient = new Token(
provider.connection,
newMint.publicKey,
TOKEN_PROGRAM_ID,
wallet.payer
);
const mintAccount = await mintClient.getMintInfo();
assert.strictEqual(mintAccount.decimals, 6);
assert.strictEqual(
mintAccount.mintAuthority.toString(),
provider.wallet.publicKey.toString()
);
assert.strictEqual(
mintAccount.freezeAuthority.toString(),
provider.wallet.publicKey.toString()
);
assert.strictEqual(mintAccount.supply.toNumber(), 0);
const rawAccount = await provider.connection.getAccountInfo(
newMint.publicKey
);
assert.strictEqual(
rawAccount.owner.toString(),
TOKEN_PROGRAM_ID.toString()
);
});
it("init_if_needed creates mint account if not exists with token program", async () => {
const newMint = anchor.web3.Keypair.generate();
await program.rpc.testInitMintIfNeededWithTokenProgram({
accounts: {
mint: newMint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
mintTokenProgram: TOKEN_PROGRAM_ID,
mintAuthority: provider.wallet.publicKey,
freezeAuthority: provider.wallet.publicKey,
},
signers: [newMint],
});
const mintClient = new Token(
provider.connection,
newMint.publicKey,
TOKEN_PROGRAM_ID,
wallet.payer
);
const mintAccount = await mintClient.getMintInfo();
assert.strictEqual(mintAccount.decimals, 6);
assert.strictEqual(
mintAccount.mintAuthority.toString(),
provider.wallet.publicKey.toString()
);
assert.strictEqual(
mintAccount.freezeAuthority.toString(),
provider.wallet.publicKey.toString()
);
assert.strictEqual(mintAccount.supply.toNumber(), 0);
const rawAccount = await provider.connection.getAccountInfo(
newMint.publicKey
);
assert.strictEqual(
rawAccount.owner.toString(),
TOKEN_PROGRAM_ID.toString()
);
});
it("init_if_needed creates token account if not exists", async () => {
const newMint = anchor.web3.Keypair.generate();
await program.rpc.testInitMint({
accounts: {
mint: newMint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
},
signers: [newMint],
});
const newToken = anchor.web3.Keypair.generate();
await program.rpc.testInitTokenIfNeeded({
accounts: {
token: newToken.publicKey,
mint: newMint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
authority: provider.wallet.publicKey,
},
signers: [newToken],
});
const mintClient = new Token(
provider.connection,
newMint.publicKey,
TOKEN_PROGRAM_ID,
wallet.payer
);
const tokenAccount = await mintClient.getAccountInfo(newToken.publicKey);
assert.strictEqual(tokenAccount.amount.toNumber(), 0);
assert.strictEqual(
tokenAccount.mint.toString(),
newMint.publicKey.toString()
);
assert.strictEqual(
tokenAccount.owner.toString(),
provider.wallet.publicKey.toString()
);
const rawAccount = await provider.connection.getAccountInfo(
newToken.publicKey
);
assert.strictEqual(
rawAccount.owner.toString(),
TOKEN_PROGRAM_ID.toString()
);
});
it("init_if_needed creates token account if not exists with token program", async () => {
const newMint = anchor.web3.Keypair.generate();
await program.rpc.testInitMint({
accounts: {
mint: newMint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
},
signers: [newMint],
});
const newToken = anchor.web3.Keypair.generate();
await program.rpc.testInitTokenIfNeededWithTokenProgram({
accounts: {
token: newToken.publicKey,
mint: newMint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
tokenTokenProgram: TOKEN_PROGRAM_ID,
authority: provider.wallet.publicKey,
},
signers: [newToken],
});
const mintClient = new Token(
provider.connection,
newMint.publicKey,
TOKEN_PROGRAM_ID,
wallet.payer
);
const tokenAccount = await mintClient.getAccountInfo(newToken.publicKey);
assert.strictEqual(tokenAccount.amount.toNumber(), 0);
assert.strictEqual(
tokenAccount.mint.toString(),
newMint.publicKey.toString()
);
assert.strictEqual(
tokenAccount.owner.toString(),
provider.wallet.publicKey.toString()
);
const rawAccount = await provider.connection.getAccountInfo(
newToken.publicKey
);
assert.strictEqual(
rawAccount.owner.toString(),
TOKEN_PROGRAM_ID.toString()
);
});
it("init_if_needed creates associated token account if not exists", async () => {
const newMint = anchor.web3.Keypair.generate();
await program.rpc.testInitMint({
accounts: {
mint: newMint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
},
signers: [newMint],
});
const associatedToken = await Token.getAssociatedTokenAddress(
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
newMint.publicKey,
provider.wallet.publicKey
);
await program.rpc.testInitAssociatedTokenIfNeeded({
accounts: {
token: associatedToken,
mint: newMint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
tokenProgram: TOKEN_PROGRAM_ID,
authority: provider.wallet.publicKey,
},
});
const mintClient = new Token(
provider.connection,
newMint.publicKey,
TOKEN_PROGRAM_ID,
wallet.payer
);
const ataAccount = await mintClient.getAccountInfo(associatedToken);
assert.strictEqual(ataAccount.amount.toNumber(), 0);
assert.strictEqual(
ataAccount.mint.toString(),
newMint.publicKey.toString()
);
assert.strictEqual(
ataAccount.owner.toString(),
provider.wallet.publicKey.toString()
);
const rawAccount = await provider.connection.getAccountInfo(
associatedToken
);
assert.strictEqual(
rawAccount.owner.toString(),
TOKEN_PROGRAM_ID.toString()
);
});
it("init_if_needed creates associated token account if not exists with token program", async () => {
const newMint = anchor.web3.Keypair.generate();
await program.rpc.testInitMint({
accounts: {
mint: newMint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
},
signers: [newMint],
});
const associatedToken = await Token.getAssociatedTokenAddress(
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
newMint.publicKey,
provider.wallet.publicKey
);
await program.rpc.testInitAssociatedTokenIfNeededWithTokenProgram({
accounts: {
token: associatedToken,
mint: newMint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
associatedTokenTokenProgram: TOKEN_PROGRAM_ID,
authority: provider.wallet.publicKey,
},
});
const mintClient = new Token(
provider.connection,
newMint.publicKey,
TOKEN_PROGRAM_ID,
wallet.payer
);
const ataAccount = await mintClient.getAccountInfo(associatedToken);
assert.strictEqual(ataAccount.amount.toNumber(), 0);
assert.strictEqual(
ataAccount.mint.toString(),
newMint.publicKey.toString()
);
assert.strictEqual(
ataAccount.owner.toString(),
provider.wallet.publicKey.toString()
);
const rawAccount = await provider.connection.getAccountInfo(
associatedToken
);
assert.strictEqual(
rawAccount.owner.toString(),
TOKEN_PROGRAM_ID.toString()
);
});
it("init_if_needed throws if account exists but is not owned by the expected program", async () => {
const newAcc = await anchor.web3.PublicKey.findProgramAddress(
[utf8.encode("hello")],
@ -1447,6 +1961,68 @@ const miscTest = (
}
});
it("init_if_needed pass if mint exists with token program", async () => {
const newMint = anchor.web3.Keypair.generate();
await program.rpc.testInitMint({
accounts: {
mint: newMint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
},
signers: [newMint],
});
await program.rpc.testInitMintIfNeededWithTokenProgram({
accounts: {
mint: newMint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
mintTokenProgram: TOKEN_PROGRAM_ID,
mintAuthority: provider.wallet.publicKey,
freezeAuthority: provider.wallet.publicKey,
},
signers: [newMint],
});
});
it("init_if_needed throws if mint exists but has the wrong token program", async () => {
const newMint = anchor.web3.Keypair.generate();
await program.rpc.testInitMint({
accounts: {
mint: newMint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
},
signers: [newMint],
});
const fakeTokenProgram = Keypair.generate();
try {
await program.rpc.testInitMintIfNeededWithTokenProgram({
accounts: {
mint: newMint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
mintTokenProgram: fakeTokenProgram.publicKey,
mintAuthority: provider.wallet.publicKey,
freezeAuthority: provider.wallet.publicKey,
},
signers: [newMint],
});
expect(false).to.be.true;
} catch (_err) {
assert.isTrue(_err instanceof AnchorError);
const err: AnchorError = _err;
assert.strictEqual(err.error.errorCode.number, 2022);
assert.strictEqual(
err.error.errorCode.code,
"ConstraintMintTokenProgram"
);
}
});
it("init_if_needed throws if token exists but has the wrong owner", async () => {
const mint = anchor.web3.Keypair.generate();
await program.rpc.testInitMint({
@ -1546,6 +2122,92 @@ const miscTest = (
}
});
it("init_if_needed pass if token exists with token program", async () => {
const mint = anchor.web3.Keypair.generate();
await program.rpc.testInitMint({
accounts: {
mint: mint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
},
signers: [mint],
});
const token = anchor.web3.Keypair.generate();
await program.rpc.testInitToken({
accounts: {
token: token.publicKey,
mint: mint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
},
signers: [token],
});
await program.rpc.testInitTokenIfNeededWithTokenProgram({
accounts: {
token: token.publicKey,
mint: mint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
tokenTokenProgram: TOKEN_PROGRAM_ID,
authority: provider.wallet.publicKey,
},
signers: [token],
});
});
it("init_if_needed throws if token exists but has the wrong token program", async () => {
const mint = anchor.web3.Keypair.generate();
await program.rpc.testInitMint({
accounts: {
mint: mint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
},
signers: [mint],
});
const token = anchor.web3.Keypair.generate();
await program.rpc.testInitToken({
accounts: {
token: token.publicKey,
mint: mint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
},
signers: [token],
});
const fakeTokenProgram = Keypair.generate();
try {
await program.rpc.testInitTokenIfNeededWithTokenProgram({
accounts: {
token: token.publicKey,
mint: mint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
tokenTokenProgram: fakeTokenProgram.publicKey,
authority: provider.wallet.publicKey,
},
signers: [token],
});
expect(false).to.be.true;
} catch (_err) {
assert.isTrue(_err instanceof AnchorError);
const err: AnchorError = _err;
assert.strictEqual(err.error.errorCode.number, 2021);
assert.strictEqual(
err.error.errorCode.code,
"ConstraintTokenTokenProgram"
);
}
});
it("init_if_needed throws if associated token exists but has the wrong owner", async () => {
const mint = Keypair.generate();
await program.rpc.testInitMint({
@ -1636,7 +2298,6 @@ const miscTest = (
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
},
});
console.log("InitAssocToken:", txn);
try {
await program.rpc.testInitAssociatedTokenIfNeeded({
@ -1722,6 +2383,104 @@ const miscTest = (
}
});
it("init_if_needed pass if associated token exists with token program", async () => {
const mint = anchor.web3.Keypair.generate();
await program.rpc.testInitMint({
accounts: {
mint: mint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
},
signers: [mint],
});
const associatedToken = await Token.getAssociatedTokenAddress(
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
mint.publicKey,
provider.wallet.publicKey
);
await program.rpc.testInitAssociatedToken({
accounts: {
token: associatedToken,
mint: mint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
},
});
await program.rpc.testInitAssociatedTokenIfNeededWithTokenProgram({
accounts: {
token: associatedToken,
mint: mint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
associatedTokenTokenProgram: TOKEN_PROGRAM_ID,
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
authority: provider.wallet.publicKey,
},
});
});
it("init_if_needed throws if associated token exists but has the wrong token program", async () => {
const mint = anchor.web3.Keypair.generate();
await program.rpc.testInitMint({
accounts: {
mint: mint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
},
signers: [mint],
});
const associatedToken = await Token.getAssociatedTokenAddress(
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
mint.publicKey,
provider.wallet.publicKey
);
await program.rpc.testInitAssociatedToken({
accounts: {
token: associatedToken,
mint: mint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
},
});
const fakeTokenProgram = Keypair.generate();
try {
await program.rpc.testInitAssociatedTokenIfNeededWithTokenProgram({
accounts: {
token: associatedToken,
mint: mint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
associatedTokenTokenProgram: fakeTokenProgram.publicKey,
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
authority: provider.wallet.publicKey,
},
});
expect(false).to.be.true;
} catch (_err) {
assert.isTrue(_err instanceof AnchorError);
const err: AnchorError = _err;
assert.strictEqual(err.error.errorCode.number, 2023);
assert.strictEqual(
err.error.errorCode.code,
"ConstraintAssociatedTokenTokenProgram"
);
}
});
it("Can use multidimensional array", async () => {
const array2d = new Array(10).fill(new Array(10).fill(99));
const data = anchor.web3.Keypair.generate();
@ -1991,6 +2750,45 @@ const miscTest = (
assert.isTrue(account.mint.equals(mint.publicKey));
});
it("Token Constraint Test(no init) - Can make only token::token_program", async () => {
const mint = anchor.web3.Keypair.generate();
await program.rpc.testInitMint({
accounts: {
mint: mint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
},
signers: [mint],
});
const token = anchor.web3.Keypair.generate();
await program.rpc.testInitToken({
accounts: {
token: token.publicKey,
mint: mint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
},
signers: [token],
});
await program.rpc.testOnlyTokenProgramConstraint({
accounts: {
token: token.publicKey,
tokenTokenProgram: TOKEN_PROGRAM_ID,
},
});
const account = await provider.connection.getAccountInfo(
token.publicKey
);
assert.strictEqual(
account.owner.toString(),
TOKEN_PROGRAM_ID.toString()
);
});
it("Token Constraint Test(no init) - throws if token::mint mismatch", async () => {
const mint = anchor.web3.Keypair.generate();
await program.rpc.testInitMint({
@ -2132,6 +2930,48 @@ const miscTest = (
}
});
it("Token Constraint Test(no init) - throws if token::token_program mismatch", async () => {
const mint = anchor.web3.Keypair.generate();
await program.rpc.testInitMint({
accounts: {
mint: mint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
},
signers: [mint],
});
const token = anchor.web3.Keypair.generate();
await program.rpc.testInitToken({
accounts: {
token: token.publicKey,
mint: mint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
},
signers: [token],
});
const fakeTokenProgram = Keypair.generate();
try {
await program.rpc.testOnlyTokenProgramConstraint({
accounts: {
token: token.publicKey,
tokenTokenProgram: fakeTokenProgram.publicKey,
},
});
assert.isTrue(false);
} catch (_err) {
assert.isTrue(_err instanceof AnchorError);
const err: AnchorError = _err;
assert.strictEqual(err.error.errorCode.number, 2021);
assert.strictEqual(
err.error.errorCode.code,
"ConstraintTokenTokenProgram"
);
}
});
it("Mint Constraint Test(no init) - mint::decimals, mint::authority, mint::freeze_authority", async () => {
const mint = anchor.web3.Keypair.generate();
await program.rpc.testInitMint({
@ -2264,6 +3104,38 @@ const miscTest = (
}
});
it("Mint Constraint Test(no init) - throws if mint::token_program mismatch", async () => {
const mint = anchor.web3.Keypair.generate();
await program.rpc.testInitMint({
accounts: {
mint: mint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
},
signers: [mint],
});
const fakeTokenProgram = Keypair.generate();
try {
await program.rpc.testMintOnlyTokenProgramConstraint({
accounts: {
mint: mint.publicKey,
mintTokenProgram: fakeTokenProgram.publicKey,
},
});
assert.isTrue(false);
} catch (_err) {
assert.isTrue(_err instanceof AnchorError);
const err: AnchorError = _err;
assert.strictEqual(err.error.errorCode.number, 2022);
assert.strictEqual(
err.error.errorCode.code,
"ConstraintMintTokenProgram"
);
}
});
it("Mint Constraint Test(no init) - can write only mint::decimals", async () => {
const mint = anchor.web3.Keypair.generate();
await program.rpc.testInitMint({
@ -2385,6 +3257,34 @@ const miscTest = (
mintAccount.freezeAuthority.equals(provider.wallet.publicKey)
);
});
it("Mint Constraint Test(no init) - can write only mint::token_program", async () => {
const mint = anchor.web3.Keypair.generate();
await program.rpc.testInitMint({
accounts: {
mint: mint.publicKey,
payer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
},
signers: [mint],
});
await program.rpc.testMintOnlyTokenProgramConstraint({
accounts: {
mint: mint.publicKey,
mintTokenProgram: TOKEN_PROGRAM_ID,
},
});
const mintAccount = await provider.connection.getAccountInfo(
mint.publicKey
);
assert.strictEqual(
mintAccount.owner.toString(),
TOKEN_PROGRAM_ID.toString()
);
});
it("check versioned transaction is now available", async () => {
let thisTx = new VersionedTransaction(
new Message({

View File

@ -29,6 +29,7 @@
"pyth",
"realloc",
"spl/token-proxy",
"spl/token-wrapper",
"swap",
"system-accounts",
"sysvars",

View File

@ -0,0 +1,17 @@
[provider]
cluster = "localnet"
wallet = "~/.config/solana/id.json"
[programs.localnet]
token_wrapper = "4ZPcGU8MX8oL2u1EtErHzixAbgNBNeE9yoYq3kKMqnAy"
[scripts]
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

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

View File

@ -0,0 +1,19 @@
{
"name": "token-wrapper",
"version": "0.27.0",
"license": "(MIT OR Apache-2.0)",
"homepage": "https://github.com/coral-xyz/anchor#readme",
"bugs": {
"url": "https://github.com/coral-xyz/anchor/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/coral-xyz/anchor.git"
},
"engines": {
"node": ">=11"
},
"scripts": {
"test": "anchor test"
}
}

View File

@ -0,0 +1,21 @@
[package]
name = "token-wrapper"
version = "0.1.0"
description = "Created with Anchor"
rust-version = "1.60"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
name = "token_wrapper"
[features]
no-entrypoint = []
no-idl = []
cpi = ["no-entrypoint"]
default = []
[dependencies]
anchor-lang = { path = "../../../../../lang" }
anchor-spl = { path = "../../../../../spl" }
spl-token-2022 = { version = "0.5.0", features = ["no-entrypoint"] }

View File

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

View File

@ -0,0 +1,309 @@
//! An example program where users can wrap tokens
//! Source tokens are deposited into a vault in exchange for wrapped tokens at a 1:1 ratio
//! This is a stateless implementation which relies on PDAs for security
//!
//! Initializer initializes a new wrapper:
//! - SPL token/token-2022 mint (X) the deposit tokens the wrapper will receive
//! - SPL token/token-2022 mint (Y) the wrapped tokens returned for each deposit token
//!
//! Once this wrapper is initialised:
//! 1. Users can call the wrap function to deposit X and mint Y wrapped tokens
//! 2. Users can call the unwrap function to burn Y and withdraw X unwrapped tokens
use anchor_lang::prelude::*;
use anchor_spl::token_interface::{
self, Mint, TokenAccount, TokenInterface,
};
declare_id!("4ZPcGU8MX8oL2u1EtErHzixAbgNBNeE9yoYq3kKMqnAy");
#[program]
pub mod token_wrapper {
use super::*;
pub const WRAPPER_AUTH_SEED: &[u8] = b"wrapr";
pub const WRAPPER_VAULT_SEED: &[u8] = b"vault";
pub fn initialize(
ctx: Context<Initialize>,
initializer_amount: u64,
) -> Result<()> {
// deposit into vault
token_interface::transfer_checked(
CpiContext::new(
ctx.accounts.deposit_token_program.to_account_info(),
token_interface::TransferChecked {
from: ctx.accounts.initializer_deposit_token_account.to_account_info(),
mint: ctx.accounts.deposit_mint.to_account_info(),
to: ctx.accounts.deposit_token_vault.to_account_info(),
authority: ctx.accounts.initializer.to_account_info(),
},
),
initializer_amount,
ctx.accounts.deposit_mint.decimals,
)?;
// mint wrapped tokens
let inner_seeds = [
WRAPPER_AUTH_SEED,
ctx.accounts.deposit_mint.to_account_info().key.as_ref(),
ctx.accounts.wrapped_mint.to_account_info().key.as_ref(),
&[*ctx.bumps.get("wrapper_authority").unwrap()],
];
let signer_seeds = &[&inner_seeds[..]];
token_interface::mint_to(
CpiContext::new_with_signer(
ctx.accounts.wrapped_token_program.to_account_info(),
token_interface::MintTo {
mint: ctx.accounts.wrapped_mint.to_account_info(),
to: ctx.accounts.initializer_wrapped_token_account.to_account_info(),
authority: ctx.accounts.wrapper_authority.to_account_info(),
},
signer_seeds,
),
initializer_amount,
)?;
Ok(())
}
pub fn wrap(ctx: Context<Wrap>, wrap_amount: u64) -> Result<()> {
// deposit into vault
token_interface::transfer_checked(
CpiContext::new(
ctx.accounts.deposit_token_program.to_account_info(),
token_interface::TransferChecked {
from: ctx.accounts.user_deposit_token_account.to_account_info(),
mint: ctx.accounts.deposit_mint.to_account_info(),
to: ctx.accounts.deposit_token_vault.to_account_info(),
authority: ctx.accounts.signer.to_account_info(),
},
),
wrap_amount,
ctx.accounts.deposit_mint.decimals,
)?;
// mint wrapped tokens
let inner_seeds = [
WRAPPER_AUTH_SEED,
ctx.accounts.deposit_mint.to_account_info().key.as_ref(),
ctx.accounts.wrapped_mint.to_account_info().key.as_ref(),
&[*ctx.bumps.get("wrapper_authority").unwrap()],
];
let signer_seeds = &[&inner_seeds[..]];
token_interface::mint_to(
CpiContext::new_with_signer(
ctx.accounts.wrapped_token_program.to_account_info(),
token_interface::MintTo {
mint: ctx.accounts.wrapped_mint.to_account_info(),
to: ctx.accounts.user_wrapped_token_account.to_account_info(),
authority: ctx.accounts.wrapper_authority.to_account_info(),
},
signer_seeds,
),
wrap_amount,
)?;
Ok(())
}
pub fn unwrap(ctx: Context<Unwrap>, unwrap_amount: u64) -> Result<()> {
// burn wrapped tokens
token_interface::burn(
CpiContext::new(
ctx.accounts.wrapped_token_program.to_account_info(),
token_interface::Burn {
mint: ctx.accounts.wrapped_mint.to_account_info(),
from: ctx.accounts.user_wrapped_token_account.to_account_info(),
authority: ctx.accounts.signer.to_account_info(),
},
),
unwrap_amount,
)?;
// withdraw from vault
let inner_seeds = [
WRAPPER_AUTH_SEED,
ctx.accounts.deposit_mint.to_account_info().key.as_ref(),
ctx.accounts.wrapped_mint.to_account_info().key.as_ref(),
&[*ctx.bumps.get("wrapper_authority").unwrap()],
];
let signer_seeds = &[&inner_seeds[..]];
token_interface::transfer_checked(
CpiContext::new_with_signer(
ctx.accounts.deposit_token_program.to_account_info(),
token_interface::TransferChecked {
from: ctx.accounts.deposit_token_vault.to_account_info(),
mint: ctx.accounts.deposit_mint.to_account_info(),
to: ctx.accounts.user_deposit_token_account.to_account_info(),
authority: ctx.accounts.wrapper_authority.to_account_info(),
},
signer_seeds
),
unwrap_amount,
ctx.accounts.deposit_mint.decimals,
)?;
Ok(())
}
}
#[derive(Accounts)]
#[instruction(initializer_amount: u64)]
pub struct Initialize<'info> {
#[account(mut)]
pub initializer: Signer<'info>,
#[account(
mint::token_program = deposit_token_program,
)]
pub deposit_mint: Box<InterfaceAccount<'info, Mint>>,
#[account(init,
payer = initializer,
mint::decimals = deposit_mint.decimals,
mint::authority = wrapper_authority,
mint::token_program = wrapped_token_program,
)]
pub wrapped_mint: Box<InterfaceAccount<'info, Mint>>,
/// Program-owned vault to store deposited tokens
#[account(init,
seeds = [WRAPPER_VAULT_SEED, deposit_mint.key().as_ref(), wrapped_mint.key().as_ref()],
bump,
payer = initializer,
token::mint = deposit_mint,
token::authority = wrapper_authority,
token::token_program = deposit_token_program,
)]
pub deposit_token_vault: Box<InterfaceAccount<'info, TokenAccount>>,
/// User token account to deposit tokens from
#[account(mut,
constraint = initializer_deposit_token_account.amount >= initializer_amount,
token::mint = deposit_mint,
token::authority = initializer,
token::token_program = deposit_token_program,
)]
pub initializer_deposit_token_account: Box<InterfaceAccount<'info, TokenAccount>>,
/// User token account to send wrapped tokens to
#[account(init,
payer = initializer,
token::mint = wrapped_mint,
token::authority = initializer,
token::token_program = wrapped_token_program,
)]
pub initializer_wrapped_token_account: Box<InterfaceAccount<'info, TokenAccount>>,
/// CHECK: PDA owned by the program
#[account(mut,
seeds = [WRAPPER_AUTH_SEED, deposit_mint.key().as_ref(), wrapped_mint.key().as_ref()],
bump,
)]
pub wrapper_authority: AccountInfo<'info>,
pub system_program: Program<'info, System>,
pub deposit_token_program: Interface<'info, TokenInterface>,
pub wrapped_token_program: Interface<'info, TokenInterface>,
}
#[derive(Accounts)]
#[instruction(wrap_amount: u64)]
pub struct Wrap<'info> {
#[account(mut)]
pub signer: Signer<'info>,
#[account(
mint::token_program = deposit_token_program,
)]
pub deposit_mint: Box<InterfaceAccount<'info, Mint>>,
#[account(mut,
mint::authority = wrapper_authority,
mint::token_program = wrapped_token_program,
)]
pub wrapped_mint: Box<InterfaceAccount<'info, Mint>>,
#[account(mut,
seeds = [WRAPPER_VAULT_SEED, deposit_mint.key().as_ref(), wrapped_mint.key().as_ref()],
bump,
token::mint = deposit_mint,
token::authority = wrapper_authority,
token::token_program = deposit_token_program,
)]
pub deposit_token_vault: Box<InterfaceAccount<'info, TokenAccount>>,
#[account(mut,
constraint = user_deposit_token_account.amount >= wrap_amount,
token::mint = deposit_mint,
token::token_program = deposit_token_program,
)]
pub user_deposit_token_account: Box<InterfaceAccount<'info, TokenAccount>>,
#[account(mut,
token::mint = wrapped_mint,
token::token_program = wrapped_token_program,
)]
pub user_wrapped_token_account: Box<InterfaceAccount<'info, TokenAccount>>,
/// CHECK: PDA owned by the program
#[account(mut,
seeds = [WRAPPER_AUTH_SEED, deposit_mint.key().as_ref(), wrapped_mint.key().as_ref()],
bump,
)]
pub wrapper_authority: AccountInfo<'info>,
pub deposit_token_program: Interface<'info, TokenInterface>,
pub wrapped_token_program: Interface<'info, TokenInterface>,
}
#[derive(Accounts)]
#[instruction(unwrap_amount: u64)]
pub struct Unwrap<'info> {
#[account(mut)]
pub signer: Signer<'info>,
#[account(
mint::token_program = deposit_token_program,
)]
pub deposit_mint: Box<InterfaceAccount<'info, Mint>>,
#[account(mut,
mint::token_program = wrapped_token_program,
mint::authority = wrapper_authority,
)]
pub wrapped_mint: Box<InterfaceAccount<'info, Mint>>,
#[account(mut,
seeds = [WRAPPER_VAULT_SEED, deposit_mint.key().as_ref(), wrapped_mint.key().as_ref()],
bump,
token::mint = deposit_mint,
token::authority = wrapper_authority,
token::token_program = deposit_token_program,
)]
pub deposit_token_vault: Box<InterfaceAccount<'info, TokenAccount>>,
#[account(mut,
token::mint = deposit_mint,
token::token_program = deposit_token_program,
)]
pub user_deposit_token_account: Box<InterfaceAccount<'info, TokenAccount>>,
#[account(mut,
constraint = user_wrapped_token_account.amount >= unwrap_amount,
token::mint = wrapped_mint,
token::token_program = wrapped_token_program,
)]
pub user_wrapped_token_account: Box<InterfaceAccount<'info, TokenAccount>>,
/// CHECK: PDA owned by the program
#[account(mut,
seeds = [crate::token_wrapper::WRAPPER_AUTH_SEED, deposit_mint.key().as_ref(), wrapped_mint.key().as_ref()],
bump,
)]
pub wrapper_authority: AccountInfo<'info>,
pub deposit_token_program: Interface<'info, TokenInterface>,
pub wrapped_token_program: Interface<'info, TokenInterface>,
}

View File

@ -0,0 +1,219 @@
import * as anchor from "@coral-xyz/anchor";
import { Program, BN, IdlAccounts } from "@coral-xyz/anchor";
import { PublicKey, Keypair, SystemProgram } from "@solana/web3.js";
import { TOKEN_PROGRAM_ID, Token } from "@solana/spl-token";
import { assert } from "chai";
import { TokenWrapper } from "../target/types/token_wrapper";
describe("wrapper", () => {
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const TOKEN_2022_PROGRAM_ID = new anchor.web3.PublicKey(
"TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
);
const TEST_TOKEN_PROGRAM_IDS = [
[TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID],
[TOKEN_2022_PROGRAM_ID, TOKEN_2022_PROGRAM_ID],
[TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID],
[TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID],
];
const program = anchor.workspace.TokenWrapper as Program<TokenWrapper>;
let depositMint: Token = null;
let wrappedMint: Token = null;
let initializerDepositTokenAccount: PublicKey = null;
let userWrappedTokenAccount: PublicKey = null;
let userDepositTokenAccount: PublicKey = null;
let depositTokenVault: PublicKey = null;
let wrapperAuthority: PublicKey = null;
const wrapAmount = 1000;
const initializerAmount = 500;
const payer = Keypair.generate();
const mintAuthority = Keypair.generate();
TEST_TOKEN_PROGRAM_IDS.forEach((tokenProgramIds) => {
const [depositTokenProgram, wrappedTokenProgram] = tokenProgramIds;
let name;
if (depositTokenProgram === wrappedTokenProgram) {
name = wrappedTokenProgram === TOKEN_PROGRAM_ID ? "token" : "token-2022";
} else {
name =
wrappedTokenProgram === TOKEN_PROGRAM_ID
? "mixed-wrapped-token"
: "mixed-wrapped-token-2022";
}
describe(name, () => {
it("Initialise wrapper", async () => {
// Airdropping tokens to a payer.
await provider.connection.confirmTransaction(
await provider.connection.requestAirdrop(
payer.publicKey,
10000000000
),
"confirmed"
);
depositMint = await Token.createMint(
provider.connection,
payer,
mintAuthority.publicKey,
null,
6,
depositTokenProgram
);
initializerDepositTokenAccount = await depositMint.createAccount(
provider.wallet.publicKey
);
await depositMint.mintTo(
initializerDepositTokenAccount,
mintAuthority.publicKey,
[mintAuthority],
initializerAmount
);
const wrappedMintKP = new Keypair();
const initializerWrappedTokenAccountKP = new Keypair();
// Get the PDA that is assigned authority to the deposit vault account
const [_wrapperAuthority, _] = PublicKey.findProgramAddressSync(
[
Buffer.from(anchor.utils.bytes.utf8.encode("wrapr")),
depositMint.publicKey.toBuffer(),
wrappedMintKP.publicKey.toBuffer(),
],
program.programId
);
wrapperAuthority = _wrapperAuthority;
// Get the deposit vault account PDA
const [_depositTokenVault, __] = PublicKey.findProgramAddressSync(
[
Buffer.from(anchor.utils.bytes.utf8.encode("vault")),
depositMint.publicKey.toBuffer(),
wrappedMintKP.publicKey.toBuffer(),
],
program.programId
);
depositTokenVault = _depositTokenVault;
await program.rpc.initialize(new BN(initializerAmount), {
accounts: {
initializer: provider.wallet.publicKey,
depositMint: depositMint.publicKey,
wrappedMint: wrappedMintKP.publicKey,
depositTokenVault,
initializerDepositTokenAccount,
initializerWrappedTokenAccount:
initializerWrappedTokenAccountKP.publicKey,
wrapperAuthority,
systemProgram: SystemProgram.programId,
depositTokenProgram,
wrappedTokenProgram,
},
signers: [wrappedMintKP, initializerWrappedTokenAccountKP],
});
wrappedMint = new Token(
provider.connection,
wrappedMintKP.publicKey,
wrappedTokenProgram,
payer
);
let _initializerDepositTokenAccount = await depositMint.getAccountInfo(
initializerDepositTokenAccount
);
let _initializerWrappedTokenAccount = await wrappedMint.getAccountInfo(
initializerWrappedTokenAccountKP.publicKey
);
assert.strictEqual(
_initializerDepositTokenAccount.amount.toNumber(),
0
);
assert.strictEqual(
_initializerWrappedTokenAccount.amount.toNumber(),
initializerAmount
);
});
it("Wrap", async () => {
userDepositTokenAccount = await depositMint.createAccount(
provider.wallet.publicKey
);
userWrappedTokenAccount = await wrappedMint.createAccount(
provider.wallet.publicKey
);
await depositMint.mintTo(
userDepositTokenAccount,
mintAuthority.publicKey,
[mintAuthority],
wrapAmount
);
await program.rpc.wrap(new BN(wrapAmount), {
accounts: {
signer: provider.wallet.publicKey,
depositMint: depositMint.publicKey,
wrappedMint: wrappedMint.publicKey,
depositTokenVault: depositTokenVault,
userDepositTokenAccount,
userWrappedTokenAccount,
wrapperAuthority,
depositTokenProgram,
wrappedTokenProgram,
},
});
let _userDepositTokenAccount = await depositMint.getAccountInfo(
userDepositTokenAccount
);
let _userWrappedTokenAccount = await wrappedMint.getAccountInfo(
userWrappedTokenAccount
);
assert.strictEqual(_userDepositTokenAccount.amount.toNumber(), 0);
assert.strictEqual(
_userWrappedTokenAccount.amount.toNumber(),
wrapAmount
);
});
it("Unwrap", async () => {
await program.rpc.unwrap(new BN(wrapAmount - 1), {
accounts: {
signer: provider.wallet.publicKey,
depositMint: depositMint.publicKey,
wrappedMint: wrappedMint.publicKey,
depositTokenVault: depositTokenVault,
userDepositTokenAccount,
userWrappedTokenAccount,
wrapperAuthority,
depositTokenProgram,
wrappedTokenProgram,
},
});
let _userDepositTokenAccount = await depositMint.getAccountInfo(
userDepositTokenAccount
);
let _userWrappedTokenAccount = await wrappedMint.getAccountInfo(
userWrappedTokenAccount
);
assert.strictEqual(
_userDepositTokenAccount.amount.toNumber(),
wrapAmount - 1
);
assert.strictEqual(_userWrappedTokenAccount.amount.toNumber(), 1);
});
});
});
});

View File

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