diff --git a/Cargo.lock b/Cargo.lock index 2c719ab0..df9d5c3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9346,7 +9346,6 @@ dependencies = [ "serde_json", "serde_with", "serial_test", - "solana-account", "solana-clap-v3-utils", "solana-cli-config", "solana-cli-output", @@ -9364,6 +9363,7 @@ dependencies = [ "solana-system-interface", "solana-test-validator", "solana-transaction", + "spl-associated-token-account-client", "spl-token", "spl-token-2022 7.0.0 (git+https://github.com/solana-program/token-2022?rev=00e0f4723c2606c0facbb4921e1b2e2e030d1fa6)", "spl-token-wrap", diff --git a/Cargo.toml b/Cargo.toml index 30145ff8..479a1f79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,6 +68,7 @@ solana-system-interface = { version = "1.0.0", features = ["bincode"] } solana-sysvar = "2.2.1" solana-test-validator = "2.2.1" solana-transaction = "2.2.1" +spl-associated-token-account-client = "2.0.0" spl-pod = "0.5.1" spl-tlv-account-resolution = "0.10.0" spl-token = { version = "7.0.0", features = ["no-entrypoint"] } diff --git a/clients/cli/Cargo.toml b/clients/cli/Cargo.toml index 7e0afdce..8e158061 100644 --- a/clients/cli/Cargo.toml +++ b/clients/cli/Cargo.toml @@ -13,7 +13,6 @@ clap = { workspace = true } serde = { workspace = true } serde_derive = { workspace = true } serde_with = { workspace = true } -solana-account = { workspace = true } solana-clap-v3-utils = { workspace = true } solana-cli-config = { workspace = true } solana-cli-output = { workspace = true } @@ -28,6 +27,7 @@ solana-signature = { workspace = true } solana-signer = { workspace = true } solana-system-interface = { workspace = true } solana-transaction = { workspace = true } +spl-associated-token-account-client = { workspace = true } spl-token = { workspace = true } spl-token-wrap = { workspace = true } spl-token-2022 = { workspace = true } diff --git a/clients/cli/src/cli.rs b/clients/cli/src/cli.rs index d8d7841a..3d5b4b3a 100644 --- a/clients/cli/src/cli.rs +++ b/clients/cli/src/cli.rs @@ -4,6 +4,7 @@ use { create_mint::{command_create_mint, CreateMintArgs}, find_pdas::{command_get_pdas, FindPdasArgs}, output::parse_output_format, + wrap::{command_wrap, WrapArgs}, CommandResult, }, clap::{ @@ -85,19 +86,21 @@ pub struct Cli { pub enum Command { /// Create a wrapped mint for a given SPL Token CreateMint(CreateMintArgs), + Wrap(WrapArgs), FindPdas(FindPdasArgs), - // TODO: Wrap, Unwrap + // TODO: Unwrap } impl Command { pub async fn execute( self, config: &Config, - _matches: &ArgMatches, - _wallet_manager: &mut Option>, + matches: &ArgMatches, + wallet_manager: &mut Option>, ) -> CommandResult { match self { Command::CreateMint(args) => command_create_mint(config, args).await, + Command::Wrap(args) => command_wrap(config, args, matches, wallet_manager).await, Command::FindPdas(args) => command_get_pdas(config, args).await, } } diff --git a/clients/cli/src/main.rs b/clients/cli/src/main.rs index 44f2abf0..8fd4c0ac 100644 --- a/clients/cli/src/main.rs +++ b/clients/cli/src/main.rs @@ -4,6 +4,7 @@ mod config; mod create_mint; mod find_pdas; mod output; +mod wrap; use { crate::{cli::Cli, config::Config}, diff --git a/clients/cli/src/wrap.rs b/clients/cli/src/wrap.rs new file mode 100644 index 00000000..4994682f --- /dev/null +++ b/clients/cli/src/wrap.rs @@ -0,0 +1,235 @@ +use { + crate::{ + common::{parse_pubkey, parse_token_program, process_transaction}, + config::Config, + output::{format_output, println_display}, + CommandResult, Error, + }, + clap::Args, + serde_derive::{Deserialize, Serialize}, + serde_with::{serde_as, DisplayFromStr}, + solana_cli_output::{display::writeln_name_value, QuietDisplay, VerboseDisplay}, + solana_client::nonblocking::rpc_client::RpcClient, + solana_pubkey::Pubkey, + solana_remote_wallet::remote_wallet::RemoteWalletManager, + solana_signature::Signature, + solana_signer::Signer, + solana_transaction::Transaction, + spl_associated_token_account_client::address::get_associated_token_address_with_program_id, + spl_token_2022::{extension::PodStateWithExtensions, pod::PodAccount}, + spl_token_wrap::{get_wrapped_mint_address, get_wrapped_mint_authority, instruction::wrap}, + std::{ + fmt::{Display, Formatter}, + rc::Rc, + sync::Arc, + }, +}; + +#[derive(Clone, Debug, Args)] +pub struct WrapArgs { + /// The address of the unwrapped token account to wrap from + #[clap(value_parser = parse_pubkey)] + pub unwrapped_token_account: Pubkey, + + /// The address of the escrow account that will hold the unwrapped tokens + #[clap(value_parser = parse_pubkey)] + pub escrow_account: Pubkey, + + /// The address of the token program that the wrapped mint should belong to + #[clap(value_parser = parse_token_program)] + pub wrapped_token_program: Pubkey, + + /// The amount of tokens to wrap + #[clap(value_parser)] + pub amount: u64, + + /// Path to the signer for the transfer authority if different from + /// fee payer + #[clap(long, value_name = "PATH")] + pub transfer_authority: Option, + + /// The address of the mint to wrap, queried if not provided + #[clap(long, value_parser = parse_pubkey)] + pub unwrapped_mint: Option, + + /// The address of the token account to receive wrapped tokens. + /// If not provided, defaults to fee payer associated token account + #[clap(long, value_parser = parse_pubkey)] + pub recipient_token_account: Option, + + /// The address of the token program that the unwrapped mint belongs to. + /// Queries account for `unwrapped_token_account` if not provided. + #[clap(long, value_parser = parse_token_program)] + pub unwrapped_token_program: Option, +} + +#[serde_as] +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WrapOutput { + #[serde_as(as = "DisplayFromStr")] + pub unwrapped_mint_address: Pubkey, + #[serde_as(as = "DisplayFromStr")] + pub wrapped_mint_address: Pubkey, + #[serde_as(as = "DisplayFromStr")] + pub unwrapped_token_account: Pubkey, + #[serde_as(as = "DisplayFromStr")] + pub recipient_token_account: Pubkey, + #[serde_as(as = "DisplayFromStr")] + pub escrow_account: Pubkey, + pub amount: u64, + #[serde_as(as = "Option")] + pub signature: Option, +} + +impl Display for WrapOutput { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + writeln_name_value( + f, + "Unwrapped mint address:", + &self.unwrapped_mint_address.to_string(), + )?; + writeln_name_value( + f, + "Wrapped mint address:", + &self.wrapped_mint_address.to_string(), + )?; + writeln_name_value( + f, + "Unwrapped token account:", + &self.unwrapped_token_account.to_string(), + )?; + writeln_name_value( + f, + "Recipient wrapped token account:", + &self.recipient_token_account.to_string(), + )?; + writeln_name_value(f, "Escrow account:", &self.escrow_account.to_string())?; + writeln_name_value(f, "Amount:", &self.amount.to_string())?; + if let Some(signature) = self.signature { + writeln_name_value(f, "Signature:", &signature.to_string())?; + } + Ok(()) + } +} + +impl QuietDisplay for WrapOutput { + fn write_str(&self, _: &mut dyn std::fmt::Write) -> std::fmt::Result { + Ok(()) + } +} +impl VerboseDisplay for WrapOutput {} + +async fn get_unwrapped_mint( + rpc_client: &RpcClient, + unwrapped_token_account: &Pubkey, +) -> Result { + let token_account_info = rpc_client.get_account(unwrapped_token_account).await?; + let unpacked_account = PodStateWithExtensions::::unpack(&token_account_info.data)?; + Ok(unpacked_account.base.mint) +} + +pub async fn command_wrap( + config: &Config, + args: WrapArgs, + matches: &clap::ArgMatches, + wallet_manager: &mut Option>, +) -> CommandResult { + let payer = config.fee_payer()?; + + let unwrapped_mint = if let Some(mint) = args.unwrapped_mint { + mint + } else { + get_unwrapped_mint(&config.rpc_client, &args.unwrapped_token_account).await? + }; + + println_display( + config, + format!( + "Wrapping {} tokens from mint {}", + args.amount, unwrapped_mint + ), + ); + + // Derive wrapped mint address and mint authority + let wrapped_mint_address = + get_wrapped_mint_address(&unwrapped_mint, &args.wrapped_token_program); + let wrapped_mint_authority = get_wrapped_mint_authority(&wrapped_mint_address); + + // If no recipient passed, get ATA of payer + let recipient_token_account = args.recipient_token_account.unwrap_or_else(|| { + get_associated_token_address_with_program_id( + &payer.pubkey(), + &wrapped_mint_address, + &args.wrapped_token_program, + ) + }); + + // If transfer_authority is provided, use it as a signer, + // else default to fee payer + let transfer_authority_signer = if let Some(authority_keypair_path) = &args.transfer_authority { + let signer = solana_clap_v3_utils::keypair::signer_from_path( + matches, + authority_keypair_path, + "transfer-authority", + wallet_manager, + ) + .map_err(|e| e.to_string())?; + Arc::from(signer) + } else { + payer.clone() + }; + + let unwrapped_token_program = if let Some(pubkey) = args.unwrapped_token_program { + pubkey + } else { + config + .rpc_client + .get_account(&args.unwrapped_token_account) + .await? + .owner + }; + + let instruction = wrap( + &spl_token_wrap::id(), + &recipient_token_account, + &wrapped_mint_address, + &wrapped_mint_authority, + &unwrapped_token_program, + &args.wrapped_token_program, + &args.unwrapped_token_account, + &unwrapped_mint, + &args.escrow_account, + &transfer_authority_signer.pubkey(), + &[], // TODO: Add multisig support + args.amount, + ); + + let latest_blockhash = config.rpc_client.get_latest_blockhash().await?; + let mut signers = vec![payer.as_ref()]; + + if payer.pubkey() != transfer_authority_signer.pubkey() { + signers.push(transfer_authority_signer.as_ref()); + } + + let transaction = Transaction::new_signed_with_payer( + &[instruction], + Some(&payer.pubkey()), + &signers, + latest_blockhash, + ); + + let signature = process_transaction(config, transaction).await?; + + let output = WrapOutput { + unwrapped_mint_address: unwrapped_mint, + wrapped_mint_address, + unwrapped_token_account: args.unwrapped_token_account, + recipient_token_account, + escrow_account: args.escrow_account, + amount: args.amount, + signature, + }; + + Ok(format_output(config, output)) +} diff --git a/clients/cli/tests/helpers.rs b/clients/cli/tests/helpers.rs index 2012faa7..a195c10f 100644 --- a/clients/cli/tests/helpers.rs +++ b/clients/cli/tests/helpers.rs @@ -1,27 +1,34 @@ +#![allow(dead_code)] + use { - solana_account::{Account, AccountSharedData}, solana_cli_config::Config as SolanaConfig, solana_client::nonblocking::rpc_client::RpcClient, solana_keypair::{write_keypair_file, Keypair}, solana_program_pack::Pack, solana_pubkey::Pubkey, - solana_sdk_ids::{bpf_loader_upgradeable, system_program}, + solana_sdk_ids::bpf_loader_upgradeable, solana_signer::Signer, solana_test_validator::{TestValidator, TestValidatorGenesis, UpgradeableProgramInfo}, - spl_token::{ - self, - solana_program::{program_option::COption, rent::Rent}, - }, - spl_token_wrap::{self}, - std::{path::PathBuf, sync::Arc}, + solana_transaction::Transaction, + spl_associated_token_account_client::address::get_associated_token_address_with_program_id, + spl_token::{self, instruction::initialize_mint, state::Mint as SplTokenMint}, + std::{path::PathBuf, process::Command, sync::Arc}, tempfile::NamedTempFile, }; pub const TOKEN_WRAP_CLI_BIN: &str = "../../target/debug/spl-token-wrap"; -const UNWRAPPED_TOKEN_PROGRAM: Pubkey = spl_token::id(); -const WRAPPED_TOKEN_PROGRAM: Pubkey = spl_token_2022::id(); -pub async fn start_validator(starting_accounts: Vec<(Pubkey, AccountSharedData)>) -> TestValidator { +pub struct TestEnv { + pub rpc_client: Arc, + pub payer: Keypair, + pub config_file_path: String, + // Persist these to keep them in scope + _validator: TestValidator, + _keypair_file: NamedTempFile, + _config_file: NamedTempFile, +} + +pub async fn start_validator() -> (TestValidator, Keypair) { solana_logger::setup(); let mut test_validator_genesis = TestValidatorGenesis::default(); @@ -32,32 +39,12 @@ pub async fn start_validator(starting_accounts: Vec<(Pubkey, AccountSharedData)> upgrade_authority: Pubkey::default(), }]); - test_validator_genesis.add_accounts(starting_accounts); - - test_validator_genesis.start_async().await.0 + test_validator_genesis.start_async().await } -pub struct Env { - pub rpc_client: Arc, - pub config_file_path: String, - pub unwrapped_mint: Pubkey, - pub unwrapped_token_program: Pubkey, - pub wrapped_token_program: Pubkey, - // Persist these to keep them in scope - _validator: TestValidator, - _keypair_file: NamedTempFile, - _config_file: NamedTempFile, -} - -pub async fn setup() -> Env { - // Setup starting accounts - let payer = Keypair::new(); - let unwrapped_mint = setup_mint(&payer.pubkey()); - let payer_account = AccountSharedData::new(1_000_000_000, 0, &system_program::id()); - let starting_accounts = vec![(payer.pubkey(), payer_account), unwrapped_mint.clone()]; - - // Start the test validator with necessary programs - let validator = start_validator(starting_accounts).await; +pub async fn setup_test_env() -> TestEnv { + let (validator, payer) = start_validator().await; + let rpc_client = Arc::new(validator.get_async_rpc_client()); // Write payer keypair to a temporary file let keypair_file = NamedTempFile::new().unwrap(); @@ -75,36 +62,220 @@ pub async fn setup() -> Env { }; solana_config.save(&config_file_path).unwrap(); - Env { - rpc_client: Arc::new(validator.get_async_rpc_client()), + TestEnv { + payer, + rpc_client, config_file_path, - unwrapped_mint: unwrapped_mint.0, - unwrapped_token_program: UNWRAPPED_TOKEN_PROGRAM, - wrapped_token_program: WRAPPED_TOKEN_PROGRAM, - _validator: validator, _keypair_file: keypair_file, _config_file: config_file, + _validator: validator, } } -pub fn setup_mint(mint_authority: &Pubkey) -> (Pubkey, AccountSharedData) { - let state = spl_token::state::Mint { - decimals: 8, - is_initialized: true, - supply: 1_000_000_000, - mint_authority: COption::Some(*mint_authority), - freeze_authority: COption::None, - }; - let mut data = vec![0u8; spl_token::state::Mint::LEN]; - state.pack_into_slice(&mut data); +pub async fn create_unwrapped_mint(env: &TestEnv, token_program_addr: &Pubkey) -> Pubkey { + let mint_account = Keypair::new(); + let rent = env + .rpc_client + .get_minimum_balance_for_rent_exemption(SplTokenMint::LEN) + .await + .unwrap(); - let lamports = Rent::default().minimum_balance(data.len()); + let blockhash = env.rpc_client.get_latest_blockhash().await.unwrap(); - let account = Account { - lamports, - data, - owner: UNWRAPPED_TOKEN_PROGRAM, - ..Default::default() - }; - (Pubkey::new_unique(), account.into()) + let transaction = Transaction::new_signed_with_payer( + &[ + solana_system_interface::instruction::create_account( + &env.payer.pubkey(), + &mint_account.pubkey(), + rent, + SplTokenMint::LEN as u64, + token_program_addr, + ), + initialize_mint( + token_program_addr, + &mint_account.pubkey(), + &env.payer.pubkey(), + None, + 9, + ) + .unwrap(), + ], + Some(&env.payer.pubkey()), + &[env.payer.insecure_clone(), mint_account.insecure_clone()], + blockhash, + ); + + env.rpc_client + .send_and_confirm_transaction(&transaction) + .await + .unwrap(); + mint_account.pubkey() +} + +pub async fn execute_create_mint( + env: &TestEnv, + unwrapped_mint: &Pubkey, + unwrapped_token_program: &Pubkey, + wrapped_token_program: &Pubkey, +) { + let status = Command::new(TOKEN_WRAP_CLI_BIN) + .args([ + "create-mint", + "-C", + &env.config_file_path, + &unwrapped_mint.to_string(), + &unwrapped_token_program.to_string(), + &wrapped_token_program.to_string(), + "--idempotent", + ]) + .status() + .unwrap(); + assert!(status.success()); +} + +#[allow(clippy::too_many_arguments)] +pub async fn execute_wrap( + env: &TestEnv, + unwrapped_token_account: &Pubkey, + escrow_account: &Pubkey, + wrapped_token_program: &Pubkey, + amount: u64, + unwrapped_token_program: Option<&Pubkey>, + mint_address: Option<&Pubkey>, + recipient_account: Option<&Pubkey>, +) { + let mut args = vec![ + "wrap".to_string(), + "-C".to_string(), + env.config_file_path.clone(), + unwrapped_token_account.to_string(), + escrow_account.to_string(), + wrapped_token_program.to_string(), + amount.to_string(), + ]; + + if let Some(program) = unwrapped_token_program { + args.push("--unwrapped-token-program".to_string()); + args.push(program.to_string()); + } + + if let Some(mint) = mint_address { + args.push("--unwrapped-mint".to_string()); + args.push(mint.to_string()); + } + + if let Some(recipient) = recipient_account { + args.push("--recipient-token-account".to_string()); + args.push(recipient.to_string()); + } + + let status = Command::new(TOKEN_WRAP_CLI_BIN) + .args(args) + .status() + .unwrap(); + assert!(status.success()); +} + +pub async fn create_associated_token_account( + env: &TestEnv, + token_program: &Pubkey, + mint: &Pubkey, +) -> Pubkey { + let ata = + get_associated_token_address_with_program_id(&env.payer.pubkey(), mint, token_program); + + let ata_account = env.rpc_client.get_account(&ata).await; + if ata_account.is_ok() { + return ata; // Return early if it exists + } + + let instruction = + spl_associated_token_account_client::instruction::create_associated_token_account( + &env.payer.pubkey(), + &env.payer.pubkey(), + mint, + token_program, + ); + + let tx = Transaction::new_signed_with_payer( + &[instruction], + Some(&env.payer.pubkey()), + &[&env.payer], + env.rpc_client.get_latest_blockhash().await.unwrap(), + ); + + env.rpc_client + .send_and_confirm_transaction(&tx) + .await + .unwrap(); + + ata +} + +pub async fn create_token_account( + env: &TestEnv, + token_program: &Pubkey, + mint: &Pubkey, + owner: &Pubkey, +) -> Pubkey { + let token_account = Keypair::new(); + let account_size = spl_token::state::Account::LEN; + + let tx = Transaction::new_signed_with_payer( + &[ + solana_system_interface::instruction::create_account( + &env.payer.pubkey(), + &token_account.pubkey(), + env.rpc_client + .get_minimum_balance_for_rent_exemption(account_size) + .await + .unwrap(), + account_size as u64, + token_program, + ), + spl_token_2022::instruction::initialize_account( + token_program, + &token_account.pubkey(), + mint, + owner, + ) + .unwrap(), + ], + Some(&env.payer.pubkey()), + &[&env.payer, &token_account], + env.rpc_client.get_latest_blockhash().await.unwrap(), + ); + env.rpc_client + .send_and_confirm_transaction(&tx) + .await + .unwrap(); + + token_account.pubkey() +} + +pub async fn mint_to( + env: &TestEnv, + token_program: &Pubkey, + mint: &Pubkey, + token_account: &Pubkey, + amount: u64, +) { + let tx = Transaction::new_signed_with_payer( + &[spl_token::instruction::mint_to( + token_program, + mint, + token_account, + &env.payer.pubkey(), + &[&env.payer.pubkey()], + amount, + ) + .unwrap()], + Some(&env.payer.pubkey()), + &[&env.payer], + env.rpc_client.get_latest_blockhash().await.unwrap(), + ); + env.rpc_client + .send_and_confirm_transaction(&tx) + .await + .unwrap(); } diff --git a/clients/cli/tests/test_create_mint.rs b/clients/cli/tests/test_create_mint.rs index 9c111df5..4ee06e4e 100644 --- a/clients/cli/tests/test_create_mint.rs +++ b/clients/cli/tests/test_create_mint.rs @@ -1,5 +1,5 @@ use { - crate::helpers::{setup, TOKEN_WRAP_CLI_BIN}, + crate::helpers::{create_unwrapped_mint, execute_create_mint, setup_test_env}, serial_test::serial, solana_program_pack::Pack, spl_token::{self, state::Mint as SplTokenMint}, @@ -7,7 +7,6 @@ use { spl_token_wrap::{ self, get_wrapped_mint_address, get_wrapped_mint_backpointer_address, state::Backpointer, }, - std::process::Command, }; mod helpers; @@ -15,24 +14,21 @@ mod helpers; #[tokio::test] #[serial] async fn test_create_mint() { - let env = setup().await; - let status = Command::new(TOKEN_WRAP_CLI_BIN) - .args([ - "create-mint", - "-C", - &env.config_file_path, - &env.unwrapped_mint.to_string(), - &env.unwrapped_token_program.to_string(), - &env.wrapped_token_program.to_string(), - "--idempotent", - ]) - .status() - .unwrap(); - assert!(status.success()); + let env = setup_test_env().await; + + let unwrapped_token_program = spl_token::id(); + let wrapped_token_program = spl_token_2022::id(); + let unwrapped_mint = create_unwrapped_mint(&env, &unwrapped_token_program).await; + execute_create_mint( + &env, + &unwrapped_mint, + &unwrapped_token_program, + &wrapped_token_program, + ) + .await; // Derive expected account addresses - let wrapped_mint_address = - get_wrapped_mint_address(&env.unwrapped_mint, &env.wrapped_token_program); + let wrapped_mint_address = get_wrapped_mint_address(&unwrapped_mint, &wrapped_token_program); let backpointer_address = get_wrapped_mint_backpointer_address(&wrapped_mint_address); // Fetch created accounts @@ -48,20 +44,16 @@ async fn test_create_mint() { .unwrap(); // Verify owners - assert_eq!(wrapped_mint_account.owner, env.wrapped_token_program); + assert_eq!(wrapped_mint_account.owner, wrapped_token_program); assert_eq!(backpointer_account.owner, spl_token_wrap::id()); // Verify mint properties - let unwrapped_mint_account = env - .rpc_client - .get_account(&env.unwrapped_mint) - .await - .unwrap(); + let unwrapped_mint_account = env.rpc_client.get_account(&unwrapped_mint).await.unwrap(); let unwrapped_mint_data = SplTokenMint::unpack(&unwrapped_mint_account.data).unwrap(); let wrapped_mint_data = SplToken2022Mint::unpack(&wrapped_mint_account.data).unwrap(); assert_eq!(wrapped_mint_data.decimals, unwrapped_mint_data.decimals); // Verify backpointer data let backpointer = *bytemuck::from_bytes::(&backpointer_account.data); - assert_eq!(backpointer.unwrapped_mint, env.unwrapped_mint); + assert_eq!(backpointer.unwrapped_mint, unwrapped_mint); } diff --git a/clients/cli/tests/test_wrap.rs b/clients/cli/tests/test_wrap.rs new file mode 100644 index 00000000..9bcdd590 --- /dev/null +++ b/clients/cli/tests/test_wrap.rs @@ -0,0 +1,210 @@ +use { + crate::helpers::{ + create_associated_token_account, create_token_account, create_unwrapped_mint, + execute_create_mint, execute_wrap, mint_to, setup_test_env, TestEnv, + }, + serial_test::serial, + solana_pubkey::Pubkey, + solana_signer::Signer, + spl_token::{self}, + spl_token_2022::{extension::PodStateWithExtensions, pod::PodAccount}, + spl_token_wrap::{get_wrapped_mint_address, get_wrapped_mint_authority}, +}; + +mod helpers; + +#[tokio::test] +#[serial] +async fn test_wrap_single_signer_with_defaults() { + let env = setup_test_env().await; + + // Create Mint + let unwrapped_token_program = spl_token::id(); + let wrapped_token_program = spl_token_2022::id(); + let unwrapped_mint = create_unwrapped_mint(&env, &unwrapped_token_program).await; + execute_create_mint( + &env, + &unwrapped_mint, + &unwrapped_token_program, + &wrapped_token_program, + ) + .await; + + // Fund initial unwrapped token account + let unwrapped_token_account = create_token_account( + &env, + &unwrapped_token_program, + &unwrapped_mint, + &env.payer.pubkey(), + ) + .await; + let starting_amount = 100; + mint_to( + &env, + &unwrapped_token_program, + &unwrapped_mint, + &unwrapped_token_account, + starting_amount, + ) + .await; + + // Setup recipient account with zero balance + let wrapped_mint = get_wrapped_mint_address(&unwrapped_mint, &wrapped_token_program); + let recipient_account = + create_associated_token_account(&env, &wrapped_token_program, &wrapped_mint).await; + + // Setup escrow with mint_authority as owner + let wrapped_mint_authority = get_wrapped_mint_authority(&wrapped_mint); + let escrow_account = create_token_account( + &env, + &unwrapped_token_program, + &unwrapped_mint, + &wrapped_mint_authority, + ) + .await; + + // Execute wrap instruction + let unwrap_amount = 50; + execute_wrap( + &env, + &unwrapped_token_account, + &escrow_account, + &wrapped_token_program, + unwrap_amount, + None, + None, + None, + ) + .await; + + assert_result( + env, + &unwrapped_token_account, + starting_amount, + &recipient_account, + &escrow_account, + unwrap_amount, + ) + .await; +} + +#[tokio::test] +#[serial] +async fn test_wrap_single_signer_with_optional_flags() { + let env = setup_test_env().await; + + // Create Mint + let unwrapped_token_program = spl_token::id(); + let wrapped_token_program = spl_token_2022::id(); + let unwrapped_mint = create_unwrapped_mint(&env, &unwrapped_token_program).await; + execute_create_mint( + &env, + &unwrapped_mint, + &unwrapped_token_program, + &wrapped_token_program, + ) + .await; + + // Fund initial unwrapped token account + let unwrapped_token_account = create_token_account( + &env, + &unwrapped_token_program, + &unwrapped_mint, + &env.payer.pubkey(), + ) + .await; + let starting_amount = 100; + mint_to( + &env, + &unwrapped_token_program, + &unwrapped_mint, + &unwrapped_token_account, + starting_amount, + ) + .await; + + // Setup recipient account with zero balance + // This time it is not an ATA, but a fresh token account + let wrapped_mint = get_wrapped_mint_address(&unwrapped_mint, &wrapped_token_program); + let recipient_account = create_token_account( + &env, + &wrapped_token_program, + &wrapped_mint, + &env.payer.pubkey(), + ) + .await; + + // Setup escrow with mint_authority as owner + let wrapped_mint_authority = get_wrapped_mint_authority(&wrapped_mint); + let escrow_account = create_token_account( + &env, + &unwrapped_token_program, + &unwrapped_mint, + &wrapped_mint_authority, + ) + .await; + + // Execute wrap instruction + let unwrap_amount = 50; + execute_wrap( + &env, + &unwrapped_token_account, + &escrow_account, + &wrapped_token_program, + unwrap_amount, + Some(&unwrapped_token_program), + Some(&unwrapped_mint), + Some(&recipient_account), + ) + .await; + + assert_result( + env, + &unwrapped_token_account, + starting_amount, + &recipient_account, + &escrow_account, + unwrap_amount, + ) + .await; +} + +async fn assert_result( + env: TestEnv, + unwrapped_token_account: &Pubkey, + starting_amount: u64, + recipient_account: &Pubkey, + escrow_account: &Pubkey, + unwrap_amount: u64, +) { + let unwrapped_account_data = env + .rpc_client + .get_account_data(unwrapped_token_account) + .await + .unwrap(); + let unwrapped_token_state = + PodStateWithExtensions::::unpack(&unwrapped_account_data).unwrap(); + + // Unwrapped token account should be lower + assert_eq!( + u64::from(unwrapped_token_state.base.amount), + starting_amount.checked_sub(unwrap_amount).unwrap() + ); + + // Escrow account should have the tokens + let escrow_account_data = env + .rpc_client + .get_account_data(escrow_account) + .await + .unwrap(); + let escrow_token_state = + PodStateWithExtensions::::unpack(&escrow_account_data).unwrap(); + assert_eq!(u64::from(escrow_token_state.base.amount), unwrap_amount); + + // Recipient should have wrapped tokens + let wrapped_account = env.rpc_client.get_account(recipient_account).await.unwrap(); + assert_eq!(wrapped_account.owner, spl_token_2022::id()); + let wrapped_token_state = + PodStateWithExtensions::::unpack(&wrapped_account.data).unwrap(); + assert_eq!(u64::from(wrapped_token_state.base.amount), unwrap_amount); +}