Skip to content

Commit 40adf0b

Browse files
dev-jodeegithub-actions
andauthored
feat (PRO-254): Add Token 2022 integration tests (#203)
* feat (PRO-254): Add Token 2022 integration tests - Introduced new `test-token` target in Makefile for running Token tests. - Added `USDC_MINT_2022_KEYPAIR_PATH` constant for local testing. - Enhanced `TestAccountInfo` and `TestAccountSetup` structs to include Token 2022 fields and methods. - Implemented `create_usdc_mint_2022` method for minting Token 2022. - Added comprehensive tests for Token 2022 transfer and signing transactions. - Updated existing test utilities to support Token 2022 functionality. feat: Testing of blocked extensions on token account and mint account - Introduced `ExtensionHelpers` for creating interest-bearing mints and token accounts with memo transfer extensions. - Added constants for interest-bearing mint keypair path. - Implemented tests to validate the blocking of memo transfer and interest-bearing config extensions in transactions. - Updated transaction builder to support SPL Token 2022 transfer with specific accounts. - Enhanced test fixtures and configuration for local testing of interest-bearing mints. * Added transfer fee tests with payment amount bounds --------- Co-authored-by: github-actions <github-actions@github.com>
1 parent d051c3d commit 40adf0b

File tree

17 files changed

+1445
-59
lines changed

17 files changed

+1445
-59
lines changed

.github/badges/coverage.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"schemaVersion": 1, "label": "coverage", "message": "86.3%", "color": "green"}

crates/lib/src/oracle/jupiter.rs

Lines changed: 22 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -163,10 +163,11 @@ mod tests {
163163
use mockito::{Matcher, Server};
164164

165165
#[tokio::test]
166-
async fn test_jupiter_price_fetch() {
167-
// No API key
166+
async fn test_jupiter_price_fetch_comprehensive() {
167+
// Test case 1: No API key - should use lite API
168168
{
169169
let mut api_key_guard = GLOBAL_JUPITER_API_KEY.write();
170+
170171
*api_key_guard = None;
171172
}
172173

@@ -185,7 +186,7 @@ mod tests {
185186
}
186187
}"#;
187188
let mut server = Server::new_async().await;
188-
let _m = server
189+
let _m1 = server
189190
.mock("GET", "/price/v3")
190191
.match_query(Matcher::Any)
191192
.with_status(200)
@@ -194,38 +195,23 @@ mod tests {
194195
.create();
195196

196197
let client = Client::new();
197-
// Test without API key - should use lite API
198198
let mut oracle = JupiterPriceOracle::new();
199199
oracle.lite_api_url = format!("{}/price/v3", server.url());
200200

201201
let result = oracle.get_price(&client, "So11111111111111111111111111111111111111112").await;
202-
203202
assert!(result.is_ok());
204203
let price = result.unwrap();
205204
assert_eq!(price.price, 1.0);
206205
assert_eq!(price.source, PriceSource::Jupiter);
207206

208-
// With API key
207+
// Test case 2: With API key - should use pro API
209208
{
210209
let mut api_key_guard = GLOBAL_JUPITER_API_KEY.write();
211210
*api_key_guard = Some("test-api-key".to_string());
212211
}
213-
let mock_response = r#"{
214-
"So11111111111111111111111111111111111111112": {
215-
"usdPrice": 100.0,
216-
"blockId": 12345,
217-
"decimals": 9,
218-
"priceChange24h": 2.5
219-
},
220-
"JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN": {
221-
"usdPrice": 0.532,
222-
"blockId": 12345,
223-
"decimals": 6,
224-
"priceChange24h": -1.2
225-
}
226-
}"#;
227-
let mut server = Server::new_async().await;
228-
let _m = server
212+
213+
let mut server2 = Server::new_async().await;
214+
let _m2 = server2
229215
.mock("GET", "/price/v3")
230216
.match_header("x-api-key", "test-api-key")
231217
.match_query(Matcher::Any)
@@ -234,55 +220,48 @@ mod tests {
234220
.with_body(mock_response)
235221
.create();
236222

237-
let client = Client::new();
238-
// Test with API key - should use pro API
239-
let mut oracle = JupiterPriceOracle::new();
240-
oracle.pro_api_url = format!("{}/price/v3", server.url());
241-
242-
let result = oracle.get_price(&client, "So11111111111111111111111111111111111111112").await;
223+
let mut oracle2 = JupiterPriceOracle::new();
224+
oracle2.pro_api_url = format!("{}/price/v3", server2.url());
243225

226+
let result =
227+
oracle2.get_price(&client, "So11111111111111111111111111111111111111112").await;
244228
assert!(result.is_ok());
245229
let price = result.unwrap();
246230
assert_eq!(price.price, 1.0);
247231
assert_eq!(price.source, PriceSource::Jupiter);
248-
}
249232

250-
#[tokio::test]
251-
async fn test_jupiter_price_fetch_when_no_price_data() {
252-
// No API key
233+
// Test case 3: No price data available - should return error
253234
{
254235
let mut api_key_guard = GLOBAL_JUPITER_API_KEY.write();
255236
*api_key_guard = None;
256237
}
257238

258-
let mock_response = r#"{
239+
let no_price_response = r#"{
259240
"So11111111111111111111111111111111111111112": {
260241
"usdPrice": 100.0,
261242
"blockId": 12345,
262243
"decimals": 9,
263244
"priceChange24h": 2.5
264245
}
265246
}"#;
266-
let mut server = Server::new_async().await;
267-
let _m = server
247+
let mut server3 = Server::new_async().await;
248+
let _m3 = server3
268249
.mock("GET", "/price/v3")
269250
.match_query(Matcher::Any)
270251
.with_status(200)
271252
.with_header("content-type", "application/json")
272-
.with_body(mock_response)
253+
.with_body(no_price_response)
273254
.create();
274255

275-
let client = Client::new();
276-
// Test without API key - should use lite API
277-
let mut oracle = JupiterPriceOracle::new();
278-
oracle.lite_api_url = format!("{}/price/v3", server.url());
279-
280-
let result = oracle.get_price(&client, "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN").await;
256+
let mut oracle3 = JupiterPriceOracle::new();
257+
oracle3.lite_api_url = format!("{}/price/v3", server3.url());
281258

259+
let result =
260+
oracle3.get_price(&client, "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN").await;
282261
assert!(result.is_err());
283262
assert_eq!(
284263
result.err(),
285264
Some(KoraError::RpcError("No price data from Jupiter".to_string()))
286-
)
265+
);
287266
}
288267
}

crates/lib/src/transaction/versioned_transaction.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,7 @@ impl VersionedTransactionResolved {
146146
.map_err(|e| KoraError::RpcError(format!("Failed to simulate transaction: {e}")))?;
147147

148148
if let Some(err) = simulation_result.value.err {
149-
log::warn!(
150-
"Transaction simulation failed: {err}, continuing without inner instructions",
151-
);
149+
log::warn!("Transaction simulation failed: {err}");
152150
return Err(KoraError::InvalidTransaction(
153151
"Transaction inner instructions fetching failed.".to_string(),
154152
));

crates/lib/src/validator/config_validator.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -680,7 +680,6 @@ mod tests {
680680

681681
let _ = update_config(config);
682682

683-
let mock_account = create_mock_program_account();
684683
let rpc_client = RpcMockBuilder::new().build();
685684

686685
let result = ConfigValidator::validate_with_result(&rpc_client, true).await;

makefiles/RUST_TESTS.makefile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ test-regular:
2929
$(call run_integration_phase,1,Regular Tests,$(REGULAR_CONFIG),,--test rpc,)
3030
@$(call stop_solana_validator)
3131

32+
test-token:
33+
$(call print_header,TOKEN TESTS)
34+
@$(call start_solana_validator)
35+
@cargo run -p tests --bin setup_test_env $(QUIET_OUTPUT)
36+
$(call run_integration_phase,1,Tokens Tests,$(REGULAR_CONFIG),,--test tokens,)
37+
@$(call stop_solana_validator)
38+
3239
test-auth:
3340
$(call print_header,AUTHENTICATION TESTS)
3441
@$(call start_solana_validator)

tests/src/common/constants.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ pub const SENDER_KEYPAIR_PATH: &str =
3030
pub const USDC_MINT_KEYPAIR_PATH: &str =
3131
concat!(env!("CARGO_MANIFEST_DIR"), "/src/common/local-keys/usdc-mint-local.json");
3232

33+
/// USDC mint 2022 keypair path (local testing only)
34+
pub const USDC_MINT_2022_KEYPAIR_PATH: &str =
35+
concat!(env!("CARGO_MANIFEST_DIR"), "/src/common/local-keys/usdc-mint-2022-local.json");
36+
37+
/// Interest bearing mint keypair path (local testing only)
38+
pub const INTEREST_BEARING_MINT_KEYPAIR_PATH: &str =
39+
concat!(env!("CARGO_MANIFEST_DIR"), "/src/common/local-keys/mint-2022-interest-bearing.json");
40+
3341
/// Second signer keypair path (for multi-signer tests)
3442
pub const SIGNER2_KEYPAIR_PATH: &str =
3543
concat!(env!("CARGO_MANIFEST_DIR"), "/src/common/local-keys/signer2-local.json");
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
use anyhow::Result;
2+
use solana_client::nonblocking::rpc_client::RpcClient;
3+
use solana_sdk::{
4+
pubkey::Pubkey,
5+
signature::{Keypair, Signer},
6+
transaction::Transaction,
7+
};
8+
use spl_token_2022::{
9+
extension::{interest_bearing_mint::instruction::initialize, ExtensionType},
10+
instruction as token_2022_instruction,
11+
state::{Account as Token2022Account, Mint as Token2022Mint},
12+
};
13+
use std::sync::Arc;
14+
15+
use crate::common::USDCMintTestHelper;
16+
17+
/// Helper functions for creating Token 2022 accounts with specific extensions for testing
18+
pub struct ExtensionHelpers;
19+
20+
impl ExtensionHelpers {
21+
/// Create a mint with InterestBearingConfig extension
22+
pub async fn create_mint_with_interest_bearing(
23+
rpc_client: &Arc<RpcClient>,
24+
payer: &Keypair,
25+
mint_keypair: &Keypair,
26+
) -> Result<()> {
27+
if (rpc_client.get_account(&mint_keypair.pubkey()).await).is_ok() {
28+
return Ok(());
29+
}
30+
31+
let decimals = USDCMintTestHelper::get_test_usdc_mint_decimals();
32+
33+
let space = ExtensionType::try_calculate_account_len::<Token2022Mint>(&[
34+
ExtensionType::InterestBearingConfig,
35+
])?;
36+
37+
let rent = rpc_client.get_minimum_balance_for_rent_exemption(space).await?;
38+
39+
let create_account_instruction = solana_sdk::system_instruction::create_account(
40+
&payer.pubkey(),
41+
&mint_keypair.pubkey(),
42+
rent,
43+
space as u64,
44+
&spl_token_2022::id(),
45+
);
46+
47+
let initialize_interest_bearing_instruction =
48+
initialize(&spl_token_2022::id(), &mint_keypair.pubkey(), Some(payer.pubkey()), 10)?;
49+
50+
let initialize_mint_instruction = token_2022_instruction::initialize_mint2(
51+
&spl_token_2022::id(),
52+
&mint_keypair.pubkey(),
53+
&payer.pubkey(),
54+
Some(&payer.pubkey()),
55+
decimals,
56+
)?;
57+
58+
let recent_blockhash = rpc_client.get_latest_blockhash().await?;
59+
60+
let transaction = Transaction::new_signed_with_payer(
61+
&[
62+
create_account_instruction,
63+
initialize_interest_bearing_instruction,
64+
initialize_mint_instruction,
65+
],
66+
Some(&payer.pubkey()),
67+
&[payer, mint_keypair],
68+
recent_blockhash,
69+
);
70+
71+
rpc_client.send_and_confirm_transaction(&transaction).await?;
72+
Ok(())
73+
}
74+
75+
/// Create a manual token account with MemoTransfer extension
76+
pub async fn create_token_account_with_memo_transfer(
77+
rpc_client: &Arc<RpcClient>,
78+
payer: &Keypair,
79+
token_account_keypair: &Keypair,
80+
mint: &Pubkey,
81+
owner: &Keypair,
82+
) -> Result<()> {
83+
if (rpc_client.get_account(&token_account_keypair.pubkey()).await).is_ok() {
84+
return Ok(());
85+
}
86+
87+
// Calculate space for token accounts with MemoTransfer extension
88+
// Also include TransferFeeAmount if the mint has TransferFeeConfig
89+
// (The USDC mint 2022 has TransferFeeConfig, so we need to account for it)
90+
let account_space = ExtensionType::try_calculate_account_len::<Token2022Account>(&[
91+
ExtensionType::MemoTransfer,
92+
ExtensionType::TransferFeeAmount,
93+
])?;
94+
let rent = rpc_client.get_minimum_balance_for_rent_exemption(account_space).await?;
95+
96+
let create_account_instruction = solana_sdk::system_instruction::create_account(
97+
&payer.pubkey(),
98+
&token_account_keypair.pubkey(),
99+
rent,
100+
account_space as u64,
101+
&spl_token_2022::id(),
102+
);
103+
104+
// Initialize MemoTransfer account extension (requires memo for transfers)
105+
let initialize_memo_transfer_instruction =
106+
spl_token_2022::extension::memo_transfer::instruction::enable_required_transfer_memos(
107+
&spl_token_2022::id(),
108+
&token_account_keypair.pubkey(),
109+
&owner.pubkey(),
110+
&[&owner.pubkey()],
111+
)?;
112+
113+
let initialize_account_instruction = token_2022_instruction::initialize_account3(
114+
&spl_token_2022::id(),
115+
&token_account_keypair.pubkey(),
116+
mint,
117+
&owner.pubkey(),
118+
)?;
119+
120+
let recent_blockhash = rpc_client.get_latest_blockhash().await?;
121+
let transaction = Transaction::new_signed_with_payer(
122+
&[
123+
create_account_instruction,
124+
initialize_account_instruction,
125+
initialize_memo_transfer_instruction,
126+
],
127+
Some(&payer.pubkey()),
128+
&[payer, token_account_keypair, owner],
129+
recent_blockhash,
130+
);
131+
132+
rpc_client.send_and_confirm_transaction(&transaction).await?;
133+
Ok(())
134+
}
135+
136+
pub async fn mint_tokens_to_account(
137+
rpc_client: &Arc<RpcClient>,
138+
payer: &Keypair,
139+
mint: &Pubkey,
140+
token_account: &Pubkey,
141+
mint_authority: &Keypair,
142+
amount: Option<u64>,
143+
) -> Result<()> {
144+
let amount = amount.unwrap_or_else(|| {
145+
1_000_000 * 10_u64.pow(USDCMintTestHelper::get_test_usdc_mint_decimals() as u32)
146+
});
147+
148+
let instruction = token_2022_instruction::mint_to(
149+
&spl_token_2022::id(),
150+
mint,
151+
token_account,
152+
&mint_authority.pubkey(),
153+
&[],
154+
amount,
155+
)?;
156+
157+
let recent_blockhash = rpc_client.get_latest_blockhash().await?;
158+
let transaction = Transaction::new_signed_with_payer(
159+
&[instruction],
160+
Some(&payer.pubkey()),
161+
&[payer, mint_authority],
162+
recent_blockhash,
163+
);
164+
165+
rpc_client.send_and_confirm_transaction(&transaction).await?;
166+
Ok(())
167+
}
168+
}

tests/src/common/fixtures/kora-test.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,28 @@ allowed_programs = [
3535
]
3636
allowed_tokens = [
3737
"9BgeTKqmFsPVnfYscfM6NvsgmZxei7XfdciShQ6D3bxJ", # Test USDC mint for local testing
38+
"95kSi2m5MDiKAs8bucgzengMTP5M5FiQnJps9duYcmfG", # Test USDC mint 2022 for local testing
39+
"AtCGtK6HPgdpk2c2LcpZimbH8dtHXYmJdoKsawWNCh2m", # Test Interest Bearing mint 2022 for local testing
3840
]
3941
allowed_spl_paid_tokens = [
4042
"9BgeTKqmFsPVnfYscfM6NvsgmZxei7XfdciShQ6D3bxJ", # Test USDC mint for local testing
43+
"95kSi2m5MDiKAs8bucgzengMTP5M5FiQnJps9duYcmfG", # Test USDC mint 2022 for local testing
44+
"AtCGtK6HPgdpk2c2LcpZimbH8dtHXYmJdoKsawWNCh2m", # Test Interest Bearing mint 2022 for local testing
4145
]
4246

4347
disallowed_accounts = [
4448
"hndXZGK45hCxfBYvxejAXzCfCujoqkNf7rk4sTB8pek", # Test disallowed account for lookup table
4549
]
4650

51+
# Block specific extensions for testing (only affects extension test accounts)
52+
[validation.token_2022]
53+
blocked_mint_extensions = [
54+
"interest_bearing_config", # Block mints with interest bearing config for extension testing
55+
]
56+
blocked_account_extensions = [
57+
"memo_transfer", # Block token accounts with MemoTransfer extension for extension testing
58+
]
59+
4760
[validation.fee_payer_policy]
4861
allow_sol_transfers = true
4962
allow_spl_transfers = true

0 commit comments

Comments
 (0)