Просмотр исходного кода

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

Elliot Kennedy 2 лет назад
Родитель
Сommit
670b4f5005

+ 2 - 0
.github/workflows/reusable-tests.yaml

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

+ 1 - 0
CHANGELOG.md

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

+ 46 - 0
lang/derive/accounts/src/lib.rs

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

+ 11 - 0
lang/src/error.rs

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

+ 80 - 14
lang/syn/src/codegen/accounts/constraints.rs

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

+ 14 - 0
lang/syn/src/lib.rs

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

+ 123 - 10
lang/syn/src/parser/accounts/constraints.rs

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

+ 15 - 6
lang/syn/src/parser/accounts/mod.rs

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

+ 148 - 2
tests/misc/programs/misc-optional/src/context.rs

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

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

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

+ 151 - 3
tests/misc/programs/misc/src/context.rs

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

+ 42 - 0
tests/misc/programs/misc/src/lib.rs

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

Разница между файлами не показана из-за своего большого размера
+ 885 - 13
tests/misc/tests/misc/misc.ts


+ 1 - 0
tests/package.json

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

+ 17 - 0
tests/spl/token-wrapper/Anchor.toml

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

+ 4 - 0
tests/spl/token-wrapper/Cargo.toml

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

+ 19 - 0
tests/spl/token-wrapper/package.json

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

+ 21 - 0
tests/spl/token-wrapper/programs/token-wrapper/Cargo.toml

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

+ 2 - 0
tests/spl/token-wrapper/programs/token-wrapper/Xargo.toml

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

+ 309 - 0
tests/spl/token-wrapper/programs/token-wrapper/src/lib.rs

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

+ 219 - 0
tests/spl/token-wrapper/tests/wrapper.ts

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

+ 11 - 0
tests/spl/token-wrapper/tsconfig.json

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

Некоторые файлы не были показаны из-за большого количества измененных файлов