Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 18 additions & 16 deletions .github/actions/setup-solana-validator/action.yml
Original file line number Diff line number Diff line change
@@ -1,42 +1,44 @@
name: 'Setup Solana Validator'
description: 'Start Solana test validator with health check'
name: "Setup Solana Validator"
description: "Start Solana test validator with health check"

inputs:
rpc-url:
description: 'Solana RPC URL'
description: "Solana RPC URL"
required: false
default: 'http://127.0.0.1:8899'
default: "http://127.0.0.1:8899"
timeout:
description: 'Timeout in seconds to wait for validator'
description: "Timeout in seconds to wait for validator"
required: false
default: '60'
default: "60"

outputs:
rpc-url:
description: 'Solana RPC URL'
description: "Solana RPC URL"
value: ${{ inputs.rpc-url }}

runs:
using: 'composite'
using: "composite"
steps:
- name: Start Solana test validator
shell: bash
run: |
echo "🚀 Starting Solana test validator..."

# Start validator in background
solana-test-validator --reset --quiet &


# Start validator with transfer hook program loaded
solana-test-validator --reset --quiet \
--bpf-program Bcdikjss8HWzKEuj6gEQoFq9TCnGnk6v3kUnRU1gb6hA tests/src/common/transfer-hook-example/transfer_hook_example.so &
VALIDATOR_PID=$!
echo "VALIDATOR_PID=$VALIDATOR_PID" >> $GITHUB_ENV

# Save PID to file for cleanup action
echo $VALIDATOR_PID > /tmp/validator_pid

# Wait for validator to be ready
echo "⏳ Waiting for validator to be ready..."
timeout=${{ inputs.timeout }}
counter=0

while [ $counter -lt $timeout ]; do
if solana cluster-version --url ${{ inputs.rpc-url }} >/dev/null 2>&1; then
echo "✅ Solana validator ready at ${{ inputs.rpc-url }}!"
Expand All @@ -45,10 +47,10 @@ runs:
sleep 1
counter=$((counter + 1))
done

if [ $counter -eq $timeout ]; then
echo "❌ Solana validator timeout after $timeout seconds"
echo "Current processes:"
ps aux | grep solana || true
exit 1
fi
fi
5 changes: 5 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ jobs:
echo "🧪 Running RPC integration tests..."
cargo llvm-cov test --no-report -p tests --test rpc

- name: Build transfer hook program for Token 2022 tests
run: |
echo "🔧 Building transfer hook program..."
make build-transfer-hook

- name: Run token integration tests
run: |
echo "🧪 Running token integration tests..."
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
*.pdb
generated/

/tests/src/common/transfer-hook-example/target

# SDK
node_modules/
dist/
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ include makefiles/DOCUMENTATION.makefile
include makefiles/COVERAGE.makefile
include makefiles/METRICS.makefile

.PHONY: check lint test build run clean all install generate-key setup-test-env test-integration test-all test-ts coverage coverage-clean build-bin build-lib build-cli run-presigned openapi gen-ts-client run-metrics
.PHONY: check lint test build run clean all install generate-key setup-test-env test-integration test-all test-ts coverage coverage-clean build-bin build-lib build-cli run-presigned openapi gen-ts-client run-metrics build-transfer-hook

# Default target
all: check test build
Expand Down
7 changes: 3 additions & 4 deletions crates/lib/src/transaction/versioned_transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,10 +155,9 @@ 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}");
return Err(KoraError::InvalidTransaction(
"Transaction inner instructions fetching failed.".to_string(),
));
return Err(KoraError::InvalidTransaction(format!(
"Transaction simulation failed: {err}"
)));
}

if let Some(inner_instructions) = simulation_result.value.inner_instructions {
Expand Down
9 changes: 9 additions & 0 deletions makefiles/RUST_TESTS.makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
test:
@cargo test --lib --workspace --exclude tests --quiet 2>/dev/null || true

# Build transfer hook program (is checked in, so only need to build if changes are made)
build-transfer-hook:
$(call print_header,BUILDING TRANSFER HOOK PROGRAM)
$(call print_step,Building transfer hook program...)
cd tests/src/common/transfer-hook-example && \
chmod +x build.sh && \
./build.sh
$(call print_success,Transfer hook program built at tests/src/common/transfer-hook-example/target/deploy/)

# Run all integration tests with clean output
test-integration:
$(call print_header,KORA INTEGRATION TEST SUITE)
Expand Down
11 changes: 10 additions & 1 deletion makefiles/UTILS.makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ MULTI_SIGNERS_CONFIG := tests/src/common/fixtures/multi-signers.toml
REGULAR_CONFIG := tests/src/common/fixtures/kora-test.toml
AUTH_CONFIG := tests/src/common/fixtures/auth-test.toml
PAYMENT_ADDRESS_CONFIG := tests/src/common/fixtures/paymaster-address-test.toml
TRANSFER_HOOK_PROGRAM_ID := Bcdikjss8HWzKEuj6gEQoFq9TCnGnk6v3kUnRU1gb6hA

# CI-aware timeouts
VALIDATOR_TIMEOUT := $(if $(CI),20,30)
Expand Down Expand Up @@ -55,13 +56,21 @@ define print_error
@printf " $(RED)✗$(RESET) $(1)\n"
endef


# Solana validator lifecycle management functions
define start_solana_validator
$(call print_step,Starting Solana test validator...)
@pkill -f "solana-test-validator" 2>/dev/null || true
@sleep 2
@rm -rf test-ledger 2>/dev/null || true
@solana-test-validator --reset --quiet $(QUIET_OUTPUT) &
@if [ -f "tests/src/common/transfer-hook-example/transfer_hook_example.so" ]; then \
printf " $(YELLOW)•$(RESET) Loading transfer hook program: $(TRANSFER_HOOK_PROGRAM_ID)\\n"; \
printf " $(YELLOW)•$(RESET) Program file: tests/src/common/transfer-hook-example/transfer_hook_example.so\\n"; \
solana-test-validator --reset --quiet --bpf-program $(TRANSFER_HOOK_PROGRAM_ID) tests/src/common/transfer-hook-example/transfer_hook_example.so $(QUIET_OUTPUT) & \
else \
printf " $(RED)✗$(RESET) Transfer hook program not found, starting validator without it\\n"; \
solana-test-validator --reset --quiet $(QUIET_OUTPUT) & \
fi
@echo $$! > .validator.pid
@counter=0; \
while [ $$counter -lt $(VALIDATOR_TIMEOUT) ]; do \
Expand Down
1 change: 1 addition & 0 deletions tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ solana-client = { workspace = true }
spl-token = { workspace = true }
spl-token-2022 = { workspace = true }
spl-associated-token-account = { workspace = true }
spl-transfer-hook-interface = { version = "0.10.0" }
solana-compute-budget-interface = "2.2.2"
bincode = { workspace = true }
bs58 = { workspace = true }
Expand Down
7 changes: 7 additions & 0 deletions tests/src/common/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ pub const USDC_MINT_2022_KEYPAIR_PATH: &str =
pub const INTEREST_BEARING_MINT_KEYPAIR_PATH: &str =
concat!(env!("CARGO_MANIFEST_DIR"), "/src/common/local-keys/mint-2022-interest-bearing.json");

/// Transfer hook mint keypair path (local testing only)
pub const TRANSFER_HOOK_MINT_KEYPAIR_PATH: &str =
concat!(env!("CARGO_MANIFEST_DIR"), "/src/common/local-keys/mint-transfer-hook-local.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");
Expand All @@ -62,6 +66,9 @@ pub const TEST_PAYMENT_ADDRESS: &str = "CWvWnVwqAb9HzqwCGkn4purGEUuu27aNsPQM252u
/// PYUSD token mint on devnet
pub const PYUSD_MINT: &str = "CXk2AMBfi3TwaEL2468s6zP8xq9NxTXjp9gjMgzeUynM";

/// Transfer hook program ID
pub const TRANSFER_HOOK_PROGRAM_ID: &str = "Bcdikjss8HWzKEuj6gEQoFq9TCnGnk6v3kUnRU1gb6hA";

// ============================================================================
// Test Configuration
// ============================================================================
Expand Down
121 changes: 116 additions & 5 deletions tests/src/common/extension_helpers.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
use crate::common::USDCMintTestHelper;
use anyhow::Result;
use solana_client::nonblocking::rpc_client::RpcClient;
use solana_sdk::{
instruction::AccountMeta,
pubkey::Pubkey,
signature::{Keypair, Signer},
transaction::Transaction,
};
use solana_system_interface::instruction::create_account;
use spl_token_2022::{
extension::{interest_bearing_mint::instruction::initialize, ExtensionType},
extension::{interest_bearing_mint::instruction::initialize, transfer_hook, ExtensionType},
instruction as token_2022_instruction,
state::{Account as Token2022Account, Mint as Token2022Mint},
};
use spl_transfer_hook_interface::{
get_extra_account_metas_address, instruction::initialize_extra_account_meta_list,
};
use std::sync::Arc;

use crate::common::USDCMintTestHelper;

/// Helper functions for creating Token 2022 accounts with specific extensions for testing
pub struct ExtensionHelpers;

Expand All @@ -36,7 +40,7 @@ impl ExtensionHelpers {

let rent = rpc_client.get_minimum_balance_for_rent_exemption(space).await?;

let create_account_instruction = solana_sdk::system_instruction::create_account(
let create_account_instruction = create_account(
&payer.pubkey(),
&mint_keypair.pubkey(),
rent,
Expand Down Expand Up @@ -93,7 +97,7 @@ impl ExtensionHelpers {
])?;
let rent = rpc_client.get_minimum_balance_for_rent_exemption(account_space).await?;

let create_account_instruction = solana_sdk::system_instruction::create_account(
let create_account_instruction = create_account(
&payer.pubkey(),
&token_account_keypair.pubkey(),
rent,
Expand Down Expand Up @@ -165,4 +169,111 @@ impl ExtensionHelpers {
rpc_client.send_and_confirm_transaction(&transaction).await?;
Ok(())
}

/// Create a mint with TransferHook extension for testing
pub async fn create_mint_with_transfer_hook(
rpc_client: &Arc<RpcClient>,
payer: &Keypair,
mint_keypair: &Keypair,
hook_program_id: &Pubkey,
) -> Result<()> {
if (rpc_client.get_account(&mint_keypair.pubkey()).await).is_ok() {
return Ok(());
}

// Calculate space for mint with TransferHook extension
let space = ExtensionType::try_calculate_account_len::<Token2022Mint>(&[
ExtensionType::TransferHook,
])?;

let rent = rpc_client.get_minimum_balance_for_rent_exemption(space).await?;

let create_account_instruction = create_account(
&payer.pubkey(),
&mint_keypair.pubkey(),
rent,
space as u64,
&spl_token_2022::id(),
);

// Initialize the transfer hook extension
let initialize_hook_instruction = transfer_hook::instruction::initialize(
&spl_token_2022::id(),
&mint_keypair.pubkey(),
Some(payer.pubkey()),
Some(*hook_program_id),
)?;

let initialize_mint_instruction = token_2022_instruction::initialize_mint2(
&spl_token_2022::id(),
&mint_keypair.pubkey(),
&payer.pubkey(),
Some(&payer.pubkey()),
USDCMintTestHelper::get_test_usdc_mint_decimals(),
)?;

let recent_blockhash = rpc_client.get_latest_blockhash().await?;

let transaction = Transaction::new_signed_with_payer(
&[create_account_instruction, initialize_hook_instruction, initialize_mint_instruction],
Some(&payer.pubkey()),
&[payer, mint_keypair],
recent_blockhash,
);

rpc_client.send_and_confirm_transaction(&transaction).await?;

// After mint is created, we need to initialize the Extra Account Meta List
Self::initialize_extra_account_meta_list(
rpc_client,
payer,
&mint_keypair.pubkey(),
hook_program_id,
)
.await?;

Ok(())
}

/// Initialize Extra Account Meta List for transfer hook
async fn initialize_extra_account_meta_list(
rpc_client: &Arc<RpcClient>,
payer: &Keypair,
mint: &Pubkey,
hook_program_id: &Pubkey,
) -> Result<()> {
let extra_account_metas_address = get_extra_account_metas_address(mint, hook_program_id);

if rpc_client.get_account(&extra_account_metas_address).await.is_ok() {
return Ok(());
}

// Create an empty list of extra account metas (our simple hook doesn't need any)
let extra_account_metas = vec![];

let mut initialize_instruction = initialize_extra_account_meta_list(
hook_program_id,
&extra_account_metas_address,
mint,
&payer.pubkey(),
&extra_account_metas,
);

// Add the system program account which is needed for PDA creation
initialize_instruction
.accounts
.push(AccountMeta::new_readonly(solana_sdk::system_program::id(), false));

let recent_blockhash = rpc_client.get_latest_blockhash().await?;
let transaction = Transaction::new_signed_with_payer(
&[initialize_instruction],
Some(&payer.pubkey()),
&[payer],
recent_blockhash,
);

rpc_client.send_and_confirm_transaction(&transaction).await?;

Ok(())
}
}
15 changes: 15 additions & 0 deletions tests/src/common/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,19 @@ impl USDCMint2022TestHelper {
pub fn get_test_interest_bearing_mint_pubkey() -> Pubkey {
Self::get_test_interest_bearing_mint_keypair().pubkey()
}

pub fn get_test_transfer_hook_mint_keypair() -> Keypair {
dotenv::dotenv().ok();
let mint_keypair = match std::env::var("TEST_TRANSFER_HOOK_MINT_KEYPAIR") {
Ok(key) => key,
Err(_) => std::fs::read_to_string(TRANSFER_HOOK_MINT_KEYPAIR_PATH)
.expect("Failed to read transfer hook mint private key file"),
};
parse_private_key_string(&mint_keypair)
.expect("Failed to parse test transfer hook mint private key")
}

pub fn get_test_transfer_hook_mint_pubkey() -> Pubkey {
Self::get_test_transfer_hook_mint_keypair().pubkey()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[24,76,19,115,76,184,74,133,28,64,179,98,233,151,179,252,37,171,7,151,148,42,5,250,7,241,157,57,104,35,105,33,106,60,44,67,36,199,253,163,7,64,41,206,101,8,228,177,46,134,218,229,147,97,112,71,47,2,198,225,131,180,176,140]
Loading
Loading