diff --git a/.github/badges/coverage.json b/.github/badges/coverage.json new file mode 100644 index 00000000..c512d63e --- /dev/null +++ b/.github/badges/coverage.json @@ -0,0 +1 @@ +{"schemaVersion": 1, "label": "coverage", "message": "86.3%", "color": "green"} diff --git a/crates/lib/src/oracle/jupiter.rs b/crates/lib/src/oracle/jupiter.rs index b0655dc0..0bc6e79e 100644 --- a/crates/lib/src/oracle/jupiter.rs +++ b/crates/lib/src/oracle/jupiter.rs @@ -163,10 +163,11 @@ mod tests { use mockito::{Matcher, Server}; #[tokio::test] - async fn test_jupiter_price_fetch() { - // No API key + async fn test_jupiter_price_fetch_comprehensive() { + // Test case 1: No API key - should use lite API { let mut api_key_guard = GLOBAL_JUPITER_API_KEY.write(); + *api_key_guard = None; } @@ -185,7 +186,7 @@ mod tests { } }"#; let mut server = Server::new_async().await; - let _m = server + let _m1 = server .mock("GET", "/price/v3") .match_query(Matcher::Any) .with_status(200) @@ -194,38 +195,23 @@ mod tests { .create(); let client = Client::new(); - // Test without API key - should use lite API let mut oracle = JupiterPriceOracle::new(); oracle.lite_api_url = format!("{}/price/v3", server.url()); let result = oracle.get_price(&client, "So11111111111111111111111111111111111111112").await; - assert!(result.is_ok()); let price = result.unwrap(); assert_eq!(price.price, 1.0); assert_eq!(price.source, PriceSource::Jupiter); - // With API key + // Test case 2: With API key - should use pro API { let mut api_key_guard = GLOBAL_JUPITER_API_KEY.write(); *api_key_guard = Some("test-api-key".to_string()); } - let mock_response = r#"{ - "So11111111111111111111111111111111111111112": { - "usdPrice": 100.0, - "blockId": 12345, - "decimals": 9, - "priceChange24h": 2.5 - }, - "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN": { - "usdPrice": 0.532, - "blockId": 12345, - "decimals": 6, - "priceChange24h": -1.2 - } - }"#; - let mut server = Server::new_async().await; - let _m = server + + let mut server2 = Server::new_async().await; + let _m2 = server2 .mock("GET", "/price/v3") .match_header("x-api-key", "test-api-key") .match_query(Matcher::Any) @@ -234,28 +220,23 @@ mod tests { .with_body(mock_response) .create(); - let client = Client::new(); - // Test with API key - should use pro API - let mut oracle = JupiterPriceOracle::new(); - oracle.pro_api_url = format!("{}/price/v3", server.url()); - - let result = oracle.get_price(&client, "So11111111111111111111111111111111111111112").await; + let mut oracle2 = JupiterPriceOracle::new(); + oracle2.pro_api_url = format!("{}/price/v3", server2.url()); + let result = + oracle2.get_price(&client, "So11111111111111111111111111111111111111112").await; assert!(result.is_ok()); let price = result.unwrap(); assert_eq!(price.price, 1.0); assert_eq!(price.source, PriceSource::Jupiter); - } - #[tokio::test] - async fn test_jupiter_price_fetch_when_no_price_data() { - // No API key + // Test case 3: No price data available - should return error { let mut api_key_guard = GLOBAL_JUPITER_API_KEY.write(); *api_key_guard = None; } - let mock_response = r#"{ + let no_price_response = r#"{ "So11111111111111111111111111111111111111112": { "usdPrice": 100.0, "blockId": 12345, @@ -263,26 +244,24 @@ mod tests { "priceChange24h": 2.5 } }"#; - let mut server = Server::new_async().await; - let _m = server + let mut server3 = Server::new_async().await; + let _m3 = server3 .mock("GET", "/price/v3") .match_query(Matcher::Any) .with_status(200) .with_header("content-type", "application/json") - .with_body(mock_response) + .with_body(no_price_response) .create(); - let client = Client::new(); - // Test without API key - should use lite API - let mut oracle = JupiterPriceOracle::new(); - oracle.lite_api_url = format!("{}/price/v3", server.url()); - - let result = oracle.get_price(&client, "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN").await; + let mut oracle3 = JupiterPriceOracle::new(); + oracle3.lite_api_url = format!("{}/price/v3", server3.url()); + let result = + oracle3.get_price(&client, "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN").await; assert!(result.is_err()); assert_eq!( result.err(), Some(KoraError::RpcError("No price data from Jupiter".to_string())) - ) + ); } } diff --git a/crates/lib/src/transaction/versioned_transaction.rs b/crates/lib/src/transaction/versioned_transaction.rs index fc066b47..7ca2b1b3 100644 --- a/crates/lib/src/transaction/versioned_transaction.rs +++ b/crates/lib/src/transaction/versioned_transaction.rs @@ -146,9 +146,7 @@ impl VersionedTransactionResolved { .map_err(|e| KoraError::RpcError(format!("Failed to simulate transaction: {e}")))?; if let Some(err) = simulation_result.value.err { - log::warn!( - "Transaction simulation failed: {err}, continuing without inner instructions", - ); + log::warn!("Transaction simulation failed: {err}"); return Err(KoraError::InvalidTransaction( "Transaction inner instructions fetching failed.".to_string(), )); diff --git a/crates/lib/src/validator/config_validator.rs b/crates/lib/src/validator/config_validator.rs index 35fa306d..c183b669 100644 --- a/crates/lib/src/validator/config_validator.rs +++ b/crates/lib/src/validator/config_validator.rs @@ -680,7 +680,6 @@ mod tests { let _ = update_config(config); - let mock_account = create_mock_program_account(); let rpc_client = RpcMockBuilder::new().build(); let result = ConfigValidator::validate_with_result(&rpc_client, true).await; diff --git a/makefiles/RUST_TESTS.makefile b/makefiles/RUST_TESTS.makefile index d4397319..1bfac950 100644 --- a/makefiles/RUST_TESTS.makefile +++ b/makefiles/RUST_TESTS.makefile @@ -29,6 +29,13 @@ test-regular: $(call run_integration_phase,1,Regular Tests,$(REGULAR_CONFIG),,--test rpc,) @$(call stop_solana_validator) +test-token: + $(call print_header,TOKEN TESTS) + @$(call start_solana_validator) + @cargo run -p tests --bin setup_test_env $(QUIET_OUTPUT) + $(call run_integration_phase,1,Tokens Tests,$(REGULAR_CONFIG),,--test tokens,) + @$(call stop_solana_validator) + test-auth: $(call print_header,AUTHENTICATION TESTS) @$(call start_solana_validator) diff --git a/tests/src/common/constants.rs b/tests/src/common/constants.rs index b9b53c39..f30bfac8 100644 --- a/tests/src/common/constants.rs +++ b/tests/src/common/constants.rs @@ -30,6 +30,14 @@ pub const SENDER_KEYPAIR_PATH: &str = pub const USDC_MINT_KEYPAIR_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/src/common/local-keys/usdc-mint-local.json"); +/// USDC mint 2022 keypair path (local testing only) +pub const USDC_MINT_2022_KEYPAIR_PATH: &str = + concat!(env!("CARGO_MANIFEST_DIR"), "/src/common/local-keys/usdc-mint-2022-local.json"); + +/// Interest bearing mint keypair path (local testing only) +pub const INTEREST_BEARING_MINT_KEYPAIR_PATH: &str = + concat!(env!("CARGO_MANIFEST_DIR"), "/src/common/local-keys/mint-2022-interest-bearing.json"); + /// Second signer keypair path (for multi-signer tests) pub const SIGNER2_KEYPAIR_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/src/common/local-keys/signer2-local.json"); diff --git a/tests/src/common/extension_helpers.rs b/tests/src/common/extension_helpers.rs new file mode 100644 index 00000000..5cd61e51 --- /dev/null +++ b/tests/src/common/extension_helpers.rs @@ -0,0 +1,168 @@ +use anyhow::Result; +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_sdk::{ + pubkey::Pubkey, + signature::{Keypair, Signer}, + transaction::Transaction, +}; +use spl_token_2022::{ + extension::{interest_bearing_mint::instruction::initialize, ExtensionType}, + instruction as token_2022_instruction, + state::{Account as Token2022Account, Mint as Token2022Mint}, +}; +use std::sync::Arc; + +use crate::common::USDCMintTestHelper; + +/// Helper functions for creating Token 2022 accounts with specific extensions for testing +pub struct ExtensionHelpers; + +impl ExtensionHelpers { + /// Create a mint with InterestBearingConfig extension + pub async fn create_mint_with_interest_bearing( + rpc_client: &Arc, + payer: &Keypair, + mint_keypair: &Keypair, + ) -> Result<()> { + if (rpc_client.get_account(&mint_keypair.pubkey()).await).is_ok() { + return Ok(()); + } + + let decimals = USDCMintTestHelper::get_test_usdc_mint_decimals(); + + let space = ExtensionType::try_calculate_account_len::(&[ + ExtensionType::InterestBearingConfig, + ])?; + + let rent = rpc_client.get_minimum_balance_for_rent_exemption(space).await?; + + let create_account_instruction = solana_sdk::system_instruction::create_account( + &payer.pubkey(), + &mint_keypair.pubkey(), + rent, + space as u64, + &spl_token_2022::id(), + ); + + let initialize_interest_bearing_instruction = + initialize(&spl_token_2022::id(), &mint_keypair.pubkey(), Some(payer.pubkey()), 10)?; + + let initialize_mint_instruction = token_2022_instruction::initialize_mint2( + &spl_token_2022::id(), + &mint_keypair.pubkey(), + &payer.pubkey(), + Some(&payer.pubkey()), + decimals, + )?; + + let recent_blockhash = rpc_client.get_latest_blockhash().await?; + + let transaction = Transaction::new_signed_with_payer( + &[ + create_account_instruction, + initialize_interest_bearing_instruction, + initialize_mint_instruction, + ], + Some(&payer.pubkey()), + &[payer, mint_keypair], + recent_blockhash, + ); + + rpc_client.send_and_confirm_transaction(&transaction).await?; + Ok(()) + } + + /// Create a manual token account with MemoTransfer extension + pub async fn create_token_account_with_memo_transfer( + rpc_client: &Arc, + payer: &Keypair, + token_account_keypair: &Keypair, + mint: &Pubkey, + owner: &Keypair, + ) -> Result<()> { + if (rpc_client.get_account(&token_account_keypair.pubkey()).await).is_ok() { + return Ok(()); + } + + // Calculate space for token accounts with MemoTransfer extension + // Also include TransferFeeAmount if the mint has TransferFeeConfig + // (The USDC mint 2022 has TransferFeeConfig, so we need to account for it) + let account_space = ExtensionType::try_calculate_account_len::(&[ + ExtensionType::MemoTransfer, + ExtensionType::TransferFeeAmount, + ])?; + let rent = rpc_client.get_minimum_balance_for_rent_exemption(account_space).await?; + + let create_account_instruction = solana_sdk::system_instruction::create_account( + &payer.pubkey(), + &token_account_keypair.pubkey(), + rent, + account_space as u64, + &spl_token_2022::id(), + ); + + // Initialize MemoTransfer account extension (requires memo for transfers) + let initialize_memo_transfer_instruction = + spl_token_2022::extension::memo_transfer::instruction::enable_required_transfer_memos( + &spl_token_2022::id(), + &token_account_keypair.pubkey(), + &owner.pubkey(), + &[&owner.pubkey()], + )?; + + let initialize_account_instruction = token_2022_instruction::initialize_account3( + &spl_token_2022::id(), + &token_account_keypair.pubkey(), + mint, + &owner.pubkey(), + )?; + + let recent_blockhash = rpc_client.get_latest_blockhash().await?; + let transaction = Transaction::new_signed_with_payer( + &[ + create_account_instruction, + initialize_account_instruction, + initialize_memo_transfer_instruction, + ], + Some(&payer.pubkey()), + &[payer, token_account_keypair, owner], + recent_blockhash, + ); + + rpc_client.send_and_confirm_transaction(&transaction).await?; + Ok(()) + } + + pub async fn mint_tokens_to_account( + rpc_client: &Arc, + payer: &Keypair, + mint: &Pubkey, + token_account: &Pubkey, + mint_authority: &Keypair, + amount: Option, + ) -> Result<()> { + let amount = amount.unwrap_or_else(|| { + 1_000_000 * 10_u64.pow(USDCMintTestHelper::get_test_usdc_mint_decimals() as u32) + }); + + let instruction = token_2022_instruction::mint_to( + &spl_token_2022::id(), + mint, + token_account, + &mint_authority.pubkey(), + &[], + amount, + )?; + + let recent_blockhash = rpc_client.get_latest_blockhash().await?; + let transaction = Transaction::new_signed_with_payer( + &[instruction], + Some(&payer.pubkey()), + &[payer, mint_authority], + recent_blockhash, + ); + + rpc_client.send_and_confirm_transaction(&transaction).await?; + Ok(()) + } +} diff --git a/tests/src/common/fixtures/kora-test.toml b/tests/src/common/fixtures/kora-test.toml index 454015f5..4b35fe3e 100644 --- a/tests/src/common/fixtures/kora-test.toml +++ b/tests/src/common/fixtures/kora-test.toml @@ -35,15 +35,28 @@ allowed_programs = [ ] allowed_tokens = [ "9BgeTKqmFsPVnfYscfM6NvsgmZxei7XfdciShQ6D3bxJ", # Test USDC mint for local testing + "95kSi2m5MDiKAs8bucgzengMTP5M5FiQnJps9duYcmfG", # Test USDC mint 2022 for local testing + "AtCGtK6HPgdpk2c2LcpZimbH8dtHXYmJdoKsawWNCh2m", # Test Interest Bearing mint 2022 for local testing ] allowed_spl_paid_tokens = [ "9BgeTKqmFsPVnfYscfM6NvsgmZxei7XfdciShQ6D3bxJ", # Test USDC mint for local testing + "95kSi2m5MDiKAs8bucgzengMTP5M5FiQnJps9duYcmfG", # Test USDC mint 2022 for local testing + "AtCGtK6HPgdpk2c2LcpZimbH8dtHXYmJdoKsawWNCh2m", # Test Interest Bearing mint 2022 for local testing ] disallowed_accounts = [ "hndXZGK45hCxfBYvxejAXzCfCujoqkNf7rk4sTB8pek", # Test disallowed account for lookup table ] +# Block specific extensions for testing (only affects extension test accounts) +[validation.token_2022] +blocked_mint_extensions = [ + "interest_bearing_config", # Block mints with interest bearing config for extension testing +] +blocked_account_extensions = [ + "memo_transfer", # Block token accounts with MemoTransfer extension for extension testing +] + [validation.fee_payer_policy] allow_sol_transfers = true allow_spl_transfers = true diff --git a/tests/src/common/helpers.rs b/tests/src/common/helpers.rs index f0b80dd2..2b4dea39 100644 --- a/tests/src/common/helpers.rs +++ b/tests/src/common/helpers.rs @@ -18,6 +18,11 @@ pub struct TestAccountInfo { pub sender_token_account: Pubkey, pub recipient_token_account: Pubkey, pub fee_payer_token_account: Pubkey, + // Token 2022 fields + pub usdc_mint_2022_pubkey: Pubkey, + pub sender_token_2022_account: Pubkey, + pub recipient_token_2022_account: Pubkey, + pub fee_payer_token_2022_account: Pubkey, } /// Helper function to parse a private key string in multiple formats. @@ -93,3 +98,37 @@ impl USDCMintTestHelper { .unwrap_or(TEST_USDC_MINT_DECIMALS) } } + +pub struct USDCMint2022TestHelper; + +impl USDCMint2022TestHelper { + pub fn get_test_usdc_mint_2022_keypair() -> Keypair { + dotenv::dotenv().ok(); + let mint_keypair = match std::env::var("TEST_USDC_MINT_2022_KEYPAIR") { + Ok(key) => key, + Err(_) => std::fs::read_to_string(USDC_MINT_2022_KEYPAIR_PATH) + .expect("Failed to read USDC mint 2022 private key file"), + }; + parse_private_key_string(&mint_keypair) + .expect("Failed to parse test USDC mint 2022 private key") + } + + pub fn get_test_usdc_mint_2022_pubkey() -> Pubkey { + Self::get_test_usdc_mint_2022_keypair().pubkey() + } + + pub fn get_test_interest_bearing_mint_keypair() -> Keypair { + dotenv::dotenv().ok(); + let mint_keypair = match std::env::var("TEST_INTEREST_BEARING_MINT_KEYPAIR") { + Ok(key) => key, + Err(_) => std::fs::read_to_string(INTEREST_BEARING_MINT_KEYPAIR_PATH) + .expect("Failed to read interest bearing mint private key file"), + }; + parse_private_key_string(&mint_keypair) + .expect("Failed to parse test interest bearing mint private key") + } + + pub fn get_test_interest_bearing_mint_pubkey() -> Pubkey { + Self::get_test_interest_bearing_mint_keypair().pubkey() + } +} diff --git a/tests/src/common/local-keys/mint-2022-interest-bearing.json b/tests/src/common/local-keys/mint-2022-interest-bearing.json new file mode 100644 index 00000000..f69da225 --- /dev/null +++ b/tests/src/common/local-keys/mint-2022-interest-bearing.json @@ -0,0 +1 @@ +[78,63,83,18,2,4,230,26,94,142,178,78,179,23,4,195,116,211,25,214,9,31,129,226,122,194,246,64,215,104,16,86,146,214,141,14,252,8,190,24,86,19,255,157,82,235,44,165,148,6,30,152,175,62,180,176,51,161,19,28,22,83,141,46] \ No newline at end of file diff --git a/tests/src/common/local-keys/usdc-mint-2022-local.json b/tests/src/common/local-keys/usdc-mint-2022-local.json new file mode 100644 index 00000000..4d8d1a95 --- /dev/null +++ b/tests/src/common/local-keys/usdc-mint-2022-local.json @@ -0,0 +1 @@ +[88,238,182,173,120,156,180,249,148,252,218,221,159,84,55,159,191,243,121,129,173,14,168,89,104,182,108,47,177,47,225,222,120,20,240,168,32,222,220,197,136,143,185,82,29,124,70,158,138,58,140,15,125,198,38,156,255,86,143,167,1,165,96,195] \ No newline at end of file diff --git a/tests/src/common/mod.rs b/tests/src/common/mod.rs index 34f7846c..d1a0d719 100644 --- a/tests/src/common/mod.rs +++ b/tests/src/common/mod.rs @@ -3,6 +3,7 @@ pub mod assertions; pub mod auth_helpers; pub mod client; pub mod constants; +pub mod extension_helpers; pub mod helpers; pub mod lookup_tables; pub mod setup; @@ -11,6 +12,7 @@ pub mod transaction; // Re-export commonly used items for convenience pub use assertions::{JsonRpcErrorCodes, RpcAssertions, TransactionAssertions}; pub use client::{TestClient, TestContext}; +pub use extension_helpers::ExtensionHelpers; pub use transaction::{TransactionBuilder, TransactionVersion}; // Re-export auth helpers (excluding constants that are in constants.rs) @@ -21,7 +23,7 @@ pub use auth_helpers::{ // Re-export helpers (excluding constants that are in constants.rs) pub use helpers::{ parse_private_key_string, FeePayerTestHelper, RecipientTestHelper, SenderTestHelper, - TestAccountInfo, USDCMintTestHelper, + TestAccountInfo, USDCMint2022TestHelper, USDCMintTestHelper, }; pub use lookup_tables::{LookupTableHelper, LookupTablesAddresses}; diff --git a/tests/src/common/setup.rs b/tests/src/common/setup.rs index efe20410..c2baf2ae 100644 --- a/tests/src/common/setup.rs +++ b/tests/src/common/setup.rs @@ -8,13 +8,20 @@ use solana_sdk::{ signature::{Keypair, Signer}, transaction::Transaction, }; -use spl_associated_token_account::get_associated_token_address; +use spl_associated_token_account::{ + get_associated_token_address, get_associated_token_address_with_program_id, +}; use spl_token::instruction as token_instruction; +use spl_token_2022::{ + extension::{transfer_fee::instruction::initialize_transfer_fee_config, ExtensionType}, + instruction as token_2022_instruction, + state::Mint as Token2022Mint, +}; use std::sync::Arc; use crate::common::{ FeePayerTestHelper, LookupTableHelper, RecipientTestHelper, SenderTestHelper, TestAccountInfo, - USDCMintTestHelper, DEFAULT_RPC_URL, + USDCMint2022TestHelper, USDCMintTestHelper, DEFAULT_RPC_URL, }; /// Test account setup utilities for local validator @@ -24,6 +31,7 @@ pub struct TestAccountSetup { pub fee_payer_keypair: Keypair, pub recipient_pubkey: Pubkey, pub usdc_mint: Keypair, + pub usdc_mint_2022: Keypair, } impl TestAccountSetup { @@ -49,14 +57,23 @@ impl TestAccountSetup { let fee_payer_keypair = FeePayerTestHelper::get_fee_payer_keypair(); let usdc_mint = USDCMintTestHelper::get_test_usdc_mint_keypair(); - - Self { rpc_client, sender_keypair, fee_payer_keypair, recipient_pubkey, usdc_mint } + let usdc_mint_2022 = USDCMint2022TestHelper::get_test_usdc_mint_2022_keypair(); + + Self { + rpc_client, + sender_keypair, + fee_payer_keypair, + recipient_pubkey, + usdc_mint, + usdc_mint_2022, + } } pub async fn setup_all_accounts(&mut self) -> Result { self.fund_sol_accounts().await?; self.create_usdc_mint().await?; + self.create_usdc_mint_2022().await?; self.create_lookup_tables().await?; @@ -150,7 +167,65 @@ impl TestAccountSetup { Ok(()) } + pub async fn create_usdc_mint_2022(&self) -> Result<()> { + if (self.rpc_client.get_account(&self.usdc_mint_2022.pubkey()).await).is_ok() { + return Ok(()); + } + + let decimals = USDCMintTestHelper::get_test_usdc_mint_decimals(); + + // Calculate space required for mint with transfer fee extension + let space = spl_token_2022::extension::ExtensionType::try_calculate_account_len::< + Token2022Mint, + >(&[ExtensionType::TransferFeeConfig])?; + + let rent = self.rpc_client.get_minimum_balance_for_rent_exemption(space).await?; + + let create_account_instruction = solana_sdk::system_instruction::create_account( + &self.sender_keypair.pubkey(), + &self.usdc_mint_2022.pubkey(), + rent, + space as u64, + &spl_token_2022::id(), + ); + + let initialize_transfer_fee_config_instruction = initialize_transfer_fee_config( + &spl_token_2022::id(), + &self.usdc_mint_2022.pubkey(), + Some(&self.sender_keypair.pubkey()), + Some(&self.sender_keypair.pubkey()), + 100, // 1% transfer fee basis points + 1_000_000, // 1 USDC max fee (in micro-units) + )?; + + let initialize_mint_instruction = token_2022_instruction::initialize_mint2( + &spl_token_2022::id(), + &self.usdc_mint_2022.pubkey(), + &self.sender_keypair.pubkey(), + Some(&self.sender_keypair.pubkey()), + decimals, + )?; + + let recent_blockhash = self.rpc_client.get_latest_blockhash().await?; + + let transaction = Transaction::new_signed_with_payer( + &[ + create_account_instruction, + initialize_transfer_fee_config_instruction, + initialize_mint_instruction, + ], + Some(&self.sender_keypair.pubkey()), + &[&self.sender_keypair, &self.usdc_mint_2022], + recent_blockhash, + ); + + self.rpc_client.send_and_confirm_transaction(&transaction).await?; + + Ok(()) + } + pub async fn setup_token_accounts(&self) -> Result { + // SPL Token accounts let sender_token_account = get_associated_token_address(&self.sender_keypair.pubkey(), &self.usdc_mint.pubkey()); let recipient_token_account = @@ -160,6 +235,24 @@ impl TestAccountSetup { &self.usdc_mint.pubkey(), ); + // Token 2022 accounts + let sender_token_2022_account = get_associated_token_address_with_program_id( + &self.sender_keypair.pubkey(), + &self.usdc_mint_2022.pubkey(), + &spl_token_2022::id(), + ); + let recipient_token_2022_account = get_associated_token_address_with_program_id( + &self.recipient_pubkey, + &self.usdc_mint_2022.pubkey(), + &spl_token_2022::id(), + ); + let fee_payer_token_2022_account = get_associated_token_address_with_program_id( + &self.fee_payer_keypair.pubkey(), + &self.usdc_mint_2022.pubkey(), + &spl_token_2022::id(), + ); + + // Create regular SPL Token accounts let create_associated_token_account_instruction = spl_associated_token_account::instruction::create_associated_token_account_idempotent( &self.sender_keypair.pubkey(), @@ -184,13 +277,45 @@ impl TestAccountSetup { &spl_token::id(), ); + // Create Token 2022 accounts using associated token account instructions + let create_token_2022_account_instruction_sender = + spl_associated_token_account::instruction::create_associated_token_account_idempotent( + &self.sender_keypair.pubkey(), + &self.sender_keypair.pubkey(), + &self.usdc_mint_2022.pubkey(), + &spl_token_2022::id(), + ); + + let create_token_2022_account_instruction_recipient = + spl_associated_token_account::instruction::create_associated_token_account_idempotent( + &self.sender_keypair.pubkey(), + &self.recipient_pubkey, + &self.usdc_mint_2022.pubkey(), + &spl_token_2022::id(), + ); + + let create_token_2022_account_instruction_fee_payer = + spl_associated_token_account::instruction::create_associated_token_account_idempotent( + &self.sender_keypair.pubkey(), + &self.fee_payer_keypair.pubkey(), + &self.usdc_mint_2022.pubkey(), + &spl_token_2022::id(), + ); + let recent_blockhash = self.rpc_client.get_latest_blockhash().await?; + + // Combine all instructions + let all_instructions = vec![ + create_associated_token_account_instruction, + create_associated_token_account_instruction_recipient, + create_associated_token_account_instruction_fee_payer, + create_token_2022_account_instruction_sender, + create_token_2022_account_instruction_recipient, + create_token_2022_account_instruction_fee_payer, + ]; + let transaction = Transaction::new_signed_with_payer( - &[ - create_associated_token_account_instruction, - create_associated_token_account_instruction_recipient, - create_associated_token_account_instruction_fee_payer, - ], + &all_instructions, Some(&self.sender_keypair.pubkey()), &[&self.sender_keypair], recent_blockhash, @@ -201,8 +326,12 @@ impl TestAccountSetup { let mint_amount = 1_000_000 * 10_u64.pow(USDCMintTestHelper::get_test_usdc_mint_decimals() as u32); + // Mint regular SPL tokens self.mint_tokens_to_account(&sender_token_account, mint_amount).await?; + // Mint Token 2022 tokens + self.mint_tokens_2022_to_account(&sender_token_2022_account, mint_amount).await?; + Ok(TestAccountInfo { fee_payer_pubkey: self.fee_payer_keypair.pubkey(), sender_pubkey: self.sender_keypair.pubkey(), @@ -211,6 +340,11 @@ impl TestAccountSetup { sender_token_account, recipient_token_account, fee_payer_token_account, + // Token 2022 fields + usdc_mint_2022_pubkey: self.usdc_mint_2022.pubkey(), + sender_token_2022_account, + recipient_token_2022_account, + fee_payer_token_2022_account, }) } @@ -236,6 +370,28 @@ impl TestAccountSetup { Ok(()) } + async fn mint_tokens_2022_to_account(&self, token_account: &Pubkey, amount: u64) -> Result<()> { + let instruction = token_2022_instruction::mint_to( + &spl_token_2022::id(), + &self.usdc_mint_2022.pubkey(), + token_account, + &self.sender_keypair.pubkey(), + &[], + amount, + )?; + + let recent_blockhash = self.rpc_client.get_latest_blockhash().await?; + let transaction = Transaction::new_signed_with_payer( + &[instruction], + Some(&self.sender_keypair.pubkey()), + &[&self.sender_keypair], + recent_blockhash, + ); + + self.rpc_client.send_and_confirm_transaction(&transaction).await?; + Ok(()) + } + async fn create_lookup_tables(&mut self) -> Result<()> { LookupTableHelper::setup_and_save_lookup_tables(self.rpc_client.clone()).await?; diff --git a/tests/src/common/transaction.rs b/tests/src/common/transaction.rs index 6015cdc5..e12b6846 100644 --- a/tests/src/common/transaction.rs +++ b/tests/src/common/transaction.rs @@ -1,7 +1,7 @@ use anyhow::Result; use base64::{engine::general_purpose::STANDARD, Engine as _}; use kora_lib::{ - token::{TokenInterface, TokenProgram}, + token::{Token2022Program, TokenInterface, TokenProgram}, transaction::TransactionUtil, }; use solana_address_lookup_table_interface::state::AddressLookupTable; @@ -19,7 +19,9 @@ use solana_sdk::{ transaction::VersionedTransaction, }; use solana_system_interface::instruction::transfer; -use spl_associated_token_account::get_associated_token_address; +use spl_associated_token_account::{ + get_associated_token_address, get_associated_token_address_with_program_id, +}; use std::sync::Arc; /// Transaction version types @@ -188,6 +190,68 @@ impl TransactionBuilder { self } + /// Add a Token 2022 transfer_checked instruction + pub fn with_spl_token_2022_transfer_checked( + mut self, + token_mint: &Pubkey, + from_authority: &Pubkey, + to_pubkey: &Pubkey, + amount: u64, + decimals: u8, + ) -> Self { + let from_ata = get_associated_token_address_with_program_id( + from_authority, + token_mint, + &spl_token_2022::id(), + ); + let to_ata = get_associated_token_address_with_program_id( + to_pubkey, + token_mint, + &spl_token_2022::id(), + ); + + let token_interface = Token2022Program::new(); + let instruction = token_interface + .create_transfer_checked_instruction( + &from_ata, + token_mint, + &to_ata, + from_authority, + amount, + decimals, + ) + .expect("Failed to create Token 2022 transfer_checked instruction"); + + self.instructions.push(instruction); + self + } + + /// Add Token 2022 transfer checked instruction with specific token accounts + pub fn with_spl_token_2022_transfer_checked_with_accounts( + mut self, + token_mint: &Pubkey, + from_token_account: &Pubkey, + to_token_account: &Pubkey, + from_authority: &Pubkey, + amount: u64, + decimals: u8, + ) -> Self { + let token_interface = Token2022Program::new(); + let instruction = token_interface + .create_transfer_checked_instruction( + from_token_account, + token_mint, + to_token_account, + from_authority, + amount, + decimals, + ) + .expect("Failed to create Token 2022 transfer_checked instruction with accounts"); + + self.instructions.push(instruction); + self + } + /// Build the transaction and return as base64-encoded string pub async fn build(self) -> Result { let rpc_client = diff --git a/tests/tokens/main.rs b/tests/tokens/main.rs index 499acfd9..cae4fa9a 100644 --- a/tests/tokens/main.rs +++ b/tests/tokens/main.rs @@ -6,6 +6,9 @@ // - Token2022 features and validation // - Payment address validation and rules +mod token_2022_extensions_test; +mod token_2022_test; + // Make common utilities available #[path = "../src/common/mod.rs"] mod common; diff --git a/tests/tokens/token_2022_extensions_test.rs b/tests/tokens/token_2022_extensions_test.rs new file mode 100644 index 00000000..4a1b4f6a --- /dev/null +++ b/tests/tokens/token_2022_extensions_test.rs @@ -0,0 +1,430 @@ +use crate::common::{ + ExtensionHelpers, FeePayerTestHelper, TestContext, TransactionBuilder, USDCMint2022TestHelper, +}; +use jsonrpsee::rpc_params; +use solana_sdk::{ + signature::{Keypair, Signer}, + transaction::Transaction, +}; +use spl_associated_token_account::get_associated_token_address_with_program_id; +use tests::common::SenderTestHelper; + +#[tokio::test] +async fn test_blocked_memo_transfer_extension() { + // This test creates manual token accounts with MemoTransfer extension + // Should be blocked by kora-test.toml when using token accounts with MemoTransfer extension + + let ctx = TestContext::new().await.expect("Failed to create test context"); + let fee_payer = FeePayerTestHelper::get_fee_payer_keypair(); + let sender = SenderTestHelper::get_test_sender_keypair(); + let mint_keypair = USDCMint2022TestHelper::get_test_usdc_mint_2022_keypair(); + + let sender_token_account = Keypair::new(); + + // Create manual token accounts with MemoTransfer extension + ExtensionHelpers::create_token_account_with_memo_transfer( + ctx.rpc_client(), + &sender, + &sender_token_account, + &mint_keypair.pubkey(), + &sender, + ) + .await + .expect("Failed to create sender token account"); + + let fee_payer_token_account = get_associated_token_address_with_program_id( + &fee_payer.pubkey(), + &mint_keypair.pubkey(), + &spl_token_2022::id(), + ); + + // Create recipient ATA for custom mint (normal ATA without MemoTransfer extension) + let create_fee_payer_ata_instruction = + spl_associated_token_account::instruction::create_associated_token_account_idempotent( + &fee_payer.pubkey(), + &fee_payer.pubkey(), + &mint_keypair.pubkey(), + &spl_token_2022::id(), + ); + + let create_fee_payer_payment_ata_instruction = + spl_associated_token_account::instruction::create_associated_token_account_idempotent( + &fee_payer.pubkey(), + &fee_payer.pubkey(), + &mint_keypair.pubkey(), + &spl_token_2022::id(), + ); + + let recent_blockhash = ctx.rpc_client().get_latest_blockhash().await.unwrap(); + let create_atas_transaction = Transaction::new_signed_with_payer( + &[create_fee_payer_ata_instruction, create_fee_payer_payment_ata_instruction], + Some(&fee_payer.pubkey()), + &[&fee_payer], + recent_blockhash, + ); + + ctx.rpc_client() + .send_and_confirm_transaction(&create_atas_transaction) + .await + .expect("Failed to create ATAs"); + + // Mint tokens to sender account for the main transfer + ExtensionHelpers::mint_tokens_to_account( + ctx.rpc_client(), + &sender, + &mint_keypair.pubkey(), + &sender_token_account.pubkey(), + &sender, + Some(1_000_000), + ) + .await + .expect("Failed to mint tokens"); + + // Build transaction with manual token accounts that have MemoTransfer extension + let transaction = TransactionBuilder::v0() + .with_rpc_client(ctx.rpc_client().clone()) + .with_fee_payer(fee_payer.pubkey()) + .with_signer(&sender) + // Payment instructions + .with_spl_token_2022_transfer_checked_with_accounts( + &mint_keypair.pubkey(), + &sender_token_account.pubkey(), + &fee_payer_token_account, + &sender.pubkey(), + 1_000_000, + 6, + ) + .build() + .await + .expect("Failed to build transaction"); + + // Try to sign the transaction if paid - should fail due to blocked MemoTransfer on token accounts + let result: Result = + ctx.rpc_call("signTransactionIfPaid", rpc_params![transaction]).await; + + // This should fail when disallowed_token_extensions includes "MemoTransfer" + assert!(result.is_err(), "Transaction should have failed"); + + let error = result.unwrap_err().to_string(); + + assert!( + error.contains("Blocked account extension found on source account"), + "Error should mention blocked extension: {error}", + ); +} + +#[tokio::test] +async fn test_blocked_interest_bearing_config_extension() { + // This test creates a mint with InterestBearingConfig extension on-demand + // Should be blocked by kora-test.toml when using mint with InterestBearingConfig extension + + let ctx = TestContext::new().await.expect("Failed to create test context"); + let fee_payer = FeePayerTestHelper::get_fee_payer_keypair(); + let sender = SenderTestHelper::get_test_sender_keypair(); + + // Create mint with InterestBearingConfig extension + let mint_keypair = USDCMint2022TestHelper::get_test_interest_bearing_mint_keypair(); + + // Create mint with InterestBearingConfig extension + ExtensionHelpers::create_mint_with_interest_bearing( + ctx.rpc_client(), + &fee_payer, + &mint_keypair, + ) + .await + .expect("Failed to create mint with interest bearing"); + + // Create ATAs for sender and fee payer since it's a new mint + let sender_ata = get_associated_token_address_with_program_id( + &sender.pubkey(), + &mint_keypair.pubkey(), + &spl_token_2022::id(), + ); + + let fee_payer_ata = get_associated_token_address_with_program_id( + &fee_payer.pubkey(), + &mint_keypair.pubkey(), + &spl_token_2022::id(), + ); + + let create_sender_ata_instruction = + spl_associated_token_account::instruction::create_associated_token_account_idempotent( + &fee_payer.pubkey(), + &sender.pubkey(), + &mint_keypair.pubkey(), + &spl_token_2022::id(), + ); + + let create_fee_payer_ata_instruction = + spl_associated_token_account::instruction::create_associated_token_account_idempotent( + &fee_payer.pubkey(), + &fee_payer.pubkey(), + &mint_keypair.pubkey(), + &spl_token_2022::id(), + ); + + let recent_blockhash = ctx.rpc_client().get_latest_blockhash().await.unwrap(); + let create_atas_transaction = Transaction::new_signed_with_payer( + &[create_sender_ata_instruction, create_fee_payer_ata_instruction], + Some(&fee_payer.pubkey()), + &[&fee_payer], + recent_blockhash, + ); + + ctx.rpc_client() + .send_and_confirm_transaction(&create_atas_transaction) + .await + .expect("Failed to create ATAs"); + + // Mint tokens to sender + ExtensionHelpers::mint_tokens_to_account( + ctx.rpc_client(), + &fee_payer, + &mint_keypair.pubkey(), + &sender_ata, + &fee_payer, + Some(1_000_000), + ) + .await + .expect("Failed to mint tokens to sender"); + + // Use regular ATAs for the transfer (no blocked token account extensions) + // This way we test ONLY the mint extension blocking (InterestBearingConfig) + let transaction = TransactionBuilder::v0() + .with_rpc_client(ctx.rpc_client().clone()) + .with_fee_payer(fee_payer.pubkey()) + .with_signer(&sender) + .with_spl_token_2022_transfer_checked_with_accounts( + &mint_keypair.pubkey(), + &sender_ata, + &fee_payer_ata, + &sender.pubkey(), + 1_000_000, + 6, + ) + .build() + .await + .expect("Failed to build transaction"); + + // Try to sign the transaction if paid - should fail due to blocked InterestBearingConfig on mint + let result: Result = + ctx.rpc_call("signTransactionIfPaid", rpc_params![transaction]).await; + + // This should fail when disallowed_mint_extensions includes "InterestBearingConfig" + assert!(result.is_err(), "Transaction should have failed"); + + let error = result.unwrap_err().to_string(); + + assert!( + error.contains("Blocked mint extension found on mint"), + "Error should mention blocked extension: {error}", + ); +} + +#[tokio::test] +async fn test_transfer_fee_insufficient_payment() { + // Test that signTransactionIfPaid fails when payment amount doesn't account for transfer fee + // With 1% transfer fee: sending 1000 tokens results in recipient getting 990 tokens + // If Kora expects 1000 tokens, the payment should fail validation + + let ctx = TestContext::new().await.expect("Failed to create test context"); + let fee_payer = FeePayerTestHelper::get_fee_payer_keypair(); + let sender = SenderTestHelper::get_test_sender_keypair(); + let mint_keypair = USDCMint2022TestHelper::get_test_usdc_mint_2022_keypair(); + + // Create ATAs for sender and fee payer + let sender_ata = get_associated_token_address_with_program_id( + &sender.pubkey(), + &mint_keypair.pubkey(), + &spl_token_2022::id(), + ); + + let fee_payer_ata = get_associated_token_address_with_program_id( + &fee_payer.pubkey(), + &mint_keypair.pubkey(), + &spl_token_2022::id(), + ); + + // Create ATAs if they don't exist + let create_sender_ata_instruction = + spl_associated_token_account::instruction::create_associated_token_account_idempotent( + &fee_payer.pubkey(), + &sender.pubkey(), + &mint_keypair.pubkey(), + &spl_token_2022::id(), + ); + + let create_fee_payer_ata_instruction = + spl_associated_token_account::instruction::create_associated_token_account_idempotent( + &fee_payer.pubkey(), + &fee_payer.pubkey(), + &mint_keypair.pubkey(), + &spl_token_2022::id(), + ); + + let recent_blockhash = ctx.rpc_client().get_latest_blockhash().await.unwrap(); + let create_atas_transaction = Transaction::new_signed_with_payer( + &[create_sender_ata_instruction, create_fee_payer_ata_instruction], + Some(&fee_payer.pubkey()), + &[&fee_payer], + recent_blockhash, + ); + + ctx.rpc_client() + .send_and_confirm_transaction(&create_atas_transaction) + .await + .expect("Failed to create ATAs"); + + // Mint tokens to sender + ExtensionHelpers::mint_tokens_to_account( + ctx.rpc_client(), + &sender, + &mint_keypair.pubkey(), + &sender_ata, + &sender, + Some(100_000_000), // 100 USDC (with 6 decimals) + ) + .await + .expect("Failed to mint tokens to sender"); + + // Build transaction with INSUFFICIENT payment + // Actual fee: 10,000 lamports = 0.1 USDC equivalent (100,000 micro-USDC equivalent) + // To make payment insufficient, we send less than what would result in 10,000 lamports after transfer fee + // If we send 10,000 micro-USDC with 1% transfer fee, recipient gets 9,900 micro-USDC (insufficient) + let payment_amount = 10_000; // 0.01 USDC in micro-units + + let transaction = TransactionBuilder::v0() + .with_rpc_client(ctx.rpc_client().clone()) + .with_fee_payer(fee_payer.pubkey()) + .with_signer(&sender) + .with_spl_token_2022_transfer_checked_with_accounts( + &mint_keypair.pubkey(), + &sender_ata, + &fee_payer_ata, + &sender.pubkey(), + payment_amount, + 6, + ) + .build() + .await + .expect("Failed to build transaction"); + + // Try to sign the transaction if paid - should fail due to insufficient payment after fees + let result: Result = + ctx.rpc_call("signTransactionIfPaid", rpc_params![transaction]).await; + + assert!(result.is_err(), "Transaction should have failed due to insufficient payment"); + + let error = result.unwrap_err().to_string(); + + assert!( + error.contains("Insufficient payment") + || error.contains("transfer fee") + || error.contains("Invalid transaction") + || error.contains("does not meet the required amount"), + "Error should mention insufficient payment or transfer fee: {error}", + ); +} + +#[tokio::test] +async fn test_transfer_fee_sufficient_payment() { + // Test that signTransactionIfPaid succeeds when payment amount accounts for transfer fee + // To receive 10,000 micro-USDC after 1% fee, sender must send ~10,101 micro-USDC + + let ctx = TestContext::new().await.expect("Failed to create test context"); + let fee_payer = FeePayerTestHelper::get_fee_payer_keypair(); + let sender = SenderTestHelper::get_test_sender_keypair(); + let mint_keypair = USDCMint2022TestHelper::get_test_usdc_mint_2022_keypair(); + + // Create ATAs for sender and fee payer + let sender_ata = get_associated_token_address_with_program_id( + &sender.pubkey(), + &mint_keypair.pubkey(), + &spl_token_2022::id(), + ); + + let fee_payer_ata = get_associated_token_address_with_program_id( + &fee_payer.pubkey(), + &mint_keypair.pubkey(), + &spl_token_2022::id(), + ); + + // Create ATAs if they don't exist + let create_sender_ata_instruction = + spl_associated_token_account::instruction::create_associated_token_account_idempotent( + &fee_payer.pubkey(), + &sender.pubkey(), + &mint_keypair.pubkey(), + &spl_token_2022::id(), + ); + + let create_fee_payer_ata_instruction = + spl_associated_token_account::instruction::create_associated_token_account_idempotent( + &fee_payer.pubkey(), + &fee_payer.pubkey(), + &mint_keypair.pubkey(), + &spl_token_2022::id(), + ); + + let recent_blockhash = ctx.rpc_client().get_latest_blockhash().await.unwrap(); + let create_atas_transaction = Transaction::new_signed_with_payer( + &[create_sender_ata_instruction, create_fee_payer_ata_instruction], + Some(&fee_payer.pubkey()), + &[&fee_payer], + recent_blockhash, + ); + + ctx.rpc_client() + .send_and_confirm_transaction(&create_atas_transaction) + .await + .expect("Failed to create ATAs"); + + // Mint tokens to sender + ExtensionHelpers::mint_tokens_to_account( + ctx.rpc_client(), + &sender, + &mint_keypair.pubkey(), + &sender_ata, + &sender, + Some(100_000_000), // 100 USDC (with 6 decimals) + ) + .await + .expect("Failed to mint tokens to sender"); + + // Build transaction with SUFFICIENT payment + // To get 10,000 micro-USDC after 1% fee, we need to send: + // amount / (1 - 0.01) = 10,000 / 0.99 ≈ 10,101 + let payment_amount = 10_101; // This should result in ~10,000 after 1% fee + + let transaction = TransactionBuilder::v0() + .with_rpc_client(ctx.rpc_client().clone()) + .with_fee_payer(fee_payer.pubkey()) + .with_signer(&sender) + .with_spl_token_2022_transfer_checked_with_accounts( + &mint_keypair.pubkey(), + &sender_ata, + &fee_payer_ata, + &sender.pubkey(), + payment_amount, + 6, + ) + .build() + .await + .expect("Failed to build transaction"); + + let result: Result = + ctx.rpc_call("signTransactionIfPaid", rpc_params![transaction]).await; + + assert!( + result.is_ok(), + "Transaction should have succeeded with sufficient payment: {:?}", + result.unwrap_err() + ); + + let response = result.unwrap(); + + assert!( + response.get("signed_transaction").is_some(), + "Response should contain signed_transaction" + ); +} diff --git a/tests/tokens/token_2022_test.rs b/tests/tokens/token_2022_test.rs new file mode 100644 index 00000000..f6e55fad --- /dev/null +++ b/tests/tokens/token_2022_test.rs @@ -0,0 +1,517 @@ +use std::str::FromStr; + +use crate::common::*; +use jsonrpsee::rpc_params; +use kora_lib::transaction::TransactionUtil; +use solana_sdk::{ + pubkey::Pubkey, + signature::{Keypair, Signer}, +}; + +// ************************************************************************************** +// Token 2022 Transfer Tests +// ************************************************************************************** + +/// Test transferTransaction with Token 2022 transfer +#[tokio::test] +async fn test_transfer_transaction_token_2022_legacy() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + + let sender = SenderTestHelper::get_test_sender_keypair(); + let recipient = RecipientTestHelper::get_recipient_pubkey(); + let token_mint_2022 = USDCMint2022TestHelper::get_test_usdc_mint_2022_pubkey(); + let amount = 1_000_000; // 1 USDC + + let request_params = rpc_params![ + amount, + token_mint_2022.to_string(), + sender.pubkey().to_string(), + recipient.to_string() + ]; + + let response: serde_json::Value = ctx + .rpc_call("transferTransaction", request_params) + .await + .expect("Failed to transfer Token 2022"); + + response.assert_success(); + + // transferTransaction returns unsigned transaction data, not a signed transaction + assert!(response["transaction"].as_str().is_some(), "Expected transaction in response"); + assert!(response["message"].as_str().is_some(), "Expected message in response"); + assert!(response["blockhash"].as_str().is_some(), "Expected blockhash in response"); +} + +/// Test Token 2022 transfer transaction with automatic ATA creation +#[tokio::test] +async fn test_transfer_transaction_token_2022_with_ata_legacy() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + + let rpc_client = ctx.rpc_client(); + let random_keypair = Keypair::new(); + let random_pubkey = random_keypair.pubkey(); + + let sender = SenderTestHelper::get_test_sender_keypair(); + let response: serde_json::Value = ctx + .rpc_call( + "transferTransaction", + rpc_params![ + 10, + &USDCMint2022TestHelper::get_test_usdc_mint_2022_pubkey().to_string(), + sender.pubkey().to_string(), + random_pubkey.to_string() + ], + ) + .await + .expect("Failed to submit Token 2022 transfer transaction"); + + response.assert_success(); + assert!(response["transaction"].as_str().is_some(), "Expected transaction in response"); + assert!(response["message"].as_str().is_some(), "Expected message in response"); + assert!(response["blockhash"].as_str().is_some(), "Expected blockhash in response"); + + let transaction_string = response["transaction"].as_str().unwrap(); + let transaction = TransactionUtil::decode_b64_transaction(transaction_string) + .expect("Failed to decode transaction from base64"); + + let simulated_tx = rpc_client + .simulate_transaction(&transaction) + .await + .expect("Failed to simulate Token 2022 transaction"); + + assert!(simulated_tx.value.err.is_none(), "Token 2022 transaction simulation failed"); +} + +// ************************************************************************************** +// Token 2022 Sign Transaction Tests +// ************************************************************************************** + +#[tokio::test] +async fn test_sign_token_2022_transaction_legacy() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + let sender = SenderTestHelper::get_test_sender_keypair(); + let recipient = RecipientTestHelper::get_recipient_pubkey(); + let token_mint_2022 = USDCMint2022TestHelper::get_test_usdc_mint_2022_pubkey(); + + let test_tx = ctx + .transaction_builder() + .with_fee_payer(FeePayerTestHelper::get_fee_payer_pubkey()) + .with_signer(&sender) + .with_spl_token_2022_transfer_checked(&token_mint_2022, &sender.pubkey(), &recipient, 10, 6) + .build() + .await + .expect("Failed to create Token 2022 test transaction"); + + let response: serde_json::Value = ctx + .rpc_call("signTransaction", rpc_params![test_tx]) + .await + .expect("Failed to sign Token 2022 transaction"); + + assert!(response["signature"].as_str().is_some(), "Expected signature in response"); + assert!( + response["signed_transaction"].as_str().is_some(), + "Expected signed_transaction in response" + ); + + let transaction_string = response["signed_transaction"].as_str().unwrap(); + let transaction = TransactionUtil::decode_b64_transaction(transaction_string) + .expect("Failed to decode transaction from base64"); + + let simulated_tx = ctx + .rpc_client() + .simulate_transaction(&transaction) + .await + .expect("Failed to simulate Token 2022 transaction"); + + assert!(simulated_tx.value.err.is_none(), "Token 2022 transaction simulation failed"); +} + +#[tokio::test] +async fn test_sign_token_2022_transaction_v0() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + let sender = SenderTestHelper::get_test_sender_keypair(); + let recipient = RecipientTestHelper::get_recipient_pubkey(); + let token_mint_2022 = USDCMint2022TestHelper::get_test_usdc_mint_2022_pubkey(); + + let test_tx = ctx + .v0_transaction_builder() + .with_fee_payer(FeePayerTestHelper::get_fee_payer_pubkey()) + .with_signer(&sender) + .with_spl_token_2022_transfer_checked(&token_mint_2022, &sender.pubkey(), &recipient, 10, 6) + .build() + .await + .expect("Failed to create V0 Token 2022 test transaction"); + + let response: serde_json::Value = ctx + .rpc_call("signTransaction", rpc_params![test_tx]) + .await + .expect("Failed to sign V0 Token 2022 transaction"); + + assert!(response["signature"].as_str().is_some(), "Expected signature in response"); + assert!( + response["signed_transaction"].as_str().is_some(), + "Expected signed_transaction in response" + ); + + let transaction_string = response["signed_transaction"].as_str().unwrap(); + let transaction = TransactionUtil::decode_b64_transaction(transaction_string) + .expect("Failed to decode transaction from base64"); + + let simulated_tx = ctx + .rpc_client() + .simulate_transaction(&transaction) + .await + .expect("Failed to simulate V0 Token 2022 transaction"); + + assert!(simulated_tx.value.err.is_none(), "V0 Token 2022 transaction simulation failed"); +} + +#[tokio::test] +async fn test_sign_token_2022_transaction_v0_with_lookup() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + let sender = SenderTestHelper::get_test_sender_keypair(); + let recipient = RecipientTestHelper::get_recipient_pubkey(); + let token_mint_2022 = USDCMint2022TestHelper::get_test_usdc_mint_2022_pubkey(); + + // Use the transaction lookup table which contains the mint address and the spl token program + let transaction_lookup_table = LookupTableHelper::get_transaction_lookup_table_address() + .expect("Failed to get transaction lookup table from fixtures"); + + let test_tx = ctx + .v0_transaction_builder_with_lookup(vec![transaction_lookup_table]) + .with_fee_payer(FeePayerTestHelper::get_fee_payer_pubkey()) + .with_signer(&sender) + .with_spl_token_2022_transfer_checked(&token_mint_2022, &sender.pubkey(), &recipient, 10, 6) + .build() + .await + .expect("Failed to create V0 Token 2022 test transaction with lookup table"); + + let response: serde_json::Value = ctx + .rpc_call("signTransaction", rpc_params![test_tx]) + .await + .expect("Failed to sign V0 Token 2022 transaction with lookup table"); + + assert!(response["signature"].as_str().is_some(), "Expected signature in response"); + assert!( + response["signed_transaction"].as_str().is_some(), + "Expected signed_transaction in response" + ); + + let transaction_string = response["signed_transaction"].as_str().unwrap(); + let transaction = TransactionUtil::decode_b64_transaction(transaction_string) + .expect("Failed to decode transaction from base64"); + + let simulated_tx = ctx + .rpc_client() + .simulate_transaction(&transaction) + .await + .expect("Failed to simulate V0 Token 2022 transaction with lookup table"); + + assert!( + simulated_tx.value.err.is_none(), + "V0 Token 2022 transaction with lookup table simulation failed" + ); +} + +// ************************************************************************************** +// Token 2022 Sign and Send Transaction Tests +// ************************************************************************************** + +#[tokio::test] +async fn test_sign_and_send_token_2022_transaction_legacy() { + let sender = SenderTestHelper::get_test_sender_keypair(); + let recipient = RecipientTestHelper::get_recipient_pubkey(); + let fee_payer = FeePayerTestHelper::get_fee_payer_pubkey(); + let token_mint_2022 = USDCMint2022TestHelper::get_test_usdc_mint_2022_pubkey(); + + let ctx = TestContext::new().await.expect("Failed to create test context"); + + let test_tx = ctx + .transaction_builder() + .with_fee_payer(fee_payer) + .with_signer(&sender) + .with_spl_token_2022_transfer_checked(&token_mint_2022, &sender.pubkey(), &recipient, 10, 6) + .build() + .await + .expect("Failed to create signed Token 2022 test transaction"); + + let result: Result = + ctx.rpc_call("signAndSendTransaction", rpc_params![test_tx]).await; + + assert!(result.is_ok(), "Expected signAndSendTransaction to succeed for Token 2022"); + let response = result.unwrap(); + assert!(response["signature"].as_str().is_some(), "Expected signature in response"); + assert!( + response["signed_transaction"].as_str().is_some(), + "Expected signed_transaction in response" + ); +} + +#[tokio::test] +async fn test_sign_and_send_token_2022_transaction_v0() { + let sender = SenderTestHelper::get_test_sender_keypair(); + let recipient = RecipientTestHelper::get_recipient_pubkey(); + let fee_payer = FeePayerTestHelper::get_fee_payer_pubkey(); + let token_mint_2022 = USDCMint2022TestHelper::get_test_usdc_mint_2022_pubkey(); + + let ctx = TestContext::new().await.expect("Failed to create test context"); + + let test_tx = ctx + .v0_transaction_builder() + .with_fee_payer(fee_payer) + .with_signer(&sender) + .with_spl_token_2022_transfer_checked(&token_mint_2022, &sender.pubkey(), &recipient, 10, 6) + .build() + .await + .expect("Failed to create V0 Token 2022 test transaction"); + + let result: Result = + ctx.rpc_call("signAndSendTransaction", rpc_params![test_tx]).await; + + assert!(result.is_ok(), "Expected signAndSendTransaction to succeed for V0 Token 2022"); + let response = result.unwrap(); + assert!(response["signature"].as_str().is_some(), "Expected signature in response"); + assert!( + response["signed_transaction"].as_str().is_some(), + "Expected signed_transaction in response" + ); +} + +#[tokio::test] +async fn test_sign_and_send_token_2022_transaction_v0_with_lookup() { + let sender = SenderTestHelper::get_test_sender_keypair(); + let recipient = RecipientTestHelper::get_recipient_pubkey(); + let fee_payer = FeePayerTestHelper::get_fee_payer_pubkey(); + let token_mint_2022 = USDCMint2022TestHelper::get_test_usdc_mint_2022_pubkey(); + + let ctx = TestContext::new().await.expect("Failed to create test context"); + + // Use the transaction lookup table which contains the mint address and the spl token program used for ATA derivation + let transaction_lookup_table = LookupTableHelper::get_transaction_lookup_table_address() + .expect("Failed to get transaction lookup table from fixtures"); + + let test_tx = ctx + .v0_transaction_builder_with_lookup(vec![transaction_lookup_table]) + .with_fee_payer(fee_payer) + .with_signer(&sender) + .with_spl_token_2022_transfer_checked(&token_mint_2022, &sender.pubkey(), &recipient, 10, 6) + .build() + .await + .expect("Failed to create V0 Token 2022 test transaction with lookup table"); + + let result: Result = + ctx.rpc_call("signAndSendTransaction", rpc_params![test_tx]).await; + + assert!( + result.is_ok(), + "Expected signAndSendTransaction to succeed for V0 Token 2022 with lookup" + ); + let response = result.unwrap(); + assert!(response["signature"].as_str().is_some(), "Expected signature in response"); + assert!( + response["signed_transaction"].as_str().is_some(), + "Expected signed_transaction in response" + ); +} + +// ************************************************************************************** +// Token 2022 Sign Transaction If Paid Tests +// ************************************************************************************** + +/// Test Token 2022 sign transaction if paid with fee payer pool logic +#[tokio::test] +async fn test_sign_token_2022_transaction_if_paid_legacy() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + + let rpc_client = ctx.rpc_client(); + + // Get fee payer from config (use first one from the pool) + let response: serde_json::Value = + ctx.rpc_call("getConfig", rpc_params![]).await.expect("Failed to get config"); + + response.assert_success(); + let fee_payers = response["fee_payers"].as_array().unwrap(); + let fee_payer = Pubkey::from_str(fee_payers[0].as_str().unwrap()).unwrap(); + + let sender = SenderTestHelper::get_test_sender_keypair(); + let recipient = RecipientTestHelper::get_recipient_pubkey(); + + let token_mint_2022 = USDCMint2022TestHelper::get_test_usdc_mint_2022_pubkey(); + let fee_amount = 100000; + + // Use transaction builder with proper signing and automatic ATA derivation + let base64_transaction = ctx + .transaction_builder() + .with_fee_payer(fee_payer) + .with_signer(&sender) + .with_spl_token_2022_transfer_checked( + &token_mint_2022, + &sender.pubkey(), + &fee_payer, + fee_amount, + 6, + ) + .with_spl_token_2022_transfer_checked(&token_mint_2022, &sender.pubkey(), &recipient, 1, 6) + .build() + .await + .expect("Failed to create signed Token 2022 transaction"); + + // Test signTransactionIfPaid + let response: serde_json::Value = ctx + .rpc_call("signTransactionIfPaid", rpc_params![base64_transaction]) + .await + .expect("Failed to sign Token 2022 transaction"); + + response.assert_success(); + assert!( + response["signed_transaction"].as_str().is_some(), + "Expected signed_transaction in response" + ); + + // Decode the base64 transaction string + let transaction_string = response["signed_transaction"].as_str().unwrap(); + let transaction = TransactionUtil::decode_b64_transaction(transaction_string) + .expect("Failed to decode transaction from base64"); + + // Simulate the transaction + let simulated_tx = rpc_client + .simulate_transaction(&transaction) + .await + .expect("Failed to simulate Token 2022 transaction"); + + assert!(simulated_tx.value.err.is_none(), "Token 2022 transaction simulation failed"); +} + +/// Test Token 2022 sign transaction if paid with V0 transaction +#[tokio::test] +async fn test_sign_token_2022_transaction_if_paid_v0() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + + let rpc_client = ctx.rpc_client(); + + // Get fee payer from config (use first one from the pool) + let response: serde_json::Value = + ctx.rpc_call("getConfig", rpc_params![]).await.expect("Failed to get config"); + + response.assert_success(); + let fee_payers = response["fee_payers"].as_array().unwrap(); + let fee_payer = Pubkey::from_str(fee_payers[0].as_str().unwrap()).unwrap(); + + let sender = SenderTestHelper::get_test_sender_keypair(); + let recipient = RecipientTestHelper::get_recipient_pubkey(); + let token_mint_2022 = USDCMint2022TestHelper::get_test_usdc_mint_2022_pubkey(); + + let fee_amount = 100000; + + let base64_transaction = ctx + .v0_transaction_builder() + .with_fee_payer(fee_payer) + .with_signer(&sender) + .with_spl_token_2022_transfer_checked( + &token_mint_2022, + &sender.pubkey(), + &fee_payer, + fee_amount, + 6, + ) + .with_spl_token_2022_transfer_checked(&token_mint_2022, &sender.pubkey(), &recipient, 1, 6) + .build() + .await + .expect("Failed to create V0 signed Token 2022 transaction"); + + // Test signTransactionIfPaid + let response: serde_json::Value = ctx + .rpc_call("signTransactionIfPaid", rpc_params![base64_transaction]) + .await + .expect("Failed to sign V0 Token 2022 transaction"); + + response.assert_success(); + assert!( + response["signed_transaction"].as_str().is_some(), + "Expected signed_transaction in response" + ); + + // Decode the base64 transaction string + let transaction_string = response["signed_transaction"].as_str().unwrap(); + let transaction = TransactionUtil::decode_b64_transaction(transaction_string) + .expect("Failed to decode transaction from base64"); + + // Simulate the transaction + let simulated_tx = rpc_client + .simulate_transaction(&transaction) + .await + .expect("Failed to simulate V0 Token 2022 transaction"); + + assert!(simulated_tx.value.err.is_none(), "V0 Token 2022 transaction simulation failed"); +} + +/// Test Token 2022 sign transaction if paid with V0 transaction and lookup table +#[tokio::test] +async fn test_sign_token_2022_transaction_if_paid_v0_with_lookup() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + + let rpc_client = ctx.rpc_client(); + + // Get fee payer from config (use first one from the pool) + let response: serde_json::Value = + ctx.rpc_call("getConfig", rpc_params![]).await.expect("Failed to get config"); + + response.assert_success(); + let fee_payers = response["fee_payers"].as_array().unwrap(); + let fee_payer = Pubkey::from_str(fee_payers[0].as_str().unwrap()).unwrap(); + + let sender = SenderTestHelper::get_test_sender_keypair(); + let recipient = RecipientTestHelper::get_recipient_pubkey(); + let token_mint_2022 = USDCMint2022TestHelper::get_test_usdc_mint_2022_pubkey(); + + let fee_amount = 100000; + + // Use the transaction lookup table which contains the mint address and the spl token program + let transaction_lookup_table = LookupTableHelper::get_transaction_lookup_table_address() + .expect("Failed to get transaction lookup table from fixtures"); + + // Use V0 transaction builder with lookup table and proper signing + let base64_transaction = ctx + .v0_transaction_builder_with_lookup(vec![transaction_lookup_table]) + .with_fee_payer(fee_payer) + .with_signer(&sender) + .with_spl_token_2022_transfer_checked( + &token_mint_2022, + &sender.pubkey(), + &fee_payer, + fee_amount, + 6, + ) + .with_spl_token_2022_transfer_checked(&token_mint_2022, &sender.pubkey(), &recipient, 1, 6) + .build() + .await + .expect("Failed to create V0 signed Token 2022 transaction with lookup table"); + + // Test signTransactionIfPaid + let response: serde_json::Value = ctx + .rpc_call("signTransactionIfPaid", rpc_params![base64_transaction]) + .await + .expect("Failed to sign V0 Token 2022 transaction with lookup table"); + + response.assert_success(); + assert!( + response["signed_transaction"].as_str().is_some(), + "Expected signed_transaction in response" + ); + + // Decode the base64 transaction string + let transaction_string = response["signed_transaction"].as_str().unwrap(); + let transaction = TransactionUtil::decode_b64_transaction(transaction_string) + .expect("Failed to decode transaction from base64"); + + // Simulate the transaction + let simulated_tx = rpc_client + .simulate_transaction(&transaction) + .await + .expect("Failed to simulate V0 Token 2022 transaction with lookup table"); + + assert!( + simulated_tx.value.err.is_none(), + "V0 Token 2022 transaction with lookup table simulation failed" + ); +}