From 178fa5a3eb658a7d2f83fea78ca71762a23d9767 Mon Sep 17 00:00:00 2001 From: jo <17280917+dev-jodee@users.noreply.github.com> Date: Thu, 18 Sep 2025 17:12:46 -0400 Subject: [PATCH 01/29] =?UTF-8?q?feat:=20(PRO-203)=20Implement=20TurnkeyEr?= =?UTF-8?q?ror=20handling=20and=20enhance=20PrivySign=E2=80=A6=20(#221)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: (PRO-203) Implement TurnkeyError handling and enhance PrivySigner error logging - Added TurnkeyError enum for comprehensive error handling in TurnkeySigner. - Updated sign and create_stamp methods to return TurnkeyError variants. - Enhanced error logging in PrivySigner for API call failures, including status and response details. - Refactored TurnkeySigner initialization to remove unnecessary error handling, improving clarity. - Added tests for TurnkeyError display and conversion to KoraError. * Update coverage badge [skip ci] --------- Co-authored-by: github-actions --- .github/badges/coverage.json | 2 +- crates/lib/src/error.rs | 6 + crates/lib/src/signer/privy/signer.rs | 17 ++- crates/lib/src/signer/turnkey/config.rs | 7 +- crates/lib/src/signer/turnkey/signer.rs | 124 ++++++++++++------ crates/lib/src/signer/turnkey/types.rs | 69 ++++++++++ .../src/validator/transaction_validator.rs | 20 --- 7 files changed, 178 insertions(+), 67 deletions(-) diff --git a/.github/badges/coverage.json b/.github/badges/coverage.json index 95e69740..efd879e5 100644 --- a/.github/badges/coverage.json +++ b/.github/badges/coverage.json @@ -1 +1 @@ -{"schemaVersion": 1, "label": "coverage", "message": "85.5%", "color": "green"} +{"schemaVersion": 1, "label": "coverage", "message": "85.7%", "color": "green"} diff --git a/crates/lib/src/error.rs b/crates/lib/src/error.rs index 40ecbca1..76499606 100644 --- a/crates/lib/src/error.rs +++ b/crates/lib/src/error.rs @@ -199,6 +199,12 @@ impl From for KoraError { } } +impl From for KoraError { + fn from(err: crate::signer::turnkey::types::TurnkeyError) -> Self { + KoraError::SigningError(err.to_string()) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/lib/src/signer/privy/signer.rs b/crates/lib/src/signer/privy/signer.rs index dea88ca1..583d681d 100644 --- a/crates/lib/src/signer/privy/signer.rs +++ b/crates/lib/src/signer/privy/signer.rs @@ -51,7 +51,16 @@ impl PrivySigner { .await?; if !response.status().is_success() { - return Err(PrivyError::ApiError(response.status().as_u16())); + let status = response.status().as_u16(); + let error_text = response + .text() + .await + .unwrap_or_else(|_| "Failed to read error response".to_string()); + + log::error!( + "Privy API get_public_key error - status: {status}, response: {error_text}" + ); + return Err(PrivyError::ApiError(status)); } let wallet_info: WalletResponse = response.json().await?; @@ -91,6 +100,12 @@ impl PrivySigner { if !response.status().is_success() { let status = response.status().as_u16(); + let error_text = response + .text() + .await + .unwrap_or_else(|_| "Failed to read error response".to_string()); + + log::error!("Privy API sign_solana error - status: {status}, response: {error_text}"); return Err(PrivyError::ApiError(status)); } diff --git a/crates/lib/src/signer/turnkey/config.rs b/crates/lib/src/signer/turnkey/config.rs index 8859334f..7eb7735c 100644 --- a/crates/lib/src/signer/turnkey/config.rs +++ b/crates/lib/src/signer/turnkey/config.rs @@ -63,12 +63,7 @@ impl SignerConfigTrait for TurnkeySignerHandler { organization_id, private_key_id, public_key, - ) - .map_err(|e| { - KoraError::ValidationError(format!( - "Failed to create Turnkey signer '{signer_name}': {e}" - )) - })?; + ); Ok(KoraSigner::Turnkey(signer)) } diff --git a/crates/lib/src/signer/turnkey/signer.rs b/crates/lib/src/signer/turnkey/signer.rs index 05b637c1..ffd1acd7 100644 --- a/crates/lib/src/signer/turnkey/signer.rs +++ b/crates/lib/src/signer/turnkey/signer.rs @@ -8,7 +8,7 @@ use solana_sdk::{pubkey::Pubkey, signature::Signature}; use solana_sdk::transaction::VersionedTransaction; use crate::signer::{ - turnkey::types::{ActivityResponse, SignParameters, SignRequest, TurnkeySigner}, + turnkey::types::{ActivityResponse, SignParameters, SignRequest, TurnkeyError, TurnkeySigner}, utils::{bytes_to_hex, hex_to_bytes}, }; @@ -19,8 +19,8 @@ impl TurnkeySigner { organization_id: String, private_key_id: String, public_key: String, - ) -> Result { - Ok(Self { + ) -> Self { + Self { api_public_key, api_private_key, organization_id, @@ -28,10 +28,10 @@ impl TurnkeySigner { public_key, api_base_url: "https://api.turnkey.com".to_string(), client: Client::new(), - }) + } } - pub async fn sign(&self, transaction: &VersionedTransaction) -> Result, anyhow::Error> { + pub async fn sign(&self, transaction: &VersionedTransaction) -> Result, TurnkeyError> { let hex_message = hex::encode(transaction.message.serialize()); let request = SignRequest { @@ -46,7 +46,7 @@ impl TurnkeySigner { }, }; - let body = serde_json::to_string(&request).map_err(|e| anyhow::anyhow!(e.to_string()))?; + let body = serde_json::to_string(&request).map_err(TurnkeyError::JsonError)?; let stamp = self.create_stamp(&body)?; @@ -59,22 +59,33 @@ impl TurnkeySigner { .body(body) .send() .await - .map_err(|e| anyhow::anyhow!(e.to_string()))?; + .map_err(TurnkeyError::RequestError)?; + + if !response.status().is_success() { + let status = response.status().as_u16(); + let error_text = response + .text() + .await + .unwrap_or_else(|_| "Failed to read error response".to_string()); + + log::error!("Turnkey API error - status: {status}, response: {error_text}"); + return Err(TurnkeyError::ApiError(status)); + } + + let response_text = response.text().await.map_err(TurnkeyError::RequestError)?; - let response = serde_json::from_str::(&response.text().await.unwrap()) - .map_err(|e| anyhow::anyhow!(e.to_string()))?; + let response = serde_json::from_str::(&response_text) + .map_err(TurnkeyError::JsonError)?; if let Some(result) = response.activity.result { if let Some(sign_result) = result.sign_raw_payload_result { // Decode r and s components - let r_bytes = hex::decode(&sign_result.r) - .map_err(|e| anyhow::anyhow!(format!("Invalid r component: {}", e)))?; - let s_bytes = hex::decode(&sign_result.s) - .map_err(|e| anyhow::anyhow!(format!("Invalid s component: {}", e)))?; + let r_bytes = hex::decode(&sign_result.r).map_err(TurnkeyError::InvalidHex)?; + let s_bytes = hex::decode(&sign_result.s).map_err(TurnkeyError::InvalidHex)?; // Ensure each component is exactly 32 bytes if r_bytes.len() > 32 || s_bytes.len() > 32 { - return Err(anyhow::anyhow!("Signature component too long")); + return Err(TurnkeyError::InvalidSignature); } // Create properly padded 32-byte arrays @@ -94,26 +105,25 @@ impl TurnkeySigner { } } - Err(anyhow::anyhow!("Failed to get signature from response")) + Err(TurnkeyError::InvalidResponse) } pub async fn sign_solana( &self, transaction: &VersionedTransaction, - ) -> Result { + ) -> Result { let sig = self.sign(transaction).await?; let sig_bytes: [u8; 64] = sig.try_into().unwrap(); Ok(Signature::from(sig_bytes)) } - fn create_stamp(&self, message: &str) -> Result { + fn create_stamp(&self, message: &str) -> Result { let private_key_bytes = - hex_to_bytes(&self.api_private_key).map_err(|e| anyhow::anyhow!(e.to_string()))?; - let private_key_array: [u8; 32] = private_key_bytes - .try_into() - .map_err(|_| anyhow::anyhow!("Invalid private key length"))?; + hex_to_bytes(&self.api_private_key).map_err(TurnkeyError::InvalidStamp)?; + let private_key_array: [u8; 32] = + private_key_bytes.try_into().map_err(|_| TurnkeyError::InvalidPrivateKeyLength)?; let signing_key = p256::ecdsa::SigningKey::from_slice(&private_key_array) - .map_err(|e| anyhow::anyhow!(e.to_string()))?; + .map_err(TurnkeyError::SigningKeyError)?; let signature: p256::ecdsa::Signature = signing_key.sign(message.as_bytes()); let signature_der = signature.to_der().to_bytes(); @@ -125,8 +135,7 @@ impl TurnkeySigner { "scheme": "SIGNATURE_SCHEME_TK_API_P256" }); - let json_stamp = - serde_json::to_string(&stamp).map_err(|e| anyhow::anyhow!(e.to_string()))?; + let json_stamp = serde_json::to_string(&stamp).map_err(TurnkeyError::JsonError)?; Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(json_stamp.as_bytes())) } @@ -151,7 +160,7 @@ mod tests { let private_key_id = "test_private_key_id".to_string(); let public_key = "11111111111111111111111111111111".to_string(); - let result = TurnkeySigner::new( + let signer = TurnkeySigner::new( api_public_key.clone(), api_private_key.clone(), organization_id.clone(), @@ -159,8 +168,6 @@ mod tests { public_key.clone(), ); - assert!(result.is_ok()); - let signer = result.unwrap(); assert_eq!(signer.api_public_key, api_public_key); assert_eq!(signer.api_private_key, api_private_key); assert_eq!(signer.organization_id, organization_id); @@ -176,8 +183,7 @@ mod tests { "org".to_string(), "key_id".to_string(), "11111111111111111111111111111111".to_string(), - ) - .unwrap(); + ); let pubkey = signer.solana_pubkey(); assert_eq!(pubkey.to_string(), "11111111111111111111111111111111"); @@ -220,8 +226,7 @@ mod tests { "test_org_id".to_string(), "test_private_key_id".to_string(), "11111111111111111111111111111111".to_string(), - ) - .unwrap(); + ); signer.api_base_url = server.url(); let result = signer.sign(&test_transaction).await; @@ -263,15 +268,57 @@ mod tests { "invalid_org_id".to_string(), "invalid_private_key_id".to_string(), "11111111111111111111111111111111".to_string(), - ) - .unwrap(); + ); + signer.api_base_url = server.url(); // Test API error handling let result = signer.sign(&test_transaction).await; assert!(result.is_err()); - let error_msg = result.unwrap_err().to_string(); - assert!(error_msg.contains("Failed to get signature from response")); + assert!(matches!(result.unwrap_err(), TurnkeyError::ApiError(_))); + } + + #[tokio::test] + async fn test_sign_rate_limit_error() { + let mut server = Server::new_async().await; + + let rate_limit_response = r#"{ + "code": 8, + "message": "", + "details": [], + "turnkeyErrorCode": "" + }"#; + + let _mock = server + .mock("POST", "/public/v1/submit/sign_raw_payload") + .with_status(429) + .with_header("content-type", "application/json") + .with_body(rate_limit_response) + .create_async() + .await; + + let test_transaction = create_mock_transaction(); + + let mut signer = TurnkeySigner::new( + "test_api_public_key".to_string(), + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(), + "test_org_id".to_string(), + "test_private_key_id".to_string(), + "11111111111111111111111111111111".to_string(), + ); + + signer.api_base_url = server.url(); + + // Test that 429 rate limit is properly handled + let result = signer.sign(&test_transaction).await; + assert!(result.is_err()); + + match result.unwrap_err() { + TurnkeyError::ApiError(status) => { + assert_eq!(status, 429); + } + _ => panic!("Expected ApiError with 429 status"), + } } #[tokio::test] @@ -311,8 +358,8 @@ mod tests { "test_org_id".to_string(), "test_private_key_id".to_string(), "11111111111111111111111111111111".to_string(), - ) - .unwrap(); + ); + signer.api_base_url = server.url(); // Test successful signing returns Signature @@ -331,8 +378,7 @@ mod tests { "test_org_id".to_string(), "test_private_key_id".to_string(), "11111111111111111111111111111111".to_string(), - ) - .unwrap(); + ); let test_message = r#"{"test": "message"}"#; diff --git a/crates/lib/src/signer/turnkey/types.rs b/crates/lib/src/signer/turnkey/types.rs index 8f59991f..06300a4f 100644 --- a/crates/lib/src/signer/turnkey/types.rs +++ b/crates/lib/src/signer/turnkey/types.rs @@ -1,3 +1,5 @@ +use hex::FromHexError; +use p256::ecdsa::signature; use reqwest::Client; use serde::{Deserialize, Serialize}; @@ -55,3 +57,70 @@ pub struct SignResult { pub r: String, pub s: String, } + +#[derive(Debug)] +pub enum TurnkeyError { + ApiError(u16), + RequestError(reqwest::Error), + JsonError(serde_json::Error), + InvalidSignature, + InvalidHex(FromHexError), + InvalidStamp(anyhow::Error), + SigningKeyError(signature::Error), + InvalidResponse, + InvalidPrivateKeyLength, + InvalidPublicKey, + Other(anyhow::Error), +} + +impl std::fmt::Display for TurnkeyError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TurnkeyError::ApiError(status) => write!(f, "API error: {status}"), + TurnkeyError::InvalidResponse => write!(f, "Invalid response"), + TurnkeyError::InvalidPublicKey => write!(f, "Invalid public key"), + TurnkeyError::InvalidSignature => write!(f, "Invalid signature"), + TurnkeyError::InvalidPrivateKeyLength => write!(f, "Invalid private key length"), + TurnkeyError::RequestError(e) => write!(f, "Request error: {e}"), + TurnkeyError::JsonError(e) => write!(f, "JSON error: {e}"), + TurnkeyError::InvalidStamp(e) => write!(f, "Invalid stamp: {e}"), + TurnkeyError::InvalidHex(e) => write!(f, "Invalid Hex: {e}"), + TurnkeyError::SigningKeyError(e) => write!(f, "Signing key error: {e}"), + TurnkeyError::Other(e) => write!(f, "{e}"), + } + } +} + +impl std::error::Error for TurnkeyError {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_turnkey_error_display() { + let error = TurnkeyError::ApiError(429); + assert_eq!(error.to_string(), "API error: 429"); + + let error = TurnkeyError::InvalidResponse; + assert_eq!(error.to_string(), "Invalid response"); + + let error = TurnkeyError::InvalidSignature; + assert_eq!(error.to_string(), "Invalid signature"); + } + + #[test] + fn test_turnkey_error_conversion_to_kora_error() { + use crate::error::KoraError; + + let turnkey_error = TurnkeyError::ApiError(429); + let kora_error: KoraError = turnkey_error.into(); + + match kora_error { + KoraError::SigningError(msg) => { + assert_eq!(msg, "API error: 429"); + } + _ => panic!("Expected SigningError"), + } + } +} diff --git a/crates/lib/src/validator/transaction_validator.rs b/crates/lib/src/validator/transaction_validator.rs index 40871aa8..05f3d533 100644 --- a/crates/lib/src/validator/transaction_validator.rs +++ b/crates/lib/src/validator/transaction_validator.rs @@ -347,26 +347,6 @@ mod tests { update_config(config).unwrap(); } - fn setup_spl_token_config() { - let config = ConfigMockBuilder::new() - .with_price_source(PriceSource::Mock) - .with_allowed_programs(vec![spl_token::id().to_string()]) - .with_max_allowed_lamports(1_000_000) - .with_fee_payer_policy(FeePayerPolicy::default()) - .build(); - update_config(config).unwrap(); - } - - fn setup_token2022_config() { - let config = ConfigMockBuilder::new() - .with_price_source(PriceSource::Mock) - .with_allowed_programs(vec![spl_token_2022::id().to_string()]) - .with_max_allowed_lamports(1_000_000) - .with_fee_payer_policy(FeePayerPolicy::default()) - .build(); - update_config(config).unwrap(); - } - fn setup_config_with_policy(policy: FeePayerPolicy) { let config = ConfigMockBuilder::new() .with_price_source(PriceSource::Mock) From 4c26c2788d375cf8136e8926dc4ac54747d6507d Mon Sep 17 00:00:00 2001 From: jo <17280917+dev-jodee@users.noreply.github.com> Date: Mon, 22 Sep 2025 16:07:21 -0400 Subject: [PATCH 02/29] chore: (PRO-271) Update / cleanup Makefile and add debug commands and test runner (#222) * chore: (PRO-271) Update / cleanup Makefile and add debug commands - Simplified .gitignore to exclude all .pid files. - Added new debug command targets in DEBUG_COMMANDS.makefile for various test scenarios. - Updated Makefile to include DEBUG_COMMANDS.makefile and added debug-related targets. - Modified BUILD.makefile to specify the binary name for installation. - Adjusted CLIENT.makefile to streamline TypeScript client generation process. * chore: Implement rust test runner - Implemented rust test runner instead of having complex makefiles and obscure code, will be easier to read and control - Cleaned up a lot of repeated constants for file names and duplicated code that was complex across our testing utils / helpers - Added a new directory for test accounts in the fixtures. - Removed unused JSON files for examples - Refactored test account structure to include additional fields for lookup tables and token accounts. - Cleaned up imports and adjusted helper functions for better organization. - Cleaned up a lot the lookup table healper, to make it more dynamic * Cleaned up test runner * Typescript test support + makefile cleanup * CI Actions updated and improvements to test runner (speed, cleanup, etc.) - Includes not hardcoded ports - Includes reuse of files to prevent too many I/O (including async io) - Exponential backoff for when we test health check to make sure its not hogging resources - Using kora binary instead of rebuilding * PR comment fixes --------- Co-authored-by: amilz <85324096+amilz@users.noreply.github.com> --- .github/actions/cleanup-test-env/action.yml | 46 +- .github/actions/run-test-runner/action.yml | 45 ++ .github/actions/setup-kora-rpc/action.yml | 112 ---- .../actions/setup-solana-validator/action.yml | 56 -- .github/workflows/build-rust.yml | 39 ++ .github/workflows/rust-unit.yml | 128 +++++ .github/workflows/rust.yml | 231 ++------ .github/workflows/typescript-integration.yml | 74 +-- .gitignore | 6 +- Cargo.lock | 1 + Makefile | 4 +- .../rpc_server/method/transfer_transaction.rs | 2 +- .../src/transaction/versioned_transaction.rs | 2 +- crates/lib/src/validator/config_validator.rs | 61 ++- makefiles/BUILD.makefile | 10 +- makefiles/CLIENT.makefile | 4 +- makefiles/METRICS.makefile | 4 +- makefiles/RUST_TESTS.makefile | 69 +-- makefiles/TYPESCRIPT_TESTS.makefile | 49 +- makefiles/UTILS.makefile | 198 ------- sdks/ts/src/client.ts | 7 +- sdks/ts/test/auth-setup.ts | 15 +- sdks/ts/test/integration.test.ts | 4 +- sdks/ts/test/setup.ts | 18 +- tests/Cargo.toml | 7 +- tests/examples/estimateTransactionFee.json | 9 - tests/examples/getBlockhash.json | 6 - tests/examples/getConfig.json | 6 - tests/examples/getEnabledFeatures.json | 6 - tests/examples/getSupportedTokens.json | 6 - tests/examples/signAndSendTransaction.json | 8 - tests/examples/signTransaction.json | 8 - tests/examples/transferTransaction.json | 11 - .../payment_address_v0_tests.rs | 6 +- tests/rpc/transaction_signing.rs | 5 +- tests/src/bin/setup_test_env.rs | 6 - tests/src/bin/test_runner.rs | 507 ++++++++++++++++++ tests/src/common/auth_helpers.rs | 10 +- tests/src/common/client.rs | 16 +- tests/src/common/constants.rs | 118 ++-- tests/src/common/helpers.rs | 144 +++-- tests/src/common/lookup_tables.rs | 120 ++--- tests/src/common/mod.rs | 29 +- tests/src/common/setup.rs | 98 +++- tests/src/common/transaction.rs | 2 +- tests/src/lib.rs | 2 +- tests/src/setup_test_env.rs | 56 -- tests/src/test_runner/accounts.rs | 268 +++++++++ tests/src/test_runner/commands.rs | 111 ++++ tests/src/test_runner/config.rs | 32 ++ tests/src/test_runner/kora.rs | 96 ++++ tests/src/test_runner/mod.rs | 6 + tests/src/test_runner/output.rs | 193 +++++++ tests/src/test_runner/test_cases.toml | 42 ++ tests/src/test_runner/validator.rs | 74 +++ 55 files changed, 2008 insertions(+), 1185 deletions(-) create mode 100644 .github/actions/run-test-runner/action.yml delete mode 100644 .github/actions/setup-kora-rpc/action.yml delete mode 100644 .github/actions/setup-solana-validator/action.yml create mode 100644 .github/workflows/build-rust.yml create mode 100644 .github/workflows/rust-unit.yml delete mode 100644 tests/examples/estimateTransactionFee.json delete mode 100644 tests/examples/getBlockhash.json delete mode 100644 tests/examples/getConfig.json delete mode 100644 tests/examples/getEnabledFeatures.json delete mode 100644 tests/examples/getSupportedTokens.json delete mode 100644 tests/examples/signAndSendTransaction.json delete mode 100644 tests/examples/signTransaction.json delete mode 100644 tests/examples/transferTransaction.json delete mode 100644 tests/src/bin/setup_test_env.rs create mode 100644 tests/src/bin/test_runner.rs delete mode 100644 tests/src/setup_test_env.rs create mode 100644 tests/src/test_runner/accounts.rs create mode 100644 tests/src/test_runner/commands.rs create mode 100644 tests/src/test_runner/config.rs create mode 100644 tests/src/test_runner/kora.rs create mode 100644 tests/src/test_runner/mod.rs create mode 100644 tests/src/test_runner/output.rs create mode 100644 tests/src/test_runner/test_cases.toml create mode 100644 tests/src/test_runner/validator.rs diff --git a/.github/actions/cleanup-test-env/action.yml b/.github/actions/cleanup-test-env/action.yml index d5bf4d78..9b5a26d3 100644 --- a/.github/actions/cleanup-test-env/action.yml +++ b/.github/actions/cleanup-test-env/action.yml @@ -1,53 +1,15 @@ name: 'Cleanup Test Environment' -description: 'Stop all test processes (Kora RPC, Solana validator, etc.)' +description: 'Kill any remaining test processes (safety net for test runner failures)' runs: using: 'composite' steps: - - name: Stop test processes + - name: Kill remaining processes shell: bash if: always() run: | - echo "๐Ÿงน Cleaning up test environment..." - - # Stop Kora RPC server using saved PID - if [ -f /tmp/kora_pid ]; then - KORA_PID=$(cat /tmp/kora_pid) - if [ ! -z "$KORA_PID" ]; then - echo "Stopping Kora RPC server (PID: $KORA_PID)" - kill $KORA_PID 2>/dev/null || true - fi - rm -f /tmp/kora_pid - fi - - # Stop using environment variable as fallback - if [ ! -z "$KORA_PID" ]; then - echo "Stopping Kora RPC server (ENV PID: $KORA_PID)" - kill $KORA_PID 2>/dev/null || true - fi - - # Stop Solana validator using saved PID - if [ -f /tmp/validator_pid ]; then - VALIDATOR_PID=$(cat /tmp/validator_pid) - if [ ! -z "$VALIDATOR_PID" ]; then - echo "Stopping Solana validator (PID: $VALIDATOR_PID)" - kill $VALIDATOR_PID 2>/dev/null || true - fi - rm -f /tmp/validator_pid - fi - - # Stop using environment variable as fallback - if [ ! -z "$VALIDATOR_PID" ]; then - echo "Stopping Solana validator (ENV PID: $VALIDATOR_PID)" - kill $VALIDATOR_PID 2>/dev/null || true - fi - - # Kill any remaining processes by name (nuclear option) - echo "Killing any remaining test processes..." + echo "๐Ÿงน Safety cleanup of any remaining processes..." pkill -f "solana-test-validator" 2>/dev/null || true pkill -f "kora" 2>/dev/null || true - - # Wait a moment for processes to stop - sleep 2 - + sleep 1 echo "โœ… Cleanup completed" \ No newline at end of file diff --git a/.github/actions/run-test-runner/action.yml b/.github/actions/run-test-runner/action.yml new file mode 100644 index 00000000..f18394de --- /dev/null +++ b/.github/actions/run-test-runner/action.yml @@ -0,0 +1,45 @@ +name: "Run Test Runner" +description: "Execute Kora integration tests using the test runner with specified filters" + +inputs: + filters: + description: "Test filters to apply (e.g., '--filter regular --filter auth')" + required: true + verbose: + description: "Enable verbose output" + required: false + default: "true" + rpc-url: + description: "Solana RPC URL to use" + required: false + default: "http://127.0.0.1:8899" + force-refresh: + description: "Force refresh of test accounts" + required: false + default: "false" + +runs: + using: "composite" + steps: + - name: Run integration tests with test runner + shell: bash + run: | + echo "๐Ÿงช Running integration tests with filters: ${{ inputs.filters }}" + + # Build command arguments + ARGS="" + if [ "${{ inputs.verbose }}" = "true" ]; then + ARGS="$ARGS --verbose" + fi + if [ "${{ inputs.force-refresh }}" = "true" ]; then + ARGS="$ARGS --force-refresh" + fi + if [ "${{ inputs.rpc-url }}" != "http://127.0.0.1:8899" ]; then + ARGS="$ARGS --rpc-url ${{ inputs.rpc-url }}" + fi + + # Add filters + ARGS="$ARGS ${{ inputs.filters }}" + + # Run the test runner + cargo run -p tests --bin test_runner -- $ARGS \ No newline at end of file diff --git a/.github/actions/setup-kora-rpc/action.yml b/.github/actions/setup-kora-rpc/action.yml deleted file mode 100644 index 445ff017..00000000 --- a/.github/actions/setup-kora-rpc/action.yml +++ /dev/null @@ -1,112 +0,0 @@ -name: "Setup Kora RPC Server" -description: "Build and start the Kora RPC server with health check" - -inputs: - config-file: - description: "Kora config file path" - required: false - default: "tests/src/common/fixtures/kora-test.toml" - signers-config: - description: "Signers config file path" - required: false - default: "tests/src/common/fixtures/signers.toml" - rpc-url: - description: "Solana RPC URL" - required: false - default: "http://127.0.0.1:8899" - port: - description: "Kora RPC server port" - required: false - default: "8080" - timeout: - description: "Timeout in seconds to wait for server startup" - required: false - default: "30" - test-server-url: - description: "Test server URL for health checks" - required: false - default: "http://127.0.0.1:8080" - initialize-atas: - description: "Whether to initialize payment ATAs after starting server" - required: false - default: "false" - -outputs: - kora-pid: - description: "Process ID of the started Kora RPC server" - value: ${{ steps.start-server.outputs.kora-pid }} - -runs: - using: "composite" - steps: - - name: Build Kora RPC server - shell: bash - run: make build - - - name: Start Kora RPC server - id: start-server - shell: bash - run: | - echo "๐Ÿš€ Starting Kora RPC server..." - - # Set environment variables for signers (always required now) - export KORA_PRIVATE_KEY="$(cat tests/src/common/local-keys/fee-payer-local.json)" - - # Set second signer if it exists (for multi-signer configs) - if [ -f "tests/src/common/local-keys/signer2-local.json" ]; then - export KORA_PRIVATE_KEY_2="$(cat tests/src/common/local-keys/signer2-local.json)" - fi - - cargo run -p kora-cli --bin kora -- \ - --config ${{ inputs.config-file }} \ - --rpc-url ${{ inputs.rpc-url }} \ - rpc start \ - --signers-config ${{ inputs.signers-config }} \ - --port ${{ inputs.port }} \ - & - KORA_PID=$! - echo "KORA_PID=$KORA_PID" >> $GITHUB_ENV - echo "kora-pid=$KORA_PID" >> $GITHUB_OUTPUT - - # Wait for Kora RPC server to be ready - echo "โณ Waiting for Kora RPC server to be ready..." - timeout=${{ inputs.timeout }} - counter=0 - while [ $counter -lt $timeout ]; do - if curl -s ${{ inputs.test-server-url }}/health >/dev/null 2>&1; then - echo "โœ… Kora RPC server health check passed!" - break - fi - sleep 1 - counter=$((counter + 1)) - done - - if [ $counter -eq $timeout ]; then - echo "โŒ Kora RPC server failed to start within $timeout seconds" - jobs - exit 1 - fi - - - name: Initialize payment ATAs - if: ${{ inputs.initialize-atas == 'true' }} - shell: bash - run: | - echo "๐Ÿ”ง Initializing payment ATAs..." - - # Set environment variables for signers - export KORA_PRIVATE_KEY="$(cat tests/src/common/local-keys/fee-payer-local.json)" - - # Set second signer if it exists (for multi-signer configs) - if [ -f "tests/src/common/local-keys/signer2-local.json" ]; then - export KORA_PRIVATE_KEY_2="$(cat tests/src/common/local-keys/signer2-local.json)" - fi - - cargo run -p kora-cli --bin kora -- \ - --config ${{ inputs.config-file }} \ - --rpc-url ${{ inputs.rpc-url }} \ - rpc initialize-atas \ - --signers-config ${{ inputs.signers-config }} - - # Additional wait to ensure server is fully initialized - echo "โณ Ensuring RPC server is fully initialized..." - sleep 3 \ No newline at end of file diff --git a/.github/actions/setup-solana-validator/action.yml b/.github/actions/setup-solana-validator/action.yml deleted file mode 100644 index 8fadd3aa..00000000 --- a/.github/actions/setup-solana-validator/action.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: "Setup Solana Validator" -description: "Start Solana test validator with health check" - -inputs: - rpc-url: - description: "Solana RPC URL" - required: false - default: "http://127.0.0.1:8899" - timeout: - description: "Timeout in seconds to wait for validator" - required: false - default: "60" - -outputs: - rpc-url: - description: "Solana RPC URL" - value: ${{ inputs.rpc-url }} - -runs: - using: "composite" - steps: - - name: Start Solana test validator - shell: bash - run: | - echo "๐Ÿš€ Starting Solana test validator..." - - - # 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 }}!" - break - fi - 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 diff --git a/.github/workflows/build-rust.yml b/.github/workflows/build-rust.yml new file mode 100644 index 00000000..f8d0207a --- /dev/null +++ b/.github/workflows/build-rust.yml @@ -0,0 +1,39 @@ +name: Build Rust Artifacts + +on: + workflow_call: + inputs: + cache-key: + description: "Cache key suffix for rust cache" + required: true + type: string + artifact-name: + description: "Name for the uploaded artifact" + required: true + type: string + +jobs: + build: + name: Build Rust Artifacts + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + + - uses: Swatinem/rust-cache@v2 + with: + shared-key: ${{ inputs.cache-key }} + + - name: Build workspace + run: make build + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.artifact-name }} + path: | + target/debug/kora + target/debug/test_runner + retention-days: 1 \ No newline at end of file diff --git a/.github/workflows/rust-unit.yml b/.github/workflows/rust-unit.yml new file mode 100644 index 00000000..b22ba6e6 --- /dev/null +++ b/.github/workflows/rust-unit.yml @@ -0,0 +1,128 @@ +name: Rust Unit Tests + +on: + push: + branches: [main, "release/*"] + paths: + - "crates/**" + - "Cargo.*" + - "Makefile" + - ".github/workflows/rust-unit.yml" + pull_request: + branches: [main, "release/*"] + paths: + - "crates/**" + - "Cargo.*" + - "Makefile" + - ".github/workflows/rust-unit.yml" + +env: + CARGO_TERM_COLOR: always + RUST_LOG: info + CI: true + +jobs: + test: + name: Rust Unit Tests & Coverage + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + pull-requests: write + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy, llvm-tools-preview + - uses: Swatinem/rust-cache@v2 + - name: Check formatting + run: make check + - name: Run clippy + run: make lint + - name: Build + run: make build + + - name: Setup Solana CLI + uses: ./.github/actions/setup-solana + + - name: Install cargo-llvm-cov for coverage + run: cargo install cargo-llvm-cov + + - name: Run unit tests with coverage + run: | + echo "๐Ÿงช Running unit tests with coverage instrumentation..." + cargo llvm-cov clean --workspace + cargo llvm-cov test --no-report --workspace --lib + + - name: Generate coverage reports + run: | + echo "๐Ÿ“Š Generating coverage reports..." + mkdir -p coverage + cargo llvm-cov report --lcov --output-path coverage/lcov.info + + - name: Display coverage summary + run: | + echo "๐Ÿ“Š Coverage Summary:" + if [ -f "coverage/lcov.info" ]; then + echo "โœ… Coverage report generated successfully" + echo "๐Ÿ“„ Generated: coverage/lcov.info" + else + echo "โŒ Coverage report not found" + fi + + - name: Upload coverage artifacts + uses: actions/upload-artifact@v4 + with: + name: rust-unit-coverage-report + path: coverage/ + retention-days: 30 + + - name: Update PR description with coverage badge + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository + uses: actions/github-script@v7 + with: + script: | + // Extract coverage percentage from lcov.info + const fs = require('fs'); + let coverage = '0'; + + try { + const lcov = fs.readFileSync('coverage/lcov.info', 'utf8'); + const linesFound = lcov.match(/^LF:(\d+)$/gm)?.reduce((sum, line) => sum + parseInt(line.split(':')[1]), 0) || 0; + const linesHit = lcov.match(/^LH:(\d+)$/gm)?.reduce((sum, line) => sum + parseInt(line.split(':')[1]), 0) || 0; + coverage = linesFound > 0 ? ((linesHit / linesFound) * 100).toFixed(1) : '0'; + } catch (error) { + console.log('Error reading coverage:', error); + } + + // Determine badge color + let color = 'red'; + if (parseFloat(coverage) >= 80) color = 'green'; + else if (parseFloat(coverage) >= 60) color = 'yellow'; + + // Get current PR + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + }); + + // Create coverage badge section + const coverageBadge = `![Coverage](https://img.shields.io/badge/coverage-${coverage}%25-${color})`; + const coverageSection = `\n\n## ๐Ÿ“Š Unit Test Coverage\n${coverageBadge}\n\n**Unit Test Coverage: ${coverage}%**\n\n[View Detailed Coverage Report](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`; + + // Update PR body + let newBody = pr.body || ''; + + // Remove existing coverage section if present + newBody = newBody.replace(/\n## ๐Ÿ“Š Unit Test Coverage[\s\S]*?(?=\n## |\n$|$)/g, ''); + + // Add new coverage section + newBody += coverageSection; + + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + body: newBody + }); \ No newline at end of file diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 04ef52d6..ed799864 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,20 +1,28 @@ -name: Rust CI +name: Rust Integration Tests on: push: branches: [main, "release/*"] paths: - "crates/**" + - "tests/**" - "Cargo.*" - "Makefile" + - "makefiles/**" - ".github/workflows/rust.yml" + - ".github/workflows/build-rust.yml" + - ".github/actions/run-test-runner/**" pull_request: branches: [main, "release/*"] paths: - "crates/**" + - "tests/**" - "Cargo.*" - "Makefile" + - "makefiles/**" - ".github/workflows/rust.yml" + - ".github/workflows/build-rust.yml" + - ".github/actions/run-test-runner/**" env: CARGO_TERM_COLOR: always @@ -22,208 +30,47 @@ env: CI: true jobs: - test: - name: Rust Tests & Coverage + build: + name: Build Artifacts + uses: ./.github/workflows/build-rust.yml + with: + cache-key: "rust-integration-build" + artifact-name: "rust-binaries" + + integration: + name: Integration Tests runs-on: ubuntu-latest - timeout-minutes: 30 - permissions: - contents: read - pull-requests: write + needs: build + timeout-minutes: 25 + strategy: + fail-fast: false + matrix: + test-group: [regular, auth, payment_address, multi_signer] steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - with: - components: rustfmt, clippy, llvm-tools-preview - - uses: Swatinem/rust-cache@v2 - - name: Check formatting - run: make check - - name: Run clippy - run: make lint - - name: Build - run: make build - - - name: Setup Solana CLI - uses: ./.github/actions/setup-solana - - - name: Install cargo-llvm-cov for coverage - run: cargo install cargo-llvm-cov - - - name: Run unit tests with coverage - run: | - echo "๐Ÿงช Running unit tests with coverage instrumentation..." - cargo llvm-cov clean --workspace - cargo llvm-cov test --no-report --workspace --lib - - - name: Setup Solana test validator - uses: ./.github/actions/setup-solana-validator - - - name: Setup test environment - run: | - echo "๐Ÿ”ง Setting up test environment..." - # Create second signer key for multi-signer tests - if [ ! -f "tests/src/common/local-keys/signer2-local.json" ]; then - echo "Creating second signer key..." - solana-keygen new --outfile tests/src/common/local-keys/signer2-local.json --no-bip39-passphrase --silent - fi - KORA_PRIVATE_KEY="$(cat tests/src/common/local-keys/fee-payer-local.json)" cargo run -p tests --bin setup_test_env - - - name: Setup Kora RPC server (regular config) - uses: ./.github/actions/setup-kora-rpc - with: - config-file: "tests/src/common/fixtures/kora-test.toml" - - - name: Run RPC integration tests - run: | - 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..." - cargo llvm-cov test --no-report -p tests --test tokens - - - name: Run external integration tests - run: | - echo "๐Ÿงช Running external integration tests..." - cargo llvm-cov test --no-report -p tests --test external - - - name: Stop Kora RPC server - run: | - if [ ! -z "$KORA_PID" ]; then - kill $KORA_PID || true - fi - sleep 2 - - - name: Setup Kora RPC server (auth config) - uses: ./.github/actions/setup-kora-rpc - with: - config-file: "tests/src/common/fixtures/auth-test.toml" - - name: Run auth integration tests - run: | - echo "๐Ÿงช Running auth integration tests..." - cargo llvm-cov test --no-report -p tests --test auth - - - name: Stop Kora RPC server - run: | - if [ ! -z "$KORA_PID" ]; then - kill $KORA_PID || true - fi - sleep 2 + - uses: dtolnay/rust-toolchain@stable - - name: Setup Kora RPC server (payment address config) - uses: ./.github/actions/setup-kora-rpc + - uses: Swatinem/rust-cache@v2 with: - config-file: "tests/src/common/fixtures/paymaster-address-test.toml" - initialize-atas: "true" - - - name: Run payment address integration tests - run: | - echo "๐Ÿงช Running payment address integration tests..." - cargo llvm-cov test --no-report -p tests --test payment_address - - - name: Stop Kora RPC server - run: | - if [ ! -z "$KORA_PID" ]; then - kill $KORA_PID || true - fi - sleep 2 + shared-key: "rust-integration" - - name: Setup multi-signer test environment - run: | - echo "๐Ÿ”ง Setting up multi-signer test environment..." - export KORA_PRIVATE_KEY="$(cat tests/src/common/local-keys/fee-payer-local.json)" - export KORA_PRIVATE_KEY_2="$(cat tests/src/common/local-keys/signer2-local.json)" - cargo run -p tests --bin setup_test_env - - - name: Setup Kora RPC server (multi-signer config) - uses: ./.github/actions/setup-kora-rpc + - name: Download build artifacts + uses: actions/download-artifact@v4 with: - config-file: "tests/src/common/fixtures/kora-test.toml" - signers-config: "tests/src/common/fixtures/multi-signers.toml" - - - name: Run multi-signer integration tests - run: | - echo "๐Ÿงช Running multi-signer integration tests..." - cargo llvm-cov test --no-report -p tests --test multi_signer + name: rust-binaries + path: target/debug/ - - name: Generate coverage reports - run: | - echo "๐Ÿ“Š Generating coverage reports..." - mkdir -p coverage - cargo llvm-cov report --lcov --output-path coverage/lcov.info + - name: Make binaries executable + run: chmod +x target/debug/kora target/debug/test_runner - - name: Display coverage summary - run: | - echo "๐Ÿ“Š Coverage Summary:" - if [ -f "coverage/lcov.info" ]; then - echo "โœ… Coverage report generated successfully" - echo "๐Ÿ“„ Generated: coverage/lcov.info" - else - echo "โŒ Coverage report not found" - fi - - - name: Upload coverage artifacts - uses: actions/upload-artifact@v4 - with: - name: rust-coverage-report - path: coverage/ - retention-days: 30 + - name: Setup Solana CLI + uses: ./.github/actions/setup-solana - - name: Update PR description with coverage badge - if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository - uses: actions/github-script@v7 + - name: Run integration tests + uses: ./.github/actions/run-test-runner with: - script: | - // Extract coverage percentage from lcov.info - const fs = require('fs'); - let coverage = '0'; - - try { - const lcov = fs.readFileSync('coverage/lcov.info', 'utf8'); - const linesFound = lcov.match(/^LF:(\d+)$/gm)?.reduce((sum, line) => sum + parseInt(line.split(':')[1]), 0) || 0; - const linesHit = lcov.match(/^LH:(\d+)$/gm)?.reduce((sum, line) => sum + parseInt(line.split(':')[1]), 0) || 0; - coverage = linesFound > 0 ? ((linesHit / linesFound) * 100).toFixed(1) : '0'; - } catch (error) { - console.log('Error reading coverage:', error); - } - - // Determine badge color - let color = 'red'; - if (parseFloat(coverage) >= 80) color = 'green'; - else if (parseFloat(coverage) >= 60) color = 'yellow'; - - // Get current PR - const { data: pr } = await github.rest.pulls.get({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: context.issue.number, - }); - - // Create coverage badge section - const coverageBadge = `![Coverage](https://img.shields.io/badge/coverage-${coverage}%25-${color})`; - const coverageSection = `\n\n## ๐Ÿ“Š Test Coverage\n${coverageBadge}\n\n**Coverage: ${coverage}%**\n\n[View Detailed Coverage Report](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`; - - // Update PR body - let newBody = pr.body || ''; - - // Remove existing coverage section if present - newBody = newBody.replace(/\n## ๐Ÿ“Š Test Coverage[\s\S]*?(?=\n## |\n$|$)/g, ''); - - // Add new coverage section - newBody += coverageSection; - - await github.rest.pulls.update({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: context.issue.number, - body: newBody - }); + filters: "--filter ${{ matrix.test-group }}" - name: Cleanup test environment if: always() @@ -233,4 +80,4 @@ jobs: if: failure() uses: ./.github/actions/show-failure-logs with: - test-type: "Rust integration" + test-type: "Rust integration (${{ matrix.test-group }})" \ No newline at end of file diff --git a/.github/workflows/typescript-integration.yml b/.github/workflows/typescript-integration.yml index 4e2f11b8..6643bc98 100644 --- a/.github/workflows/typescript-integration.yml +++ b/.github/workflows/typescript-integration.yml @@ -10,9 +10,13 @@ on: - "sdks/ts/**" # Backend changes that affect TS integration - "crates/**" + - "tests/**" - "Cargo.*" - "Makefile" + - "makefiles/**" - ".github/workflows/typescript-integration.yml" + - ".github/workflows/build-rust.yml" + - ".github/actions/run-test-runner/**" pull_request: branches: [main, "release/*"] @@ -21,38 +25,56 @@ on: - "sdks/ts/**" # Backend changes that affect TS integration - "crates/**" + - "tests/**" - "Cargo.*" - "Makefile" + - "makefiles/**" - ".github/workflows/typescript-integration.yml" + - ".github/workflows/build-rust.yml" + - ".github/actions/run-test-runner/**" env: CARGO_TERM_COLOR: always RUST_LOG: info jobs: + build: + name: Build Rust Backend + uses: ./.github/workflows/build-rust.yml + with: + cache-key: "typescript-integration-build" + artifact-name: "rust-binaries-ts" + typescript-integration: name: TypeScript SDK Integration Tests runs-on: ubuntu-latest - timeout-minutes: 25 + needs: build + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + test-group: [typescript_basic, typescript_auth, typescript_turnkey, typescript_privy] steps: - - name: Checkout repository - uses: actions/checkout@v4 + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable - - name: Setup Rust toolchain - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + shared-key: "typescript-integration-${{ matrix.test-group }}" - - name: Cache Rust dependencies - uses: Swatinem/rust-cache@v2 + - name: Download build artifacts + uses: actions/download-artifact@v4 with: - shared-key: "typescript-integration" - cache-on-failure: true + name: rust-binaries-ts + path: target/debug/ + + - name: Make binaries executable + run: chmod +x target/debug/kora target/debug/test_runner - name: Setup Solana CLI uses: ./.github/actions/setup-solana - - name: Setup Kora RPC server - uses: ./.github/actions/setup-kora-rpc - - name: Setup Node.js uses: actions/setup-node@v4 with: @@ -84,34 +106,16 @@ jobs: run: pnpm build - name: Run TypeScript integration tests - working-directory: sdks/ts - run: | - echo "๐Ÿงช Running TypeScript SDK integration tests..." - # The test script handles starting/stopping its own validator - pnpm test:ci:integration - - - name: Stop Kora RPC server - run: | - if [ ! -z "$KORA_PID" ]; then - kill $KORA_PID || true - fi - sleep 2 - - - name: Setup Kora RPC server (auth config) - uses: ./.github/actions/setup-kora-rpc + uses: ./.github/actions/run-test-runner with: - config-file: "tests/src/common/fixtures/auth-test.toml" - - - name: Run TypeScript auth integration tests - working-directory: sdks/ts - run: | - echo "๐Ÿงช Running TypeScript SDK auth integration tests..." - pnpm test:ci:integration:auth + filters: "--filter ${{ matrix.test-group }}" - name: Cleanup test environment + if: always() uses: ./.github/actions/cleanup-test-env - name: Show failure logs + if: failure() uses: ./.github/actions/show-failure-logs with: - test-type: "TypeScript integration" + test-type: "TypeScript integration (${{ matrix.test-group }})" diff --git a/.gitignore b/.gitignore index a64ecf6c..db3b5603 100644 --- a/.gitignore +++ b/.gitignore @@ -20,11 +20,11 @@ coverage/ .DS_Store # Make test files -.validator.pid -.kora.pid +*.pid test-ledger/ .local/ tests/src/common/fixtures/lookup_tables.json +tests/src/common/fixtures/test-accounts/ # Docs sdks/ts/docs-html/ @@ -32,4 +32,4 @@ sdks/ts/docs-md/ docs/api-reference-generated/ # AI -.claude/ \ No newline at end of file +.claude/ diff --git a/Cargo.lock b/Cargo.lock index 6865d444..487edc2b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7996,6 +7996,7 @@ dependencies = [ "base64 0.22.1", "bincode", "bs58 0.5.1", + "chrono", "clap", "colored", "dotenv", diff --git a/Makefile b/Makefile index ffd9cef1..9d7bdffd 100644 --- a/Makefile +++ b/Makefile @@ -13,5 +13,5 @@ include makefiles/METRICS.makefile # Default target all: check test build -# Run all tests (unit + integration + TypeScript) -test-all: test test-integration test-ts \ No newline at end of file +# Run all tests (unit + TypeScript + integration) +test-all: test test-ts test-integration \ No newline at end of file diff --git a/crates/lib/src/rpc_server/method/transfer_transaction.rs b/crates/lib/src/rpc_server/method/transfer_transaction.rs index 5d81ee04..93c6f5ab 100644 --- a/crates/lib/src/rpc_server/method/transfer_transaction.rs +++ b/crates/lib/src/rpc_server/method/transfer_transaction.rs @@ -112,7 +112,7 @@ pub async fn transfer_transaction( } let blockhash = - rpc_client.get_latest_blockhash_with_commitment(CommitmentConfig::finalized()).await?; + rpc_client.get_latest_blockhash_with_commitment(CommitmentConfig::confirmed()).await?; let message = VersionedMessage::Legacy(Message::new_with_blockhash( &instructions, diff --git a/crates/lib/src/transaction/versioned_transaction.rs b/crates/lib/src/transaction/versioned_transaction.rs index bbc9b58a..7fe819f9 100644 --- a/crates/lib/src/transaction/versioned_transaction.rs +++ b/crates/lib/src/transaction/versioned_transaction.rs @@ -242,7 +242,7 @@ impl VersionedTransactionOps for VersionedTransactionResolved { if transaction.signatures.is_empty() { let blockhash = rpc_client - .get_latest_blockhash_with_commitment(CommitmentConfig::finalized()) + .get_latest_blockhash_with_commitment(CommitmentConfig::confirmed()) .await?; transaction.message.set_recent_blockhash(blockhash.0); } diff --git a/crates/lib/src/validator/config_validator.rs b/crates/lib/src/validator/config_validator.rs index d33d069a..fbfeee1b 100644 --- a/crates/lib/src/validator/config_validator.rs +++ b/crates/lib/src/validator/config_validator.rs @@ -355,6 +355,7 @@ mod tests { }, }; use serial_test::serial; + use solana_commitment_config::CommitmentConfig; use super::*; @@ -385,7 +386,10 @@ mod tests { config.validation.allowed_tokens = vec![]; let _ = update_config(config); - let rpc_client = RpcClient::new("http://localhost:8899".to_string()); + let rpc_client = RpcClient::new_with_commitment( + "http://localhost:8899".to_string(), + CommitmentConfig::confirmed(), + ); let result = ConfigValidator::validate(&rpc_client).await; assert!(result.is_err()); assert!(matches!(result.unwrap_err(), KoraError::InternalServerError(_))); @@ -419,7 +423,10 @@ mod tests { // Initialize global config let _ = update_config(config); - let rpc_client = RpcClient::new("http://localhost:8899".to_string()); + let rpc_client = RpcClient::new_with_commitment( + "http://localhost:8899".to_string(), + CommitmentConfig::confirmed(), + ); let result = ConfigValidator::validate_with_result(&rpc_client, true).await; assert!(result.is_ok()); let warnings = result.unwrap(); @@ -467,7 +474,10 @@ mod tests { // Initialize global config let _ = update_config(config); - let rpc_client = RpcClient::new("http://localhost:8899".to_string()); + let rpc_client = RpcClient::new_with_commitment( + "http://localhost:8899".to_string(), + CommitmentConfig::confirmed(), + ); let result = ConfigValidator::validate_with_result(&rpc_client, true).await; assert!(result.is_ok()); let warnings = result.unwrap(); @@ -504,7 +514,10 @@ mod tests { // Initialize global config let _ = update_config(config); - let rpc_client = RpcClient::new("http://localhost:8899".to_string()); + let rpc_client = RpcClient::new_with_commitment( + "http://localhost:8899".to_string(), + CommitmentConfig::confirmed(), + ); let result = ConfigValidator::validate_with_result(&rpc_client, true).await; assert!(result.is_ok()); let warnings = result.unwrap(); @@ -539,7 +552,10 @@ mod tests { let _ = update_config(config); - let rpc_client = RpcClient::new("http://localhost:8899".to_string()); + let rpc_client = RpcClient::new_with_commitment( + "http://localhost:8899".to_string(), + CommitmentConfig::confirmed(), + ); let result = ConfigValidator::validate_with_result(&rpc_client, true).await; assert!(result.is_err()); let errors = result.unwrap_err(); @@ -579,7 +595,10 @@ mod tests { let _ = update_config(config); - let rpc_client = RpcClient::new("http://localhost:8899".to_string()); + let rpc_client = RpcClient::new_with_commitment( + "http://localhost:8899".to_string(), + CommitmentConfig::confirmed(), + ); let result = ConfigValidator::validate_with_result(&rpc_client, true).await; assert!(result.is_err()); let errors = result.unwrap_err(); @@ -616,7 +635,10 @@ mod tests { let _ = update_config(config); - let rpc_client = RpcClient::new("http://localhost:8899".to_string()); + let rpc_client = RpcClient::new_with_commitment( + "http://localhost:8899".to_string(), + CommitmentConfig::confirmed(), + ); let result = ConfigValidator::validate_with_result(&rpc_client, true).await; assert!(result.is_err()); let errors = result.unwrap_err(); @@ -661,7 +683,10 @@ mod tests { let _ = update_config(config); - let rpc_client = RpcClient::new("http://localhost:8899".to_string()); + let rpc_client = RpcClient::new_with_commitment( + "http://localhost:8899".to_string(), + CommitmentConfig::confirmed(), + ); let result = ConfigValidator::validate_with_result(&rpc_client, true).await; assert!(result.is_ok()); let warnings = result.unwrap(); @@ -693,7 +718,10 @@ mod tests { let _ = update_config(config); - let rpc_client = RpcClient::new("http://localhost:8899".to_string()); + let rpc_client = RpcClient::new_with_commitment( + "http://localhost:8899".to_string(), + CommitmentConfig::confirmed(), + ); let result = ConfigValidator::validate_with_result(&rpc_client, true).await; assert!(result.is_err()); let errors = result.unwrap_err(); @@ -980,7 +1008,10 @@ mod tests { let _ = update_config(config); - let rpc_client = RpcClient::new("http://localhost:8899".to_string()); + let rpc_client = RpcClient::new_with_commitment( + "http://localhost:8899".to_string(), + CommitmentConfig::confirmed(), + ); let result = ConfigValidator::validate_with_result(&rpc_client, true).await; assert!(result.is_ok()); } @@ -1011,7 +1042,10 @@ mod tests { let _ = update_config(config); - let rpc_client = RpcClient::new("http://localhost:8899".to_string()); + let rpc_client = RpcClient::new_with_commitment( + "http://localhost:8899".to_string(), + CommitmentConfig::confirmed(), + ); let result = ConfigValidator::validate_with_result(&rpc_client, true).await; assert!(result.is_err()); let errors = result.unwrap_err(); @@ -1046,7 +1080,10 @@ mod tests { let _ = update_config(config); - let rpc_client = RpcClient::new("http://localhost:8899".to_string()); + let rpc_client = RpcClient::new_with_commitment( + "http://localhost:8899".to_string(), + CommitmentConfig::confirmed(), + ); let result = ConfigValidator::validate_with_result(&rpc_client, true).await; assert!(result.is_err()); let errors = result.unwrap_err(); diff --git a/makefiles/BUILD.makefile b/makefiles/BUILD.makefile index a9e9cc7c..7638df05 100644 --- a/makefiles/BUILD.makefile +++ b/makefiles/BUILD.makefile @@ -1,6 +1,6 @@ # install install: - cargo install --path crates/cli + cargo install --path crates/cli --bin kora # Check code formatting check: @@ -28,14 +28,6 @@ build-lib: build-cli: cargo build -p kora-cli -# Build tk-rs -build-tk: - cargo build -p tk-rs - -# Run presigned release binary -run-presigned: - cargo run --bin presigned - # Run with default configuration run: cargo run -p kora-cli --bin kora -- --config kora.toml --rpc-url http://127.0.0.1:8899 rpc start --signers-config $(TEST_SIGNERS_CONFIG) diff --git a/makefiles/CLIENT.makefile b/makefiles/CLIENT.makefile index 8600a5ac..cc0ebbfb 100644 --- a/makefiles/CLIENT.makefile +++ b/makefiles/CLIENT.makefile @@ -1,8 +1,6 @@ # Generate TypeScript client -gen-ts-client: - @echo "๐Ÿ”ง Generating OpenAPI spec with docs feature..." - cargo run -p kora-cli --bin kora --features docs -- openapi -o openapi.json +gen-ts-client: openapi @echo "๐Ÿš€ Generating TypeScript client..." docker run --rm -v "${PWD}:/local" openapitools/openapi-generator-cli generate \ -i /local/crates/lib/src/rpc_server/openapi/spec/combined_api.json \ diff --git a/makefiles/METRICS.makefile b/makefiles/METRICS.makefile index f9bb769f..0131ff83 100644 --- a/makefiles/METRICS.makefile +++ b/makefiles/METRICS.makefile @@ -12,6 +12,4 @@ update-metrics-config: # Run metrics (Prometheus + Grafana) - automatically updates config first run-metrics: update-metrics-config cd crates/lib/src/metrics && docker compose -f docker-compose.metrics.yml down - cd crates/lib/src/metrics && docker compose -f docker-compose.metrics.yml up - -# install ts sdk \ No newline at end of file + cd crates/lib/src/metrics && docker compose -f docker-compose.metrics.yml up \ No newline at end of file diff --git a/makefiles/RUST_TESTS.makefile b/makefiles/RUST_TESTS.makefile index 5e6decfb..e1e3a68e 100644 --- a/makefiles/RUST_TESTS.makefile +++ b/makefiles/RUST_TESTS.makefile @@ -5,62 +5,27 @@ test: # 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 +# Run all integration tests using new config-driven test runner test-integration: $(call print_header,KORA INTEGRATION TEST SUITE) - $(call print_step,Initializing test infrastructure) - @$(call start_solana_validator) - $(call print_substep,Setting up base test environment...) - @KORA_PRIVATE_KEY="$$(cat tests/src/common/local-keys/fee-payer-local.json)" cargo run -p tests --bin setup_test_env $(QUIET_OUTPUT) - $(call print_success,Infrastructure ready) - - @$(call run_integration_phase,1,RPC tests,$(REGULAR_CONFIG),,--test rpc,) - @$(call run_integration_phase,2,token tests,$(REGULAR_CONFIG),,--test tokens,) - @$(call run_integration_phase,3,external tests,$(REGULAR_CONFIG),,--test external,) - @$(call run_integration_phase,4,auth tests,$(AUTH_CONFIG),,--test auth,) - @$(call run_integration_phase,5,payment address tests,$(PAYMENT_ADDRESS_CONFIG),,--test payment_address,true) - @$(call run_multi_signer_phase,6,multi-signer tests,$(REGULAR_CONFIG),$(MULTI_SIGNERS_CONFIG),--test multi_signer) - - $(call print_header,TEST SUITE COMPLETE) - @$(call stop_solana_validator) - -# Individual test targets for development -test-regular: - $(call print_header,REGULAR TESTS) - @$(call start_solana_validator) - @cargo run -p tests --bin setup_test_env $(QUIET_OUTPUT) - $(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) - @cargo run -p tests --bin setup_test_env $(QUIET_OUTPUT) - $(call run_integration_phase,1,Authentication Tests,$(AUTH_CONFIG),,--test auth,) - @$(call stop_solana_validator) - -test-payment: - $(call print_header,PAYMENT ADDRESS TESTS) - @$(call start_solana_validator) - @cargo run -p tests --bin setup_test_env $(QUIET_OUTPUT) - $(call run_integration_phase,1,Payment Address Tests,$(PAYMENT_ADDRESS_CONFIG),,--test payment_address,true) - @$(call stop_solana_validator) - -test-multi-signer: - $(call print_header,MULTI-SIGNER TESTS) - @$(call start_solana_validator) - $(call run_multi_signer_phase,1,Multi-Signer Tests,$(REGULAR_CONFIG),$(MULTI_SIGNERS_CONFIG),--test multi-signers) - @$(call stop_solana_validator) \ No newline at end of file + @cargo run -p tests --bin test_runner + +# Verbose integration tests (shows detailed output) +test-integration-verbose: + $(call print_header,KORA INTEGRATION TEST SUITE - VERBOSE) + @cargo run -p tests --bin test_runner -- --verbose + +# Force refresh test accounts (ignore cached) +test-integration-fresh: + $(call print_header,KORA INTEGRATION TEST SUITE - FRESH SETUP) + @cargo run -p tests --bin test_runner -- --force-refresh + +# Run specific test phases with filters (for CI) +test-integration-filtered: + $(call print_header,KORA INTEGRATION TEST SUITE - FILTERED) + @cargo run -p tests --bin test_runner -- $(FILTERS) \ No newline at end of file diff --git a/makefiles/TYPESCRIPT_TESTS.makefile b/makefiles/TYPESCRIPT_TESTS.makefile index 16aeb1e8..789b6717 100644 --- a/makefiles/TYPESCRIPT_TESTS.makefile +++ b/makefiles/TYPESCRIPT_TESTS.makefile @@ -1,46 +1,9 @@ -# Run TypeScript SDK tests with local validator and Kora node +# TypeScript SDK Tests +# NOTE: TypeScript integration tests are now integrated into the main test runner +# Use 'make test-integration' to run all tests including TypeScript phases + test-ts-unit: - @printf "Running TypeScript SDK tests (unit tests)...\n" + @printf "Running TypeScript SDK unit tests...\n" -@cd sdks/ts && pnpm test:unit - -test-ts-integration-basic: - @$(call start_solana_validator) - @$(call start_kora_server,node for TS tests,cargo run,,$(REGULAR_CONFIG),) - @printf "Running TypeScript SDK tests (basic config)...\n" - -@cd sdks/ts && pnpm test:integration - @$(call stop_kora_server) - @$(call stop_solana_validator) - -# Run TypeScript auth tests specifically (using integration tests with auth enabled) -test-ts-integration-auth: - @$(call start_solana_validator) - @$(call start_kora_server,node for TS auth tests,cargo run,,$(AUTH_CONFIG),) - @printf "Running TypeScript SDK auth tests...\n" - -@cd sdks/ts && pnpm test:integration:auth - @$(call stop_kora_server) - @$(call stop_solana_validator) - -# Run TypeScript tests with Turnkey signer -test-ts-integration-turnkey: - @$(call start_solana_validator) - @$(call start_kora_server,node for TS Turnkey tests,cargo run,,$(REGULAR_CONFIG),,$(TEST_SIGNERS_TURNKEY_CONFIG)) - @printf "Running TypeScript SDK tests with Turnkey signer...\n" - -@cd sdks/ts && pnpm test:integration:turnkey - @$(call stop_kora_server) - @$(call stop_solana_validator) - -# Run TypeScript tests with Privy signer -test-ts-integration-privy: - @$(call start_solana_validator) - @$(call start_kora_server,node for TS Privy tests,cargo run,,$(REGULAR_CONFIG),,$(TEST_SIGNERS_PRIVY_CONFIG)) - @printf "Running TypeScript SDK tests with Privy signer...\n" - -@cd sdks/ts && pnpm test:integration:privy - @$(call stop_kora_server) - @$(call stop_solana_validator) - -# Run all signer tests -test-ts-signers: test-ts-integration-turnkey test-ts-integration-privy - -# Run all TypeScript SDK tests (no signers b/c api rate limits) -test-ts: test-ts-unit test-ts-integration-basic test-ts-integration-auth # test-ts-signers +test-ts: test-ts-unit diff --git a/makefiles/UTILS.makefile b/makefiles/UTILS.makefile index 83679183..4a1dc228 100644 --- a/makefiles/UTILS.makefile +++ b/makefiles/UTILS.makefile @@ -1,32 +1,11 @@ # Color codes for terminal output -RED := \033[0;31m GREEN := \033[0;32m -YELLOW := \033[1;33m BLUE := \033[0;34m -MAGENTA := \033[0;35m -CYAN := \033[0;36m BOLD := \033[1m RESET := \033[0m -# Common configuration -TEST_PORT := 8080 -TEST_RPC_URL := http://127.0.0.1:8899 -TEST_SIGNERS_CONFIG := tests/src/common/fixtures/signers.toml -TEST_SIGNERS_TURNKEY_CONFIG := tests/src/common/fixtures/signers-turnkey.toml -TEST_SIGNERS_PRIVY_CONFIG := tests/src/common/fixtures/signers-privy.toml -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) -SERVER_TIMEOUT := $(if $(CI),20,30) - # Output control patterns QUIET_OUTPUT := >/dev/null 2>&1 -TEST_OUTPUT_FILTER := 2>&1 | grep -E "(test |running |ok$$|FAILED|failed|error:|Error:|ERROR)" | grep -v "running 0 tests" || true # Clean structured output functions define print_header @@ -35,187 +14,10 @@ define print_header @printf "================================================================================\n$(RESET)" endef -define print_phase - @printf "\n$(BOLD)$(CYAN)[Phase $(1)] $(2)$(RESET)\n" - @printf "$(CYAN)--------------------------------------------------------------------------------$(RESET)\n" -endef - -define print_step - @printf " $(GREEN)โ†’$(RESET) $(1)\n" -endef - -define print_substep - @printf " $(YELLOW)โ€ข$(RESET) $(1)\n" -endef - define print_success @printf " $(GREEN)โœ“$(RESET) $(1)\n" endef -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 - @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 \ - if solana cluster-version --url $(TEST_RPC_URL) >/dev/null 2>&1; then \ - break; \ - fi; \ - sleep 1; \ - counter=$$((counter + 1)); \ - done; \ - if [ $$counter -eq $(VALIDATOR_TIMEOUT) ]; then \ - printf " $(RED)โœ—$(RESET) Validator failed to start\n"; \ - exit 1; \ - fi - $(call print_substep,Validator ready on port 8899) -endef - -define stop_solana_validator - @if [ -f .validator.pid ]; then \ - PID=$$(cat .validator.pid); \ - if kill -0 $$PID 2>/dev/null; then \ - kill $$PID 2>/dev/null || true; \ - sleep 1; \ - kill -9 $$PID 2>/dev/null || true; \ - fi; \ - rm -f .validator.pid; \ - fi; \ - pkill -f "solana-test-validator" 2>/dev/null || true; \ - sleep 2; \ - rm -rf test-ledger 2>/dev/null || true -endef - -# Start Kora server with flexible configuration -# Usage: $(call start_kora_server,description,cargo_cmd,cargo_flags,config_file,setup_env,signers_config) -define start_kora_server - @$(call stop_kora_server) - @$(if $(5),\ - printf " $(YELLOW)โ€ข$(RESET) Setting up test environment...\n"; \ - KORA_PRIVATE_KEY="$$(cat tests/src/common/local-keys/fee-payer-local.json)" cargo run -p tests --bin setup_test_env $(QUIET_OUTPUT) || exit 1;) - $(call print_substep,Starting Kora server with $(1) configuration...) - @$(if $(2),\ - KORA_PRIVATE_KEY="$$(cat tests/src/common/local-keys/fee-payer-local.json)" $(2) -p kora-cli --bin kora $(3) -- --config $(4) --rpc-url $(TEST_RPC_URL) rpc start --signers-config $(or $(6),$(TEST_SIGNERS_CONFIG)) --port $(TEST_PORT) $(QUIET_OUTPUT) &,\ - make run $(QUIET_OUTPUT) &) - @echo $$! > .kora.pid - @counter=0; \ - while [ $$counter -lt $(SERVER_TIMEOUT) ]; do \ - if curl -s http://127.0.0.1:$(TEST_PORT)/liveness >/dev/null 2>&1; then \ - break; \ - fi; \ - sleep 1; \ - counter=$$((counter + 1)); \ - done; \ - if [ $$counter -eq $(SERVER_TIMEOUT) ]; then \ - printf " $(RED)โœ—$(RESET) Kora server failed to start\n"; \ - if [ -f .kora.pid ]; then \ - printf " $(YELLOW)โ€ข$(RESET) PID: $$(cat .kora.pid)\n"; \ - fi; \ - exit 1; \ - fi - $(call print_substep,Server ready on port $(TEST_PORT)) -endef - -# Server lifecycle management functions -define stop_kora_server - @if [ -f .kora.pid ]; then \ - PID=$$(cat .kora.pid); \ - if kill -0 $$PID 2>/dev/null; then \ - kill -TERM $$PID 2>/dev/null || true; \ - sleep 2; \ - if kill -0 $$PID 2>/dev/null; then \ - kill -9 $$PID 2>/dev/null || true; \ - fi; \ - fi; \ - rm -f .kora.pid; \ - fi; \ - pkill -f "kora.*rpc.*start" 2>/dev/null || true; \ - sleep 1; \ - lsof -ti:$(TEST_PORT) | xargs kill -9 2>/dev/null || true; \ - sleep 1 -endef - -define run_integration_phase - $(call print_phase,$(1),$(2)) - $(call print_step,Configuring test environment) - @$(call start_kora_server,$(2),cargo run,,$(3),$(4),$(7)) - @$(if $(6),\ - printf " $(YELLOW)โ€ข$(RESET) Initializing payment ATAs...\n"; \ - KORA_PRIVATE_KEY="$$(cat tests/src/common/local-keys/fee-payer-local.json)" cargo run -p kora-cli --bin kora -- --config $(3) --rpc-url $(TEST_RPC_URL) rpc initialize-atas --signers-config $(or $(7),$(TEST_SIGNERS_CONFIG)) $(QUIET_OUTPUT) || exit 1; \ - printf " $(YELLOW)โ€ข$(RESET) Payment ATAs ready\n";) - $(call print_step,Running tests...) - @cargo test -p tests --quiet $(5) -- --nocapture $(QUIET_OUTPUT) || exit 1 - @printf " $(GREEN)โœ“$(RESET) Tests passed\n" - @$(call stop_kora_server) - $(call print_success,Phase $(1) complete) -endef - -define run_multi_signer_phase - $(call print_phase,$(1),$(2)) - @$(call stop_kora_server) - @if [ ! -f "tests/src/common/local-keys/fee-payer-local.json" ]; then \ - $(call print_error,fee-payer-local.json not found); \ - exit 1; \ - fi - @if [ ! -f "tests/src/common/local-keys/signer2-local.json" ]; then \ - $(call print_error,signer2-local.json not found); \ - printf " Create it with: solana-keygen new --outfile tests/src/common/local-keys/signer2-local.json\n"; \ - exit 1; \ - fi - $(call print_step,Configuring multi-signer environment) - $(call print_substep,Setting up test accounts...) - @KORA_PRIVATE_KEY="$$(cat tests/src/common/local-keys/fee-payer-local.json)" \ - KORA_PRIVATE_KEY_2="$$(cat tests/src/common/local-keys/signer2-local.json)" \ - cargo run -p tests --bin setup_test_env $(QUIET_OUTPUT) || exit 1 - $(call print_substep,Starting server with multi-signer configuration...) - @KORA_PRIVATE_KEY="$$(cat tests/src/common/local-keys/fee-payer-local.json)" \ - KORA_PRIVATE_KEY_2="$$(cat tests/src/common/local-keys/signer2-local.json)" \ - cargo run -p kora-cli --bin kora -- --config $(3) --rpc-url $(TEST_RPC_URL) rpc start --signers-config $(4) --port $(TEST_PORT) $(QUIET_OUTPUT) & - @echo $$! > .kora.pid - @sleep 5 - $(call print_substep,Server ready on port $(TEST_PORT)) - $(call print_step,Running tests...) - @cargo test -p tests --quiet $(5) -- --nocapture $(QUIET_OUTPUT) || exit 1 - @printf " $(GREEN)โœ“$(RESET) Tests passed\n" - @$(call stop_kora_server) - $(call print_success,Phase $(1) complete) -endef - -# Setup test environment -setup-test-env: - $(call print_step,Setting up test environment...) - @KORA_PRIVATE_KEY="$$(cat tests/src/common/local-keys/fee-payer-local.json)" \ - cargo run -p tests --bin setup_test_env $(QUIET_OUTPUT) - $(call print_success,Test environment ready) - -# Clean up any running validators -clean-validator: - @$(call stop_solana_validator) - -# Clean up any running Kora nodes -clean-kora: - @$(call stop_kora_server) - -# Clean up both validator and Kora node -clean-test-env: clean-validator clean-kora - $(call print_success,Test environment cleaned up) - # Generate a random key that can be used as an API key or as an HMAC secret generate-key: @openssl rand -hex 32 \ No newline at end of file diff --git a/sdks/ts/src/client.ts b/sdks/ts/src/client.ts index 258daecc..6b4a65ce 100644 --- a/sdks/ts/src/client.ts +++ b/sdks/ts/src/client.ts @@ -97,12 +97,7 @@ export class KoraClient { const response = await fetch(this.rpcUrl, { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' }, - body: JSON.stringify({ - jsonrpc: '2.0', - id: 1, - method, - params, - } as RpcRequest), + body, }); const json = (await response.json()) as { error?: RpcError; result: T }; diff --git a/sdks/ts/test/auth-setup.ts b/sdks/ts/test/auth-setup.ts index cf189398..a60ae27b 100644 --- a/sdks/ts/test/auth-setup.ts +++ b/sdks/ts/test/auth-setup.ts @@ -1,10 +1,13 @@ import { KoraClient } from '../src/index.js'; +import { loadEnvironmentVariables } from './setup.js'; + +export function runAuthenticationTests() { + const { koraRpcUrl } = loadEnvironmentVariables(); -export function runAuthenticationTests(rpcUrl: string = 'http://localhost:8080/') { describe('Authentication', () => { it('should fail with incorrect API key', async () => { const client = new KoraClient({ - rpcUrl, + rpcUrl: koraRpcUrl, apiKey: 'WRONG-API-KEY', }); @@ -14,7 +17,7 @@ export function runAuthenticationTests(rpcUrl: string = 'http://localhost:8080/' it('should fail with incorrect HMAC secret', async () => { const client = new KoraClient({ - rpcUrl, + rpcUrl: koraRpcUrl, hmacSecret: 'WRONG-HMAC-SECRET', }); @@ -24,7 +27,7 @@ export function runAuthenticationTests(rpcUrl: string = 'http://localhost:8080/' it('should fail with both incorrect credentials', async () => { const client = new KoraClient({ - rpcUrl, + rpcUrl: koraRpcUrl, apiKey: 'WRONG-API-KEY', hmacSecret: 'WRONG-HMAC-SECRET', }); @@ -35,7 +38,7 @@ export function runAuthenticationTests(rpcUrl: string = 'http://localhost:8080/' it('should succeed with correct credentials', async () => { const client = new KoraClient({ - rpcUrl, + rpcUrl: koraRpcUrl, apiKey: 'test-api-key-123', hmacSecret: 'test-hmac-secret-456', }); @@ -49,7 +52,7 @@ export function runAuthenticationTests(rpcUrl: string = 'http://localhost:8080/' it('should fail when no credentials provided but auth is required', async () => { const client = new KoraClient({ - rpcUrl, + rpcUrl: koraRpcUrl, }); // No credentials should fail when auth is enabled diff --git a/sdks/ts/test/integration.test.ts b/sdks/ts/test/integration.test.ts index efa47c24..72c1f3b2 100644 --- a/sdks/ts/test/integration.test.ts +++ b/sdks/ts/test/integration.test.ts @@ -29,6 +29,7 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without let destinationAddress: Address; let usdcMint: Address; let koraAddress: Address; + let koraRpcUrl: string; beforeAll(async () => { const testSuite = await setupTestSuite(); @@ -38,11 +39,12 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without destinationAddress = testSuite.destinationAddress; usdcMint = testSuite.usdcMint; koraAddress = testSuite.koraAddress; + koraRpcUrl = testSuite.koraRpcUrl; }, 90000); // allow adequate time for airdrops and token initialization // Run authentication tests only when auth is enabled if (AUTH_ENABLED) { - runAuthenticationTests('http://localhost:8080/'); + runAuthenticationTests(); } describe('Configuration and Setup', () => { diff --git a/sdks/ts/test/setup.ts b/sdks/ts/test/setup.ts index 3fd17175..eb9ea144 100644 --- a/sdks/ts/test/setup.ts +++ b/sdks/ts/test/setup.ts @@ -60,7 +60,7 @@ const DEFAULTS = { // DO NOT USE THESE KEYPAIRS IN PRODUCTION, TESTING KEYPAIRS ONLY KORA_ADDRESS: '7AqpcUvgJ7Kh1VmJZ44rWp2XDow33vswo9VK9VqpPU2d', // Make sure this matches the kora-rpc signer address on launch (root .env) - SENDER_SECRET: '3Tdt5TrRGJYPbTo8zZAscNTvgRGnCLM854tCpxapggUazqdYn6VQRQ9DqNz1UkEfoPCYKj6PwSwCNtckGGvAKugb', + SENDER_SECRET: 'tzgfgSWTE3KUA6qfRoFYLaSfJm59uUeZRDy4ybMrLn1JV2drA1mftiaEcVFvq1Lok6h6EX2C4Y9kSKLvQWyMpS5', // HhA5j2rRiPbMrpF2ZD36r69FyZf3zWmEHRNSZbbNdVjf TEST_USDC_MINT_SECRET: '59kKmXphL5UJANqpFFjtH17emEq3oRNmYsx6a3P3vSGJRmhMgVdzH77bkNEi9bArRViT45e8L2TsuPxKNFoc3Qfg', // Make sure this matches the USDC mint in kora.toml (9BgeTKqmFsPVnfYscfM6NvsgmZxei7XfdciShQ6D3bxJ) DESTINATION_ADDRESS: 'AVmDft8deQEo78bRKcGN5ZMf3hyjeLBK4Rd4xGB46yQM', KORA_SIGNER_TYPE: 'memory', // Default signer type @@ -68,6 +68,7 @@ const DEFAULTS = { interface TestSuite { koraClient: KoraClient; + koraRpcUrl: string; testWallet: KeyPairSigner; usdcMint: Address; destinationAddress: Address; @@ -85,7 +86,7 @@ const createKeyPairSignerFromB58Secret = async (b58Secret: string) => { return await createKeyPairSignerFromBytes(b58SecretEncoded); }; // TODO Add KORA_PRIVATE_KEY_2= support for multi-signer configs -function loadEnvironmentVariables() { +export function loadEnvironmentVariables() { const koraSignerType = process.env.KORA_SIGNER_TYPE || DEFAULTS.KORA_SIGNER_TYPE; let koraAddress = process.env.KORA_ADDRESS; @@ -283,7 +284,8 @@ async function initializeToken({ ), ) : []; - const instructions = [...baseInstructions, ...otherAtaInstructions]; + const alreadyExists = await mintExists(client, mint.address); + let instructions = alreadyExists ? [...otherAtaInstructions] : [...baseInstructions, ...otherAtaInstructions]; await sendAndConfirmInstructions(client, payer, instructions, 'Initialize token and ATAs', 'finalized'); } @@ -346,6 +348,7 @@ async function setupTestSuite(): Promise { return { koraClient: new KoraClient({ rpcUrl: koraRpcUrl, ...authConfig }), + koraRpcUrl, testWallet, usdcMint: usdcMint.address, destinationAddress, @@ -353,4 +356,13 @@ async function setupTestSuite(): Promise { }; } +const mintExists = async (client: Client, mint: Address) => { + try { + const mintAccount = await client.rpc.getAccountInfo(mint).send(); + return mintAccount.value !== null; + } catch (error) { + return false; + } +}; + export default setupTestSuite; diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 6ae195af..675d30d8 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -4,8 +4,8 @@ version = { workspace = true } edition = { workspace = true } [[bin]] -name = "setup_test_env" -path = "src/bin/setup_test_env.rs" +name = "test_runner" +path = "src/bin/test_runner.rs" [[test]] @@ -57,9 +57,10 @@ reqwest = { workspace = true } futures = { workspace = true } anyhow = { workspace = true } once_cell = "1.21.3" -clap = { workspace = true } +clap = { workspace = true, features = ["derive"] } toml = { workspace = true } solana-address-lookup-table-interface = { workspace = true } colored = "3.0" serde = { workspace = true } base64 = { workspace = true } +chrono = { workspace = true } diff --git a/tests/examples/estimateTransactionFee.json b/tests/examples/estimateTransactionFee.json deleted file mode 100644 index 33365ab3..00000000 --- a/tests/examples/estimateTransactionFee.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "jsonrpc": "2.0", - "id": 1, - "method": "estimateTransactionFee", - "params": [ - "XGbYe8A2uT4oUB5s64hDQE8A7mqW3NTex9U5ugCTvGpnoYh8xgdyJStbuhhLYACHbsMyqVXRvQQ8hXyHsH2paP3vGLJp1RnfMnyv4BrFDx2h9hveJgGfi7u5Gy8TE8iD66bjucdxv3BTxmKyJknUq9cZyM4iKYmVMCmTyqi31mJvr1zcYuSouaAmNR5nGSifeV7pkJDuNQrVWv4wTmLkYHinT3Qq1R3RWxv4zVrSojn5eFktuLY8qaNq9bnHa4PrWzB2A9Qf9rKgErrrM3genmcuK9qRY9M99g4kGphADvaTfzmeZDxx8vY8X57TGahMAHrDn1P7yRmHF2mRtWmqAwhjadqhVaSwM5ZzuweR3Tt8guE7mvFyzLmURvpQfNCGTi65YjqEmubPDP9z2Ks4utPUKxcmQVSN4T8Qm9i4cc6qPRiVqDggPKMHX9M4hhg5Mnirxxg5kUAf8r9fjLVNyYei3TijAHmEYu8nFi1m65oHJR8v4HrF2dXJn8mvYRpLLGsGpEec8ZGrWS2RwNVahV8c1XeNAyxYSCXD6uHRLXz4ujjG3AN93wqPRfY9yvW3fV72oprJwbiUjPWvDqb3hXADPPvK5ZbDxnYqysQArwhkBZMzEPAM1SHTCtmbWaZ4b1v3DnvDGcZ6BUqHjR47Bt8GCbpygdcfCBNnThrAGMPHJUHQ15qWNQjXqbSNDbFZAfiRv4rbegotm79eh3mArM3rhQfWFaqee1TBRKTGR3PoPhFMUoyrsA4WYtBnVadtwkhzTZtP9Y9Dw7pCwHohQqMuw5R41FgwKmAcJhfyVVqCNYgFJnudUXKK9T5iepyTCPeaJcWKwZMitauwtYtBv4t8yqvh93py1SN6RWTSmZcJT3DurPmtbyb1Seh6QebZxrc7NAvGhQYeZvqXBN2yHTCdRDHehfp8mDmK3e43h3VxB9DXw58GGYw9X8LhzhMJoHDVtiNRJRGyyF7tc8CbrBtsFEGRmWw1nFDn1wQsSkY833fcoBj2CJA9LexDBuKwpi2pSrMnEEiUDmjsuGU3yDW8Bm4pufeyHc4crhM4HHFYG1u8XghGH7zf9gLkkXHcVnqq1KWs4DtbzEuKY1Mr3vFJhYXY6v8Lu8gdiB9fb3aKoxC1yJrbP8nfDtgHNGkSRQJsszTu4hGm9uX2sK2MKMRS51NjtN1hEG5K5yDBVLjrqHUKBZRyEKAjGMfFRzJPCUR5RDgsimyJUqB3gra3xejF5Ny3csaoh9y8G2JdA4CkrZPBvpDFQqyBMUG5AUH7KkF5syiVTJ9nqkLsMa3xdmw4Ph", - "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" - ] -} \ No newline at end of file diff --git a/tests/examples/getBlockhash.json b/tests/examples/getBlockhash.json deleted file mode 100644 index 8b2f0a75..00000000 --- a/tests/examples/getBlockhash.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "jsonrpc": "2.0", - "id": 1, - "method": "getBlockhash", - "params": [] -} \ No newline at end of file diff --git a/tests/examples/getConfig.json b/tests/examples/getConfig.json deleted file mode 100644 index 997b7545..00000000 --- a/tests/examples/getConfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "jsonrpc": "2.0", - "id": 1, - "method": "getConfig", - "params": [] -} \ No newline at end of file diff --git a/tests/examples/getEnabledFeatures.json b/tests/examples/getEnabledFeatures.json deleted file mode 100644 index a78d40bd..00000000 --- a/tests/examples/getEnabledFeatures.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "jsonrpc": "2.0", - "id": 1, - "method": "getEnabledFeatures", - "params": [] -} diff --git a/tests/examples/getSupportedTokens.json b/tests/examples/getSupportedTokens.json deleted file mode 100644 index c50023c1..00000000 --- a/tests/examples/getSupportedTokens.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "jsonrpc": "2.0", - "id": 1, - "method": "getSupportedTokens", - "params": [] -} diff --git a/tests/examples/signAndSendTransaction.json b/tests/examples/signAndSendTransaction.json deleted file mode 100644 index fc99d196..00000000 --- a/tests/examples/signAndSendTransaction.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "jsonrpc": "2.0", - "id": 1, - "method": "signAndSendTransaction", - "params": [ - "3md7BBV9wFjYGnMWcMNyAZcjca2HGfXWZkrU8vvho66z2sJMZFcx6HZdBiAddjo2kzgBv3uZoac3domBRjJJSXkbBvokxSKZJ1JkQwVSYyKhcnWsgMzvEfJBaSjk7jdz8updcXb8j1Ejz1yWaZgeuV6wy4pncAFHDybqUih4TmRgcbARs5t3ZXRNTDqedyMiJUg5Jz1Apt2ohLsffqjnAhrbC41xrphouRDPx356JNz5MjKi4iLkBnRHfUMJiDxtbXCYNTe257PoASUU2v12WDYFheGKzHP8ctmN7" - ] -} \ No newline at end of file diff --git a/tests/examples/signTransaction.json b/tests/examples/signTransaction.json deleted file mode 100644 index 7c75bb55..00000000 --- a/tests/examples/signTransaction.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "jsonrpc": "2.0", - "id": 1, - "method": "signTransaction", - "params": [ - "3md7BBV9wFjYGnMWcMNyAZcjca2HGfXWZkrU8vvho66z2sJMZFcx6HZdBiAddjo2kzgBv3uZoac3domBRjJJSXkbBvokxSKZJ1JkQwVSYyKhcnWsgMzvEfJBaSjk7jdz8updcXb8j1Ejz1yWaZgeuV6wy4pncAFHDybqUih4TmRgcbARs5t3ZXRNTDqedyMiJUg5Jz1Apt2ohLsffqjnAhrbC41xrphouRDPx356JNz5MjKi4iLkBnRHfUMJiDxtbXCYNTe257PoASUU2v12WDYFheGKzHP8ctmN7" - ] -} \ No newline at end of file diff --git a/tests/examples/transferTransaction.json b/tests/examples/transferTransaction.json deleted file mode 100644 index 9ad4e535..00000000 --- a/tests/examples/transferTransaction.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "jsonrpc": "2.0", - "id": 1, - "method": "transferTransaction", - "params": [ - 1000000, - "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", - "5KKsLVU6TcbVDK4BS6K1DGDxnh4Q9xjYJ8XaDCG5t8ht", - "AVmDft8deQEo78bRKcGN5ZMf3hyjeLBK4Rd4xGB46yQM" - ] -} \ No newline at end of file diff --git a/tests/payment_address/payment_address_v0_tests.rs b/tests/payment_address/payment_address_v0_tests.rs index d60fe765..bd8c1c40 100644 --- a/tests/payment_address/payment_address_v0_tests.rs +++ b/tests/payment_address/payment_address_v0_tests.rs @@ -1,9 +1,6 @@ use crate::common::*; use jsonrpsee::rpc_params; -use kora_lib::{ - token::{TokenInterface, TokenProgram}, - transaction::{TransactionUtil, VersionedTransactionOps}, -}; +use kora_lib::token::{TokenInterface, TokenProgram}; use solana_sdk::{ pubkey::Pubkey, signature::{Keypair, Signer}, @@ -12,6 +9,7 @@ use spl_associated_token_account::{ get_associated_token_address, instruction::create_associated_token_account_idempotent, }; use std::str::FromStr; +use tests::common::helpers::get_fee_for_default_transaction_in_usdc; #[tokio::test] async fn test_sign_transaction_if_paid_with_payment_address_v0() { diff --git a/tests/rpc/transaction_signing.rs b/tests/rpc/transaction_signing.rs index 31baf657..bc5af31e 100644 --- a/tests/rpc/transaction_signing.rs +++ b/tests/rpc/transaction_signing.rs @@ -4,7 +4,7 @@ use crate::common::*; use jsonrpsee::rpc_params; use kora_lib::transaction::TransactionUtil; use solana_sdk::{pubkey::Pubkey, signature::Signer}; -use spl_associated_token_account::get_associated_token_address; +use tests::common::helpers::get_fee_for_default_transaction_in_usdc; // ************************************************************************************** // Sign transaction tests @@ -342,7 +342,8 @@ async fn test_sign_transaction_v0_with_invalid_lookup_table() { .with_fee_payer(FeePayerTestHelper::get_fee_payer_pubkey()) .with_transfer( &SenderTestHelper::get_test_sender_keypair().pubkey(), - &LookupTableHelper::get_test_disallowed_address(), + &LookupTableHelper::get_test_disallowed_address() + .expect("Failed to get test disallowed address"), 10, ) .build() diff --git a/tests/src/bin/setup_test_env.rs b/tests/src/bin/setup_test_env.rs deleted file mode 100644 index 1820f77f..00000000 --- a/tests/src/bin/setup_test_env.rs +++ /dev/null @@ -1,6 +0,0 @@ -use tests::setup_test_env; - -#[tokio::main] -async fn main() -> Result<(), Box> { - setup_test_env::run().await -} diff --git a/tests/src/bin/test_runner.rs b/tests/src/bin/test_runner.rs new file mode 100644 index 00000000..1786e0b9 --- /dev/null +++ b/tests/src/bin/test_runner.rs @@ -0,0 +1,507 @@ +use clap::Parser; +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_commitment_config::CommitmentConfig; +use std::{collections::HashMap, sync::Arc, time::Instant}; +use tests::{ + common::{constants::DEFAULT_RPC_URL, setup::TestAccountSetup, TestAccountInfo}, + test_runner::{ + accounts::{ + download_accounts, set_environment_variables, set_lookup_table_environment_variables, + AccountFile, + }, + commands::{TestCommandHelper, TestLanguage}, + config::{TestPhaseConfig, TestRunnerConfig}, + kora::{ + get_kora_binary_path, is_kora_running_with_client, release_port, start_kora_rpc_server, + }, + output::{ + filter_command_output, limit_output_size, OutputFilter, PhaseOutput, TestPhaseColor, + }, + validator::start_test_validator, + }, +}; +use tokio::{process::Child, task::JoinSet}; + +pub struct TestRunner { + pub rpc_client: Arc, + pub reqwest_client: reqwest::Client, + pub solana_test_validator_pid: Option, + pub test_accounts: TestAccountInfo, + pub kora_pids: Vec, + pub cached_keys: Arc>, +} + +impl TestRunner { + pub async fn new(rpc_url: String) -> Result> { + let mut cached_keys = HashMap::new(); + + // Cache all required keys + for &account_file in AccountFile::required_for_kora() { + let key = tokio::fs::read_to_string(account_file.local_key_path()).await?; + cached_keys.insert(account_file, key); + } + + Ok(Self { + rpc_client: Arc::new(RpcClient::new_with_commitment( + rpc_url, + CommitmentConfig::confirmed(), + )), + reqwest_client: reqwest::Client::new(), + solana_test_validator_pid: None, + test_accounts: TestAccountInfo::default(), + kora_pids: Vec::new(), + cached_keys: Arc::new(cached_keys), + }) + } + + pub fn get_cached_key( + &self, + account_file: AccountFile, + ) -> Result<&str, Box> { + self.cached_keys + .get(&account_file) + .map(|s| s.as_str()) + .ok_or_else(|| format!("Key not found in cache: {account_file:?}").into()) + } +} + +/* +CLI +*/ +#[derive(Parser, Debug)] +#[command(name = "test_runner")] +#[command(about = "Kora integration test runner with configurable options")] +pub struct Args { + /// Enable verbose output showing detailed test information + #[arg(long, help = "Enable verbose output")] + pub verbose: bool, + + /// RPC URL to use for Solana connection + #[arg( + long, + default_value = DEFAULT_RPC_URL, + help = "Solana RPC URL to connect to" + )] + pub rpc_url: String, + + /// Force refresh of test accounts, ignoring cached versions + #[arg(long, help = "Skip loading cached accounts and setup test environment from scratch")] + pub force_refresh: bool, + + /// Test configuration file + #[arg( + long, + default_value = "tests/src/test_runner/test_cases.toml", + help = "Path to test configuration file" + )] + pub config: String, + + /// Run only specific test phases (can be used multiple times) + #[arg( + long = "filter", + help = "Run only specific test phases (e.g., --filter regular --filter auth)" + )] + pub filters: Vec, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let args = Args::parse(); + let start_time = Instant::now(); + + println!("๐Ÿš€ Starting test runner at {}", chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC")); + + let mut test_runner = TestRunner::new(args.rpc_url.clone()).await?; + let custom_rpc_url = args.rpc_url != DEFAULT_RPC_URL; + + let (result, completed_phases) = async { + setup_test_env(&mut test_runner, args.force_refresh, custom_rpc_url).await?; + let phases = + run_all_test_phases(&test_runner, args.verbose, &args.config, &args.filters).await?; + Ok::>(phases) + } + .await + .map_or_else(|e| (Err(e), 0), |phases| (Ok(()), phases)); + + clean_up(&mut test_runner).await?; + + let total_duration = start_time.elapsed(); + println!( + "โœ… Test runner completed at {} ({} phases, Total time: {:.2}s)", + chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC"), + completed_phases, + total_duration.as_secs_f64() + ); + + result +} + +/* +Setting up test environment +*/ + +pub async fn setup_test_env_from_scratch( +) -> Result> { + let mut setup = TestAccountSetup::new().await; + let test_accounts = setup.setup_all_accounts().await?; + + Ok(test_accounts) +} + +async fn setup_test_env( + test_runner: &mut TestRunner, + force_refresh: bool, + custom_rpc_url: bool, +) -> Result<(), Box> { + let mut found_all_accounts = !force_refresh; + + if !force_refresh { + for account_file in AccountFile::required_test_accounts() { + if !account_file.test_account_path().exists() { + found_all_accounts = false; + break; + } + } + } + + // Only start local validator if using default RPC URL + if !custom_rpc_url { + test_runner.solana_test_validator_pid = + Some(start_test_validator(found_all_accounts).await?); + } else { + println!("Using external RPC, skipping local validator startup"); + } + + set_environment_variables(&test_runner.cached_keys)?; + + test_runner.test_accounts = setup_test_env_from_scratch().await?; + + if !found_all_accounts { + download_accounts(&test_runner.rpc_client.clone(), &test_runner.test_accounts).await?; + } + set_lookup_table_environment_variables(&test_runner.test_accounts).await?; + + Ok(()) +} + +/* +Running Tests +*/ + +pub async fn run_all_test_phases( + test_runner: &TestRunner, + verbose: bool, + config_path: &str, + filters: &[String], +) -> Result> { + let rpc_url = test_runner.rpc_client.url(); + + // Load test configuration + let config = if std::path::Path::new(config_path).exists() { + println!("Loading test configuration from: {config_path}"); + TestRunnerConfig::load_from_file(config_path).await? + } else { + panic!("Test configuration file not found: {config_path}"); + }; + + let mut join_set = JoinSet::new(); + + // Spawn test phases from config (filtered if specified) + for (phase_name, phase_config) in config.get_all_phases() { + // Apply filter if specified + if !filters.is_empty() && !filters.contains(&phase_name) { + continue; + } + + join_set.spawn({ + let rpc_url = rpc_url.clone(); + let phase_config = phase_config.clone(); + let cached_keys = test_runner.cached_keys.clone(); + let http_client = test_runner.reqwest_client.clone(); + async move { + run_test_phase_from_config( + rpc_url, + &phase_config, + verbose, + cached_keys, + http_client, + ) + .await + } + }); + } + + // Stream output as each test completes instead of waiting for all + let mut all_success = true; + let mut errors = Vec::new(); + let mut completed_phases = 0; + + while let Some(result) = join_set.join_next().await { + match result { + Ok(phase_output) => { + completed_phases += 1; + print!("{}", phase_output.output); + + if phase_output.truncated { + println!("โš ๏ธ Output truncated for phase '{}'", phase_output.phase_name); + } + + if !phase_output.success { + all_success = false; + } + } + Err(e) => { + println!("โŒ Task failed: {e}"); + errors.push(e); + all_success = false; + } + } + } + + if !errors.is_empty() { + return Err(format!("Multiple test phases failed: {errors:?}").into()); + } + + if !all_success { + return Err("One or more test phases failed".into()); + } + + Ok(completed_phases) +} + +async fn run_test_phase_from_config( + rpc_url: String, + config: &TestPhaseConfig, + verbose: bool, + cached_keys: Arc>, + http_client: reqwest::Client, +) -> PhaseOutput { + let test_names: Vec<&str> = config.tests.iter().map(|s| s.as_str()).collect(); + let preferred_port: u16 = config.port.parse().unwrap_or(8080); + + run_test_phase( + &config.name, + rpc_url, + &config.config, + &config.signers, + test_names, + config.initialize_payments_atas, + verbose, + cached_keys, + http_client, + preferred_port, + ) + .await +} + +#[allow(clippy::too_many_arguments)] +pub async fn run_test_phase( + phase_name: &str, + rpc_url: String, + config_file: &str, + signers_config: &str, + test_names: Vec<&str>, + initialize_payment_atas: bool, + verbose: bool, + cached_keys: Arc>, + http_client: reqwest::Client, + preferred_port: u16, +) -> PhaseOutput { + let color = TestPhaseColor::from_phase_name(phase_name); + let mut output = String::new(); + + output + .push_str(&color.colorize_with_controlled_flow(&format!("=== Starting {phase_name} ==="))); + + let (mut kora_pid, actual_port) = match start_kora_rpc_server( + rpc_url.clone(), + config_file, + signers_config, + &cached_keys, + preferred_port, + ) + .await + { + Ok((pid, port)) => (pid, port), + Err(e) => { + output.push_str( + &color.colorize_with_controlled_flow(&format!("Failed to start Kora server: {e}")), + ); + let (limited_output, truncated) = limit_output_size(output); + return PhaseOutput { + phase_name: phase_name.to_string(), + output: limited_output, + success: false, + truncated, + }; + } + }; + + let mut attempts = 0; + let mut delay = std::time::Duration::from_millis(50); + let max_delay = std::time::Duration::from_secs(1); + let max_attempts = 10; + let port_str = actual_port.to_string(); + + while !is_kora_running_with_client(&http_client, &port_str).await { + attempts += 1; + if attempts > max_attempts { + output.push_str(&color.colorize_with_controlled_flow(&format!( + "Kora server failed to start on port {actual_port} within {max_attempts} attempts" + ))); + kora_pid.kill().await.ok(); + release_port(actual_port); + let (limited_output, truncated) = limit_output_size(output); + return PhaseOutput { + phase_name: phase_name.to_string(), + output: limited_output, + success: false, + truncated, + }; + } + + tokio::time::sleep(delay).await; + delay = std::cmp::min(delay * 2, max_delay); + } + output.push_str( + &color.colorize_with_controlled_flow(&format!("Kora server started on port {actual_port}")), + ); + + let result = async { + if initialize_payment_atas { + run_initialize_atas_for_kora_cli_tests_buffered( + config_file, + &rpc_url, + signers_config, + color, + &mut output, + &cached_keys, + ) + .await? + } + + for test_name in test_names { + output.push_str(&color.colorize_with_controlled_flow(&format!( + "Running {test_name} tests on port {actual_port}" + ))); + if test_name.starts_with("typescript_") { + TestCommandHelper::run_test( + TestLanguage::TypeScript, + test_name, + &port_str, + color, + verbose, + &mut output, + ) + .await?; + } else { + TestCommandHelper::run_test( + TestLanguage::Rust, + test_name, + &port_str, + color, + verbose, + &mut output, + ) + .await? + } + } + + Ok::<(), Box>(()) + } + .await; + + kora_pid.kill().await.ok(); + release_port(actual_port); + + let success = result.is_ok(); + match &result { + Ok(_) => output.push_str( + &color.colorize_with_controlled_flow(&format!("\n\n=== Completed {phase_name} ===")), + ), + Err(e) => output.push_str(&color.colorize_with_controlled_flow(&format!( + "\n\n=== Failed {phase_name} - Error: {e} ===" + ))), + } + + let (limited_output, truncated) = limit_output_size(output); + PhaseOutput { phase_name: phase_name.to_string(), output: limited_output, success, truncated } +} + +pub async fn run_initialize_atas_for_kora_cli_tests_buffered( + config_file: &str, + rpc_url: &str, + signers_config: &str, + color: TestPhaseColor, + output: &mut String, + cached_keys: &Arc>, +) -> Result<(), Box> { + output.push_str(&color.colorize_with_controlled_flow("โ€ข Initializing payment ATAs...")); + + let fee_payer_key = + cached_keys.get(&AccountFile::FeePayer).ok_or("FeePayer key not found in cache")?; + + let kora_binary_path = get_kora_binary_path().await?; + + let cmd_output = tokio::process::Command::new(kora_binary_path) + .args([ + "--config", + config_file, + "--rpc-url", + rpc_url, + "rpc", + "initialize-atas", + "--signers-config", + signers_config, + ]) + .env("KORA_PRIVATE_KEY", fee_payer_key.trim()) + .output() + .await?; + + if !cmd_output.status.success() { + let stderr = String::from_utf8_lossy(&cmd_output.stderr); + let filtered_stderr = filter_command_output(&stderr, OutputFilter::CliCommand, false); + if !filtered_stderr.is_empty() { + output.push_str(&filtered_stderr); + } + return Err("Failed to initialize payment ATAs".into()); + } + + let stdout = String::from_utf8_lossy(&cmd_output.stdout); + let filtered_stdout = filter_command_output(&stdout, OutputFilter::CliCommand, false); + if !filtered_stdout.is_empty() { + output.push_str(&filtered_stdout); + } + output.push_str(&color.colorize_with_controlled_flow("โ€ข Payment ATAs ready")); + + Ok(()) +} + +/* +Clean up +*/ +pub async fn clean_up( + test_runner: &mut TestRunner, +) -> Result<(), Box> { + println!("=== Cleaning up processes ==="); + + if let Some(solana_test_validator_pid) = &mut test_runner.solana_test_validator_pid { + if let Err(e) = solana_test_validator_pid.kill().await { + println!("Failed to stop Solana test validator: {e}"); + } else { + println!("Stopped Solana test validator"); + } + } + + // Kill tracked Kora processes (though they're managed locally in each test phase) + for kora_pid in &mut test_runner.kora_pids { + if let Err(e) = kora_pid.kill().await { + println!("Failed to stop Kora process: {e}"); + } else { + println!("Stopped Kora process"); + } + } + + println!("=== Cleanup complete ==="); + Ok(()) +} diff --git a/tests/src/common/auth_helpers.rs b/tests/src/common/auth_helpers.rs index 18cec01b..fe2a201b 100644 --- a/tests/src/common/auth_helpers.rs +++ b/tests/src/common/auth_helpers.rs @@ -1,13 +1,15 @@ +#![cfg(test)] + use hmac::{Hmac, Mac}; use kora_lib::constant::{X_HMAC_SIGNATURE, X_TIMESTAMP}; use once_cell::sync::Lazy; use serde_json::{json, Value}; use sha2::Sha256; -use crate::common::client::TestClient; - -pub const TEST_API_KEY: &str = "test-api-key-123"; -pub const TEST_HMAC_SECRET: &str = "test-hmac-secret-456"; +use crate::common::{ + client::TestClient, + constants::{TEST_API_KEY, TEST_HMAC_SECRET}, +}; pub static JSON_TEST_BODY: Lazy = Lazy::new(|| { json!({ diff --git a/tests/src/common/client.rs b/tests/src/common/client.rs index 3c281157..6e7c6aaf 100644 --- a/tests/src/common/client.rs +++ b/tests/src/common/client.rs @@ -1,12 +1,17 @@ +#![cfg(test)] + use anyhow::Result; use jsonrpsee::{ core::{client::ClientT, traits::ToRpcParams}, http_client::{HttpClient, HttpClientBuilder}, }; use solana_client::nonblocking::rpc_client::RpcClient; +use solana_commitment_config::CommitmentConfig; use std::sync::Arc; -use crate::common::{TransactionBuilder, DEFAULT_RPC_URL, TEST_SERVER_URL}; +use crate::common::{ + TransactionBuilder, DEFAULT_RPC_URL, RPC_URL_ENV, TEST_SERVER_URL, TEST_SERVER_URL_ENV, +}; /// Unified test client that manages both HTTP and RPC clients #[derive(Clone)] @@ -29,7 +34,10 @@ impl TestClient { .build(&server_url) .map_err(|e| anyhow::anyhow!("Failed to create HTTP client: {}", e))?; - let rpc_client = Arc::new(RpcClient::new(rpc_url.clone())); + let rpc_client = Arc::new(RpcClient::new_with_commitment( + rpc_url.clone(), + CommitmentConfig::confirmed(), + )); Ok(Self { http_client, rpc_client, server_url, rpc_url }) } @@ -49,13 +57,13 @@ impl TestClient { /// Get the default test server URL (Kora RPC server) pub fn get_default_server_url() -> String { dotenv::dotenv().ok(); - std::env::var("TEST_SERVER_URL").unwrap_or_else(|_| TEST_SERVER_URL.to_string()) + std::env::var(TEST_SERVER_URL_ENV).unwrap_or_else(|_| TEST_SERVER_URL.to_string()) } /// Get the default RPC URL (Solana RPC) pub fn get_default_rpc_url() -> String { dotenv::dotenv().ok(); - std::env::var("RPC_URL").unwrap_or_else(|_| DEFAULT_RPC_URL.to_string()) + std::env::var(RPC_URL_ENV).unwrap_or_else(|_| DEFAULT_RPC_URL.to_string()) } } diff --git a/tests/src/common/constants.rs b/tests/src/common/constants.rs index c003cda9..7fdf9ddd 100644 --- a/tests/src/common/constants.rs +++ b/tests/src/common/constants.rs @@ -1,9 +1,3 @@ -use solana_sdk::pubkey::Pubkey; -use std::str::FromStr; - -pub const LOOKUP_TABLES_FILE_PATH: &str = - concat!(env!("CARGO_MANIFEST_DIR"), "/src/common/fixtures/lookup_tables.json"); - // ============================================================================ // Network URLs // ============================================================================ @@ -14,42 +8,6 @@ pub const DEFAULT_RPC_URL: &str = "http://127.0.0.1:8899"; /// Default Kora test server URL pub const TEST_SERVER_URL: &str = "http://127.0.0.1:8080"; -// ============================================================================ -// Test Keypair Paths -// ============================================================================ - -/// Fee payer keypair path (local testing only) -pub const FEE_PAYER_KEYPAIR_PATH: &str = - concat!(env!("CARGO_MANIFEST_DIR"), "/src/common/local-keys/fee-payer-local.json"); - -/// Sender keypair path (local testing only) -pub const SENDER_KEYPAIR_PATH: &str = - concat!(env!("CARGO_MANIFEST_DIR"), "/src/common/local-keys/sender-local.json"); - -/// USDC mint keypair path (local testing only) -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"); - -/// 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"); - -/// Payment address keypair path (for payment address tests) -pub const PAYMENT_KEYPAIR_PATH: &str = - concat!(env!("CARGO_MANIFEST_DIR"), "/src/common/local-keys/payment-local.json"); - // ============================================================================ // Test Public Keys // ============================================================================ @@ -87,36 +45,50 @@ pub const TEST_API_KEY: &str = "test-api-key-123"; pub const TEST_HMAC_SECRET: &str = "test-hmac-secret-456"; // ============================================================================ -// Helper Functions +// Test Environment Variables // ============================================================================ -/// Get recipient pubkey as Pubkey type -pub fn get_recipient_pubkey() -> Pubkey { - Pubkey::from_str(RECIPIENT_PUBKEY).expect("Invalid recipient pubkey") -} - -/// Get test disallowed address as Pubkey type -pub fn get_test_disallowed_pubkey() -> Pubkey { - Pubkey::from_str(TEST_DISALLOWED_ADDRESS).expect("Invalid disallowed address") -} - -/// Get test payment address as Pubkey type -pub fn get_test_payment_pubkey() -> Pubkey { - Pubkey::from_str(TEST_PAYMENT_ADDRESS).expect("Invalid payment address") -} - -/// Get PYUSD mint as Pubkey type -pub fn get_pyusd_mint_pubkey() -> Pubkey { - Pubkey::from_str(PYUSD_MINT).expect("Invalid PYUSD mint") -} - -/// Default fee for a transaction with 2 signers (5000 lamports each) -/// This is used for a lot of tests that only has sender and fee payer as signers -pub fn get_fee_for_default_transaction_in_usdc() -> u64 { - // 10 000 USDC priced at default 0.001 SOL / USDC (Mock pricing) (6 decimals), so 0.01 USDC - // 10 000 lamports required (2 x 5000 for signatures) (9 decimals), so 0.00001 SOL - // - // Required SOL amount is 0.01 (usdc amount) * 0.001 (usdc price) = 0.00001 SOL - // Required lamports is 0.00001 SOL * 10^9 (lamports per SOL) = 10 000 lamports - 10_000 -} +/// Test server URL environment variable +pub const TEST_SERVER_URL_ENV: &str = "TEST_SERVER_URL"; + +/// RPC URL environment variable +pub const RPC_URL_ENV: &str = "RPC_URL"; + +/// KORA private key environment variable +pub const KORA_PRIVATE_KEY_ENV: &str = "KORA_PRIVATE_KEY"; + +/// Signer 2 private key environment variable +pub const SIGNER_2_KEYPAIR_ENV: &str = "SIGNER_2_KEYPAIR"; + +/// Test sender private key environment variable +pub const TEST_SENDER_KEYPAIR_ENV: &str = "TEST_SENDER_KEYPAIR"; + +/// Test recipient public key environment variable +pub const TEST_RECIPIENT_PUBKEY_ENV: &str = "TEST_RECIPIENT_PUBKEY"; + +/// Test USDC mint private key environment variable +pub const TEST_USDC_MINT_KEYPAIR_ENV: &str = "TEST_USDC_MINT_KEYPAIR"; + +/// Test USDC mint decimals environment variable +pub const TEST_USDC_MINT_DECIMALS_ENV: &str = "TEST_USDC_MINT_DECIMALS"; + +/// Test USDC mint 2022 private key environment variable +pub const TEST_USDC_MINT_2022_KEYPAIR_ENV: &str = "TEST_USDC_MINT_2022_KEYPAIR"; + +/// Test interest bearing mint private key environment variable +pub const TEST_INTEREST_BEARING_MINT_KEYPAIR_ENV: &str = "TEST_INTEREST_BEARING_MINT_KEYPAIR"; + +/// Test transfer hook mint private key environment variable +pub const TEST_TRANSFER_HOOK_MINT_KEYPAIR_ENV: &str = "TEST_TRANSFER_HOOK_MINT_KEYPAIR"; + +/// Payment address keypair environment variable +pub const PAYMENT_ADDRESS_KEYPAIR_ENV: &str = "PAYMENT_ADDRESS_KEYPAIR"; + +/// Test allowed lookup table address environment variable +pub const TEST_ALLOWED_LOOKUP_TABLE_ADDRESS_ENV: &str = "TEST_ALLOWED_LOOKUP_TABLE_ADDRESS"; + +/// Test disallowed lookup table address environment variable +pub const TEST_DISALLOWED_LOOKUP_TABLE_ADDRESS_ENV: &str = "TEST_DISALLOWED_LOOKUP_TABLE_ADDRESS"; + +/// Test transaction lookup table address environment variable +pub const TEST_TRANSACTION_LOOKUP_TABLE_ADDRESS_ENV: &str = "TEST_TRANSACTION_LOOKUP_TABLE_ADDRESS"; diff --git a/tests/src/common/helpers.rs b/tests/src/common/helpers.rs index 63e239e5..6874217e 100644 --- a/tests/src/common/helpers.rs +++ b/tests/src/common/helpers.rs @@ -8,21 +8,15 @@ use std::str::FromStr; use crate::common::constants::*; -/// Test account information for outputting to the user -#[derive(Debug)] -pub struct TestAccountInfo { - pub fee_payer_pubkey: Pubkey, - pub sender_pubkey: Pubkey, - pub recipient_pubkey: Pubkey, - pub usdc_mint_pubkey: Pubkey, - 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, +/// Default fee for a transaction with 2 signers (5000 lamports each) +/// This is used for a lot of tests that only has sender and fee payer as signers +pub fn get_fee_for_default_transaction_in_usdc() -> u64 { + // 10 000 USDC priced at default 0.001 SOL / USDC (Mock pricing) (6 decimals), so 0.01 USDC + // 10 000 lamports required (2 x 5000 for signatures) (9 decimals), so 0.00001 SOL + // + // Required SOL amount is 0.01 (usdc amount) * 0.001 (usdc price) = 0.00001 SOL + // Required lamports is 0.00001 SOL * 10^9 (lamports per SOL) = 10 000 lamports + 10_000 } /// Helper function to parse a private key string in multiple formats. @@ -35,17 +29,29 @@ pub struct FeePayerTestHelper; impl FeePayerTestHelper { pub fn get_fee_payer_keypair() -> Keypair { dotenv::dotenv().ok(); - let private_key = match std::env::var("KORA_PRIVATE_KEY") { - Ok(key) => key, - Err(_) => std::fs::read_to_string(FEE_PAYER_KEYPAIR_PATH) - .expect("Failed to read fee payer private key file"), - }; - parse_private_key_string(&private_key).expect("Failed to parse fee payer private key") + parse_private_key_string( + &std::env::var(KORA_PRIVATE_KEY_ENV) + .expect("KORA_PRIVATE_KEY environment variable is not set"), + ) + .expect("Failed to parse fee payer private key") } pub fn get_fee_payer_pubkey() -> Pubkey { Self::get_fee_payer_keypair().pubkey() } + + pub fn get_signer_2_keypair() -> Keypair { + dotenv::dotenv().ok(); + parse_private_key_string( + &std::env::var(SIGNER_2_KEYPAIR_ENV) + .expect("SIGNER_2_KEYPAIR environment variable is not set"), + ) + .expect("Failed to parse signer 2 private key") + } + + pub fn get_signer_2_pubkey() -> Pubkey { + Self::get_signer_2_keypair().pubkey() + } } pub struct SenderTestHelper; @@ -53,12 +59,11 @@ pub struct SenderTestHelper; impl SenderTestHelper { pub fn get_test_sender_keypair() -> Keypair { dotenv::dotenv().ok(); - let private_key = match std::env::var("TEST_SENDER_KEYPAIR") { - Ok(key) => key, - Err(_) => std::fs::read_to_string(SENDER_KEYPAIR_PATH) - .expect("Failed to read sender private key file"), - }; - parse_private_key_string(&private_key).expect("Failed to parse test sender private key") + parse_private_key_string( + &std::env::var(TEST_SENDER_KEYPAIR_ENV) + .expect("TEST_SENDER_KEYPAIR environment variable is not set"), + ) + .expect("Failed to parse test sender private key") } } @@ -67,8 +72,8 @@ pub struct RecipientTestHelper; impl RecipientTestHelper { pub fn get_recipient_pubkey() -> Pubkey { dotenv::dotenv().ok(); - let recipient_str = - std::env::var("TEST_RECIPIENT_PUBKEY").unwrap_or_else(|_| RECIPIENT_PUBKEY.to_string()); + let recipient_str = std::env::var(TEST_RECIPIENT_PUBKEY_ENV) + .unwrap_or_else(|_| RECIPIENT_PUBKEY.to_string()); Pubkey::from_str(&recipient_str).expect("Invalid recipient pubkey") } } @@ -78,12 +83,11 @@ pub struct USDCMintTestHelper; impl USDCMintTestHelper { pub fn get_test_usdc_mint_keypair() -> Keypair { dotenv::dotenv().ok(); - let mint_keypair = match std::env::var("TEST_USDC_MINT_KEYPAIR") { - Ok(key) => key, - Err(_) => std::fs::read_to_string(USDC_MINT_KEYPAIR_PATH) - .expect("Failed to read USDC mint private key file"), - }; - parse_private_key_string(&mint_keypair).expect("Failed to parse test USDC mint private key") + parse_private_key_string( + &std::env::var(TEST_USDC_MINT_KEYPAIR_ENV) + .expect("TEST_USDC_MINT_KEYPAIR environment variable is not set"), + ) + .expect("Failed to parse test USDC mint private key") } pub fn get_test_usdc_mint_pubkey() -> Pubkey { @@ -92,7 +96,7 @@ impl USDCMintTestHelper { pub fn get_test_usdc_mint_decimals() -> u8 { dotenv::dotenv().ok(); - std::env::var("TEST_USDC_MINT_DECIMALS") + std::env::var(TEST_USDC_MINT_DECIMALS_ENV) .ok() .and_then(|s| s.parse().ok()) .unwrap_or(TEST_USDC_MINT_DECIMALS) @@ -104,13 +108,12 @@ 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") + + parse_private_key_string( + &std::env::var(TEST_USDC_MINT_2022_KEYPAIR_ENV) + .expect("TEST_USDC_MINT_2022_KEYPAIR environment variable is not set"), + ) + .expect("Failed to parse test USDC mint 2022 private key") } pub fn get_test_usdc_mint_2022_pubkey() -> Pubkey { @@ -119,13 +122,12 @@ impl USDCMint2022TestHelper { 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") + + parse_private_key_string( + &std::env::var(TEST_INTEREST_BEARING_MINT_KEYPAIR_ENV) + .expect("TEST_INTEREST_BEARING_MINT_KEYPAIR environment variable is not set"), + ) + .expect("Failed to parse test interest bearing mint private key") } pub fn get_test_interest_bearing_mint_pubkey() -> Pubkey { @@ -134,16 +136,44 @@ impl USDCMint2022TestHelper { 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") + + parse_private_key_string( + &std::env::var(TEST_TRANSFER_HOOK_MINT_KEYPAIR_ENV) + .expect("TEST_TRANSFER_HOOK_MINT_KEYPAIR environment variable is not set"), + ) + .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() } } + +pub struct PaymentAddressTestHelper; + +impl PaymentAddressTestHelper { + pub fn get_payment_address_keypair() -> Keypair { + dotenv::dotenv().ok(); + parse_private_key_string( + &std::env::var(PAYMENT_ADDRESS_KEYPAIR_ENV) + .expect("PAYMENT_ADDRESS_KEYPAIR environment variable is not set"), + ) + .expect("Failed to parse payment address private key") + } + + pub fn get_payment_address_pubkey() -> Pubkey { + Self::get_payment_address_keypair().pubkey() + } + + pub fn get_payment_test_address_pubkey() -> Pubkey { + Pubkey::from_str(TEST_PAYMENT_ADDRESS).expect("Invalid payment test address") + } +} + +pub struct PYUSDTestHelper; + +impl PYUSDTestHelper { + pub fn get_pyusd_mint_pubkey() -> Pubkey { + Pubkey::from_str(PYUSD_MINT).expect("Invalid PYUSD mint") + } +} diff --git a/tests/src/common/lookup_tables.rs b/tests/src/common/lookup_tables.rs index e75f09a5..b823effb 100644 --- a/tests/src/common/lookup_tables.rs +++ b/tests/src/common/lookup_tables.rs @@ -4,70 +4,22 @@ use solana_address_lookup_table_interface::instruction::{ }; use solana_client::nonblocking::rpc_client::RpcClient; use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer, transaction::Transaction}; -use std::{ - str::FromStr, - sync::{Arc, OnceLock}, -}; +use std::{str::FromStr, sync::Arc}; use crate::common::{constants::*, SenderTestHelper, USDCMintTestHelper}; -// Static cache for lookup table addresses - loaded once, reused everywhere -pub static CACHED_ADDRESSES: OnceLock = OnceLock::new(); - -#[derive(Default, serde::Serialize, serde::Deserialize)] -pub struct LookupTablesAddresses { - pub allowed_lookup_table_address: String, - pub disallowed_lookup_table_address: String, - pub transaction_lookup_table_address: String, -} - /// Comprehensive helper for all lookup table operations in tests pub struct LookupTableHelper; -impl LookupTablesAddresses { - /// Save lookup table addresses to JSON file - pub fn save(&self) -> Result<()> { - let json = serde_json::to_string_pretty(self)?; - std::fs::write(LOOKUP_TABLES_FILE_PATH, json)?; - Ok(()) - } - - /// Load lookup table addresses from JSON file - pub fn load() -> Result { - let json = std::fs::read_to_string(LOOKUP_TABLES_FILE_PATH)?; - let addresses: LookupTablesAddresses = serde_json::from_str(&json)?; - Ok(addresses) - } - - pub fn allowed_pubkey(&self) -> Result { - Pubkey::from_str(&self.allowed_lookup_table_address).map_err(Into::into) - } - - pub fn disallowed_pubkey(&self) -> Result { - Pubkey::from_str(&self.disallowed_lookup_table_address).map_err(Into::into) - } - - pub fn transaction_pubkey(&self) -> Result { - Pubkey::from_str(&self.transaction_lookup_table_address).map_err(Into::into) - } - - /// Create from Pubkeys for saving - pub fn from_pubkeys(allowed: Pubkey, disallowed: Pubkey, transaction: Pubkey) -> Self { - Self { - allowed_lookup_table_address: allowed.to_string(), - disallowed_lookup_table_address: disallowed.to_string(), - transaction_lookup_table_address: transaction.to_string(), - } - } -} - impl LookupTableHelper { // ============================================================================ // Fixtures Management // ============================================================================ /// Create all standard lookup tables and save addresses to fixtures - pub async fn setup_and_save_lookup_tables(rpc_client: Arc) -> Result<()> { + pub async fn setup_and_save_lookup_tables( + rpc_client: Arc, + ) -> Result<(Pubkey, Pubkey, Pubkey)> { let sender = SenderTestHelper::get_test_sender_keypair(); // Create all standard lookup tables @@ -78,50 +30,34 @@ impl LookupTableHelper { let transaction_lookup_table = Self::create_transaction_lookup_table(rpc_client.clone(), &sender).await?; - // Save addresses to JSON file - let addresses = LookupTablesAddresses::from_pubkeys( - allowed_lookup_table, - disallowed_lookup_table, - transaction_lookup_table, - ); - addresses.save()?; - - Ok(()) + Ok((allowed_lookup_table, disallowed_lookup_table, transaction_lookup_table)) } - fn load_lookup_table_addresses() -> Result<&'static LookupTablesAddresses> { - if let Some(cached) = CACHED_ADDRESSES.get() { - return Ok(cached); - } - - let addresses = LookupTablesAddresses::load()?; - - let _ = CACHED_ADDRESSES.set(addresses); - - Ok(CACHED_ADDRESSES.get().unwrap()) + pub fn get_test_disallowed_address() -> Result { + Pubkey::from_str(TEST_DISALLOWED_ADDRESS).map_err(Into::into) } - /// Get allowed lookup table address from fixtures pub fn get_allowed_lookup_table_address() -> Result { - let addresses = Self::load_lookup_table_addresses()?; - addresses.allowed_pubkey() + dotenv::dotenv().ok(); + let allowed_lookup_table_address = std::env::var(TEST_ALLOWED_LOOKUP_TABLE_ADDRESS_ENV) + .expect("TEST_ALLOWED_LOOKUP_TABLE_ADDRESS environment variable is not set"); + Pubkey::from_str(&allowed_lookup_table_address).map_err(Into::into) } - /// Get disallowed lookup table address from fixtures pub fn get_disallowed_lookup_table_address() -> Result { - let addresses = Self::load_lookup_table_addresses()?; - addresses.disallowed_pubkey() + dotenv::dotenv().ok(); + let disallowed_lookup_table_address = + std::env::var(TEST_DISALLOWED_LOOKUP_TABLE_ADDRESS_ENV) + .expect("TEST_DISALLOWED_LOOKUP_TABLE_ADDRESS environment variable is not set"); + Pubkey::from_str(&disallowed_lookup_table_address).map_err(Into::into) } - /// Get transaction lookup table address from fixtures, includes USDC mint and SPL token program pub fn get_transaction_lookup_table_address() -> Result { - let addresses = Self::load_lookup_table_addresses()?; - addresses.transaction_pubkey() - } - - /// Get test disallowed address (for creating custom lookup tables) - pub fn get_test_disallowed_address() -> Pubkey { - get_test_disallowed_pubkey() + dotenv::dotenv().ok(); + let transaction_lookup_table_address = + std::env::var(TEST_TRANSACTION_LOOKUP_TABLE_ADDRESS_ENV) + .expect("TEST_TRANSACTION_LOOKUP_TABLE_ADDRESS environment variable is not set"); + Pubkey::from_str(&transaction_lookup_table_address).map_err(Into::into) } // ============================================================================ @@ -172,8 +108,16 @@ impl LookupTableHelper { rpc_client.send_and_confirm_transaction(&extend_transaction).await?; } - // Wait for the lookup table to be fully initialized - tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; + // Wait for the lookup table to be activated + // Lookup tables need to be activated for at least one slot before they can be used + let creation_slot = rpc_client.get_slot().await?; + let mut current_slot = creation_slot; + + // Wait until we're at least 2 slots past creation to ensure activation + while current_slot <= creation_slot + 1 { + tokio::time::sleep(tokio::time::Duration::from_millis(400)).await; + current_slot = rpc_client.get_slot().await?; + } Ok(lookup_table_key) } @@ -197,7 +141,7 @@ impl LookupTableHelper { rpc_client: Arc, authority: &Keypair, ) -> Result { - let disallowed_address = get_test_disallowed_pubkey(); + let disallowed_address = Self::get_test_disallowed_address()?; let blocked_lookup_table: Pubkey = Self::create_lookup_table(rpc_client, authority, vec![disallowed_address]).await?; diff --git a/tests/src/common/mod.rs b/tests/src/common/mod.rs index d1a0d719..4d482574 100644 --- a/tests/src/common/mod.rs +++ b/tests/src/common/mod.rs @@ -9,23 +9,14 @@ pub mod lookup_tables; pub mod setup; 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) -pub use auth_helpers::{ - make_auth_request, make_auth_request_with_body, JSON_TEST_BODY, JSON_TEST_BODY_WITH_PARAMS, -}; - -// Re-export helpers (excluding constants that are in constants.rs) -pub use helpers::{ - parse_private_key_string, FeePayerTestHelper, RecipientTestHelper, SenderTestHelper, - TestAccountInfo, USDCMint2022TestHelper, USDCMintTestHelper, -}; -pub use lookup_tables::{LookupTableHelper, LookupTablesAddresses}; - -// Re-export all constants from the consolidated constants module +pub use assertions::*; +#[cfg(test)] +pub use auth_helpers::*; +#[cfg(test)] +pub use client::*; pub use constants::*; +pub use extension_helpers::*; +pub use helpers::*; +pub use lookup_tables::*; +pub use setup::*; +pub use transaction::*; diff --git a/tests/src/common/setup.rs b/tests/src/common/setup.rs index c2baf2ae..eacdbe6d 100644 --- a/tests/src/common/setup.rs +++ b/tests/src/common/setup.rs @@ -20,10 +20,32 @@ use spl_token_2022::{ use std::sync::Arc; use crate::common::{ - FeePayerTestHelper, LookupTableHelper, RecipientTestHelper, SenderTestHelper, TestAccountInfo, + FeePayerTestHelper, LookupTableHelper, RecipientTestHelper, SenderTestHelper, USDCMint2022TestHelper, USDCMintTestHelper, DEFAULT_RPC_URL, }; +/// Test account information for outputting to the user +#[derive(Debug, Default, Clone)] +pub struct TestAccountInfo { + pub fee_payer_pubkey: Pubkey, + pub sender_pubkey: Pubkey, + pub recipient_pubkey: Pubkey, + // USDC mint fields + pub usdc_mint_pubkey: Pubkey, + 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, + // Lookup tables + pub allowed_lookup_table: Pubkey, + pub disallowed_lookup_table: Pubkey, + pub transaction_lookup_table: Pubkey, +} + /// Test account setup utilities for local validator pub struct TestAccountSetup { pub rpc_client: Arc, @@ -70,14 +92,39 @@ impl TestAccountSetup { } pub async fn setup_all_accounts(&mut self) -> Result { - self.fund_sol_accounts().await?; + let mut account_infos = TestAccountInfo::default(); - self.create_usdc_mint().await?; - self.create_usdc_mint_2022().await?; + let (sender_pubkey, recipient_pubkey, fee_payer_pubkey) = self.fund_sol_accounts().await?; + account_infos.sender_pubkey = sender_pubkey; + account_infos.recipient_pubkey = recipient_pubkey; + account_infos.fee_payer_pubkey = fee_payer_pubkey; - self.create_lookup_tables().await?; + let usdc_mint_pubkey = self.create_usdc_mint().await?; + account_infos.usdc_mint_pubkey = usdc_mint_pubkey; - let account_info = self.setup_token_accounts().await?; + let usdc_mint_2022_pubkey = self.create_usdc_mint_2022().await?; + account_infos.usdc_mint_2022_pubkey = usdc_mint_2022_pubkey; + + let (allowed_lookup_table, disallowed_lookup_table, transaction_lookup_table) = + self.create_lookup_tables().await?; + account_infos.allowed_lookup_table = allowed_lookup_table; + account_infos.disallowed_lookup_table = disallowed_lookup_table; + account_infos.transaction_lookup_table = transaction_lookup_table; + + let ( + sender_token_account, + recipient_token_account, + fee_payer_token_account, + sender_token_2022_account, + recipient_token_2022_account, + fee_payer_token_2022_account, + ) = self.setup_token_accounts().await?; + account_infos.sender_token_account = sender_token_account; + account_infos.recipient_token_account = recipient_token_account; + account_infos.fee_payer_token_account = fee_payer_token_account; + account_infos.sender_token_2022_account = sender_token_2022_account; + account_infos.recipient_token_2022_account = recipient_token_2022_account; + account_infos.fee_payer_token_2022_account = fee_payer_token_2022_account; // Wait for the accounts to be fully initialized (lookup tables, etc.) let await_for_slot = self.rpc_client.get_slot().await? + 30; @@ -86,7 +133,7 @@ impl TestAccountSetup { tokio::time::sleep(std::time::Duration::from_millis(500)).await; } - Ok(account_info) + Ok(account_infos) } pub async fn airdrop_if_required_sol(&self, receiver: &Pubkey, amount: u64) -> Result<()> { @@ -112,7 +159,7 @@ impl TestAccountSetup { Ok(()) } - pub async fn fund_sol_accounts(&self) -> Result<()> { + pub async fn fund_sol_accounts(&self) -> Result<(Pubkey, Pubkey, Pubkey)> { let sol_to_fund = 10 * LAMPORTS_PER_SOL; let sender_pubkey = self.sender_keypair.pubkey(); @@ -124,12 +171,12 @@ impl TestAccountSetup { self.airdrop_if_required_sol(&fee_payer_pubkey, sol_to_fund) )?; - Ok(()) + Ok((self.sender_keypair.pubkey(), self.recipient_pubkey, self.fee_payer_keypair.pubkey())) } - pub async fn create_usdc_mint(&self) -> Result<()> { + pub async fn create_usdc_mint(&self) -> Result { if (self.rpc_client.get_account(&self.usdc_mint.pubkey()).await).is_ok() { - return Ok(()); + return Ok(self.usdc_mint.pubkey()); } let rent = self @@ -164,12 +211,12 @@ impl TestAccountSetup { self.rpc_client.send_and_confirm_transaction(&transaction).await?; - Ok(()) + Ok(self.usdc_mint.pubkey()) } - pub async fn create_usdc_mint_2022(&self) -> Result<()> { + 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(()); + return Ok(self.usdc_mint_2022.pubkey()); } let decimals = USDCMintTestHelper::get_test_usdc_mint_decimals(); @@ -221,10 +268,12 @@ impl TestAccountSetup { self.rpc_client.send_and_confirm_transaction(&transaction).await?; - Ok(()) + Ok(self.usdc_mint_2022.pubkey()) } - pub async fn setup_token_accounts(&self) -> Result { + pub async fn setup_token_accounts( + &self, + ) -> Result<(Pubkey, Pubkey, Pubkey, Pubkey, Pubkey, Pubkey)> { // SPL Token accounts let sender_token_account = get_associated_token_address(&self.sender_keypair.pubkey(), &self.usdc_mint.pubkey()); @@ -332,20 +381,14 @@ impl TestAccountSetup { // 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(), - recipient_pubkey: self.recipient_pubkey, - usdc_mint_pubkey: self.usdc_mint.pubkey(), + Ok(( 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, - }) + )) } async fn mint_tokens_to_account(&self, token_account: &Pubkey, amount: u64) -> Result<()> { @@ -392,9 +435,10 @@ impl TestAccountSetup { Ok(()) } - async fn create_lookup_tables(&mut self) -> Result<()> { - LookupTableHelper::setup_and_save_lookup_tables(self.rpc_client.clone()).await?; + async fn create_lookup_tables(&mut self) -> Result<(Pubkey, Pubkey, Pubkey)> { + let (allowed_lookup_table, disallowed_lookup_table, transaction_lookup_table) = + LookupTableHelper::setup_and_save_lookup_tables(self.rpc_client.clone()).await?; - Ok(()) + Ok((allowed_lookup_table, disallowed_lookup_table, transaction_lookup_table)) } } diff --git a/tests/src/common/transaction.rs b/tests/src/common/transaction.rs index e12b6846..017f555e 100644 --- a/tests/src/common/transaction.rs +++ b/tests/src/common/transaction.rs @@ -260,7 +260,7 @@ impl TransactionBuilder { let fee_payer = self.fee_payer.ok_or_else(|| anyhow::anyhow!("Fee payer is required"))?; let blockhash = - rpc_client.get_latest_blockhash_with_commitment(CommitmentConfig::finalized()).await?; + rpc_client.get_latest_blockhash_with_commitment(CommitmentConfig::confirmed()).await?; let message = match self.version { diff --git a/tests/src/lib.rs b/tests/src/lib.rs index ed29a720..4e71485c 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -1,2 +1,2 @@ pub mod common; -pub mod setup_test_env; +pub mod test_runner; diff --git a/tests/src/setup_test_env.rs b/tests/src/setup_test_env.rs deleted file mode 100644 index cebd8a09..00000000 --- a/tests/src/setup_test_env.rs +++ /dev/null @@ -1,56 +0,0 @@ -use solana_client::nonblocking::rpc_client::RpcClient; - -use crate::common::{ - constants::DEFAULT_RPC_URL, helpers::TestAccountInfo, setup::TestAccountSetup, -}; - -pub async fn check_test_validator() -> bool { - let client = RpcClient::new(DEFAULT_RPC_URL.to_string()); - client.get_health().await.is_ok() -} - -pub async fn setup_test_accounts() -> Result { - let mut setup = TestAccountSetup::new().await; - setup.setup_all_accounts().await -} - -pub async fn run() -> Result<(), Box> { - println!("๐Ÿ”ง Setting up test environment..."); - - // Check if test validator is running - if !check_test_validator().await { - eprintln!("โŒ Error: Solana test validator is not running"); - eprintln!("Please start it with: surfpool start or solana-test-validator"); - std::process::exit(1); - } - let start_time = tokio::time::Instant::now(); - - println!("โœ… Test validator is running"); - - // Setup test accounts - match setup_test_accounts().await { - Ok(account_info) => { - println!("โœ… Test environment setup complete!"); - println!(); - println!("๐Ÿ“‹ Account Summary:"); - println!(" Fee Payer: {}", account_info.fee_payer_pubkey); - println!(" Sender: {}", account_info.sender_pubkey); - println!(" Recipient: {}", account_info.recipient_pubkey); - println!(" USDC Mint: {}", account_info.usdc_mint_pubkey); - println!(" Sender Token Account: {}", account_info.sender_token_account); - println!(" Recipient Token Account: {}", account_info.recipient_token_account); - println!(" Fee Payer Token Account: {}", account_info.fee_payer_token_account); - println!(); - println!("๐ŸŽฏ Ready to run integration tests!"); - println!("Run: cargo test --test integration"); - } - Err(e) => { - eprintln!("โŒ Failed to setup test environment: {e}"); - std::process::exit(1); - } - } - - println!("Time to setup test environment: {} seconds", start_time.elapsed().as_secs()); - - Ok(()) -} diff --git a/tests/src/test_runner/accounts.rs b/tests/src/test_runner/accounts.rs new file mode 100644 index 00000000..452bdc72 --- /dev/null +++ b/tests/src/test_runner/accounts.rs @@ -0,0 +1,268 @@ +use crate::common::{ + TestAccountInfo, KORA_PRIVATE_KEY_ENV, PAYMENT_ADDRESS_KEYPAIR_ENV, SIGNER_2_KEYPAIR_ENV, + TEST_ALLOWED_LOOKUP_TABLE_ADDRESS_ENV, TEST_DISALLOWED_LOOKUP_TABLE_ADDRESS_ENV, + TEST_INTEREST_BEARING_MINT_KEYPAIR_ENV, TEST_RECIPIENT_PUBKEY_ENV, TEST_SENDER_KEYPAIR_ENV, + TEST_TRANSACTION_LOOKUP_TABLE_ADDRESS_ENV, TEST_TRANSFER_HOOK_MINT_KEYPAIR_ENV, + TEST_USDC_MINT_2022_KEYPAIR_ENV, TEST_USDC_MINT_KEYPAIR_ENV, +}; +use base64::{engine::general_purpose::STANDARD, Engine}; +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_sdk::pubkey::Pubkey; +use std::{fs, path::Path}; + +const TEST_ACCOUNTS_DIR: &str = "tests/src/common/fixtures/test-accounts"; + +#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)] +pub enum AccountFile { + FeePayer, + Sender, + Recipient, + UsdcMint, + SenderTokenAccount, + RecipientTokenAccount, + FeePayerTokenAccount, + UsdcMint2022, + SenderToken2022Account, + RecipientToken2022Account, + FeePayerToken2022Account, + AllowedLookupTable, + DisallowedLookupTable, + TransactionLookupTable, + Signer2, + InterestBearingMint, + TransferHookMint, + Payment, +} + +impl AccountFile { + pub fn filename(&self) -> &'static str { + match self { + Self::FeePayer => "fee-payer-local.json", + Self::Sender => "sender-local.json", + Self::Recipient => "recipient-local.json", + Self::UsdcMint => "usdc-mint-local.json", + Self::SenderTokenAccount => "sender-token-account-local.json", + Self::RecipientTokenAccount => "recipient-token-account-local.json", + Self::FeePayerTokenAccount => "fee-payer-token-account-local.json", + Self::UsdcMint2022 => "usdc-mint-2022-local.json", + Self::SenderToken2022Account => "sender-token-2022-account-local.json", + Self::RecipientToken2022Account => "recipient-token-2022-account-local.json", + Self::FeePayerToken2022Account => "fee-payer-token-2022-account-local.json", + Self::AllowedLookupTable => "allowed-lookup-table-local.json", + Self::DisallowedLookupTable => "disallowed-lookup-table-local.json", + Self::TransactionLookupTable => "transaction-lookup-table-local.json", + Self::Signer2 => "signer2-local.json", + Self::InterestBearingMint => "mint-2022-interest-bearing.json", + Self::TransferHookMint => "mint-transfer-hook-local.json", + Self::Payment => "payment-local.json", + } + } + + pub fn local_key_env_var(&self) -> &'static str { + match self { + Self::FeePayer => KORA_PRIVATE_KEY_ENV, + Self::Sender => TEST_SENDER_KEYPAIR_ENV, + Self::Recipient => TEST_RECIPIENT_PUBKEY_ENV, + Self::UsdcMint => TEST_USDC_MINT_KEYPAIR_ENV, + Self::UsdcMint2022 => TEST_USDC_MINT_2022_KEYPAIR_ENV, + Self::AllowedLookupTable => TEST_ALLOWED_LOOKUP_TABLE_ADDRESS_ENV, + Self::DisallowedLookupTable => TEST_DISALLOWED_LOOKUP_TABLE_ADDRESS_ENV, + Self::TransactionLookupTable => TEST_TRANSACTION_LOOKUP_TABLE_ADDRESS_ENV, + Self::Signer2 => SIGNER_2_KEYPAIR_ENV, + Self::InterestBearingMint => TEST_INTEREST_BEARING_MINT_KEYPAIR_ENV, + Self::TransferHookMint => TEST_TRANSFER_HOOK_MINT_KEYPAIR_ENV, + Self::Payment => PAYMENT_ADDRESS_KEYPAIR_ENV, + _ => panic!("Invalid account env"), + } + } + + pub fn local_key_path(&self) -> String { + format!("tests/src/common/local-keys/{}", self.filename()) + } + + pub fn test_account_path(&self) -> std::path::PathBuf { + Path::new(TEST_ACCOUNTS_DIR).join(self.filename()) + } + + pub fn required_test_accounts() -> &'static [AccountFile] { + &[ + Self::FeePayer, + Self::Sender, + Self::Recipient, + Self::UsdcMint, + Self::SenderTokenAccount, + Self::RecipientTokenAccount, + Self::FeePayerTokenAccount, + Self::UsdcMint2022, + Self::SenderToken2022Account, + Self::RecipientToken2022Account, + Self::FeePayerToken2022Account, + Self::AllowedLookupTable, + Self::DisallowedLookupTable, + Self::TransactionLookupTable, + Self::Signer2, + Self::InterestBearingMint, + Self::TransferHookMint, + Self::Payment, + ] + } + + pub fn required_test_accounts_env_vars() -> &'static [AccountFile] { + &[ + Self::FeePayer, + Self::Signer2, + Self::Sender, + Self::UsdcMint, + Self::UsdcMint2022, + Self::InterestBearingMint, + Self::TransferHookMint, + Self::Payment, + ] + } + + pub fn required_for_kora() -> &'static [AccountFile] { + &[Self::FeePayer, Self::Signer2] + } + + pub fn set_environment_variable_from_cache( + &self, + cached_keys: &std::collections::HashMap, + ) -> Result<(), Box> { + let key = + cached_keys.get(self).ok_or_else(|| format!("Key not found in cache: {self:?}"))?; + std::env::set_var(self.local_key_env_var(), key.trim()); + Ok(()) + } + + pub fn set_dynamic_environment_variable( + &self, + value: &str, + ) -> Result<(), Box> { + std::env::set_var(self.local_key_env_var(), value); + Ok(()) + } + + pub async fn save_account_for_file( + &self, + client: &RpcClient, + address: &Pubkey, + ) -> Result<(), Box> { + save_account(client, address, self.test_account_path()).await + } + + pub fn get_as_env_var(&self) -> (&'static str, String) { + (self.local_key_env_var(), std::env::var(self.local_key_env_var()).unwrap()) + } +} + +pub fn set_environment_variables( + cached_keys: &std::collections::HashMap, +) -> Result<(), Box> { + for account_file in AccountFile::required_test_accounts_env_vars() { + if cached_keys.contains_key(account_file) { + account_file.set_environment_variable_from_cache(cached_keys)?; + } else { + // For accounts not in cache, fallback to file read + let key = std::fs::read_to_string(account_file.local_key_path())?; + std::env::set_var(account_file.local_key_env_var(), key.trim()); + } + } + + Ok(()) +} + +pub async fn set_lookup_table_environment_variables( + test_accounts: &TestAccountInfo, +) -> Result<(), Box> { + AccountFile::AllowedLookupTable + .set_dynamic_environment_variable(&test_accounts.allowed_lookup_table.to_string())?; + AccountFile::DisallowedLookupTable + .set_dynamic_environment_variable(&test_accounts.disallowed_lookup_table.to_string())?; + AccountFile::TransactionLookupTable + .set_dynamic_environment_variable(&test_accounts.transaction_lookup_table.to_string())?; + Ok(()) +} + +pub async fn get_account_address_from_file( + account_path: &Path, +) -> Result> { + let account_json = tokio::fs::read_to_string(account_path).await?; + let account_data: serde_json::Value = serde_json::from_str(&account_json)?; + + if let Some(pubkey) = account_data["account"]["pubkey"].as_str() { + return Ok(pubkey.to_string()); + } + + if let Some(pubkey) = account_data["pubkey"].as_str() { + return Ok(pubkey.to_string()); + } + + Err("Could not find pubkey in account file".into()) +} + +pub async fn download_accounts( + client: &RpcClient, + test_accounts: &TestAccountInfo, +) -> Result<(), Box> { + let accounts_dir = Path::new(TEST_ACCOUNTS_DIR); + fs::create_dir_all(accounts_dir)?; + + AccountFile::FeePayer.save_account_for_file(client, &test_accounts.fee_payer_pubkey).await?; + AccountFile::Sender.save_account_for_file(client, &test_accounts.sender_pubkey).await?; + AccountFile::Recipient.save_account_for_file(client, &test_accounts.recipient_pubkey).await?; + AccountFile::UsdcMint.save_account_for_file(client, &test_accounts.usdc_mint_pubkey).await?; + AccountFile::SenderTokenAccount + .save_account_for_file(client, &test_accounts.sender_token_account) + .await?; + AccountFile::RecipientTokenAccount + .save_account_for_file(client, &test_accounts.recipient_token_account) + .await?; + AccountFile::FeePayerTokenAccount + .save_account_for_file(client, &test_accounts.fee_payer_token_account) + .await?; + AccountFile::UsdcMint2022 + .save_account_for_file(client, &test_accounts.usdc_mint_2022_pubkey) + .await?; + AccountFile::SenderToken2022Account + .save_account_for_file(client, &test_accounts.sender_token_2022_account) + .await?; + AccountFile::RecipientToken2022Account + .save_account_for_file(client, &test_accounts.recipient_token_2022_account) + .await?; + AccountFile::FeePayerToken2022Account + .save_account_for_file(client, &test_accounts.fee_payer_token_2022_account) + .await?; + AccountFile::AllowedLookupTable + .save_account_for_file(client, &test_accounts.allowed_lookup_table) + .await?; + AccountFile::DisallowedLookupTable + .save_account_for_file(client, &test_accounts.disallowed_lookup_table) + .await?; + AccountFile::TransactionLookupTable + .save_account_for_file(client, &test_accounts.transaction_lookup_table) + .await?; + Ok(()) +} + +async fn save_account( + client: &RpcClient, + address: &Pubkey, + path: std::path::PathBuf, +) -> Result<(), Box> { + let account = client.get_account(address).await?; + + let account_data = serde_json::json!({ + "pubkey": address.to_string(), + "account": { + "lamports": account.lamports, + "data": [STANDARD.encode(&account.data), "base64"], + "owner": account.owner.to_string(), + "executable": account.executable, + "rentEpoch": 0 + } + }); + + std::fs::write(path, serde_json::to_string_pretty(&account_data)?)?; + + Ok(()) +} diff --git a/tests/src/test_runner/commands.rs b/tests/src/test_runner/commands.rs new file mode 100644 index 00000000..1d3ac7d1 --- /dev/null +++ b/tests/src/test_runner/commands.rs @@ -0,0 +1,111 @@ +use crate::{ + common::TEST_SERVER_URL_ENV, + test_runner::{ + accounts::AccountFile, + output::{filter_and_colorize_output, OutputFilter, TestPhaseColor}, + }, +}; + +pub enum TestLanguage { + Rust, + TypeScript, +} + +pub struct TestCommandHelper; + +impl TestCommandHelper { + pub async fn run_test( + test_language: TestLanguage, + test_name: &str, + port: &str, + color: TestPhaseColor, + verbose: bool, + output: &mut String, + ) -> Result<(), Box> { + match test_language { + TestLanguage::Rust => { + Self::run_tests_buffered(test_name, port, color, verbose, output).await + } + TestLanguage::TypeScript => { + Self::run_typescript_tests(test_name, port, color, verbose, output).await + } + } + } + + async fn run_tests_buffered( + test_name: &str, + port: &str, + color: TestPhaseColor, + verbose: bool, + output: &mut String, + ) -> Result<(), Box> { + let server_url = format!("http://127.0.0.1:{port}"); + + let mut cmd = tokio::process::Command::new("cargo"); + + cmd.args(["test", "-p", "tests", "--test", test_name, "--", "--nocapture"]) + .env(TEST_SERVER_URL_ENV, &server_url); + + for account_file in AccountFile::required_test_accounts_env_vars() { + let (env_var, value) = account_file.get_as_env_var(); + cmd.env(env_var, value); + } + + let cmd_output = cmd.output().await?; + + if !cmd_output.status.success() { + let stderr = String::from_utf8_lossy(&cmd_output.stderr); + let filtered_stderr = + filter_and_colorize_output(&stderr, OutputFilter::Test, verbose, color); + if !filtered_stderr.is_empty() { + output.push_str(&filtered_stderr); + } + return Err(format!("{test_name} tests failed").into()); + } + + let stdout = String::from_utf8_lossy(&cmd_output.stdout); + let filtered_stdout = + filter_and_colorize_output(&stdout, OutputFilter::Test, verbose, color); + output.push_str(&filtered_stdout); + Ok(()) + } + + async fn run_typescript_tests( + test_name: &str, + port: &str, + color: TestPhaseColor, + verbose: bool, + output: &mut String, + ) -> Result<(), Box> { + let pnpm_command = match test_name { + "typescript_basic" => "test:integration", + "typescript_auth" => "test:integration:auth", + "typescript_turnkey" => "test:integration:turnkey", + "typescript_privy" => "test:integration:privy", + _ => return Err(format!("Unknown TypeScript test: {test_name}").into()), + }; + + let server_url = format!("http://127.0.0.1:{port}"); + + let mut cmd = tokio::process::Command::new("pnpm"); + cmd.current_dir("sdks/ts").args(["run", pnpm_command]).env("KORA_RPC_URL", server_url); + + let cmd_output = cmd.output().await?; + + if !cmd_output.status.success() { + let stderr = String::from_utf8_lossy(&cmd_output.stderr); + let filtered_stderr = + filter_and_colorize_output(&stderr, OutputFilter::TypeScript, verbose, color); + if !filtered_stderr.is_empty() { + output.push_str(&filtered_stderr); + } + return Err(format!("{test_name} TypeScript tests failed").into()); + } + + let stdout = String::from_utf8_lossy(&cmd_output.stdout); + let filtered_stdout = + filter_and_colorize_output(&stdout, OutputFilter::TypeScript, verbose, color); + output.push_str(&filtered_stdout); + Ok(()) + } +} diff --git a/tests/src/test_runner/config.rs b/tests/src/test_runner/config.rs new file mode 100644 index 00000000..f5d880da --- /dev/null +++ b/tests/src/test_runner/config.rs @@ -0,0 +1,32 @@ +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, path::Path}; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct TestPhaseConfig { + pub name: String, + pub config: String, + pub signers: String, + pub port: String, + pub tests: Vec, + #[serde(default)] + pub initialize_payments_atas: bool, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct TestRunnerConfig { + pub test: HashMap, +} + +impl TestRunnerConfig { + pub async fn load_from_file>( + path: P, + ) -> Result> { + let content = tokio::fs::read_to_string(path).await?; + let config: TestRunnerConfig = toml::from_str(&content)?; + Ok(config) + } + + pub fn get_all_phases(&self) -> Vec<(String, TestPhaseConfig)> { + self.test.iter().map(|(key, config)| (key.clone(), config.clone())).collect() + } +} diff --git a/tests/src/test_runner/kora.rs b/tests/src/test_runner/kora.rs new file mode 100644 index 00000000..080c8ee0 --- /dev/null +++ b/tests/src/test_runner/kora.rs @@ -0,0 +1,96 @@ +use crate::test_runner::accounts::AccountFile; +use std::{ + collections::HashSet, + path::Path, + sync::{LazyLock, Mutex}, +}; +use tokio::{net::TcpListener, process::Child}; + +// Global port tracker to prevent immediate reuse +static USED_PORTS: LazyLock>> = LazyLock::new(|| Mutex::new(HashSet::new())); + +pub const KORA_BINARY_PATH: &str = "target/debug/kora"; +pub const PORT_RANGE_START: u16 = 8080; +pub const PORT_RANGE_END: u16 = 8180; + +pub async fn get_kora_binary_path() -> Result> { + if !Path::new(KORA_BINARY_PATH).exists() { + return Err(format!( + "Pre-built Kora binary not found at '{KORA_BINARY_PATH}'. \ + Run 'cargo build --bin kora' or 'make build' first for much better performance.", + ) + .into()); + } + Ok(KORA_BINARY_PATH.to_string()) +} + +pub async fn check_port_available(port: u16) -> bool { + TcpListener::bind(("127.0.0.1", port)).await.is_ok() +} + +pub async fn find_available_port() -> Result> { + for port in PORT_RANGE_START..PORT_RANGE_END { + // Check if port is available and not recently used + if check_port_available(port).await { + let mut used_ports = USED_PORTS.lock().unwrap(); + if !used_ports.contains(&port) { + used_ports.insert(port); + return Ok(port); + } + } + } + Err(format!("No available ports found in range {PORT_RANGE_START}-{PORT_RANGE_END}").into()) +} + +pub fn release_port(port: u16) { + let mut used_ports = USED_PORTS.lock().unwrap(); + used_ports.remove(&port); +} + +pub async fn is_kora_running_with_client(client: &reqwest::Client, port: &str) -> bool { + let url = format!("http://127.0.0.1:{port}/liveness"); + client.get(&url).timeout(std::time::Duration::from_secs(5)).send().await.is_ok() +} + +pub async fn start_kora_rpc_server( + rpc_url: String, + config_file: &str, + signers_config: &str, + cached_keys: &std::collections::HashMap, + preferred_port: u16, +) -> Result<(Child, u16), Box> { + let fee_payer_key = + cached_keys.get(&AccountFile::FeePayer).ok_or("FeePayer key not found in cache")?; + let signer_2 = + cached_keys.get(&AccountFile::Signer2).ok_or("Signer2 key not found in cache")?; + + let port = if check_port_available(preferred_port).await { + let mut used_ports = USED_PORTS.lock().unwrap(); + used_ports.insert(preferred_port); + preferred_port + } else { + find_available_port().await? + }; + let kora_binary_path = get_kora_binary_path().await?; + + let kora_pid = tokio::process::Command::new(kora_binary_path) + .args([ + "--config", + config_file, + "--rpc-url", + rpc_url.as_str(), + "rpc", + "start", + "--signers-config", + signers_config, + "--port", + &port.to_string(), + ]) + .env("KORA_PRIVATE_KEY", fee_payer_key.trim()) + .env("KORA_PRIVATE_KEY_2", signer_2.trim()) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .spawn()?; + + Ok((kora_pid, port)) +} diff --git a/tests/src/test_runner/mod.rs b/tests/src/test_runner/mod.rs new file mode 100644 index 00000000..066a4c85 --- /dev/null +++ b/tests/src/test_runner/mod.rs @@ -0,0 +1,6 @@ +pub mod accounts; +pub mod commands; +pub mod config; +pub mod kora; +pub mod output; +pub mod validator; diff --git a/tests/src/test_runner/output.rs b/tests/src/test_runner/output.rs new file mode 100644 index 00000000..5caa59f1 --- /dev/null +++ b/tests/src/test_runner/output.rs @@ -0,0 +1,193 @@ +pub const MAX_OUTPUT_SIZE: usize = 1024 * 1024; // 1MB limit + +#[derive(Debug)] +pub struct PhaseOutput { + pub phase_name: String, + pub output: String, + pub success: bool, + pub truncated: bool, +} + +#[derive(Debug, Clone, Copy)] +pub enum OutputFilter { + Test, + CliCommand, + TypeScript, +} + +#[derive(Debug, Clone, Copy)] +pub enum TestPhaseColor { + Regular, + Auth, + Payment, + MultiSigner, + TypeScriptBasic, + TypeScriptAuth, + TypeScriptTurnkey, + TypeScriptPrivy, +} + +impl TestPhaseColor { + pub fn from_phase_name(phase_name: &str) -> Self { + match phase_name { + "Regular Integration Tests" => Self::Regular, + "Auth Tests" => Self::Auth, + "Payment Address Tests" => Self::Payment, + "Multi-Signer Tests" => Self::MultiSigner, + name if name.starts_with("TypeScript") => Self::from_typescript_phase(name), + name if name.starts_with("typescript_") => Self::from_typescript_phase(name), + // Fallback patterns + name if name.to_lowercase().contains("auth") => Self::Auth, + name if name.to_lowercase().contains("payment") => Self::Payment, + name if name.to_lowercase().contains("multi") => Self::MultiSigner, + name if name.to_lowercase().contains("typescript") => Self::TypeScriptBasic, + _ => Self::Regular, + } + } + + fn from_typescript_phase(name: &str) -> Self { + match name { + "typescript_basic" => Self::TypeScriptBasic, + "typescript_auth" => Self::TypeScriptAuth, + "typescript_turnkey" => Self::TypeScriptTurnkey, + "typescript_privy" => Self::TypeScriptPrivy, + _ => Self::TypeScriptBasic, + } + } + + pub fn ansi_code(&self) -> &'static str { + match self { + Self::Regular => "\x1b[32m", // Green + Self::Auth => "\x1b[34m", // Blue + Self::Payment => "\x1b[33m", // Yellow + Self::MultiSigner => "\x1b[35m", // Magenta + Self::TypeScriptBasic => "\x1b[36m", // Cyan + Self::TypeScriptAuth => "\x1b[31m", // Red + Self::TypeScriptTurnkey => "\x1b[37m", // White + Self::TypeScriptPrivy => "\x1b[90m", // Gray + } + } + + pub fn reset_code() -> &'static str { + "\x1b[0m" + } + + pub fn colorize(&self, text: &str) -> String { + format!("{}{}{}", self.ansi_code(), text, Self::reset_code()) + } + + pub fn colorize_with_controlled_flow(&self, text: &str) -> String { + // Remove all existing newlines and add controlled ones with proper spacing + let cleaned_text = text.replace('\n', ""); + let controlled_text = format!("{cleaned_text}\n\n"); + format!("{}{}{}", self.ansi_code(), controlled_text, Self::reset_code()) + } +} + +impl OutputFilter { + pub fn should_show_line(&self, line: &str, show_verbose: bool) -> bool { + match self { + Self::Test => { + // + line.contains("test result:") + || line.contains("FAILED") + || line.contains("failures:") + || line.contains("panicked") + || line.contains("assertion") + || line.contains("ERROR") + || (show_verbose + && (line.contains("Compiling") + || line.contains("running ") + || line.starts_with("test ") + || line.contains("Finished") + || line.contains("warning:") + || line.contains("error:"))) + } + Self::CliCommand => { + line.contains("ERROR") + || line.contains("error") + || line.contains("Failed") + || line.contains("Success") + || line.contains("โœ—") + || (show_verbose + && (line.contains("INFO") + || line.contains("โœ“") + || line.contains("Initialized") + || line.contains("Created"))) + } + Self::TypeScript => { + // Jest and TypeScript test output patterns + line.contains("PASS") + || line.contains("FAIL") + || line.contains("โœ“") + || line.contains("โœ—") + || line.contains("Tests:") + || line.contains("Test Suites:") + || line.contains("Snapshots:") + || line.contains("Time:") + || line.contains("Ran all test suites") + || line.contains("Test results:") + || line.contains("expect") + || line.contains("Error:") + || line.contains("AssertionError") + || line.contains("TypeError") + || line.contains("ReferenceError") + || line.contains("failed with exit code") + || line.contains("npm ERR!") + || line.contains("pnpm ERR!") + || (show_verbose + && (line.contains("Running") + || line.contains("Starting") + || line.contains("Finished"))) + } + } + } +} + +pub fn filter_command_output(output: &str, filter: OutputFilter, show_verbose: bool) -> String { + // If verbose, show everything without filtering + if show_verbose { + return clean_multiple_newlines(output); + } + + // Otherwise apply pattern filtering + let filtered = output + .lines() + .filter(|line| filter.should_show_line(line, show_verbose)) + .collect::>() + .join("\n"); + + clean_multiple_newlines(&filtered) +} + +fn clean_multiple_newlines(text: &str) -> String { + // Replace multiple consecutive newlines with single newlines + let mut result = text.to_string(); + while result.contains("\n\n\n") { + result = result.replace("\n\n\n", "\n\n"); + } + result.trim_end().to_string() +} + +pub fn filter_and_colorize_output( + output: &str, + filter: OutputFilter, + show_verbose: bool, + color: TestPhaseColor, +) -> String { + let filtered = filter_command_output(output, filter, show_verbose); + color.colorize(&filtered) +} + +pub fn limit_output_size(output: String) -> (String, bool) { + if output.len() > MAX_OUTPUT_SIZE { + let truncated_output = format!( + "{}... (truncated {} bytes)", + &output[..MAX_OUTPUT_SIZE], + output.len() - MAX_OUTPUT_SIZE + ); + (truncated_output, true) + } else { + (output, false) + } +} diff --git a/tests/src/test_runner/test_cases.toml b/tests/src/test_runner/test_cases.toml new file mode 100644 index 00000000..4fe73bfb --- /dev/null +++ b/tests/src/test_runner/test_cases.toml @@ -0,0 +1,42 @@ +[test.regular] +name = "Regular Integration Tests" +config = "tests/src/common/fixtures/kora-test.toml" +signers = "tests/src/common/fixtures/signers.toml" +port = "8080" +tests = ["rpc", "tokens", "external"] + +[test.auth] +name = "Auth Tests" +config = "tests/src/common/fixtures/auth-test.toml" +signers = "tests/src/common/fixtures/signers.toml" +port = "8081" +tests = ["auth"] + +[test.payment_address] +name = "Payment Address Tests" +config = "tests/src/common/fixtures/paymaster-address-test.toml" +signers = "tests/src/common/fixtures/signers.toml" +port = "8082" +tests = ["payment_address"] +initialize_payments_atas = true + +[test.multi_signer] +name = "Multi-Signer Tests" +config = "tests/src/common/fixtures/kora-test.toml" +signers = "tests/src/common/fixtures/multi-signers.toml" +port = "8083" +tests = ["multi_signer"] + +[test.typescript_basic] +name = "TypeScript SDK Tests" +config = "tests/src/common/fixtures/kora-test.toml" +signers = "tests/src/common/fixtures/signers.toml" +port = "8084" +tests = ["typescript_basic"] + +[test.typescript_auth] +name = "TypeScript Auth Tests" +config = "tests/src/common/fixtures/auth-test.toml" +signers = "tests/src/common/fixtures/signers.toml" +port = "8085" +tests = ["typescript_auth"] diff --git a/tests/src/test_runner/validator.rs b/tests/src/test_runner/validator.rs new file mode 100644 index 00000000..e17157bf --- /dev/null +++ b/tests/src/test_runner/validator.rs @@ -0,0 +1,74 @@ +use crate::{ + common::constants::DEFAULT_RPC_URL, + test_runner::accounts::{get_account_address_from_file, AccountFile}, +}; +use solana_client::nonblocking::rpc_client::RpcClient; +use std::path::Path; +use tokio::process::Child; + +const TRANSFER_HOOK_PROGRAM_ID: &str = "Bcdikjss8HWzKEuj6gEQoFq9TCnGnk6v3kUnRU1gb6hA"; +const TRANSFER_HOOK_PROGRAM_PATH: &str = + "tests/src/common/transfer-hook-example/transfer_hook_example.so"; + +pub async fn check_test_validator(rpc_url: &str) -> bool { + let client = RpcClient::new_with_commitment( + rpc_url.to_string(), + solana_commitment_config::CommitmentConfig::confirmed(), + ); + client.get_health().await.is_ok() +} + +pub async fn start_test_validator( + load_accounts: bool, +) -> Result> { + let mut cmd = tokio::process::Command::new("solana-test-validator"); + cmd.arg("--reset").arg("--quiet"); + + if Path::new(TRANSFER_HOOK_PROGRAM_PATH).exists() { + cmd.arg("--bpf-program").arg(TRANSFER_HOOK_PROGRAM_ID).arg(TRANSFER_HOOK_PROGRAM_PATH); + } else { + println!("โš ๏ธ Transfer hook program not found at: {TRANSFER_HOOK_PROGRAM_PATH}"); + println!(" Starting validator without transfer hook program"); + println!(" Run 'make build-transfer-hook' to build it if needed"); + } + + if load_accounts { + for account_file in AccountFile::required_test_accounts() { + let account_path = account_file.test_account_path(); + if account_path.exists() { + if let Ok(account_address) = get_account_address_from_file(&account_path).await { + cmd.arg("--account").arg(&account_address).arg(&account_path); + println!( + "Loading account: {} from {}", + account_address, + account_path.display() + ); + } + } + } + } + + let validator_pid = + cmd.stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null()).spawn()?; + + let mut attempts = 0; + let mut delay = std::time::Duration::from_millis(100); + let max_delay = std::time::Duration::from_secs(2); + let max_attempts = 15; + + while !check_test_validator(DEFAULT_RPC_URL).await { + attempts += 1; + if attempts > max_attempts { + return Err(format!( + "Solana test validator failed to start within {max_attempts} attempts" + ) + .into()); + } + + tokio::time::sleep(delay).await; + delay = std::cmp::min(delay * 2, max_delay); + } + + println!("Solana test validator started successfully"); + Ok(validator_pid) +} From ad5d81f1a13cfe8f3654aca9d7a9f8a6e38bff21 Mon Sep 17 00:00:00 2001 From: Jo D Date: Mon, 22 Sep 2025 16:09:11 -0400 Subject: [PATCH 03/29] Removed privy and turnkey tests from CI --- .github/workflows/typescript-integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/typescript-integration.yml b/.github/workflows/typescript-integration.yml index 6643bc98..11515966 100644 --- a/.github/workflows/typescript-integration.yml +++ b/.github/workflows/typescript-integration.yml @@ -53,7 +53,7 @@ jobs: strategy: fail-fast: false matrix: - test-group: [typescript_basic, typescript_auth, typescript_turnkey, typescript_privy] + test-group: [typescript_basic, typescript_auth] steps: - uses: actions/checkout@v4 From ed44607e816c0667d5737a100b40540ee17f0b07 Mon Sep 17 00:00:00 2001 From: amilz <85324096+amilz@users.noreply.github.com> Date: Mon, 22 Sep 2025 14:28:18 -0700 Subject: [PATCH 04/29] docs: add runner to ADDING_SIGNERS.md and CLAUDE.md --- CLAUDE.md | 117 +++++++++++++--------------- docs/contributors/ADDING_SIGNERS.md | 84 +++++++++++++------- 2 files changed, 110 insertions(+), 91 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0bbe8cd7..0134b2ca 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -76,10 +76,7 @@ make lint-fix-all # Run unit tests make test -# Setup test environment (for integration tests) -make setup-test-env - -# Run integration tests +# Run integration tests (automatically handles environment setup) make test-integration # Run all tests @@ -88,7 +85,7 @@ cargo test --workspace #### Integration Test Environment Setup -Integration tests are fully automated using a makefile-based approach that handles sequential test execution: +Integration tests are fully automated using a Rust test runner binary that handles sequential test execution: **Quick Start:** ```bash @@ -100,24 +97,28 @@ make test-integration 2. **Test Environment Setup**: Creates test accounts, tokens, and ATAs 3. **Sequential Test Phases**: Runs 3 test suites with different configurations -**Test Phases (Auto-managed):** +**Test Phases (Configured in `tests/src/test_runner/test_cases.toml`):** -**Phase 1: Regular integration tests (26 tests)** +**Regular Tests** - Config: `tests/src/common/fixtures/kora-test.toml` (no auth) -- Server: Standard Kora RPC server - Tests: Core RPC functionality, token operations, compute budget -**Phase 2: Auth integration tests (4 tests)** +**Auth Tests** - Config: `tests/src/common/fixtures/auth-test.toml` (auth enabled) -- Server: Restart with auth middleware - Tests: API key and HMAC authentication validation -**Phase 3: Payment address tests (2 tests)** +**Payment Address Tests** - Config: `tests/src/common/fixtures/paymaster-address-test.toml` (payment address) -- Server: Restart with payment address configuration - **CLI ATA Initialization**: Automatically runs `kora rpc initialize-atas` before tests - Tests: Payment address validation and wrong destination rejection +**Multi-Signer Tests** +- Config: `tests/src/common/fixtures/multi-signers.toml` +- Tests: Multiple signer configurations + +**TypeScript Tests** +- Tests: TypeScript SDK integration tests + **File Structure:** ``` tests/ @@ -132,55 +133,48 @@ tests/ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ payment-local.json # Payment address keypair โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ sender-local.json # Sender keypair โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ usdc-mint-local.json # USDC mint keypair -โ”‚ โ”‚ โ””โ”€โ”€ setup_test_env.rs # Test environment setup +โ”‚ โ”‚ โ””โ”€โ”€ setup.rs # Test environment setup +โ”‚ โ”œโ”€โ”€ test_runner/ # Test runner modules +โ”‚ โ”‚ โ”œโ”€โ”€ accounts.rs # Account management +โ”‚ โ”‚ โ”œโ”€โ”€ commands.rs # Test command execution +โ”‚ โ”‚ โ”œโ”€โ”€ config.rs # Test configuration +โ”‚ โ”‚ โ”œโ”€โ”€ kora.rs # Kora server management +โ”‚ โ”‚ โ”œโ”€โ”€ output.rs # Output handling +โ”‚ โ”‚ โ”œโ”€โ”€ test_cases.toml # Test phase configurations +โ”‚ โ”‚ โ””โ”€โ”€ validator.rs # Solana validator management โ”‚ โ””โ”€โ”€ bin/ -โ”‚ โ””โ”€โ”€ setup_test_env.rs # Binary wrapper for setup +โ”‚ โ””โ”€โ”€ test_runner.rs # Main test runner binary โ”œโ”€โ”€ integration/ # Regular integration tests โ”œโ”€โ”€ auth/ # Authentication tests โ””โ”€โ”€ payment-address/ # Payment address tests ``` -**Manual Test Commands (if needed):** +**Test Runner Commands:** ```bash -# Setup test environment only -make setup-test-env +# Run all integration tests (default) +make test-integration + +# Run with verbose output +make test-integration-verbose -# Clean up test environment -make clean-test-env +# Force refresh test accounts (ignore cached) +make test-integration-fresh -# Run specific test phase -cargo test -p tests --test integration -cargo test -p tests --test auth -cargo test -p tests --test payment-address +# Run specific test +cargo run -p tests --bin test_runner -- --filter regular +cargo run -p tests --bin test_runner -- --filter auth +cargo run -p tests --bin test_runner -- --filter payment_address +cargo run -p tests --bin test_runner -- --filter multi_signer +cargo run -p tests --bin test_runner -- --filter typescript_basic +cargo run -p tests --bin test_runner -- --filter typescript_auth ``` #### Customize Test Environment -The test suite uses environment variables for configuration (checked before falling back to defaults): - -| Variable | Description | Default | -|----------|-------------|---------| -| `RPC_URL` | Solana RPC endpoint | `http://127.0.0.1:8899` | -| `TEST_SERVER_URL` | Kora RPC server URL | `http://127.0.0.1:8080` | -| `TEST_SENDER_KEYPAIR` | Base58 encoded test sender keypair | Built-in test keypair | -| `TEST_RECIPIENT_PUBKEY` | Test recipient public key | Built-in test pubkey | -| `KORA_PRIVATE_KEY` | Kora fee payer private key | Built-in test keypair | -| `TEST_USDC_MINT_KEYPAIR` | Test USDC mint keypair | Built-in test mint | -| `TEST_USDC_MINT_DECIMALS` | USDC mint decimals | `6` | +The test suite uses environment variables for configuration specified in `tests/src/common/constants.rs`. Make sure to update the appropriate config file (kora.toml for production, tests/common/fixtures/kora-test.toml for testing) to reflect the public key of TEST_USDC_MINT_KEYPAIR. -**Example with custom test configuration:** -```bash -# Create .env file for custom test setup -echo "RPC_URL=https://api.devnet.solana.com" > .env -echo "TEST_SENDER_KEYPAIR=your_base58_keypair" >> .env -echo "KORA_PRIVATE_KEY=your_fee_payer_keypair" >> .env - -# Run tests with custom config -make test-integration -``` - ### Running Services ```bash @@ -562,29 +556,28 @@ Both skip `/liveness` endpoint and can be used simultaneously. Implementation us - **API tests** - Include example JSON payloads in `tests/examples/` - **SDK tests** - TypeScript tests in `sdks/*/test/` directories -## Makefile Structure +## Test Runner Architecture -The project uses a modular makefile approach for better organization: +The project uses a Rust-based test runner for integration testing: ``` -/Makefile # Main makefile with includes -/makefiles/ -โ”œโ”€โ”€ utils.mk # Common utilities and functions -โ”œโ”€โ”€ rs.mk # Rust build commands -โ”œโ”€โ”€ tests_rs.mk # Integration test orchestration -โ””โ”€โ”€ docs.mk # Documentation generation +/tests/src/ +โ”œโ”€โ”€ bin/test_runner.rs # Main test runner binary +โ”œโ”€โ”€ test_runner/ # Test runner modules +โ”‚ โ”œโ”€โ”€ test_cases.toml # Test phase configurations +โ”‚ โ”œโ”€โ”€ accounts.rs # Account management & caching +โ”‚ โ”œโ”€โ”€ commands.rs # Test execution logic +โ”‚ โ”œโ”€โ”€ kora.rs # Kora server lifecycle +โ”‚ โ””โ”€โ”€ validator.rs # Solana validator management +โ””โ”€โ”€ common/ # Shared test utilities ``` **Key Features:** -- **Sequential Test Execution**: Prevents port conflicts and ensures clean config isolation -- **Server Lifecycle Management**: Automatic start/stop with proper cleanup -- **CLI Integration**: Automated `kora rpc initialize-atas` for payment address tests -- **Parallel Tool Execution**: Optimized for development workflow efficiency - -**Core Functions (from utils.mk):** -- `start_solana_validator` / `stop_solana_validator`: Validator lifecycle -- `start_kora_server` / `stop_kora_server`: Server lifecycle with config switching -- `run_integration_phase`: Orchestrates server + CLI + tests for each phase +- **Rust Test Runner**: Single binary manages all test phases +- **TOML Configuration**: Test phases defined in `test_cases.toml` +- **Account Caching**: Reuses test accounts for faster execution +- **Isolated Ports**: Each test phase uses unique ports to avoid conflicts +- **TypeScript Integration**: Seamlessly runs TS SDK tests alongside Rust tests ## Development Guidelines diff --git a/docs/contributors/ADDING_SIGNERS.md b/docs/contributors/ADDING_SIGNERS.md index 8e6c0a22..7eee6c1a 100644 --- a/docs/contributors/ADDING_SIGNERS.md +++ b/docs/contributors/ADDING_SIGNERS.md @@ -400,48 +400,74 @@ Define a `example-signer.toml` with your signer's configuration and necessary en ### Integration Tests -Testing should utilize the existing `sdks/ts/test/integration.test.ts` file. We manage this with an environment variable `KORA_SIGNER_TYPE` that is set to the signer type you are testing. - -Setup: -- `sdks/ts/test/setup.ts`: You will need to add an environment variable to `loadEnvironmentVariables` to set the signer type to your service. -- `sdks/ts/package.json`: You will need to add a new test script that uses the signer type you added to the setup script: `"test:integration:your-service": "KORA_SIGNER_TYPE=your-service pnpm test integration.test.ts"`. When executed, the test will run with the signer type set to your service. - -Integration testing requires a local Solana test validator and a local Kora node. We can use the [TypeScript Test Makefile](/makefiles/tests_ts.mk) to start a local Solana test validator and a local Kora node by adding a new script for your signer: - -```makefile -# Run TypeScript tests with YourService signer -test-ts-integration-your-service: - @$(call start_solana_validator) - @echo "๐Ÿš€ Starting Kora node with YourService signer..." - @$(call stop_kora_server) - @cargo run -p kora-cli --bin kora -- --config $(REGULAR_CONFIG) --rpc-url $(TEST_RPC_URL) rpc start --signers-config $(TEST_SIGNERS_YOUR_SERVICE_CONFIG) --port $(TEST_PORT) $(QUIET_OUTPUT) & - @echo $$! > .kora.pid - @echo "โณ Waiting for server to start..." - @sleep 5 - @printf "Running TypeScript SDK tests with YourService signer...\n" - -@cd sdks/ts && pnpm test:integration:your-service - @$(call stop_kora_server) - @$(call stop_solana_validator) +Kora uses a unified test runner (`tests/src/bin/test_runner.rs`) that manages all integration testing phases including TypeScript tests. To add tests for your new signer: + +#### 1. Add Test Configuration + +Create a new signer configuration file in `tests/src/common/fixtures/` for your service: + +```toml +# tests/src/common/fixtures/signers-your-service.toml +[[signers]] +name = "yourservice_main" +type = "your_service" +api_key_env = "YOUR_SERVICE_API_KEY" +api_secret_env = "YOUR_SERVICE_API_SECRET" +wallet_id_env = "YOUR_SERVICE_WALLET_ID" ``` -And add your new script to the `test-ts-signers` script: +#### 2. Add Test Phase to Test Runner + +Update `tests/src/test_runner/test_cases.toml` to include a test phase for your signer: -```makefile -test-ts-signers: test-ts-integration-your-service # ... test-ts-existing-signer-tests +```toml +[test.your_service] +name = "YourService Signer Tests" +config = "tests/src/common/fixtures/kora-test.toml" +signers = "tests/src/common/fixtures/signers-your-service.toml" +port = "8090" # Use a unique port +tests = ["your_service"] ``` -Make sure your local environment is set up: +#### 3. TypeScript SDK Integration -```makefile +For TypeScript SDK testing with your signer: + +1. Update `sdks/ts/test/setup.ts` to recognize your signer type: + - Add environment variable handling for `KORA_SIGNER_TYPE=your-service` + +2. Add a test script in `sdks/ts/package.json`: + ```json + "test:integration:your-service": "KORA_SIGNER_TYPE=your-service pnpm test integration.test.ts" + ``` + +#### 4. Running Tests + +Make sure your environment is set up: + +```bash +# Install binaries and dependencies make install make install-ts-sdk make build-ts-sdk + +# Set environment variables for your service +export YOUR_SERVICE_API_KEY="your_key" +export YOUR_SERVICE_API_SECRET="your_secret" +export YOUR_SERVICE_WALLET_ID="your_wallet" ``` -Now you can test your integration by running: +Run tests using the unified test runner: ```bash -make test-ts-integration-your-service +# Run all integration tests (includes your new signer phase) +make test-integration + +# Run tests with verbose output +make test-integration-verbose + +# Run specific test phase with filter +cargo run -p tests --bin test_runner -- --phases your_service ``` From cbf722545d90d0722dbb3bade816e8d2adb8ebb5 Mon Sep 17 00:00:00 2001 From: jo <17280917+dev-jodee@users.noreply.github.com> Date: Tue, 23 Sep 2025 16:26:18 -0400 Subject: [PATCH 05/29] =?UTF-8?q?feat:=20(PRO-255)=20add=20adversarial=20t?= =?UTF-8?q?ests=20for=20fee=20payer=20policy=20violations=E2=80=A6=20(#224?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Cache issue for github action, tested locally successfully** * feat: (PRO-255) add adversarial tests for fee payer policy violations and other scenarios - Introduced new tests for fee payer policy violations, including SOL and SPL transfer restrictions. - Added adversarial test suite to validate fee payer behavior under restrictive policies. - Updated configuration files for testing scenarios and added new test cases for various fee payer policy violations. - Refactored existing test utilities to support new test cases and improved error assertions. * PR Fixes --- CLAUDE.md | 2 +- .../src/validator/transaction_validator.rs | 38 ++- tests/Cargo.toml | 8 + tests/adversarial/fee_payer_exploitation.rs | 100 ++++++ tests/adversarial/main.rs | 15 + tests/adversarial/program_validation.rs | 64 ++++ tests/adversarial/token_states.rs | 98 ++++++ .../fee_payer_policy_violations.rs | 301 ++++++++++++++++++ tests/fee_payer_policy/main.rs | 17 + tests/src/common/assertions.rs | 74 +++++ tests/src/common/auth_helpers.rs | 5 +- .../fixtures/fee-payer-policy-test.toml | 65 ++++ ...{multi-signers.toml => signers-multi.toml} | 0 tests/src/common/setup.rs | 8 +- tests/src/common/transaction.rs | 22 ++ tests/src/test_runner/output.rs | 11 +- tests/src/test_runner/test_cases.toml | 11 +- 17 files changed, 815 insertions(+), 24 deletions(-) create mode 100644 tests/adversarial/fee_payer_exploitation.rs create mode 100644 tests/adversarial/main.rs create mode 100644 tests/adversarial/program_validation.rs create mode 100644 tests/adversarial/token_states.rs create mode 100644 tests/fee_payer_policy/fee_payer_policy_violations.rs create mode 100644 tests/fee_payer_policy/main.rs create mode 100644 tests/src/common/fixtures/fee-payer-policy-test.toml rename tests/src/common/fixtures/{multi-signers.toml => signers-multi.toml} (100%) diff --git a/CLAUDE.md b/CLAUDE.md index 0134b2ca..797b539b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -113,7 +113,7 @@ make test-integration - Tests: Payment address validation and wrong destination rejection **Multi-Signer Tests** -- Config: `tests/src/common/fixtures/multi-signers.toml` +- Config: `tests/src/common/fixtures/signers-multi.toml` - Tests: Multiple signer configurations **TypeScript Tests** diff --git a/crates/lib/src/validator/transaction_validator.rs b/crates/lib/src/validator/transaction_validator.rs index 05f3d533..30ef3949 100644 --- a/crates/lib/src/validator/transaction_validator.rs +++ b/crates/lib/src/validator/transaction_validator.rs @@ -175,11 +175,11 @@ impl TransactionValidator { ) -> Result<(), KoraError> { let system_instructions = transaction_resolved.get_or_parse_system_instructions()?; - let check_if_allowed = |address: &Pubkey, policy_allowed: bool| { + let check_if_allowed = |address: &Pubkey, policy_allowed: bool, instruction_type: &str| { if *address == self.fee_payer_pubkey && !policy_allowed { - return Err(KoraError::InvalidTransaction( - "Fee payer cannot be used as source account".to_string(), - )); + return Err(KoraError::InvalidTransaction(format!( + "Fee payer cannot be used for '{instruction_type}'", + ))); } Ok(()) }; @@ -189,7 +189,11 @@ impl TransactionValidator { system_instructions.get(&ParsedSystemInstructionType::SystemTransfer).unwrap_or(&vec![]) { if let ParsedSystemInstructionData::SystemTransfer { sender, .. } = instruction { - check_if_allowed(sender, self.fee_payer_policy.allow_sol_transfers)?; + check_if_allowed( + sender, + self.fee_payer_policy.allow_sol_transfers, + "System Transfer", + )?; } } @@ -197,7 +201,7 @@ impl TransactionValidator { system_instructions.get(&ParsedSystemInstructionType::SystemAssign).unwrap_or(&vec![]) { if let ParsedSystemInstructionData::SystemAssign { authority } = instruction { - check_if_allowed(authority, self.fee_payer_policy.allow_assign)?; + check_if_allowed(authority, self.fee_payer_policy.allow_assign, "System Assign")?; } } @@ -209,9 +213,17 @@ impl TransactionValidator { { if let ParsedSPLInstructionData::SplTokenTransfer { owner, is_2022, .. } = instruction { if *is_2022 { - check_if_allowed(owner, self.fee_payer_policy.allow_token2022_transfers)?; + check_if_allowed( + owner, + self.fee_payer_policy.allow_token2022_transfers, + "Token2022 Token Transfer", + )?; } else { - check_if_allowed(owner, self.fee_payer_policy.allow_spl_transfers)?; + check_if_allowed( + owner, + self.fee_payer_policy.allow_spl_transfers, + "SPL Token Transfer", + )?; } } } @@ -220,7 +232,7 @@ impl TransactionValidator { spl_instructions.get(&ParsedSPLInstructionType::SplTokenApprove).unwrap_or(&vec![]) { if let ParsedSPLInstructionData::SplTokenApprove { owner, .. } = instruction { - check_if_allowed(owner, self.fee_payer_policy.allow_approve)?; + check_if_allowed(owner, self.fee_payer_policy.allow_approve, "SPL Token Approve")?; } } @@ -228,7 +240,7 @@ impl TransactionValidator { spl_instructions.get(&ParsedSPLInstructionType::SplTokenBurn).unwrap_or(&vec![]) { if let ParsedSPLInstructionData::SplTokenBurn { owner, .. } = instruction { - check_if_allowed(owner, self.fee_payer_policy.allow_burn)?; + check_if_allowed(owner, self.fee_payer_policy.allow_burn, "SPL Token Burn")?; } } @@ -236,7 +248,11 @@ impl TransactionValidator { spl_instructions.get(&ParsedSPLInstructionType::SplTokenCloseAccount).unwrap_or(&vec![]) { if let ParsedSPLInstructionData::SplTokenCloseAccount { owner, .. } = instruction { - check_if_allowed(owner, self.fee_payer_policy.allow_close_account)?; + check_if_allowed( + owner, + self.fee_payer_policy.allow_close_account, + "SPL Token Close Account", + )?; } } diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 675d30d8..70b244ba 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -20,6 +20,14 @@ path = "tokens/main.rs" name = "auth" path = "auth/main.rs" +[[test]] +name = "adversarial" +path = "adversarial/main.rs" + +[[test]] +name = "fee_payer_policy" +path = "fee_payer_policy/main.rs" + [[test]] name = "multi_signer" path = "multi_signer/main.rs" diff --git a/tests/adversarial/fee_payer_exploitation.rs b/tests/adversarial/fee_payer_exploitation.rs new file mode 100644 index 00000000..2250484f --- /dev/null +++ b/tests/adversarial/fee_payer_exploitation.rs @@ -0,0 +1,100 @@ +use crate::common::{assertions::RpcErrorAssertions, *}; +use jsonrpsee::rpc_params; +use solana_sdk::{signer::Signer, transaction::Transaction}; +use solana_system_interface::instruction::transfer; +use spl_associated_token_account::get_associated_token_address; +use spl_token::instruction as token_instruction; + +#[tokio::test] +async fn test_fee_payer_as_sol_transfer_source() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + let setup = TestAccountSetup::new().await; + + let fee_payer_pubkey = FeePayerTestHelper::get_fee_payer_pubkey(); + + let large_sol_transfer = transfer( + &fee_payer_pubkey, + &setup.sender_keypair.pubkey(), + 1_000_000, // 0.001 SOL in lamports + ); + + let malicious_tx = ctx + .transaction_builder() + .with_fee_payer(fee_payer_pubkey) + .with_spl_payment( + &setup.usdc_mint.pubkey(), + &setup.sender_keypair.pubkey(), + &fee_payer_pubkey, + 10, // Small payment: 0.00001 USDC tokens (much less than 0.001 SOL value) + ) + .with_instruction(large_sol_transfer) + .build() + .await + .expect("Failed to create transaction with fee payer as SOL source"); + + let result = ctx + .rpc_call::("signTransactionIfPaid", rpc_params![malicious_tx]) + .await; + + match result { + Err(error) => { + error.assert_contains_message("Insufficient token payment"); + } + Ok(_) => panic!("Expected error for fee payer as SOL transfer source"), + } +} + +#[tokio::test] +async fn test_fee_payer_as_spl_transfer_source() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + let setup = TestAccountSetup::new().await; + + let fee_payer_pubkey = FeePayerTestHelper::get_fee_payer_pubkey(); + + let fee_payer_token_account = + get_associated_token_address(&fee_payer_pubkey, &setup.usdc_mint.pubkey()); + let sender_token_account = + get_associated_token_address(&setup.sender_keypair.pubkey(), &setup.usdc_mint.pubkey()); + + // Mint tokens for the fee payer + setup + .mint_tokens_to_account(&fee_payer_token_account, 100_000) + .await + .expect("Failed to mint tokens"); + + // Run the test transaction + let large_token_transfer = token_instruction::transfer( + &spl_token::id(), + &fee_payer_token_account, + &sender_token_account, + &fee_payer_pubkey, + &[&fee_payer_pubkey], + 100_000, + ) + .expect("Failed to create token transfer instruction"); + + let malicious_tx = ctx + .transaction_builder() + .with_fee_payer(fee_payer_pubkey) + .with_spl_payment( + &setup.usdc_mint.pubkey(), + &setup.sender_keypair.pubkey(), + &fee_payer_pubkey, + 1_000, // Smaller than the 100,000 USDC transfer + ) + .with_instruction(large_token_transfer) + .build() + .await + .expect("Failed to create transaction with fee payer as USDC source"); + + let result = ctx + .rpc_call::("signTransactionIfPaid", rpc_params![malicious_tx]) + .await; + + match result { + Err(error) => { + error.assert_contains_message("Insufficient token payment"); + } + Ok(_) => panic!("Expected error for fee payer as USDC transfer source"), + } +} diff --git a/tests/adversarial/main.rs b/tests/adversarial/main.rs new file mode 100644 index 00000000..3c384c39 --- /dev/null +++ b/tests/adversarial/main.rs @@ -0,0 +1,15 @@ +// Adversarial Basic Tests +// +// CONFIG: Uses tests/src/common/fixtures/kora-test.toml (permissive policies) +// TESTS: Security and robustness testing with normal configuration +// - Program validation attacks (disallowed programs) +// - Invalid token states (frozen) +// - Fee payer exploitation + +mod fee_payer_exploitation; +mod program_validation; +mod token_states; + +// Make common utilities available +#[path = "../src/common/mod.rs"] +mod common; diff --git a/tests/adversarial/program_validation.rs b/tests/adversarial/program_validation.rs new file mode 100644 index 00000000..e921ff0a --- /dev/null +++ b/tests/adversarial/program_validation.rs @@ -0,0 +1,64 @@ +use crate::common::{assertions::RpcErrorAssertions, *}; +use jsonrpsee::rpc_params; +use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; +use std::str::FromStr; + +#[tokio::test] +async fn test_disallowed_memo_program() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + + let disallowed_program_id = Pubkey::from_str("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr") + .expect("Failed to parse SPL Memo program ID"); + + let malicious_instruction = Instruction::new_with_bincode(disallowed_program_id, &(), vec![]); + + let malicious_tx = ctx + .transaction_builder() + .with_fee_payer(FeePayerTestHelper::get_fee_payer_pubkey()) + .with_instruction(malicious_instruction) + .build() + .await + .expect("Failed to create transaction with disallowed program"); + + let result = + ctx.rpc_call::("signTransaction", rpc_params![malicious_tx]).await; + + match result { + Err(error) => { + let expected_message = + format!("Program {disallowed_program_id} is not in the allowed list"); + error.assert_error_type_and_message("Invalid transaction", &expected_message); + } + Ok(_) => panic!("Expected error for transaction with disallowed program"), + } +} + +#[tokio::test] +async fn test_disallowed_program_v0_transaction() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + + let disallowed_program_id = Pubkey::from_str("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr") + .expect("Failed to parse BPF Loader Upgradeable program ID"); + + let malicious_instruction = Instruction::new_with_bincode(disallowed_program_id, &(), vec![]); + + let malicious_tx = ctx + .v0_transaction_builder() + .with_fee_payer(FeePayerTestHelper::get_fee_payer_pubkey()) + .with_instruction(malicious_instruction) + .build() + .await + .expect("Failed to create V0 transaction with disallowed program"); + + let result = + ctx.rpc_call::("signTransaction", rpc_params![malicious_tx]).await; + + match result { + Err(error) => { + let expected_message = + format!("Program {disallowed_program_id} is not in the allowed list"); + error.assert_error_type_and_message("Invalid transaction", &expected_message); + } + Ok(_) => panic!("Expected error for V0 transaction with disallowed program"), + } +} diff --git a/tests/adversarial/token_states.rs b/tests/adversarial/token_states.rs new file mode 100644 index 00000000..0e31c3d6 --- /dev/null +++ b/tests/adversarial/token_states.rs @@ -0,0 +1,98 @@ +use crate::common::{assertions::RpcErrorAssertions, *}; +use jsonrpsee::rpc_params; +use solana_sdk::{ + program_pack::Pack, signature::Keypair, signer::Signer, transaction::Transaction, +}; +use solana_system_interface::instruction::create_account; +use spl_associated_token_account::get_associated_token_address; +use spl_token::instruction as token_instruction; + +#[tokio::test] +async fn test_frozen_token_account_as_fee_payment() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + let setup = TestAccountSetup::new().await; + + let frozen_token_account_keypair = Keypair::new(); + + let rent = setup + .rpc_client + .get_minimum_balance_for_rent_exemption(spl_token::state::Account::LEN) + .await + .expect("Failed to get rent exemption"); + + let create_account_ix = create_account( + &setup.sender_keypair.pubkey(), + &frozen_token_account_keypair.pubkey(), + rent, + spl_token::state::Account::LEN as u64, + &spl_token::id(), + ); + + let create_frozen_token_account_ix = spl_token::instruction::initialize_account( + &spl_token::id(), + &frozen_token_account_keypair.pubkey(), + &setup.usdc_mint.pubkey(), + &setup.sender_keypair.pubkey(), + ) + .expect("Failed to create initialize account instruction"); + + let mint_tokens_ix = token_instruction::mint_to( + &spl_token::id(), + &setup.usdc_mint.pubkey(), + &frozen_token_account_keypair.pubkey(), + &setup.sender_keypair.pubkey(), + &[&setup.sender_keypair.pubkey()], + 100_000, + ) + .expect("Failed to create mint instruction"); + + let freeze_instruction = token_instruction::freeze_account( + &spl_token::id(), + &frozen_token_account_keypair.pubkey(), + &setup.usdc_mint.pubkey(), + &setup.sender_keypair.pubkey(), + &[&setup.sender_keypair.pubkey()], + ) + .expect("Failed to create freeze instruction"); + + let recent_blockhash = setup.rpc_client.get_latest_blockhash().await.unwrap(); + let setup_tx = Transaction::new_signed_with_payer( + &[create_account_ix, create_frozen_token_account_ix, mint_tokens_ix, freeze_instruction], + Some(&setup.sender_keypair.pubkey()), + &[&setup.sender_keypair, &frozen_token_account_keypair], + recent_blockhash, + ); + + setup + .rpc_client + .send_and_confirm_transaction(&setup_tx) + .await + .expect("Failed to setup and freeze token account"); + + let malicious_tx = ctx + .transaction_builder() + .with_fee_payer(FeePayerTestHelper::get_fee_payer_pubkey()) + .with_spl_payment_with_accounts( + &frozen_token_account_keypair.pubkey(), + &get_associated_token_address( + &FeePayerTestHelper::get_fee_payer_pubkey(), + &setup.usdc_mint.pubkey(), + ), + &setup.sender_keypair.pubkey(), + 50_000, + ) + .build() + .await + .expect("Failed to create transaction with frozen fee payer token account"); + + let result = + ctx.rpc_call::("signTransaction", rpc_params![malicious_tx]).await; + + match result { + // 0x11: Frozen token account + Err(error) => { + error.assert_contains_message("custom program error: 0x11"); + } + Ok(_) => panic!("Expected error for transaction with frozen fee payment account"), + } +} diff --git a/tests/fee_payer_policy/fee_payer_policy_violations.rs b/tests/fee_payer_policy/fee_payer_policy_violations.rs new file mode 100644 index 00000000..be9c8ec3 --- /dev/null +++ b/tests/fee_payer_policy/fee_payer_policy_violations.rs @@ -0,0 +1,301 @@ +use crate::common::{assertions::RpcErrorAssertions, *}; +use jsonrpsee::rpc_params; +use solana_sdk::{ + program_pack::Pack, signature::Keypair, signer::Signer, transaction::Transaction, +}; +use solana_system_interface::instruction::{create_account, transfer}; +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::instruction as token_2022_instruction; + +#[tokio::test] +async fn test_sol_transfer_policy_violation() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + + let fee_payer_pubkey = FeePayerTestHelper::get_fee_payer_pubkey(); + let recipient_pubkey = RecipientTestHelper::get_recipient_pubkey(); + + let sol_transfer_instruction = transfer(&fee_payer_pubkey, &recipient_pubkey, 1_000_000); + + let malicious_tx = ctx + .transaction_builder() + .with_fee_payer(fee_payer_pubkey) + .with_instruction(sol_transfer_instruction) + .build() + .await + .expect("Failed to create transaction with SOL transfer"); + + let result = + ctx.rpc_call::("signTransaction", rpc_params![malicious_tx]).await; + + match result { + Err(error) => { + error.assert_contains_message("Fee payer cannot be used for 'System Transfer'"); + } + Ok(_) => panic!("Expected error for SOL transfer policy violation"), + } +} + +#[tokio::test] +async fn test_spl_transfer_policy_violation() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + let setup = TestAccountSetup::new().await; + + let fee_payer_pubkey = FeePayerTestHelper::get_fee_payer_pubkey(); + let recipient_pubkey = RecipientTestHelper::get_recipient_pubkey(); + + let fee_payer_token_account = + get_associated_token_address(&fee_payer_pubkey, &setup.usdc_mint.pubkey()); + let recipient_token_account = + get_associated_token_address(&recipient_pubkey, &setup.usdc_mint.pubkey()); + + setup + .mint_tokens_to_account(&fee_payer_token_account, 100_000) + .await + .expect("Failed to mint tokens"); + + let spl_transfer_instruction = token_instruction::transfer( + &spl_token::id(), + &fee_payer_token_account, + &recipient_token_account, + &fee_payer_pubkey, + &[&fee_payer_pubkey], + 1_000, + ) + .expect("Failed to create SPL transfer instruction"); + + let malicious_tx = ctx + .transaction_builder() + .with_fee_payer(fee_payer_pubkey) + .with_instruction(spl_transfer_instruction) + .build() + .await + .expect("Failed to create transaction with SPL transfer"); + + let result = + ctx.rpc_call::("signTransaction", rpc_params![malicious_tx]).await; + + match result { + Err(error) => { + error.assert_contains_message("Fee payer cannot be used for 'SPL Token Transfer'"); + } + Ok(_) => panic!("Expected error for SPL transfer policy violation"), + } +} + +#[tokio::test] +async fn test_token2022_transfer_policy_violation() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + let setup = TestAccountSetup::new().await; + + let fee_payer_pubkey = FeePayerTestHelper::get_fee_payer_pubkey(); + let recipient_pubkey = RecipientTestHelper::get_recipient_pubkey(); + + let fee_payer_token_2022_account = get_associated_token_address_with_program_id( + &fee_payer_pubkey, + &setup.usdc_mint_2022.pubkey(), + &spl_token_2022::id(), + ); + let recipient_token_2022_account = get_associated_token_address_with_program_id( + &recipient_pubkey, + &setup.usdc_mint_2022.pubkey(), + &spl_token_2022::id(), + ); + + setup + .mint_tokens_2022_to_account(&fee_payer_token_2022_account, 100_000) + .await + .expect("Failed to mint tokens"); + + let token_2022_transfer_instruction = token_2022_instruction::transfer_checked( + &spl_token_2022::id(), + &fee_payer_token_2022_account, + &setup.usdc_mint_2022.pubkey(), + &recipient_token_2022_account, + &fee_payer_pubkey, + &[&fee_payer_pubkey], + 1_000, + USDCMintTestHelper::get_test_usdc_mint_decimals(), + ) + .expect("Failed to create Token2022 transfer instruction"); + + let malicious_tx = ctx + .transaction_builder() + .with_fee_payer(fee_payer_pubkey) + .with_instruction(token_2022_transfer_instruction) + .build() + .await + .expect("Failed to create transaction with Token2022 transfer"); + + let result = + ctx.rpc_call::("signTransaction", rpc_params![malicious_tx]).await; + + match result { + Err(error) => { + error + .assert_contains_message("Fee payer cannot be used for 'Token2022 Token Transfer'"); + } + Ok(_) => panic!("Expected error for Token2022 transfer policy violation"), + } +} + +#[tokio::test] +async fn test_burn_policy_violation() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + let setup = TestAccountSetup::new().await; + + let fee_payer_pubkey = FeePayerTestHelper::get_fee_payer_pubkey(); + let fee_payer_token_account = + get_associated_token_address(&fee_payer_pubkey, &setup.usdc_mint.pubkey()); + + setup + .mint_tokens_to_account(&fee_payer_token_account, 1_000_000) + .await + .expect("Failed to mint SPL"); + + let burn_instruction = token_instruction::burn( + &spl_token::id(), + &fee_payer_token_account, + &setup.usdc_mint.pubkey(), + &fee_payer_pubkey, + &[&fee_payer_pubkey], + 1_000, + ) + .expect("Failed to create burn instruction"); + + let malicious_tx = ctx + .transaction_builder() + .with_fee_payer(fee_payer_pubkey) + .with_instruction(burn_instruction) + .build() + .await + .expect("Failed to create transaction with burn"); + + let result = + ctx.rpc_call::("signTransaction", rpc_params![malicious_tx]).await; + + match result { + Err(error) => { + error.assert_contains_message("Fee payer cannot be used for 'SPL Token Burn'"); + } + Ok(_) => panic!("Expected error for burn policy violation"), + } +} + +#[tokio::test] +async fn test_close_account_policy_violation() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + let setup = TestAccountSetup::new().await; + + // Create a new token account to now affect other tests + let closable_token_account_keypair = Keypair::new(); + + let rent = setup + .rpc_client + .get_minimum_balance_for_rent_exemption(spl_token::state::Account::LEN) + .await + .expect("Failed to get rent exemption"); + + let create_account_ix = create_account( + &setup.fee_payer_keypair.pubkey(), + &closable_token_account_keypair.pubkey(), + rent, + spl_token::state::Account::LEN as u64, + &spl_token::id(), + ); + + let create_closable_token_account_ix = spl_token::instruction::initialize_account( + &spl_token::id(), + &closable_token_account_keypair.pubkey(), + &setup.usdc_mint.pubkey(), + &setup.fee_payer_keypair.pubkey(), + ) + .expect("Failed to create initialize account instruction"); + + let recent_blockhash = setup.rpc_client.get_latest_blockhash().await.unwrap(); + let setup_tx = Transaction::new_signed_with_payer( + &[create_account_ix, create_closable_token_account_ix], + Some(&setup.fee_payer_keypair.pubkey()), + &[&setup.fee_payer_keypair, &closable_token_account_keypair], + recent_blockhash, + ); + + setup + .rpc_client + .send_and_confirm_transaction(&setup_tx) + .await + .expect("Failed to setup and freeze token account"); + + let close_account_instruction = token_instruction::close_account( + &spl_token::id(), + &closable_token_account_keypair.pubkey(), + &setup.recipient_pubkey, + &setup.fee_payer_keypair.pubkey(), + &[&setup.fee_payer_keypair.pubkey()], + ) + .expect("Failed to create close account instruction"); + + let malicious_tx = ctx + .transaction_builder() + .with_fee_payer(setup.fee_payer_keypair.pubkey()) + .with_instruction(close_account_instruction) + .build() + .await + .expect("Failed to create transaction with close account"); + + let result = + ctx.rpc_call::("signTransaction", rpc_params![malicious_tx]).await; + + match result { + Err(error) => { + error.assert_contains_message("Fee payer cannot be used for 'SPL Token Close Account'"); + } + Ok(_) => panic!("Expected error for close account policy violation"), + } +} + +#[tokio::test] +async fn test_approve_policy_violation() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + let setup = TestAccountSetup::new().await; + + let fee_payer_pubkey = FeePayerTestHelper::get_fee_payer_pubkey(); + let recipient_pubkey = RecipientTestHelper::get_recipient_pubkey(); + let fee_payer_token_account = + get_associated_token_address(&fee_payer_pubkey, &setup.usdc_mint.pubkey()); + + setup + .mint_tokens_to_account(&fee_payer_token_account, 1_000_000) + .await + .expect("Failed to mint tokens"); + + let approve_instruction = token_instruction::approve( + &spl_token::id(), + &fee_payer_token_account, + &recipient_pubkey, + &fee_payer_pubkey, + &[&fee_payer_pubkey], + 1_000, + ) + .expect("Failed to create approve instruction"); + + let malicious_tx = ctx + .transaction_builder() + .with_fee_payer(fee_payer_pubkey) + .with_instruction(approve_instruction) + .build() + .await + .expect("Failed to create transaction with approve"); + + let result = + ctx.rpc_call::("signTransaction", rpc_params![malicious_tx]).await; + + match result { + Err(error) => { + error.assert_contains_message("Fee payer cannot be used for 'SPL Token Approve'"); + } + Ok(_) => panic!("Expected error for approve policy violation"), + } +} diff --git a/tests/fee_payer_policy/main.rs b/tests/fee_payer_policy/main.rs new file mode 100644 index 00000000..200b7686 --- /dev/null +++ b/tests/fee_payer_policy/main.rs @@ -0,0 +1,17 @@ +// Adversarial Restrictive Tests +// +// CONFIG: Uses tests/src/common/fixtures/fee-payer-policy-test.toml (restrictive policies) +// TESTS: Fee payer policy violation testing with all policies disabled +// - SOL transfer policy violations (allow_sol_transfers = false) +// - SPL transfer policy violations (allow_spl_transfers = false) +// - Token2022 transfer policy violations (allow_token2022_transfers = false) +// - Assignment policy violations (allow_assign = false) +// - Burn policy violations (allow_burn = false) +// - Close account policy violations (allow_close_account = false) +// - Approve policy violations (allow_approve = false) + +mod fee_payer_policy_violations; + +// Make common utilities available +#[path = "../src/common/mod.rs"] +mod common; diff --git a/tests/src/common/assertions.rs b/tests/src/common/assertions.rs index 32af7069..62356904 100644 --- a/tests/src/common/assertions.rs +++ b/tests/src/common/assertions.rs @@ -1,3 +1,5 @@ +use anyhow::Error as AnyhowError; +use jsonrpsee::core::Error as RpcError; use serde_json::Value; use solana_sdk::signature::Signature; use std::str::FromStr; @@ -108,3 +110,75 @@ pub struct JsonRpcErrorCodes; impl JsonRpcErrorCodes { pub const METHOD_NOT_FOUND: i32 = -32601; } + +/// Trait for RPC error assertions +pub trait RpcErrorAssertions { + /// Assert the RPC error contains a specific message + fn assert_contains_message(&self, expected_message: &str); + + /// Assert the RPC error is of a specific type (e.g., "InvalidTransaction", "ValidationError") + fn assert_error_type(&self, expected_type: &str); + + /// Assert both error type and message + fn assert_error_type_and_message(&self, expected_type: &str, expected_message: &str); +} + +impl RpcErrorAssertions for RpcError { + fn assert_contains_message(&self, expected_message: &str) { + let error_str = self.to_string(); + assert!( + error_str.contains(expected_message), + "Expected error to contain '{expected_message}', got: {error_str}", + ); + } + + fn assert_error_type(&self, expected_type: &str) { + let error_str = self.to_string(); + assert!( + error_str.contains(expected_type), + "Expected error type '{expected_type}', got: {error_str}", + ); + } + + fn assert_error_type_and_message(&self, expected_type: &str, expected_message: &str) { + let error_str = self.to_string(); + assert!( + error_str.contains(expected_type), + "Expected error type '{expected_type}', got: {error_str}", + ); + assert!( + error_str.contains(expected_message), + "Expected error to contain '{expected_message}', got: {error_str}", + ); + } +} + +impl RpcErrorAssertions for AnyhowError { + fn assert_contains_message(&self, expected_message: &str) { + let error_str = self.to_string(); + assert!( + error_str.contains(expected_message), + "Expected error to contain '{expected_message}', got: {error_str}", + ); + } + + fn assert_error_type(&self, expected_type: &str) { + let error_str = self.to_string(); + assert!( + error_str.contains(expected_type), + "Expected error type '{expected_type}', got: {error_str}", + ); + } + + fn assert_error_type_and_message(&self, expected_type: &str, expected_message: &str) { + let error_str = self.to_string(); + assert!( + error_str.contains(expected_type), + "Expected error type '{expected_type}', got: {error_str}", + ); + assert!( + error_str.contains(expected_message), + "Expected error to contain '{expected_message}', got: {error_str}", + ); + } +} diff --git a/tests/src/common/auth_helpers.rs b/tests/src/common/auth_helpers.rs index fe2a201b..681782a3 100644 --- a/tests/src/common/auth_helpers.rs +++ b/tests/src/common/auth_helpers.rs @@ -6,10 +6,7 @@ use once_cell::sync::Lazy; use serde_json::{json, Value}; use sha2::Sha256; -use crate::common::{ - client::TestClient, - constants::{TEST_API_KEY, TEST_HMAC_SECRET}, -}; +use crate::common::{client::TestClient, constants::TEST_HMAC_SECRET}; pub static JSON_TEST_BODY: Lazy = Lazy::new(|| { json!({ diff --git a/tests/src/common/fixtures/fee-payer-policy-test.toml b/tests/src/common/fixtures/fee-payer-policy-test.toml new file mode 100644 index 00000000..3129f8ce --- /dev/null +++ b/tests/src/common/fixtures/fee-payer-policy-test.toml @@ -0,0 +1,65 @@ +# Adversarial testing configuration file with restrictive fee payer policies +[kora] +rate_limit = 100 + +[kora.auth] + +# Cache configuration - disabled for testing +[kora.cache] +enabled = false +default_ttl = 300 +account_ttl = 60 + +[kora.enabled_methods] +liveness = true +estimate_transaction_fee = true +get_supported_tokens = true +sign_transaction = true +sign_and_send_transaction = true +transfer_transaction = true +get_blockhash = true +get_config = true +get_payer_signer = true +sign_transaction_if_paid = true + +[validation] +max_allowed_lamports = 1000000 +max_signatures = 10 +price_source = "Mock" +allowed_programs = [ + "11111111111111111111111111111111", # System Program + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", # Token Program + "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb", # Token-2022 Program + "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", # Associated Token Program + "AddressLookupTab1e1111111111111111111111111", # Address Lookup Table Program +] +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 +] + +# RESTRICTIVE fee payer policy - ALL SET TO FALSE +[validation.fee_payer_policy] +allow_sol_transfers = false +allow_spl_transfers = false +allow_token2022_transfers = false +allow_assign = false +allow_burn = false +allow_close_account = false +allow_approve = false + +[kora.usage_limit] +enabled = false +cache_url = "redis://redis:6379" +max_transactions = 2 +fallback_if_unavailable = false diff --git a/tests/src/common/fixtures/multi-signers.toml b/tests/src/common/fixtures/signers-multi.toml similarity index 100% rename from tests/src/common/fixtures/multi-signers.toml rename to tests/src/common/fixtures/signers-multi.toml diff --git a/tests/src/common/setup.rs b/tests/src/common/setup.rs index eacdbe6d..6cecca0d 100644 --- a/tests/src/common/setup.rs +++ b/tests/src/common/setup.rs @@ -391,7 +391,7 @@ impl TestAccountSetup { )) } - async fn mint_tokens_to_account(&self, token_account: &Pubkey, amount: u64) -> Result<()> { + pub async fn mint_tokens_to_account(&self, token_account: &Pubkey, amount: u64) -> Result<()> { let instruction = token_instruction::mint_to( &spl_token::id(), &self.usdc_mint.pubkey(), @@ -413,7 +413,11 @@ impl TestAccountSetup { Ok(()) } - async fn mint_tokens_2022_to_account(&self, token_account: &Pubkey, amount: u64) -> Result<()> { + pub 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(), diff --git a/tests/src/common/transaction.rs b/tests/src/common/transaction.rs index 017f555e..35ec10d1 100644 --- a/tests/src/common/transaction.rs +++ b/tests/src/common/transaction.rs @@ -149,6 +149,28 @@ impl TransactionBuilder { self } + /// Add an SPL payment instruction with specified token accounts + pub fn with_spl_payment_with_accounts( + mut self, + from_token_account: &Pubkey, + to_token_account: &Pubkey, + from_authority: &Pubkey, + amount: u64, + ) -> Self { + let instruction = spl_token::instruction::transfer( + &spl_token::ID, + from_token_account, + to_token_account, + from_authority, + &[], + amount, + ) + .expect("Failed to create SPL payment instruction"); + + self.instructions.push(instruction); + self + } + /// Add an SPL token transfer_checked instruction (includes mint address for lookup table testing) pub fn with_spl_transfer_checked( mut self, diff --git a/tests/src/test_runner/output.rs b/tests/src/test_runner/output.rs index 5caa59f1..9b251d0d 100644 --- a/tests/src/test_runner/output.rs +++ b/tests/src/test_runner/output.rs @@ -21,6 +21,7 @@ pub enum TestPhaseColor { Auth, Payment, MultiSigner, + FeePayerPolicy, TypeScriptBasic, TypeScriptAuth, TypeScriptTurnkey, @@ -34,6 +35,7 @@ impl TestPhaseColor { "Auth Tests" => Self::Auth, "Payment Address Tests" => Self::Payment, "Multi-Signer Tests" => Self::MultiSigner, + "Fee Payer Policy Tests" => Self::FeePayerPolicy, name if name.starts_with("TypeScript") => Self::from_typescript_phase(name), name if name.starts_with("typescript_") => Self::from_typescript_phase(name), // Fallback patterns @@ -57,10 +59,11 @@ impl TestPhaseColor { pub fn ansi_code(&self) -> &'static str { match self { - Self::Regular => "\x1b[32m", // Green - Self::Auth => "\x1b[34m", // Blue - Self::Payment => "\x1b[33m", // Yellow - Self::MultiSigner => "\x1b[35m", // Magenta + Self::Regular => "\x1b[32m", // Green + Self::Auth => "\x1b[34m", // Blue + Self::Payment => "\x1b[33m", // Yellow + Self::MultiSigner => "\x1b[35m", // Magenta + Self::FeePayerPolicy => "\x1b[39m", Self::TypeScriptBasic => "\x1b[36m", // Cyan Self::TypeScriptAuth => "\x1b[31m", // Red Self::TypeScriptTurnkey => "\x1b[37m", // White diff --git a/tests/src/test_runner/test_cases.toml b/tests/src/test_runner/test_cases.toml index 4fe73bfb..77598490 100644 --- a/tests/src/test_runner/test_cases.toml +++ b/tests/src/test_runner/test_cases.toml @@ -3,7 +3,7 @@ name = "Regular Integration Tests" config = "tests/src/common/fixtures/kora-test.toml" signers = "tests/src/common/fixtures/signers.toml" port = "8080" -tests = ["rpc", "tokens", "external"] +tests = ["rpc", "tokens", "external", "adversarial"] [test.auth] name = "Auth Tests" @@ -20,10 +20,17 @@ port = "8082" tests = ["payment_address"] initialize_payments_atas = true +[test.fee_payer_policy] +name = "Fee Payer Policy Tests" +config = "tests/src/common/fixtures/fee-payer-policy-test.toml" +signers = "tests/src/common/fixtures/signers.toml" +port = "8086" +tests = ["fee_payer_policy"] + [test.multi_signer] name = "Multi-Signer Tests" config = "tests/src/common/fixtures/kora-test.toml" -signers = "tests/src/common/fixtures/multi-signers.toml" +signers = "tests/src/common/fixtures/signers-multi.toml" port = "8083" tests = ["multi_signer"] From 01f69c23f59bb72115e7e267c660cbd00d31a306 Mon Sep 17 00:00:00 2001 From: jo <17280917+dev-jodee@users.noreply.github.com> Date: Thu, 25 Sep 2025 14:37:14 -0400 Subject: [PATCH 06/29] feat: Some fixes post discussions with Audit & others (#225) * feat: Some fixes post discussions with Audit & others (PRO-342, PRO-341, PRO-340, PRO-339) - Price source should not optional anywhere - Double fee counting with ATA calculation and then fee payer outflow calculation, removed ATA separated calc completely - Checked math operations everywhere to prevent overflow errors - .unwrap() removed with better graceful error handling -remove signature from response in signAndSendTransaction and signTransaction * Pr fixes * CI fixes * Fix testing with rpc not optional * Updated ts types and docs --- .github/workflows/build-rust.yml | 17 +- .github/workflows/rust.yml | 2 +- .github/workflows/typescript-integration.yml | 2 +- crates/cli/src/main.rs | 12 +- crates/lib/src/cache.rs | 2 +- crates/lib/src/error.rs | 3 + crates/lib/src/fee/fee.rs | 394 ++---------------- crates/lib/src/fee/price.rs | 99 ++--- crates/lib/src/rpc_server/auth.rs | 11 +- .../method/estimate_transaction_fee.rs | 2 +- .../method/sign_and_send_transaction.rs | 4 +- .../src/rpc_server/method/sign_transaction.rs | 2 - .../rpc_server/openapi/spec/combined_api.json | 303 ++++++++++++-- crates/lib/src/signer/turnkey/config.rs | 13 +- crates/lib/src/signer/turnkey/signer.rs | 20 +- crates/lib/src/signer/turnkey/types.rs | 11 +- crates/lib/src/token/interface.rs | 11 +- crates/lib/src/token/spl_token.rs | 37 +- crates/lib/src/token/spl_token_2022.rs | 150 ++----- .../src/transaction/versioned_transaction.rs | 2 +- sdks/ts/src/types/index.ts | 4 - sdks/ts/test/integration.test.ts | 3 - sdks/ts/test/unit.test.ts | 2 - tests/rpc/transaction_signing.rs | 14 +- tests/tokens/token_2022_extensions_test.rs | 1 - tests/tokens/token_2022_test.rs | 9 +- 26 files changed, 464 insertions(+), 666 deletions(-) diff --git a/.github/workflows/build-rust.yml b/.github/workflows/build-rust.yml index f8d0207a..befd6873 100644 --- a/.github/workflows/build-rust.yml +++ b/.github/workflows/build-rust.yml @@ -8,15 +8,21 @@ on: required: true type: string artifact-name: - description: "Name for the uploaded artifact" + description: "Base name for the uploaded artifact" required: true type: string + outputs: + artifact-name: + description: "The actual artifact name with hash suffix" + value: ${{ jobs.build.outputs.artifact-name }} jobs: build: name: Build Rust Artifacts runs-on: ubuntu-latest timeout-minutes: 15 + outputs: + artifact-name: ${{ inputs.artifact-name }}-${{ steps.source-hash.outputs.hash }} steps: - uses: actions/checkout@v4 @@ -26,13 +32,20 @@ jobs: with: shared-key: ${{ inputs.cache-key }} + - name: Calculate source files hash + id: source-hash + run: | + SOURCE_HASH=$(find crates tests Cargo.toml Cargo.lock Makefile makefiles -type f \( -name "*.rs" -o -name "*.toml" -o -name "Makefile" \) -exec sha256sum {} \; | sort | sha256sum | cut -d' ' -f1 | head -c 8) + echo "hash=$SOURCE_HASH" >> $GITHUB_OUTPUT + echo "Source files hash: $SOURCE_HASH" + - name: Build workspace run: make build - name: Upload build artifacts uses: actions/upload-artifact@v4 with: - name: ${{ inputs.artifact-name }} + name: ${{ inputs.artifact-name }}-${{ steps.source-hash.outputs.hash }} path: | target/debug/kora target/debug/test_runner diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index ed799864..db3c34c5 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -58,7 +58,7 @@ jobs: - name: Download build artifacts uses: actions/download-artifact@v4 with: - name: rust-binaries + name: ${{ needs.build.outputs.artifact-name }} path: target/debug/ - name: Make binaries executable diff --git a/.github/workflows/typescript-integration.yml b/.github/workflows/typescript-integration.yml index 11515966..d6563489 100644 --- a/.github/workflows/typescript-integration.yml +++ b/.github/workflows/typescript-integration.yml @@ -66,7 +66,7 @@ jobs: - name: Download build artifacts uses: actions/download-artifact@v4 with: - name: rust-binaries-ts + name: ${{ needs.build.outputs.artifact-name }} path: target/debug/ - name: Make binaries executable diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 57694d70..863cbce3 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -190,7 +190,9 @@ async fn main() -> Result<(), KoraError> { let ServerHandles { rpc_handle, metrics_handle, balance_tracker_handle } = run_rpc_server(kora_rpc, rpc_args.port).await?; - tokio::signal::ctrl_c().await.unwrap(); + if let Err(e) = tokio::signal::ctrl_c().await { + panic!("Error waiting for Ctrl+C signal: {e:?}"); + } println!("Shutting down server..."); // Stop the balance tracker task @@ -200,11 +202,15 @@ async fn main() -> Result<(), KoraError> { } // Stop the RPC server - rpc_handle.stop().unwrap(); + if let Err(e) = rpc_handle.stop() { + panic!("Error stopping RPC server: {e:?}"); + } // Stop the metrics server if running if let Some(handle) = metrics_handle { - handle.stop().unwrap(); + if let Err(e) = handle.stop() { + panic!("Error stopping metrics server: {e:?}"); + } } } RpcCommands::InitializeAtas { diff --git a/crates/lib/src/cache.rs b/crates/lib/src/cache.rs index 619ad1bf..8a0ee459 100644 --- a/crates/lib/src/cache.rs +++ b/crates/lib/src/cache.rs @@ -34,7 +34,7 @@ impl CacheUtil { let config = get_config()?; let pool = if CacheUtil::is_cache_enabled() { - let redis_url = config.kora.cache.url.as_ref().unwrap(); + let redis_url = config.kora.cache.url.as_ref().ok_or(KoraError::ConfigError)?; let cfg = deadpool_redis::Config::from_url(redis_url); let pool = cfg.create_pool(Some(Runtime::Tokio1)).map_err(|e| { diff --git a/crates/lib/src/error.rs b/crates/lib/src/error.rs index 76499606..e5ab698f 100644 --- a/crates/lib/src/error.rs +++ b/crates/lib/src/error.rs @@ -61,6 +61,9 @@ pub enum KoraError { #[error("Usage limit exceeded: {0}")] UsageLimitExceeded(String), + + #[error("Invalid configuration for Kora")] + ConfigError, } impl From for KoraError { diff --git a/crates/lib/src/fee/fee.rs b/crates/lib/src/fee/fee.rs index 3f2f4706..3a7d86b5 100644 --- a/crates/lib/src/fee/fee.rs +++ b/crates/lib/src/fee/fee.rs @@ -24,16 +24,12 @@ use {crate::cache::CacheUtil, crate::state::get_config}; use crate::tests::{cache_mock::MockCacheUtil as CacheUtil, config_mock::mock_state::get_config}; use solana_client::nonblocking::rpc_client::RpcClient; use solana_message::VersionedMessage; -use solana_program::program_pack::Pack; -use solana_sdk::{pubkey::Pubkey, rent::Rent}; -use spl_associated_token_account::get_associated_token_address; -use spl_token::state::Account as SplTokenAccountState; +use solana_sdk::pubkey::Pubkey; #[derive(Debug, Clone)] pub struct TotalFeeCalculation { pub total_fee_lamports: u64, pub base_fee: u64, - pub account_creation_fee: u64, pub kora_signature_fee: u64, pub fee_payer_outflow: u64, pub payment_instruction_fee: u64, @@ -63,75 +59,6 @@ impl FeeConfigUtil { }) } - async fn get_associated_token_account_creation_fees( - rpc_client: &RpcClient, - transaction: &VersionedTransactionResolved, - ) -> Result { - let mut total_lamports = 0u64; - - // Check each instruction in the transaction for ATA creation - for instruction in &transaction.all_instructions { - // Skip if not an ATA program instruction - if instruction.program_id != spl_associated_token_account::id() { - continue; - } - - let ata = instruction.accounts[1].pubkey; - let owner = instruction.accounts[2].pubkey; - let mint = instruction.accounts[3].pubkey; - - let expected_ata = get_associated_token_address(&owner, &mint); - - // Force refresh in case extensions are modified - // Check if ATA doesn't exist - this is expected for new token accounts - if ata == expected_ata { - match CacheUtil::get_account(rpc_client, &ata, true).await { - Ok(_) => { - // ATA already exists, no creation fee needed - continue; - } - Err(KoraError::AccountNotFound(_)) => { - // ATA doesn't exist, calculate creation fee - // Continue to the account size calculation below - } - Err(e) => { - // Other errors should still be propagated - return Err(e); - } - }; - - // Determine the appropriate token program and get account size - let account_size = match CacheUtil::get_account(rpc_client, &mint, true).await { - Ok(mint_account) => { - match TokenType::get_token_program_from_owner(&mint_account.owner) { - Ok(token_program) => token_program - .get_ata_account_size(&mint, &mint_account) - .await - .unwrap_or(SplTokenAccountState::LEN), - Err(_) => { - return Err(KoraError::InternalServerError( - "Unknown token program".to_string(), - )); - } - } - } - Err(_) => { - return Err(KoraError::InternalServerError( - "Failed to fetch mint account".to_string(), - )); - } - }; - - // Get rent cost in lamports for ATA creation with the determined size - let rent = Rent::default(); - let exempt_min = rent.minimum_balance(account_size); - total_lamports += exempt_min; - } - } - - Ok(total_lamports) - } - /// Helper function to check if a token transfer instruction is a payment to Kora /// Returns Some(token_account_data) if it's a payment, None otherwise async fn get_payment_instruction_info( @@ -250,7 +177,7 @@ impl FeeConfigUtil { let current_epoch = rpc_client.get_epoch_info().await?.epoch; if let Some(fee_amount) = - token2022_mint.calculate_transfer_fee(*amount, current_epoch) + token2022_mint.calculate_transfer_fee(*amount, current_epoch)? { return Ok(fee_amount); } @@ -273,13 +200,8 @@ impl FeeConfigUtil { let base_fee = TransactionFeeUtil::get_estimate_fee_resolved(rpc_client, transaction).await?; - // Get account creation fees (for ATA creation) - let account_creation_fee = - FeeConfigUtil::get_associated_token_account_creation_fees(rpc_client, transaction) - .await - .map_err(|e| KoraError::RpcError(e.to_string()))?; - // Priority fees are now included in the calculate done by the RPC getFeeForMessage + // ATA and Token account creation fees are captured in the calculate fee payer outflow (System Transfer) // If the Kora signer is not inclded in the signers, we add another base fee, since each transaction will be 5000 lamports let mut kora_signature_fee = 0u64; @@ -303,16 +225,19 @@ impl FeeConfigUtil { FeeConfigUtil::calculate_transfer_fees(rpc_client, transaction, fee_payer).await?; let total_fee_lamports = base_fee - + account_creation_fee - + kora_signature_fee - + fee_payer_outflow - + fee_for_payment_instruction - + transfer_fee_config_amount; + .checked_add(kora_signature_fee) + .and_then(|sum| sum.checked_add(fee_payer_outflow)) + .and_then(|sum| sum.checked_add(fee_for_payment_instruction)) + .and_then(|sum| sum.checked_add(transfer_fee_config_amount)) + .ok_or_else(|| { + log::error!("Fee calculation overflow: base_fee={}, kora_signature_fee={}, fee_payer_outflow={}, payment_instruction_fee={}, transfer_fee_amount={}", + base_fee, kora_signature_fee, fee_payer_outflow, fee_for_payment_instruction, transfer_fee_config_amount); + KoraError::ValidationError("Fee calculation overflow".to_string()) + })?; Ok(TotalFeeCalculation { total_fee_lamports, base_fee, - account_creation_fee, kora_signature_fee, fee_payer_outflow, payment_instruction_fee: fee_for_payment_instruction, @@ -326,7 +251,7 @@ impl FeeConfigUtil { transaction: &mut VersionedTransactionResolved, fee_payer: &Pubkey, is_payment_required: bool, - price_source: Option, + price_source: PriceSource, ) -> Result { let config = get_config()?; @@ -335,7 +260,6 @@ impl FeeConfigUtil { return Ok(TotalFeeCalculation { total_fee_lamports: 0, base_fee: 0, - account_creation_fee: 0, kora_signature_fee: 0, fee_payer_outflow: 0, payment_instruction_fee: 0, @@ -349,20 +273,14 @@ impl FeeConfigUtil { .await?; // Apply Kora's price model - if let Some(price_source) = price_source { - let adjusted_fee = config - .validation - .price - .get_required_lamports( - Some(rpc_client), - Some(price_source), - fee_calculation.total_fee_lamports, - ) - .await?; + let adjusted_fee = config + .validation + .price + .get_required_lamports(rpc_client, price_source, fee_calculation.total_fee_lamports) + .await?; - // Update the total with the price model applied - fee_calculation.total_fee_lamports = adjusted_fee; - } + // Update the total with the price model applied + fee_calculation.total_fee_lamports = adjusted_fee; Ok(fee_calculation) } @@ -517,11 +435,11 @@ mod tests { fee::fee::{FeeConfigUtil, TransactionFeeUtil}, tests::{ common::{ - create_mock_rpc_client_with_account, create_mock_spl_mint_account, - create_mock_token_account, setup_or_get_test_config, setup_or_get_test_signer, + create_mock_rpc_client_with_account, create_mock_token_account, + setup_or_get_test_config, setup_or_get_test_signer, }, config_mock::ConfigMockBuilder, - rpc_mock::{create_mock_rpc_client_account_not_found, RpcMockBuilder}, + rpc_mock::RpcMockBuilder, }, token::{interface::TokenInterface, TokenProgram}, transaction::TransactionUtil, @@ -541,14 +459,7 @@ mod tests { }, program::ID as SYSTEM_PROGRAM_ID, }; - use spl_associated_token_account::{ - get_associated_token_address, instruction::create_associated_token_account, - }; - use spl_token::state::Account as SplTokenAccountState; - use std::{ - collections::VecDeque, - sync::{Arc, Mutex}, - }; + use spl_associated_token_account::get_associated_token_address; #[test] fn test_is_fee_payer_in_signers_legacy_fee_payer_is_signer() { @@ -995,182 +906,6 @@ mod tests { ); } - #[tokio::test] - async fn test_get_associated_token_account_creation_fees_no_ata_creation() { - let mocked_rpc_client = create_mock_rpc_client_with_account(&Account::default()); - - // Create a transaction with no ATA creation instructions - let sender = Keypair::new(); - let recipient = Pubkey::new_unique(); - let transfer_instruction = transfer(&sender.pubkey(), &recipient, 100_000); - - let message = - VersionedMessage::Legacy(Message::new(&[transfer_instruction], Some(&sender.pubkey()))); - let resolved_transaction = - TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - - let result = FeeConfigUtil::get_associated_token_account_creation_fees( - &mocked_rpc_client, - &resolved_transaction, - ) - .await - .unwrap(); - - assert_eq!(result, 0, "Should return 0 when no ATA creation instructions"); - } - - #[tokio::test] - async fn test_get_associated_token_account_creation_fees_ata_exists() { - let _m = ConfigMockBuilder::new().build_and_setup(); - let cache_ctx = CacheUtil::get_account_context(); - cache_ctx.checkpoint(); - - let owner = Keypair::new(); - let mint = Pubkey::new_unique(); - - // Mock existing ATA account - let existing_ata_account = create_mock_token_account(&owner.pubkey(), &mint); - - let mocked_rpc_client = create_mock_rpc_client_with_account(&existing_ata_account); - - // Set up cache mock to return existing ATA account - cache_ctx.expect().times(1).returning(move |_, _, _| Ok(existing_ata_account.clone())); - - // Create ATA creation instruction - let ata_instruction = create_associated_token_account( - &owner.pubkey(), - &owner.pubkey(), - &mint, - &spl_token::id(), - ); - - let message = - VersionedMessage::Legacy(Message::new(&[ata_instruction], Some(&owner.pubkey()))); - let resolved_transaction = - TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - - let result = FeeConfigUtil::get_associated_token_account_creation_fees( - &mocked_rpc_client, - &resolved_transaction, - ) - .await - .unwrap(); - - assert_eq!(result, 0, "Should return 0 when ATA already exists"); - } - - #[tokio::test] - async fn test_get_associated_token_account_creation_fees_with_proper_cache_mock() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - // Test success case: ATA doesn't exist but mint account exists, should calculate rent - let owner = Keypair::new(); - let mint = Pubkey::new_unique(); - - let cache_ctx = CacheUtil::get_account_context(); - cache_ctx.checkpoint(); // Clear any previous expectations - - let mocked_rpc_client = create_mock_rpc_client_with_account(&Account::default()); - - // Call 1: ATA doesn't exist (Err) - // Call 2: Mint exists (Ok) - let responses = Arc::new(Mutex::new(VecDeque::from([ - Err(KoraError::AccountNotFound( - get_associated_token_address(&owner.pubkey(), &mint).to_string(), - )), - Ok(create_mock_token_account(&owner.pubkey(), &mint)), - ]))); - - let responses_clone = responses.clone(); - cache_ctx - .expect() - .times(2) - .returning(move |_, _, _| responses_clone.lock().unwrap().pop_front().unwrap()); - - // Create ATA creation instruction - let ata_instruction = create_associated_token_account( - &owner.pubkey(), - &owner.pubkey(), - &mint, - &spl_token::id(), - ); - - let message = - VersionedMessage::Legacy(Message::new(&[ata_instruction], Some(&owner.pubkey()))); - let resolved_transaction = - TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - - let result = FeeConfigUtil::get_associated_token_account_creation_fees( - &mocked_rpc_client, - &resolved_transaction, - ) - .await - .unwrap(); - - // Should calculate rent cost for new ATA (using standard token account size) - let rent = solana_sdk::rent::Rent::default(); - let expected_rent = rent.minimum_balance(SplTokenAccountState::LEN); - assert_eq!(result, expected_rent, "Should return rent cost for new ATA"); - } - - #[tokio::test] - async fn test_get_associated_token_account_creation_fees_mint_not_found() { - let _m = ConfigMockBuilder::new().build_and_setup(); - let cache_ctx = CacheUtil::get_account_context(); - cache_ctx.checkpoint(); - - // Test error case: ATA doesn't exist AND mint account is also missing - let owner = Keypair::new(); - let mint = Pubkey::new_unique(); - - // Use account not found mock for both ATA and mint calls - let mocked_rpc_client = create_mock_rpc_client_account_not_found(); - - // Set up sequential cache responses: ATA not found, mint not found - let responses = Arc::new(Mutex::new(VecDeque::from([ - Err(KoraError::AccountNotFound( - get_associated_token_address(&owner.pubkey(), &mint).to_string(), - )), - Err(KoraError::AccountNotFound(mint.to_string())), - ]))); - - let responses_clone = responses.clone(); - cache_ctx - .expect() - .times(2) - .returning(move |_, _, _| responses_clone.lock().unwrap().pop_front().unwrap()); - - // Create ATA creation instruction - let ata_instruction = create_associated_token_account( - &owner.pubkey(), - &owner.pubkey(), - &mint, - &spl_token::id(), - ); - - let message = - VersionedMessage::Legacy(Message::new(&[ata_instruction], Some(&owner.pubkey()))); - let resolved_transaction = - TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - - let result = FeeConfigUtil::get_associated_token_account_creation_fees( - &mocked_rpc_client, - &resolved_transaction, - ) - .await; - - assert!(result.is_err(), "Should return error when mint account not found"); - match result { - Err(KoraError::InternalServerError(msg)) => { - assert_eq!( - msg, "Failed to fetch mint account", - "Should get mint account fetch error" - ); - } - _ => panic!("Expected InternalServerError about mint account not found"), - } - } - #[tokio::test] async fn test_estimate_transaction_fee_basic() { let _m = ConfigMockBuilder::new().build_and_setup(); @@ -1316,85 +1051,4 @@ mod tests { assert_eq!(result, 12500, "Should return mocked base fee for V0 message"); } - - #[tokio::test] - async fn test_can_estimate_transaction_fees_on_transfers_with_uninitialized_atas() { - let _m = ConfigMockBuilder::new().build_and_setup(); - let _signer = setup_or_get_test_signer(); - let cache_ctx = CacheUtil::get_account_context(); - cache_ctx.checkpoint(); - - let fee_payer = Keypair::new(); - let sender = Keypair::new(); - let recipient = Keypair::new(); // This will be a newly generated wallet - let mint = Pubkey::new_unique(); - - // Mock RPC client that returns base fee and handles epoch info - let mocked_rpc_client = - RpcMockBuilder::new().with_fee_estimate(5000).with_epoch_info_mock().build(); - - // Create ATA creation instruction for recipient (this is what triggers the fee calculation) - let recipient_ata = get_associated_token_address(&recipient.pubkey(), &mint); - let sender_ata = get_associated_token_address(&sender.pubkey(), &mint); - - let create_ata_instruction = create_associated_token_account( - &fee_payer.pubkey(), - &recipient.pubkey(), - &mint, - &spl_token::id(), - ); - - let transfer_instruction = TokenProgram::new() - .create_transfer_instruction(&sender_ata, &recipient_ata, &sender.pubkey(), 1000) - .unwrap(); - - let message = VersionedMessage::Legacy(Message::new( - &[create_ata_instruction, transfer_instruction], - Some(&fee_payer.pubkey()), - )); - let mut resolved_transaction = - TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - - // Setup cache responses - correct order based on estimate_transaction_fee execution: - // 1. ATA creation: Recipient ATA (doesn't exist - AccountNotFound) - this is expected - // 2. ATA creation: Mint account exists (Ok) - needed to determine token program - // 3. calculate_transfer_fees: Recipient ATA (doesn't exist - AccountNotFound) โ†’ skip - let responses = Arc::new(Mutex::new(VecDeque::from([ - Err(KoraError::AccountNotFound(recipient_ata.to_string())), // ATA creation check - Ok(create_mock_spl_mint_account(6)), // mint exists - Err(KoraError::AccountNotFound(recipient_ata.to_string())), // calculate_transfer_fees -> skip - ]))); - - let responses_clone = responses.clone(); - cache_ctx - .expect() - .times(3) - .returning(move |_, _, _| responses_clone.lock().unwrap().pop_front().unwrap()); - - // This should succeed without throwing InternalServerError - let result = FeeConfigUtil::estimate_transaction_fee( - &mocked_rpc_client, - &mut resolved_transaction, - &fee_payer.pubkey(), - false, - ) - .await; - - assert!( - result.is_ok(), - "Fee estimation should succeed for transaction with uninitialized ATAs: {:?}", - result.err() - ); - - let fee = result.unwrap(); - // Fee should include: base fee (5000) + ATA creation rent - let rent = Rent::default(); - let expected_ata_rent = rent.minimum_balance(SplTokenAccountState::LEN); - let expected_min_fee = 5000 + expected_ata_rent; - - assert_eq!( - fee.total_fee_lamports, expected_min_fee, - "Fee should include base transaction fee plus ATA creation cost. Got: {}, Expected at least: {expected_min_fee}", fee.total_fee_lamports - ); - } } diff --git a/crates/lib/src/fee/price.rs b/crates/lib/src/fee/price.rs index a6d4ba19..60e74c38 100644 --- a/crates/lib/src/fee/price.rs +++ b/crates/lib/src/fee/price.rs @@ -28,26 +28,42 @@ pub struct PriceConfig { impl PriceConfig { pub async fn get_required_lamports( &self, - rpc_client: Option<&RpcClient>, - price_source: Option, + rpc_client: &RpcClient, + price_source: PriceSource, min_transaction_fee: u64, ) -> Result { match &self.model { PriceModel::Margin { margin } => { - Ok((min_transaction_fee as f64 * (1.0 + margin)) as u64) + let multiplier = 1.0 + margin; + let result = min_transaction_fee as f64 * multiplier; + + // Check for overflow/underflow before casting to u64 + if result > u64::MAX as f64 || result < 0.0 { + log::error!( + "Margin calculation overflow: min_transaction_fee={}, margin={}, result={}", + min_transaction_fee, + margin, + result + ); + return Err(KoraError::ValidationError( + "Margin calculation overflow".to_string(), + )); + } + + Ok(result as u64) } PriceModel::Fixed { amount, token } => { - if let (Some(price_source), Some(rpc_client)) = (price_source, rpc_client) { - Ok(TokenUtil::calculate_token_value_in_lamports( - *amount, - &Pubkey::from_str(token).unwrap(), - price_source, - rpc_client, - ) - .await?) - } else { - Ok(*amount) - } + Ok(TokenUtil::calculate_token_value_in_lamports( + *amount, + &Pubkey::from_str(token).map_err(|e| { + log::error!("Invalid Pubkey for price {e}"); + + KoraError::ConfigError + })?, + price_source, + rpc_client, + ) + .await?) } PriceModel::Free => Ok(0), } @@ -69,8 +85,12 @@ mod tests { let min_transaction_fee = 5000u64; // 5000 lamports base fee let expected_lamports = (5000.0 * 1.1) as u64; // 5500 lamports - let result = - price_config.get_required_lamports(None, None, min_transaction_fee).await.unwrap(); + let rpc_client = create_mock_rpc_client_with_mint(6); + + let result = price_config + .get_required_lamports(&rpc_client, PriceSource::Mock, min_transaction_fee) + .await + .unwrap(); assert_eq!(result, expected_lamports); } @@ -82,8 +102,12 @@ mod tests { let min_transaction_fee = 5000u64; - let result = - price_config.get_required_lamports(None, None, min_transaction_fee).await.unwrap(); + let rpc_client = create_mock_rpc_client_with_mint(6); + + let result = price_config + .get_required_lamports(&rpc_client, PriceSource::Mock, min_transaction_fee) + .await + .unwrap(); assert_eq!(result, min_transaction_fee); } @@ -101,11 +125,11 @@ mod tests { }; // Use Mock price source which returns 0.0001 SOL per USDC - let price_source = Some(PriceSource::Mock); + let price_source = PriceSource::Mock; let min_transaction_fee = 5000u64; let result = price_config - .get_required_lamports(Some(&rpc_client), price_source, min_transaction_fee) + .get_required_lamports(&rpc_client, price_source, min_transaction_fee) .await .unwrap(); @@ -129,11 +153,11 @@ mod tests { }; // Mock oracle returns 1.0 SOL price for SOL mint - let price_source = Some(PriceSource::Mock); + let price_source = PriceSource::Mock; let min_transaction_fee = 5000u64; let result = price_config - .get_required_lamports(Some(&rpc_client), price_source, min_transaction_fee) + .get_required_lamports(&rpc_client, price_source, min_transaction_fee) .await .unwrap(); @@ -144,29 +168,6 @@ mod tests { assert_eq!(result, 500000000); } - #[tokio::test] - async fn test_fixed_model_get_required_lamports_without_oracle() { - let rpc_client = create_mock_rpc_client_with_mint(6); - - let price_config = PriceConfig { - model: PriceModel::Fixed { - amount: 25000, - token: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(), - }, - }; - - // No price source provided - should return amount directly - let price_source = None; - let min_transaction_fee = 5000u64; - - let result = price_config - .get_required_lamports(Some(&rpc_client), price_source, min_transaction_fee) - .await - .unwrap(); - - assert_eq!(result, 25000); - } - #[tokio::test] async fn test_fixed_model_get_required_lamports_small_amount() { let rpc_client = create_mock_rpc_client_with_mint(6); // USDC has 6 decimals @@ -179,11 +180,11 @@ mod tests { }, }; - let price_source = Some(PriceSource::Mock); + let price_source = PriceSource::Mock; let min_transaction_fee = 5000u64; let result = price_config - .get_required_lamports(Some(&rpc_client), price_source, min_transaction_fee) + .get_required_lamports(&rpc_client, price_source, min_transaction_fee) .await .unwrap(); @@ -203,7 +204,7 @@ mod tests { let min_transaction_fee = 10000u64; let result = price_config - .get_required_lamports(Some(&rpc_client), Some(PriceSource::Mock), min_transaction_fee) + .get_required_lamports(&rpc_client, PriceSource::Mock, min_transaction_fee) .await .unwrap(); @@ -219,7 +220,7 @@ mod tests { let min_transaction_fee = 1000000u64; let result = price_config - .get_required_lamports(Some(&rpc_client), Some(PriceSource::Mock), min_transaction_fee) + .get_required_lamports(&rpc_client, PriceSource::Mock, min_transaction_fee) .await .unwrap(); diff --git a/crates/lib/src/rpc_server/auth.rs b/crates/lib/src/rpc_server/auth.rs index f8ea0f5e..4c0e2b4b 100644 --- a/crates/lib/src/rpc_server/auth.rs +++ b/crates/lib/src/rpc_server/auth.rs @@ -160,10 +160,14 @@ where return Ok(unauthorized_response); } - let ts = parsed_timestamp.unwrap(); + let ts = parsed_timestamp.expect("timestamp already validated"); let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) - .unwrap() + .map_err(|e| { + log::error!("System time error: {e:?}"); + e + }) + .unwrap_or_else(|_| std::time::Duration::from_secs(0)) .as_secs() as i64; if (now - ts).abs() > max_timestamp_age { @@ -171,7 +175,8 @@ where } // Verify HMAC signature using timestamp + body - let message = format!("{}{}", timestamp, std::str::from_utf8(&body_bytes).unwrap()); + let body_str = std::str::from_utf8(&body_bytes).unwrap_or(""); + let message = format!("{}{}", timestamp, body_str); let mut mac = match Hmac::::new_from_slice(secret.as_bytes()) { Ok(mac) => mac, diff --git a/crates/lib/src/rpc_server/method/estimate_transaction_fee.rs b/crates/lib/src/rpc_server/method/estimate_transaction_fee.rs index 7eb25b7b..a2c3c23f 100644 --- a/crates/lib/src/rpc_server/method/estimate_transaction_fee.rs +++ b/crates/lib/src/rpc_server/method/estimate_transaction_fee.rs @@ -66,7 +66,7 @@ pub async fn estimate_transaction_fee( &mut resolved_transaction, &fee_payer, validation_config.is_payment_required(), - Some(validation_config.price_source.clone()), + validation_config.price_source.clone(), ) .await?; diff --git a/crates/lib/src/rpc_server/method/sign_and_send_transaction.rs b/crates/lib/src/rpc_server/method/sign_and_send_transaction.rs index f9f6554c..1655e186 100644 --- a/crates/lib/src/rpc_server/method/sign_and_send_transaction.rs +++ b/crates/lib/src/rpc_server/method/sign_and_send_transaction.rs @@ -23,7 +23,6 @@ pub struct SignAndSendTransactionRequest { #[derive(Debug, Serialize, ToSchema)] pub struct SignAndSendTransactionResponse { - pub signature: String, pub signed_transaction: String, /// Public key of the signer used (for client consistency) pub signer_pubkey: String, @@ -47,11 +46,10 @@ pub async fn sign_and_send_transaction( ) .await?; - let (signature, signed_transaction) = + let (_, signed_transaction) = resolved_transaction.sign_and_send_transaction(&signer, rpc_client).await?; Ok(SignAndSendTransactionResponse { - signature, signed_transaction, signer_pubkey: signer.solana_pubkey().to_string(), }) diff --git a/crates/lib/src/rpc_server/method/sign_transaction.rs b/crates/lib/src/rpc_server/method/sign_transaction.rs index 54219763..7903eb6f 100644 --- a/crates/lib/src/rpc_server/method/sign_transaction.rs +++ b/crates/lib/src/rpc_server/method/sign_transaction.rs @@ -23,7 +23,6 @@ pub struct SignTransactionRequest { #[derive(Debug, Serialize, ToSchema)] pub struct SignTransactionResponse { - pub signature: String, pub signed_transaction: String, /// Public key of the signer used (for client consistency) pub signer_pubkey: String, @@ -53,7 +52,6 @@ pub async fn sign_transaction( let encoded = TransactionUtil::encode_versioned_transaction(&signed_transaction); Ok(SignTransactionResponse { - signature: transaction.signatures[0].to_string(), signed_transaction: encoded, signer_pubkey: signer.solana_pubkey().to_string(), }) diff --git a/crates/lib/src/rpc_server/openapi/spec/combined_api.json b/crates/lib/src/rpc_server/openapi/spec/combined_api.json index 539b2a71..685653b0 100644 --- a/crates/lib/src/rpc_server/openapi/spec/combined_api.json +++ b/crates/lib/src/rpc_server/openapi/spec/combined_api.json @@ -60,6 +60,15 @@ "type": "string", "nullable": true }, + "sig_verify": { + "type": "boolean", + "description": "Whether to verify signatures during simulation (defaults to true)" + }, + "signer_key": { + "type": "string", + "description": "Optional signer signer_key to ensure consistency across related RPC calls", + "nullable": true + }, "transaction": { "type": "string" } @@ -79,7 +88,9 @@ "schema": { "type": "object", "required": [ - "fee_in_lamports" + "fee_in_lamports", + "signer_pubkey", + "payment_address" ], "properties": { "fee_in_lamports": { @@ -91,6 +102,14 @@ "type": "number", "format": "double", "nullable": true + }, + "payment_address": { + "type": "string", + "description": "Public key of the payment destination" + }, + "signer_pubkey": { + "type": "string", + "description": "Public key of the signer used for fee estimation (for client consistency)" } } } @@ -272,7 +291,7 @@ "schema": { "type": "object", "required": [ - "fee_payer", + "fee_payers", "validation_config", "enabled_methods" ], @@ -280,8 +299,11 @@ "enabled_methods": { "$ref": "#/components/schemas/EnabledMethods" }, - "fee_payer": { - "type": "string" + "fee_payers": { + "type": "array", + "items": { + "type": "string" + } }, "validation_config": { "$ref": "#/components/schemas/ValidationConfig" @@ -324,6 +346,105 @@ } } }, + "/getPayerSigner": { + "summary": "getPayerSigner", + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "jsonrpc", + "id", + "method" + ], + "properties": { + "id": { + "type": "string", + "description": "An ID to identify the request.", + "enum": [ + "test-account" + ] + }, + "jsonrpc": { + "type": "string", + "description": "The version of the JSON-RPC protocol.", + "enum": [ + "2.0" + ] + }, + "method": { + "type": "string", + "description": "The name of the method to invoke.", + "enum": [ + "getPayerSigner" + ] + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "signer_address", + "payment_address" + ], + "properties": { + "payment_address": { + "type": "string", + "description": "The payment destination owner address (same as signer if no separate paymaster is configured)" + }, + "signer_address": { + "type": "string", + "description": "The recommended signer's public key" + } + } + } + } + } + }, + "429": { + "description": "Exceeded rate limit.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + } + } + }, + "500": { + "description": "Internal server error.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + } + } + } + } + } + }, "/getSupportedTokens": { "summary": "getSupportedTokens", "post": { @@ -462,6 +583,15 @@ "transaction" ], "properties": { + "sig_verify": { + "type": "boolean", + "description": "Whether to verify signatures during simulation (defaults to true)" + }, + "signer_key": { + "type": "string", + "description": "Optional signer signer_key to ensure consistency across related RPC calls", + "nullable": true + }, "transaction": { "type": "string" } @@ -481,15 +611,16 @@ "schema": { "type": "object", "required": [ - "signature", - "signed_transaction" + "signed_transaction", + "signer_pubkey" ], "properties": { - "signature": { - "type": "string" - }, "signed_transaction": { "type": "string" + }, + "signer_pubkey": { + "type": "string", + "description": "Public key of the signer used (for client consistency)" } } } @@ -571,6 +702,15 @@ "transaction" ], "properties": { + "sig_verify": { + "type": "boolean", + "description": "Whether to verify signatures during simulation (defaults to true)" + }, + "signer_key": { + "type": "string", + "description": "Optional signer signer_key to ensure consistency across related RPC calls", + "nullable": true + }, "transaction": { "type": "string" } @@ -590,15 +730,16 @@ "schema": { "type": "object", "required": [ - "signature", - "signed_transaction" + "signed_transaction", + "signer_pubkey" ], "properties": { - "signature": { - "type": "string" - }, "signed_transaction": { "type": "string" + }, + "signer_pubkey": { + "type": "string", + "description": "Public key of the signer used (for client consistency)" } } } @@ -680,6 +821,15 @@ "transaction" ], "properties": { + "sig_verify": { + "type": "boolean", + "description": "Whether to verify signatures during simulation (defaults to true)" + }, + "signer_key": { + "type": "string", + "description": "Optional signer signer_key to ensure consistency across related RPC calls", + "nullable": true + }, "transaction": { "type": "string" } @@ -700,12 +850,17 @@ "type": "object", "required": [ "transaction", - "signed_transaction" + "signed_transaction", + "signer_pubkey" ], "properties": { "signed_transaction": { "type": "string" }, + "signer_pubkey": { + "type": "string", + "description": "Public key of the signer used (for client consistency)" + }, "transaction": { "type": "string" } @@ -800,6 +955,11 @@ "destination": { "type": "string" }, + "signer_key": { + "type": "string", + "description": "Optional signer signer_key to ensure consistency across related RPC calls", + "nullable": true + }, "source": { "type": "string" }, @@ -824,7 +984,8 @@ "required": [ "transaction", "message", - "blockhash" + "blockhash", + "signer_pubkey" ], "properties": { "blockhash": { @@ -833,6 +994,10 @@ "message": { "type": "string" }, + "signer_pubkey": { + "type": "string", + "description": "Public key of the signer used (for client consistency)" + }, "transaction": { "type": "string" } @@ -883,6 +1048,7 @@ "liveness", "estimate_transaction_fee", "get_supported_tokens", + "get_payer_signer", "sign_transaction", "sign_and_send_transaction", "transfer_transaction", @@ -900,6 +1066,9 @@ "get_config": { "type": "boolean" }, + "get_payer_signer": { + "type": "boolean" + }, "get_supported_tokens": { "type": "boolean" }, @@ -969,7 +1138,7 @@ "GetConfigResponse": { "type": "object", "required": [ - "fee_payer", + "fee_payers", "validation_config", "enabled_methods" ], @@ -977,14 +1146,34 @@ "enabled_methods": { "$ref": "#/components/schemas/EnabledMethods" }, - "fee_payer": { - "type": "string" + "fee_payers": { + "type": "array", + "items": { + "type": "string" + } }, "validation_config": { "$ref": "#/components/schemas/ValidationConfig" } } }, + "GetPayerSignerResponse": { + "type": "object", + "required": [ + "signer_address", + "payment_address" + ], + "properties": { + "payment_address": { + "type": "string", + "description": "The payment destination owner address (same as signer if no separate paymaster is configured)" + }, + "signer_address": { + "type": "string", + "description": "The recommended signer's public key" + } + } + }, "GetSupportedTokensResponse": { "type": "object", "required": [ @@ -1086,6 +1275,15 @@ "transaction" ], "properties": { + "sig_verify": { + "type": "boolean", + "description": "Whether to verify signatures during simulation (defaults to true)" + }, + "signer_key": { + "type": "string", + "description": "Optional signer signer_key to ensure consistency across related RPC calls", + "nullable": true + }, "transaction": { "type": "string" } @@ -1094,15 +1292,16 @@ "SignAndSendTransactionResponse": { "type": "object", "required": [ - "signature", - "signed_transaction" + "signed_transaction", + "signer_pubkey" ], "properties": { - "signature": { - "type": "string" - }, "signed_transaction": { "type": "string" + }, + "signer_pubkey": { + "type": "string", + "description": "Public key of the signer used (for client consistency)" } } }, @@ -1112,6 +1311,15 @@ "transaction" ], "properties": { + "sig_verify": { + "type": "boolean", + "description": "Whether to verify signatures during simulation (defaults to true)" + }, + "signer_key": { + "type": "string", + "description": "Optional signer signer_key to ensure consistency across related RPC calls", + "nullable": true + }, "transaction": { "type": "string" } @@ -1121,12 +1329,17 @@ "type": "object", "required": [ "transaction", - "signed_transaction" + "signed_transaction", + "signer_pubkey" ], "properties": { "signed_transaction": { "type": "string" }, + "signer_pubkey": { + "type": "string", + "description": "Public key of the signer used (for client consistency)" + }, "transaction": { "type": "string" } @@ -1138,6 +1351,15 @@ "transaction" ], "properties": { + "sig_verify": { + "type": "boolean", + "description": "Whether to verify signatures during simulation (defaults to true)" + }, + "signer_key": { + "type": "string", + "description": "Optional signer signer_key to ensure consistency across related RPC calls", + "nullable": true + }, "transaction": { "type": "string" } @@ -1146,15 +1368,16 @@ "SignTransactionResponse": { "type": "object", "required": [ - "signature", - "signed_transaction" + "signed_transaction", + "signer_pubkey" ], "properties": { - "signature": { - "type": "string" - }, "signed_transaction": { "type": "string" + }, + "signer_pubkey": { + "type": "string", + "description": "Public key of the signer used (for client consistency)" } } }, @@ -1196,6 +1419,11 @@ "destination": { "type": "string" }, + "signer_key": { + "type": "string", + "description": "Optional signer signer_key to ensure consistency across related RPC calls", + "nullable": true + }, "source": { "type": "string" }, @@ -1209,7 +1437,8 @@ "required": [ "transaction", "message", - "blockhash" + "blockhash", + "signer_pubkey" ], "properties": { "blockhash": { @@ -1218,6 +1447,10 @@ "message": { "type": "string" }, + "signer_pubkey": { + "type": "string", + "description": "Public key of the signer used (for client consistency)" + }, "transaction": { "type": "string" } @@ -1242,10 +1475,7 @@ } }, "allowed_spl_paid_tokens": { - "type": "array", - "items": { - "type": "string" - } + "$ref": "#/components/schemas/SplTokenConfig" }, "allowed_tokens": { "type": "array", @@ -1277,6 +1507,9 @@ }, "price_source": { "$ref": "#/components/schemas/PriceSource" + }, + "token_2022": { + "$ref": "#/components/schemas/Token2022Config" } } } diff --git a/crates/lib/src/signer/turnkey/config.rs b/crates/lib/src/signer/turnkey/config.rs index 7eb7735c..70f1b3f7 100644 --- a/crates/lib/src/signer/turnkey/config.rs +++ b/crates/lib/src/signer/turnkey/config.rs @@ -1,10 +1,15 @@ +use std::str::FromStr; + use serde::{Deserialize, Serialize}; +use solana_sdk::pubkey::Pubkey; use crate::{ error::KoraError, signer::{ - config_trait::SignerConfigTrait, turnkey::types::TurnkeySigner, - utils::get_env_var_for_signer, KoraSigner, + config_trait::SignerConfigTrait, + turnkey::types::{TurnkeyError, TurnkeySigner}, + utils::get_env_var_for_signer, + KoraSigner, }, }; @@ -57,12 +62,16 @@ impl SignerConfigTrait for TurnkeySignerHandler { let private_key_id = get_env_var_for_signer(&config.private_key_id_env, signer_name)?; let public_key = get_env_var_for_signer(&config.public_key_env, signer_name)?; + let pubkey = Pubkey::from_str(&public_key) + .map_err(|e| TurnkeyError::InvalidPublicKey(e.to_string()))?; + let signer = TurnkeySigner::new( api_public_key, api_private_key, organization_id, private_key_id, public_key, + pubkey, ); Ok(KoraSigner::Turnkey(signer)) diff --git a/crates/lib/src/signer/turnkey/signer.rs b/crates/lib/src/signer/turnkey/signer.rs index ffd1acd7..51fcd03a 100644 --- a/crates/lib/src/signer/turnkey/signer.rs +++ b/crates/lib/src/signer/turnkey/signer.rs @@ -1,5 +1,3 @@ -use std::str::FromStr; - use base64::Engine; use p256::ecdsa::signature::Signer; use reqwest::Client; @@ -19,6 +17,7 @@ impl TurnkeySigner { organization_id: String, private_key_id: String, public_key: String, + pubkey: Pubkey, ) -> Self { Self { api_public_key, @@ -26,6 +25,7 @@ impl TurnkeySigner { organization_id, private_key_id, public_key, + pubkey, api_base_url: "https://api.turnkey.com".to_string(), client: Client::new(), } @@ -113,7 +113,8 @@ impl TurnkeySigner { transaction: &VersionedTransaction, ) -> Result { let sig = self.sign(transaction).await?; - let sig_bytes: [u8; 64] = sig.try_into().unwrap(); + let sig_bytes: [u8; 64] = + sig.try_into().map_err(|_| TurnkeyError::InvalidSignatureLength)?; Ok(Signature::from(sig_bytes)) } @@ -127,7 +128,7 @@ impl TurnkeySigner { let signature: p256::ecdsa::Signature = signing_key.sign(message.as_bytes()); let signature_der = signature.to_der().to_bytes(); - let signature_hex = bytes_to_hex(&signature_der).unwrap(); + let signature_hex = bytes_to_hex(&signature_der).map_err(TurnkeyError::InvalidStamp)?; let stamp = serde_json::json!({ "public_key": self.api_public_key, @@ -141,12 +142,14 @@ impl TurnkeySigner { } pub fn solana_pubkey(&self) -> Pubkey { - Pubkey::from_str(&self.public_key).unwrap() + self.pubkey } } #[cfg(test)] mod tests { + use std::str::FromStr; + use crate::tests::transaction_mock::create_mock_transaction; use super::*; @@ -166,6 +169,7 @@ mod tests { organization_id.clone(), private_key_id.clone(), public_key.clone(), + Pubkey::from_str(&public_key).unwrap(), ); assert_eq!(signer.api_public_key, api_public_key); @@ -183,6 +187,7 @@ mod tests { "org".to_string(), "key_id".to_string(), "11111111111111111111111111111111".to_string(), + Pubkey::from_str(&"11111111111111111111111111111111").unwrap(), ); let pubkey = signer.solana_pubkey(); @@ -226,6 +231,7 @@ mod tests { "test_org_id".to_string(), "test_private_key_id".to_string(), "11111111111111111111111111111111".to_string(), + Pubkey::from_str(&"11111111111111111111111111111111").unwrap(), ); signer.api_base_url = server.url(); @@ -268,6 +274,7 @@ mod tests { "invalid_org_id".to_string(), "invalid_private_key_id".to_string(), "11111111111111111111111111111111".to_string(), + Pubkey::from_str(&"11111111111111111111111111111111").unwrap(), ); signer.api_base_url = server.url(); @@ -305,6 +312,7 @@ mod tests { "test_org_id".to_string(), "test_private_key_id".to_string(), "11111111111111111111111111111111".to_string(), + Pubkey::from_str(&"11111111111111111111111111111111").unwrap(), ); signer.api_base_url = server.url(); @@ -358,6 +366,7 @@ mod tests { "test_org_id".to_string(), "test_private_key_id".to_string(), "11111111111111111111111111111111".to_string(), + Pubkey::from_str(&"11111111111111111111111111111111").unwrap(), ); signer.api_base_url = server.url(); @@ -378,6 +387,7 @@ mod tests { "test_org_id".to_string(), "test_private_key_id".to_string(), "11111111111111111111111111111111".to_string(), + Pubkey::from_str(&"11111111111111111111111111111111").unwrap(), ); let test_message = r#"{"test": "message"}"#; diff --git a/crates/lib/src/signer/turnkey/types.rs b/crates/lib/src/signer/turnkey/types.rs index 06300a4f..3c71d31e 100644 --- a/crates/lib/src/signer/turnkey/types.rs +++ b/crates/lib/src/signer/turnkey/types.rs @@ -2,6 +2,7 @@ use hex::FromHexError; use p256::ecdsa::signature; use reqwest::Client; use serde::{Deserialize, Serialize}; +use solana_sdk::pubkey::Pubkey; #[derive(Clone, Debug)] pub struct TurnkeySigner { @@ -10,6 +11,7 @@ pub struct TurnkeySigner { pub api_public_key: String, pub api_private_key: String, pub public_key: String, + pub pubkey: Pubkey, pub api_base_url: String, pub client: Client, } @@ -69,7 +71,8 @@ pub enum TurnkeyError { SigningKeyError(signature::Error), InvalidResponse, InvalidPrivateKeyLength, - InvalidPublicKey, + InvalidSignatureLength, + InvalidPublicKey(String), Other(anyhow::Error), } @@ -78,9 +81,10 @@ impl std::fmt::Display for TurnkeyError { match self { TurnkeyError::ApiError(status) => write!(f, "API error: {status}"), TurnkeyError::InvalidResponse => write!(f, "Invalid response"), - TurnkeyError::InvalidPublicKey => write!(f, "Invalid public key"), + TurnkeyError::InvalidPublicKey(msg) => write!(f, "Invalid public key: {msg}"), TurnkeyError::InvalidSignature => write!(f, "Invalid signature"), TurnkeyError::InvalidPrivateKeyLength => write!(f, "Invalid private key length"), + TurnkeyError::InvalidSignatureLength => write!(f, "Invalid signature length"), TurnkeyError::RequestError(e) => write!(f, "Request error: {e}"), TurnkeyError::JsonError(e) => write!(f, "JSON error: {e}"), TurnkeyError::InvalidStamp(e) => write!(f, "Invalid stamp: {e}"), @@ -107,6 +111,9 @@ mod tests { let error = TurnkeyError::InvalidSignature; assert_eq!(error.to_string(), "Invalid signature"); + + let error = TurnkeyError::InvalidPublicKey("test error".to_string()); + assert_eq!(error.to_string(), "Invalid public key: test error"); } #[test] diff --git a/crates/lib/src/token/interface.rs b/crates/lib/src/token/interface.rs index da35c538..6f81af01 100644 --- a/crates/lib/src/token/interface.rs +++ b/crates/lib/src/token/interface.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; use mockall::automock; -use solana_sdk::{account::Account, instruction::Instruction, pubkey::Pubkey}; +use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; use std::any::Any; pub trait TokenState: Any + Send + Sync { @@ -75,13 +75,4 @@ pub trait TokenInterface: Send + Sync { mint: &Pubkey, mint_data: &[u8], ) -> Result, Box>; - - /// Get the account size required for creating an ATA for this token program - /// For SPL Token, this returns the standard size - /// For Token-2022, this query the mint to determine size requirements - async fn get_ata_account_size( - &self, - mint_pubkey: &Pubkey, - mint: &Account, - ) -> Result>; } diff --git a/crates/lib/src/token/spl_token.rs b/crates/lib/src/token/spl_token.rs index 34997fe6..cd75014a 100644 --- a/crates/lib/src/token/spl_token.rs +++ b/crates/lib/src/token/spl_token.rs @@ -3,7 +3,7 @@ use crate::token::interface::TokenMint; use super::interface::{TokenInterface, TokenState}; use async_trait::async_trait; use solana_program::pubkey::Pubkey; -use solana_sdk::{account::Account, instruction::Instruction, program_pack::Pack}; +use solana_sdk::{instruction::Instruction, program_pack::Pack}; use spl_associated_token_account::{ get_associated_token_address_with_program_id, instruction::create_associated_token_account, }; @@ -204,15 +204,6 @@ impl TokenInterface for TokenProgram { freeze_authority: mint_state.freeze_authority.into(), })) } - - async fn get_ata_account_size( - &self, - _mint_pubkey: &Pubkey, - _mint: &Account, - ) -> Result> { - // SPL Token accounts always have the same size - Ok(TokenAccountState::LEN) - } } #[cfg(test)] @@ -221,7 +212,7 @@ mod tests { use super::*; use solana_program::program_pack::Pack; - use solana_sdk::{account::Account, pubkey::Pubkey}; + use solana_sdk::pubkey::Pubkey; use spl_token::state::{Account as SplTokenAccount, AccountState}; #[test] @@ -438,30 +429,6 @@ mod tests { assert_eq!(instruction.accounts.len(), 6); // funding, ata, wallet, mint, system_program, token_program } - #[tokio::test] - async fn test_get_ata_account_size() { - let program = TokenProgram::new(); - let mint_pubkey = Pubkey::new_unique(); - let mint_account = Account { - lamports: 1000000, - data: MintAccountMockBuilder::new() - .with_mint_authority(Some(Pubkey::new_unique())) - .with_supply(1000000) - .with_decimals(6) - .with_initialized(true) - .with_freeze_authority(None) - .build() - .data, - owner: spl_token::id(), - executable: false, - rent_epoch: 0, - }; - - let result = program.get_ata_account_size(&mint_pubkey, &mint_account).await; - assert!(result.is_ok()); - assert_eq!(result.unwrap(), SplTokenAccount::LEN); - } - #[test] fn test_spl_mint_get_token_program() { let spl_mint = SplMint { diff --git a/crates/lib/src/token/spl_token_2022.rs b/crates/lib/src/token/spl_token_2022.rs index 471f1059..d238d1ad 100644 --- a/crates/lib/src/token/spl_token_2022.rs +++ b/crates/lib/src/token/spl_token_2022.rs @@ -1,18 +1,15 @@ -use crate::{ - token::{ - interface::TokenMint, - spl_token_2022_util::{ - try_parse_account_extension, try_parse_mint_extension, AccountExtension, MintExtension, - ParsedExtension, - }, +use crate::token::{ + interface::TokenMint, + spl_token_2022_util::{ + try_parse_account_extension, try_parse_mint_extension, AccountExtension, MintExtension, + ParsedExtension, }, - KoraError, }; use super::interface::{TokenInterface, TokenState}; use async_trait::async_trait; use solana_program::{program_pack::Pack, pubkey::Pubkey}; -use solana_sdk::{account::Account, instruction::Instruction}; +use solana_sdk::instruction::Instruction; use spl_associated_token_account::{ get_associated_token_address_with_program_id, instruction::create_associated_token_account, }; @@ -161,7 +158,11 @@ impl Token2022Mint { /// Calculate transfer fee for a given amount and epoch /// Returns None if no transfer fee is configured - pub fn calculate_transfer_fee(&self, amount: u64, current_epoch: u64) -> Option { + pub fn calculate_transfer_fee( + &self, + amount: u64, + current_epoch: u64, + ) -> Result, crate::error::KoraError> { if let Some(fee_config) = self.get_transfer_fee() { let transfer_fee = if current_epoch >= u64::from(fee_config.newer_transfer_fee.epoch) { &fee_config.newer_transfer_fee @@ -172,10 +173,26 @@ impl Token2022Mint { let basis_points = u16::from(transfer_fee.transfer_fee_basis_points); let maximum_fee = u64::from(transfer_fee.maximum_fee); - let fee_amount = (amount as u128 * basis_points as u128 / 10_000) as u64; - Some(std::cmp::min(fee_amount, maximum_fee)) + let fee_amount = (amount as u128) + .checked_mul(basis_points as u128) + .and_then(|product| product.checked_div(10_000)) + .and_then( + |result| if result <= u64::MAX as u128 { Some(result as u64) } else { None }, + ) + .ok_or_else(|| { + log::error!( + "Transfer fee calculation overflow: amount={}, basis_points={}", + amount, + basis_points + ); + crate::error::KoraError::ValidationError(format!( + "Transfer fee calculation overflow: amount={}, basis_points={}", + amount, basis_points + )) + })?; + Ok(Some(std::cmp::min(fee_amount, maximum_fee))) } else { - None + Ok(None) } } @@ -385,57 +402,6 @@ impl TokenInterface for Token2022Program { extensions, })) } - - async fn get_ata_account_size( - &self, - mint_pubkey: &Pubkey, - mint_account: &Account, - ) -> Result> { - let token_program = Token2022Program::new(); - let mint_with_extensions = token_program.unpack_mint(mint_pubkey, &mint_account.data)?; - - let mint_with_extensions = mint_with_extensions - .as_any() - .downcast_ref::() - .ok_or(KoraError::InternalServerError("Failed to unpack mint".to_string()))?; - - let mut required_account_extensions = Vec::new(); - - // If the mint has TransferFeeConfig, token accounts need TransferFeeAmount to store withheld fees - if mint_with_extensions.has_extension(ExtensionType::TransferFeeConfig) { - required_account_extensions.push(ExtensionType::TransferFeeAmount); - } - - // If mint is non-transferable, token accounts must be non-transferable too - if mint_with_extensions.is_non_transferable() { - required_account_extensions.push(ExtensionType::NonTransferableAccount); - } - - // If mint has confidential transfer, token accounts need confidential transfer account extension - if mint_with_extensions.has_confidential_transfer_extension() { - required_account_extensions.push(ExtensionType::ConfidentialTransferAccount); - } - - // If mint has transfer hook, token accounts need transfer hook account extension - if mint_with_extensions.has_transfer_hook_extension() { - required_account_extensions.push(ExtensionType::TransferHookAccount); - } - - // If mint is pausable, token accounts need pausable account extension - if mint_with_extensions.has_pausable_extension() { - required_account_extensions.push(ExtensionType::PausableAccount); - } - - // ATAs created for Token-2022 are immutable by default (owner cannot be changed) - required_account_extensions.push(ExtensionType::ImmutableOwner); - - // Calculate the account size with all required extensions - let account_size = ExtensionType::try_calculate_account_len::( - &required_account_extensions, - )?; - - Ok(account_size) - } } /// Trait for Token-2022 extension validation and fee calculation @@ -644,19 +610,19 @@ mod tests { let mint = MintAccountMockBuilder::new() .build_as_custom_token2022_mint(mint_pubkey, HashMap::new()); - let fee = mint.calculate_transfer_fee(1000, 0); + let fee = mint.calculate_transfer_fee(1000, 0).unwrap(); assert!(fee.is_none(), "Mint without transfer fee config should return None"); let mint = MintAccountMockBuilder::new() .build_as_custom_token2022_mint(mint_pubkey, create_test_extensions()); // Test zero amount - let zero_fee = mint.calculate_transfer_fee(0, 0); + let zero_fee = mint.calculate_transfer_fee(0, 0).unwrap(); assert!(zero_fee.is_some()); assert_eq!(zero_fee.unwrap(), 0, "Zero amount should result in zero fee"); // Test maximum fee cap - let large_amount_fee = mint.calculate_transfer_fee(1_000_000, 0); + let large_amount_fee = mint.calculate_transfer_fee(1_000_000, 0).unwrap(); assert!(large_amount_fee.is_some()); assert_eq!(large_amount_fee.unwrap(), 1000, "Large amount should be capped at maximum fee"); } @@ -685,24 +651,6 @@ mod tests { assert!(!mint.has_confidential_mint_burn_extension()); } - #[tokio::test] - async fn test_token2022_get_ata_account_size() { - let program = Token2022Program::new(); - let mint_pubkey = Pubkey::new_unique(); - - let mint_account = MintAccountMockBuilder::new().with_decimals(6).build_token2022(); - - let result = program.get_ata_account_size(&mint_pubkey, &mint_account).await; - - assert!(result.is_ok()); - let size = result.unwrap(); - - // Size should be base Token2022AccountState size + ImmutableOwner extension + 1 byte padding - // (has 0 bytes of extension data, but still has the TVL overhead (type 2 bytes, length 2 bytes)) - // (ATAs created for Token-2022 are immutable by default) - assert_eq!(size, 170); // Base (165) + Immutable Owner TVL Overhead (5) - } - #[test] fn test_token2022_account_extension_methods() { let account = TokenAccountMockBuilder::new() @@ -752,7 +700,7 @@ mod tests { ]; for (amount, _expected_adjusted) in test_cases { - let expected_fee = mint.calculate_transfer_fee(amount, 0).unwrap_or(0); + let expected_fee = mint.calculate_transfer_fee(amount, 0).unwrap().unwrap_or(0); let expected_result = amount.saturating_sub(expected_fee); assert_eq!(expected_result, expected_result); } @@ -796,11 +744,11 @@ mod tests { .build_as_custom_token2022_mint(mint_pubkey, extensions); // Test older fee (epoch < 10) - let fee_old = mint.calculate_transfer_fee(10000, 5).unwrap(); + let fee_old = mint.calculate_transfer_fee(10000, 5).unwrap().unwrap(); assert_eq!(fee_old, 100); // 10000 * 1% = 100 // Test newer fee (epoch >= 10) - let fee_new = mint.calculate_transfer_fee(10000, 15).unwrap(); + let fee_new = mint.calculate_transfer_fee(10000, 15).unwrap().unwrap(); assert_eq!(fee_new, 200); // 10000 * 2% = 200 } @@ -830,30 +778,4 @@ mod tests { // Test extensions not present assert!(!mint.is_non_transferable()); } - - #[tokio::test] - async fn test_token2022_get_ata_account_size_with_extensions() { - let program = Token2022Program::new(); - let mint_pubkey = Pubkey::new_unique(); - - // Create a mint with transfer fee extension - let mint_account = MintAccountMockBuilder::new() - .with_decimals(6) - .with_supply(1_000_000) - .with_extension(ExtensionType::TransferFeeConfig) - .build_token2022_account_state_with_extensions() - .unwrap(); - - let result = program.get_ata_account_size(&mint_pubkey, &mint_account).await; - - // Should succeed and return size that includes extension space - assert!(result.is_ok()); - - // Size would be - // Base 165 - // Immutable owner tvl overhead 4 (all ATA for 2022 have immutable owner) - // + 1 byte padding - // Transfer fee config 8 bytes for data and 4 bytes for TVL overhead - assert_eq!(result.unwrap(), 182); - } } diff --git a/crates/lib/src/transaction/versioned_transaction.rs b/crates/lib/src/transaction/versioned_transaction.rs index 7fe819f9..c5480c60 100644 --- a/crates/lib/src/transaction/versioned_transaction.rs +++ b/crates/lib/src/transaction/versioned_transaction.rs @@ -278,7 +278,7 @@ impl VersionedTransactionOps for VersionedTransactionResolved { self, &fee_payer, config.validation.is_payment_required(), - Some(config.validation.price_source.clone()), + config.validation.price_source.clone(), ) .await?; diff --git a/sdks/ts/src/types/index.ts b/sdks/ts/src/types/index.ts index 19776281..8ef65fa7 100644 --- a/sdks/ts/src/types/index.ts +++ b/sdks/ts/src/types/index.ts @@ -112,8 +112,6 @@ export interface TransferTransactionResponse { * Response from signing a transaction. */ export interface SignTransactionResponse { - /** Base58-encoded signature */ - signature: string; /** Base64-encoded signed transaction */ signed_transaction: string; /** Public key of the signer used to sign the transaction */ @@ -124,8 +122,6 @@ export interface SignTransactionResponse { * Response from signing and sending a transaction. */ export interface SignAndSendTransactionResponse { - /** Base58-encoded transaction signature */ - signature: string; /** Base64-encoded signed transaction */ signed_transaction: string; /** Public key of the signer used to send the transaction */ diff --git a/sdks/ts/test/integration.test.ts b/sdks/ts/test/integration.test.ts index 72c1f3b2..d65bdebb 100644 --- a/sdks/ts/test/integration.test.ts +++ b/sdks/ts/test/integration.test.ts @@ -173,7 +173,6 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without const signResult = await client.signTransaction({ transaction }); expect(signResult).toBeDefined(); - expect(signResult.signature).toBeDefined(); expect(signResult.signed_transaction).toBeDefined(); }); @@ -195,7 +194,6 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without }); expect(signResult).toBeDefined(); - expect(signResult.signature).toBeDefined(); expect(signResult.signed_transaction).toBeDefined(); }); @@ -332,7 +330,6 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without const { transaction } = await client.transferTransaction(request); const signResult = await client.signTransaction({ transaction }); - expect(signResult.signature).toBeDefined(); expect(signResult.signed_transaction).toBeDefined(); }); diff --git a/sdks/ts/test/unit.test.ts b/sdks/ts/test/unit.test.ts index aab3896d..36549ec5 100644 --- a/sdks/ts/test/unit.test.ts +++ b/sdks/ts/test/unit.test.ts @@ -225,7 +225,6 @@ describe('KoraClient Unit Tests', () => { transaction: 'base64_encoded_transaction', }; const mockResponse: SignTransactionResponse = { - signature: 'test_signature', signed_transaction: 'base64_signed_transaction', signer_pubkey: 'test_signer_pubkey', }; @@ -245,7 +244,6 @@ describe('KoraClient Unit Tests', () => { transaction: 'base64_encoded_transaction', }; const mockResponse: SignAndSendTransactionResponse = { - signature: 'test_signature', signed_transaction: 'base64_signed_transaction', signer_pubkey: 'test_signer_pubkey', }; diff --git a/tests/rpc/transaction_signing.rs b/tests/rpc/transaction_signing.rs index bc5af31e..8aa1628b 100644 --- a/tests/rpc/transaction_signing.rs +++ b/tests/rpc/transaction_signing.rs @@ -30,7 +30,6 @@ async fn test_sign_transaction_legacy() { .await .expect("Failed to sign 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" @@ -76,7 +75,6 @@ async fn test_sign_transaction_v0() { .await .expect("Failed to sign V0 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" @@ -125,7 +123,6 @@ async fn test_sign_transaction_v0_with_lookup() { .await .expect("Failed to sign V0 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" @@ -162,7 +159,6 @@ async fn test_sign_spl_transaction_legacy() { .await .expect("Failed to sign 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" @@ -208,7 +204,6 @@ async fn test_sign_spl_transaction_v0() { .await .expect("Failed to sign V0 SPL 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" @@ -257,7 +252,6 @@ async fn test_sign_spl_transaction_v0_with_lookup() { .await .expect("Failed to sign V0 SPL 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" @@ -309,7 +303,7 @@ async fn test_sign_transaction_v0_with_valid_lookup_table() { .expect("Failed to sign V0 transaction with valid lookup table"); response.assert_success(); - assert!(response["signature"].as_str().is_some(), "Expected signature in response"); + assert!( response["signed_transaction"].as_str().is_some(), "Expected signed_transaction in response" @@ -394,7 +388,7 @@ async fn test_sign_and_send_transaction_legacy() { assert!(result.is_ok(), "Expected signAndSendTransaction to succeed"); 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" @@ -430,7 +424,7 @@ async fn test_sign_and_send_transaction_v0() { assert!(result.is_ok(), "Expected signAndSendTransaction to succeed"); 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" @@ -469,7 +463,7 @@ async fn test_sign_and_send_transaction_v0_with_lookup() { assert!(result.is_ok(), "Expected signAndSendTransaction to succeed"); 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" diff --git a/tests/tokens/token_2022_extensions_test.rs b/tests/tokens/token_2022_extensions_test.rs index dd84e757..abb0e131 100644 --- a/tests/tokens/token_2022_extensions_test.rs +++ b/tests/tokens/token_2022_extensions_test.rs @@ -557,7 +557,6 @@ async fn test_transfer_hook_allows_transfer() { .await .expect("Failed to sign transaction with transfer hook"); - assert!(response["signature"].as_str().is_some(), "Expected signature in response"); assert!( response["signed_transaction"].as_str().is_some(), "Expected signed_transaction in response" diff --git a/tests/tokens/token_2022_test.rs b/tests/tokens/token_2022_test.rs index f6e55fad..c6d7092a 100644 --- a/tests/tokens/token_2022_test.rs +++ b/tests/tokens/token_2022_test.rs @@ -107,7 +107,6 @@ async fn test_sign_token_2022_transaction_legacy() { .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" @@ -147,7 +146,6 @@ async fn test_sign_token_2022_transaction_v0() { .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" @@ -191,7 +189,6 @@ async fn test_sign_token_2022_transaction_v0_with_lookup() { .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" @@ -240,7 +237,7 @@ async fn test_sign_and_send_token_2022_transaction_legacy() { 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" @@ -270,7 +267,7 @@ async fn test_sign_and_send_token_2022_transaction_v0() { 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" @@ -307,7 +304,7 @@ async fn test_sign_and_send_token_2022_transaction_v0_with_lookup() { "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" From 06de9a02407c97b21ecf617327917174057cfac6 Mon Sep 17 00:00:00 2001 From: amilz <85324096+amilz@users.noreply.github.com> Date: Fri, 26 Sep 2025 13:02:25 -0700 Subject: [PATCH 07/29] docs: add x402 kora demo (#227) docs/add x402 kora demo PRO-353 --- .gitignore | 1 - docs/x402/demo/.env.example | 24 + docs/x402/demo/X402_DEMO_GUIDE.md | 487 ++ docs/x402/demo/api/package.json | 22 + docs/x402/demo/api/pnpm-lock.yaml | 6039 +++++++++++++++++ docs/x402/demo/api/src/api.ts | 53 + docs/x402/demo/assets/x402-protocol-flow.png | Bin 0 -> 384752 bytes docs/x402/demo/client/package.json | 27 + docs/x402/demo/client/pnpm-lock.yaml | 4794 +++++++++++++ docs/x402/demo/client/src/index.ts | 108 + docs/x402/demo/client/src/setup.ts | 93 + docs/x402/demo/client/tsconfig.json | 20 + docs/x402/demo/facilitator/notes.md | 160 + docs/x402/demo/facilitator/package.json | 23 + docs/x402/demo/facilitator/pnpm-lock.yaml | 5138 ++++++++++++++ docs/x402/demo/facilitator/src/facilitator.ts | 148 + docs/x402/demo/kora/kora.toml | 86 + docs/x402/demo/kora/signers.toml | 7 + docs/x402/demo/package.json | 13 + 19 files changed, 17242 insertions(+), 1 deletion(-) create mode 100644 docs/x402/demo/.env.example create mode 100644 docs/x402/demo/X402_DEMO_GUIDE.md create mode 100644 docs/x402/demo/api/package.json create mode 100644 docs/x402/demo/api/pnpm-lock.yaml create mode 100644 docs/x402/demo/api/src/api.ts create mode 100644 docs/x402/demo/assets/x402-protocol-flow.png create mode 100644 docs/x402/demo/client/package.json create mode 100644 docs/x402/demo/client/pnpm-lock.yaml create mode 100644 docs/x402/demo/client/src/index.ts create mode 100644 docs/x402/demo/client/src/setup.ts create mode 100644 docs/x402/demo/client/tsconfig.json create mode 100644 docs/x402/demo/facilitator/notes.md create mode 100644 docs/x402/demo/facilitator/package.json create mode 100644 docs/x402/demo/facilitator/pnpm-lock.yaml create mode 100644 docs/x402/demo/facilitator/src/facilitator.ts create mode 100644 docs/x402/demo/kora/kora.toml create mode 100644 docs/x402/demo/kora/signers.toml create mode 100644 docs/x402/demo/package.json diff --git a/.gitignore b/.gitignore index db3b5603..733f9ebd 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,6 @@ generated/ # SDK node_modules/ dist/ -.env* # Coverage reports coverage/ diff --git a/docs/x402/demo/.env.example b/docs/x402/demo/.env.example new file mode 100644 index 00000000..0cb186c1 --- /dev/null +++ b/docs/x402/demo/.env.example @@ -0,0 +1,24 @@ +# ============================================================================== +# SERVER CONFIG +# ============================================================================== +FACILITATOR_URL=http://localhost:3000 +FACILITATOR_PORT=3000 +PROTECTED_API_URL=http://localhost:4021/protected +API_PORT=4021 +KORA_RPC_URL=http://localhost:8080/ +NETWORK=solana-devnet + + +# Should match kora/kora.toml +KORA_API_KEY=kora_facilitator_api_key_example + +# ============================================================================== +# SIGNER KEYS +# ============================================================================== + +# These will be automatically generated by the `pnpm run setup` command + +# KORA_SIGNER_ADDRESS= +# KORA_SIGNER_PRIVATE_KEY= +# PAYER_ADDRESS= +# PAYER_PRIVATE_KEY= \ No newline at end of file diff --git a/docs/x402/demo/X402_DEMO_GUIDE.md b/docs/x402/demo/X402_DEMO_GUIDE.md new file mode 100644 index 00000000..19647fe6 --- /dev/null +++ b/docs/x402/demo/X402_DEMO_GUIDE.md @@ -0,0 +1,487 @@ +--- +title: x402 Integration with Kora - Complete Demo Guide +description: Learn how to integrate the x402 payment protocol with Kora's gasless Solana signing infrastructure, enabling micropayments for API access with zero gas fees. +date: 2025-09-25 +--- + +# Solana x402 Protocol Integration with Kora RPC + +## What You'll Build + +This guide walks you through implementing a complete x402 (HTTP 402 Payment Required) integration with Kora, Solana gasless signing infrastructure. By the end, you'll have a working system where: + +- APIs can charge micropayments for access using the x402 protocol +- Users pay in USDC without needing SOL for gas fees +- Kora handles all transaction fees as the gasless facilitator +- Payments are settled atomically on Solana blockchain + +The final result will be a fully functional payment-protected API: + +```shell +โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +X402 + KORA PAYMENT FLOW DEMONSTRATION +โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” + +[1/4] Initializing payment signer + โ†’ Network: solana-devnet + โ†’ Payer address: BYJV...TbBc + โœ“ Signer initialized + +[2/4] Attempting to access protected endpoint without payment + โ†’ GET http://localhost:4021/protected + โ†’ Response: 402 Payment Required + โœ… Status code: 402 + +[3/4] Accessing protected endpoint with x402 payment + โ†’ Using x402 fetch wrapper + โ†’ Payment will be processed via Kora facilitator + โ†’ Transaction submitted to Solana + โœ… Status code: 200 + +[4/4] Processing response data + โœ“ Payment response decoded + +โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +SUCCESS: Payment completed and API accessed +โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” + +Response Data: +{ + "data": { + "message": "Protected endpoint accessed successfully", + "timestamp": "2025-09-25T20:14:04.242Z" + }, + "status_code": 200, + "payment_response": { + "transaction": "5ULZpdeThaMAy6hcEGfAoMFqJqPpCtxdCxb6JYUV6nA4x8Lk2hKEuzofGUPoe1pop6BdWMSmF5oRPrXsbdWmpruf", + "success": true, + "network": "solana-devnet" + } +} +``` + +## What is x402? + +[x402](https://www.x402.org/) is an open payment standard that enables seamless micropayments for API access. Instead of traditional subscription models or API keys, x402 allows servers to charge for individual API calls, creating true pay-per-use infrastructure. + +Key benefits of x402: +- **Instant Micropayments**: Pay fractions of a cent per API call +- **Enable AI agents to pay for API calls**: Pay for API calls with AI agents +- **No Subscriptions**: Users only pay for what they use +- **Web3 Payments**: Transparent, verifiable payments on-chain +- **Standard HTTP**: Works with existing web infrastructure using an HTTP 402 status code when payment is required + +Servers using x402 to require micropayments for API access will return an HTTP 402 status code when payment is required. To access protected endpoints, clients must pass a valid payment to the server in a `X-PAYMENT` header. x402 relies on "Facilitators" to verify and settle transactions so that servers don't need to directly interact with blockchain infrastructure. + +### Understanding Facilitators + +Facilitators are a crucial component in the x402 ecosystem. They act as specialized services that abstract blockchain payments on behalf of API servers. + +**What Facilitators Do:** +- **Verify Payments**: Validate that client's payment payloads are correctly formed and sufficient +- **Abstract Complexity**: Remove the need for servers to directly interact with blockchain infrastructure (signing and paying network fees) +- **Settle Transactions**: Submit validated transactions to Solana (or other networks) + +In our demo, we create a facilitator that leverages Kora to verify and settle transactions (more details below). + +## What is Kora? + +Kora is a Solana signer node that provides signing and gasless transaction services. It enables applications to abstract away gas fees, allowing users to pay transaction costs in tokens other than SOL, or have fees sponsored entirely. + +Key features of Kora: +- **Gasless Transactions**: Users don't need SOL to execute transactions +- **Fee Abstraction**: Pay fees in USDC or other SPL tokens +- **JSON-RPC Interface**: Simple HTTP API for transaction handling +- **Flexible Signers**: Support for multiple signer backends (memory, Vault, Turnkey, Privy) +- **Policy Engine**: Granular control over transaction validation and fee policies + +In the context of x402, Kora serves as the perfect backend for facilitators: it handles network fees, it signs transactions, and it validates transactions. + +## Architecture Overview + +Our x402 + Kora integration consists of four interconnected components with a complete request/response cycle: + +Complete Payment Flow: +1. Client requests protected resource โ†’ API returns 402 Payment Required +2. Client creates payment transaction with x402 fetch wrapper (which assembles a Solana transaction with a payment instruction) +3. Client sends payment to Facilitator for verification +4. Facilitator validates via Kora, which signs and submits to Solana +5. Transaction confirmed on-chain, Facilitator notifies API +6. API returns protected content with payment receipt to Client + +### Component Breakdown + +1. **Kora RPC Server** (Port 8080) + - Core gasless transaction service + - Handles transaction signing as fee payer + - Validates transactions against configured policies + +2. **Facilitator Wrapper/Proxy Server** (Port 3000) + - Adapts Kora to x402 protocol + - Implements `/verify`, `/settle`, and `/supported` endpoints + - Translates between x402 and Kora data formats + +3. **Protected API** (Port 4021) + - Demo API server with payment-protected endpoints + - Uses x402-express middleware for payment handling + - Returns data only after successful payment + +4. **Client Application** + - Demonstrates x402 fetch wrapper usage + - Signs transactions with user's private key + +The multi-component approach might seem complex, but it mirrors real-world production systems where payment processing, API serving, and client applications are separate concerns. + +## Prerequisites + +Before starting, ensure you have: + +- [**Rust**](https://www.rust-lang.org/tools/install) (latest stable version) +- [**Node.js**](https://nodejs.org/en/download) (LTS or later) +- [**pnpm**](https://pnpm.io/installation) (latest version) +- Basic understanding of [Solana transactions](https://solana.com/docs/core/transactions) and [SPL tokens](https://solana.com/docs/tokens) + +## Project Setup + +### Step 1: Clone and Build Kora + +```bash +# Clone the repository +git clone https://github.com/solana-foundation/kora.git +cd kora + +# Checkout the release branch as Kora is currently in a feature freeze for audit +git checkout release/feature-freeze-for-audit + +# Build and install Kora +make install +``` + +This installs the `kora` binary to your system, which we'll use to run the RPC server. + +### Step 2: Navigate to Demo Directory + +```bash +cd docs/x402/demo +``` + +### Step 3: Install Dependencies + +Install Node.js dependencies for all demo components: + +```bash +# Install dependencies for all components (facilitator, API, and client) +pnpm run install:all +``` + +This script installs dependencies for: +- The facilitator wrapper service +- The protected API server +- The client demonstration app + +### Step 4: Build Kora SDK + +Build the Kora SDK so we can use the Kora TypeScript SDK in the Facilitator: + +```bash +pnpm run build:kora-sdk +``` + +### Step 5: Configure Environment + +The demo includes a `.env.example` file with the required environment variables. First, let's set up the basic configuration: + +```bash +# Copy the example environment file +cp .env.example .env +``` + +Now you need to generate or provide keypairs for the demo. Run the following command to generate the keypairs: + +```bash +pnpm run setup +``` + +This will generate the keypairs and add them to the `.env` file: + +- `KORA_SIGNER_ADDRESS` - The address of the Kora signer +- `KORA_SIGNER_PRIVATE_KEY` - The private key of the Kora signer +- `PAYER_ADDRESS` - The address of the payer who will pay to access the protected API +- `PAYER_PRIVATE_KEY` - The private key of the payer + +### Step 5: Update Configuration Files + +#### kora.toml + +The `kora/kora.toml` file configures the Kora RPC server. You should not need to make any changes to this file, but you can verify the following settings: + +1. **Payment Token**: Ensure the Devnet USDC mint is in the allowlist: + +```toml +allowed_tokens = [ + "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU", # USDC devnet +] +``` + +2. **API Authentication**: The demo uses an API key for Kora access. This should match the `KORA_API_KEY` in the `.env` file: + +```toml +[kora.auth] +api_key = "kora_facilitator_api_key_example" +``` + +3. **Fee Payer Policy**: Configured to restrict signing unwanted transactions: + +```toml +[validation.fee_payer_policy] +allow_sol_transfers = false +# all other settings are false +``` + +4. **Allowed Programs**: Ensure the system program, token program, associated token program, and compute budget program are in the allowlist: + +```toml +allowed_programs = [ + "11111111111111111111111111111111", # System Program + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", # Token Program + "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", # Associated Token Program + "ComputeBudget111111111111111111111111111111", # Compute Budget Program +] +``` + +#### signers.toml + +The `kora/signers.toml` file configures the Kora signer. You should not need to make any changes to this file, but you can verify the following settings: + +1. **Signer Environment Variable**: Ensure the signer environment variable, `private_key_env` is set to `KORA_SIGNER_PRIVATE_KEY` (matching the env variable name in the `.env` file). + +```toml +[[signers]] +name = "main_signer" +type = "memory" +private_key_env = "KORA_SIGNER_PRIVATE_KEY" +weight = 1 +``` + + +### Step 6: Fund Accounts + +#### Devnet SOL + +Our Kora signer address will need SOL for paying transaction fees. You can airdrop devnet SOL to the Kora signer address using the Solana CLI: + +```bash +# Airdrop SOL +solana airdrop 1 --url devnet +``` + +Alternatively, you can use the [Solana Faucet](https://faucet.solana.com/) to airdrop SOL to the Kora signer address. + +#### Devnet USDC + +Your `PAYER_ADDRESS` is set in the `.env` file will need USDC for paying transaction fees. + +Get Devnet USDC from [Circle's Faucet](https://faucet.circle.com/). Make sure to select "Solana Devnet" and use your `PAYER_ADDRESS` to request USDC. + +## Running the Demo + +You'll need four terminal windows to run all components from the `docs/x402/demo` directory. + +### Terminal 1: Start Kora RPC Server + +Run the following command to start the Kora RPC server: + +```bash +pnpm run start:kora +``` + +You should see a series of logs indicating that the Kora RPC server is running, including: + +``` +INFO kora_lib::rpc_server::server: RPC server started on 0.0.0.0:8080, port 8080 +``` + +### Terminal 2: Start Facilitator + +Run the following command to start the Facilitator: + +```bash +pnpm run start:facilitator +``` + +You should see: +``` +Server listening at http://localhost:3000 +``` + +### Terminal 3: Start Protected API + +Run the following command to start the Protected API: + +```bash +pnpm run start:api +``` + +You should see: +``` +Server listening at http://localhost:4021 +``` + +### Terminal 4: Run Client Demo + +```bash +pnpm run demo +``` + +## Understanding the Implementation + +Here's what happens during a successful payment flow: + +1. **Client Request** โ†’ API returns 402 with payment requirements +2. **Payment Creation** โ†’ Client creates Solana transaction with payment +3. **Payment Submission** โ†’ Client sends request to server with payment in the `X-PAYMENT` header +4. **Verification** โ†’ Facilitator verifies via Kora's `signTransaction` +5. **Settlement** โ†’ Facilitator settles via Kora's `signAndSendTransaction` (sending the payment transaction to Solana) +6. **Access Granted** โ†’ Facilitator returns transaction signature and API returns protected content with payment receipt + +![Transaction Flow](./assets/x402-protocol-flow.png) +_Source: [x402 GitHub](https://github.com/coinbase/x402/)_ + +Let's dive into how each component works: + +- **Kora RPC** (Port 8080): Handles gasless transaction signing +- **Facilitator** (Port 3000): Bridges x402 protocol to Kora +- **Protected API** (Port 4021): Your monetized API endpoint +- **Client**: Demonstrates automatic payment flow + +### The Facilitator Wrapper/Proxy Server + +The Facilitator runs on port 3000. This is the server that handles communication with Solana (in our case, via Kora). It is used to verify and settle x402 payments. + +The facilitator (`facilitator/src/facilitator.ts`) is the bridge between x402 protocol and Kora RPC. It implements three key endpoints: + +#### 1. `/verify` Endpoint + +This endpoint: +- Receives an x402 payment payload from the Protected API server +- Extracts the Solana transaction using x402 helpers +- Uses Kora's `signTransaction` to verify validity without broadcasting +- Returns verification status, `isValid` + +#### 2. `/settle` Endpoint + +This endpoint: +- Receives the the x402 payment payload after the payment has been verified by the `/verify` endpoint +- Uses Kora's `signAndSendTransaction` to sign and broadcast the transaction +- Returns the transaction signature as proof of settlement + +#### 3. `/supported` Endpoint + +This endpoint effectively advertises the facilitator's capabilities, including: +- Supported x402 version +- Payment scheme (exact payments) +- Network (solana-devnet) +- Fee payer address which we fetch from Kora using the `getPayerSigner` method + +### The Protected API + +The API server (`api/src/api.ts`) uses x402-express middleware to protect endpoints: + +```typescript +app.use( + paymentMiddleware( + KORA_PAYER_ADDRESS, // Where payments should go + { + "GET /protected": { + price: "$0.0001", // Price in USD + network: NETWORK, // solana-devnet + }, + }, + { + url: FACILITATOR_URL, // Our facilitator wrapper + } + ), +); +``` + +The middleware: +- Intercepts requests to protected endpoints (in our case, the `/protected` endpoint) +- Returns 402 status if payment is missing +- Validates and handles payments via the facilitator +- Allows access after successful payment + +Though we are using Express, the x402 library includes middleware support for many common frameworks. See the [x402 TypeScript Packages](https://github.com/coinbase/x402/tree/main/typescript/packages) for more information. + +### The Client Application + +The client (`client/src/index.ts`) demonstrates automatic how x402 works by sending a request with a standard `fetch` call and then retrying the request with the payment wrapper: + +```typescript +// Create a signer from private key +const payer = await createSigner(NETWORK, PAYER_PRIVATE_KEY); + +// Wrap fetch with x402 payment capabilities +const fetchWithPayment = wrapFetchWithPayment(fetch, payer); + +// First attempt: Regular fetch (will fail with 402) +const expect402Response = await fetch(PROTECTED_API_URL); +console.log(`Status: ${expect402Response.status}`); // 402 + +// Second attempt: Fetch with payment wrapper (succeeds) +const response = await fetchWithPayment(PROTECTED_API_URL); +console.log(`Status: ${response.status}`); // 200 +``` + +The x402 fetch wrapper: +- Detects 402 responses +- Automatically creates payment transaction based on the protected API's payment requirements +- Signs with user's private key +- Sends payment to the facilitator for verification and processing +- Retries request with payment proof in the `x-payment-response` header +- Returns successful response + +## Wrapping Up + +Congratulations! ๐Ÿ”ฅ You've successfully implemented a complete x402 payment flow with Kora's gasless infrastructure. This demonstration shows how: + +- **x402 Protocol** enables frictionless API monetization through micropayments +- **Kora RPC** works as a facilitator for x402 payments by verifying and settling transactions +- **Users** can pay for API access without holding SOL or managing gas fees + +This architecture creates a powerful foundation for: +- AI Agent marketplaces +- Pay-per-use APIs +- Micropayment content platforms +- Usage-based SaaS pricing +- Any service requiring instant, verifiable payments + +The combination of x402 and Kora bring the power of Solana to traditional web infrastructure. + +### Keep Building + +- **Customize Pricing**: Modify the API to charge different amounts for different endpoints +- **Add Multiple Tokens**: Configure Kora to accept various SPL tokens for payment +- **Production Deployment**: Deploy to mainnet with production signers (Vault, Turnkey, or Privy) +- **Build Your Own API**: Create a real service that monetizes through x402 payments + +## Additional Resources + +#### x402 Protocol +- [x402 Documentation](https://x402.gitbook.io/x402/) +- [x402 GitHub](https://github.com/coinbase/x402) +- [x402 TypeScript SDK](https://www.npmjs.com/package/x402) + +#### Kora +- [Kora Configuration Guide](../../operators/CONFIGURATION.md) +- [Kora Signers Guide](../../operators/SIGNERS.md) +- [Kora API Reference](../../../sdks/ts/docs/README.md) + +#### Solana +- [Solana Documentation](https://solana.com/docs) +- [SPL Token Program](https://spl.solana.com/token) + +## Support + +Need help? +- Ask questions on [Solana Stack Exchange](https://solana.stackexchange.com/) with `kora` and `x402` tags +- Open issues on the [Kora GitHub repository](https://github.com/solana-foundation/kora) diff --git a/docs/x402/demo/api/package.json b/docs/x402/demo/api/package.json new file mode 100644 index 00000000..1eb3ffdf --- /dev/null +++ b/docs/x402/demo/api/package.json @@ -0,0 +1,22 @@ +{ + "name": "api", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "tsx src/api.ts" + }, + "keywords": [], + "author": "", + "license": "ISC", + "packageManager": "pnpm@10.14.0", + "dependencies": { + "dotenv": "^17.2.2", + "express": "^5.1.0", + "x402-express": "^0.6.1" + }, + "devDependencies": { + "@types/node": "^24.5.2", + "tsx": "^4.20.5" + } +} diff --git a/docs/x402/demo/api/pnpm-lock.yaml b/docs/x402/demo/api/pnpm-lock.yaml new file mode 100644 index 00000000..5d29f148 --- /dev/null +++ b/docs/x402/demo/api/pnpm-lock.yaml @@ -0,0 +1,6039 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + dotenv: + specifier: ^17.2.2 + version: 17.2.2 + express: + specifier: ^5.1.0 + version: 5.1.0 + x402-express: + specifier: ^0.6.1 + version: 0.6.1(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + devDependencies: + '@types/node': + specifier: ^24.5.2 + version: 24.5.2 + tsx: + specifier: ^4.20.5 + version: 4.20.5 + +packages: + + '@adraffy/ens-normalize@1.11.1': + resolution: {integrity: sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==} + + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} + engines: {node: '>=6.9.0'} + + '@base-org/account@1.1.1': + resolution: {integrity: sha512-IfVJPrDPhHfqXRDb89472hXkpvJuQQR7FDI9isLPHEqSYt/45whIoBxSPgZ0ssTt379VhQo4+87PWI1DoLSfAQ==} + + '@coinbase/cdp-sdk@1.38.2': + resolution: {integrity: sha512-S7+xKeZGGi+zc1PotTeQ5Pvn5a0e4ikT+iMm8pPfPjObNlDEUKLei4cmRHON1wXgql0oDXn30YKTo1LQWxXvDw==} + + '@coinbase/wallet-sdk@3.9.3': + resolution: {integrity: sha512-N/A2DRIf0Y3PHc1XAMvbBUu4zisna6qAdqABMZwBMNEfWrXpAwx16pZGkYCLGE+Rvv1edbcB2LYDRnACNcmCiw==} + + '@coinbase/wallet-sdk@4.3.6': + resolution: {integrity: sha512-4q8BNG1ViL4mSAAvPAtpwlOs1gpC+67eQtgIwNvT3xyeyFFd+guwkc8bcX5rTmQhXpqnhzC4f0obACbP9CqMSA==} + + '@ecies/ciphers@0.2.4': + resolution: {integrity: sha512-t+iX+Wf5nRKyNzk8dviW3Ikb/280+aEJAnw9YXvCp2tYGPSkMki+NRY+8aNLmVFv3eNtMdvViPNOPxS8SZNP+w==} + engines: {bun: '>=1', deno: '>=2', node: '>=16'} + peerDependencies: + '@noble/ciphers': ^1.0.0 + + '@esbuild/aix-ppc64@0.25.10': + resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.10': + resolution: {integrity: sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.10': + resolution: {integrity: sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.10': + resolution: {integrity: sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.10': + resolution: {integrity: sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.10': + resolution: {integrity: sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.10': + resolution: {integrity: sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.10': + resolution: {integrity: sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.10': + resolution: {integrity: sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.10': + resolution: {integrity: sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.10': + resolution: {integrity: sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.10': + resolution: {integrity: sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.10': + resolution: {integrity: sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.10': + resolution: {integrity: sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.10': + resolution: {integrity: sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.10': + resolution: {integrity: sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.10': + resolution: {integrity: sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.10': + resolution: {integrity: sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.10': + resolution: {integrity: sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.10': + resolution: {integrity: sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.10': + resolution: {integrity: sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.10': + resolution: {integrity: sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.10': + resolution: {integrity: sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.10': + resolution: {integrity: sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.10': + resolution: {integrity: sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.10': + resolution: {integrity: sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@ethereumjs/common@3.2.0': + resolution: {integrity: sha512-pksvzI0VyLgmuEF2FA/JR/4/y6hcPq8OUail3/AvycBaW1d5VSauOZzqGvJ3RTmR4MU35lWE8KseKOsEhrFRBA==} + + '@ethereumjs/rlp@4.0.1': + resolution: {integrity: sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw==} + engines: {node: '>=14'} + hasBin: true + + '@ethereumjs/tx@4.2.0': + resolution: {integrity: sha512-1nc6VO4jtFd172BbSnTnDQVr9IYBFl1y4xPzZdtkrkKIncBCkdbgfdRV+MiTkJYAtTxvV12GRZLqBFT1PNK6Yw==} + engines: {node: '>=14'} + + '@ethereumjs/util@8.1.0': + resolution: {integrity: sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA==} + engines: {node: '>=14'} + + '@gemini-wallet/core@0.2.0': + resolution: {integrity: sha512-vv9aozWnKrrPWQ3vIFcWk7yta4hQW1Ie0fsNNPeXnjAxkbXr2hqMagEptLuMxpEP2W3mnRu05VDNKzcvAuuZDw==} + peerDependencies: + viem: '>=2.0.0' + + '@lit-labs/ssr-dom-shim@1.4.0': + resolution: {integrity: sha512-ficsEARKnmmW5njugNYKipTm4SFnbik7CXtoencDZzmzo/dQ+2Q0bgkzJuoJP20Aj0F+izzJjOqsnkd6F/o1bw==} + + '@lit/reactive-element@2.1.1': + resolution: {integrity: sha512-N+dm5PAYdQ8e6UlywyyrgI2t++wFGXfHx+dSJ1oBrg6FAxUj40jId++EaRm80MKX5JnlH1sBsyZ5h0bcZKemCg==} + + '@metamask/eth-json-rpc-provider@1.0.1': + resolution: {integrity: sha512-whiUMPlAOrVGmX8aKYVPvlKyG4CpQXiNNyt74vE1xb5sPvmx5oA7B/kOi/JdBvhGQq97U1/AVdXEdk2zkP8qyA==} + engines: {node: '>=14.0.0'} + + '@metamask/json-rpc-engine@7.3.3': + resolution: {integrity: sha512-dwZPq8wx9yV3IX2caLi9q9xZBw2XeIoYqdyihDDDpuHVCEiqadJLwqM3zy+uwf6F1QYQ65A8aOMQg1Uw7LMLNg==} + engines: {node: '>=16.0.0'} + + '@metamask/json-rpc-engine@8.0.2': + resolution: {integrity: sha512-IoQPmql8q7ABLruW7i4EYVHWUbF74yrp63bRuXV5Zf9BQwcn5H9Ww1eLtROYvI1bUXwOiHZ6qT5CWTrDc/t/AA==} + engines: {node: '>=16.0.0'} + + '@metamask/json-rpc-middleware-stream@7.0.2': + resolution: {integrity: sha512-yUdzsJK04Ev98Ck4D7lmRNQ8FPioXYhEUZOMS01LXW8qTvPGiRVXmVltj2p4wrLkh0vW7u6nv0mNl5xzC5Qmfg==} + engines: {node: '>=16.0.0'} + + '@metamask/object-multiplex@2.1.0': + resolution: {integrity: sha512-4vKIiv0DQxljcXwfpnbsXcfa5glMj5Zg9mqn4xpIWqkv6uJ2ma5/GtUfLFSxhlxnR8asRMv8dDmWya1Tc1sDFA==} + engines: {node: ^16.20 || ^18.16 || >=20} + + '@metamask/onboarding@1.0.1': + resolution: {integrity: sha512-FqHhAsCI+Vacx2qa5mAFcWNSrTcVGMNjzxVgaX8ECSny/BJ9/vgXP9V7WF/8vb9DltPeQkxr+Fnfmm6GHfmdTQ==} + + '@metamask/providers@16.1.0': + resolution: {integrity: sha512-znVCvux30+3SaUwcUGaSf+pUckzT5ukPRpcBmy+muBLC0yaWnBcvDqGfcsw6CBIenUdFrVoAFa8B6jsuCY/a+g==} + engines: {node: ^18.18 || >=20} + + '@metamask/rpc-errors@6.4.0': + resolution: {integrity: sha512-1ugFO1UoirU2esS3juZanS/Fo8C8XYocCuBpfZI5N7ECtoG+zu0wF+uWZASik6CkO6w9n/Iebt4iI4pT0vptpg==} + engines: {node: '>=16.0.0'} + + '@metamask/rpc-errors@7.0.2': + resolution: {integrity: sha512-YYYHsVYd46XwY2QZzpGeU4PSdRhHdxnzkB8piWGvJW2xbikZ3R+epAYEL4q/K8bh9JPTucsUdwRFnACor1aOYw==} + engines: {node: ^18.20 || ^20.17 || >=22} + + '@metamask/safe-event-emitter@2.0.0': + resolution: {integrity: sha512-/kSXhY692qiV1MXu6EeOZvg5nECLclxNXcKCxJ3cXQgYuRymRHpdx/t7JXfsK+JLjwA1e1c1/SBrlQYpusC29Q==} + + '@metamask/safe-event-emitter@3.1.2': + resolution: {integrity: sha512-5yb2gMI1BDm0JybZezeoX/3XhPDOtTbcFvpTXM9kxsoZjPZFh4XciqRbpD6N86HYZqWDhEaKUDuOyR0sQHEjMA==} + engines: {node: '>=12.0.0'} + + '@metamask/sdk-analytics@0.0.5': + resolution: {integrity: sha512-fDah+keS1RjSUlC8GmYXvx6Y26s3Ax1U9hGpWb6GSY5SAdmTSIqp2CvYy6yW0WgLhnYhW+6xERuD0eVqV63QIQ==} + + '@metamask/sdk-communication-layer@0.33.1': + resolution: {integrity: sha512-0bI9hkysxcfbZ/lk0T2+aKVo1j0ynQVTuB3sJ5ssPWlz+Z3VwveCkP1O7EVu1tsVVCb0YV5WxK9zmURu2FIiaA==} + peerDependencies: + cross-fetch: ^4.0.0 + eciesjs: '*' + eventemitter2: ^6.4.9 + readable-stream: ^3.6.2 + socket.io-client: ^4.5.1 + + '@metamask/sdk-install-modal-web@0.32.1': + resolution: {integrity: sha512-MGmAo6qSjf1tuYXhCu2EZLftq+DSt5Z7fsIKr2P+lDgdTPWgLfZB1tJKzNcwKKOdf6q9Qmmxn7lJuI/gq5LrKw==} + + '@metamask/sdk@0.33.1': + resolution: {integrity: sha512-1mcOQVGr9rSrVcbKPNVzbZ8eCl1K0FATsYH3WJ/MH4WcZDWGECWrXJPNMZoEAkLxWiMe8jOQBumg2pmcDa9zpQ==} + + '@metamask/superstruct@3.2.1': + resolution: {integrity: sha512-fLgJnDOXFmuVlB38rUN5SmU7hAFQcCjrg3Vrxz67KTY7YHFnSNEKvX4avmEBdOI0yTCxZjwMCFEqsC8k2+Wd3g==} + engines: {node: '>=16.0.0'} + + '@metamask/utils@11.8.1': + resolution: {integrity: sha512-DIbsNUyqWLFgqJlZxi1OOCMYvI23GqFCvNJAtzv8/WXWzJfnJnvp1M24j7VvUe3URBi3S86UgQ7+7aWU9p/cnQ==} + engines: {node: ^18.18 || ^20.14 || >=22} + + '@metamask/utils@5.0.2': + resolution: {integrity: sha512-yfmE79bRQtnMzarnKfX7AEJBwFTxvTyw3nBQlu/5rmGXrjAeAMltoGxO62TFurxrQAFMNa/fEjIHNvungZp0+g==} + engines: {node: '>=14.0.0'} + + '@metamask/utils@8.5.0': + resolution: {integrity: sha512-I6bkduevXb72TIM9q2LRO63JSsF9EXduh3sBr9oybNX2hNNpr/j1tEjXrsG0Uabm4MJ1xkGAQEMwifvKZIkyxQ==} + engines: {node: '>=16.0.0'} + + '@metamask/utils@9.3.0': + resolution: {integrity: sha512-w8CVbdkDrVXFJbfBSlDfafDR6BAkpDmv1bC1UJVCoVny5tW2RKAdn9i68Xf7asYT4TnUhl/hN4zfUiKQq9II4g==} + engines: {node: '>=16.0.0'} + + '@noble/ciphers@1.2.1': + resolution: {integrity: sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==} + engines: {node: ^14.21.3 || >=16} + + '@noble/ciphers@1.3.0': + resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/curves@1.4.2': + resolution: {integrity: sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==} + + '@noble/curves@1.8.0': + resolution: {integrity: sha512-j84kjAbzEnQHaSIhRPUmB3/eVXu2k3dKPl2LOrR8fSOIL+89U+7lV117EWHtq/GHM3ReGHM46iRBdZfpc4HRUQ==} + engines: {node: ^14.21.3 || >=16} + + '@noble/curves@1.8.1': + resolution: {integrity: sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==} + engines: {node: ^14.21.3 || >=16} + + '@noble/curves@1.9.1': + resolution: {integrity: sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==} + engines: {node: ^14.21.3 || >=16} + + '@noble/curves@1.9.7': + resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.4.0': + resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} + engines: {node: '>= 16'} + + '@noble/hashes@1.7.0': + resolution: {integrity: sha512-HXydb0DgzTpDPwbVeDGCG1gIu7X6+AuU6Zl6av/E/KG8LMsvPntvq+w17CHRpKBmN6Ybdrt1eP3k4cj8DJa78w==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.7.1': + resolution: {integrity: sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@paulmillr/qr@0.2.1': + resolution: {integrity: sha512-IHnV6A+zxU7XwmKFinmYjUcwlyK9+xkG3/s9KcQhI9BjQKycrJ1JRO+FbNYPwZiPKW3je/DR0k7w8/gLa5eaxQ==} + deprecated: 'The package is now available as "qr": npm install qr' + + '@reown/appkit-common@1.7.8': + resolution: {integrity: sha512-ridIhc/x6JOp7KbDdwGKY4zwf8/iK8EYBl+HtWrruutSLwZyVi5P8WaZa+8iajL6LcDcDF7LoyLwMTym7SRuwQ==} + + '@reown/appkit-controllers@1.7.8': + resolution: {integrity: sha512-IdXlJlivrlj6m63VsGLsjtPHHsTWvKGVzWIP1fXZHVqmK+rZCBDjCi9j267Rb9/nYRGHWBtlFQhO8dK35WfeDA==} + + '@reown/appkit-pay@1.7.8': + resolution: {integrity: sha512-OSGQ+QJkXx0FEEjlpQqIhT8zGJKOoHzVnyy/0QFrl3WrQTjCzg0L6+i91Ad5Iy1zb6V5JjqtfIFpRVRWN4M3pw==} + + '@reown/appkit-polyfills@1.7.8': + resolution: {integrity: sha512-W/kq786dcHHAuJ3IV2prRLEgD/2iOey4ueMHf1sIFjhhCGMynMkhsOhQMUH0tzodPqUgAC494z4bpIDYjwWXaA==} + + '@reown/appkit-scaffold-ui@1.7.8': + resolution: {integrity: sha512-RCeHhAwOrIgcvHwYlNWMcIDibdI91waaoEYBGw71inE0kDB8uZbE7tE6DAXJmDkvl0qPh+DqlC4QbJLF1FVYdQ==} + + '@reown/appkit-ui@1.7.8': + resolution: {integrity: sha512-1hjCKjf6FLMFzrulhl0Y9Vb9Fu4royE+SXCPSWh4VhZhWqlzUFc7kutnZKx8XZFVQH4pbBvY62SpRC93gqoHow==} + + '@reown/appkit-utils@1.7.8': + resolution: {integrity: sha512-8X7UvmE8GiaoitCwNoB86pttHgQtzy4ryHZM9kQpvjQ0ULpiER44t1qpVLXNM4X35O0v18W0Dk60DnYRMH2WRw==} + peerDependencies: + valtio: 1.13.2 + + '@reown/appkit-wallet@1.7.8': + resolution: {integrity: sha512-kspz32EwHIOT/eg/ZQbFPxgXq0B/olDOj3YMu7gvLEFz4xyOFd/wgzxxAXkp5LbG4Cp++s/elh79rVNmVFdB9A==} + + '@reown/appkit@1.7.8': + resolution: {integrity: sha512-51kTleozhA618T1UvMghkhKfaPcc9JlKwLJ5uV+riHyvSoWPKPRIa5A6M1Wano5puNyW0s3fwywhyqTHSilkaA==} + + '@safe-global/safe-apps-provider@0.18.6': + resolution: {integrity: sha512-4LhMmjPWlIO8TTDC2AwLk44XKXaK6hfBTWyljDm0HQ6TWlOEijVWNrt2s3OCVMSxlXAcEzYfqyu1daHZooTC2Q==} + + '@safe-global/safe-apps-sdk@9.1.0': + resolution: {integrity: sha512-N5p/ulfnnA2Pi2M3YeWjULeWbjo7ei22JwU/IXnhoHzKq3pYCN6ynL9mJBOlvDVv892EgLPCWCOwQk/uBT2v0Q==} + + '@safe-global/safe-gateway-typescript-sdk@3.23.1': + resolution: {integrity: sha512-6ORQfwtEJYpalCeVO21L4XXGSdbEMfyp2hEv6cP82afKXSwvse6d3sdelgaPWUxHIsFRkWvHDdzh8IyyKHZKxw==} + engines: {node: '>=16'} + + '@scure/base@1.1.9': + resolution: {integrity: sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==} + + '@scure/base@1.2.6': + resolution: {integrity: sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==} + + '@scure/bip32@1.4.0': + resolution: {integrity: sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==} + + '@scure/bip32@1.6.2': + resolution: {integrity: sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==} + + '@scure/bip32@1.7.0': + resolution: {integrity: sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==} + + '@scure/bip39@1.3.0': + resolution: {integrity: sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==} + + '@scure/bip39@1.5.4': + resolution: {integrity: sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==} + + '@scure/bip39@1.6.0': + resolution: {integrity: sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==} + + '@socket.io/component-emitter@3.1.2': + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + + '@solana-program/compute-budget@0.8.0': + resolution: {integrity: sha512-qPKxdxaEsFxebZ4K5RPuy7VQIm/tfJLa1+Nlt3KNA8EYQkz9Xm8htdoEaXVrer9kpgzzp9R3I3Bh6omwCM06tQ==} + peerDependencies: + '@solana/kit': ^2.1.0 + + '@solana-program/token-2022@0.4.2': + resolution: {integrity: sha512-zIpR5t4s9qEU3hZKupzIBxJ6nUV5/UVyIT400tu9vT1HMs5JHxaTTsb5GUhYjiiTvNwU0MQavbwc4Dl29L0Xvw==} + peerDependencies: + '@solana/kit': ^2.1.0 + '@solana/sysvars': ^2.1.0 + + '@solana-program/token@0.5.1': + resolution: {integrity: sha512-bJvynW5q9SFuVOZ5vqGVkmaPGA0MCC+m9jgJj1nk5m20I389/ms69ASnhWGoOPNcie7S9OwBX0gTj2fiyWpfag==} + peerDependencies: + '@solana/kit': ^2.1.0 + + '@solana/accounts@2.3.0': + resolution: {integrity: sha512-QgQTj404Z6PXNOyzaOpSzjgMOuGwG8vC66jSDB+3zHaRcEPRVRd2sVSrd1U6sHtnV3aiaS6YyDuPQMheg4K2jw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/addresses@2.3.0': + resolution: {integrity: sha512-ypTNkY2ZaRFpHLnHAgaW8a83N0/WoqdFvCqf4CQmnMdFsZSdC7qOwcbd7YzdaQn9dy+P2hybewzB+KP7LutxGA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/assertions@2.3.0': + resolution: {integrity: sha512-Ekoet3khNg3XFLN7MIz8W31wPQISpKUGDGTylLptI+JjCDWx3PIa88xjEMqFo02WJ8sBj2NLV64Xg1sBcsHjZQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/buffer-layout-utils@0.2.0': + resolution: {integrity: sha512-szG4sxgJGktbuZYDg2FfNmkMi0DYQoVjN2h7ta1W1hPrwzarcFLBq9UpX1UjNXsNpT9dn+chgprtWGioUAr4/g==} + engines: {node: '>= 10'} + + '@solana/buffer-layout@4.0.1': + resolution: {integrity: sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==} + engines: {node: '>=5.10'} + + '@solana/codecs-core@2.0.0-rc.1': + resolution: {integrity: sha512-bauxqMfSs8EHD0JKESaNmNuNvkvHSuN3bbWAF5RjOfDu2PugxHrvRebmYauvSumZ3cTfQ4HJJX6PG5rN852qyQ==} + peerDependencies: + typescript: '>=5' + + '@solana/codecs-core@2.3.0': + resolution: {integrity: sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/codecs-data-structures@2.0.0-rc.1': + resolution: {integrity: sha512-rinCv0RrAVJ9rE/rmaibWJQxMwC5lSaORSZuwjopSUE6T0nb/MVg6Z1siNCXhh/HFTOg0l8bNvZHgBcN/yvXog==} + peerDependencies: + typescript: '>=5' + + '@solana/codecs-data-structures@2.3.0': + resolution: {integrity: sha512-qvU5LE5DqEdYMYgELRHv+HMOx73sSoV1ZZkwIrclwUmwTbTaH8QAJURBj0RhQ/zCne7VuLLOZFFGv6jGigWhSw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/codecs-numbers@2.0.0-rc.1': + resolution: {integrity: sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ==} + peerDependencies: + typescript: '>=5' + + '@solana/codecs-numbers@2.3.0': + resolution: {integrity: sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/codecs-strings@2.0.0-rc.1': + resolution: {integrity: sha512-9/wPhw8TbGRTt6mHC4Zz1RqOnuPTqq1Nb4EyuvpZ39GW6O2t2Q7Q0XxiB3+BdoEjwA2XgPw6e2iRfvYgqty44g==} + peerDependencies: + fastestsmallesttextencoderdecoder: ^1.0.22 + typescript: '>=5' + + '@solana/codecs-strings@2.3.0': + resolution: {integrity: sha512-y5pSBYwzVziXu521hh+VxqUtp0hYGTl1eWGoc1W+8mdvBdC1kTqm/X7aYQw33J42hw03JjryvYOvmGgk3Qz/Ug==} + engines: {node: '>=20.18.0'} + peerDependencies: + fastestsmallesttextencoderdecoder: ^1.0.22 + typescript: '>=5.3.3' + + '@solana/codecs@2.0.0-rc.1': + resolution: {integrity: sha512-qxoR7VybNJixV51L0G1RD2boZTcxmwUWnKCaJJExQ5qNKwbpSyDdWfFJfM5JhGyKe9DnPVOZB+JHWXnpbZBqrQ==} + peerDependencies: + typescript: '>=5' + + '@solana/codecs@2.3.0': + resolution: {integrity: sha512-JVqGPkzoeyU262hJGdH64kNLH0M+Oew2CIPOa/9tR3++q2pEd4jU2Rxdfye9sd0Ce3XJrR5AIa8ZfbyQXzjh+g==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/errors@2.0.0-rc.1': + resolution: {integrity: sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ==} + hasBin: true + peerDependencies: + typescript: '>=5' + + '@solana/errors@2.3.0': + resolution: {integrity: sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ==} + engines: {node: '>=20.18.0'} + hasBin: true + peerDependencies: + typescript: '>=5.3.3' + + '@solana/fast-stable-stringify@2.3.0': + resolution: {integrity: sha512-KfJPrMEieUg6D3hfQACoPy0ukrAV8Kio883llt/8chPEG3FVTX9z/Zuf4O01a15xZmBbmQ7toil2Dp0sxMJSxw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/functional@2.3.0': + resolution: {integrity: sha512-AgsPh3W3tE+nK3eEw/W9qiSfTGwLYEvl0rWaxHht/lRcuDVwfKRzeSa5G79eioWFFqr+pTtoCr3D3OLkwKz02Q==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/instructions@2.3.0': + resolution: {integrity: sha512-PLMsmaIKu7hEAzyElrk2T7JJx4D+9eRwebhFZpy2PXziNSmFF929eRHKUsKqBFM3cYR1Yy3m6roBZfA+bGE/oQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/keys@2.3.0': + resolution: {integrity: sha512-ZVVdga79pNH+2pVcm6fr2sWz9HTwfopDVhYb0Lh3dh+WBmJjwkabXEIHey2rUES7NjFa/G7sV8lrUn/v8LDCCQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/kit@2.3.0': + resolution: {integrity: sha512-sb6PgwoW2LjE5oTFu4lhlS/cGt/NB3YrShEyx7JgWFWysfgLdJnhwWThgwy/4HjNsmtMrQGWVls0yVBHcMvlMQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/nominal-types@2.3.0': + resolution: {integrity: sha512-uKlMnlP4PWW5UTXlhKM8lcgIaNj8dvd8xO4Y9l+FVvh9RvW2TO0GwUO6JCo7JBzCB0PSqRJdWWaQ8pu1Ti/OkA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/options@2.0.0-rc.1': + resolution: {integrity: sha512-mLUcR9mZ3qfHlmMnREdIFPf9dpMc/Bl66tLSOOWxw4ml5xMT2ohFn7WGqoKcu/UHkT9CrC6+amEdqCNvUqI7AA==} + peerDependencies: + typescript: '>=5' + + '@solana/options@2.3.0': + resolution: {integrity: sha512-PPnnZBRCWWoZQ11exPxf//DRzN2C6AoFsDI/u2AsQfYih434/7Kp4XLpfOMT/XESi+gdBMFNNfbES5zg3wAIkw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/programs@2.3.0': + resolution: {integrity: sha512-UXKujV71VCI5uPs+cFdwxybtHZAIZyQkqDiDnmK+DawtOO9mBn4Nimdb/6RjR2CXT78mzO9ZCZ3qfyX+ydcB7w==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/promises@2.3.0': + resolution: {integrity: sha512-GjVgutZKXVuojd9rWy1PuLnfcRfqsaCm7InCiZc8bqmJpoghlyluweNc7ml9Y5yQn1P2IOyzh9+p/77vIyNybQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-api@2.3.0': + resolution: {integrity: sha512-UUdiRfWoyYhJL9PPvFeJr4aJ554ob2jXcpn4vKmRVn9ire0sCbpQKYx6K8eEKHZWXKrDW8IDspgTl0gT/aJWVg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-parsed-types@2.3.0': + resolution: {integrity: sha512-B5pHzyEIbBJf9KHej+zdr5ZNAdSvu7WLU2lOUPh81KHdHQs6dEb310LGxcpCc7HVE8IEdO20AbckewDiAN6OCg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-spec-types@2.3.0': + resolution: {integrity: sha512-xQsb65lahjr8Wc9dMtP7xa0ZmDS8dOE2ncYjlvfyw/h4mpdXTUdrSMi6RtFwX33/rGuztQ7Hwaid5xLNSLvsFQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-spec@2.3.0': + resolution: {integrity: sha512-fA2LMX4BMixCrNB2n6T83AvjZ3oUQTu7qyPLyt8gHQaoEAXs8k6GZmu6iYcr+FboQCjUmRPgMaABbcr9j2J9Sw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-subscriptions-api@2.3.0': + resolution: {integrity: sha512-9mCjVbum2Hg9KGX3LKsrI5Xs0KX390lS+Z8qB80bxhar6MJPugqIPH8uRgLhCW9GN3JprAfjRNl7our8CPvsPQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-subscriptions-channel-websocket@2.3.0': + resolution: {integrity: sha512-2oL6ceFwejIgeWzbNiUHI2tZZnaOxNTSerszcin7wYQwijxtpVgUHiuItM/Y70DQmH9sKhmikQp+dqeGalaJxw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + ws: ^8.18.0 + + '@solana/rpc-subscriptions-spec@2.3.0': + resolution: {integrity: sha512-rdmVcl4PvNKQeA2l8DorIeALCgJEMSu7U8AXJS1PICeb2lQuMeaR+6cs/iowjvIB0lMVjYN2sFf6Q3dJPu6wWg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-subscriptions@2.3.0': + resolution: {integrity: sha512-Uyr10nZKGVzvCOqwCZgwYrzuoDyUdwtgQRefh13pXIrdo4wYjVmoLykH49Omt6abwStB0a4UL5gX9V4mFdDJZg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-transformers@2.3.0': + resolution: {integrity: sha512-UuHYK3XEpo9nMXdjyGKkPCOr7WsZsxs7zLYDO1A5ELH3P3JoehvrDegYRAGzBS2VKsfApZ86ZpJToP0K3PhmMA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-transport-http@2.3.0': + resolution: {integrity: sha512-HFKydmxGw8nAF5N+S0NLnPBDCe5oMDtI2RAmW8DMqP4U3Zxt2XWhvV1SNkAldT5tF0U1vP+is6fHxyhk4xqEvg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-types@2.3.0': + resolution: {integrity: sha512-O09YX2hED2QUyGxrMOxQ9GzH1LlEwwZWu69QbL4oYmIf6P5dzEEHcqRY6L1LsDVqc/dzAdEs/E1FaPrcIaIIPw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc@2.3.0': + resolution: {integrity: sha512-ZWN76iNQAOCpYC7yKfb3UNLIMZf603JckLKOOLTHuy9MZnTN8XV6uwvDFhf42XvhglgUjGCEnbUqWtxQ9pa/pQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/signers@2.3.0': + resolution: {integrity: sha512-OSv6fGr/MFRx6J+ZChQMRqKNPGGmdjkqarKkRzkwmv7v8quWsIRnJT5EV8tBy3LI4DLO/A8vKiNSPzvm1TdaiQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/spl-token-group@0.0.7': + resolution: {integrity: sha512-V1N/iX7Cr7H0uazWUT2uk27TMqlqedpXHRqqAbVO2gvmJyT0E0ummMEAVQeXZ05ZhQ/xF39DLSdBp90XebWEug==} + engines: {node: '>=16'} + peerDependencies: + '@solana/web3.js': ^1.95.3 + + '@solana/spl-token-metadata@0.1.6': + resolution: {integrity: sha512-7sMt1rsm/zQOQcUWllQX9mD2O6KhSAtY1hFR2hfFwgqfFWzSY9E9GDvFVNYUI1F0iQKcm6HmePU9QbKRXTEBiA==} + engines: {node: '>=16'} + peerDependencies: + '@solana/web3.js': ^1.95.3 + + '@solana/spl-token@0.4.14': + resolution: {integrity: sha512-u09zr96UBpX4U685MnvQsNzlvw9TiY005hk1vJmJr7gMJldoPG1eYU5/wNEyOA5lkMLiR/gOi9SFD4MefOYEsA==} + engines: {node: '>=16'} + peerDependencies: + '@solana/web3.js': ^1.95.5 + + '@solana/subscribable@2.3.0': + resolution: {integrity: sha512-DkgohEDbMkdTWiKAoatY02Njr56WXx9e/dKKfmne8/Ad6/2llUIrax78nCdlvZW9quXMaXPTxZvdQqo9N669Og==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/sysvars@2.3.0': + resolution: {integrity: sha512-LvjADZrpZ+CnhlHqfI5cmsRzX9Rpyb1Ox2dMHnbsRNzeKAMhu9w4ZBIaeTdO322zsTr509G1B+k2ABD3whvUBA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/transaction-confirmation@2.3.0': + resolution: {integrity: sha512-UiEuiHCfAAZEKdfne/XljFNJbsKAe701UQHKXEInYzIgBjRbvaeYZlBmkkqtxwcasgBTOmEaEKT44J14N9VZDw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/transaction-messages@2.3.0': + resolution: {integrity: sha512-bgqvWuy3MqKS5JdNLH649q+ngiyOu5rGS3DizSnWwYUd76RxZl1kN6CoqHSrrMzFMvis6sck/yPGG3wqrMlAww==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/transactions@2.3.0': + resolution: {integrity: sha512-LnTvdi8QnrQtuEZor5Msje61sDpPstTVwKg4y81tNxDhiyomjuvnSNLAq6QsB9gIxUqbNzPZgOG9IU4I4/Uaug==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/web3.js@1.98.4': + resolution: {integrity: sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==} + + '@swc/helpers@0.5.17': + resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} + + '@tanstack/query-core@5.90.2': + resolution: {integrity: sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==} + + '@tanstack/react-query@5.90.2': + resolution: {integrity: sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw==} + peerDependencies: + react: ^18 || ^19 + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/lodash@4.17.20': + resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node@12.20.55': + resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + + '@types/node@24.5.2': + resolution: {integrity: sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@types/uuid@8.3.4': + resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==} + + '@types/ws@7.4.7': + resolution: {integrity: sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@wagmi/connectors@5.11.1': + resolution: {integrity: sha512-2jbx6z8rk/srQdbnKF2viCc3ke02HSMQU5EZS90jWoJ1t4+Fn3+bhphgVeJ1LnVFWsuEBSuIcY63iic4emwGfg==} + peerDependencies: + '@wagmi/core': 2.21.1 + typescript: '>=5.0.4' + viem: 2.x + peerDependenciesMeta: + typescript: + optional: true + + '@wagmi/core@2.21.1': + resolution: {integrity: sha512-uG0Cujm24acrFYqbi1RGw9MRMLTGVKvyv5OAJT+2pkcasM9PP8eJCzhlvUxK9IYVXXnbaAT8JX/rXEurOSM6mg==} + peerDependencies: + '@tanstack/query-core': '>=5.0.0' + typescript: '>=5.0.4' + viem: 2.x + peerDependenciesMeta: + '@tanstack/query-core': + optional: true + typescript: + optional: true + + '@walletconnect/core@2.21.0': + resolution: {integrity: sha512-o6R7Ua4myxR8aRUAJ1z3gT9nM+jd2B2mfamu6arzy1Cc6vi10fIwFWb6vg3bC8xJ6o9H3n/cN5TOW3aA9Y1XVw==} + engines: {node: '>=18'} + + '@walletconnect/core@2.21.1': + resolution: {integrity: sha512-Tp4MHJYcdWD846PH//2r+Mu4wz1/ZU/fr9av1UWFiaYQ2t2TPLDiZxjLw54AAEpMqlEHemwCgiRiAmjR1NDdTQ==} + engines: {node: '>=18'} + + '@walletconnect/environment@1.0.1': + resolution: {integrity: sha512-T426LLZtHj8e8rYnKfzsw1aG6+M0BT1ZxayMdv/p8yM0MU+eJDISqNY3/bccxRr4LrF9csq02Rhqt08Ibl0VRg==} + + '@walletconnect/ethereum-provider@2.21.1': + resolution: {integrity: sha512-SSlIG6QEVxClgl1s0LMk4xr2wg4eT3Zn/Hb81IocyqNSGfXpjtawWxKxiC5/9Z95f1INyBD6MctJbL/R1oBwIw==} + + '@walletconnect/events@1.0.1': + resolution: {integrity: sha512-NPTqaoi0oPBVNuLv7qPaJazmGHs5JGyO8eEAk5VGKmJzDR7AHzD4k6ilox5kxk1iwiOnFopBOOMLs86Oa76HpQ==} + + '@walletconnect/heartbeat@1.2.2': + resolution: {integrity: sha512-uASiRmC5MwhuRuf05vq4AT48Pq8RMi876zV8rr8cV969uTOzWdB/k+Lj5yI2PBtB1bGQisGen7MM1GcZlQTBXw==} + + '@walletconnect/jsonrpc-http-connection@1.0.8': + resolution: {integrity: sha512-+B7cRuaxijLeFDJUq5hAzNyef3e3tBDIxyaCNmFtjwnod5AGis3RToNqzFU33vpVcxFhofkpE7Cx+5MYejbMGw==} + + '@walletconnect/jsonrpc-provider@1.0.14': + resolution: {integrity: sha512-rtsNY1XqHvWj0EtITNeuf8PHMvlCLiS3EjQL+WOkxEOA4KPxsohFnBDeyPYiNm4ZvkQdLnece36opYidmtbmow==} + + '@walletconnect/jsonrpc-types@1.0.4': + resolution: {integrity: sha512-P6679fG/M+wuWg9TY8mh6xFSdYnFyFjwFelxyISxMDrlbXokorEVXYOxiqEbrU3x1BmBoCAJJ+vtEaEoMlpCBQ==} + + '@walletconnect/jsonrpc-utils@1.0.8': + resolution: {integrity: sha512-vdeb03bD8VzJUL6ZtzRYsFMq1eZQcM3EAzT0a3st59dyLfJ0wq+tKMpmGH7HlB7waD858UWgfIcudbPFsbzVdw==} + + '@walletconnect/jsonrpc-ws-connection@1.0.16': + resolution: {integrity: sha512-G81JmsMqh5nJheE1mPst1W0WfVv0SG3N7JggwLLGnI7iuDZJq8cRJvQwLGKHn5H1WTW7DEPCo00zz5w62AbL3Q==} + + '@walletconnect/keyvaluestorage@1.1.1': + resolution: {integrity: sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA==} + peerDependencies: + '@react-native-async-storage/async-storage': 1.x + peerDependenciesMeta: + '@react-native-async-storage/async-storage': + optional: true + + '@walletconnect/logger@2.1.2': + resolution: {integrity: sha512-aAb28I3S6pYXZHQm5ESB+V6rDqIYfsnHaQyzFbwUUBFY4H0OXx/YtTl8lvhUNhMMfb9UxbwEBS253TlXUYJWSw==} + + '@walletconnect/relay-api@1.0.11': + resolution: {integrity: sha512-tLPErkze/HmC9aCmdZOhtVmYZq1wKfWTJtygQHoWtgg722Jd4homo54Cs4ak2RUFUZIGO2RsOpIcWipaua5D5Q==} + + '@walletconnect/relay-auth@1.1.0': + resolution: {integrity: sha512-qFw+a9uRz26jRCDgL7Q5TA9qYIgcNY8jpJzI1zAWNZ8i7mQjaijRnWFKsCHAU9CyGjvt6RKrRXyFtFOpWTVmCQ==} + + '@walletconnect/safe-json@1.0.2': + resolution: {integrity: sha512-Ogb7I27kZ3LPC3ibn8ldyUr5544t3/STow9+lzz7Sfo808YD7SBWk7SAsdBFlYgP2zDRy2hS3sKRcuSRM0OTmA==} + + '@walletconnect/sign-client@2.21.0': + resolution: {integrity: sha512-z7h+PeLa5Au2R591d/8ZlziE0stJvdzP9jNFzFolf2RG/OiXulgFKum8PrIyXy+Rg2q95U9nRVUF9fWcn78yBA==} + + '@walletconnect/sign-client@2.21.1': + resolution: {integrity: sha512-QaXzmPsMnKGV6tc4UcdnQVNOz4zyXgarvdIQibJ4L3EmLat73r5ZVl4c0cCOcoaV7rgM9Wbphgu5E/7jNcd3Zg==} + + '@walletconnect/time@1.0.2': + resolution: {integrity: sha512-uzdd9woDcJ1AaBZRhqy5rNC9laqWGErfc4dxA9a87mPdKOgWMD85mcFo9dIYIts/Jwocfwn07EC6EzclKubk/g==} + + '@walletconnect/types@2.21.0': + resolution: {integrity: sha512-ll+9upzqt95ZBWcfkOszXZkfnpbJJ2CmxMfGgE5GmhdxxxCcO5bGhXkI+x8OpiS555RJ/v/sXJYMSOLkmu4fFw==} + + '@walletconnect/types@2.21.1': + resolution: {integrity: sha512-UeefNadqP6IyfwWC1Yi7ux+ljbP2R66PLfDrDm8izmvlPmYlqRerJWJvYO4t0Vvr9wrG4Ko7E0c4M7FaPKT/sQ==} + + '@walletconnect/universal-provider@2.21.0': + resolution: {integrity: sha512-mtUQvewt+X0VBQay/xOJBvxsB3Xsm1lTwFjZ6WUwSOTR1X+FNb71hSApnV5kbsdDIpYPXeQUbGt2se1n5E5UBg==} + + '@walletconnect/universal-provider@2.21.1': + resolution: {integrity: sha512-Wjx9G8gUHVMnYfxtasC9poGm8QMiPCpXpbbLFT+iPoQskDDly8BwueWnqKs4Mx2SdIAWAwuXeZ5ojk5qQOxJJg==} + + '@walletconnect/utils@2.21.0': + resolution: {integrity: sha512-zfHLiUoBrQ8rP57HTPXW7rQMnYxYI4gT9yTACxVW6LhIFROTF6/ytm5SKNoIvi4a5nX5dfXG4D9XwQUCu8Ilig==} + + '@walletconnect/utils@2.21.1': + resolution: {integrity: sha512-VPZvTcrNQCkbGOjFRbC24mm/pzbRMUq2DSQoiHlhh0X1U7ZhuIrzVtAoKsrzu6rqjz0EEtGxCr3K1TGRqDG4NA==} + + '@walletconnect/window-getters@1.0.1': + resolution: {integrity: sha512-vHp+HqzGxORPAN8gY03qnbTMnhqIwjeRJNOMOAzePRg4xVEEE2WvYsI9G2NMjOknA8hnuYbU3/hwLcKbjhc8+Q==} + + '@walletconnect/window-metadata@1.0.1': + resolution: {integrity: sha512-9koTqyGrM2cqFRW517BPY/iEtUDx2r1+Pwwu5m7sJ7ka79wi3EyqhqcICk/yDmv6jAS1rjKgTKXlEhanYjijcA==} + + abitype@1.0.6: + resolution: {integrity: sha512-MMSqYh4+C/aVqI2RQaWqbvI4Kxo5cQV40WQ4QFtDnNzCkqChm8MuENhElmynZlO0qUy/ObkEUaXtKqYnx1Kp3A==} + peerDependencies: + typescript: '>=5.0.4' + zod: ^3 >=3.22.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + + abitype@1.0.8: + resolution: {integrity: sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==} + peerDependencies: + typescript: '>=5.0.4' + zod: ^3 >=3.22.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + + abitype@1.1.0: + resolution: {integrity: sha512-6Vh4HcRxNMLA0puzPjM5GBgT4aAcFGKZzSgAXvuZ27shJP6NEpielTuqbBmZILR5/xd0PizkBGy5hReKz9jl5A==} + peerDependencies: + typescript: '>=5.0.4' + zod: ^3.22.0 || ^4.0.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + + abitype@1.1.1: + resolution: {integrity: sha512-Loe5/6tAgsBukY95eGaPSDmQHIjRZYQq8PB1MpsNccDIK8WiV+Uw6WzaIXipvaxTEL2yEB0OpEaQv3gs8pkS9Q==} + peerDependencies: + typescript: '>=5.0.4' + zod: ^3.22.0 || ^4.0.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + async-mutex@0.2.6: + resolution: {integrity: sha512-Hs4R+4SPgamu6rSGW8C7cV9gaWUKEHykfzCCvIRuaVv636Ju10ZdeUbvb4TBEW0INuq2DHZqXbK4Nd3yG4RaRw==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axios-retry@4.5.0: + resolution: {integrity: sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ==} + peerDependencies: + axios: 0.x || 1.x + + axios@1.12.2: + resolution: {integrity: sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==} + + base-x@3.0.11: + resolution: {integrity: sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==} + + base-x@5.0.1: + resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + big.js@6.2.2: + resolution: {integrity: sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ==} + + bigint-buffer@1.1.5: + resolution: {integrity: sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==} + engines: {node: '>= 10.0.0'} + + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + bn.js@5.2.2: + resolution: {integrity: sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==} + + body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + body-parser@2.2.0: + resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} + engines: {node: '>=18'} + + borsh@0.7.0: + resolution: {integrity: sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==} + + bowser@2.12.1: + resolution: {integrity: sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==} + + bs58@4.0.1: + resolution: {integrity: sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==} + + bs58@6.0.0: + resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + bufferutil@4.0.9: + resolution: {integrity: sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==} + engines: {node: '>=6.14.2'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + charenc@0.0.2: + resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + + clsx@1.2.1: + resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} + engines: {node: '>=6'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + + commander@14.0.1: + resolution: {integrity: sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==} + engines: {node: '>=20'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-disposition@1.0.0: + resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cookie-es@1.2.2: + resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} + engines: {node: '>= 0.6'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + cross-fetch@3.2.0: + resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==} + + cross-fetch@4.1.0: + resolution: {integrity: sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==} + + crossws@0.3.5: + resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} + + crypt@0.0.2: + resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} + + date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} + + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + + decode-uri-component@0.2.2: + resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} + engines: {node: '>=0.10'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + + delay@5.0.0: + resolution: {integrity: sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==} + engines: {node: '>=10'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + derive-valtio@0.1.0: + resolution: {integrity: sha512-OCg2UsLbXK7GmmpzMXhYkdO64vhJ1ROUUGaTFyHjVwEdMEcTTRj7W1TxLbSBxdY8QLBPCcp66MTyaSy0RpO17A==} + peerDependencies: + valtio: '*' + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + detect-browser@5.3.0: + resolution: {integrity: sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w==} + + dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + + dotenv@17.2.2: + resolution: {integrity: sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + duplexify@4.1.3: + resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} + + eciesjs@0.4.15: + resolution: {integrity: sha512-r6kEJXDKecVOCj2nLMuXK/FCPeurW33+3JRpfXVbjLja3XUYFfD9I/JBreH6sUyzcm3G/YQboBjMla6poKeSdA==} + engines: {bun: '>=1', deno: '>=2', node: '>=16'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + encode-utf8@1.0.3: + resolution: {integrity: sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==} + + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + engine.io-client@6.6.3: + resolution: {integrity: sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==} + + engine.io-parser@5.2.3: + resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} + engines: {node: '>=10.0.0'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-toolkit@1.33.0: + resolution: {integrity: sha512-X13Q/ZSc+vsO1q600bvNK4bxgXMkHcf//RxCmYDaRY5DAcT+eoXjY5hoAPGMdRnWQjvyLEcyauG3b6hz76LNqg==} + + es6-promise@4.2.8: + resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} + + es6-promisify@5.0.0: + resolution: {integrity: sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==} + + esbuild@0.25.10: + resolution: {integrity: sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==} + engines: {node: '>=18'} + hasBin: true + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eth-block-tracker@7.1.0: + resolution: {integrity: sha512-8YdplnuE1IK4xfqpf4iU7oBxnOYAc35934o083G8ao+8WM8QQtt/mVlAY6yIAdY1eMeLqg4Z//PZjJGmWGPMRg==} + engines: {node: '>=14.0.0'} + + eth-json-rpc-filters@6.0.1: + resolution: {integrity: sha512-ITJTvqoCw6OVMLs7pI8f4gG92n/St6x80ACtHodeS+IXmO0w+t1T5OOzfSt7KLSMLRkVUoexV7tztLgDxg+iig==} + engines: {node: '>=14.0.0'} + + eth-query@2.1.2: + resolution: {integrity: sha512-srES0ZcvwkR/wd5OQBRA1bIJMww1skfGS0s8wlwK3/oNP4+wnds60krvu5R1QbpRQjMmpG5OMIWro5s7gvDPsA==} + + eth-rpc-errors@4.0.3: + resolution: {integrity: sha512-Z3ymjopaoft7JDoxZcEb3pwdGh7yiYMhOwm2doUt6ASXlMavpNlK6Cre0+IMl2VSGyEU9rkiperQhp5iRxn5Pg==} + + ethereum-cryptography@2.2.1: + resolution: {integrity: sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==} + + eventemitter2@6.4.9: + resolution: {integrity: sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==} + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + express@4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} + engines: {node: '>= 0.10.0'} + + express@5.1.0: + resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} + engines: {node: '>= 18'} + + extension-port-stream@3.0.0: + resolution: {integrity: sha512-an2S5quJMiy5bnZKEf6AkfH/7r8CzHvhchU40gxN+OM6HPhe7Z9T1FUychcf2M9PpPOO0Hf7BAEfJkw2TDIBDw==} + engines: {node: '>=12.0.0'} + + eyes@0.1.8: + resolution: {integrity: sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==} + engines: {node: '> 0.1.90'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-redact@3.5.0: + resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} + engines: {node: '>=6'} + + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + + fast-stable-stringify@1.0.0: + resolution: {integrity: sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==} + + fastestsmallesttextencoderdecoder@1.0.22: + resolution: {integrity: sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==} + + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + + filter-obj@1.1.0: + resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==} + engines: {node: '>=0.10.0'} + + finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} + engines: {node: '>= 0.8'} + + finalhandler@2.1.0: + resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} + engines: {node: '>= 0.8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + engines: {node: '>= 6'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.10.1: + resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + h3@1.15.4: + resolution: {integrity: sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hono@4.9.8: + resolution: {integrity: sha512-JW8Bb4RFWD9iOKxg5PbUarBYGM99IcxFl2FPBo2gSJO11jjUDqlP1Bmfyqt8Z/dGhIQ63PMA9LdcLefXyIasyg==} + engines: {node: '>=16.9.0'} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.7.0: + resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} + engines: {node: '>=0.10.0'} + + idb-keyval@6.2.1: + resolution: {integrity: sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==} + + idb-keyval@6.2.2: + resolution: {integrity: sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + iron-webcrypto@1.2.1: + resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + + is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-function@1.1.0: + resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} + engines: {node: '>= 0.4'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-retry-allowed@2.2.0: + resolution: {integrity: sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==} + engines: {node: '>=10'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isomorphic-ws@4.0.1: + resolution: {integrity: sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==} + peerDependencies: + ws: '*' + + isows@1.0.6: + resolution: {integrity: sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw==} + peerDependencies: + ws: '*' + + isows@1.0.7: + resolution: {integrity: sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==} + peerDependencies: + ws: '*' + + jayson@4.2.0: + resolution: {integrity: sha512-VfJ9t1YLwacIubLhONk0KFeosUBwstRWQ0IRT1KDjEjnVnSOVHC3uwugyV7L0c7R9lpVyrUGT2XWiBA1UTtpyg==} + engines: {node: '>=8'} + hasBin: true + + jose@6.1.0: + resolution: {integrity: sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + json-rpc-engine@6.1.0: + resolution: {integrity: sha512-NEdLrtrq1jUZyfjkr9OCz9EzCNhnRyWtt1PAnvnhwy6e8XETS0Dtc+ZNCO2gvuAoKsIn2+vCSowXTYE4CkgnAQ==} + engines: {node: '>=10.0.0'} + + json-rpc-random-id@1.0.1: + resolution: {integrity: sha512-RJ9YYNCkhVDBuP4zN5BBtYAzEl03yq/jIIsyif0JY9qyJuQQZNeDK7anAPKKlyEtLSj2s8h6hNh2F8zO5q7ScA==} + + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + + keccak@3.0.4: + resolution: {integrity: sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q==} + engines: {node: '>=10.0.0'} + + keyvaluestorage-interface@1.0.0: + resolution: {integrity: sha512-8t6Q3TclQ4uZynJY9IGr2+SsIGwK9JHcO6ootkHCGA0CrQCRy+VkouYNO2xicET6b9al7QKzpebNow+gkpCL8g==} + + lit-element@4.2.1: + resolution: {integrity: sha512-WGAWRGzirAgyphK2urmYOV72tlvnxw7YfyLDgQ+OZnM9vQQBQnumQ7jUJe6unEzwGU3ahFOjuz1iz1jjrpCPuw==} + + lit-html@3.3.1: + resolution: {integrity: sha512-S9hbyDu/vs1qNrithiNyeyv64c9yqiW9l+DBgI18fL+MTvOtWoFR0FWiyq1TxaYef5wNlpEmzlXoBlZEO+WjoA==} + + lit@3.3.0: + resolution: {integrity: sha512-DGVsqsOIHBww2DqnuZzW7QsuCdahp50ojuDaBPC7jUDRpYoH0z7kHBBYZewRzer75FwtrkmkKk7iOAwSaWdBmw==} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + md5@2.3.0: + resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + micro-ftch@0.3.1: + resolution: {integrity: sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg==} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.1: + resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} + engines: {node: '>= 0.6'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + mipd@0.0.7: + resolution: {integrity: sha512-aAPZPNDQ3uMTdKbuO2YmAw2TxLHO0moa4YKAyETM/DTj5FloZo+a+8tU+iv4GmW+sOxKLSRwcSFuczk+Cpt6fg==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + multiformats@9.9.0: + resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==} + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + node-addon-api@2.0.2: + resolution: {integrity: sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==} + + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + + node-mock-http@1.0.3: + resolution: {integrity: sha512-jN8dK25fsfnMrVsEhluUTPkBFY+6ybu7jSB1n+ri/vOGjJxU8J9CZhpSGkHXSkFjtUhbmoncG/YG9ta5Ludqog==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + obj-multiplex@1.0.0: + resolution: {integrity: sha512-0GNJAOsHoBHeNTvl5Vt6IWnpUEcc3uSRxzBri7EDyIcMgYvnY2JL2qdeV5zTMjWQX5OHcD5amcW2HFfDh0gjIA==} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + ofetch@1.4.1: + resolution: {integrity: sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==} + + on-exit-leak-free@0.2.0: + resolution: {integrity: sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg==} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + openapi-fetch@0.13.8: + resolution: {integrity: sha512-yJ4QKRyNxE44baQ9mY5+r/kAzZ8yXMemtNAOFwOzRXJscdjSxxzWSNlyBAr+o5JjkUw9Lc3W7OIoca0cY3PYnQ==} + + openapi-typescript-helpers@0.0.15: + resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==} + + ox@0.6.7: + resolution: {integrity: sha512-17Gk/eFsFRAZ80p5eKqv89a57uXjd3NgIf1CaXojATPBuujVc/fQSVhBeAU9JCRB+k7J50WQAyWTxK19T9GgbA==} + peerDependencies: + typescript: '>=5.4.0' + peerDependenciesMeta: + typescript: + optional: true + + ox@0.6.9: + resolution: {integrity: sha512-wi5ShvzE4eOcTwQVsIPdFr+8ycyX+5le/96iAJutaZAvCes1J0+RvpEPg5QDPDiaR0XQQAvZVl7AwqQcINuUug==} + peerDependencies: + typescript: '>=5.4.0' + peerDependenciesMeta: + typescript: + optional: true + + ox@0.9.6: + resolution: {integrity: sha512-8SuCbHPvv2eZLYXrNmC0EC12rdzXQLdhnOMlHDW2wiCPLxBrOOJwX5L5E61by+UjTPOryqQiRSnjIKCI+GykKg==} + peerDependencies: + typescript: '>=5.4.0' + peerDependenciesMeta: + typescript: + optional: true + + ox@0.9.7: + resolution: {integrity: sha512-KX9Lvv0Rd+SKZXcT6Y85cuYbkmrQbK8Bz26k+s3X4EiT5bSU/hTDNSK/ApBeorJeIaOZbxEmJD2hHW0q1vIDEA==} + peerDependencies: + typescript: '>=5.4.0' + peerDependenciesMeta: + typescript: + optional: true + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + pify@3.0.0: + resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} + engines: {node: '>=4'} + + pify@5.0.0: + resolution: {integrity: sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==} + engines: {node: '>=10'} + + pino-abstract-transport@0.5.0: + resolution: {integrity: sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ==} + + pino-std-serializers@4.0.0: + resolution: {integrity: sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q==} + + pino@7.11.0: + resolution: {integrity: sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg==} + hasBin: true + + pngjs@5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + + pony-cause@2.1.11: + resolution: {integrity: sha512-M7LhCsdNbNgiLYiP4WjsfLUuFmCfnjdF6jKe2R9NKl4WFN+HZPGHJZ9lnLP7f9ZnKe3U9nuWD0szirmj+migUg==} + engines: {node: '>=12.0.0'} + + porto@0.2.19: + resolution: {integrity: sha512-q1vEJgdtlEOf6byWgD31GHiMwpfLuxFSfx9f7Sw4RGdvpQs2ANBGfnzzardADZegr87ZXsebSp+3vaaznEUzPQ==} + hasBin: true + peerDependencies: + '@tanstack/react-query': '>=5.59.0' + '@wagmi/core': '>=2.16.3' + react: '>=18' + typescript: '>=5.4.0' + viem: '>=2.37.0' + wagmi: '>=2.0.0' + peerDependenciesMeta: + '@tanstack/react-query': + optional: true + react: + optional: true + typescript: + optional: true + wagmi: + optional: true + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + preact@10.24.2: + resolution: {integrity: sha512-1cSoF0aCC8uaARATfrlz4VCBqE8LwZwRfLgkxJOQwAlQt6ayTmi0D9OF7nXid1POI5SZidFuG9CnlXbDfLqY/Q==} + + preact@10.27.2: + resolution: {integrity: sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + process-warning@1.0.0: + resolution: {integrity: sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + proxy-compare@2.6.0: + resolution: {integrity: sha512-8xuCeM3l8yqdmbPoYeLbrAXCBWu19XEYc5/F28f5qOaoAIMyfmBUkl5axiK+x9olUvRlcekvnm98AP9RDngOIw==} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + + qrcode@1.5.3: + resolution: {integrity: sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==} + engines: {node: '>=10.13.0'} + hasBin: true + + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + + query-string@7.1.3: + resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} + engines: {node: '>=6'} + + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + + radix3@1.1.2: + resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + + raw-body@3.0.1: + resolution: {integrity: sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==} + engines: {node: '>= 0.10'} + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + real-require@0.1.0: + resolution: {integrity: sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg==} + engines: {node: '>= 12.13.0'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + rpc-websockets@9.2.0: + resolution: {integrity: sha512-DS/XHdPxplQTtNRKiBCRWGBJfjOk56W7fyFUpiYi9fSTWTzoEMbUkn3J4gB0IMniIEVeAGR1/rzFQogzD5MxvQ==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + + send@1.2.0: + resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} + engines: {node: '>= 18'} + + serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} + + serve-static@2.2.0: + resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} + engines: {node: '>= 18'} + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + sha.js@2.4.12: + resolution: {integrity: sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==} + engines: {node: '>= 0.10'} + hasBin: true + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + socket.io-client@4.8.1: + resolution: {integrity: sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==} + engines: {node: '>=10.0.0'} + + socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + + sonic-boom@2.8.0: + resolution: {integrity: sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg==} + + split-on-first@1.1.0: + resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} + engines: {node: '>=6'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + stream-chain@2.2.5: + resolution: {integrity: sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==} + + stream-json@1.9.1: + resolution: {integrity: sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==} + + stream-shift@1.0.3: + resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} + + strict-uri-encode@2.0.0: + resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} + engines: {node: '>=4'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + superstruct@1.0.4: + resolution: {integrity: sha512-7JpaAoX2NGyoFlI9NBh66BQXGONc+uE+MRS5i2iOBKuS4e+ccgMDjATgZldkah+33DakBxDHiss9kvUcGAO8UQ==} + engines: {node: '>=14.0.0'} + + superstruct@2.0.2: + resolution: {integrity: sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==} + engines: {node: '>=14.0.0'} + + text-encoding-utf-8@1.0.2: + resolution: {integrity: sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==} + + thread-stream@0.15.2: + resolution: {integrity: sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA==} + + to-buffer@1.2.2: + resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==} + engines: {node: '>= 0.4'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.20.5: + resolution: {integrity: sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typescript@5.9.2: + resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + + uint8arrays@3.1.0: + resolution: {integrity: sha512-ei5rfKtoRO8OyOIor2Rz5fhzjThwIHJZ3uyDPnDHTXbP0aMQ1RN/6AI5B5d9dBxJOU+BvOAk7ZQ1xphsX8Lrog==} + + uncrypto@0.1.3: + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + + undici-types@7.12.0: + resolution: {integrity: sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + unstorage@1.17.1: + resolution: {integrity: sha512-KKGwRTT0iVBCErKemkJCLs7JdxNVfqTPc/85ae1XES0+bsHbc/sFBfVi5kJp156cc51BHinIH2l3k0EZ24vOBQ==} + peerDependencies: + '@azure/app-configuration': ^1.8.0 + '@azure/cosmos': ^4.2.0 + '@azure/data-tables': ^13.3.0 + '@azure/identity': ^4.6.0 + '@azure/keyvault-secrets': ^4.9.0 + '@azure/storage-blob': ^12.26.0 + '@capacitor/preferences': ^6.0.3 || ^7.0.0 + '@deno/kv': '>=0.9.0' + '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 + '@planetscale/database': ^1.19.0 + '@upstash/redis': ^1.34.3 + '@vercel/blob': '>=0.27.1' + '@vercel/functions': ^2.2.12 || ^3.0.0 + '@vercel/kv': ^1.0.1 + aws4fetch: ^1.0.20 + db0: '>=0.2.1' + idb-keyval: ^6.2.1 + ioredis: ^5.4.2 + uploadthing: ^7.4.4 + peerDependenciesMeta: + '@azure/app-configuration': + optional: true + '@azure/cosmos': + optional: true + '@azure/data-tables': + optional: true + '@azure/identity': + optional: true + '@azure/keyvault-secrets': + optional: true + '@azure/storage-blob': + optional: true + '@capacitor/preferences': + optional: true + '@deno/kv': + optional: true + '@netlify/blobs': + optional: true + '@planetscale/database': + optional: true + '@upstash/redis': + optional: true + '@vercel/blob': + optional: true + '@vercel/functions': + optional: true + '@vercel/kv': + optional: true + aws4fetch: + optional: true + db0: + optional: true + idb-keyval: + optional: true + ioredis: + optional: true + uploadthing: + optional: true + + use-sync-external-store@1.2.0: + resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + use-sync-external-store@1.4.0: + resolution: {integrity: sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + utf-8-validate@5.0.10: + resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==} + engines: {node: '>=6.14.2'} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + + valtio@1.13.2: + resolution: {integrity: sha512-Qik0o+DSy741TmkqmRfjq+0xpZBXi/Y6+fXZLn0xNF1z/waFMbE3rkivv5Zcf9RrMUp6zswf2J7sbh2KBlba5A==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=16.8' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + viem@2.23.2: + resolution: {integrity: sha512-NVmW/E0c5crMOtbEAqMF0e3NmvQykFXhLOc/CkLIXOlzHSA6KXVz3CYVmaKqBF8/xtjsjHAGjdJN3Ru1kFJLaA==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + + viem@2.37.8: + resolution: {integrity: sha512-mL+5yvCQbRIR6QvngDQMfEiZTfNWfd+/QL5yFaOoYbpH3b1Q2ddwF7YG2eI2AcYSh9LE1gtUkbzZLFUAVyj4oQ==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + + wagmi@2.17.4: + resolution: {integrity: sha512-AJpTQ5O7JFv2LDaRuMiCKxZEVBeDprh9oagA0dLSp/iAEsbd5VwmMy3rn4lCO3pl9umJ+afEkuRIq+5dx02jzg==} + peerDependencies: + '@tanstack/react-query': '>=5.0.0' + react: '>=18' + typescript: '>=5.0.4' + viem: 2.x + peerDependenciesMeta: + typescript: + optional: true + + webextension-polyfill@0.10.0: + resolution: {integrity: sha512-c5s35LgVa5tFaHhrZDnr3FpQpjj1BB+RXhLTYUxGqBVN460HkbM8TBtEqdXWbpTKfzwCcjAZVF7zXCYSKtcp9g==} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + engines: {node: '>= 0.4'} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + x402-express@0.6.1: + resolution: {integrity: sha512-J3jDs3+mFfjSQgppYP1Tnn8NXSJFMjj7jcoTIfl9m84evgi54puJrfZKgAhCs8+1FlrP5cEj/NmkYkG3u6zA9Q==} + + x402@0.6.1: + resolution: {integrity: sha512-9UmeCSsYzFGav5FdVP70VplKlR3V90P0DZ9fPSrlLVp0ifUVi1S9TztvegkmIHE9xTGZ1GWNi+bkne6N0Ea58w==} + + xmlhttprequest-ssl@2.1.2: + resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} + engines: {node: '>=0.4.0'} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + + zod@3.22.4: + resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + + zod@4.1.11: + resolution: {integrity: sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==} + + zustand@5.0.0: + resolution: {integrity: sha512-LE+VcmbartOPM+auOjCCLQOsQ05zUTp8RkgwRzefUk+2jISdMMFnxvyTjA4YNWr5ZGXYbVsEMZosttuxUBkojQ==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + + zustand@5.0.3: + resolution: {integrity: sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + + zustand@5.0.8: + resolution: {integrity: sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + +snapshots: + + '@adraffy/ens-normalize@1.11.1': {} + + '@babel/runtime@7.28.4': {} + + '@base-org/account@1.1.1(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@noble/hashes': 1.4.0 + clsx: 1.2.1 + eventemitter3: 5.0.1 + idb-keyval: 6.2.1 + ox: 0.6.9(typescript@5.9.2)(zod@3.25.76) + preact: 10.24.2 + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11) + zustand: 5.0.3(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) + transitivePeerDependencies: + - '@types/react' + - bufferutil + - immer + - react + - typescript + - use-sync-external-store + - utf-8-validate + - zod + + '@coinbase/cdp-sdk@1.38.2(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)': + dependencies: + '@solana/spl-token': 0.4.14(@solana/web3.js@1.98.4(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) + '@solana/web3.js': 1.98.4(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) + abitype: 1.0.6(typescript@5.9.2)(zod@3.25.76) + axios: 1.12.2 + axios-retry: 4.5.0(axios@1.12.2) + jose: 6.1.0 + md5: 2.3.0 + uncrypto: 0.1.3 + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + zod: 3.25.76 + transitivePeerDependencies: + - bufferutil + - debug + - encoding + - fastestsmallesttextencoderdecoder + - typescript + - utf-8-validate + + '@coinbase/wallet-sdk@3.9.3': + dependencies: + bn.js: 5.2.2 + buffer: 6.0.3 + clsx: 1.2.1 + eth-block-tracker: 7.1.0 + eth-json-rpc-filters: 6.0.1 + eventemitter3: 5.0.1 + keccak: 3.0.4 + preact: 10.27.2 + sha.js: 2.4.12 + transitivePeerDependencies: + - supports-color + + '@coinbase/wallet-sdk@4.3.6(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@noble/hashes': 1.4.0 + clsx: 1.2.1 + eventemitter3: 5.0.1 + idb-keyval: 6.2.1 + ox: 0.6.9(typescript@5.9.2)(zod@3.25.76) + preact: 10.24.2 + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11) + zustand: 5.0.3(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) + transitivePeerDependencies: + - '@types/react' + - bufferutil + - immer + - react + - typescript + - use-sync-external-store + - utf-8-validate + - zod + + '@ecies/ciphers@0.2.4(@noble/ciphers@1.3.0)': + dependencies: + '@noble/ciphers': 1.3.0 + + '@esbuild/aix-ppc64@0.25.10': + optional: true + + '@esbuild/android-arm64@0.25.10': + optional: true + + '@esbuild/android-arm@0.25.10': + optional: true + + '@esbuild/android-x64@0.25.10': + optional: true + + '@esbuild/darwin-arm64@0.25.10': + optional: true + + '@esbuild/darwin-x64@0.25.10': + optional: true + + '@esbuild/freebsd-arm64@0.25.10': + optional: true + + '@esbuild/freebsd-x64@0.25.10': + optional: true + + '@esbuild/linux-arm64@0.25.10': + optional: true + + '@esbuild/linux-arm@0.25.10': + optional: true + + '@esbuild/linux-ia32@0.25.10': + optional: true + + '@esbuild/linux-loong64@0.25.10': + optional: true + + '@esbuild/linux-mips64el@0.25.10': + optional: true + + '@esbuild/linux-ppc64@0.25.10': + optional: true + + '@esbuild/linux-riscv64@0.25.10': + optional: true + + '@esbuild/linux-s390x@0.25.10': + optional: true + + '@esbuild/linux-x64@0.25.10': + optional: true + + '@esbuild/netbsd-arm64@0.25.10': + optional: true + + '@esbuild/netbsd-x64@0.25.10': + optional: true + + '@esbuild/openbsd-arm64@0.25.10': + optional: true + + '@esbuild/openbsd-x64@0.25.10': + optional: true + + '@esbuild/openharmony-arm64@0.25.10': + optional: true + + '@esbuild/sunos-x64@0.25.10': + optional: true + + '@esbuild/win32-arm64@0.25.10': + optional: true + + '@esbuild/win32-ia32@0.25.10': + optional: true + + '@esbuild/win32-x64@0.25.10': + optional: true + + '@ethereumjs/common@3.2.0': + dependencies: + '@ethereumjs/util': 8.1.0 + crc-32: 1.2.2 + + '@ethereumjs/rlp@4.0.1': {} + + '@ethereumjs/tx@4.2.0': + dependencies: + '@ethereumjs/common': 3.2.0 + '@ethereumjs/rlp': 4.0.1 + '@ethereumjs/util': 8.1.0 + ethereum-cryptography: 2.2.1 + + '@ethereumjs/util@8.1.0': + dependencies: + '@ethereumjs/rlp': 4.0.1 + ethereum-cryptography: 2.2.1 + micro-ftch: 0.3.1 + + '@gemini-wallet/core@0.2.0(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))': + dependencies: + '@metamask/rpc-errors': 7.0.2 + eventemitter3: 5.0.1 + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11) + transitivePeerDependencies: + - supports-color + + '@lit-labs/ssr-dom-shim@1.4.0': {} + + '@lit/reactive-element@2.1.1': + dependencies: + '@lit-labs/ssr-dom-shim': 1.4.0 + + '@metamask/eth-json-rpc-provider@1.0.1': + dependencies: + '@metamask/json-rpc-engine': 7.3.3 + '@metamask/safe-event-emitter': 3.1.2 + '@metamask/utils': 5.0.2 + transitivePeerDependencies: + - supports-color + + '@metamask/json-rpc-engine@7.3.3': + dependencies: + '@metamask/rpc-errors': 6.4.0 + '@metamask/safe-event-emitter': 3.1.2 + '@metamask/utils': 8.5.0 + transitivePeerDependencies: + - supports-color + + '@metamask/json-rpc-engine@8.0.2': + dependencies: + '@metamask/rpc-errors': 6.4.0 + '@metamask/safe-event-emitter': 3.1.2 + '@metamask/utils': 8.5.0 + transitivePeerDependencies: + - supports-color + + '@metamask/json-rpc-middleware-stream@7.0.2': + dependencies: + '@metamask/json-rpc-engine': 8.0.2 + '@metamask/safe-event-emitter': 3.1.2 + '@metamask/utils': 8.5.0 + readable-stream: 3.6.2 + transitivePeerDependencies: + - supports-color + + '@metamask/object-multiplex@2.1.0': + dependencies: + once: 1.4.0 + readable-stream: 3.6.2 + + '@metamask/onboarding@1.0.1': + dependencies: + bowser: 2.12.1 + + '@metamask/providers@16.1.0': + dependencies: + '@metamask/json-rpc-engine': 8.0.2 + '@metamask/json-rpc-middleware-stream': 7.0.2 + '@metamask/object-multiplex': 2.1.0 + '@metamask/rpc-errors': 6.4.0 + '@metamask/safe-event-emitter': 3.1.2 + '@metamask/utils': 8.5.0 + detect-browser: 5.3.0 + extension-port-stream: 3.0.0 + fast-deep-equal: 3.1.3 + is-stream: 2.0.1 + readable-stream: 3.6.2 + webextension-polyfill: 0.10.0 + transitivePeerDependencies: + - supports-color + + '@metamask/rpc-errors@6.4.0': + dependencies: + '@metamask/utils': 9.3.0 + fast-safe-stringify: 2.1.1 + transitivePeerDependencies: + - supports-color + + '@metamask/rpc-errors@7.0.2': + dependencies: + '@metamask/utils': 11.8.1 + fast-safe-stringify: 2.1.1 + transitivePeerDependencies: + - supports-color + + '@metamask/safe-event-emitter@2.0.0': {} + + '@metamask/safe-event-emitter@3.1.2': {} + + '@metamask/sdk-analytics@0.0.5': + dependencies: + openapi-fetch: 0.13.8 + + '@metamask/sdk-communication-layer@0.33.1(cross-fetch@4.1.0)(eciesjs@0.4.15)(eventemitter2@6.4.9)(readable-stream@3.6.2)(socket.io-client@4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@metamask/sdk-analytics': 0.0.5 + bufferutil: 4.0.9 + cross-fetch: 4.1.0 + date-fns: 2.30.0 + debug: 4.3.4 + eciesjs: 0.4.15 + eventemitter2: 6.4.9 + readable-stream: 3.6.2 + socket.io-client: 4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + utf-8-validate: 5.0.10 + uuid: 8.3.2 + transitivePeerDependencies: + - supports-color + + '@metamask/sdk-install-modal-web@0.32.1': + dependencies: + '@paulmillr/qr': 0.2.1 + + '@metamask/sdk@0.33.1(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + dependencies: + '@babel/runtime': 7.28.4 + '@metamask/onboarding': 1.0.1 + '@metamask/providers': 16.1.0 + '@metamask/sdk-analytics': 0.0.5 + '@metamask/sdk-communication-layer': 0.33.1(cross-fetch@4.1.0)(eciesjs@0.4.15)(eventemitter2@6.4.9)(readable-stream@3.6.2)(socket.io-client@4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@metamask/sdk-install-modal-web': 0.32.1 + '@paulmillr/qr': 0.2.1 + bowser: 2.12.1 + cross-fetch: 4.1.0 + debug: 4.3.4 + eciesjs: 0.4.15 + eth-rpc-errors: 4.0.3 + eventemitter2: 6.4.9 + obj-multiplex: 1.0.0 + pump: 3.0.3 + readable-stream: 3.6.2 + socket.io-client: 4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + tslib: 2.8.1 + util: 0.12.5 + uuid: 8.3.2 + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - utf-8-validate + + '@metamask/superstruct@3.2.1': {} + + '@metamask/utils@11.8.1': + dependencies: + '@ethereumjs/tx': 4.2.0 + '@metamask/superstruct': 3.2.1 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + '@types/debug': 4.1.12 + '@types/lodash': 4.17.20 + debug: 4.4.3 + lodash: 4.17.21 + pony-cause: 2.1.11 + semver: 7.7.2 + uuid: 9.0.1 + transitivePeerDependencies: + - supports-color + + '@metamask/utils@5.0.2': + dependencies: + '@ethereumjs/tx': 4.2.0 + '@types/debug': 4.1.12 + debug: 4.4.3 + semver: 7.7.2 + superstruct: 1.0.4 + transitivePeerDependencies: + - supports-color + + '@metamask/utils@8.5.0': + dependencies: + '@ethereumjs/tx': 4.2.0 + '@metamask/superstruct': 3.2.1 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + '@types/debug': 4.1.12 + debug: 4.4.3 + pony-cause: 2.1.11 + semver: 7.7.2 + uuid: 9.0.1 + transitivePeerDependencies: + - supports-color + + '@metamask/utils@9.3.0': + dependencies: + '@ethereumjs/tx': 4.2.0 + '@metamask/superstruct': 3.2.1 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + '@types/debug': 4.1.12 + debug: 4.4.3 + pony-cause: 2.1.11 + semver: 7.7.2 + uuid: 9.0.1 + transitivePeerDependencies: + - supports-color + + '@noble/ciphers@1.2.1': {} + + '@noble/ciphers@1.3.0': {} + + '@noble/curves@1.4.2': + dependencies: + '@noble/hashes': 1.4.0 + + '@noble/curves@1.8.0': + dependencies: + '@noble/hashes': 1.7.0 + + '@noble/curves@1.8.1': + dependencies: + '@noble/hashes': 1.7.1 + + '@noble/curves@1.9.1': + dependencies: + '@noble/hashes': 1.8.0 + + '@noble/curves@1.9.7': + dependencies: + '@noble/hashes': 1.8.0 + + '@noble/hashes@1.4.0': {} + + '@noble/hashes@1.7.0': {} + + '@noble/hashes@1.7.1': {} + + '@noble/hashes@1.8.0': {} + + '@paulmillr/qr@0.2.1': {} + + '@reown/appkit-common@1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4)': + dependencies: + big.js: 6.2.2 + dayjs: 1.11.13 + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4) + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - zod + + '@reown/appkit-common@1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + big.js: 6.2.2 + dayjs: 1.11.13 + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11) + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - zod + + '@reown/appkit-controllers@1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) + '@walletconnect/universal-provider': 2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + valtio: 1.13.2(react@18.3.1) + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - react + - typescript + - uploadthing + - utf-8-validate + - zod + + '@reown/appkit-pay@1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-ui': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(react@18.3.1))(zod@3.25.76) + lit: 3.3.0 + valtio: 1.13.2(react@18.3.1) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - react + - typescript + - uploadthing + - utf-8-validate + - zod + + '@reown/appkit-polyfills@1.7.8': + dependencies: + buffer: 6.0.3 + + '@reown/appkit-scaffold-ui@1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(react@18.3.1))(zod@3.25.76)': + dependencies: + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-ui': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(react@18.3.1))(zod@3.25.76) + '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) + lit: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - react + - typescript + - uploadthing + - utf-8-validate + - valtio + - zod + + '@reown/appkit-ui@1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) + lit: 3.3.0 + qrcode: 1.5.3 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - react + - typescript + - uploadthing + - utf-8-validate + - zod + + '@reown/appkit-utils@1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(react@18.3.1))(zod@3.25.76)': + dependencies: + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-polyfills': 1.7.8 + '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) + '@walletconnect/logger': 2.1.2 + '@walletconnect/universal-provider': 2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + valtio: 1.13.2(react@18.3.1) + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - react + - typescript + - uploadthing + - utf-8-validate + - zod + + '@reown/appkit-wallet@1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)': + dependencies: + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4) + '@reown/appkit-polyfills': 1.7.8 + '@walletconnect/logger': 2.1.2 + zod: 3.22.4 + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + + '@reown/appkit@1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-pay': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-polyfills': 1.7.8 + '@reown/appkit-scaffold-ui': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(react@18.3.1))(zod@3.25.76) + '@reown/appkit-ui': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(react@18.3.1))(zod@3.25.76) + '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) + '@walletconnect/types': 2.21.0 + '@walletconnect/universal-provider': 2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + bs58: 6.0.0 + valtio: 1.13.2(react@18.3.1) + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - react + - typescript + - uploadthing + - utf-8-validate + - zod + + '@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + events: 3.3.0 + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - zod + + '@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@safe-global/safe-gateway-typescript-sdk': 3.23.1 + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11) + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - zod + + '@safe-global/safe-gateway-typescript-sdk@3.23.1': {} + + '@scure/base@1.1.9': {} + + '@scure/base@1.2.6': {} + + '@scure/bip32@1.4.0': + dependencies: + '@noble/curves': 1.4.2 + '@noble/hashes': 1.4.0 + '@scure/base': 1.1.9 + + '@scure/bip32@1.6.2': + dependencies: + '@noble/curves': 1.8.1 + '@noble/hashes': 1.7.1 + '@scure/base': 1.2.6 + + '@scure/bip32@1.7.0': + dependencies: + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + + '@scure/bip39@1.3.0': + dependencies: + '@noble/hashes': 1.4.0 + '@scure/base': 1.1.9 + + '@scure/bip39@1.5.4': + dependencies: + '@noble/hashes': 1.7.1 + '@scure/base': 1.2.6 + + '@scure/bip39@1.6.0': + dependencies: + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + + '@socket.io/component-emitter@3.1.2': {} + + '@solana-program/compute-budget@0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': + dependencies: + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + + '@solana-program/token-2022@0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))': + dependencies: + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/sysvars': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + + '@solana-program/token@0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': + dependencies: + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + + '@solana/accounts@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec': 2.3.0(typescript@5.9.2) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/addresses@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/assertions': 2.3.0(typescript@5.9.2) + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/nominal-types': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/assertions@2.3.0(typescript@5.9.2)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + + '@solana/buffer-layout-utils@0.2.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)': + dependencies: + '@solana/buffer-layout': 4.0.1 + '@solana/web3.js': 1.98.4(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) + bigint-buffer: 1.1.5 + bignumber.js: 9.3.1 + transitivePeerDependencies: + - bufferutil + - encoding + - typescript + - utf-8-validate + + '@solana/buffer-layout@4.0.1': + dependencies: + buffer: 6.0.3 + + '@solana/codecs-core@2.0.0-rc.1(typescript@5.9.2)': + dependencies: + '@solana/errors': 2.0.0-rc.1(typescript@5.9.2) + typescript: 5.9.2 + + '@solana/codecs-core@2.3.0(typescript@5.9.2)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + + '@solana/codecs-data-structures@2.0.0-rc.1(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.2) + '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.9.2) + '@solana/errors': 2.0.0-rc.1(typescript@5.9.2) + typescript: 5.9.2 + + '@solana/codecs-data-structures@2.3.0(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-numbers': 2.3.0(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + + '@solana/codecs-numbers@2.0.0-rc.1(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.2) + '@solana/errors': 2.0.0-rc.1(typescript@5.9.2) + typescript: 5.9.2 + + '@solana/codecs-numbers@2.3.0(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + + '@solana/codecs-strings@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.2) + '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.9.2) + '@solana/errors': 2.0.0-rc.1(typescript@5.9.2) + fastestsmallesttextencoderdecoder: 1.0.22 + typescript: 5.9.2 + + '@solana/codecs-strings@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-numbers': 2.3.0(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + fastestsmallesttextencoderdecoder: 1.0.22 + typescript: 5.9.2 + + '@solana/codecs@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.2) + '@solana/codecs-data-structures': 2.0.0-rc.1(typescript@5.9.2) + '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.9.2) + '@solana/codecs-strings': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/options': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/codecs@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-data-structures': 2.3.0(typescript@5.9.2) + '@solana/codecs-numbers': 2.3.0(typescript@5.9.2) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/options': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/errors@2.0.0-rc.1(typescript@5.9.2)': + dependencies: + chalk: 5.6.2 + commander: 12.1.0 + typescript: 5.9.2 + + '@solana/errors@2.3.0(typescript@5.9.2)': + dependencies: + chalk: 5.6.2 + commander: 14.0.1 + typescript: 5.9.2 + + '@solana/fast-stable-stringify@2.3.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + + '@solana/functional@2.3.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + + '@solana/instructions@2.3.0(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + + '@solana/keys@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/assertions': 2.3.0(typescript@5.9.2) + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/nominal-types': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/accounts': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/functional': 2.3.0(typescript@5.9.2) + '@solana/instructions': 2.3.0(typescript@5.9.2) + '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/programs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-parsed-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/signers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/sysvars': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + + '@solana/nominal-types@2.3.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + + '@solana/options@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.2) + '@solana/codecs-data-structures': 2.0.0-rc.1(typescript@5.9.2) + '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.9.2) + '@solana/codecs-strings': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.0.0-rc.1(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/options@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-data-structures': 2.3.0(typescript@5.9.2) + '@solana/codecs-numbers': 2.3.0(typescript@5.9.2) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/programs@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/promises@2.3.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + + '@solana/rpc-api@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-parsed-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec': 2.3.0(typescript@5.9.2) + '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/rpc-parsed-types@2.3.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + + '@solana/rpc-spec-types@2.3.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + + '@solana/rpc-spec@2.3.0(typescript@5.9.2)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + + '@solana/rpc-subscriptions-api@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.2) + '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/rpc-subscriptions-channel-websocket@2.3.0(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/functional': 2.3.0(typescript@5.9.2) + '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.2) + '@solana/subscribable': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + + '@solana/rpc-subscriptions-spec@2.3.0(typescript@5.9.2)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/promises': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) + '@solana/subscribable': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + + '@solana/rpc-subscriptions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/fast-stable-stringify': 2.3.0(typescript@5.9.2) + '@solana/functional': 2.3.0(typescript@5.9.2) + '@solana/promises': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-subscriptions-api': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-subscriptions-channel-websocket': 2.3.0(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.2) + '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/subscribable': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + + '@solana/rpc-transformers@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/functional': 2.3.0(typescript@5.9.2) + '@solana/nominal-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/rpc-transport-http@2.3.0(typescript@5.9.2)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + undici-types: 7.12.0 + + '@solana/rpc-types@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-numbers': 2.3.0(typescript@5.9.2) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/nominal-types': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/rpc@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/fast-stable-stringify': 2.3.0(typescript@5.9.2) + '@solana/functional': 2.3.0(typescript@5.9.2) + '@solana/rpc-api': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-spec': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-transport-http': 2.3.0(typescript@5.9.2) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/signers@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/instructions': 2.3.0(typescript@5.9.2) + '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/nominal-types': 2.3.0(typescript@5.9.2) + '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/spl-token-group@0.0.7(@solana/web3.js@1.98.4(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/codecs': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/web3.js': 1.98.4(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - typescript + + '@solana/spl-token-metadata@0.1.6(@solana/web3.js@1.98.4(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/codecs': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/web3.js': 1.98.4(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - typescript + + '@solana/spl-token@0.4.14(@solana/web3.js@1.98.4(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)': + dependencies: + '@solana/buffer-layout': 4.0.1 + '@solana/buffer-layout-utils': 0.2.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) + '@solana/spl-token-group': 0.0.7(@solana/web3.js@1.98.4(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/spl-token-metadata': 0.1.6(@solana/web3.js@1.98.4(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/web3.js': 1.98.4(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) + buffer: 6.0.3 + transitivePeerDependencies: + - bufferutil + - encoding + - fastestsmallesttextencoderdecoder + - typescript + - utf-8-validate + + '@solana/subscribable@2.3.0(typescript@5.9.2)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + + '@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/accounts': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/transaction-confirmation@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/promises': 2.3.0(typescript@5.9.2) + '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + + '@solana/transaction-messages@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-data-structures': 2.3.0(typescript@5.9.2) + '@solana/codecs-numbers': 2.3.0(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/functional': 2.3.0(typescript@5.9.2) + '@solana/instructions': 2.3.0(typescript@5.9.2) + '@solana/nominal-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/transactions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-data-structures': 2.3.0(typescript@5.9.2) + '@solana/codecs-numbers': 2.3.0(typescript@5.9.2) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/functional': 2.3.0(typescript@5.9.2) + '@solana/instructions': 2.3.0(typescript@5.9.2) + '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/nominal-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/web3.js@1.98.4(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)': + dependencies: + '@babel/runtime': 7.28.4 + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + '@solana/buffer-layout': 4.0.1 + '@solana/codecs-numbers': 2.3.0(typescript@5.9.2) + agentkeepalive: 4.6.0 + bn.js: 5.2.2 + borsh: 0.7.0 + bs58: 4.0.1 + buffer: 6.0.3 + fast-stable-stringify: 1.0.0 + jayson: 4.2.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + node-fetch: 2.7.0 + rpc-websockets: 9.2.0 + superstruct: 2.0.2 + transitivePeerDependencies: + - bufferutil + - encoding + - typescript + - utf-8-validate + + '@swc/helpers@0.5.17': + dependencies: + tslib: 2.8.1 + + '@tanstack/query-core@5.90.2': {} + + '@tanstack/react-query@5.90.2(react@18.3.1)': + dependencies: + '@tanstack/query-core': 5.90.2 + react: 18.3.1 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 24.5.2 + + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + + '@types/lodash@4.17.20': {} + + '@types/ms@2.1.0': {} + + '@types/node@12.20.55': {} + + '@types/node@24.5.2': + dependencies: + undici-types: 7.12.0 + + '@types/trusted-types@2.0.7': {} + + '@types/uuid@8.3.4': {} + + '@types/ws@7.4.7': + dependencies: + '@types/node': 24.5.2 + + '@types/ws@8.18.1': + dependencies: + '@types/node': 24.5.2 + + '@wagmi/connectors@5.11.1(@tanstack/react-query@5.90.2(react@18.3.1))(@wagmi/core@2.21.1(@tanstack/query-core@5.90.2)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.17.4(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76))(zod@3.25.76)': + dependencies: + '@base-org/account': 1.1.1(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@3.25.76) + '@coinbase/wallet-sdk': 4.3.6(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@3.25.76) + '@gemini-wallet/core': 0.2.0(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@metamask/sdk': 0.33.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@wagmi/core': 2.21.1(@tanstack/query-core@5.90.2)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@walletconnect/ethereum-provider': 2.21.1(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + cbw-sdk: '@coinbase/wallet-sdk@3.9.3' + porto: 0.2.19(@tanstack/react-query@5.90.2(react@18.3.1))(@wagmi/core@2.21.1(@tanstack/query-core@5.90.2)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.17.4(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)) + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11) + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@tanstack/react-query' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - immer + - ioredis + - react + - supports-color + - uploadthing + - use-sync-external-store + - utf-8-validate + - wagmi + - zod + + '@wagmi/core@2.21.1(@tanstack/query-core@5.90.2)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))': + dependencies: + eventemitter3: 5.0.1 + mipd: 0.0.7(typescript@5.9.2) + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11) + zustand: 5.0.0(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) + optionalDependencies: + '@tanstack/query-core': 5.90.2 + typescript: 5.9.2 + transitivePeerDependencies: + - '@types/react' + - immer + - react + - use-sync-external-store + + '@walletconnect/core@2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-provider': 1.0.14 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/logger': 2.1.2 + '@walletconnect/relay-api': 1.0.11 + '@walletconnect/relay-auth': 1.1.0 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.21.0 + '@walletconnect/utils': 2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/window-getters': 1.0.1 + es-toolkit: 1.33.0 + events: 3.3.0 + uint8arrays: 3.1.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + + '@walletconnect/core@2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-provider': 1.0.14 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/logger': 2.1.2 + '@walletconnect/relay-api': 1.0.11 + '@walletconnect/relay-auth': 1.1.0 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.21.1 + '@walletconnect/utils': 2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/window-getters': 1.0.1 + es-toolkit: 1.33.0 + events: 3.3.0 + uint8arrays: 3.1.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + + '@walletconnect/environment@1.0.1': + dependencies: + tslib: 1.14.1 + + '@walletconnect/ethereum-provider@2.21.1(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@reown/appkit': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/jsonrpc-http-connection': 1.0.8 + '@walletconnect/jsonrpc-provider': 1.0.14 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/sign-client': 2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/types': 2.21.1 + '@walletconnect/universal-provider': 2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/utils': 2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - react + - typescript + - uploadthing + - utf-8-validate + - zod + + '@walletconnect/events@1.0.1': + dependencies: + keyvaluestorage-interface: 1.0.0 + tslib: 1.14.1 + + '@walletconnect/heartbeat@1.2.2': + dependencies: + '@walletconnect/events': 1.0.1 + '@walletconnect/time': 1.0.2 + events: 3.3.0 + + '@walletconnect/jsonrpc-http-connection@1.0.8': + dependencies: + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/safe-json': 1.0.2 + cross-fetch: 3.2.0 + events: 3.3.0 + transitivePeerDependencies: + - encoding + + '@walletconnect/jsonrpc-provider@1.0.14': + dependencies: + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/safe-json': 1.0.2 + events: 3.3.0 + + '@walletconnect/jsonrpc-types@1.0.4': + dependencies: + events: 3.3.0 + keyvaluestorage-interface: 1.0.0 + + '@walletconnect/jsonrpc-utils@1.0.8': + dependencies: + '@walletconnect/environment': 1.0.1 + '@walletconnect/jsonrpc-types': 1.0.4 + tslib: 1.14.1 + + '@walletconnect/jsonrpc-ws-connection@1.0.16(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + dependencies: + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/safe-json': 1.0.2 + events: 3.3.0 + ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@walletconnect/keyvaluestorage@1.1.1': + dependencies: + '@walletconnect/safe-json': 1.0.2 + idb-keyval: 6.2.2 + unstorage: 1.17.1(idb-keyval@6.2.2) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - db0 + - ioredis + - uploadthing + + '@walletconnect/logger@2.1.2': + dependencies: + '@walletconnect/safe-json': 1.0.2 + pino: 7.11.0 + + '@walletconnect/relay-api@1.0.11': + dependencies: + '@walletconnect/jsonrpc-types': 1.0.4 + + '@walletconnect/relay-auth@1.1.0': + dependencies: + '@noble/curves': 1.8.0 + '@noble/hashes': 1.7.0 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + uint8arrays: 3.1.0 + + '@walletconnect/safe-json@1.0.2': + dependencies: + tslib: 1.14.1 + + '@walletconnect/sign-client@2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@walletconnect/core': 2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/events': 1.0.1 + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/logger': 2.1.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.21.0 + '@walletconnect/utils': 2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + + '@walletconnect/sign-client@2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@walletconnect/core': 2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/events': 1.0.1 + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/logger': 2.1.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.21.1 + '@walletconnect/utils': 2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + + '@walletconnect/time@1.0.2': + dependencies: + tslib: 1.14.1 + + '@walletconnect/types@2.21.0': + dependencies: + '@walletconnect/events': 1.0.1 + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/logger': 2.1.2 + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - db0 + - ioredis + - uploadthing + + '@walletconnect/types@2.21.1': + dependencies: + '@walletconnect/events': 1.0.1 + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/logger': 2.1.2 + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - db0 + - ioredis + - uploadthing + + '@walletconnect/universal-provider@2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@walletconnect/events': 1.0.1 + '@walletconnect/jsonrpc-http-connection': 1.0.8 + '@walletconnect/jsonrpc-provider': 1.0.14 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/logger': 2.1.2 + '@walletconnect/sign-client': 2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/types': 2.21.0 + '@walletconnect/utils': 2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + es-toolkit: 1.33.0 + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + + '@walletconnect/universal-provider@2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@walletconnect/events': 1.0.1 + '@walletconnect/jsonrpc-http-connection': 1.0.8 + '@walletconnect/jsonrpc-provider': 1.0.14 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/logger': 2.1.2 + '@walletconnect/sign-client': 2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/types': 2.21.1 + '@walletconnect/utils': 2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + es-toolkit: 1.33.0 + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + + '@walletconnect/utils@2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@noble/ciphers': 1.2.1 + '@noble/curves': 1.8.1 + '@noble/hashes': 1.7.1 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/relay-api': 1.0.11 + '@walletconnect/relay-auth': 1.1.0 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.21.0 + '@walletconnect/window-getters': 1.0.1 + '@walletconnect/window-metadata': 1.0.1 + bs58: 6.0.0 + detect-browser: 5.3.0 + query-string: 7.1.3 + uint8arrays: 3.1.0 + viem: 2.23.2(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + + '@walletconnect/utils@2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@noble/ciphers': 1.2.1 + '@noble/curves': 1.8.1 + '@noble/hashes': 1.7.1 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/relay-api': 1.0.11 + '@walletconnect/relay-auth': 1.1.0 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.21.1 + '@walletconnect/window-getters': 1.0.1 + '@walletconnect/window-metadata': 1.0.1 + bs58: 6.0.0 + detect-browser: 5.3.0 + query-string: 7.1.3 + uint8arrays: 3.1.0 + viem: 2.23.2(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + + '@walletconnect/window-getters@1.0.1': + dependencies: + tslib: 1.14.1 + + '@walletconnect/window-metadata@1.0.1': + dependencies: + '@walletconnect/window-getters': 1.0.1 + tslib: 1.14.1 + + abitype@1.0.6(typescript@5.9.2)(zod@3.25.76): + optionalDependencies: + typescript: 5.9.2 + zod: 3.25.76 + + abitype@1.0.8(typescript@5.9.2)(zod@3.25.76): + optionalDependencies: + typescript: 5.9.2 + zod: 3.25.76 + + abitype@1.1.0(typescript@5.9.2)(zod@3.22.4): + optionalDependencies: + typescript: 5.9.2 + zod: 3.22.4 + + abitype@1.1.0(typescript@5.9.2)(zod@3.25.76): + optionalDependencies: + typescript: 5.9.2 + zod: 3.25.76 + + abitype@1.1.0(typescript@5.9.2)(zod@4.1.11): + optionalDependencies: + typescript: 5.9.2 + zod: 4.1.11 + + abitype@1.1.1(typescript@5.9.2)(zod@3.25.76): + optionalDependencies: + typescript: 5.9.2 + zod: 3.25.76 + + abitype@1.1.1(typescript@5.9.2)(zod@4.1.11): + optionalDependencies: + typescript: 5.9.2 + zod: 4.1.11 + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + accepts@2.0.0: + dependencies: + mime-types: 3.0.1 + negotiator: 1.0.0 + + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + array-flatten@1.1.1: {} + + async-mutex@0.2.6: + dependencies: + tslib: 2.8.1 + + asynckit@0.4.0: {} + + atomic-sleep@1.0.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + axios-retry@4.5.0(axios@1.12.2): + dependencies: + axios: 1.12.2 + is-retry-allowed: 2.2.0 + + axios@1.12.2: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.4 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + base-x@3.0.11: + dependencies: + safe-buffer: 5.2.1 + + base-x@5.0.1: {} + + base64-js@1.5.1: {} + + big.js@6.2.2: {} + + bigint-buffer@1.1.5: + dependencies: + bindings: 1.5.0 + + bignumber.js@9.3.1: {} + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + bn.js@5.2.2: {} + + body-parser@1.20.3: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.13.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + body-parser@2.2.0: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + on-finished: 2.4.1 + qs: 6.14.0 + raw-body: 3.0.1 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + + borsh@0.7.0: + dependencies: + bn.js: 5.2.2 + bs58: 4.0.1 + text-encoding-utf-8: 1.0.2 + + bowser@2.12.1: {} + + bs58@4.0.1: + dependencies: + base-x: 3.0.11 + + bs58@6.0.0: + dependencies: + base-x: 5.0.1 + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bufferutil@4.0.9: + dependencies: + node-gyp-build: 4.8.4 + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + camelcase@5.3.1: {} + + chalk@5.6.2: {} + + charenc@0.0.2: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + + clsx@1.2.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@12.1.0: {} + + commander@14.0.1: {} + + commander@2.20.3: {} + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-disposition@1.0.0: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + cookie-es@1.2.2: {} + + cookie-signature@1.0.6: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.1: {} + + cookie@0.7.2: {} + + core-util-is@1.0.3: {} + + crc-32@1.2.2: {} + + cross-fetch@3.2.0: + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + cross-fetch@4.1.0: + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + crossws@0.3.5: + dependencies: + uncrypto: 0.1.3 + + crypt@0.0.2: {} + + date-fns@2.30.0: + dependencies: + '@babel/runtime': 7.28.4 + + dayjs@1.11.13: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@4.3.4: + dependencies: + ms: 2.1.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decamelize@1.2.0: {} + + decode-uri-component@0.2.2: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + defu@6.1.4: {} + + delay@5.0.0: {} + + delayed-stream@1.0.0: {} + + depd@2.0.0: {} + + derive-valtio@0.1.0(valtio@1.13.2(react@18.3.1)): + dependencies: + valtio: 1.13.2(react@18.3.1) + + destr@2.0.5: {} + + destroy@1.2.0: {} + + detect-browser@5.3.0: {} + + dijkstrajs@1.0.3: {} + + dotenv@17.2.2: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + duplexify@4.1.3: + dependencies: + end-of-stream: 1.4.5 + inherits: 2.0.4 + readable-stream: 3.6.2 + stream-shift: 1.0.3 + + eciesjs@0.4.15: + dependencies: + '@ecies/ciphers': 0.2.4(@noble/ciphers@1.3.0) + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + + ee-first@1.1.1: {} + + emoji-regex@8.0.0: {} + + encode-utf8@1.0.3: {} + + encodeurl@1.0.2: {} + + encodeurl@2.0.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + engine.io-client@6.6.3(bufferutil@4.0.9)(utf-8-validate@5.0.10): + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.4 + engine.io-parser: 5.2.3 + ws: 8.17.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + xmlhttprequest-ssl: 2.1.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + engine.io-parser@5.2.3: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-toolkit@1.33.0: {} + + es6-promise@4.2.8: {} + + es6-promisify@5.0.0: + dependencies: + es6-promise: 4.2.8 + + esbuild@0.25.10: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.10 + '@esbuild/android-arm': 0.25.10 + '@esbuild/android-arm64': 0.25.10 + '@esbuild/android-x64': 0.25.10 + '@esbuild/darwin-arm64': 0.25.10 + '@esbuild/darwin-x64': 0.25.10 + '@esbuild/freebsd-arm64': 0.25.10 + '@esbuild/freebsd-x64': 0.25.10 + '@esbuild/linux-arm': 0.25.10 + '@esbuild/linux-arm64': 0.25.10 + '@esbuild/linux-ia32': 0.25.10 + '@esbuild/linux-loong64': 0.25.10 + '@esbuild/linux-mips64el': 0.25.10 + '@esbuild/linux-ppc64': 0.25.10 + '@esbuild/linux-riscv64': 0.25.10 + '@esbuild/linux-s390x': 0.25.10 + '@esbuild/linux-x64': 0.25.10 + '@esbuild/netbsd-arm64': 0.25.10 + '@esbuild/netbsd-x64': 0.25.10 + '@esbuild/openbsd-arm64': 0.25.10 + '@esbuild/openbsd-x64': 0.25.10 + '@esbuild/openharmony-arm64': 0.25.10 + '@esbuild/sunos-x64': 0.25.10 + '@esbuild/win32-arm64': 0.25.10 + '@esbuild/win32-ia32': 0.25.10 + '@esbuild/win32-x64': 0.25.10 + + escape-html@1.0.3: {} + + etag@1.8.1: {} + + eth-block-tracker@7.1.0: + dependencies: + '@metamask/eth-json-rpc-provider': 1.0.1 + '@metamask/safe-event-emitter': 3.1.2 + '@metamask/utils': 5.0.2 + json-rpc-random-id: 1.0.1 + pify: 3.0.0 + transitivePeerDependencies: + - supports-color + + eth-json-rpc-filters@6.0.1: + dependencies: + '@metamask/safe-event-emitter': 3.1.2 + async-mutex: 0.2.6 + eth-query: 2.1.2 + json-rpc-engine: 6.1.0 + pify: 5.0.0 + + eth-query@2.1.2: + dependencies: + json-rpc-random-id: 1.0.1 + xtend: 4.0.2 + + eth-rpc-errors@4.0.3: + dependencies: + fast-safe-stringify: 2.1.1 + + ethereum-cryptography@2.2.1: + dependencies: + '@noble/curves': 1.4.2 + '@noble/hashes': 1.4.0 + '@scure/bip32': 1.4.0 + '@scure/bip39': 1.3.0 + + eventemitter2@6.4.9: {} + + eventemitter3@5.0.1: {} + + events@3.3.0: {} + + express@4.21.2: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.3 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.1 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.13.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + express@5.1.0: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.0 + content-disposition: 1.0.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.0 + fresh: 2.0.0 + http-errors: 2.0.0 + merge-descriptors: 2.0.0 + mime-types: 3.0.1 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.0 + serve-static: 2.2.0 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + extension-port-stream@3.0.0: + dependencies: + readable-stream: 3.6.2 + webextension-polyfill: 0.10.0 + + eyes@0.1.8: {} + + fast-deep-equal@3.1.3: {} + + fast-redact@3.5.0: {} + + fast-safe-stringify@2.1.1: {} + + fast-stable-stringify@1.0.0: {} + + fastestsmallesttextencoderdecoder@1.0.22: {} + + file-uri-to-path@1.0.0: {} + + filter-obj@1.1.0: {} + + finalhandler@1.3.1: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + finalhandler@2.1.0: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + follow-redirects@1.15.11: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + form-data@4.0.4: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + forwarded@0.2.0: {} + + fresh@0.5.2: {} + + fresh@2.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-tsconfig@4.10.1: + dependencies: + resolve-pkg-maps: 1.0.0 + + gopd@1.2.0: {} + + h3@1.15.4: + dependencies: + cookie-es: 1.2.2 + crossws: 0.3.5 + defu: 6.1.4 + destr: 2.0.5 + iron-webcrypto: 1.2.1 + node-mock-http: 1.0.3 + radix3: 1.1.2 + ufo: 1.6.1 + uncrypto: 0.1.3 + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hono@4.9.8: {} + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.7.0: + dependencies: + safer-buffer: 2.1.2 + + idb-keyval@6.2.1: {} + + idb-keyval@6.2.2: {} + + ieee754@1.2.1: {} + + inherits@2.0.4: {} + + ipaddr.js@1.9.1: {} + + iron-webcrypto@1.2.1: {} + + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-buffer@1.1.6: {} + + is-callable@1.2.7: {} + + is-fullwidth-code-point@3.0.0: {} + + is-generator-function@1.1.0: + dependencies: + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-promise@4.0.0: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-retry-allowed@2.2.0: {} + + is-stream@2.0.1: {} + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.19 + + isarray@1.0.0: {} + + isarray@2.0.5: {} + + isomorphic-ws@4.0.1(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + dependencies: + ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) + + isows@1.0.6(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + dependencies: + ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + + isows@1.0.7(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + dependencies: + ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + + jayson@4.2.0(bufferutil@4.0.9)(utf-8-validate@5.0.10): + dependencies: + '@types/connect': 3.4.38 + '@types/node': 12.20.55 + '@types/ws': 7.4.7 + commander: 2.20.3 + delay: 5.0.0 + es6-promisify: 5.0.0 + eyes: 0.1.8 + isomorphic-ws: 4.0.1(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + json-stringify-safe: 5.0.1 + stream-json: 1.9.1 + uuid: 8.3.2 + ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + jose@6.1.0: {} + + js-tokens@4.0.0: {} + + json-rpc-engine@6.1.0: + dependencies: + '@metamask/safe-event-emitter': 2.0.0 + eth-rpc-errors: 4.0.3 + + json-rpc-random-id@1.0.1: {} + + json-stringify-safe@5.0.1: {} + + keccak@3.0.4: + dependencies: + node-addon-api: 2.0.2 + node-gyp-build: 4.8.4 + readable-stream: 3.6.2 + + keyvaluestorage-interface@1.0.0: {} + + lit-element@4.2.1: + dependencies: + '@lit-labs/ssr-dom-shim': 1.4.0 + '@lit/reactive-element': 2.1.1 + lit-html: 3.3.1 + + lit-html@3.3.1: + dependencies: + '@types/trusted-types': 2.0.7 + + lit@3.3.0: + dependencies: + '@lit/reactive-element': 2.1.1 + lit-element: 4.2.1 + lit-html: 3.3.1 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + lodash@4.17.21: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@10.4.3: {} + + math-intrinsics@1.1.0: {} + + md5@2.3.0: + dependencies: + charenc: 0.0.2 + crypt: 0.0.2 + is-buffer: 1.1.6 + + media-typer@0.3.0: {} + + media-typer@1.1.0: {} + + merge-descriptors@1.0.3: {} + + merge-descriptors@2.0.0: {} + + methods@1.1.2: {} + + micro-ftch@0.3.1: {} + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.1: + dependencies: + mime-db: 1.54.0 + + mime@1.6.0: {} + + mipd@0.0.7(typescript@5.9.2): + optionalDependencies: + typescript: 5.9.2 + + ms@2.0.0: {} + + ms@2.1.2: {} + + ms@2.1.3: {} + + multiformats@9.9.0: {} + + negotiator@0.6.3: {} + + negotiator@1.0.0: {} + + node-addon-api@2.0.2: {} + + node-fetch-native@1.6.7: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-gyp-build@4.8.4: {} + + node-mock-http@1.0.3: {} + + normalize-path@3.0.0: {} + + obj-multiplex@1.0.0: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + readable-stream: 2.3.8 + + object-inspect@1.13.4: {} + + ofetch@1.4.1: + dependencies: + destr: 2.0.5 + node-fetch-native: 1.6.7 + ufo: 1.6.1 + + on-exit-leak-free@0.2.0: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + openapi-fetch@0.13.8: + dependencies: + openapi-typescript-helpers: 0.0.15 + + openapi-typescript-helpers@0.0.15: {} + + ox@0.6.7(typescript@5.9.2)(zod@3.25.76): + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@noble/curves': 1.8.1 + '@noble/hashes': 1.7.1 + '@scure/bip32': 1.6.2 + '@scure/bip39': 1.5.4 + abitype: 1.0.8(typescript@5.9.2)(zod@3.25.76) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - zod + + ox@0.6.9(typescript@5.9.2)(zod@3.25.76): + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.1.1(typescript@5.9.2)(zod@3.25.76) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - zod + + ox@0.9.6(typescript@5.9.2)(zod@3.22.4): + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.1.0(typescript@5.9.2)(zod@3.22.4) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - zod + + ox@0.9.6(typescript@5.9.2)(zod@3.25.76): + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.1.0(typescript@5.9.2)(zod@3.25.76) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - zod + + ox@0.9.6(typescript@5.9.2)(zod@4.1.11): + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.1.0(typescript@5.9.2)(zod@4.1.11) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - zod + + ox@0.9.7(typescript@5.9.2)(zod@4.1.11): + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.1.1(typescript@5.9.2)(zod@4.1.11) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - zod + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-try@2.2.0: {} + + parseurl@1.3.3: {} + + path-exists@4.0.0: {} + + path-to-regexp@0.1.12: {} + + path-to-regexp@8.3.0: {} + + picomatch@2.3.1: {} + + pify@3.0.0: {} + + pify@5.0.0: {} + + pino-abstract-transport@0.5.0: + dependencies: + duplexify: 4.1.3 + split2: 4.2.0 + + pino-std-serializers@4.0.0: {} + + pino@7.11.0: + dependencies: + atomic-sleep: 1.0.0 + fast-redact: 3.5.0 + on-exit-leak-free: 0.2.0 + pino-abstract-transport: 0.5.0 + pino-std-serializers: 4.0.0 + process-warning: 1.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.1.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 2.8.0 + thread-stream: 0.15.2 + + pngjs@5.0.0: {} + + pony-cause@2.1.11: {} + + porto@0.2.19(@tanstack/react-query@5.90.2(react@18.3.1))(@wagmi/core@2.21.1(@tanstack/query-core@5.90.2)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.17.4(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)): + dependencies: + '@wagmi/core': 2.21.1(@tanstack/query-core@5.90.2)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + hono: 4.9.8 + idb-keyval: 6.2.2 + mipd: 0.0.7(typescript@5.9.2) + ox: 0.9.7(typescript@5.9.2)(zod@4.1.11) + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11) + zod: 4.1.11 + zustand: 5.0.8(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) + optionalDependencies: + '@tanstack/react-query': 5.90.2(react@18.3.1) + react: 18.3.1 + typescript: 5.9.2 + wagmi: 2.17.4(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@4.1.11) + transitivePeerDependencies: + - '@types/react' + - immer + - use-sync-external-store + + possible-typed-array-names@1.1.0: {} + + preact@10.24.2: {} + + preact@10.27.2: {} + + process-nextick-args@2.0.1: {} + + process-warning@1.0.0: {} + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + proxy-compare@2.6.0: {} + + proxy-from-env@1.1.0: {} + + pump@3.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + qrcode@1.5.3: + dependencies: + dijkstrajs: 1.0.3 + encode-utf8: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 + + qs@6.13.0: + dependencies: + side-channel: 1.1.0 + + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + + query-string@7.1.3: + dependencies: + decode-uri-component: 0.2.2 + filter-obj: 1.1.0 + split-on-first: 1.1.0 + strict-uri-encode: 2.0.0 + + quick-format-unescaped@4.0.4: {} + + radix3@1.1.2: {} + + range-parser@1.2.1: {} + + raw-body@2.5.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + raw-body@3.0.1: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.7.0 + unpipe: 1.0.0 + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@4.1.2: {} + + real-require@0.1.0: {} + + require-directory@2.1.1: {} + + require-main-filename@2.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + + rpc-websockets@9.2.0: + dependencies: + '@swc/helpers': 0.5.17 + '@types/uuid': 8.3.4 + '@types/ws': 8.18.1 + buffer: 6.0.3 + eventemitter3: 5.0.1 + uuid: 8.3.2 + ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + optionalDependencies: + bufferutil: 4.0.9 + utf-8-validate: 5.0.10 + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safe-stable-stringify@2.5.0: {} + + safer-buffer@2.1.2: {} + + semver@7.7.2: {} + + send@0.19.0: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + send@1.2.0: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.0 + mime-types: 3.0.1 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@1.16.2: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.0 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.0: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.0 + transitivePeerDependencies: + - supports-color + + set-blocking@2.0.0: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + setprototypeof@1.2.0: {} + + sha.js@2.4.12: + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + to-buffer: 1.2.2 + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + socket.io-client@4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10): + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.4 + engine.io-client: 6.6.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-parser@4.2.4: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + + sonic-boom@2.8.0: + dependencies: + atomic-sleep: 1.0.0 + + split-on-first@1.1.0: {} + + split2@4.2.0: {} + + statuses@2.0.1: {} + + statuses@2.0.2: {} + + stream-chain@2.2.5: {} + + stream-json@1.9.1: + dependencies: + stream-chain: 2.2.5 + + stream-shift@1.0.3: {} + + strict-uri-encode@2.0.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + superstruct@1.0.4: {} + + superstruct@2.0.2: {} + + text-encoding-utf-8@1.0.2: {} + + thread-stream@0.15.2: + dependencies: + real-require: 0.1.0 + + to-buffer@1.2.2: + dependencies: + isarray: 2.0.5 + safe-buffer: 5.2.1 + typed-array-buffer: 1.0.3 + + toidentifier@1.0.1: {} + + tr46@0.0.3: {} + + tslib@1.14.1: {} + + tslib@2.8.1: {} + + tsx@4.20.5: + dependencies: + esbuild: 0.25.10 + get-tsconfig: 4.10.1 + optionalDependencies: + fsevents: 2.3.3 + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.1 + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typescript@5.9.2: {} + + ufo@1.6.1: {} + + uint8arrays@3.1.0: + dependencies: + multiformats: 9.9.0 + + uncrypto@0.1.3: {} + + undici-types@7.12.0: {} + + unpipe@1.0.0: {} + + unstorage@1.17.1(idb-keyval@6.2.2): + dependencies: + anymatch: 3.1.3 + chokidar: 4.0.3 + destr: 2.0.5 + h3: 1.15.4 + lru-cache: 10.4.3 + node-fetch-native: 1.6.7 + ofetch: 1.4.1 + ufo: 1.6.1 + optionalDependencies: + idb-keyval: 6.2.2 + + use-sync-external-store@1.2.0(react@18.3.1): + dependencies: + react: 18.3.1 + + use-sync-external-store@1.4.0(react@18.3.1): + dependencies: + react: 18.3.1 + + utf-8-validate@5.0.10: + dependencies: + node-gyp-build: 4.8.4 + + util-deprecate@1.0.2: {} + + util@0.12.5: + dependencies: + inherits: 2.0.4 + is-arguments: 1.2.0 + is-generator-function: 1.1.0 + is-typed-array: 1.1.15 + which-typed-array: 1.1.19 + + utils-merge@1.0.1: {} + + uuid@8.3.2: {} + + uuid@9.0.1: {} + + valtio@1.13.2(react@18.3.1): + dependencies: + derive-valtio: 0.1.0(valtio@1.13.2(react@18.3.1)) + proxy-compare: 2.6.0 + use-sync-external-store: 1.2.0(react@18.3.1) + optionalDependencies: + react: 18.3.1 + + vary@1.1.2: {} + + viem@2.23.2(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76): + dependencies: + '@noble/curves': 1.8.1 + '@noble/hashes': 1.7.1 + '@scure/bip32': 1.6.2 + '@scure/bip39': 1.5.4 + abitype: 1.0.8(typescript@5.9.2)(zod@3.25.76) + isows: 1.0.6(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + ox: 0.6.7(typescript@5.9.2)(zod@3.25.76) + ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + + viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4): + dependencies: + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.1.0(typescript@5.9.2)(zod@3.22.4) + isows: 1.0.7(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + ox: 0.9.6(typescript@5.9.2)(zod@3.22.4) + ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + + viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76): + dependencies: + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.1.0(typescript@5.9.2)(zod@3.25.76) + isows: 1.0.7(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + ox: 0.9.6(typescript@5.9.2)(zod@3.25.76) + ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + + viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11): + dependencies: + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.1.0(typescript@5.9.2)(zod@4.1.11) + isows: 1.0.7(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + ox: 0.9.6(typescript@5.9.2)(zod@4.1.11) + ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + + wagmi@2.17.4(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@4.1.11): + dependencies: + '@tanstack/react-query': 5.90.2(react@18.3.1) + '@wagmi/connectors': 5.11.1(@tanstack/react-query@5.90.2(react@18.3.1))(@wagmi/core@2.21.1(@tanstack/query-core@5.90.2)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.17.4(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76))(zod@3.25.76) + '@wagmi/core': 2.21.1(@tanstack/query-core@5.90.2)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + react: 18.3.1 + use-sync-external-store: 1.4.0(react@18.3.1) + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11) + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@tanstack/query-core' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - immer + - ioredis + - supports-color + - uploadthing + - utf-8-validate + - zod + + webextension-polyfill@0.10.0: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which-module@2.0.1: {} + + which-typed-array@1.1.19: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10): + optionalDependencies: + bufferutil: 4.0.9 + utf-8-validate: 5.0.10 + + ws@8.17.1(bufferutil@4.0.9)(utf-8-validate@5.0.10): + optionalDependencies: + bufferutil: 4.0.9 + utf-8-validate: 5.0.10 + + ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10): + optionalDependencies: + bufferutil: 4.0.9 + utf-8-validate: 5.0.10 + + ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10): + optionalDependencies: + bufferutil: 4.0.9 + utf-8-validate: 5.0.10 + + x402-express@0.6.1(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + dependencies: + '@coinbase/cdp-sdk': 1.38.2(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + express: 4.21.2 + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + x402: 0.6.1(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + zod: 3.25.76 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@solana/sysvars' + - '@tanstack/query-core' + - '@tanstack/react-query' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - debug + - encoding + - fastestsmallesttextencoderdecoder + - immer + - ioredis + - react + - supports-color + - typescript + - uploadthing + - utf-8-validate + - ws + + x402@0.6.1(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + dependencies: + '@scure/base': 1.2.6 + '@solana-program/compute-budget': 0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + '@solana-program/token': 0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + '@solana-program/token-2022': 0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)) + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11) + wagmi: 2.17.4(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@4.1.11) + zod: 3.25.76 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@solana/sysvars' + - '@tanstack/query-core' + - '@tanstack/react-query' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - fastestsmallesttextencoderdecoder + - immer + - ioredis + - react + - supports-color + - typescript + - uploadthing + - utf-8-validate + - ws + + xmlhttprequest-ssl@2.1.2: {} + + xtend@4.0.2: {} + + y18n@4.0.3: {} + + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + + zod@3.22.4: {} + + zod@3.25.76: {} + + zod@4.1.11: {} + + zustand@5.0.0(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)): + optionalDependencies: + react: 18.3.1 + use-sync-external-store: 1.4.0(react@18.3.1) + + zustand@5.0.3(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)): + optionalDependencies: + react: 18.3.1 + use-sync-external-store: 1.4.0(react@18.3.1) + + zustand@5.0.8(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)): + optionalDependencies: + react: 18.3.1 + use-sync-external-store: 1.4.0(react@18.3.1) diff --git a/docs/x402/demo/api/src/api.ts b/docs/x402/demo/api/src/api.ts new file mode 100644 index 00000000..86c98598 --- /dev/null +++ b/docs/x402/demo/api/src/api.ts @@ -0,0 +1,53 @@ +import express from "express"; +import { Network, paymentMiddleware, SolanaAddress } from "x402-express"; +import { config } from "dotenv"; +import path from "path"; + +config({ path: path.join(process.cwd(), '..', '.env') }); + +type Resource = `${string}://${string}`; + +const API_PORT = process.env.API_PORT || 4021; +const FACILITATOR_URL = process.env.FACILITATOR_URL as Resource || "http://localhost:3000"; +const NETWORK = (process.env.NETWORK || "solana-devnet") as Network; +const KORA_SIGNER_ADDRESS = process.env.KORA_SIGNER_ADDRESS as SolanaAddress; + +if (!KORA_SIGNER_ADDRESS) { + throw new Error("KORA_SIGNER_ADDRESS is not set"); +} + +const app = express(); + +app.use( + paymentMiddleware( + KORA_SIGNER_ADDRESS, + { + "GET /protected": { + price: "$0.0001", + network: NETWORK, + }, + }, + { + url: FACILITATOR_URL, + } + ), +); + +app.get("/protected", (req, res) => { + res.json({ + message: "Protected endpoint accessed successfully", + timestamp: new Date().toISOString(), + }); +}); + +app.get("/health", (req, res) => { + res.json({ status: "ok" }); +}); + + +app.listen(API_PORT, () => { + console.log(`Server listening at http://localhost:${API_PORT}`); +}); + +// curl -X GET http://localhost:4021/protected +// curl -X GET http://localhost:4021/health \ No newline at end of file diff --git a/docs/x402/demo/assets/x402-protocol-flow.png b/docs/x402/demo/assets/x402-protocol-flow.png new file mode 100644 index 0000000000000000000000000000000000000000..65b80f88c0ef416de206755d3eabc274a1c07593 GIT binary patch literal 384752 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 SeV8@_{#+c~>`&SSCH@DiF*m0G literal 0 HcmV?d00001 diff --git a/docs/x402/demo/client/package.json b/docs/x402/demo/client/package.json new file mode 100644 index 00000000..02ea4f51 --- /dev/null +++ b/docs/x402/demo/client/package.json @@ -0,0 +1,27 @@ +{ + "name": "kora-demo-client", + "version": "1.0.0", + "description": "", + "scripts": { + "start": "tsx src/index.ts", + "setup": "tsx src/setup.ts" + }, + "keywords": [ + "solana", + "kora", + "client" + ], + "author": "Solana Foundation", + "license": "MIT", + "packageManager": "pnpm@10.12.3", + "dependencies": { + "dotenv": "^17.2.0", + "gill": "^0.11.0", + "x402-fetch": "^0.6.0" + }, + "devDependencies": { + "@types/node": "^24.0.13", + "tsx": "^4.20.5", + "typescript": "^5.8.3" + } +} \ No newline at end of file diff --git a/docs/x402/demo/client/pnpm-lock.yaml b/docs/x402/demo/client/pnpm-lock.yaml new file mode 100644 index 00000000..837d8426 --- /dev/null +++ b/docs/x402/demo/client/pnpm-lock.yaml @@ -0,0 +1,4794 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + dotenv: + specifier: ^17.2.0 + version: 17.2.2 + gill: + specifier: ^0.11.0 + version: 0.11.0(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + x402-fetch: + specifier: ^0.6.0 + version: 0.6.0(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + devDependencies: + '@types/node': + specifier: ^24.0.13 + version: 24.5.2 + tsx: + specifier: ^4.20.5 + version: 4.20.5 + typescript: + specifier: ^5.8.3 + version: 5.9.2 + +packages: + + '@adraffy/ens-normalize@1.11.1': + resolution: {integrity: sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==} + + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} + engines: {node: '>=6.9.0'} + + '@base-org/account@1.1.1': + resolution: {integrity: sha512-IfVJPrDPhHfqXRDb89472hXkpvJuQQR7FDI9isLPHEqSYt/45whIoBxSPgZ0ssTt379VhQo4+87PWI1DoLSfAQ==} + + '@coinbase/wallet-sdk@3.9.3': + resolution: {integrity: sha512-N/A2DRIf0Y3PHc1XAMvbBUu4zisna6qAdqABMZwBMNEfWrXpAwx16pZGkYCLGE+Rvv1edbcB2LYDRnACNcmCiw==} + + '@coinbase/wallet-sdk@4.3.6': + resolution: {integrity: sha512-4q8BNG1ViL4mSAAvPAtpwlOs1gpC+67eQtgIwNvT3xyeyFFd+guwkc8bcX5rTmQhXpqnhzC4f0obACbP9CqMSA==} + + '@ecies/ciphers@0.2.4': + resolution: {integrity: sha512-t+iX+Wf5nRKyNzk8dviW3Ikb/280+aEJAnw9YXvCp2tYGPSkMki+NRY+8aNLmVFv3eNtMdvViPNOPxS8SZNP+w==} + engines: {bun: '>=1', deno: '>=2', node: '>=16'} + peerDependencies: + '@noble/ciphers': ^1.0.0 + + '@esbuild/aix-ppc64@0.25.10': + resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.10': + resolution: {integrity: sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.10': + resolution: {integrity: sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.10': + resolution: {integrity: sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.10': + resolution: {integrity: sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.10': + resolution: {integrity: sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.10': + resolution: {integrity: sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.10': + resolution: {integrity: sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.10': + resolution: {integrity: sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.10': + resolution: {integrity: sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.10': + resolution: {integrity: sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.10': + resolution: {integrity: sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.10': + resolution: {integrity: sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.10': + resolution: {integrity: sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.10': + resolution: {integrity: sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.10': + resolution: {integrity: sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.10': + resolution: {integrity: sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.10': + resolution: {integrity: sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.10': + resolution: {integrity: sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.10': + resolution: {integrity: sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.10': + resolution: {integrity: sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.10': + resolution: {integrity: sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.10': + resolution: {integrity: sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.10': + resolution: {integrity: sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.10': + resolution: {integrity: sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.10': + resolution: {integrity: sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@ethereumjs/common@3.2.0': + resolution: {integrity: sha512-pksvzI0VyLgmuEF2FA/JR/4/y6hcPq8OUail3/AvycBaW1d5VSauOZzqGvJ3RTmR4MU35lWE8KseKOsEhrFRBA==} + + '@ethereumjs/rlp@4.0.1': + resolution: {integrity: sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw==} + engines: {node: '>=14'} + hasBin: true + + '@ethereumjs/tx@4.2.0': + resolution: {integrity: sha512-1nc6VO4jtFd172BbSnTnDQVr9IYBFl1y4xPzZdtkrkKIncBCkdbgfdRV+MiTkJYAtTxvV12GRZLqBFT1PNK6Yw==} + engines: {node: '>=14'} + + '@ethereumjs/util@8.1.0': + resolution: {integrity: sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA==} + engines: {node: '>=14'} + + '@gemini-wallet/core@0.2.0': + resolution: {integrity: sha512-vv9aozWnKrrPWQ3vIFcWk7yta4hQW1Ie0fsNNPeXnjAxkbXr2hqMagEptLuMxpEP2W3mnRu05VDNKzcvAuuZDw==} + peerDependencies: + viem: '>=2.0.0' + + '@lit-labs/ssr-dom-shim@1.4.0': + resolution: {integrity: sha512-ficsEARKnmmW5njugNYKipTm4SFnbik7CXtoencDZzmzo/dQ+2Q0bgkzJuoJP20Aj0F+izzJjOqsnkd6F/o1bw==} + + '@lit/reactive-element@2.1.1': + resolution: {integrity: sha512-N+dm5PAYdQ8e6UlywyyrgI2t++wFGXfHx+dSJ1oBrg6FAxUj40jId++EaRm80MKX5JnlH1sBsyZ5h0bcZKemCg==} + + '@metamask/eth-json-rpc-provider@1.0.1': + resolution: {integrity: sha512-whiUMPlAOrVGmX8aKYVPvlKyG4CpQXiNNyt74vE1xb5sPvmx5oA7B/kOi/JdBvhGQq97U1/AVdXEdk2zkP8qyA==} + engines: {node: '>=14.0.0'} + + '@metamask/json-rpc-engine@7.3.3': + resolution: {integrity: sha512-dwZPq8wx9yV3IX2caLi9q9xZBw2XeIoYqdyihDDDpuHVCEiqadJLwqM3zy+uwf6F1QYQ65A8aOMQg1Uw7LMLNg==} + engines: {node: '>=16.0.0'} + + '@metamask/json-rpc-engine@8.0.2': + resolution: {integrity: sha512-IoQPmql8q7ABLruW7i4EYVHWUbF74yrp63bRuXV5Zf9BQwcn5H9Ww1eLtROYvI1bUXwOiHZ6qT5CWTrDc/t/AA==} + engines: {node: '>=16.0.0'} + + '@metamask/json-rpc-middleware-stream@7.0.2': + resolution: {integrity: sha512-yUdzsJK04Ev98Ck4D7lmRNQ8FPioXYhEUZOMS01LXW8qTvPGiRVXmVltj2p4wrLkh0vW7u6nv0mNl5xzC5Qmfg==} + engines: {node: '>=16.0.0'} + + '@metamask/object-multiplex@2.1.0': + resolution: {integrity: sha512-4vKIiv0DQxljcXwfpnbsXcfa5glMj5Zg9mqn4xpIWqkv6uJ2ma5/GtUfLFSxhlxnR8asRMv8dDmWya1Tc1sDFA==} + engines: {node: ^16.20 || ^18.16 || >=20} + + '@metamask/onboarding@1.0.1': + resolution: {integrity: sha512-FqHhAsCI+Vacx2qa5mAFcWNSrTcVGMNjzxVgaX8ECSny/BJ9/vgXP9V7WF/8vb9DltPeQkxr+Fnfmm6GHfmdTQ==} + + '@metamask/providers@16.1.0': + resolution: {integrity: sha512-znVCvux30+3SaUwcUGaSf+pUckzT5ukPRpcBmy+muBLC0yaWnBcvDqGfcsw6CBIenUdFrVoAFa8B6jsuCY/a+g==} + engines: {node: ^18.18 || >=20} + + '@metamask/rpc-errors@6.4.0': + resolution: {integrity: sha512-1ugFO1UoirU2esS3juZanS/Fo8C8XYocCuBpfZI5N7ECtoG+zu0wF+uWZASik6CkO6w9n/Iebt4iI4pT0vptpg==} + engines: {node: '>=16.0.0'} + + '@metamask/rpc-errors@7.0.2': + resolution: {integrity: sha512-YYYHsVYd46XwY2QZzpGeU4PSdRhHdxnzkB8piWGvJW2xbikZ3R+epAYEL4q/K8bh9JPTucsUdwRFnACor1aOYw==} + engines: {node: ^18.20 || ^20.17 || >=22} + + '@metamask/safe-event-emitter@2.0.0': + resolution: {integrity: sha512-/kSXhY692qiV1MXu6EeOZvg5nECLclxNXcKCxJ3cXQgYuRymRHpdx/t7JXfsK+JLjwA1e1c1/SBrlQYpusC29Q==} + + '@metamask/safe-event-emitter@3.1.2': + resolution: {integrity: sha512-5yb2gMI1BDm0JybZezeoX/3XhPDOtTbcFvpTXM9kxsoZjPZFh4XciqRbpD6N86HYZqWDhEaKUDuOyR0sQHEjMA==} + engines: {node: '>=12.0.0'} + + '@metamask/sdk-analytics@0.0.5': + resolution: {integrity: sha512-fDah+keS1RjSUlC8GmYXvx6Y26s3Ax1U9hGpWb6GSY5SAdmTSIqp2CvYy6yW0WgLhnYhW+6xERuD0eVqV63QIQ==} + + '@metamask/sdk-communication-layer@0.33.1': + resolution: {integrity: sha512-0bI9hkysxcfbZ/lk0T2+aKVo1j0ynQVTuB3sJ5ssPWlz+Z3VwveCkP1O7EVu1tsVVCb0YV5WxK9zmURu2FIiaA==} + peerDependencies: + cross-fetch: ^4.0.0 + eciesjs: '*' + eventemitter2: ^6.4.9 + readable-stream: ^3.6.2 + socket.io-client: ^4.5.1 + + '@metamask/sdk-install-modal-web@0.32.1': + resolution: {integrity: sha512-MGmAo6qSjf1tuYXhCu2EZLftq+DSt5Z7fsIKr2P+lDgdTPWgLfZB1tJKzNcwKKOdf6q9Qmmxn7lJuI/gq5LrKw==} + + '@metamask/sdk@0.33.1': + resolution: {integrity: sha512-1mcOQVGr9rSrVcbKPNVzbZ8eCl1K0FATsYH3WJ/MH4WcZDWGECWrXJPNMZoEAkLxWiMe8jOQBumg2pmcDa9zpQ==} + + '@metamask/superstruct@3.2.1': + resolution: {integrity: sha512-fLgJnDOXFmuVlB38rUN5SmU7hAFQcCjrg3Vrxz67KTY7YHFnSNEKvX4avmEBdOI0yTCxZjwMCFEqsC8k2+Wd3g==} + engines: {node: '>=16.0.0'} + + '@metamask/utils@11.8.1': + resolution: {integrity: sha512-DIbsNUyqWLFgqJlZxi1OOCMYvI23GqFCvNJAtzv8/WXWzJfnJnvp1M24j7VvUe3URBi3S86UgQ7+7aWU9p/cnQ==} + engines: {node: ^18.18 || ^20.14 || >=22} + + '@metamask/utils@5.0.2': + resolution: {integrity: sha512-yfmE79bRQtnMzarnKfX7AEJBwFTxvTyw3nBQlu/5rmGXrjAeAMltoGxO62TFurxrQAFMNa/fEjIHNvungZp0+g==} + engines: {node: '>=14.0.0'} + + '@metamask/utils@8.5.0': + resolution: {integrity: sha512-I6bkduevXb72TIM9q2LRO63JSsF9EXduh3sBr9oybNX2hNNpr/j1tEjXrsG0Uabm4MJ1xkGAQEMwifvKZIkyxQ==} + engines: {node: '>=16.0.0'} + + '@metamask/utils@9.3.0': + resolution: {integrity: sha512-w8CVbdkDrVXFJbfBSlDfafDR6BAkpDmv1bC1UJVCoVny5tW2RKAdn9i68Xf7asYT4TnUhl/hN4zfUiKQq9II4g==} + engines: {node: '>=16.0.0'} + + '@noble/ciphers@1.2.1': + resolution: {integrity: sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==} + engines: {node: ^14.21.3 || >=16} + + '@noble/ciphers@1.3.0': + resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/curves@1.4.2': + resolution: {integrity: sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==} + + '@noble/curves@1.8.0': + resolution: {integrity: sha512-j84kjAbzEnQHaSIhRPUmB3/eVXu2k3dKPl2LOrR8fSOIL+89U+7lV117EWHtq/GHM3ReGHM46iRBdZfpc4HRUQ==} + engines: {node: ^14.21.3 || >=16} + + '@noble/curves@1.8.1': + resolution: {integrity: sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==} + engines: {node: ^14.21.3 || >=16} + + '@noble/curves@1.9.1': + resolution: {integrity: sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==} + engines: {node: ^14.21.3 || >=16} + + '@noble/curves@1.9.7': + resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.4.0': + resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} + engines: {node: '>= 16'} + + '@noble/hashes@1.7.0': + resolution: {integrity: sha512-HXydb0DgzTpDPwbVeDGCG1gIu7X6+AuU6Zl6av/E/KG8LMsvPntvq+w17CHRpKBmN6Ybdrt1eP3k4cj8DJa78w==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.7.1': + resolution: {integrity: sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@paulmillr/qr@0.2.1': + resolution: {integrity: sha512-IHnV6A+zxU7XwmKFinmYjUcwlyK9+xkG3/s9KcQhI9BjQKycrJ1JRO+FbNYPwZiPKW3je/DR0k7w8/gLa5eaxQ==} + deprecated: 'The package is now available as "qr": npm install qr' + + '@reown/appkit-common@1.7.8': + resolution: {integrity: sha512-ridIhc/x6JOp7KbDdwGKY4zwf8/iK8EYBl+HtWrruutSLwZyVi5P8WaZa+8iajL6LcDcDF7LoyLwMTym7SRuwQ==} + + '@reown/appkit-controllers@1.7.8': + resolution: {integrity: sha512-IdXlJlivrlj6m63VsGLsjtPHHsTWvKGVzWIP1fXZHVqmK+rZCBDjCi9j267Rb9/nYRGHWBtlFQhO8dK35WfeDA==} + + '@reown/appkit-pay@1.7.8': + resolution: {integrity: sha512-OSGQ+QJkXx0FEEjlpQqIhT8zGJKOoHzVnyy/0QFrl3WrQTjCzg0L6+i91Ad5Iy1zb6V5JjqtfIFpRVRWN4M3pw==} + + '@reown/appkit-polyfills@1.7.8': + resolution: {integrity: sha512-W/kq786dcHHAuJ3IV2prRLEgD/2iOey4ueMHf1sIFjhhCGMynMkhsOhQMUH0tzodPqUgAC494z4bpIDYjwWXaA==} + + '@reown/appkit-scaffold-ui@1.7.8': + resolution: {integrity: sha512-RCeHhAwOrIgcvHwYlNWMcIDibdI91waaoEYBGw71inE0kDB8uZbE7tE6DAXJmDkvl0qPh+DqlC4QbJLF1FVYdQ==} + + '@reown/appkit-ui@1.7.8': + resolution: {integrity: sha512-1hjCKjf6FLMFzrulhl0Y9Vb9Fu4royE+SXCPSWh4VhZhWqlzUFc7kutnZKx8XZFVQH4pbBvY62SpRC93gqoHow==} + + '@reown/appkit-utils@1.7.8': + resolution: {integrity: sha512-8X7UvmE8GiaoitCwNoB86pttHgQtzy4ryHZM9kQpvjQ0ULpiER44t1qpVLXNM4X35O0v18W0Dk60DnYRMH2WRw==} + peerDependencies: + valtio: 1.13.2 + + '@reown/appkit-wallet@1.7.8': + resolution: {integrity: sha512-kspz32EwHIOT/eg/ZQbFPxgXq0B/olDOj3YMu7gvLEFz4xyOFd/wgzxxAXkp5LbG4Cp++s/elh79rVNmVFdB9A==} + + '@reown/appkit@1.7.8': + resolution: {integrity: sha512-51kTleozhA618T1UvMghkhKfaPcc9JlKwLJ5uV+riHyvSoWPKPRIa5A6M1Wano5puNyW0s3fwywhyqTHSilkaA==} + + '@safe-global/safe-apps-provider@0.18.6': + resolution: {integrity: sha512-4LhMmjPWlIO8TTDC2AwLk44XKXaK6hfBTWyljDm0HQ6TWlOEijVWNrt2s3OCVMSxlXAcEzYfqyu1daHZooTC2Q==} + + '@safe-global/safe-apps-sdk@9.1.0': + resolution: {integrity: sha512-N5p/ulfnnA2Pi2M3YeWjULeWbjo7ei22JwU/IXnhoHzKq3pYCN6ynL9mJBOlvDVv892EgLPCWCOwQk/uBT2v0Q==} + + '@safe-global/safe-gateway-typescript-sdk@3.23.1': + resolution: {integrity: sha512-6ORQfwtEJYpalCeVO21L4XXGSdbEMfyp2hEv6cP82afKXSwvse6d3sdelgaPWUxHIsFRkWvHDdzh8IyyKHZKxw==} + engines: {node: '>=16'} + + '@scure/base@1.1.9': + resolution: {integrity: sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==} + + '@scure/base@1.2.6': + resolution: {integrity: sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==} + + '@scure/bip32@1.4.0': + resolution: {integrity: sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==} + + '@scure/bip32@1.6.2': + resolution: {integrity: sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==} + + '@scure/bip32@1.7.0': + resolution: {integrity: sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==} + + '@scure/bip39@1.3.0': + resolution: {integrity: sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==} + + '@scure/bip39@1.5.4': + resolution: {integrity: sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==} + + '@scure/bip39@1.6.0': + resolution: {integrity: sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==} + + '@socket.io/component-emitter@3.1.2': + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + + '@solana-program/address-lookup-table@0.7.0': + resolution: {integrity: sha512-dzCeIO5LtiK3bIg0AwO+TPeGURjSG2BKt0c4FRx7105AgLy7uzTktpUzUj6NXAK9SzbirI8HyvHUvw1uvL8O9A==} + peerDependencies: + '@solana/kit': ^2.1.0 + + '@solana-program/compute-budget@0.8.0': + resolution: {integrity: sha512-qPKxdxaEsFxebZ4K5RPuy7VQIm/tfJLa1+Nlt3KNA8EYQkz9Xm8htdoEaXVrer9kpgzzp9R3I3Bh6omwCM06tQ==} + peerDependencies: + '@solana/kit': ^2.1.0 + + '@solana-program/system@0.7.0': + resolution: {integrity: sha512-FKTBsKHpvHHNc1ATRm7SlC5nF/VdJtOSjldhcyfMN9R7xo712Mo2jHIzvBgn8zQO5Kg0DcWuKB7268Kv1ocicw==} + peerDependencies: + '@solana/kit': ^2.1.0 + + '@solana-program/token-2022@0.4.2': + resolution: {integrity: sha512-zIpR5t4s9qEU3hZKupzIBxJ6nUV5/UVyIT400tu9vT1HMs5JHxaTTsb5GUhYjiiTvNwU0MQavbwc4Dl29L0Xvw==} + peerDependencies: + '@solana/kit': ^2.1.0 + '@solana/sysvars': ^2.1.0 + + '@solana-program/token@0.5.1': + resolution: {integrity: sha512-bJvynW5q9SFuVOZ5vqGVkmaPGA0MCC+m9jgJj1nk5m20I389/ms69ASnhWGoOPNcie7S9OwBX0gTj2fiyWpfag==} + peerDependencies: + '@solana/kit': ^2.1.0 + + '@solana/accounts@2.3.0': + resolution: {integrity: sha512-QgQTj404Z6PXNOyzaOpSzjgMOuGwG8vC66jSDB+3zHaRcEPRVRd2sVSrd1U6sHtnV3aiaS6YyDuPQMheg4K2jw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/addresses@2.3.0': + resolution: {integrity: sha512-ypTNkY2ZaRFpHLnHAgaW8a83N0/WoqdFvCqf4CQmnMdFsZSdC7qOwcbd7YzdaQn9dy+P2hybewzB+KP7LutxGA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/assertions@2.3.0': + resolution: {integrity: sha512-Ekoet3khNg3XFLN7MIz8W31wPQISpKUGDGTylLptI+JjCDWx3PIa88xjEMqFo02WJ8sBj2NLV64Xg1sBcsHjZQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/codecs-core@2.3.0': + resolution: {integrity: sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/codecs-data-structures@2.3.0': + resolution: {integrity: sha512-qvU5LE5DqEdYMYgELRHv+HMOx73sSoV1ZZkwIrclwUmwTbTaH8QAJURBj0RhQ/zCne7VuLLOZFFGv6jGigWhSw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/codecs-numbers@2.3.0': + resolution: {integrity: sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/codecs-strings@2.3.0': + resolution: {integrity: sha512-y5pSBYwzVziXu521hh+VxqUtp0hYGTl1eWGoc1W+8mdvBdC1kTqm/X7aYQw33J42hw03JjryvYOvmGgk3Qz/Ug==} + engines: {node: '>=20.18.0'} + peerDependencies: + fastestsmallesttextencoderdecoder: ^1.0.22 + typescript: '>=5.3.3' + + '@solana/codecs@2.3.0': + resolution: {integrity: sha512-JVqGPkzoeyU262hJGdH64kNLH0M+Oew2CIPOa/9tR3++q2pEd4jU2Rxdfye9sd0Ce3XJrR5AIa8ZfbyQXzjh+g==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/errors@2.3.0': + resolution: {integrity: sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ==} + engines: {node: '>=20.18.0'} + hasBin: true + peerDependencies: + typescript: '>=5.3.3' + + '@solana/fast-stable-stringify@2.3.0': + resolution: {integrity: sha512-KfJPrMEieUg6D3hfQACoPy0ukrAV8Kio883llt/8chPEG3FVTX9z/Zuf4O01a15xZmBbmQ7toil2Dp0sxMJSxw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/functional@2.3.0': + resolution: {integrity: sha512-AgsPh3W3tE+nK3eEw/W9qiSfTGwLYEvl0rWaxHht/lRcuDVwfKRzeSa5G79eioWFFqr+pTtoCr3D3OLkwKz02Q==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/instructions@2.3.0': + resolution: {integrity: sha512-PLMsmaIKu7hEAzyElrk2T7JJx4D+9eRwebhFZpy2PXziNSmFF929eRHKUsKqBFM3cYR1Yy3m6roBZfA+bGE/oQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/keys@2.3.0': + resolution: {integrity: sha512-ZVVdga79pNH+2pVcm6fr2sWz9HTwfopDVhYb0Lh3dh+WBmJjwkabXEIHey2rUES7NjFa/G7sV8lrUn/v8LDCCQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/kit@2.3.0': + resolution: {integrity: sha512-sb6PgwoW2LjE5oTFu4lhlS/cGt/NB3YrShEyx7JgWFWysfgLdJnhwWThgwy/4HjNsmtMrQGWVls0yVBHcMvlMQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/nominal-types@2.3.0': + resolution: {integrity: sha512-uKlMnlP4PWW5UTXlhKM8lcgIaNj8dvd8xO4Y9l+FVvh9RvW2TO0GwUO6JCo7JBzCB0PSqRJdWWaQ8pu1Ti/OkA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/options@2.3.0': + resolution: {integrity: sha512-PPnnZBRCWWoZQ11exPxf//DRzN2C6AoFsDI/u2AsQfYih434/7Kp4XLpfOMT/XESi+gdBMFNNfbES5zg3wAIkw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/programs@2.3.0': + resolution: {integrity: sha512-UXKujV71VCI5uPs+cFdwxybtHZAIZyQkqDiDnmK+DawtOO9mBn4Nimdb/6RjR2CXT78mzO9ZCZ3qfyX+ydcB7w==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/promises@2.3.0': + resolution: {integrity: sha512-GjVgutZKXVuojd9rWy1PuLnfcRfqsaCm7InCiZc8bqmJpoghlyluweNc7ml9Y5yQn1P2IOyzh9+p/77vIyNybQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-api@2.3.0': + resolution: {integrity: sha512-UUdiRfWoyYhJL9PPvFeJr4aJ554ob2jXcpn4vKmRVn9ire0sCbpQKYx6K8eEKHZWXKrDW8IDspgTl0gT/aJWVg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-parsed-types@2.3.0': + resolution: {integrity: sha512-B5pHzyEIbBJf9KHej+zdr5ZNAdSvu7WLU2lOUPh81KHdHQs6dEb310LGxcpCc7HVE8IEdO20AbckewDiAN6OCg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-spec-types@2.3.0': + resolution: {integrity: sha512-xQsb65lahjr8Wc9dMtP7xa0ZmDS8dOE2ncYjlvfyw/h4mpdXTUdrSMi6RtFwX33/rGuztQ7Hwaid5xLNSLvsFQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-spec@2.3.0': + resolution: {integrity: sha512-fA2LMX4BMixCrNB2n6T83AvjZ3oUQTu7qyPLyt8gHQaoEAXs8k6GZmu6iYcr+FboQCjUmRPgMaABbcr9j2J9Sw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-subscriptions-api@2.3.0': + resolution: {integrity: sha512-9mCjVbum2Hg9KGX3LKsrI5Xs0KX390lS+Z8qB80bxhar6MJPugqIPH8uRgLhCW9GN3JprAfjRNl7our8CPvsPQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-subscriptions-channel-websocket@2.3.0': + resolution: {integrity: sha512-2oL6ceFwejIgeWzbNiUHI2tZZnaOxNTSerszcin7wYQwijxtpVgUHiuItM/Y70DQmH9sKhmikQp+dqeGalaJxw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + ws: ^8.18.0 + + '@solana/rpc-subscriptions-spec@2.3.0': + resolution: {integrity: sha512-rdmVcl4PvNKQeA2l8DorIeALCgJEMSu7U8AXJS1PICeb2lQuMeaR+6cs/iowjvIB0lMVjYN2sFf6Q3dJPu6wWg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-subscriptions@2.3.0': + resolution: {integrity: sha512-Uyr10nZKGVzvCOqwCZgwYrzuoDyUdwtgQRefh13pXIrdo4wYjVmoLykH49Omt6abwStB0a4UL5gX9V4mFdDJZg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-transformers@2.3.0': + resolution: {integrity: sha512-UuHYK3XEpo9nMXdjyGKkPCOr7WsZsxs7zLYDO1A5ELH3P3JoehvrDegYRAGzBS2VKsfApZ86ZpJToP0K3PhmMA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-transport-http@2.3.0': + resolution: {integrity: sha512-HFKydmxGw8nAF5N+S0NLnPBDCe5oMDtI2RAmW8DMqP4U3Zxt2XWhvV1SNkAldT5tF0U1vP+is6fHxyhk4xqEvg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-types@2.3.0': + resolution: {integrity: sha512-O09YX2hED2QUyGxrMOxQ9GzH1LlEwwZWu69QbL4oYmIf6P5dzEEHcqRY6L1LsDVqc/dzAdEs/E1FaPrcIaIIPw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc@2.3.0': + resolution: {integrity: sha512-ZWN76iNQAOCpYC7yKfb3UNLIMZf603JckLKOOLTHuy9MZnTN8XV6uwvDFhf42XvhglgUjGCEnbUqWtxQ9pa/pQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/signers@2.3.0': + resolution: {integrity: sha512-OSv6fGr/MFRx6J+ZChQMRqKNPGGmdjkqarKkRzkwmv7v8quWsIRnJT5EV8tBy3LI4DLO/A8vKiNSPzvm1TdaiQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/subscribable@2.3.0': + resolution: {integrity: sha512-DkgohEDbMkdTWiKAoatY02Njr56WXx9e/dKKfmne8/Ad6/2llUIrax78nCdlvZW9quXMaXPTxZvdQqo9N669Og==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/sysvars@2.3.0': + resolution: {integrity: sha512-LvjADZrpZ+CnhlHqfI5cmsRzX9Rpyb1Ox2dMHnbsRNzeKAMhu9w4ZBIaeTdO322zsTr509G1B+k2ABD3whvUBA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/transaction-confirmation@2.3.0': + resolution: {integrity: sha512-UiEuiHCfAAZEKdfne/XljFNJbsKAe701UQHKXEInYzIgBjRbvaeYZlBmkkqtxwcasgBTOmEaEKT44J14N9VZDw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/transaction-messages@2.3.0': + resolution: {integrity: sha512-bgqvWuy3MqKS5JdNLH649q+ngiyOu5rGS3DizSnWwYUd76RxZl1kN6CoqHSrrMzFMvis6sck/yPGG3wqrMlAww==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/transactions@2.3.0': + resolution: {integrity: sha512-LnTvdi8QnrQtuEZor5Msje61sDpPstTVwKg4y81tNxDhiyomjuvnSNLAq6QsB9gIxUqbNzPZgOG9IU4I4/Uaug==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@tanstack/query-core@5.90.2': + resolution: {integrity: sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==} + + '@tanstack/react-query@5.90.2': + resolution: {integrity: sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw==} + peerDependencies: + react: ^18 || ^19 + + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/lodash@4.17.20': + resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node@24.5.2': + resolution: {integrity: sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@wagmi/connectors@5.11.1': + resolution: {integrity: sha512-2jbx6z8rk/srQdbnKF2viCc3ke02HSMQU5EZS90jWoJ1t4+Fn3+bhphgVeJ1LnVFWsuEBSuIcY63iic4emwGfg==} + peerDependencies: + '@wagmi/core': 2.21.1 + typescript: '>=5.0.4' + viem: 2.x + peerDependenciesMeta: + typescript: + optional: true + + '@wagmi/core@2.21.1': + resolution: {integrity: sha512-uG0Cujm24acrFYqbi1RGw9MRMLTGVKvyv5OAJT+2pkcasM9PP8eJCzhlvUxK9IYVXXnbaAT8JX/rXEurOSM6mg==} + peerDependencies: + '@tanstack/query-core': '>=5.0.0' + typescript: '>=5.0.4' + viem: 2.x + peerDependenciesMeta: + '@tanstack/query-core': + optional: true + typescript: + optional: true + + '@walletconnect/core@2.21.0': + resolution: {integrity: sha512-o6R7Ua4myxR8aRUAJ1z3gT9nM+jd2B2mfamu6arzy1Cc6vi10fIwFWb6vg3bC8xJ6o9H3n/cN5TOW3aA9Y1XVw==} + engines: {node: '>=18'} + + '@walletconnect/core@2.21.1': + resolution: {integrity: sha512-Tp4MHJYcdWD846PH//2r+Mu4wz1/ZU/fr9av1UWFiaYQ2t2TPLDiZxjLw54AAEpMqlEHemwCgiRiAmjR1NDdTQ==} + engines: {node: '>=18'} + + '@walletconnect/environment@1.0.1': + resolution: {integrity: sha512-T426LLZtHj8e8rYnKfzsw1aG6+M0BT1ZxayMdv/p8yM0MU+eJDISqNY3/bccxRr4LrF9csq02Rhqt08Ibl0VRg==} + + '@walletconnect/ethereum-provider@2.21.1': + resolution: {integrity: sha512-SSlIG6QEVxClgl1s0LMk4xr2wg4eT3Zn/Hb81IocyqNSGfXpjtawWxKxiC5/9Z95f1INyBD6MctJbL/R1oBwIw==} + + '@walletconnect/events@1.0.1': + resolution: {integrity: sha512-NPTqaoi0oPBVNuLv7qPaJazmGHs5JGyO8eEAk5VGKmJzDR7AHzD4k6ilox5kxk1iwiOnFopBOOMLs86Oa76HpQ==} + + '@walletconnect/heartbeat@1.2.2': + resolution: {integrity: sha512-uASiRmC5MwhuRuf05vq4AT48Pq8RMi876zV8rr8cV969uTOzWdB/k+Lj5yI2PBtB1bGQisGen7MM1GcZlQTBXw==} + + '@walletconnect/jsonrpc-http-connection@1.0.8': + resolution: {integrity: sha512-+B7cRuaxijLeFDJUq5hAzNyef3e3tBDIxyaCNmFtjwnod5AGis3RToNqzFU33vpVcxFhofkpE7Cx+5MYejbMGw==} + + '@walletconnect/jsonrpc-provider@1.0.14': + resolution: {integrity: sha512-rtsNY1XqHvWj0EtITNeuf8PHMvlCLiS3EjQL+WOkxEOA4KPxsohFnBDeyPYiNm4ZvkQdLnece36opYidmtbmow==} + + '@walletconnect/jsonrpc-types@1.0.4': + resolution: {integrity: sha512-P6679fG/M+wuWg9TY8mh6xFSdYnFyFjwFelxyISxMDrlbXokorEVXYOxiqEbrU3x1BmBoCAJJ+vtEaEoMlpCBQ==} + + '@walletconnect/jsonrpc-utils@1.0.8': + resolution: {integrity: sha512-vdeb03bD8VzJUL6ZtzRYsFMq1eZQcM3EAzT0a3st59dyLfJ0wq+tKMpmGH7HlB7waD858UWgfIcudbPFsbzVdw==} + + '@walletconnect/jsonrpc-ws-connection@1.0.16': + resolution: {integrity: sha512-G81JmsMqh5nJheE1mPst1W0WfVv0SG3N7JggwLLGnI7iuDZJq8cRJvQwLGKHn5H1WTW7DEPCo00zz5w62AbL3Q==} + + '@walletconnect/keyvaluestorage@1.1.1': + resolution: {integrity: sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA==} + peerDependencies: + '@react-native-async-storage/async-storage': 1.x + peerDependenciesMeta: + '@react-native-async-storage/async-storage': + optional: true + + '@walletconnect/logger@2.1.2': + resolution: {integrity: sha512-aAb28I3S6pYXZHQm5ESB+V6rDqIYfsnHaQyzFbwUUBFY4H0OXx/YtTl8lvhUNhMMfb9UxbwEBS253TlXUYJWSw==} + + '@walletconnect/relay-api@1.0.11': + resolution: {integrity: sha512-tLPErkze/HmC9aCmdZOhtVmYZq1wKfWTJtygQHoWtgg722Jd4homo54Cs4ak2RUFUZIGO2RsOpIcWipaua5D5Q==} + + '@walletconnect/relay-auth@1.1.0': + resolution: {integrity: sha512-qFw+a9uRz26jRCDgL7Q5TA9qYIgcNY8jpJzI1zAWNZ8i7mQjaijRnWFKsCHAU9CyGjvt6RKrRXyFtFOpWTVmCQ==} + + '@walletconnect/safe-json@1.0.2': + resolution: {integrity: sha512-Ogb7I27kZ3LPC3ibn8ldyUr5544t3/STow9+lzz7Sfo808YD7SBWk7SAsdBFlYgP2zDRy2hS3sKRcuSRM0OTmA==} + + '@walletconnect/sign-client@2.21.0': + resolution: {integrity: sha512-z7h+PeLa5Au2R591d/8ZlziE0stJvdzP9jNFzFolf2RG/OiXulgFKum8PrIyXy+Rg2q95U9nRVUF9fWcn78yBA==} + + '@walletconnect/sign-client@2.21.1': + resolution: {integrity: sha512-QaXzmPsMnKGV6tc4UcdnQVNOz4zyXgarvdIQibJ4L3EmLat73r5ZVl4c0cCOcoaV7rgM9Wbphgu5E/7jNcd3Zg==} + + '@walletconnect/time@1.0.2': + resolution: {integrity: sha512-uzdd9woDcJ1AaBZRhqy5rNC9laqWGErfc4dxA9a87mPdKOgWMD85mcFo9dIYIts/Jwocfwn07EC6EzclKubk/g==} + + '@walletconnect/types@2.21.0': + resolution: {integrity: sha512-ll+9upzqt95ZBWcfkOszXZkfnpbJJ2CmxMfGgE5GmhdxxxCcO5bGhXkI+x8OpiS555RJ/v/sXJYMSOLkmu4fFw==} + + '@walletconnect/types@2.21.1': + resolution: {integrity: sha512-UeefNadqP6IyfwWC1Yi7ux+ljbP2R66PLfDrDm8izmvlPmYlqRerJWJvYO4t0Vvr9wrG4Ko7E0c4M7FaPKT/sQ==} + + '@walletconnect/universal-provider@2.21.0': + resolution: {integrity: sha512-mtUQvewt+X0VBQay/xOJBvxsB3Xsm1lTwFjZ6WUwSOTR1X+FNb71hSApnV5kbsdDIpYPXeQUbGt2se1n5E5UBg==} + + '@walletconnect/universal-provider@2.21.1': + resolution: {integrity: sha512-Wjx9G8gUHVMnYfxtasC9poGm8QMiPCpXpbbLFT+iPoQskDDly8BwueWnqKs4Mx2SdIAWAwuXeZ5ojk5qQOxJJg==} + + '@walletconnect/utils@2.21.0': + resolution: {integrity: sha512-zfHLiUoBrQ8rP57HTPXW7rQMnYxYI4gT9yTACxVW6LhIFROTF6/ytm5SKNoIvi4a5nX5dfXG4D9XwQUCu8Ilig==} + + '@walletconnect/utils@2.21.1': + resolution: {integrity: sha512-VPZvTcrNQCkbGOjFRbC24mm/pzbRMUq2DSQoiHlhh0X1U7ZhuIrzVtAoKsrzu6rqjz0EEtGxCr3K1TGRqDG4NA==} + + '@walletconnect/window-getters@1.0.1': + resolution: {integrity: sha512-vHp+HqzGxORPAN8gY03qnbTMnhqIwjeRJNOMOAzePRg4xVEEE2WvYsI9G2NMjOknA8hnuYbU3/hwLcKbjhc8+Q==} + + '@walletconnect/window-metadata@1.0.1': + resolution: {integrity: sha512-9koTqyGrM2cqFRW517BPY/iEtUDx2r1+Pwwu5m7sJ7ka79wi3EyqhqcICk/yDmv6jAS1rjKgTKXlEhanYjijcA==} + + abitype@1.0.8: + resolution: {integrity: sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==} + peerDependencies: + typescript: '>=5.0.4' + zod: ^3 >=3.22.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + + abitype@1.1.0: + resolution: {integrity: sha512-6Vh4HcRxNMLA0puzPjM5GBgT4aAcFGKZzSgAXvuZ27shJP6NEpielTuqbBmZILR5/xd0PizkBGy5hReKz9jl5A==} + peerDependencies: + typescript: '>=5.0.4' + zod: ^3.22.0 || ^4.0.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + + abitype@1.1.1: + resolution: {integrity: sha512-Loe5/6tAgsBukY95eGaPSDmQHIjRZYQq8PB1MpsNccDIK8WiV+Uw6WzaIXipvaxTEL2yEB0OpEaQv3gs8pkS9Q==} + peerDependencies: + typescript: '>=5.0.4' + zod: ^3.22.0 || ^4.0.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + async-mutex@0.2.6: + resolution: {integrity: sha512-Hs4R+4SPgamu6rSGW8C7cV9gaWUKEHykfzCCvIRuaVv636Ju10ZdeUbvb4TBEW0INuq2DHZqXbK4Nd3yG4RaRw==} + + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + base-x@5.0.1: + resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + big.js@6.2.2: + resolution: {integrity: sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ==} + + bn.js@5.2.2: + resolution: {integrity: sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==} + + bowser@2.12.1: + resolution: {integrity: sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==} + + bs58@6.0.0: + resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + bufferutil@4.0.9: + resolution: {integrity: sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==} + engines: {node: '>=6.14.2'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + + clsx@1.2.1: + resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} + engines: {node: '>=6'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@14.0.1: + resolution: {integrity: sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==} + engines: {node: '>=20'} + + cookie-es@1.2.2: + resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + cross-fetch@3.2.0: + resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==} + + cross-fetch@4.1.0: + resolution: {integrity: sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==} + + crossws@0.3.5: + resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} + + date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} + + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + + debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + + decode-uri-component@0.2.2: + resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} + engines: {node: '>=0.10'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + + derive-valtio@0.1.0: + resolution: {integrity: sha512-OCg2UsLbXK7GmmpzMXhYkdO64vhJ1ROUUGaTFyHjVwEdMEcTTRj7W1TxLbSBxdY8QLBPCcp66MTyaSy0RpO17A==} + peerDependencies: + valtio: '*' + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + detect-browser@5.3.0: + resolution: {integrity: sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w==} + + dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + + dotenv@17.2.2: + resolution: {integrity: sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + duplexify@4.1.3: + resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} + + eciesjs@0.4.15: + resolution: {integrity: sha512-r6kEJXDKecVOCj2nLMuXK/FCPeurW33+3JRpfXVbjLja3XUYFfD9I/JBreH6sUyzcm3G/YQboBjMla6poKeSdA==} + engines: {bun: '>=1', deno: '>=2', node: '>=16'} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + encode-utf8@1.0.3: + resolution: {integrity: sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + engine.io-client@6.6.3: + resolution: {integrity: sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==} + + engine.io-parser@5.2.3: + resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} + engines: {node: '>=10.0.0'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-toolkit@1.33.0: + resolution: {integrity: sha512-X13Q/ZSc+vsO1q600bvNK4bxgXMkHcf//RxCmYDaRY5DAcT+eoXjY5hoAPGMdRnWQjvyLEcyauG3b6hz76LNqg==} + + esbuild@0.25.10: + resolution: {integrity: sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==} + engines: {node: '>=18'} + hasBin: true + + eth-block-tracker@7.1.0: + resolution: {integrity: sha512-8YdplnuE1IK4xfqpf4iU7oBxnOYAc35934o083G8ao+8WM8QQtt/mVlAY6yIAdY1eMeLqg4Z//PZjJGmWGPMRg==} + engines: {node: '>=14.0.0'} + + eth-json-rpc-filters@6.0.1: + resolution: {integrity: sha512-ITJTvqoCw6OVMLs7pI8f4gG92n/St6x80ACtHodeS+IXmO0w+t1T5OOzfSt7KLSMLRkVUoexV7tztLgDxg+iig==} + engines: {node: '>=14.0.0'} + + eth-query@2.1.2: + resolution: {integrity: sha512-srES0ZcvwkR/wd5OQBRA1bIJMww1skfGS0s8wlwK3/oNP4+wnds60krvu5R1QbpRQjMmpG5OMIWro5s7gvDPsA==} + + eth-rpc-errors@4.0.3: + resolution: {integrity: sha512-Z3ymjopaoft7JDoxZcEb3pwdGh7yiYMhOwm2doUt6ASXlMavpNlK6Cre0+IMl2VSGyEU9rkiperQhp5iRxn5Pg==} + + ethereum-cryptography@2.2.1: + resolution: {integrity: sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==} + + eventemitter2@6.4.9: + resolution: {integrity: sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==} + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + extension-port-stream@3.0.0: + resolution: {integrity: sha512-an2S5quJMiy5bnZKEf6AkfH/7r8CzHvhchU40gxN+OM6HPhe7Z9T1FUychcf2M9PpPOO0Hf7BAEfJkw2TDIBDw==} + engines: {node: '>=12.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-redact@3.5.0: + resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} + engines: {node: '>=6'} + + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + + fastestsmallesttextencoderdecoder@1.0.22: + resolution: {integrity: sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==} + + filter-obj@1.1.0: + resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==} + engines: {node: '>=0.10.0'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.10.1: + resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + + gill@0.11.0: + resolution: {integrity: sha512-kEdLdSMFuvaDl3O38rTAXLQdyOG0QEtFQRG4kDArx3pDIh3T/5PhhPZ1zmmC0q7FpVZk7Eks2W6NdDKQjSq/6A==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5' + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + h3@1.15.4: + resolution: {integrity: sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hono@4.9.8: + resolution: {integrity: sha512-JW8Bb4RFWD9iOKxg5PbUarBYGM99IcxFl2FPBo2gSJO11jjUDqlP1Bmfyqt8Z/dGhIQ63PMA9LdcLefXyIasyg==} + engines: {node: '>=16.9.0'} + + idb-keyval@6.2.1: + resolution: {integrity: sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==} + + idb-keyval@6.2.2: + resolution: {integrity: sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + iron-webcrypto@1.2.1: + resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-function@1.1.0: + resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} + engines: {node: '>= 0.4'} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isows@1.0.6: + resolution: {integrity: sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw==} + peerDependencies: + ws: '*' + + isows@1.0.7: + resolution: {integrity: sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==} + peerDependencies: + ws: '*' + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + json-rpc-engine@6.1.0: + resolution: {integrity: sha512-NEdLrtrq1jUZyfjkr9OCz9EzCNhnRyWtt1PAnvnhwy6e8XETS0Dtc+ZNCO2gvuAoKsIn2+vCSowXTYE4CkgnAQ==} + engines: {node: '>=10.0.0'} + + json-rpc-random-id@1.0.1: + resolution: {integrity: sha512-RJ9YYNCkhVDBuP4zN5BBtYAzEl03yq/jIIsyif0JY9qyJuQQZNeDK7anAPKKlyEtLSj2s8h6hNh2F8zO5q7ScA==} + + keccak@3.0.4: + resolution: {integrity: sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q==} + engines: {node: '>=10.0.0'} + + keyvaluestorage-interface@1.0.0: + resolution: {integrity: sha512-8t6Q3TclQ4uZynJY9IGr2+SsIGwK9JHcO6ootkHCGA0CrQCRy+VkouYNO2xicET6b9al7QKzpebNow+gkpCL8g==} + + lit-element@4.2.1: + resolution: {integrity: sha512-WGAWRGzirAgyphK2urmYOV72tlvnxw7YfyLDgQ+OZnM9vQQBQnumQ7jUJe6unEzwGU3ahFOjuz1iz1jjrpCPuw==} + + lit-html@3.3.1: + resolution: {integrity: sha512-S9hbyDu/vs1qNrithiNyeyv64c9yqiW9l+DBgI18fL+MTvOtWoFR0FWiyq1TxaYef5wNlpEmzlXoBlZEO+WjoA==} + + lit@3.3.0: + resolution: {integrity: sha512-DGVsqsOIHBww2DqnuZzW7QsuCdahp50ojuDaBPC7jUDRpYoH0z7kHBBYZewRzer75FwtrkmkKk7iOAwSaWdBmw==} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + micro-ftch@0.3.1: + resolution: {integrity: sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg==} + + mipd@0.0.7: + resolution: {integrity: sha512-aAPZPNDQ3uMTdKbuO2YmAw2TxLHO0moa4YKAyETM/DTj5FloZo+a+8tU+iv4GmW+sOxKLSRwcSFuczk+Cpt6fg==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + multiformats@9.9.0: + resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==} + + node-addon-api@2.0.2: + resolution: {integrity: sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==} + + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + + node-mock-http@1.0.3: + resolution: {integrity: sha512-jN8dK25fsfnMrVsEhluUTPkBFY+6ybu7jSB1n+ri/vOGjJxU8J9CZhpSGkHXSkFjtUhbmoncG/YG9ta5Ludqog==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + obj-multiplex@1.0.0: + resolution: {integrity: sha512-0GNJAOsHoBHeNTvl5Vt6IWnpUEcc3uSRxzBri7EDyIcMgYvnY2JL2qdeV5zTMjWQX5OHcD5amcW2HFfDh0gjIA==} + + ofetch@1.4.1: + resolution: {integrity: sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==} + + on-exit-leak-free@0.2.0: + resolution: {integrity: sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg==} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + openapi-fetch@0.13.8: + resolution: {integrity: sha512-yJ4QKRyNxE44baQ9mY5+r/kAzZ8yXMemtNAOFwOzRXJscdjSxxzWSNlyBAr+o5JjkUw9Lc3W7OIoca0cY3PYnQ==} + + openapi-typescript-helpers@0.0.15: + resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==} + + ox@0.6.7: + resolution: {integrity: sha512-17Gk/eFsFRAZ80p5eKqv89a57uXjd3NgIf1CaXojATPBuujVc/fQSVhBeAU9JCRB+k7J50WQAyWTxK19T9GgbA==} + peerDependencies: + typescript: '>=5.4.0' + peerDependenciesMeta: + typescript: + optional: true + + ox@0.6.9: + resolution: {integrity: sha512-wi5ShvzE4eOcTwQVsIPdFr+8ycyX+5le/96iAJutaZAvCes1J0+RvpEPg5QDPDiaR0XQQAvZVl7AwqQcINuUug==} + peerDependencies: + typescript: '>=5.4.0' + peerDependenciesMeta: + typescript: + optional: true + + ox@0.9.6: + resolution: {integrity: sha512-8SuCbHPvv2eZLYXrNmC0EC12rdzXQLdhnOMlHDW2wiCPLxBrOOJwX5L5E61by+UjTPOryqQiRSnjIKCI+GykKg==} + peerDependencies: + typescript: '>=5.4.0' + peerDependenciesMeta: + typescript: + optional: true + + ox@0.9.7: + resolution: {integrity: sha512-KX9Lvv0Rd+SKZXcT6Y85cuYbkmrQbK8Bz26k+s3X4EiT5bSU/hTDNSK/ApBeorJeIaOZbxEmJD2hHW0q1vIDEA==} + peerDependencies: + typescript: '>=5.4.0' + peerDependenciesMeta: + typescript: + optional: true + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + pify@3.0.0: + resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} + engines: {node: '>=4'} + + pify@5.0.0: + resolution: {integrity: sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==} + engines: {node: '>=10'} + + pino-abstract-transport@0.5.0: + resolution: {integrity: sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ==} + + pino-std-serializers@4.0.0: + resolution: {integrity: sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q==} + + pino@7.11.0: + resolution: {integrity: sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg==} + hasBin: true + + pngjs@5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + + pony-cause@2.1.11: + resolution: {integrity: sha512-M7LhCsdNbNgiLYiP4WjsfLUuFmCfnjdF6jKe2R9NKl4WFN+HZPGHJZ9lnLP7f9ZnKe3U9nuWD0szirmj+migUg==} + engines: {node: '>=12.0.0'} + + porto@0.2.19: + resolution: {integrity: sha512-q1vEJgdtlEOf6byWgD31GHiMwpfLuxFSfx9f7Sw4RGdvpQs2ANBGfnzzardADZegr87ZXsebSp+3vaaznEUzPQ==} + hasBin: true + peerDependencies: + '@tanstack/react-query': '>=5.59.0' + '@wagmi/core': '>=2.16.3' + react: '>=18' + typescript: '>=5.4.0' + viem: '>=2.37.0' + wagmi: '>=2.0.0' + peerDependenciesMeta: + '@tanstack/react-query': + optional: true + react: + optional: true + typescript: + optional: true + wagmi: + optional: true + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + preact@10.24.2: + resolution: {integrity: sha512-1cSoF0aCC8uaARATfrlz4VCBqE8LwZwRfLgkxJOQwAlQt6ayTmi0D9OF7nXid1POI5SZidFuG9CnlXbDfLqY/Q==} + + preact@10.27.2: + resolution: {integrity: sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + process-warning@1.0.0: + resolution: {integrity: sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==} + + proxy-compare@2.6.0: + resolution: {integrity: sha512-8xuCeM3l8yqdmbPoYeLbrAXCBWu19XEYc5/F28f5qOaoAIMyfmBUkl5axiK+x9olUvRlcekvnm98AP9RDngOIw==} + + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + + qrcode@1.5.3: + resolution: {integrity: sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==} + engines: {node: '>=10.13.0'} + hasBin: true + + query-string@7.1.3: + resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} + engines: {node: '>=6'} + + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + + radix3@1.1.2: + resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + real-require@0.1.0: + resolution: {integrity: sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg==} + engines: {node: '>= 12.13.0'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + sha.js@2.4.12: + resolution: {integrity: sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==} + engines: {node: '>= 0.10'} + hasBin: true + + socket.io-client@4.8.1: + resolution: {integrity: sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==} + engines: {node: '>=10.0.0'} + + socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + + sonic-boom@2.8.0: + resolution: {integrity: sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg==} + + split-on-first@1.1.0: + resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} + engines: {node: '>=6'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + stream-shift@1.0.3: + resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} + + strict-uri-encode@2.0.0: + resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} + engines: {node: '>=4'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + superstruct@1.0.4: + resolution: {integrity: sha512-7JpaAoX2NGyoFlI9NBh66BQXGONc+uE+MRS5i2iOBKuS4e+ccgMDjATgZldkah+33DakBxDHiss9kvUcGAO8UQ==} + engines: {node: '>=14.0.0'} + + thread-stream@0.15.2: + resolution: {integrity: sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA==} + + to-buffer@1.2.2: + resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==} + engines: {node: '>= 0.4'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.20.5: + resolution: {integrity: sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==} + engines: {node: '>=18.0.0'} + hasBin: true + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typescript@5.9.2: + resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + + uint8arrays@3.1.0: + resolution: {integrity: sha512-ei5rfKtoRO8OyOIor2Rz5fhzjThwIHJZ3uyDPnDHTXbP0aMQ1RN/6AI5B5d9dBxJOU+BvOAk7ZQ1xphsX8Lrog==} + + uncrypto@0.1.3: + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + + undici-types@7.12.0: + resolution: {integrity: sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==} + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + unstorage@1.17.1: + resolution: {integrity: sha512-KKGwRTT0iVBCErKemkJCLs7JdxNVfqTPc/85ae1XES0+bsHbc/sFBfVi5kJp156cc51BHinIH2l3k0EZ24vOBQ==} + peerDependencies: + '@azure/app-configuration': ^1.8.0 + '@azure/cosmos': ^4.2.0 + '@azure/data-tables': ^13.3.0 + '@azure/identity': ^4.6.0 + '@azure/keyvault-secrets': ^4.9.0 + '@azure/storage-blob': ^12.26.0 + '@capacitor/preferences': ^6.0.3 || ^7.0.0 + '@deno/kv': '>=0.9.0' + '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 + '@planetscale/database': ^1.19.0 + '@upstash/redis': ^1.34.3 + '@vercel/blob': '>=0.27.1' + '@vercel/functions': ^2.2.12 || ^3.0.0 + '@vercel/kv': ^1.0.1 + aws4fetch: ^1.0.20 + db0: '>=0.2.1' + idb-keyval: ^6.2.1 + ioredis: ^5.4.2 + uploadthing: ^7.4.4 + peerDependenciesMeta: + '@azure/app-configuration': + optional: true + '@azure/cosmos': + optional: true + '@azure/data-tables': + optional: true + '@azure/identity': + optional: true + '@azure/keyvault-secrets': + optional: true + '@azure/storage-blob': + optional: true + '@capacitor/preferences': + optional: true + '@deno/kv': + optional: true + '@netlify/blobs': + optional: true + '@planetscale/database': + optional: true + '@upstash/redis': + optional: true + '@vercel/blob': + optional: true + '@vercel/functions': + optional: true + '@vercel/kv': + optional: true + aws4fetch: + optional: true + db0: + optional: true + idb-keyval: + optional: true + ioredis: + optional: true + uploadthing: + optional: true + + use-sync-external-store@1.2.0: + resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + use-sync-external-store@1.4.0: + resolution: {integrity: sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + utf-8-validate@5.0.10: + resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==} + engines: {node: '>=6.14.2'} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + + valtio@1.13.2: + resolution: {integrity: sha512-Qik0o+DSy741TmkqmRfjq+0xpZBXi/Y6+fXZLn0xNF1z/waFMbE3rkivv5Zcf9RrMUp6zswf2J7sbh2KBlba5A==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=16.8' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + + viem@2.23.2: + resolution: {integrity: sha512-NVmW/E0c5crMOtbEAqMF0e3NmvQykFXhLOc/CkLIXOlzHSA6KXVz3CYVmaKqBF8/xtjsjHAGjdJN3Ru1kFJLaA==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + + viem@2.37.8: + resolution: {integrity: sha512-mL+5yvCQbRIR6QvngDQMfEiZTfNWfd+/QL5yFaOoYbpH3b1Q2ddwF7YG2eI2AcYSh9LE1gtUkbzZLFUAVyj4oQ==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + + wagmi@2.17.4: + resolution: {integrity: sha512-AJpTQ5O7JFv2LDaRuMiCKxZEVBeDprh9oagA0dLSp/iAEsbd5VwmMy3rn4lCO3pl9umJ+afEkuRIq+5dx02jzg==} + peerDependencies: + '@tanstack/react-query': '>=5.0.0' + react: '>=18' + typescript: '>=5.0.4' + viem: 2.x + peerDependenciesMeta: + typescript: + optional: true + + webextension-polyfill@0.10.0: + resolution: {integrity: sha512-c5s35LgVa5tFaHhrZDnr3FpQpjj1BB+RXhLTYUxGqBVN460HkbM8TBtEqdXWbpTKfzwCcjAZVF7zXCYSKtcp9g==} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + engines: {node: '>= 0.4'} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + x402-fetch@0.6.0: + resolution: {integrity: sha512-c7ZQQOjgBWyX9xbmRCliUguVeG0RCofTlcnPKBrAgNYxEo8p/yuQr2PRvyN9tdQdXVTlGgIsII7GO1wLHJpLhA==} + + x402@0.6.1: + resolution: {integrity: sha512-9UmeCSsYzFGav5FdVP70VplKlR3V90P0DZ9fPSrlLVp0ifUVi1S9TztvegkmIHE9xTGZ1GWNi+bkne6N0Ea58w==} + + xmlhttprequest-ssl@2.1.2: + resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} + engines: {node: '>=0.4.0'} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + + zod@3.22.4: + resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + + zod@4.1.11: + resolution: {integrity: sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==} + + zustand@5.0.0: + resolution: {integrity: sha512-LE+VcmbartOPM+auOjCCLQOsQ05zUTp8RkgwRzefUk+2jISdMMFnxvyTjA4YNWr5ZGXYbVsEMZosttuxUBkojQ==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + + zustand@5.0.3: + resolution: {integrity: sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + + zustand@5.0.8: + resolution: {integrity: sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + +snapshots: + + '@adraffy/ens-normalize@1.11.1': {} + + '@babel/runtime@7.28.4': {} + + '@base-org/account@1.1.1(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@noble/hashes': 1.4.0 + clsx: 1.2.1 + eventemitter3: 5.0.1 + idb-keyval: 6.2.1 + ox: 0.6.9(typescript@5.9.2)(zod@3.25.76) + preact: 10.24.2 + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + zustand: 5.0.3(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) + transitivePeerDependencies: + - '@types/react' + - bufferutil + - immer + - react + - typescript + - use-sync-external-store + - utf-8-validate + - zod + + '@coinbase/wallet-sdk@3.9.3': + dependencies: + bn.js: 5.2.2 + buffer: 6.0.3 + clsx: 1.2.1 + eth-block-tracker: 7.1.0 + eth-json-rpc-filters: 6.0.1 + eventemitter3: 5.0.1 + keccak: 3.0.4 + preact: 10.27.2 + sha.js: 2.4.12 + transitivePeerDependencies: + - supports-color + + '@coinbase/wallet-sdk@4.3.6(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@noble/hashes': 1.4.0 + clsx: 1.2.1 + eventemitter3: 5.0.1 + idb-keyval: 6.2.1 + ox: 0.6.9(typescript@5.9.2)(zod@3.25.76) + preact: 10.24.2 + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + zustand: 5.0.3(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) + transitivePeerDependencies: + - '@types/react' + - bufferutil + - immer + - react + - typescript + - use-sync-external-store + - utf-8-validate + - zod + + '@ecies/ciphers@0.2.4(@noble/ciphers@1.3.0)': + dependencies: + '@noble/ciphers': 1.3.0 + + '@esbuild/aix-ppc64@0.25.10': + optional: true + + '@esbuild/android-arm64@0.25.10': + optional: true + + '@esbuild/android-arm@0.25.10': + optional: true + + '@esbuild/android-x64@0.25.10': + optional: true + + '@esbuild/darwin-arm64@0.25.10': + optional: true + + '@esbuild/darwin-x64@0.25.10': + optional: true + + '@esbuild/freebsd-arm64@0.25.10': + optional: true + + '@esbuild/freebsd-x64@0.25.10': + optional: true + + '@esbuild/linux-arm64@0.25.10': + optional: true + + '@esbuild/linux-arm@0.25.10': + optional: true + + '@esbuild/linux-ia32@0.25.10': + optional: true + + '@esbuild/linux-loong64@0.25.10': + optional: true + + '@esbuild/linux-mips64el@0.25.10': + optional: true + + '@esbuild/linux-ppc64@0.25.10': + optional: true + + '@esbuild/linux-riscv64@0.25.10': + optional: true + + '@esbuild/linux-s390x@0.25.10': + optional: true + + '@esbuild/linux-x64@0.25.10': + optional: true + + '@esbuild/netbsd-arm64@0.25.10': + optional: true + + '@esbuild/netbsd-x64@0.25.10': + optional: true + + '@esbuild/openbsd-arm64@0.25.10': + optional: true + + '@esbuild/openbsd-x64@0.25.10': + optional: true + + '@esbuild/openharmony-arm64@0.25.10': + optional: true + + '@esbuild/sunos-x64@0.25.10': + optional: true + + '@esbuild/win32-arm64@0.25.10': + optional: true + + '@esbuild/win32-ia32@0.25.10': + optional: true + + '@esbuild/win32-x64@0.25.10': + optional: true + + '@ethereumjs/common@3.2.0': + dependencies: + '@ethereumjs/util': 8.1.0 + crc-32: 1.2.2 + + '@ethereumjs/rlp@4.0.1': {} + + '@ethereumjs/tx@4.2.0': + dependencies: + '@ethereumjs/common': 3.2.0 + '@ethereumjs/rlp': 4.0.1 + '@ethereumjs/util': 8.1.0 + ethereum-cryptography: 2.2.1 + + '@ethereumjs/util@8.1.0': + dependencies: + '@ethereumjs/rlp': 4.0.1 + ethereum-cryptography: 2.2.1 + micro-ftch: 0.3.1 + + '@gemini-wallet/core@0.2.0(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))': + dependencies: + '@metamask/rpc-errors': 7.0.2 + eventemitter3: 5.0.1 + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + transitivePeerDependencies: + - supports-color + + '@lit-labs/ssr-dom-shim@1.4.0': {} + + '@lit/reactive-element@2.1.1': + dependencies: + '@lit-labs/ssr-dom-shim': 1.4.0 + + '@metamask/eth-json-rpc-provider@1.0.1': + dependencies: + '@metamask/json-rpc-engine': 7.3.3 + '@metamask/safe-event-emitter': 3.1.2 + '@metamask/utils': 5.0.2 + transitivePeerDependencies: + - supports-color + + '@metamask/json-rpc-engine@7.3.3': + dependencies: + '@metamask/rpc-errors': 6.4.0 + '@metamask/safe-event-emitter': 3.1.2 + '@metamask/utils': 8.5.0 + transitivePeerDependencies: + - supports-color + + '@metamask/json-rpc-engine@8.0.2': + dependencies: + '@metamask/rpc-errors': 6.4.0 + '@metamask/safe-event-emitter': 3.1.2 + '@metamask/utils': 8.5.0 + transitivePeerDependencies: + - supports-color + + '@metamask/json-rpc-middleware-stream@7.0.2': + dependencies: + '@metamask/json-rpc-engine': 8.0.2 + '@metamask/safe-event-emitter': 3.1.2 + '@metamask/utils': 8.5.0 + readable-stream: 3.6.2 + transitivePeerDependencies: + - supports-color + + '@metamask/object-multiplex@2.1.0': + dependencies: + once: 1.4.0 + readable-stream: 3.6.2 + + '@metamask/onboarding@1.0.1': + dependencies: + bowser: 2.12.1 + + '@metamask/providers@16.1.0': + dependencies: + '@metamask/json-rpc-engine': 8.0.2 + '@metamask/json-rpc-middleware-stream': 7.0.2 + '@metamask/object-multiplex': 2.1.0 + '@metamask/rpc-errors': 6.4.0 + '@metamask/safe-event-emitter': 3.1.2 + '@metamask/utils': 8.5.0 + detect-browser: 5.3.0 + extension-port-stream: 3.0.0 + fast-deep-equal: 3.1.3 + is-stream: 2.0.1 + readable-stream: 3.6.2 + webextension-polyfill: 0.10.0 + transitivePeerDependencies: + - supports-color + + '@metamask/rpc-errors@6.4.0': + dependencies: + '@metamask/utils': 9.3.0 + fast-safe-stringify: 2.1.1 + transitivePeerDependencies: + - supports-color + + '@metamask/rpc-errors@7.0.2': + dependencies: + '@metamask/utils': 11.8.1 + fast-safe-stringify: 2.1.1 + transitivePeerDependencies: + - supports-color + + '@metamask/safe-event-emitter@2.0.0': {} + + '@metamask/safe-event-emitter@3.1.2': {} + + '@metamask/sdk-analytics@0.0.5': + dependencies: + openapi-fetch: 0.13.8 + + '@metamask/sdk-communication-layer@0.33.1(cross-fetch@4.1.0)(eciesjs@0.4.15)(eventemitter2@6.4.9)(readable-stream@3.6.2)(socket.io-client@4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@metamask/sdk-analytics': 0.0.5 + bufferutil: 4.0.9 + cross-fetch: 4.1.0 + date-fns: 2.30.0 + debug: 4.3.4 + eciesjs: 0.4.15 + eventemitter2: 6.4.9 + readable-stream: 3.6.2 + socket.io-client: 4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + utf-8-validate: 5.0.10 + uuid: 8.3.2 + transitivePeerDependencies: + - supports-color + + '@metamask/sdk-install-modal-web@0.32.1': + dependencies: + '@paulmillr/qr': 0.2.1 + + '@metamask/sdk@0.33.1(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + dependencies: + '@babel/runtime': 7.28.4 + '@metamask/onboarding': 1.0.1 + '@metamask/providers': 16.1.0 + '@metamask/sdk-analytics': 0.0.5 + '@metamask/sdk-communication-layer': 0.33.1(cross-fetch@4.1.0)(eciesjs@0.4.15)(eventemitter2@6.4.9)(readable-stream@3.6.2)(socket.io-client@4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@metamask/sdk-install-modal-web': 0.32.1 + '@paulmillr/qr': 0.2.1 + bowser: 2.12.1 + cross-fetch: 4.1.0 + debug: 4.3.4 + eciesjs: 0.4.15 + eth-rpc-errors: 4.0.3 + eventemitter2: 6.4.9 + obj-multiplex: 1.0.0 + pump: 3.0.3 + readable-stream: 3.6.2 + socket.io-client: 4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + tslib: 2.8.1 + util: 0.12.5 + uuid: 8.3.2 + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - utf-8-validate + + '@metamask/superstruct@3.2.1': {} + + '@metamask/utils@11.8.1': + dependencies: + '@ethereumjs/tx': 4.2.0 + '@metamask/superstruct': 3.2.1 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + '@types/debug': 4.1.12 + '@types/lodash': 4.17.20 + debug: 4.4.3 + lodash: 4.17.21 + pony-cause: 2.1.11 + semver: 7.7.2 + uuid: 9.0.1 + transitivePeerDependencies: + - supports-color + + '@metamask/utils@5.0.2': + dependencies: + '@ethereumjs/tx': 4.2.0 + '@types/debug': 4.1.12 + debug: 4.4.3 + semver: 7.7.2 + superstruct: 1.0.4 + transitivePeerDependencies: + - supports-color + + '@metamask/utils@8.5.0': + dependencies: + '@ethereumjs/tx': 4.2.0 + '@metamask/superstruct': 3.2.1 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + '@types/debug': 4.1.12 + debug: 4.3.4 + pony-cause: 2.1.11 + semver: 7.7.2 + uuid: 9.0.1 + transitivePeerDependencies: + - supports-color + + '@metamask/utils@9.3.0': + dependencies: + '@ethereumjs/tx': 4.2.0 + '@metamask/superstruct': 3.2.1 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + '@types/debug': 4.1.12 + debug: 4.3.4 + pony-cause: 2.1.11 + semver: 7.7.2 + uuid: 9.0.1 + transitivePeerDependencies: + - supports-color + + '@noble/ciphers@1.2.1': {} + + '@noble/ciphers@1.3.0': {} + + '@noble/curves@1.4.2': + dependencies: + '@noble/hashes': 1.4.0 + + '@noble/curves@1.8.0': + dependencies: + '@noble/hashes': 1.7.0 + + '@noble/curves@1.8.1': + dependencies: + '@noble/hashes': 1.7.1 + + '@noble/curves@1.9.1': + dependencies: + '@noble/hashes': 1.8.0 + + '@noble/curves@1.9.7': + dependencies: + '@noble/hashes': 1.8.0 + + '@noble/hashes@1.4.0': {} + + '@noble/hashes@1.7.0': {} + + '@noble/hashes@1.7.1': {} + + '@noble/hashes@1.8.0': {} + + '@paulmillr/qr@0.2.1': {} + + '@reown/appkit-common@1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4)': + dependencies: + big.js: 6.2.2 + dayjs: 1.11.13 + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4) + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - zod + + '@reown/appkit-common@1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + big.js: 6.2.2 + dayjs: 1.11.13 + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - zod + + '@reown/appkit-controllers@1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) + '@walletconnect/universal-provider': 2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + valtio: 1.13.2(react@18.3.1) + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - react + - typescript + - uploadthing + - utf-8-validate + - zod + + '@reown/appkit-pay@1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-ui': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(react@18.3.1))(zod@3.25.76) + lit: 3.3.0 + valtio: 1.13.2(react@18.3.1) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - react + - typescript + - uploadthing + - utf-8-validate + - zod + + '@reown/appkit-polyfills@1.7.8': + dependencies: + buffer: 6.0.3 + + '@reown/appkit-scaffold-ui@1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(react@18.3.1))(zod@3.25.76)': + dependencies: + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-ui': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(react@18.3.1))(zod@3.25.76) + '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) + lit: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - react + - typescript + - uploadthing + - utf-8-validate + - valtio + - zod + + '@reown/appkit-ui@1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) + lit: 3.3.0 + qrcode: 1.5.3 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - react + - typescript + - uploadthing + - utf-8-validate + - zod + + '@reown/appkit-utils@1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(react@18.3.1))(zod@3.25.76)': + dependencies: + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-polyfills': 1.7.8 + '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) + '@walletconnect/logger': 2.1.2 + '@walletconnect/universal-provider': 2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + valtio: 1.13.2(react@18.3.1) + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - react + - typescript + - uploadthing + - utf-8-validate + - zod + + '@reown/appkit-wallet@1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)': + dependencies: + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4) + '@reown/appkit-polyfills': 1.7.8 + '@walletconnect/logger': 2.1.2 + zod: 3.22.4 + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + + '@reown/appkit@1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-pay': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-polyfills': 1.7.8 + '@reown/appkit-scaffold-ui': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(react@18.3.1))(zod@3.25.76) + '@reown/appkit-ui': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(react@18.3.1))(zod@3.25.76) + '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) + '@walletconnect/types': 2.21.0 + '@walletconnect/universal-provider': 2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + bs58: 6.0.0 + valtio: 1.13.2(react@18.3.1) + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - react + - typescript + - uploadthing + - utf-8-validate + - zod + + '@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + events: 3.3.0 + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - zod + + '@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@safe-global/safe-gateway-typescript-sdk': 3.23.1 + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - zod + + '@safe-global/safe-gateway-typescript-sdk@3.23.1': {} + + '@scure/base@1.1.9': {} + + '@scure/base@1.2.6': {} + + '@scure/bip32@1.4.0': + dependencies: + '@noble/curves': 1.4.2 + '@noble/hashes': 1.4.0 + '@scure/base': 1.1.9 + + '@scure/bip32@1.6.2': + dependencies: + '@noble/curves': 1.8.1 + '@noble/hashes': 1.7.1 + '@scure/base': 1.2.6 + + '@scure/bip32@1.7.0': + dependencies: + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + + '@scure/bip39@1.3.0': + dependencies: + '@noble/hashes': 1.4.0 + '@scure/base': 1.1.9 + + '@scure/bip39@1.5.4': + dependencies: + '@noble/hashes': 1.7.1 + '@scure/base': 1.2.6 + + '@scure/bip39@1.6.0': + dependencies: + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + + '@socket.io/component-emitter@3.1.2': {} + + '@solana-program/address-lookup-table@0.7.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': + dependencies: + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + + '@solana-program/compute-budget@0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': + dependencies: + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + + '@solana-program/system@0.7.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': + dependencies: + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + + '@solana-program/token-2022@0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))': + dependencies: + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/sysvars': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + + '@solana-program/token@0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': + dependencies: + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + + '@solana/accounts@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec': 2.3.0(typescript@5.9.2) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/addresses@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/assertions': 2.3.0(typescript@5.9.2) + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/nominal-types': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/assertions@2.3.0(typescript@5.9.2)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + + '@solana/codecs-core@2.3.0(typescript@5.9.2)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + + '@solana/codecs-data-structures@2.3.0(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-numbers': 2.3.0(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + + '@solana/codecs-numbers@2.3.0(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + + '@solana/codecs-strings@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-numbers': 2.3.0(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + fastestsmallesttextencoderdecoder: 1.0.22 + typescript: 5.9.2 + + '@solana/codecs@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-data-structures': 2.3.0(typescript@5.9.2) + '@solana/codecs-numbers': 2.3.0(typescript@5.9.2) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/options': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/errors@2.3.0(typescript@5.9.2)': + dependencies: + chalk: 5.6.2 + commander: 14.0.1 + typescript: 5.9.2 + + '@solana/fast-stable-stringify@2.3.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + + '@solana/functional@2.3.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + + '@solana/instructions@2.3.0(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + + '@solana/keys@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/assertions': 2.3.0(typescript@5.9.2) + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/nominal-types': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/accounts': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/functional': 2.3.0(typescript@5.9.2) + '@solana/instructions': 2.3.0(typescript@5.9.2) + '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/programs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-parsed-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/signers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/sysvars': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + + '@solana/nominal-types@2.3.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + + '@solana/options@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-data-structures': 2.3.0(typescript@5.9.2) + '@solana/codecs-numbers': 2.3.0(typescript@5.9.2) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/programs@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/promises@2.3.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + + '@solana/rpc-api@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-parsed-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec': 2.3.0(typescript@5.9.2) + '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/rpc-parsed-types@2.3.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + + '@solana/rpc-spec-types@2.3.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + + '@solana/rpc-spec@2.3.0(typescript@5.9.2)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + + '@solana/rpc-subscriptions-api@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.2) + '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/rpc-subscriptions-channel-websocket@2.3.0(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/functional': 2.3.0(typescript@5.9.2) + '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.2) + '@solana/subscribable': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + + '@solana/rpc-subscriptions-spec@2.3.0(typescript@5.9.2)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/promises': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) + '@solana/subscribable': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + + '@solana/rpc-subscriptions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/fast-stable-stringify': 2.3.0(typescript@5.9.2) + '@solana/functional': 2.3.0(typescript@5.9.2) + '@solana/promises': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-subscriptions-api': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-subscriptions-channel-websocket': 2.3.0(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.2) + '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/subscribable': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + + '@solana/rpc-transformers@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/functional': 2.3.0(typescript@5.9.2) + '@solana/nominal-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/rpc-transport-http@2.3.0(typescript@5.9.2)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + undici-types: 7.16.0 + + '@solana/rpc-types@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-numbers': 2.3.0(typescript@5.9.2) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/nominal-types': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/rpc@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/fast-stable-stringify': 2.3.0(typescript@5.9.2) + '@solana/functional': 2.3.0(typescript@5.9.2) + '@solana/rpc-api': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-spec': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-transport-http': 2.3.0(typescript@5.9.2) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/signers@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/instructions': 2.3.0(typescript@5.9.2) + '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/nominal-types': 2.3.0(typescript@5.9.2) + '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/subscribable@2.3.0(typescript@5.9.2)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + + '@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/accounts': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/transaction-confirmation@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/promises': 2.3.0(typescript@5.9.2) + '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + + '@solana/transaction-messages@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-data-structures': 2.3.0(typescript@5.9.2) + '@solana/codecs-numbers': 2.3.0(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/functional': 2.3.0(typescript@5.9.2) + '@solana/instructions': 2.3.0(typescript@5.9.2) + '@solana/nominal-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/transactions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-data-structures': 2.3.0(typescript@5.9.2) + '@solana/codecs-numbers': 2.3.0(typescript@5.9.2) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/functional': 2.3.0(typescript@5.9.2) + '@solana/instructions': 2.3.0(typescript@5.9.2) + '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/nominal-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@tanstack/query-core@5.90.2': {} + + '@tanstack/react-query@5.90.2(react@18.3.1)': + dependencies: + '@tanstack/query-core': 5.90.2 + react: 18.3.1 + + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + + '@types/lodash@4.17.20': {} + + '@types/ms@2.1.0': {} + + '@types/node@24.5.2': + dependencies: + undici-types: 7.12.0 + + '@types/trusted-types@2.0.7': {} + + '@wagmi/connectors@5.11.1(@tanstack/react-query@5.90.2(react@18.3.1))(@wagmi/core@2.21.1(@tanstack/query-core@5.90.2)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.17.4(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76))(zod@3.25.76)': + dependencies: + '@base-org/account': 1.1.1(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@3.25.76) + '@coinbase/wallet-sdk': 4.3.6(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@3.25.76) + '@gemini-wallet/core': 0.2.0(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@metamask/sdk': 0.33.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@wagmi/core': 2.21.1(@tanstack/query-core@5.90.2)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@walletconnect/ethereum-provider': 2.21.1(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + cbw-sdk: '@coinbase/wallet-sdk@3.9.3' + porto: 0.2.19(@tanstack/react-query@5.90.2(react@18.3.1))(@wagmi/core@2.21.1(@tanstack/query-core@5.90.2)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.17.4(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)) + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@tanstack/react-query' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - immer + - ioredis + - react + - supports-color + - uploadthing + - use-sync-external-store + - utf-8-validate + - wagmi + - zod + + '@wagmi/core@2.21.1(@tanstack/query-core@5.90.2)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))': + dependencies: + eventemitter3: 5.0.1 + mipd: 0.0.7(typescript@5.9.2) + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + zustand: 5.0.0(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) + optionalDependencies: + '@tanstack/query-core': 5.90.2 + typescript: 5.9.2 + transitivePeerDependencies: + - '@types/react' + - immer + - react + - use-sync-external-store + + '@walletconnect/core@2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-provider': 1.0.14 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/logger': 2.1.2 + '@walletconnect/relay-api': 1.0.11 + '@walletconnect/relay-auth': 1.1.0 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.21.0 + '@walletconnect/utils': 2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/window-getters': 1.0.1 + es-toolkit: 1.33.0 + events: 3.3.0 + uint8arrays: 3.1.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + + '@walletconnect/core@2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-provider': 1.0.14 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/logger': 2.1.2 + '@walletconnect/relay-api': 1.0.11 + '@walletconnect/relay-auth': 1.1.0 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.21.1 + '@walletconnect/utils': 2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/window-getters': 1.0.1 + es-toolkit: 1.33.0 + events: 3.3.0 + uint8arrays: 3.1.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + + '@walletconnect/environment@1.0.1': + dependencies: + tslib: 1.14.1 + + '@walletconnect/ethereum-provider@2.21.1(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@reown/appkit': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/jsonrpc-http-connection': 1.0.8 + '@walletconnect/jsonrpc-provider': 1.0.14 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/sign-client': 2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/types': 2.21.1 + '@walletconnect/universal-provider': 2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/utils': 2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - react + - typescript + - uploadthing + - utf-8-validate + - zod + + '@walletconnect/events@1.0.1': + dependencies: + keyvaluestorage-interface: 1.0.0 + tslib: 1.14.1 + + '@walletconnect/heartbeat@1.2.2': + dependencies: + '@walletconnect/events': 1.0.1 + '@walletconnect/time': 1.0.2 + events: 3.3.0 + + '@walletconnect/jsonrpc-http-connection@1.0.8': + dependencies: + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/safe-json': 1.0.2 + cross-fetch: 3.2.0 + events: 3.3.0 + transitivePeerDependencies: + - encoding + + '@walletconnect/jsonrpc-provider@1.0.14': + dependencies: + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/safe-json': 1.0.2 + events: 3.3.0 + + '@walletconnect/jsonrpc-types@1.0.4': + dependencies: + events: 3.3.0 + keyvaluestorage-interface: 1.0.0 + + '@walletconnect/jsonrpc-utils@1.0.8': + dependencies: + '@walletconnect/environment': 1.0.1 + '@walletconnect/jsonrpc-types': 1.0.4 + tslib: 1.14.1 + + '@walletconnect/jsonrpc-ws-connection@1.0.16(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + dependencies: + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/safe-json': 1.0.2 + events: 3.3.0 + ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@walletconnect/keyvaluestorage@1.1.1': + dependencies: + '@walletconnect/safe-json': 1.0.2 + idb-keyval: 6.2.2 + unstorage: 1.17.1(idb-keyval@6.2.2) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - db0 + - ioredis + - uploadthing + + '@walletconnect/logger@2.1.2': + dependencies: + '@walletconnect/safe-json': 1.0.2 + pino: 7.11.0 + + '@walletconnect/relay-api@1.0.11': + dependencies: + '@walletconnect/jsonrpc-types': 1.0.4 + + '@walletconnect/relay-auth@1.1.0': + dependencies: + '@noble/curves': 1.8.0 + '@noble/hashes': 1.7.0 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + uint8arrays: 3.1.0 + + '@walletconnect/safe-json@1.0.2': + dependencies: + tslib: 1.14.1 + + '@walletconnect/sign-client@2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@walletconnect/core': 2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/events': 1.0.1 + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/logger': 2.1.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.21.0 + '@walletconnect/utils': 2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + + '@walletconnect/sign-client@2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@walletconnect/core': 2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/events': 1.0.1 + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/logger': 2.1.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.21.1 + '@walletconnect/utils': 2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + + '@walletconnect/time@1.0.2': + dependencies: + tslib: 1.14.1 + + '@walletconnect/types@2.21.0': + dependencies: + '@walletconnect/events': 1.0.1 + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/logger': 2.1.2 + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - db0 + - ioredis + - uploadthing + + '@walletconnect/types@2.21.1': + dependencies: + '@walletconnect/events': 1.0.1 + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/logger': 2.1.2 + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - db0 + - ioredis + - uploadthing + + '@walletconnect/universal-provider@2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@walletconnect/events': 1.0.1 + '@walletconnect/jsonrpc-http-connection': 1.0.8 + '@walletconnect/jsonrpc-provider': 1.0.14 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/logger': 2.1.2 + '@walletconnect/sign-client': 2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/types': 2.21.0 + '@walletconnect/utils': 2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + es-toolkit: 1.33.0 + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + + '@walletconnect/universal-provider@2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@walletconnect/events': 1.0.1 + '@walletconnect/jsonrpc-http-connection': 1.0.8 + '@walletconnect/jsonrpc-provider': 1.0.14 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/logger': 2.1.2 + '@walletconnect/sign-client': 2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/types': 2.21.1 + '@walletconnect/utils': 2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + es-toolkit: 1.33.0 + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + + '@walletconnect/utils@2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@noble/ciphers': 1.2.1 + '@noble/curves': 1.8.1 + '@noble/hashes': 1.7.1 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/relay-api': 1.0.11 + '@walletconnect/relay-auth': 1.1.0 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.21.0 + '@walletconnect/window-getters': 1.0.1 + '@walletconnect/window-metadata': 1.0.1 + bs58: 6.0.0 + detect-browser: 5.3.0 + query-string: 7.1.3 + uint8arrays: 3.1.0 + viem: 2.23.2(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + + '@walletconnect/utils@2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@noble/ciphers': 1.2.1 + '@noble/curves': 1.8.1 + '@noble/hashes': 1.7.1 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/relay-api': 1.0.11 + '@walletconnect/relay-auth': 1.1.0 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.21.1 + '@walletconnect/window-getters': 1.0.1 + '@walletconnect/window-metadata': 1.0.1 + bs58: 6.0.0 + detect-browser: 5.3.0 + query-string: 7.1.3 + uint8arrays: 3.1.0 + viem: 2.23.2(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + + '@walletconnect/window-getters@1.0.1': + dependencies: + tslib: 1.14.1 + + '@walletconnect/window-metadata@1.0.1': + dependencies: + '@walletconnect/window-getters': 1.0.1 + tslib: 1.14.1 + + abitype@1.0.8(typescript@5.9.2)(zod@3.25.76): + optionalDependencies: + typescript: 5.9.2 + zod: 3.25.76 + + abitype@1.1.0(typescript@5.9.2)(zod@3.22.4): + optionalDependencies: + typescript: 5.9.2 + zod: 3.22.4 + + abitype@1.1.0(typescript@5.9.2)(zod@3.25.76): + optionalDependencies: + typescript: 5.9.2 + zod: 3.25.76 + + abitype@1.1.1(typescript@5.9.2)(zod@3.25.76): + optionalDependencies: + typescript: 5.9.2 + zod: 3.25.76 + + abitype@1.1.1(typescript@5.9.2)(zod@4.1.11): + optionalDependencies: + typescript: 5.9.2 + zod: 4.1.11 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + async-mutex@0.2.6: + dependencies: + tslib: 2.8.1 + + atomic-sleep@1.0.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + base-x@5.0.1: {} + + base64-js@1.5.1: {} + + big.js@6.2.2: {} + + bn.js@5.2.2: {} + + bowser@2.12.1: {} + + bs58@6.0.0: + dependencies: + base-x: 5.0.1 + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bufferutil@4.0.9: + dependencies: + node-gyp-build: 4.8.4 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + camelcase@5.3.1: {} + + chalk@5.6.2: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + + clsx@1.2.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@14.0.1: {} + + cookie-es@1.2.2: {} + + core-util-is@1.0.3: {} + + crc-32@1.2.2: {} + + cross-fetch@3.2.0: + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + cross-fetch@4.1.0: + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + crossws@0.3.5: + dependencies: + uncrypto: 0.1.3 + + date-fns@2.30.0: + dependencies: + '@babel/runtime': 7.28.4 + + dayjs@1.11.13: {} + + debug@4.3.4: + dependencies: + ms: 2.1.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decamelize@1.2.0: {} + + decode-uri-component@0.2.2: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + defu@6.1.4: {} + + derive-valtio@0.1.0(valtio@1.13.2(react@18.3.1)): + dependencies: + valtio: 1.13.2(react@18.3.1) + + destr@2.0.5: {} + + detect-browser@5.3.0: {} + + dijkstrajs@1.0.3: {} + + dotenv@17.2.2: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + duplexify@4.1.3: + dependencies: + end-of-stream: 1.4.5 + inherits: 2.0.4 + readable-stream: 3.6.2 + stream-shift: 1.0.3 + + eciesjs@0.4.15: + dependencies: + '@ecies/ciphers': 0.2.4(@noble/ciphers@1.3.0) + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + + emoji-regex@8.0.0: {} + + encode-utf8@1.0.3: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + engine.io-client@6.6.3(bufferutil@4.0.9)(utf-8-validate@5.0.10): + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.4 + engine.io-parser: 5.2.3 + ws: 8.17.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + xmlhttprequest-ssl: 2.1.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + engine.io-parser@5.2.3: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-toolkit@1.33.0: {} + + esbuild@0.25.10: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.10 + '@esbuild/android-arm': 0.25.10 + '@esbuild/android-arm64': 0.25.10 + '@esbuild/android-x64': 0.25.10 + '@esbuild/darwin-arm64': 0.25.10 + '@esbuild/darwin-x64': 0.25.10 + '@esbuild/freebsd-arm64': 0.25.10 + '@esbuild/freebsd-x64': 0.25.10 + '@esbuild/linux-arm': 0.25.10 + '@esbuild/linux-arm64': 0.25.10 + '@esbuild/linux-ia32': 0.25.10 + '@esbuild/linux-loong64': 0.25.10 + '@esbuild/linux-mips64el': 0.25.10 + '@esbuild/linux-ppc64': 0.25.10 + '@esbuild/linux-riscv64': 0.25.10 + '@esbuild/linux-s390x': 0.25.10 + '@esbuild/linux-x64': 0.25.10 + '@esbuild/netbsd-arm64': 0.25.10 + '@esbuild/netbsd-x64': 0.25.10 + '@esbuild/openbsd-arm64': 0.25.10 + '@esbuild/openbsd-x64': 0.25.10 + '@esbuild/openharmony-arm64': 0.25.10 + '@esbuild/sunos-x64': 0.25.10 + '@esbuild/win32-arm64': 0.25.10 + '@esbuild/win32-ia32': 0.25.10 + '@esbuild/win32-x64': 0.25.10 + + eth-block-tracker@7.1.0: + dependencies: + '@metamask/eth-json-rpc-provider': 1.0.1 + '@metamask/safe-event-emitter': 3.1.2 + '@metamask/utils': 5.0.2 + json-rpc-random-id: 1.0.1 + pify: 3.0.0 + transitivePeerDependencies: + - supports-color + + eth-json-rpc-filters@6.0.1: + dependencies: + '@metamask/safe-event-emitter': 3.1.2 + async-mutex: 0.2.6 + eth-query: 2.1.2 + json-rpc-engine: 6.1.0 + pify: 5.0.0 + + eth-query@2.1.2: + dependencies: + json-rpc-random-id: 1.0.1 + xtend: 4.0.2 + + eth-rpc-errors@4.0.3: + dependencies: + fast-safe-stringify: 2.1.1 + + ethereum-cryptography@2.2.1: + dependencies: + '@noble/curves': 1.4.2 + '@noble/hashes': 1.4.0 + '@scure/bip32': 1.4.0 + '@scure/bip39': 1.3.0 + + eventemitter2@6.4.9: {} + + eventemitter3@5.0.1: {} + + events@3.3.0: {} + + extension-port-stream@3.0.0: + dependencies: + readable-stream: 3.6.2 + webextension-polyfill: 0.10.0 + + fast-deep-equal@3.1.3: {} + + fast-redact@3.5.0: {} + + fast-safe-stringify@2.1.1: {} + + fastestsmallesttextencoderdecoder@1.0.22: {} + + filter-obj@1.1.0: {} + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-tsconfig@4.10.1: + dependencies: + resolve-pkg-maps: 1.0.0 + + gill@0.11.0(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + dependencies: + '@solana-program/address-lookup-table': 0.7.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + '@solana-program/compute-budget': 0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + '@solana-program/system': 0.7.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + '@solana-program/token-2022': 0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)) + '@solana/assertions': 2.3.0(typescript@5.9.2) + '@solana/codecs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + typescript: 5.9.2 + transitivePeerDependencies: + - '@solana/sysvars' + - fastestsmallesttextencoderdecoder + - ws + + gopd@1.2.0: {} + + h3@1.15.4: + dependencies: + cookie-es: 1.2.2 + crossws: 0.3.5 + defu: 6.1.4 + destr: 2.0.5 + iron-webcrypto: 1.2.1 + node-mock-http: 1.0.3 + radix3: 1.1.2 + ufo: 1.6.1 + uncrypto: 0.1.3 + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hono@4.9.8: {} + + idb-keyval@6.2.1: {} + + idb-keyval@6.2.2: {} + + ieee754@1.2.1: {} + + inherits@2.0.4: {} + + iron-webcrypto@1.2.1: {} + + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + + is-fullwidth-code-point@3.0.0: {} + + is-generator-function@1.1.0: + dependencies: + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-stream@2.0.1: {} + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.19 + + isarray@1.0.0: {} + + isarray@2.0.5: {} + + isows@1.0.6(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + dependencies: + ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + + isows@1.0.7(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + dependencies: + ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + + js-tokens@4.0.0: {} + + json-rpc-engine@6.1.0: + dependencies: + '@metamask/safe-event-emitter': 2.0.0 + eth-rpc-errors: 4.0.3 + + json-rpc-random-id@1.0.1: {} + + keccak@3.0.4: + dependencies: + node-addon-api: 2.0.2 + node-gyp-build: 4.8.4 + readable-stream: 3.6.2 + + keyvaluestorage-interface@1.0.0: {} + + lit-element@4.2.1: + dependencies: + '@lit-labs/ssr-dom-shim': 1.4.0 + '@lit/reactive-element': 2.1.1 + lit-html: 3.3.1 + + lit-html@3.3.1: + dependencies: + '@types/trusted-types': 2.0.7 + + lit@3.3.0: + dependencies: + '@lit/reactive-element': 2.1.1 + lit-element: 4.2.1 + lit-html: 3.3.1 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + lodash@4.17.21: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@10.4.3: {} + + math-intrinsics@1.1.0: {} + + micro-ftch@0.3.1: {} + + mipd@0.0.7(typescript@5.9.2): + optionalDependencies: + typescript: 5.9.2 + + ms@2.1.2: {} + + ms@2.1.3: {} + + multiformats@9.9.0: {} + + node-addon-api@2.0.2: {} + + node-fetch-native@1.6.7: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-gyp-build@4.8.4: {} + + node-mock-http@1.0.3: {} + + normalize-path@3.0.0: {} + + obj-multiplex@1.0.0: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + readable-stream: 2.3.8 + + ofetch@1.4.1: + dependencies: + destr: 2.0.5 + node-fetch-native: 1.6.7 + ufo: 1.6.1 + + on-exit-leak-free@0.2.0: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + openapi-fetch@0.13.8: + dependencies: + openapi-typescript-helpers: 0.0.15 + + openapi-typescript-helpers@0.0.15: {} + + ox@0.6.7(typescript@5.9.2)(zod@3.25.76): + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@noble/curves': 1.8.1 + '@noble/hashes': 1.7.1 + '@scure/bip32': 1.6.2 + '@scure/bip39': 1.5.4 + abitype: 1.0.8(typescript@5.9.2)(zod@3.25.76) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - zod + + ox@0.6.9(typescript@5.9.2)(zod@3.25.76): + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.1.1(typescript@5.9.2)(zod@3.25.76) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - zod + + ox@0.9.6(typescript@5.9.2)(zod@3.22.4): + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.1.0(typescript@5.9.2)(zod@3.22.4) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - zod + + ox@0.9.6(typescript@5.9.2)(zod@3.25.76): + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.1.0(typescript@5.9.2)(zod@3.25.76) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - zod + + ox@0.9.7(typescript@5.9.2)(zod@4.1.11): + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.1.1(typescript@5.9.2)(zod@4.1.11) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - zod + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-try@2.2.0: {} + + path-exists@4.0.0: {} + + picomatch@2.3.1: {} + + pify@3.0.0: {} + + pify@5.0.0: {} + + pino-abstract-transport@0.5.0: + dependencies: + duplexify: 4.1.3 + split2: 4.2.0 + + pino-std-serializers@4.0.0: {} + + pino@7.11.0: + dependencies: + atomic-sleep: 1.0.0 + fast-redact: 3.5.0 + on-exit-leak-free: 0.2.0 + pino-abstract-transport: 0.5.0 + pino-std-serializers: 4.0.0 + process-warning: 1.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.1.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 2.8.0 + thread-stream: 0.15.2 + + pngjs@5.0.0: {} + + pony-cause@2.1.11: {} + + porto@0.2.19(@tanstack/react-query@5.90.2(react@18.3.1))(@wagmi/core@2.21.1(@tanstack/query-core@5.90.2)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.17.4(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)): + dependencies: + '@wagmi/core': 2.21.1(@tanstack/query-core@5.90.2)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + hono: 4.9.8 + idb-keyval: 6.2.2 + mipd: 0.0.7(typescript@5.9.2) + ox: 0.9.7(typescript@5.9.2)(zod@4.1.11) + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + zod: 4.1.11 + zustand: 5.0.8(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) + optionalDependencies: + '@tanstack/react-query': 5.90.2(react@18.3.1) + react: 18.3.1 + typescript: 5.9.2 + wagmi: 2.17.4(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) + transitivePeerDependencies: + - '@types/react' + - immer + - use-sync-external-store + + possible-typed-array-names@1.1.0: {} + + preact@10.24.2: {} + + preact@10.27.2: {} + + process-nextick-args@2.0.1: {} + + process-warning@1.0.0: {} + + proxy-compare@2.6.0: {} + + pump@3.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + qrcode@1.5.3: + dependencies: + dijkstrajs: 1.0.3 + encode-utf8: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 + + query-string@7.1.3: + dependencies: + decode-uri-component: 0.2.2 + filter-obj: 1.1.0 + split-on-first: 1.1.0 + strict-uri-encode: 2.0.0 + + quick-format-unescaped@4.0.4: {} + + radix3@1.1.2: {} + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@4.1.2: {} + + real-require@0.1.0: {} + + require-directory@2.1.1: {} + + require-main-filename@2.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safe-stable-stringify@2.5.0: {} + + semver@7.7.2: {} + + set-blocking@2.0.0: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + sha.js@2.4.12: + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + to-buffer: 1.2.2 + + socket.io-client@4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10): + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.4 + engine.io-client: 6.6.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-parser@4.2.4: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + + sonic-boom@2.8.0: + dependencies: + atomic-sleep: 1.0.0 + + split-on-first@1.1.0: {} + + split2@4.2.0: {} + + stream-shift@1.0.3: {} + + strict-uri-encode@2.0.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + superstruct@1.0.4: {} + + thread-stream@0.15.2: + dependencies: + real-require: 0.1.0 + + to-buffer@1.2.2: + dependencies: + isarray: 2.0.5 + safe-buffer: 5.2.1 + typed-array-buffer: 1.0.3 + + tr46@0.0.3: {} + + tslib@1.14.1: {} + + tslib@2.8.1: {} + + tsx@4.20.5: + dependencies: + esbuild: 0.25.10 + get-tsconfig: 4.10.1 + optionalDependencies: + fsevents: 2.3.3 + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typescript@5.9.2: {} + + ufo@1.6.1: {} + + uint8arrays@3.1.0: + dependencies: + multiformats: 9.9.0 + + uncrypto@0.1.3: {} + + undici-types@7.12.0: {} + + undici-types@7.16.0: {} + + unstorage@1.17.1(idb-keyval@6.2.2): + dependencies: + anymatch: 3.1.3 + chokidar: 4.0.3 + destr: 2.0.5 + h3: 1.15.4 + lru-cache: 10.4.3 + node-fetch-native: 1.6.7 + ofetch: 1.4.1 + ufo: 1.6.1 + optionalDependencies: + idb-keyval: 6.2.2 + + use-sync-external-store@1.2.0(react@18.3.1): + dependencies: + react: 18.3.1 + + use-sync-external-store@1.4.0(react@18.3.1): + dependencies: + react: 18.3.1 + + utf-8-validate@5.0.10: + dependencies: + node-gyp-build: 4.8.4 + + util-deprecate@1.0.2: {} + + util@0.12.5: + dependencies: + inherits: 2.0.4 + is-arguments: 1.2.0 + is-generator-function: 1.1.0 + is-typed-array: 1.1.15 + which-typed-array: 1.1.19 + + uuid@8.3.2: {} + + uuid@9.0.1: {} + + valtio@1.13.2(react@18.3.1): + dependencies: + derive-valtio: 0.1.0(valtio@1.13.2(react@18.3.1)) + proxy-compare: 2.6.0 + use-sync-external-store: 1.2.0(react@18.3.1) + optionalDependencies: + react: 18.3.1 + + viem@2.23.2(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76): + dependencies: + '@noble/curves': 1.8.1 + '@noble/hashes': 1.7.1 + '@scure/bip32': 1.6.2 + '@scure/bip39': 1.5.4 + abitype: 1.0.8(typescript@5.9.2)(zod@3.25.76) + isows: 1.0.6(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + ox: 0.6.7(typescript@5.9.2)(zod@3.25.76) + ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + + viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4): + dependencies: + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.1.0(typescript@5.9.2)(zod@3.22.4) + isows: 1.0.7(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + ox: 0.9.6(typescript@5.9.2)(zod@3.22.4) + ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + + viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76): + dependencies: + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.1.0(typescript@5.9.2)(zod@3.25.76) + isows: 1.0.7(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + ox: 0.9.6(typescript@5.9.2)(zod@3.25.76) + ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + + wagmi@2.17.4(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76): + dependencies: + '@tanstack/react-query': 5.90.2(react@18.3.1) + '@wagmi/connectors': 5.11.1(@tanstack/react-query@5.90.2(react@18.3.1))(@wagmi/core@2.21.1(@tanstack/query-core@5.90.2)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.17.4(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76))(zod@3.25.76) + '@wagmi/core': 2.21.1(@tanstack/query-core@5.90.2)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + react: 18.3.1 + use-sync-external-store: 1.4.0(react@18.3.1) + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@tanstack/query-core' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - immer + - ioredis + - supports-color + - uploadthing + - utf-8-validate + - zod + + webextension-polyfill@0.10.0: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which-module@2.0.1: {} + + which-typed-array@1.1.19: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10): + optionalDependencies: + bufferutil: 4.0.9 + utf-8-validate: 5.0.10 + + ws@8.17.1(bufferutil@4.0.9)(utf-8-validate@5.0.10): + optionalDependencies: + bufferutil: 4.0.9 + utf-8-validate: 5.0.10 + + ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10): + optionalDependencies: + bufferutil: 4.0.9 + utf-8-validate: 5.0.10 + + ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10): + optionalDependencies: + bufferutil: 4.0.9 + utf-8-validate: 5.0.10 + + x402-fetch@0.6.0(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + dependencies: + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + x402: 0.6.1(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + zod: 3.25.76 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@solana/sysvars' + - '@tanstack/query-core' + - '@tanstack/react-query' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - fastestsmallesttextencoderdecoder + - immer + - ioredis + - react + - supports-color + - typescript + - uploadthing + - utf-8-validate + - ws + + x402@0.6.1(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + dependencies: + '@scure/base': 1.2.6 + '@solana-program/compute-budget': 0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + '@solana-program/token': 0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + '@solana-program/token-2022': 0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)) + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + wagmi: 2.17.4(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) + zod: 3.25.76 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@solana/sysvars' + - '@tanstack/query-core' + - '@tanstack/react-query' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - fastestsmallesttextencoderdecoder + - immer + - ioredis + - react + - supports-color + - typescript + - uploadthing + - utf-8-validate + - ws + + xmlhttprequest-ssl@2.1.2: {} + + xtend@4.0.2: {} + + y18n@4.0.3: {} + + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + + zod@3.22.4: {} + + zod@3.25.76: {} + + zod@4.1.11: {} + + zustand@5.0.0(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)): + optionalDependencies: + react: 18.3.1 + use-sync-external-store: 1.4.0(react@18.3.1) + + zustand@5.0.3(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)): + optionalDependencies: + react: 18.3.1 + use-sync-external-store: 1.4.0(react@18.3.1) + + zustand@5.0.8(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)): + optionalDependencies: + react: 18.3.1 + use-sync-external-store: 1.4.0(react@18.3.1) diff --git a/docs/x402/demo/client/src/index.ts b/docs/x402/demo/client/src/index.ts new file mode 100644 index 00000000..7620b318 --- /dev/null +++ b/docs/x402/demo/client/src/index.ts @@ -0,0 +1,108 @@ +import { config } from "dotenv"; +import { createSigner, decodeXPaymentResponse, wrapFetchWithPayment, MultiNetworkSigner } from "x402-fetch"; +import path from "path"; + +config({ path: path.join(process.cwd(), '..', '.env') }); + +const PAYER_PRIVATE_KEY = process.env.PAYER_PRIVATE_KEY as string; +const PROTECTED_API_URL = process.env.PROTECTED_API_URL || "http://localhost:4021/protected"; +const NETWORK = process.env.NETWORK || "solana-devnet"; + +async function main() { + console.log('\nโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”'); + console.log('X402 + KORA PAYMENT FLOW DEMONSTRATION'); + console.log('โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”'); + + if (!PAYER_PRIVATE_KEY) { + console.error("\nโŒ ERROR: Missing required environment variables"); + console.error(" โ†’ Ensure PAYER_PRIVATE_KEY is set in your .env file"); + process.exit(1); + } + + try { + console.log('\n[1/4] Initializing payment signer'); + const payer = await createSigner(NETWORK, PAYER_PRIVATE_KEY); + const signer = { svm: payer } as MultiNetworkSigner; + console.log(' โ†’ Network:', NETWORK); + console.log(' โ†’ Payer address:', signer.svm.address.slice(0, 4) + '...' + signer.svm.address.slice(-4)); + console.log(' โœ“ Signer initialized'); + const fetchWithPayment = wrapFetchWithPayment(fetch, payer); + + console.log('\n[2/4] Attempting to access protected endpoint without payment'); + console.log(' โ†’ GET', PROTECTED_API_URL); + const expect402Response = await fetch(PROTECTED_API_URL, { + method: "GET", + }); + console.log(' โ†’ Response:', expect402Response.status, expect402Response.statusText); + console.log(` ${expect402Response.status === 402 ? "โœ…" : "โŒ"} Status code: ${expect402Response.status}`); + + console.log('\n[3/4] Accessing protected endpoint with x402 payment'); + console.log(' โ†’ Using x402 fetch wrapper'); + console.log(' โ†’ Payment will be processed via Kora facilitator'); + const response = await fetchWithPayment(PROTECTED_API_URL, { + method: "GET", + }); + console.log(' โ†’ Transaction submitted to Solana'); + console.log(` ${response.status === 200 ? "โœ…" : "โŒ"} Status code: ${response.status}`); + + console.log('\n[4/4] Processing response data'); + const data = await response.json(); + const paymentResponseHeader = response.headers.get("x-payment-response"); + + let decodedPaymentResponse; + try { + decodedPaymentResponse = decodeXPaymentResponse(paymentResponseHeader!); + console.log(' โœ“ Payment response decoded'); + } catch (decodeError) { + decodedPaymentResponse = null; + console.log(' โš  No payment response to decode'); + } + + const result = { + data: data, + status_code: response.status, + payment_response: decodedPaymentResponse + }; + + console.log('\nโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”'); + console.log('SUCCESS: Payment completed and API accessed'); + console.log('โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”'); + + console.log('\nResponse Data:'); + console.log(JSON.stringify(result, null, 2)); + + if (decodedPaymentResponse?.transaction) { + console.log('\nTransaction signature:'); + console.log(decodedPaymentResponse.transaction); + console.log('\nView on explorer:'); + const explorerUrl = NETWORK === 'solana-devnet' + ? `https://explorer.solana.com/tx/${decodedPaymentResponse.transaction}?cluster=devnet` + : `https://explorer.solana.com/tx/${decodedPaymentResponse.transaction}?cluster=custom&customUrl=http%3A%2F%2Flocalhost%3A8899`; + console.log(explorerUrl); + } + + process.exit(0); + } catch (error) { + console.log('\nโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”'); + console.log('ERROR: Demo failed'); + console.log('โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”'); + + const errorResult = { + success: false, + error: error instanceof Error ? error.message : String(error), + status_code: (error as any).response?.status + }; + + console.log('\nError details:'); + console.log(JSON.stringify(errorResult, null, 2)); + + console.log('\nTroubleshooting tips:'); + console.log(' โ†’ Ensure all services are running (Kora, Facilitator, API)'); + console.log(' โ†’ Verify your account has sufficient USDC balance'); + console.log(' โ†’ Check that Kora fee payer has SOL for gas'); + console.log(' โ†’ Confirm API key matches in .env and kora.toml'); + + process.exit(1); + } +} +main(); diff --git a/docs/x402/demo/client/src/setup.ts b/docs/x402/demo/client/src/setup.ts new file mode 100644 index 00000000..ae7b82a4 --- /dev/null +++ b/docs/x402/demo/client/src/setup.ts @@ -0,0 +1,93 @@ +import { getBase58Decoder, getBase58Encoder, createKeyPairSignerFromBytes, createSolanaRpc, Address, lamports, LAMPORTS_PER_SOL, createSolanaClient } from "gill"; +import { appendFile } from 'fs/promises'; +import path from "path"; + + +async function createB58SecretKey(): Promise { + // await assertKeyGenerationIsAvailable(); + const base58Decoder = getBase58Decoder(); + // Create keypair with exportable private key + // For demo purposes only + const keyPair = await crypto.subtle.generateKey( + "Ed25519", // Algorithm. Native implementation status: https://github.com/WICG/webcrypto-secure-curves/issues/20 + true, // Allows the private key to be exported (eg for saving it to a file) - public key is always extractable see https://wicg.github.io/webcrypto-secure-curves/#ed25519-operations + ["sign", "verify"], // Allowed uses + ); + + // Get the raw 32-byte private key + const pkcs8ArrayBuffer = await crypto.subtle.exportKey("pkcs8", keyPair.privateKey); + const pkcs8Bytes = new Uint8Array(pkcs8ArrayBuffer); + const rawPrivateKey = pkcs8Bytes.slice(-32); + + // Get the 32-byte public key + const publicKeyArrayBuffer = await crypto.subtle.exportKey("raw", keyPair.publicKey); + const publicKeyBytes = new Uint8Array(publicKeyArrayBuffer); + + // Create Solana-style 64-byte secret key (private + public) + const solanaSecretKey = new Uint8Array(64); + solanaSecretKey.set(rawPrivateKey, 0); // First 32 bytes + solanaSecretKey.set(publicKeyBytes, 32); // Next 32 bytes + + const b58Secret = base58Decoder.decode(solanaSecretKey) + + return b58Secret; +} + +const createKeyPairSignerFromB58Secret = async (b58Secret: string) => { + const base58Encoder = getBase58Encoder(); + const b58SecretEncoded = base58Encoder.encode(b58Secret); + return await createKeyPairSignerFromBytes(b58SecretEncoded); +} + +const addKeypairToEnvFile = async ( + variableName: string, + attemptAirdrop: boolean = false, + envPath: string = path.join(process.cwd(), '..'), + envFileName: string = ".env", + b58Secret?: string, +) => { + + const ADDRESS_SUFFIX = "_ADDRESS"; + const PRIVATE_KEY_SUFFIX = "_PRIVATE_KEY"; + + if (!b58Secret) { + b58Secret = await createB58SecretKey(); + } + + const keypairSigner = await createKeyPairSignerFromB58Secret(b58Secret); + const addressLog = `\n${variableName}${ADDRESS_SUFFIX}=${keypairSigner.address}\n`; + const privateKeyLog = `${variableName}${PRIVATE_KEY_SUFFIX}=${b58Secret}\n`; + + + const fullPath = path.join(envPath, envFileName); + try { + await appendFile( + fullPath, + `${addressLog}${privateKeyLog}`, + ); + console.log(`${variableName}${ADDRESS_SUFFIX} and ${variableName}${PRIVATE_KEY_SUFFIX} added to env file successfully`); + if (attemptAirdrop) { + console.warn(`Attempting airdrop on devnet - if you need to run multiple times, you may run into rate limiting. Check out https://faucet.solana.com/ for other options.`) + + await airdrop(keypairSigner.address); + } + return keypairSigner; + } catch (e) { + throw e; + } +}; + +async function airdrop(address: Address) { + const client = createSolanaClient({ urlOrMoniker: 'devnet' }); + await client.rpc.requestAirdrop( + address, + lamports(BigInt(LAMPORTS_PER_SOL / 10)), + { commitment: 'processed' } + ).send(); +} + +async function test() { + await addKeypairToEnvFile('KORA_SIGNER'); + await addKeypairToEnvFile('PAYER'); +} +test(); \ No newline at end of file diff --git a/docs/x402/demo/client/tsconfig.json b/docs/x402/demo/client/tsconfig.json new file mode 100644 index 00000000..8406331b --- /dev/null +++ b/docs/x402/demo/client/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "nodenext", + "lib": ["es2020", "DOM"], + "declaration": true, + "outDir": "./dist", + "rootDir": "./", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "moduleResolution": "nodenext" + }, + "rootDir": "./", + "outDir": "./dist", + "include": ["./"], + "exclude": ["node_modules", "dist"] +} \ No newline at end of file diff --git a/docs/x402/demo/facilitator/notes.md b/docs/x402/demo/facilitator/notes.md new file mode 100644 index 00000000..09bfa1da --- /dev/null +++ b/docs/x402/demo/facilitator/notes.md @@ -0,0 +1,160 @@ +wraps kora + +that does this: +https://github.com/coinbase/x402/blob/19db25989697cfb3d1d72ec57ab96fcc79d53df1/typescript/packages/x402/src/schemes/exact/svm/facilitator/verify.ts + +https://github.com/coinbase/x402/blob/19db25989697cfb3d1d72ec57ab96fcc79d53df1/examples/typescript/facilitator/index.ts + +```ts +/* eslint-env node */ +import { config } from "dotenv"; +import express, { Request, Response } from "express"; +import { verify, settle } from "x402/facilitator"; +import { + PaymentRequirementsSchema, + type PaymentRequirements, + type PaymentPayload, + PaymentPayloadSchema, + createConnectedClient, + createSigner, + SupportedEVMNetworks, + SupportedSVMNetworks, + Signer, + ConnectedClient, + SupportedPaymentKind, + isSvmSignerWallet, +} from "x402/types"; + +config(); + +const EVM_PRIVATE_KEY = process.env.EVM_PRIVATE_KEY || ""; +const SVM_PRIVATE_KEY = process.env.SVM_PRIVATE_KEY || ""; + +if (!EVM_PRIVATE_KEY && !SVM_PRIVATE_KEY) { + console.error("Missing required environment variables"); + process.exit(1); +} + +const app = express(); + +// Configure express to parse JSON bodies +app.use(express.json()); + +type VerifyRequest = { + paymentPayload: PaymentPayload; + paymentRequirements: PaymentRequirements; +}; + +type SettleRequest = { + paymentPayload: PaymentPayload; + paymentRequirements: PaymentRequirements; +}; + +app.get("/verify", (req: Request, res: Response) => { + res.json({ + endpoint: "/verify", + description: "POST to verify x402 payments", + body: { + paymentPayload: "PaymentPayload", + paymentRequirements: "PaymentRequirements", + }, + }); +}); + +app.post("/verify", async (req: Request, res: Response) => { + try { + const body: VerifyRequest = req.body; + const paymentRequirements = PaymentRequirementsSchema.parse(body.paymentRequirements); + const paymentPayload = PaymentPayloadSchema.parse(body.paymentPayload); + + // use the correct client/signer based on the requested network + // svm verify requires a Signer because it signs & simulates the txn + let client: Signer | ConnectedClient; + if (SupportedEVMNetworks.includes(paymentRequirements.network)) { + client = createConnectedClient(paymentRequirements.network); + } else if (SupportedSVMNetworks.includes(paymentRequirements.network)) { + client = await createSigner(paymentRequirements.network, SVM_PRIVATE_KEY); + } else { + throw new Error("Invalid network"); + } + + // verify + const valid = await verify(client, paymentPayload, paymentRequirements); + res.json(valid); + } catch (error) { + console.error("error", error); + res.status(400).json({ error: "Invalid request" }); + } +}); + +app.get("/settle", (req: Request, res: Response) => { + res.json({ + endpoint: "/settle", + description: "POST to settle x402 payments", + body: { + paymentPayload: "PaymentPayload", + paymentRequirements: "PaymentRequirements", + }, + }); +}); + +app.get("/supported", async (req: Request, res: Response) => { + let kinds: SupportedPaymentKind[] = []; + + // evm + if (EVM_PRIVATE_KEY) { + kinds.push({ + x402Version: 1, + scheme: "exact", + network: "base-sepolia", + }); + } + + // svm + if (SVM_PRIVATE_KEY) { + const signer = await createSigner("solana-devnet", SVM_PRIVATE_KEY); + const feePayer = isSvmSignerWallet(signer) ? signer.address : undefined; + + kinds.push({ + x402Version: 1, + scheme: "exact", + network: "solana-devnet", + extra: { + feePayer, + }, + }); + } + res.json({ + kinds, + }); +}); + +app.post("/settle", async (req: Request, res: Response) => { + try { + const body: SettleRequest = req.body; + const paymentRequirements = PaymentRequirementsSchema.parse(body.paymentRequirements); + const paymentPayload = PaymentPayloadSchema.parse(body.paymentPayload); + + // use the correct private key based on the requested network + let signer: Signer; + if (SupportedEVMNetworks.includes(paymentRequirements.network)) { + signer = await createSigner(paymentRequirements.network, EVM_PRIVATE_KEY); + } else if (SupportedSVMNetworks.includes(paymentRequirements.network)) { + signer = await createSigner(paymentRequirements.network, SVM_PRIVATE_KEY); + } else { + throw new Error("Invalid network"); + } + + // settle + const response = await settle(signer, paymentPayload, paymentRequirements); + res.json(response); + } catch (error) { + console.error("error", error); + res.status(400).json({ error: `Invalid request: ${error}` }); + } +}); + +app.listen(process.env.PORT || 3000, () => { + console.log(`Server listening at http://localhost:${process.env.PORT || 3000}`); +}); +``` \ No newline at end of file diff --git a/docs/x402/demo/facilitator/package.json b/docs/x402/demo/facilitator/package.json new file mode 100644 index 00000000..e49587ca --- /dev/null +++ b/docs/x402/demo/facilitator/package.json @@ -0,0 +1,23 @@ +{ + "name": "facilitator", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "tsx src/facilitator.ts" + }, + "keywords": [], + "author": "", + "license": "ISC", + "packageManager": "pnpm@10.14.0", + "dependencies": { + "@kora/sdk": "file:../../../../sdks/ts", + "dotenv": "^17.2.2", + "express": "^5.1.0", + "x402": "^0.6.1" + }, + "devDependencies": { + "@types/node": "^24.5.2", + "tsx": "^4.20.5" + } +} diff --git a/docs/x402/demo/facilitator/pnpm-lock.yaml b/docs/x402/demo/facilitator/pnpm-lock.yaml new file mode 100644 index 00000000..a8fb01ef --- /dev/null +++ b/docs/x402/demo/facilitator/pnpm-lock.yaml @@ -0,0 +1,5138 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@kora/sdk': + specifier: file:../../../../sdks/ts + version: file:../../../../sdks/ts + dotenv: + specifier: ^17.2.2 + version: 17.2.2 + express: + specifier: ^5.1.0 + version: 5.1.0 + x402: + specifier: ^0.6.1 + version: 0.6.1(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + devDependencies: + '@types/node': + specifier: ^24.5.2 + version: 24.5.2 + tsx: + specifier: ^4.20.5 + version: 4.20.5 + +packages: + + '@adraffy/ens-normalize@1.11.1': + resolution: {integrity: sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==} + + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} + engines: {node: '>=6.9.0'} + + '@base-org/account@1.1.1': + resolution: {integrity: sha512-IfVJPrDPhHfqXRDb89472hXkpvJuQQR7FDI9isLPHEqSYt/45whIoBxSPgZ0ssTt379VhQo4+87PWI1DoLSfAQ==} + + '@coinbase/wallet-sdk@3.9.3': + resolution: {integrity: sha512-N/A2DRIf0Y3PHc1XAMvbBUu4zisna6qAdqABMZwBMNEfWrXpAwx16pZGkYCLGE+Rvv1edbcB2LYDRnACNcmCiw==} + + '@coinbase/wallet-sdk@4.3.6': + resolution: {integrity: sha512-4q8BNG1ViL4mSAAvPAtpwlOs1gpC+67eQtgIwNvT3xyeyFFd+guwkc8bcX5rTmQhXpqnhzC4f0obACbP9CqMSA==} + + '@ecies/ciphers@0.2.4': + resolution: {integrity: sha512-t+iX+Wf5nRKyNzk8dviW3Ikb/280+aEJAnw9YXvCp2tYGPSkMki+NRY+8aNLmVFv3eNtMdvViPNOPxS8SZNP+w==} + engines: {bun: '>=1', deno: '>=2', node: '>=16'} + peerDependencies: + '@noble/ciphers': ^1.0.0 + + '@esbuild/aix-ppc64@0.25.10': + resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.10': + resolution: {integrity: sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.10': + resolution: {integrity: sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.10': + resolution: {integrity: sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.10': + resolution: {integrity: sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.10': + resolution: {integrity: sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.10': + resolution: {integrity: sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.10': + resolution: {integrity: sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.10': + resolution: {integrity: sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.10': + resolution: {integrity: sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.10': + resolution: {integrity: sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.10': + resolution: {integrity: sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.10': + resolution: {integrity: sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.10': + resolution: {integrity: sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.10': + resolution: {integrity: sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.10': + resolution: {integrity: sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.10': + resolution: {integrity: sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.10': + resolution: {integrity: sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.10': + resolution: {integrity: sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.10': + resolution: {integrity: sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.10': + resolution: {integrity: sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.10': + resolution: {integrity: sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.10': + resolution: {integrity: sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.10': + resolution: {integrity: sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.10': + resolution: {integrity: sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.10': + resolution: {integrity: sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@ethereumjs/common@3.2.0': + resolution: {integrity: sha512-pksvzI0VyLgmuEF2FA/JR/4/y6hcPq8OUail3/AvycBaW1d5VSauOZzqGvJ3RTmR4MU35lWE8KseKOsEhrFRBA==} + + '@ethereumjs/rlp@4.0.1': + resolution: {integrity: sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw==} + engines: {node: '>=14'} + hasBin: true + + '@ethereumjs/tx@4.2.0': + resolution: {integrity: sha512-1nc6VO4jtFd172BbSnTnDQVr9IYBFl1y4xPzZdtkrkKIncBCkdbgfdRV+MiTkJYAtTxvV12GRZLqBFT1PNK6Yw==} + engines: {node: '>=14'} + + '@ethereumjs/util@8.1.0': + resolution: {integrity: sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA==} + engines: {node: '>=14'} + + '@gemini-wallet/core@0.2.0': + resolution: {integrity: sha512-vv9aozWnKrrPWQ3vIFcWk7yta4hQW1Ie0fsNNPeXnjAxkbXr2hqMagEptLuMxpEP2W3mnRu05VDNKzcvAuuZDw==} + peerDependencies: + viem: '>=2.0.0' + + '@kora/sdk@file:../../../../sdks/ts': + resolution: {directory: ../../../../sdks/ts, type: directory} + + '@lit-labs/ssr-dom-shim@1.4.0': + resolution: {integrity: sha512-ficsEARKnmmW5njugNYKipTm4SFnbik7CXtoencDZzmzo/dQ+2Q0bgkzJuoJP20Aj0F+izzJjOqsnkd6F/o1bw==} + + '@lit/reactive-element@2.1.1': + resolution: {integrity: sha512-N+dm5PAYdQ8e6UlywyyrgI2t++wFGXfHx+dSJ1oBrg6FAxUj40jId++EaRm80MKX5JnlH1sBsyZ5h0bcZKemCg==} + + '@metamask/eth-json-rpc-provider@1.0.1': + resolution: {integrity: sha512-whiUMPlAOrVGmX8aKYVPvlKyG4CpQXiNNyt74vE1xb5sPvmx5oA7B/kOi/JdBvhGQq97U1/AVdXEdk2zkP8qyA==} + engines: {node: '>=14.0.0'} + + '@metamask/json-rpc-engine@7.3.3': + resolution: {integrity: sha512-dwZPq8wx9yV3IX2caLi9q9xZBw2XeIoYqdyihDDDpuHVCEiqadJLwqM3zy+uwf6F1QYQ65A8aOMQg1Uw7LMLNg==} + engines: {node: '>=16.0.0'} + + '@metamask/json-rpc-engine@8.0.2': + resolution: {integrity: sha512-IoQPmql8q7ABLruW7i4EYVHWUbF74yrp63bRuXV5Zf9BQwcn5H9Ww1eLtROYvI1bUXwOiHZ6qT5CWTrDc/t/AA==} + engines: {node: '>=16.0.0'} + + '@metamask/json-rpc-middleware-stream@7.0.2': + resolution: {integrity: sha512-yUdzsJK04Ev98Ck4D7lmRNQ8FPioXYhEUZOMS01LXW8qTvPGiRVXmVltj2p4wrLkh0vW7u6nv0mNl5xzC5Qmfg==} + engines: {node: '>=16.0.0'} + + '@metamask/object-multiplex@2.1.0': + resolution: {integrity: sha512-4vKIiv0DQxljcXwfpnbsXcfa5glMj5Zg9mqn4xpIWqkv6uJ2ma5/GtUfLFSxhlxnR8asRMv8dDmWya1Tc1sDFA==} + engines: {node: ^16.20 || ^18.16 || >=20} + + '@metamask/onboarding@1.0.1': + resolution: {integrity: sha512-FqHhAsCI+Vacx2qa5mAFcWNSrTcVGMNjzxVgaX8ECSny/BJ9/vgXP9V7WF/8vb9DltPeQkxr+Fnfmm6GHfmdTQ==} + + '@metamask/providers@16.1.0': + resolution: {integrity: sha512-znVCvux30+3SaUwcUGaSf+pUckzT5ukPRpcBmy+muBLC0yaWnBcvDqGfcsw6CBIenUdFrVoAFa8B6jsuCY/a+g==} + engines: {node: ^18.18 || >=20} + + '@metamask/rpc-errors@6.4.0': + resolution: {integrity: sha512-1ugFO1UoirU2esS3juZanS/Fo8C8XYocCuBpfZI5N7ECtoG+zu0wF+uWZASik6CkO6w9n/Iebt4iI4pT0vptpg==} + engines: {node: '>=16.0.0'} + + '@metamask/rpc-errors@7.0.2': + resolution: {integrity: sha512-YYYHsVYd46XwY2QZzpGeU4PSdRhHdxnzkB8piWGvJW2xbikZ3R+epAYEL4q/K8bh9JPTucsUdwRFnACor1aOYw==} + engines: {node: ^18.20 || ^20.17 || >=22} + + '@metamask/safe-event-emitter@2.0.0': + resolution: {integrity: sha512-/kSXhY692qiV1MXu6EeOZvg5nECLclxNXcKCxJ3cXQgYuRymRHpdx/t7JXfsK+JLjwA1e1c1/SBrlQYpusC29Q==} + + '@metamask/safe-event-emitter@3.1.2': + resolution: {integrity: sha512-5yb2gMI1BDm0JybZezeoX/3XhPDOtTbcFvpTXM9kxsoZjPZFh4XciqRbpD6N86HYZqWDhEaKUDuOyR0sQHEjMA==} + engines: {node: '>=12.0.0'} + + '@metamask/sdk-analytics@0.0.5': + resolution: {integrity: sha512-fDah+keS1RjSUlC8GmYXvx6Y26s3Ax1U9hGpWb6GSY5SAdmTSIqp2CvYy6yW0WgLhnYhW+6xERuD0eVqV63QIQ==} + + '@metamask/sdk-communication-layer@0.33.1': + resolution: {integrity: sha512-0bI9hkysxcfbZ/lk0T2+aKVo1j0ynQVTuB3sJ5ssPWlz+Z3VwveCkP1O7EVu1tsVVCb0YV5WxK9zmURu2FIiaA==} + peerDependencies: + cross-fetch: ^4.0.0 + eciesjs: '*' + eventemitter2: ^6.4.9 + readable-stream: ^3.6.2 + socket.io-client: ^4.5.1 + + '@metamask/sdk-install-modal-web@0.32.1': + resolution: {integrity: sha512-MGmAo6qSjf1tuYXhCu2EZLftq+DSt5Z7fsIKr2P+lDgdTPWgLfZB1tJKzNcwKKOdf6q9Qmmxn7lJuI/gq5LrKw==} + + '@metamask/sdk@0.33.1': + resolution: {integrity: sha512-1mcOQVGr9rSrVcbKPNVzbZ8eCl1K0FATsYH3WJ/MH4WcZDWGECWrXJPNMZoEAkLxWiMe8jOQBumg2pmcDa9zpQ==} + + '@metamask/superstruct@3.2.1': + resolution: {integrity: sha512-fLgJnDOXFmuVlB38rUN5SmU7hAFQcCjrg3Vrxz67KTY7YHFnSNEKvX4avmEBdOI0yTCxZjwMCFEqsC8k2+Wd3g==} + engines: {node: '>=16.0.0'} + + '@metamask/utils@11.8.1': + resolution: {integrity: sha512-DIbsNUyqWLFgqJlZxi1OOCMYvI23GqFCvNJAtzv8/WXWzJfnJnvp1M24j7VvUe3URBi3S86UgQ7+7aWU9p/cnQ==} + engines: {node: ^18.18 || ^20.14 || >=22} + + '@metamask/utils@5.0.2': + resolution: {integrity: sha512-yfmE79bRQtnMzarnKfX7AEJBwFTxvTyw3nBQlu/5rmGXrjAeAMltoGxO62TFurxrQAFMNa/fEjIHNvungZp0+g==} + engines: {node: '>=14.0.0'} + + '@metamask/utils@8.5.0': + resolution: {integrity: sha512-I6bkduevXb72TIM9q2LRO63JSsF9EXduh3sBr9oybNX2hNNpr/j1tEjXrsG0Uabm4MJ1xkGAQEMwifvKZIkyxQ==} + engines: {node: '>=16.0.0'} + + '@metamask/utils@9.3.0': + resolution: {integrity: sha512-w8CVbdkDrVXFJbfBSlDfafDR6BAkpDmv1bC1UJVCoVny5tW2RKAdn9i68Xf7asYT4TnUhl/hN4zfUiKQq9II4g==} + engines: {node: '>=16.0.0'} + + '@noble/ciphers@1.2.1': + resolution: {integrity: sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==} + engines: {node: ^14.21.3 || >=16} + + '@noble/ciphers@1.3.0': + resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/curves@1.4.2': + resolution: {integrity: sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==} + + '@noble/curves@1.8.0': + resolution: {integrity: sha512-j84kjAbzEnQHaSIhRPUmB3/eVXu2k3dKPl2LOrR8fSOIL+89U+7lV117EWHtq/GHM3ReGHM46iRBdZfpc4HRUQ==} + engines: {node: ^14.21.3 || >=16} + + '@noble/curves@1.8.1': + resolution: {integrity: sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==} + engines: {node: ^14.21.3 || >=16} + + '@noble/curves@1.9.1': + resolution: {integrity: sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==} + engines: {node: ^14.21.3 || >=16} + + '@noble/curves@1.9.7': + resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.4.0': + resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} + engines: {node: '>= 16'} + + '@noble/hashes@1.7.0': + resolution: {integrity: sha512-HXydb0DgzTpDPwbVeDGCG1gIu7X6+AuU6Zl6av/E/KG8LMsvPntvq+w17CHRpKBmN6Ybdrt1eP3k4cj8DJa78w==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.7.1': + resolution: {integrity: sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@paulmillr/qr@0.2.1': + resolution: {integrity: sha512-IHnV6A+zxU7XwmKFinmYjUcwlyK9+xkG3/s9KcQhI9BjQKycrJ1JRO+FbNYPwZiPKW3je/DR0k7w8/gLa5eaxQ==} + deprecated: 'The package is now available as "qr": npm install qr' + + '@reown/appkit-common@1.7.8': + resolution: {integrity: sha512-ridIhc/x6JOp7KbDdwGKY4zwf8/iK8EYBl+HtWrruutSLwZyVi5P8WaZa+8iajL6LcDcDF7LoyLwMTym7SRuwQ==} + + '@reown/appkit-controllers@1.7.8': + resolution: {integrity: sha512-IdXlJlivrlj6m63VsGLsjtPHHsTWvKGVzWIP1fXZHVqmK+rZCBDjCi9j267Rb9/nYRGHWBtlFQhO8dK35WfeDA==} + + '@reown/appkit-pay@1.7.8': + resolution: {integrity: sha512-OSGQ+QJkXx0FEEjlpQqIhT8zGJKOoHzVnyy/0QFrl3WrQTjCzg0L6+i91Ad5Iy1zb6V5JjqtfIFpRVRWN4M3pw==} + + '@reown/appkit-polyfills@1.7.8': + resolution: {integrity: sha512-W/kq786dcHHAuJ3IV2prRLEgD/2iOey4ueMHf1sIFjhhCGMynMkhsOhQMUH0tzodPqUgAC494z4bpIDYjwWXaA==} + + '@reown/appkit-scaffold-ui@1.7.8': + resolution: {integrity: sha512-RCeHhAwOrIgcvHwYlNWMcIDibdI91waaoEYBGw71inE0kDB8uZbE7tE6DAXJmDkvl0qPh+DqlC4QbJLF1FVYdQ==} + + '@reown/appkit-ui@1.7.8': + resolution: {integrity: sha512-1hjCKjf6FLMFzrulhl0Y9Vb9Fu4royE+SXCPSWh4VhZhWqlzUFc7kutnZKx8XZFVQH4pbBvY62SpRC93gqoHow==} + + '@reown/appkit-utils@1.7.8': + resolution: {integrity: sha512-8X7UvmE8GiaoitCwNoB86pttHgQtzy4ryHZM9kQpvjQ0ULpiER44t1qpVLXNM4X35O0v18W0Dk60DnYRMH2WRw==} + peerDependencies: + valtio: 1.13.2 + + '@reown/appkit-wallet@1.7.8': + resolution: {integrity: sha512-kspz32EwHIOT/eg/ZQbFPxgXq0B/olDOj3YMu7gvLEFz4xyOFd/wgzxxAXkp5LbG4Cp++s/elh79rVNmVFdB9A==} + + '@reown/appkit@1.7.8': + resolution: {integrity: sha512-51kTleozhA618T1UvMghkhKfaPcc9JlKwLJ5uV+riHyvSoWPKPRIa5A6M1Wano5puNyW0s3fwywhyqTHSilkaA==} + + '@safe-global/safe-apps-provider@0.18.6': + resolution: {integrity: sha512-4LhMmjPWlIO8TTDC2AwLk44XKXaK6hfBTWyljDm0HQ6TWlOEijVWNrt2s3OCVMSxlXAcEzYfqyu1daHZooTC2Q==} + + '@safe-global/safe-apps-sdk@9.1.0': + resolution: {integrity: sha512-N5p/ulfnnA2Pi2M3YeWjULeWbjo7ei22JwU/IXnhoHzKq3pYCN6ynL9mJBOlvDVv892EgLPCWCOwQk/uBT2v0Q==} + + '@safe-global/safe-gateway-typescript-sdk@3.23.1': + resolution: {integrity: sha512-6ORQfwtEJYpalCeVO21L4XXGSdbEMfyp2hEv6cP82afKXSwvse6d3sdelgaPWUxHIsFRkWvHDdzh8IyyKHZKxw==} + engines: {node: '>=16'} + + '@scure/base@1.1.9': + resolution: {integrity: sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==} + + '@scure/base@1.2.6': + resolution: {integrity: sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==} + + '@scure/bip32@1.4.0': + resolution: {integrity: sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==} + + '@scure/bip32@1.6.2': + resolution: {integrity: sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==} + + '@scure/bip32@1.7.0': + resolution: {integrity: sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==} + + '@scure/bip39@1.3.0': + resolution: {integrity: sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==} + + '@scure/bip39@1.5.4': + resolution: {integrity: sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==} + + '@scure/bip39@1.6.0': + resolution: {integrity: sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==} + + '@socket.io/component-emitter@3.1.2': + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + + '@solana-program/compute-budget@0.8.0': + resolution: {integrity: sha512-qPKxdxaEsFxebZ4K5RPuy7VQIm/tfJLa1+Nlt3KNA8EYQkz9Xm8htdoEaXVrer9kpgzzp9R3I3Bh6omwCM06tQ==} + peerDependencies: + '@solana/kit': ^2.1.0 + + '@solana-program/token-2022@0.4.2': + resolution: {integrity: sha512-zIpR5t4s9qEU3hZKupzIBxJ6nUV5/UVyIT400tu9vT1HMs5JHxaTTsb5GUhYjiiTvNwU0MQavbwc4Dl29L0Xvw==} + peerDependencies: + '@solana/kit': ^2.1.0 + '@solana/sysvars': ^2.1.0 + + '@solana-program/token@0.5.1': + resolution: {integrity: sha512-bJvynW5q9SFuVOZ5vqGVkmaPGA0MCC+m9jgJj1nk5m20I389/ms69ASnhWGoOPNcie7S9OwBX0gTj2fiyWpfag==} + peerDependencies: + '@solana/kit': ^2.1.0 + + '@solana/accounts@2.3.0': + resolution: {integrity: sha512-QgQTj404Z6PXNOyzaOpSzjgMOuGwG8vC66jSDB+3zHaRcEPRVRd2sVSrd1U6sHtnV3aiaS6YyDuPQMheg4K2jw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/addresses@2.3.0': + resolution: {integrity: sha512-ypTNkY2ZaRFpHLnHAgaW8a83N0/WoqdFvCqf4CQmnMdFsZSdC7qOwcbd7YzdaQn9dy+P2hybewzB+KP7LutxGA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/assertions@2.3.0': + resolution: {integrity: sha512-Ekoet3khNg3XFLN7MIz8W31wPQISpKUGDGTylLptI+JjCDWx3PIa88xjEMqFo02WJ8sBj2NLV64Xg1sBcsHjZQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/codecs-core@2.3.0': + resolution: {integrity: sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/codecs-data-structures@2.3.0': + resolution: {integrity: sha512-qvU5LE5DqEdYMYgELRHv+HMOx73sSoV1ZZkwIrclwUmwTbTaH8QAJURBj0RhQ/zCne7VuLLOZFFGv6jGigWhSw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/codecs-numbers@2.3.0': + resolution: {integrity: sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/codecs-strings@2.3.0': + resolution: {integrity: sha512-y5pSBYwzVziXu521hh+VxqUtp0hYGTl1eWGoc1W+8mdvBdC1kTqm/X7aYQw33J42hw03JjryvYOvmGgk3Qz/Ug==} + engines: {node: '>=20.18.0'} + peerDependencies: + fastestsmallesttextencoderdecoder: ^1.0.22 + typescript: '>=5.3.3' + + '@solana/codecs@2.3.0': + resolution: {integrity: sha512-JVqGPkzoeyU262hJGdH64kNLH0M+Oew2CIPOa/9tR3++q2pEd4jU2Rxdfye9sd0Ce3XJrR5AIa8ZfbyQXzjh+g==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/errors@2.3.0': + resolution: {integrity: sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ==} + engines: {node: '>=20.18.0'} + hasBin: true + peerDependencies: + typescript: '>=5.3.3' + + '@solana/fast-stable-stringify@2.3.0': + resolution: {integrity: sha512-KfJPrMEieUg6D3hfQACoPy0ukrAV8Kio883llt/8chPEG3FVTX9z/Zuf4O01a15xZmBbmQ7toil2Dp0sxMJSxw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/functional@2.3.0': + resolution: {integrity: sha512-AgsPh3W3tE+nK3eEw/W9qiSfTGwLYEvl0rWaxHht/lRcuDVwfKRzeSa5G79eioWFFqr+pTtoCr3D3OLkwKz02Q==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/instructions@2.3.0': + resolution: {integrity: sha512-PLMsmaIKu7hEAzyElrk2T7JJx4D+9eRwebhFZpy2PXziNSmFF929eRHKUsKqBFM3cYR1Yy3m6roBZfA+bGE/oQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/keys@2.3.0': + resolution: {integrity: sha512-ZVVdga79pNH+2pVcm6fr2sWz9HTwfopDVhYb0Lh3dh+WBmJjwkabXEIHey2rUES7NjFa/G7sV8lrUn/v8LDCCQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/kit@2.3.0': + resolution: {integrity: sha512-sb6PgwoW2LjE5oTFu4lhlS/cGt/NB3YrShEyx7JgWFWysfgLdJnhwWThgwy/4HjNsmtMrQGWVls0yVBHcMvlMQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/nominal-types@2.3.0': + resolution: {integrity: sha512-uKlMnlP4PWW5UTXlhKM8lcgIaNj8dvd8xO4Y9l+FVvh9RvW2TO0GwUO6JCo7JBzCB0PSqRJdWWaQ8pu1Ti/OkA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/options@2.3.0': + resolution: {integrity: sha512-PPnnZBRCWWoZQ11exPxf//DRzN2C6AoFsDI/u2AsQfYih434/7Kp4XLpfOMT/XESi+gdBMFNNfbES5zg3wAIkw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/programs@2.3.0': + resolution: {integrity: sha512-UXKujV71VCI5uPs+cFdwxybtHZAIZyQkqDiDnmK+DawtOO9mBn4Nimdb/6RjR2CXT78mzO9ZCZ3qfyX+ydcB7w==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/promises@2.3.0': + resolution: {integrity: sha512-GjVgutZKXVuojd9rWy1PuLnfcRfqsaCm7InCiZc8bqmJpoghlyluweNc7ml9Y5yQn1P2IOyzh9+p/77vIyNybQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-api@2.3.0': + resolution: {integrity: sha512-UUdiRfWoyYhJL9PPvFeJr4aJ554ob2jXcpn4vKmRVn9ire0sCbpQKYx6K8eEKHZWXKrDW8IDspgTl0gT/aJWVg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-parsed-types@2.3.0': + resolution: {integrity: sha512-B5pHzyEIbBJf9KHej+zdr5ZNAdSvu7WLU2lOUPh81KHdHQs6dEb310LGxcpCc7HVE8IEdO20AbckewDiAN6OCg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-spec-types@2.3.0': + resolution: {integrity: sha512-xQsb65lahjr8Wc9dMtP7xa0ZmDS8dOE2ncYjlvfyw/h4mpdXTUdrSMi6RtFwX33/rGuztQ7Hwaid5xLNSLvsFQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-spec@2.3.0': + resolution: {integrity: sha512-fA2LMX4BMixCrNB2n6T83AvjZ3oUQTu7qyPLyt8gHQaoEAXs8k6GZmu6iYcr+FboQCjUmRPgMaABbcr9j2J9Sw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-subscriptions-api@2.3.0': + resolution: {integrity: sha512-9mCjVbum2Hg9KGX3LKsrI5Xs0KX390lS+Z8qB80bxhar6MJPugqIPH8uRgLhCW9GN3JprAfjRNl7our8CPvsPQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-subscriptions-channel-websocket@2.3.0': + resolution: {integrity: sha512-2oL6ceFwejIgeWzbNiUHI2tZZnaOxNTSerszcin7wYQwijxtpVgUHiuItM/Y70DQmH9sKhmikQp+dqeGalaJxw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + ws: ^8.18.0 + + '@solana/rpc-subscriptions-spec@2.3.0': + resolution: {integrity: sha512-rdmVcl4PvNKQeA2l8DorIeALCgJEMSu7U8AXJS1PICeb2lQuMeaR+6cs/iowjvIB0lMVjYN2sFf6Q3dJPu6wWg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-subscriptions@2.3.0': + resolution: {integrity: sha512-Uyr10nZKGVzvCOqwCZgwYrzuoDyUdwtgQRefh13pXIrdo4wYjVmoLykH49Omt6abwStB0a4UL5gX9V4mFdDJZg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-transformers@2.3.0': + resolution: {integrity: sha512-UuHYK3XEpo9nMXdjyGKkPCOr7WsZsxs7zLYDO1A5ELH3P3JoehvrDegYRAGzBS2VKsfApZ86ZpJToP0K3PhmMA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-transport-http@2.3.0': + resolution: {integrity: sha512-HFKydmxGw8nAF5N+S0NLnPBDCe5oMDtI2RAmW8DMqP4U3Zxt2XWhvV1SNkAldT5tF0U1vP+is6fHxyhk4xqEvg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-types@2.3.0': + resolution: {integrity: sha512-O09YX2hED2QUyGxrMOxQ9GzH1LlEwwZWu69QbL4oYmIf6P5dzEEHcqRY6L1LsDVqc/dzAdEs/E1FaPrcIaIIPw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc@2.3.0': + resolution: {integrity: sha512-ZWN76iNQAOCpYC7yKfb3UNLIMZf603JckLKOOLTHuy9MZnTN8XV6uwvDFhf42XvhglgUjGCEnbUqWtxQ9pa/pQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/signers@2.3.0': + resolution: {integrity: sha512-OSv6fGr/MFRx6J+ZChQMRqKNPGGmdjkqarKkRzkwmv7v8quWsIRnJT5EV8tBy3LI4DLO/A8vKiNSPzvm1TdaiQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/subscribable@2.3.0': + resolution: {integrity: sha512-DkgohEDbMkdTWiKAoatY02Njr56WXx9e/dKKfmne8/Ad6/2llUIrax78nCdlvZW9quXMaXPTxZvdQqo9N669Og==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/sysvars@2.3.0': + resolution: {integrity: sha512-LvjADZrpZ+CnhlHqfI5cmsRzX9Rpyb1Ox2dMHnbsRNzeKAMhu9w4ZBIaeTdO322zsTr509G1B+k2ABD3whvUBA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/transaction-confirmation@2.3.0': + resolution: {integrity: sha512-UiEuiHCfAAZEKdfne/XljFNJbsKAe701UQHKXEInYzIgBjRbvaeYZlBmkkqtxwcasgBTOmEaEKT44J14N9VZDw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/transaction-messages@2.3.0': + resolution: {integrity: sha512-bgqvWuy3MqKS5JdNLH649q+ngiyOu5rGS3DizSnWwYUd76RxZl1kN6CoqHSrrMzFMvis6sck/yPGG3wqrMlAww==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/transactions@2.3.0': + resolution: {integrity: sha512-LnTvdi8QnrQtuEZor5Msje61sDpPstTVwKg4y81tNxDhiyomjuvnSNLAq6QsB9gIxUqbNzPZgOG9IU4I4/Uaug==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@tanstack/query-core@5.90.2': + resolution: {integrity: sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==} + + '@tanstack/react-query@5.90.2': + resolution: {integrity: sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw==} + peerDependencies: + react: ^18 || ^19 + + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/lodash@4.17.20': + resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node@24.5.2': + resolution: {integrity: sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@wagmi/connectors@5.11.1': + resolution: {integrity: sha512-2jbx6z8rk/srQdbnKF2viCc3ke02HSMQU5EZS90jWoJ1t4+Fn3+bhphgVeJ1LnVFWsuEBSuIcY63iic4emwGfg==} + peerDependencies: + '@wagmi/core': 2.21.1 + typescript: '>=5.0.4' + viem: 2.x + peerDependenciesMeta: + typescript: + optional: true + + '@wagmi/core@2.21.1': + resolution: {integrity: sha512-uG0Cujm24acrFYqbi1RGw9MRMLTGVKvyv5OAJT+2pkcasM9PP8eJCzhlvUxK9IYVXXnbaAT8JX/rXEurOSM6mg==} + peerDependencies: + '@tanstack/query-core': '>=5.0.0' + typescript: '>=5.0.4' + viem: 2.x + peerDependenciesMeta: + '@tanstack/query-core': + optional: true + typescript: + optional: true + + '@walletconnect/core@2.21.0': + resolution: {integrity: sha512-o6R7Ua4myxR8aRUAJ1z3gT9nM+jd2B2mfamu6arzy1Cc6vi10fIwFWb6vg3bC8xJ6o9H3n/cN5TOW3aA9Y1XVw==} + engines: {node: '>=18'} + + '@walletconnect/core@2.21.1': + resolution: {integrity: sha512-Tp4MHJYcdWD846PH//2r+Mu4wz1/ZU/fr9av1UWFiaYQ2t2TPLDiZxjLw54AAEpMqlEHemwCgiRiAmjR1NDdTQ==} + engines: {node: '>=18'} + + '@walletconnect/environment@1.0.1': + resolution: {integrity: sha512-T426LLZtHj8e8rYnKfzsw1aG6+M0BT1ZxayMdv/p8yM0MU+eJDISqNY3/bccxRr4LrF9csq02Rhqt08Ibl0VRg==} + + '@walletconnect/ethereum-provider@2.21.1': + resolution: {integrity: sha512-SSlIG6QEVxClgl1s0LMk4xr2wg4eT3Zn/Hb81IocyqNSGfXpjtawWxKxiC5/9Z95f1INyBD6MctJbL/R1oBwIw==} + + '@walletconnect/events@1.0.1': + resolution: {integrity: sha512-NPTqaoi0oPBVNuLv7qPaJazmGHs5JGyO8eEAk5VGKmJzDR7AHzD4k6ilox5kxk1iwiOnFopBOOMLs86Oa76HpQ==} + + '@walletconnect/heartbeat@1.2.2': + resolution: {integrity: sha512-uASiRmC5MwhuRuf05vq4AT48Pq8RMi876zV8rr8cV969uTOzWdB/k+Lj5yI2PBtB1bGQisGen7MM1GcZlQTBXw==} + + '@walletconnect/jsonrpc-http-connection@1.0.8': + resolution: {integrity: sha512-+B7cRuaxijLeFDJUq5hAzNyef3e3tBDIxyaCNmFtjwnod5AGis3RToNqzFU33vpVcxFhofkpE7Cx+5MYejbMGw==} + + '@walletconnect/jsonrpc-provider@1.0.14': + resolution: {integrity: sha512-rtsNY1XqHvWj0EtITNeuf8PHMvlCLiS3EjQL+WOkxEOA4KPxsohFnBDeyPYiNm4ZvkQdLnece36opYidmtbmow==} + + '@walletconnect/jsonrpc-types@1.0.4': + resolution: {integrity: sha512-P6679fG/M+wuWg9TY8mh6xFSdYnFyFjwFelxyISxMDrlbXokorEVXYOxiqEbrU3x1BmBoCAJJ+vtEaEoMlpCBQ==} + + '@walletconnect/jsonrpc-utils@1.0.8': + resolution: {integrity: sha512-vdeb03bD8VzJUL6ZtzRYsFMq1eZQcM3EAzT0a3st59dyLfJ0wq+tKMpmGH7HlB7waD858UWgfIcudbPFsbzVdw==} + + '@walletconnect/jsonrpc-ws-connection@1.0.16': + resolution: {integrity: sha512-G81JmsMqh5nJheE1mPst1W0WfVv0SG3N7JggwLLGnI7iuDZJq8cRJvQwLGKHn5H1WTW7DEPCo00zz5w62AbL3Q==} + + '@walletconnect/keyvaluestorage@1.1.1': + resolution: {integrity: sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA==} + peerDependencies: + '@react-native-async-storage/async-storage': 1.x + peerDependenciesMeta: + '@react-native-async-storage/async-storage': + optional: true + + '@walletconnect/logger@2.1.2': + resolution: {integrity: sha512-aAb28I3S6pYXZHQm5ESB+V6rDqIYfsnHaQyzFbwUUBFY4H0OXx/YtTl8lvhUNhMMfb9UxbwEBS253TlXUYJWSw==} + + '@walletconnect/relay-api@1.0.11': + resolution: {integrity: sha512-tLPErkze/HmC9aCmdZOhtVmYZq1wKfWTJtygQHoWtgg722Jd4homo54Cs4ak2RUFUZIGO2RsOpIcWipaua5D5Q==} + + '@walletconnect/relay-auth@1.1.0': + resolution: {integrity: sha512-qFw+a9uRz26jRCDgL7Q5TA9qYIgcNY8jpJzI1zAWNZ8i7mQjaijRnWFKsCHAU9CyGjvt6RKrRXyFtFOpWTVmCQ==} + + '@walletconnect/safe-json@1.0.2': + resolution: {integrity: sha512-Ogb7I27kZ3LPC3ibn8ldyUr5544t3/STow9+lzz7Sfo808YD7SBWk7SAsdBFlYgP2zDRy2hS3sKRcuSRM0OTmA==} + + '@walletconnect/sign-client@2.21.0': + resolution: {integrity: sha512-z7h+PeLa5Au2R591d/8ZlziE0stJvdzP9jNFzFolf2RG/OiXulgFKum8PrIyXy+Rg2q95U9nRVUF9fWcn78yBA==} + + '@walletconnect/sign-client@2.21.1': + resolution: {integrity: sha512-QaXzmPsMnKGV6tc4UcdnQVNOz4zyXgarvdIQibJ4L3EmLat73r5ZVl4c0cCOcoaV7rgM9Wbphgu5E/7jNcd3Zg==} + + '@walletconnect/time@1.0.2': + resolution: {integrity: sha512-uzdd9woDcJ1AaBZRhqy5rNC9laqWGErfc4dxA9a87mPdKOgWMD85mcFo9dIYIts/Jwocfwn07EC6EzclKubk/g==} + + '@walletconnect/types@2.21.0': + resolution: {integrity: sha512-ll+9upzqt95ZBWcfkOszXZkfnpbJJ2CmxMfGgE5GmhdxxxCcO5bGhXkI+x8OpiS555RJ/v/sXJYMSOLkmu4fFw==} + + '@walletconnect/types@2.21.1': + resolution: {integrity: sha512-UeefNadqP6IyfwWC1Yi7ux+ljbP2R66PLfDrDm8izmvlPmYlqRerJWJvYO4t0Vvr9wrG4Ko7E0c4M7FaPKT/sQ==} + + '@walletconnect/universal-provider@2.21.0': + resolution: {integrity: sha512-mtUQvewt+X0VBQay/xOJBvxsB3Xsm1lTwFjZ6WUwSOTR1X+FNb71hSApnV5kbsdDIpYPXeQUbGt2se1n5E5UBg==} + + '@walletconnect/universal-provider@2.21.1': + resolution: {integrity: sha512-Wjx9G8gUHVMnYfxtasC9poGm8QMiPCpXpbbLFT+iPoQskDDly8BwueWnqKs4Mx2SdIAWAwuXeZ5ojk5qQOxJJg==} + + '@walletconnect/utils@2.21.0': + resolution: {integrity: sha512-zfHLiUoBrQ8rP57HTPXW7rQMnYxYI4gT9yTACxVW6LhIFROTF6/ytm5SKNoIvi4a5nX5dfXG4D9XwQUCu8Ilig==} + + '@walletconnect/utils@2.21.1': + resolution: {integrity: sha512-VPZvTcrNQCkbGOjFRbC24mm/pzbRMUq2DSQoiHlhh0X1U7ZhuIrzVtAoKsrzu6rqjz0EEtGxCr3K1TGRqDG4NA==} + + '@walletconnect/window-getters@1.0.1': + resolution: {integrity: sha512-vHp+HqzGxORPAN8gY03qnbTMnhqIwjeRJNOMOAzePRg4xVEEE2WvYsI9G2NMjOknA8hnuYbU3/hwLcKbjhc8+Q==} + + '@walletconnect/window-metadata@1.0.1': + resolution: {integrity: sha512-9koTqyGrM2cqFRW517BPY/iEtUDx2r1+Pwwu5m7sJ7ka79wi3EyqhqcICk/yDmv6jAS1rjKgTKXlEhanYjijcA==} + + abitype@1.0.8: + resolution: {integrity: sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==} + peerDependencies: + typescript: '>=5.0.4' + zod: ^3 >=3.22.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + + abitype@1.1.0: + resolution: {integrity: sha512-6Vh4HcRxNMLA0puzPjM5GBgT4aAcFGKZzSgAXvuZ27shJP6NEpielTuqbBmZILR5/xd0PizkBGy5hReKz9jl5A==} + peerDependencies: + typescript: '>=5.0.4' + zod: ^3.22.0 || ^4.0.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + + abitype@1.1.1: + resolution: {integrity: sha512-Loe5/6tAgsBukY95eGaPSDmQHIjRZYQq8PB1MpsNccDIK8WiV+Uw6WzaIXipvaxTEL2yEB0OpEaQv3gs8pkS9Q==} + peerDependencies: + typescript: '>=5.0.4' + zod: ^3.22.0 || ^4.0.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + async-mutex@0.2.6: + resolution: {integrity: sha512-Hs4R+4SPgamu6rSGW8C7cV9gaWUKEHykfzCCvIRuaVv636Ju10ZdeUbvb4TBEW0INuq2DHZqXbK4Nd3yG4RaRw==} + + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + base-x@5.0.1: + resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + big.js@6.2.2: + resolution: {integrity: sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ==} + + bn.js@5.2.2: + resolution: {integrity: sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==} + + body-parser@2.2.0: + resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} + engines: {node: '>=18'} + + bowser@2.12.1: + resolution: {integrity: sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==} + + bs58@6.0.0: + resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + bufferutil@4.0.9: + resolution: {integrity: sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==} + engines: {node: '>=6.14.2'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + + clsx@1.2.1: + resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} + engines: {node: '>=6'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@14.0.1: + resolution: {integrity: sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==} + engines: {node: '>=20'} + + content-disposition@1.0.0: + resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cookie-es@1.2.2: + resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + cross-fetch@3.2.0: + resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==} + + cross-fetch@4.1.0: + resolution: {integrity: sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==} + + crossws@0.3.5: + resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} + + date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} + + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + + debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + + decode-uri-component@0.2.2: + resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} + engines: {node: '>=0.10'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + derive-valtio@0.1.0: + resolution: {integrity: sha512-OCg2UsLbXK7GmmpzMXhYkdO64vhJ1ROUUGaTFyHjVwEdMEcTTRj7W1TxLbSBxdY8QLBPCcp66MTyaSy0RpO17A==} + peerDependencies: + valtio: '*' + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + detect-browser@5.3.0: + resolution: {integrity: sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w==} + + dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + + dotenv@17.2.2: + resolution: {integrity: sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + duplexify@4.1.3: + resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} + + eciesjs@0.4.15: + resolution: {integrity: sha512-r6kEJXDKecVOCj2nLMuXK/FCPeurW33+3JRpfXVbjLja3XUYFfD9I/JBreH6sUyzcm3G/YQboBjMla6poKeSdA==} + engines: {bun: '>=1', deno: '>=2', node: '>=16'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + encode-utf8@1.0.3: + resolution: {integrity: sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + engine.io-client@6.6.3: + resolution: {integrity: sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==} + + engine.io-parser@5.2.3: + resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} + engines: {node: '>=10.0.0'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-toolkit@1.33.0: + resolution: {integrity: sha512-X13Q/ZSc+vsO1q600bvNK4bxgXMkHcf//RxCmYDaRY5DAcT+eoXjY5hoAPGMdRnWQjvyLEcyauG3b6hz76LNqg==} + + esbuild@0.25.10: + resolution: {integrity: sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==} + engines: {node: '>=18'} + hasBin: true + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eth-block-tracker@7.1.0: + resolution: {integrity: sha512-8YdplnuE1IK4xfqpf4iU7oBxnOYAc35934o083G8ao+8WM8QQtt/mVlAY6yIAdY1eMeLqg4Z//PZjJGmWGPMRg==} + engines: {node: '>=14.0.0'} + + eth-json-rpc-filters@6.0.1: + resolution: {integrity: sha512-ITJTvqoCw6OVMLs7pI8f4gG92n/St6x80ACtHodeS+IXmO0w+t1T5OOzfSt7KLSMLRkVUoexV7tztLgDxg+iig==} + engines: {node: '>=14.0.0'} + + eth-query@2.1.2: + resolution: {integrity: sha512-srES0ZcvwkR/wd5OQBRA1bIJMww1skfGS0s8wlwK3/oNP4+wnds60krvu5R1QbpRQjMmpG5OMIWro5s7gvDPsA==} + + eth-rpc-errors@4.0.3: + resolution: {integrity: sha512-Z3ymjopaoft7JDoxZcEb3pwdGh7yiYMhOwm2doUt6ASXlMavpNlK6Cre0+IMl2VSGyEU9rkiperQhp5iRxn5Pg==} + + ethereum-cryptography@2.2.1: + resolution: {integrity: sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==} + + eventemitter2@6.4.9: + resolution: {integrity: sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==} + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + express@5.1.0: + resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} + engines: {node: '>= 18'} + + extension-port-stream@3.0.0: + resolution: {integrity: sha512-an2S5quJMiy5bnZKEf6AkfH/7r8CzHvhchU40gxN+OM6HPhe7Z9T1FUychcf2M9PpPOO0Hf7BAEfJkw2TDIBDw==} + engines: {node: '>=12.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-redact@3.5.0: + resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} + engines: {node: '>=6'} + + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + + fastestsmallesttextencoderdecoder@1.0.22: + resolution: {integrity: sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==} + + filter-obj@1.1.0: + resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==} + engines: {node: '>=0.10.0'} + + finalhandler@2.1.0: + resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} + engines: {node: '>= 0.8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.10.1: + resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + h3@1.15.4: + resolution: {integrity: sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hono@4.9.8: + resolution: {integrity: sha512-JW8Bb4RFWD9iOKxg5PbUarBYGM99IcxFl2FPBo2gSJO11jjUDqlP1Bmfyqt8Z/dGhIQ63PMA9LdcLefXyIasyg==} + engines: {node: '>=16.9.0'} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.7.0: + resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} + engines: {node: '>=0.10.0'} + + idb-keyval@6.2.1: + resolution: {integrity: sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==} + + idb-keyval@6.2.2: + resolution: {integrity: sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + iron-webcrypto@1.2.1: + resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-function@1.1.0: + resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} + engines: {node: '>= 0.4'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isows@1.0.6: + resolution: {integrity: sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw==} + peerDependencies: + ws: '*' + + isows@1.0.7: + resolution: {integrity: sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==} + peerDependencies: + ws: '*' + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + json-rpc-engine@6.1.0: + resolution: {integrity: sha512-NEdLrtrq1jUZyfjkr9OCz9EzCNhnRyWtt1PAnvnhwy6e8XETS0Dtc+ZNCO2gvuAoKsIn2+vCSowXTYE4CkgnAQ==} + engines: {node: '>=10.0.0'} + + json-rpc-random-id@1.0.1: + resolution: {integrity: sha512-RJ9YYNCkhVDBuP4zN5BBtYAzEl03yq/jIIsyif0JY9qyJuQQZNeDK7anAPKKlyEtLSj2s8h6hNh2F8zO5q7ScA==} + + keccak@3.0.4: + resolution: {integrity: sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q==} + engines: {node: '>=10.0.0'} + + keyvaluestorage-interface@1.0.0: + resolution: {integrity: sha512-8t6Q3TclQ4uZynJY9IGr2+SsIGwK9JHcO6ootkHCGA0CrQCRy+VkouYNO2xicET6b9al7QKzpebNow+gkpCL8g==} + + lit-element@4.2.1: + resolution: {integrity: sha512-WGAWRGzirAgyphK2urmYOV72tlvnxw7YfyLDgQ+OZnM9vQQBQnumQ7jUJe6unEzwGU3ahFOjuz1iz1jjrpCPuw==} + + lit-html@3.3.1: + resolution: {integrity: sha512-S9hbyDu/vs1qNrithiNyeyv64c9yqiW9l+DBgI18fL+MTvOtWoFR0FWiyq1TxaYef5wNlpEmzlXoBlZEO+WjoA==} + + lit@3.3.0: + resolution: {integrity: sha512-DGVsqsOIHBww2DqnuZzW7QsuCdahp50ojuDaBPC7jUDRpYoH0z7kHBBYZewRzer75FwtrkmkKk7iOAwSaWdBmw==} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + micro-ftch@0.3.1: + resolution: {integrity: sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg==} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.1: + resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} + engines: {node: '>= 0.6'} + + mipd@0.0.7: + resolution: {integrity: sha512-aAPZPNDQ3uMTdKbuO2YmAw2TxLHO0moa4YKAyETM/DTj5FloZo+a+8tU+iv4GmW+sOxKLSRwcSFuczk+Cpt6fg==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + multiformats@9.9.0: + resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + node-addon-api@2.0.2: + resolution: {integrity: sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==} + + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + + node-mock-http@1.0.3: + resolution: {integrity: sha512-jN8dK25fsfnMrVsEhluUTPkBFY+6ybu7jSB1n+ri/vOGjJxU8J9CZhpSGkHXSkFjtUhbmoncG/YG9ta5Ludqog==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + obj-multiplex@1.0.0: + resolution: {integrity: sha512-0GNJAOsHoBHeNTvl5Vt6IWnpUEcc3uSRxzBri7EDyIcMgYvnY2JL2qdeV5zTMjWQX5OHcD5amcW2HFfDh0gjIA==} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + ofetch@1.4.1: + resolution: {integrity: sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==} + + on-exit-leak-free@0.2.0: + resolution: {integrity: sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg==} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + openapi-fetch@0.13.8: + resolution: {integrity: sha512-yJ4QKRyNxE44baQ9mY5+r/kAzZ8yXMemtNAOFwOzRXJscdjSxxzWSNlyBAr+o5JjkUw9Lc3W7OIoca0cY3PYnQ==} + + openapi-typescript-helpers@0.0.15: + resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==} + + ox@0.6.7: + resolution: {integrity: sha512-17Gk/eFsFRAZ80p5eKqv89a57uXjd3NgIf1CaXojATPBuujVc/fQSVhBeAU9JCRB+k7J50WQAyWTxK19T9GgbA==} + peerDependencies: + typescript: '>=5.4.0' + peerDependenciesMeta: + typescript: + optional: true + + ox@0.6.9: + resolution: {integrity: sha512-wi5ShvzE4eOcTwQVsIPdFr+8ycyX+5le/96iAJutaZAvCes1J0+RvpEPg5QDPDiaR0XQQAvZVl7AwqQcINuUug==} + peerDependencies: + typescript: '>=5.4.0' + peerDependenciesMeta: + typescript: + optional: true + + ox@0.9.6: + resolution: {integrity: sha512-8SuCbHPvv2eZLYXrNmC0EC12rdzXQLdhnOMlHDW2wiCPLxBrOOJwX5L5E61by+UjTPOryqQiRSnjIKCI+GykKg==} + peerDependencies: + typescript: '>=5.4.0' + peerDependenciesMeta: + typescript: + optional: true + + ox@0.9.7: + resolution: {integrity: sha512-KX9Lvv0Rd+SKZXcT6Y85cuYbkmrQbK8Bz26k+s3X4EiT5bSU/hTDNSK/ApBeorJeIaOZbxEmJD2hHW0q1vIDEA==} + peerDependencies: + typescript: '>=5.4.0' + peerDependenciesMeta: + typescript: + optional: true + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + pify@3.0.0: + resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} + engines: {node: '>=4'} + + pify@5.0.0: + resolution: {integrity: sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==} + engines: {node: '>=10'} + + pino-abstract-transport@0.5.0: + resolution: {integrity: sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ==} + + pino-std-serializers@4.0.0: + resolution: {integrity: sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q==} + + pino@7.11.0: + resolution: {integrity: sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg==} + hasBin: true + + pngjs@5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + + pony-cause@2.1.11: + resolution: {integrity: sha512-M7LhCsdNbNgiLYiP4WjsfLUuFmCfnjdF6jKe2R9NKl4WFN+HZPGHJZ9lnLP7f9ZnKe3U9nuWD0szirmj+migUg==} + engines: {node: '>=12.0.0'} + + porto@0.2.19: + resolution: {integrity: sha512-q1vEJgdtlEOf6byWgD31GHiMwpfLuxFSfx9f7Sw4RGdvpQs2ANBGfnzzardADZegr87ZXsebSp+3vaaznEUzPQ==} + hasBin: true + peerDependencies: + '@tanstack/react-query': '>=5.59.0' + '@wagmi/core': '>=2.16.3' + react: '>=18' + typescript: '>=5.4.0' + viem: '>=2.37.0' + wagmi: '>=2.0.0' + peerDependenciesMeta: + '@tanstack/react-query': + optional: true + react: + optional: true + typescript: + optional: true + wagmi: + optional: true + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + preact@10.24.2: + resolution: {integrity: sha512-1cSoF0aCC8uaARATfrlz4VCBqE8LwZwRfLgkxJOQwAlQt6ayTmi0D9OF7nXid1POI5SZidFuG9CnlXbDfLqY/Q==} + + preact@10.27.2: + resolution: {integrity: sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + process-warning@1.0.0: + resolution: {integrity: sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + proxy-compare@2.6.0: + resolution: {integrity: sha512-8xuCeM3l8yqdmbPoYeLbrAXCBWu19XEYc5/F28f5qOaoAIMyfmBUkl5axiK+x9olUvRlcekvnm98AP9RDngOIw==} + + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + + qrcode@1.5.3: + resolution: {integrity: sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==} + engines: {node: '>=10.13.0'} + hasBin: true + + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + + query-string@7.1.3: + resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} + engines: {node: '>=6'} + + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + + radix3@1.1.2: + resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.1: + resolution: {integrity: sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==} + engines: {node: '>= 0.10'} + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + real-require@0.1.0: + resolution: {integrity: sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg==} + engines: {node: '>= 12.13.0'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + send@1.2.0: + resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} + engines: {node: '>= 18'} + + serve-static@2.2.0: + resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} + engines: {node: '>= 18'} + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + sha.js@2.4.12: + resolution: {integrity: sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==} + engines: {node: '>= 0.10'} + hasBin: true + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + socket.io-client@4.8.1: + resolution: {integrity: sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==} + engines: {node: '>=10.0.0'} + + socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + + sonic-boom@2.8.0: + resolution: {integrity: sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg==} + + split-on-first@1.1.0: + resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} + engines: {node: '>=6'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + stream-shift@1.0.3: + resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} + + strict-uri-encode@2.0.0: + resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} + engines: {node: '>=4'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + superstruct@1.0.4: + resolution: {integrity: sha512-7JpaAoX2NGyoFlI9NBh66BQXGONc+uE+MRS5i2iOBKuS4e+ccgMDjATgZldkah+33DakBxDHiss9kvUcGAO8UQ==} + engines: {node: '>=14.0.0'} + + thread-stream@0.15.2: + resolution: {integrity: sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA==} + + to-buffer@1.2.2: + resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==} + engines: {node: '>= 0.4'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.20.5: + resolution: {integrity: sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typescript@5.9.2: + resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + + uint8arrays@3.1.0: + resolution: {integrity: sha512-ei5rfKtoRO8OyOIor2Rz5fhzjThwIHJZ3uyDPnDHTXbP0aMQ1RN/6AI5B5d9dBxJOU+BvOAk7ZQ1xphsX8Lrog==} + + uncrypto@0.1.3: + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + + undici-types@7.12.0: + resolution: {integrity: sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==} + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + unstorage@1.17.1: + resolution: {integrity: sha512-KKGwRTT0iVBCErKemkJCLs7JdxNVfqTPc/85ae1XES0+bsHbc/sFBfVi5kJp156cc51BHinIH2l3k0EZ24vOBQ==} + peerDependencies: + '@azure/app-configuration': ^1.8.0 + '@azure/cosmos': ^4.2.0 + '@azure/data-tables': ^13.3.0 + '@azure/identity': ^4.6.0 + '@azure/keyvault-secrets': ^4.9.0 + '@azure/storage-blob': ^12.26.0 + '@capacitor/preferences': ^6.0.3 || ^7.0.0 + '@deno/kv': '>=0.9.0' + '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 + '@planetscale/database': ^1.19.0 + '@upstash/redis': ^1.34.3 + '@vercel/blob': '>=0.27.1' + '@vercel/functions': ^2.2.12 || ^3.0.0 + '@vercel/kv': ^1.0.1 + aws4fetch: ^1.0.20 + db0: '>=0.2.1' + idb-keyval: ^6.2.1 + ioredis: ^5.4.2 + uploadthing: ^7.4.4 + peerDependenciesMeta: + '@azure/app-configuration': + optional: true + '@azure/cosmos': + optional: true + '@azure/data-tables': + optional: true + '@azure/identity': + optional: true + '@azure/keyvault-secrets': + optional: true + '@azure/storage-blob': + optional: true + '@capacitor/preferences': + optional: true + '@deno/kv': + optional: true + '@netlify/blobs': + optional: true + '@planetscale/database': + optional: true + '@upstash/redis': + optional: true + '@vercel/blob': + optional: true + '@vercel/functions': + optional: true + '@vercel/kv': + optional: true + aws4fetch: + optional: true + db0: + optional: true + idb-keyval: + optional: true + ioredis: + optional: true + uploadthing: + optional: true + + use-sync-external-store@1.2.0: + resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + use-sync-external-store@1.4.0: + resolution: {integrity: sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + utf-8-validate@5.0.10: + resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==} + engines: {node: '>=6.14.2'} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + + valtio@1.13.2: + resolution: {integrity: sha512-Qik0o+DSy741TmkqmRfjq+0xpZBXi/Y6+fXZLn0xNF1z/waFMbE3rkivv5Zcf9RrMUp6zswf2J7sbh2KBlba5A==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=16.8' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + viem@2.23.2: + resolution: {integrity: sha512-NVmW/E0c5crMOtbEAqMF0e3NmvQykFXhLOc/CkLIXOlzHSA6KXVz3CYVmaKqBF8/xtjsjHAGjdJN3Ru1kFJLaA==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + + viem@2.37.8: + resolution: {integrity: sha512-mL+5yvCQbRIR6QvngDQMfEiZTfNWfd+/QL5yFaOoYbpH3b1Q2ddwF7YG2eI2AcYSh9LE1gtUkbzZLFUAVyj4oQ==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + + wagmi@2.17.4: + resolution: {integrity: sha512-AJpTQ5O7JFv2LDaRuMiCKxZEVBeDprh9oagA0dLSp/iAEsbd5VwmMy3rn4lCO3pl9umJ+afEkuRIq+5dx02jzg==} + peerDependencies: + '@tanstack/react-query': '>=5.0.0' + react: '>=18' + typescript: '>=5.0.4' + viem: 2.x + peerDependenciesMeta: + typescript: + optional: true + + webextension-polyfill@0.10.0: + resolution: {integrity: sha512-c5s35LgVa5tFaHhrZDnr3FpQpjj1BB+RXhLTYUxGqBVN460HkbM8TBtEqdXWbpTKfzwCcjAZVF7zXCYSKtcp9g==} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + engines: {node: '>= 0.4'} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + x402@0.6.1: + resolution: {integrity: sha512-9UmeCSsYzFGav5FdVP70VplKlR3V90P0DZ9fPSrlLVp0ifUVi1S9TztvegkmIHE9xTGZ1GWNi+bkne6N0Ea58w==} + + xmlhttprequest-ssl@2.1.2: + resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} + engines: {node: '>=0.4.0'} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + + zod@3.22.4: + resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + + zod@4.1.11: + resolution: {integrity: sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==} + + zustand@5.0.0: + resolution: {integrity: sha512-LE+VcmbartOPM+auOjCCLQOsQ05zUTp8RkgwRzefUk+2jISdMMFnxvyTjA4YNWr5ZGXYbVsEMZosttuxUBkojQ==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + + zustand@5.0.3: + resolution: {integrity: sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + + zustand@5.0.8: + resolution: {integrity: sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + +snapshots: + + '@adraffy/ens-normalize@1.11.1': {} + + '@babel/runtime@7.28.4': {} + + '@base-org/account@1.1.1(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@noble/hashes': 1.4.0 + clsx: 1.2.1 + eventemitter3: 5.0.1 + idb-keyval: 6.2.1 + ox: 0.6.9(typescript@5.9.2)(zod@3.25.76) + preact: 10.24.2 + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + zustand: 5.0.3(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) + transitivePeerDependencies: + - '@types/react' + - bufferutil + - immer + - react + - typescript + - use-sync-external-store + - utf-8-validate + - zod + + '@coinbase/wallet-sdk@3.9.3': + dependencies: + bn.js: 5.2.2 + buffer: 6.0.3 + clsx: 1.2.1 + eth-block-tracker: 7.1.0 + eth-json-rpc-filters: 6.0.1 + eventemitter3: 5.0.1 + keccak: 3.0.4 + preact: 10.27.2 + sha.js: 2.4.12 + transitivePeerDependencies: + - supports-color + + '@coinbase/wallet-sdk@4.3.6(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@noble/hashes': 1.4.0 + clsx: 1.2.1 + eventemitter3: 5.0.1 + idb-keyval: 6.2.1 + ox: 0.6.9(typescript@5.9.2)(zod@3.25.76) + preact: 10.24.2 + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + zustand: 5.0.3(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) + transitivePeerDependencies: + - '@types/react' + - bufferutil + - immer + - react + - typescript + - use-sync-external-store + - utf-8-validate + - zod + + '@ecies/ciphers@0.2.4(@noble/ciphers@1.3.0)': + dependencies: + '@noble/ciphers': 1.3.0 + + '@esbuild/aix-ppc64@0.25.10': + optional: true + + '@esbuild/android-arm64@0.25.10': + optional: true + + '@esbuild/android-arm@0.25.10': + optional: true + + '@esbuild/android-x64@0.25.10': + optional: true + + '@esbuild/darwin-arm64@0.25.10': + optional: true + + '@esbuild/darwin-x64@0.25.10': + optional: true + + '@esbuild/freebsd-arm64@0.25.10': + optional: true + + '@esbuild/freebsd-x64@0.25.10': + optional: true + + '@esbuild/linux-arm64@0.25.10': + optional: true + + '@esbuild/linux-arm@0.25.10': + optional: true + + '@esbuild/linux-ia32@0.25.10': + optional: true + + '@esbuild/linux-loong64@0.25.10': + optional: true + + '@esbuild/linux-mips64el@0.25.10': + optional: true + + '@esbuild/linux-ppc64@0.25.10': + optional: true + + '@esbuild/linux-riscv64@0.25.10': + optional: true + + '@esbuild/linux-s390x@0.25.10': + optional: true + + '@esbuild/linux-x64@0.25.10': + optional: true + + '@esbuild/netbsd-arm64@0.25.10': + optional: true + + '@esbuild/netbsd-x64@0.25.10': + optional: true + + '@esbuild/openbsd-arm64@0.25.10': + optional: true + + '@esbuild/openbsd-x64@0.25.10': + optional: true + + '@esbuild/openharmony-arm64@0.25.10': + optional: true + + '@esbuild/sunos-x64@0.25.10': + optional: true + + '@esbuild/win32-arm64@0.25.10': + optional: true + + '@esbuild/win32-ia32@0.25.10': + optional: true + + '@esbuild/win32-x64@0.25.10': + optional: true + + '@ethereumjs/common@3.2.0': + dependencies: + '@ethereumjs/util': 8.1.0 + crc-32: 1.2.2 + + '@ethereumjs/rlp@4.0.1': {} + + '@ethereumjs/tx@4.2.0': + dependencies: + '@ethereumjs/common': 3.2.0 + '@ethereumjs/rlp': 4.0.1 + '@ethereumjs/util': 8.1.0 + ethereum-cryptography: 2.2.1 + + '@ethereumjs/util@8.1.0': + dependencies: + '@ethereumjs/rlp': 4.0.1 + ethereum-cryptography: 2.2.1 + micro-ftch: 0.3.1 + + '@gemini-wallet/core@0.2.0(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))': + dependencies: + '@metamask/rpc-errors': 7.0.2 + eventemitter3: 5.0.1 + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + transitivePeerDependencies: + - supports-color + + '@kora/sdk@file:../../../../sdks/ts': {} + + '@lit-labs/ssr-dom-shim@1.4.0': {} + + '@lit/reactive-element@2.1.1': + dependencies: + '@lit-labs/ssr-dom-shim': 1.4.0 + + '@metamask/eth-json-rpc-provider@1.0.1': + dependencies: + '@metamask/json-rpc-engine': 7.3.3 + '@metamask/safe-event-emitter': 3.1.2 + '@metamask/utils': 5.0.2 + transitivePeerDependencies: + - supports-color + + '@metamask/json-rpc-engine@7.3.3': + dependencies: + '@metamask/rpc-errors': 6.4.0 + '@metamask/safe-event-emitter': 3.1.2 + '@metamask/utils': 8.5.0 + transitivePeerDependencies: + - supports-color + + '@metamask/json-rpc-engine@8.0.2': + dependencies: + '@metamask/rpc-errors': 6.4.0 + '@metamask/safe-event-emitter': 3.1.2 + '@metamask/utils': 8.5.0 + transitivePeerDependencies: + - supports-color + + '@metamask/json-rpc-middleware-stream@7.0.2': + dependencies: + '@metamask/json-rpc-engine': 8.0.2 + '@metamask/safe-event-emitter': 3.1.2 + '@metamask/utils': 8.5.0 + readable-stream: 3.6.2 + transitivePeerDependencies: + - supports-color + + '@metamask/object-multiplex@2.1.0': + dependencies: + once: 1.4.0 + readable-stream: 3.6.2 + + '@metamask/onboarding@1.0.1': + dependencies: + bowser: 2.12.1 + + '@metamask/providers@16.1.0': + dependencies: + '@metamask/json-rpc-engine': 8.0.2 + '@metamask/json-rpc-middleware-stream': 7.0.2 + '@metamask/object-multiplex': 2.1.0 + '@metamask/rpc-errors': 6.4.0 + '@metamask/safe-event-emitter': 3.1.2 + '@metamask/utils': 8.5.0 + detect-browser: 5.3.0 + extension-port-stream: 3.0.0 + fast-deep-equal: 3.1.3 + is-stream: 2.0.1 + readable-stream: 3.6.2 + webextension-polyfill: 0.10.0 + transitivePeerDependencies: + - supports-color + + '@metamask/rpc-errors@6.4.0': + dependencies: + '@metamask/utils': 9.3.0 + fast-safe-stringify: 2.1.1 + transitivePeerDependencies: + - supports-color + + '@metamask/rpc-errors@7.0.2': + dependencies: + '@metamask/utils': 11.8.1 + fast-safe-stringify: 2.1.1 + transitivePeerDependencies: + - supports-color + + '@metamask/safe-event-emitter@2.0.0': {} + + '@metamask/safe-event-emitter@3.1.2': {} + + '@metamask/sdk-analytics@0.0.5': + dependencies: + openapi-fetch: 0.13.8 + + '@metamask/sdk-communication-layer@0.33.1(cross-fetch@4.1.0)(eciesjs@0.4.15)(eventemitter2@6.4.9)(readable-stream@3.6.2)(socket.io-client@4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@metamask/sdk-analytics': 0.0.5 + bufferutil: 4.0.9 + cross-fetch: 4.1.0 + date-fns: 2.30.0 + debug: 4.3.4 + eciesjs: 0.4.15 + eventemitter2: 6.4.9 + readable-stream: 3.6.2 + socket.io-client: 4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + utf-8-validate: 5.0.10 + uuid: 8.3.2 + transitivePeerDependencies: + - supports-color + + '@metamask/sdk-install-modal-web@0.32.1': + dependencies: + '@paulmillr/qr': 0.2.1 + + '@metamask/sdk@0.33.1(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + dependencies: + '@babel/runtime': 7.28.4 + '@metamask/onboarding': 1.0.1 + '@metamask/providers': 16.1.0 + '@metamask/sdk-analytics': 0.0.5 + '@metamask/sdk-communication-layer': 0.33.1(cross-fetch@4.1.0)(eciesjs@0.4.15)(eventemitter2@6.4.9)(readable-stream@3.6.2)(socket.io-client@4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@metamask/sdk-install-modal-web': 0.32.1 + '@paulmillr/qr': 0.2.1 + bowser: 2.12.1 + cross-fetch: 4.1.0 + debug: 4.3.4 + eciesjs: 0.4.15 + eth-rpc-errors: 4.0.3 + eventemitter2: 6.4.9 + obj-multiplex: 1.0.0 + pump: 3.0.3 + readable-stream: 3.6.2 + socket.io-client: 4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + tslib: 2.8.1 + util: 0.12.5 + uuid: 8.3.2 + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - utf-8-validate + + '@metamask/superstruct@3.2.1': {} + + '@metamask/utils@11.8.1': + dependencies: + '@ethereumjs/tx': 4.2.0 + '@metamask/superstruct': 3.2.1 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + '@types/debug': 4.1.12 + '@types/lodash': 4.17.20 + debug: 4.4.3 + lodash: 4.17.21 + pony-cause: 2.1.11 + semver: 7.7.2 + uuid: 9.0.1 + transitivePeerDependencies: + - supports-color + + '@metamask/utils@5.0.2': + dependencies: + '@ethereumjs/tx': 4.2.0 + '@types/debug': 4.1.12 + debug: 4.4.3 + semver: 7.7.2 + superstruct: 1.0.4 + transitivePeerDependencies: + - supports-color + + '@metamask/utils@8.5.0': + dependencies: + '@ethereumjs/tx': 4.2.0 + '@metamask/superstruct': 3.2.1 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + '@types/debug': 4.1.12 + debug: 4.3.4 + pony-cause: 2.1.11 + semver: 7.7.2 + uuid: 9.0.1 + transitivePeerDependencies: + - supports-color + + '@metamask/utils@9.3.0': + dependencies: + '@ethereumjs/tx': 4.2.0 + '@metamask/superstruct': 3.2.1 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + '@types/debug': 4.1.12 + debug: 4.3.4 + pony-cause: 2.1.11 + semver: 7.7.2 + uuid: 9.0.1 + transitivePeerDependencies: + - supports-color + + '@noble/ciphers@1.2.1': {} + + '@noble/ciphers@1.3.0': {} + + '@noble/curves@1.4.2': + dependencies: + '@noble/hashes': 1.4.0 + + '@noble/curves@1.8.0': + dependencies: + '@noble/hashes': 1.7.0 + + '@noble/curves@1.8.1': + dependencies: + '@noble/hashes': 1.7.1 + + '@noble/curves@1.9.1': + dependencies: + '@noble/hashes': 1.8.0 + + '@noble/curves@1.9.7': + dependencies: + '@noble/hashes': 1.8.0 + + '@noble/hashes@1.4.0': {} + + '@noble/hashes@1.7.0': {} + + '@noble/hashes@1.7.1': {} + + '@noble/hashes@1.8.0': {} + + '@paulmillr/qr@0.2.1': {} + + '@reown/appkit-common@1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4)': + dependencies: + big.js: 6.2.2 + dayjs: 1.11.13 + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4) + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - zod + + '@reown/appkit-common@1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + big.js: 6.2.2 + dayjs: 1.11.13 + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - zod + + '@reown/appkit-controllers@1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) + '@walletconnect/universal-provider': 2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + valtio: 1.13.2(react@18.3.1) + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - react + - typescript + - uploadthing + - utf-8-validate + - zod + + '@reown/appkit-pay@1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-ui': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(react@18.3.1))(zod@3.25.76) + lit: 3.3.0 + valtio: 1.13.2(react@18.3.1) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - react + - typescript + - uploadthing + - utf-8-validate + - zod + + '@reown/appkit-polyfills@1.7.8': + dependencies: + buffer: 6.0.3 + + '@reown/appkit-scaffold-ui@1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(react@18.3.1))(zod@3.25.76)': + dependencies: + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-ui': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(react@18.3.1))(zod@3.25.76) + '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) + lit: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - react + - typescript + - uploadthing + - utf-8-validate + - valtio + - zod + + '@reown/appkit-ui@1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) + lit: 3.3.0 + qrcode: 1.5.3 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - react + - typescript + - uploadthing + - utf-8-validate + - zod + + '@reown/appkit-utils@1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(react@18.3.1))(zod@3.25.76)': + dependencies: + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-polyfills': 1.7.8 + '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) + '@walletconnect/logger': 2.1.2 + '@walletconnect/universal-provider': 2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + valtio: 1.13.2(react@18.3.1) + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - react + - typescript + - uploadthing + - utf-8-validate + - zod + + '@reown/appkit-wallet@1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)': + dependencies: + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4) + '@reown/appkit-polyfills': 1.7.8 + '@walletconnect/logger': 2.1.2 + zod: 3.22.4 + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + + '@reown/appkit@1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-pay': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-polyfills': 1.7.8 + '@reown/appkit-scaffold-ui': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(react@18.3.1))(zod@3.25.76) + '@reown/appkit-ui': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(react@18.3.1))(zod@3.25.76) + '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) + '@walletconnect/types': 2.21.0 + '@walletconnect/universal-provider': 2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + bs58: 6.0.0 + valtio: 1.13.2(react@18.3.1) + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - react + - typescript + - uploadthing + - utf-8-validate + - zod + + '@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + events: 3.3.0 + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - zod + + '@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@safe-global/safe-gateway-typescript-sdk': 3.23.1 + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - zod + + '@safe-global/safe-gateway-typescript-sdk@3.23.1': {} + + '@scure/base@1.1.9': {} + + '@scure/base@1.2.6': {} + + '@scure/bip32@1.4.0': + dependencies: + '@noble/curves': 1.4.2 + '@noble/hashes': 1.4.0 + '@scure/base': 1.1.9 + + '@scure/bip32@1.6.2': + dependencies: + '@noble/curves': 1.8.1 + '@noble/hashes': 1.7.1 + '@scure/base': 1.2.6 + + '@scure/bip32@1.7.0': + dependencies: + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + + '@scure/bip39@1.3.0': + dependencies: + '@noble/hashes': 1.4.0 + '@scure/base': 1.1.9 + + '@scure/bip39@1.5.4': + dependencies: + '@noble/hashes': 1.7.1 + '@scure/base': 1.2.6 + + '@scure/bip39@1.6.0': + dependencies: + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + + '@socket.io/component-emitter@3.1.2': {} + + '@solana-program/compute-budget@0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': + dependencies: + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + + '@solana-program/token-2022@0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))': + dependencies: + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/sysvars': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + + '@solana-program/token@0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': + dependencies: + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + + '@solana/accounts@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec': 2.3.0(typescript@5.9.2) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/addresses@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/assertions': 2.3.0(typescript@5.9.2) + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/nominal-types': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/assertions@2.3.0(typescript@5.9.2)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + + '@solana/codecs-core@2.3.0(typescript@5.9.2)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + + '@solana/codecs-data-structures@2.3.0(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-numbers': 2.3.0(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + + '@solana/codecs-numbers@2.3.0(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + + '@solana/codecs-strings@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-numbers': 2.3.0(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + fastestsmallesttextencoderdecoder: 1.0.22 + typescript: 5.9.2 + + '@solana/codecs@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-data-structures': 2.3.0(typescript@5.9.2) + '@solana/codecs-numbers': 2.3.0(typescript@5.9.2) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/options': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/errors@2.3.0(typescript@5.9.2)': + dependencies: + chalk: 5.6.2 + commander: 14.0.1 + typescript: 5.9.2 + + '@solana/fast-stable-stringify@2.3.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + + '@solana/functional@2.3.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + + '@solana/instructions@2.3.0(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + + '@solana/keys@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/assertions': 2.3.0(typescript@5.9.2) + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/nominal-types': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/accounts': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/functional': 2.3.0(typescript@5.9.2) + '@solana/instructions': 2.3.0(typescript@5.9.2) + '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/programs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-parsed-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/signers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/sysvars': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + + '@solana/nominal-types@2.3.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + + '@solana/options@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-data-structures': 2.3.0(typescript@5.9.2) + '@solana/codecs-numbers': 2.3.0(typescript@5.9.2) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/programs@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/promises@2.3.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + + '@solana/rpc-api@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-parsed-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec': 2.3.0(typescript@5.9.2) + '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/rpc-parsed-types@2.3.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + + '@solana/rpc-spec-types@2.3.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + + '@solana/rpc-spec@2.3.0(typescript@5.9.2)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + + '@solana/rpc-subscriptions-api@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.2) + '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/rpc-subscriptions-channel-websocket@2.3.0(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/functional': 2.3.0(typescript@5.9.2) + '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.2) + '@solana/subscribable': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + + '@solana/rpc-subscriptions-spec@2.3.0(typescript@5.9.2)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/promises': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) + '@solana/subscribable': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + + '@solana/rpc-subscriptions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/fast-stable-stringify': 2.3.0(typescript@5.9.2) + '@solana/functional': 2.3.0(typescript@5.9.2) + '@solana/promises': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-subscriptions-api': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-subscriptions-channel-websocket': 2.3.0(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.2) + '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/subscribable': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + + '@solana/rpc-transformers@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/functional': 2.3.0(typescript@5.9.2) + '@solana/nominal-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/rpc-transport-http@2.3.0(typescript@5.9.2)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + undici-types: 7.16.0 + + '@solana/rpc-types@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-numbers': 2.3.0(typescript@5.9.2) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/nominal-types': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/rpc@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/fast-stable-stringify': 2.3.0(typescript@5.9.2) + '@solana/functional': 2.3.0(typescript@5.9.2) + '@solana/rpc-api': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-spec': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-transport-http': 2.3.0(typescript@5.9.2) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/signers@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/instructions': 2.3.0(typescript@5.9.2) + '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/nominal-types': 2.3.0(typescript@5.9.2) + '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/subscribable@2.3.0(typescript@5.9.2)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + + '@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/accounts': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/transaction-confirmation@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/promises': 2.3.0(typescript@5.9.2) + '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + + '@solana/transaction-messages@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-data-structures': 2.3.0(typescript@5.9.2) + '@solana/codecs-numbers': 2.3.0(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/functional': 2.3.0(typescript@5.9.2) + '@solana/instructions': 2.3.0(typescript@5.9.2) + '@solana/nominal-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/transactions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-data-structures': 2.3.0(typescript@5.9.2) + '@solana/codecs-numbers': 2.3.0(typescript@5.9.2) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/functional': 2.3.0(typescript@5.9.2) + '@solana/instructions': 2.3.0(typescript@5.9.2) + '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/nominal-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@tanstack/query-core@5.90.2': {} + + '@tanstack/react-query@5.90.2(react@18.3.1)': + dependencies: + '@tanstack/query-core': 5.90.2 + react: 18.3.1 + + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + + '@types/lodash@4.17.20': {} + + '@types/ms@2.1.0': {} + + '@types/node@24.5.2': + dependencies: + undici-types: 7.12.0 + + '@types/trusted-types@2.0.7': {} + + '@wagmi/connectors@5.11.1(@tanstack/react-query@5.90.2(react@18.3.1))(@wagmi/core@2.21.1(@tanstack/query-core@5.90.2)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.17.4(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76))(zod@3.25.76)': + dependencies: + '@base-org/account': 1.1.1(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@3.25.76) + '@coinbase/wallet-sdk': 4.3.6(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@3.25.76) + '@gemini-wallet/core': 0.2.0(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@metamask/sdk': 0.33.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@wagmi/core': 2.21.1(@tanstack/query-core@5.90.2)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@walletconnect/ethereum-provider': 2.21.1(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + cbw-sdk: '@coinbase/wallet-sdk@3.9.3' + porto: 0.2.19(@tanstack/react-query@5.90.2(react@18.3.1))(@wagmi/core@2.21.1(@tanstack/query-core@5.90.2)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.17.4(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)) + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@tanstack/react-query' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - immer + - ioredis + - react + - supports-color + - uploadthing + - use-sync-external-store + - utf-8-validate + - wagmi + - zod + + '@wagmi/core@2.21.1(@tanstack/query-core@5.90.2)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))': + dependencies: + eventemitter3: 5.0.1 + mipd: 0.0.7(typescript@5.9.2) + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + zustand: 5.0.0(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) + optionalDependencies: + '@tanstack/query-core': 5.90.2 + typescript: 5.9.2 + transitivePeerDependencies: + - '@types/react' + - immer + - react + - use-sync-external-store + + '@walletconnect/core@2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-provider': 1.0.14 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/logger': 2.1.2 + '@walletconnect/relay-api': 1.0.11 + '@walletconnect/relay-auth': 1.1.0 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.21.0 + '@walletconnect/utils': 2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/window-getters': 1.0.1 + es-toolkit: 1.33.0 + events: 3.3.0 + uint8arrays: 3.1.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + + '@walletconnect/core@2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-provider': 1.0.14 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/logger': 2.1.2 + '@walletconnect/relay-api': 1.0.11 + '@walletconnect/relay-auth': 1.1.0 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.21.1 + '@walletconnect/utils': 2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/window-getters': 1.0.1 + es-toolkit: 1.33.0 + events: 3.3.0 + uint8arrays: 3.1.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + + '@walletconnect/environment@1.0.1': + dependencies: + tslib: 1.14.1 + + '@walletconnect/ethereum-provider@2.21.1(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@reown/appkit': 1.7.8(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/jsonrpc-http-connection': 1.0.8 + '@walletconnect/jsonrpc-provider': 1.0.14 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/sign-client': 2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/types': 2.21.1 + '@walletconnect/universal-provider': 2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/utils': 2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - react + - typescript + - uploadthing + - utf-8-validate + - zod + + '@walletconnect/events@1.0.1': + dependencies: + keyvaluestorage-interface: 1.0.0 + tslib: 1.14.1 + + '@walletconnect/heartbeat@1.2.2': + dependencies: + '@walletconnect/events': 1.0.1 + '@walletconnect/time': 1.0.2 + events: 3.3.0 + + '@walletconnect/jsonrpc-http-connection@1.0.8': + dependencies: + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/safe-json': 1.0.2 + cross-fetch: 3.2.0 + events: 3.3.0 + transitivePeerDependencies: + - encoding + + '@walletconnect/jsonrpc-provider@1.0.14': + dependencies: + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/safe-json': 1.0.2 + events: 3.3.0 + + '@walletconnect/jsonrpc-types@1.0.4': + dependencies: + events: 3.3.0 + keyvaluestorage-interface: 1.0.0 + + '@walletconnect/jsonrpc-utils@1.0.8': + dependencies: + '@walletconnect/environment': 1.0.1 + '@walletconnect/jsonrpc-types': 1.0.4 + tslib: 1.14.1 + + '@walletconnect/jsonrpc-ws-connection@1.0.16(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + dependencies: + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/safe-json': 1.0.2 + events: 3.3.0 + ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@walletconnect/keyvaluestorage@1.1.1': + dependencies: + '@walletconnect/safe-json': 1.0.2 + idb-keyval: 6.2.2 + unstorage: 1.17.1(idb-keyval@6.2.2) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - db0 + - ioredis + - uploadthing + + '@walletconnect/logger@2.1.2': + dependencies: + '@walletconnect/safe-json': 1.0.2 + pino: 7.11.0 + + '@walletconnect/relay-api@1.0.11': + dependencies: + '@walletconnect/jsonrpc-types': 1.0.4 + + '@walletconnect/relay-auth@1.1.0': + dependencies: + '@noble/curves': 1.8.0 + '@noble/hashes': 1.7.0 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + uint8arrays: 3.1.0 + + '@walletconnect/safe-json@1.0.2': + dependencies: + tslib: 1.14.1 + + '@walletconnect/sign-client@2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@walletconnect/core': 2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/events': 1.0.1 + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/logger': 2.1.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.21.0 + '@walletconnect/utils': 2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + + '@walletconnect/sign-client@2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@walletconnect/core': 2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/events': 1.0.1 + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/logger': 2.1.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.21.1 + '@walletconnect/utils': 2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + + '@walletconnect/time@1.0.2': + dependencies: + tslib: 1.14.1 + + '@walletconnect/types@2.21.0': + dependencies: + '@walletconnect/events': 1.0.1 + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/logger': 2.1.2 + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - db0 + - ioredis + - uploadthing + + '@walletconnect/types@2.21.1': + dependencies: + '@walletconnect/events': 1.0.1 + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/logger': 2.1.2 + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - db0 + - ioredis + - uploadthing + + '@walletconnect/universal-provider@2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@walletconnect/events': 1.0.1 + '@walletconnect/jsonrpc-http-connection': 1.0.8 + '@walletconnect/jsonrpc-provider': 1.0.14 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/logger': 2.1.2 + '@walletconnect/sign-client': 2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/types': 2.21.0 + '@walletconnect/utils': 2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + es-toolkit: 1.33.0 + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + + '@walletconnect/universal-provider@2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@walletconnect/events': 1.0.1 + '@walletconnect/jsonrpc-http-connection': 1.0.8 + '@walletconnect/jsonrpc-provider': 1.0.14 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/logger': 2.1.2 + '@walletconnect/sign-client': 2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/types': 2.21.1 + '@walletconnect/utils': 2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + es-toolkit: 1.33.0 + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + + '@walletconnect/utils@2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@noble/ciphers': 1.2.1 + '@noble/curves': 1.8.1 + '@noble/hashes': 1.7.1 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/relay-api': 1.0.11 + '@walletconnect/relay-auth': 1.1.0 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.21.0 + '@walletconnect/window-getters': 1.0.1 + '@walletconnect/window-metadata': 1.0.1 + bs58: 6.0.0 + detect-browser: 5.3.0 + query-string: 7.1.3 + uint8arrays: 3.1.0 + viem: 2.23.2(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + + '@walletconnect/utils@2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@noble/ciphers': 1.2.1 + '@noble/curves': 1.8.1 + '@noble/hashes': 1.7.1 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/relay-api': 1.0.11 + '@walletconnect/relay-auth': 1.1.0 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.21.1 + '@walletconnect/window-getters': 1.0.1 + '@walletconnect/window-metadata': 1.0.1 + bs58: 6.0.0 + detect-browser: 5.3.0 + query-string: 7.1.3 + uint8arrays: 3.1.0 + viem: 2.23.2(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + + '@walletconnect/window-getters@1.0.1': + dependencies: + tslib: 1.14.1 + + '@walletconnect/window-metadata@1.0.1': + dependencies: + '@walletconnect/window-getters': 1.0.1 + tslib: 1.14.1 + + abitype@1.0.8(typescript@5.9.2)(zod@3.25.76): + optionalDependencies: + typescript: 5.9.2 + zod: 3.25.76 + + abitype@1.1.0(typescript@5.9.2)(zod@3.22.4): + optionalDependencies: + typescript: 5.9.2 + zod: 3.22.4 + + abitype@1.1.0(typescript@5.9.2)(zod@3.25.76): + optionalDependencies: + typescript: 5.9.2 + zod: 3.25.76 + + abitype@1.1.1(typescript@5.9.2)(zod@3.25.76): + optionalDependencies: + typescript: 5.9.2 + zod: 3.25.76 + + abitype@1.1.1(typescript@5.9.2)(zod@4.1.11): + optionalDependencies: + typescript: 5.9.2 + zod: 4.1.11 + + accepts@2.0.0: + dependencies: + mime-types: 3.0.1 + negotiator: 1.0.0 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + async-mutex@0.2.6: + dependencies: + tslib: 2.8.1 + + atomic-sleep@1.0.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + base-x@5.0.1: {} + + base64-js@1.5.1: {} + + big.js@6.2.2: {} + + bn.js@5.2.2: {} + + body-parser@2.2.0: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + on-finished: 2.4.1 + qs: 6.14.0 + raw-body: 3.0.1 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + + bowser@2.12.1: {} + + bs58@6.0.0: + dependencies: + base-x: 5.0.1 + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bufferutil@4.0.9: + dependencies: + node-gyp-build: 4.8.4 + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + camelcase@5.3.1: {} + + chalk@5.6.2: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + + clsx@1.2.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@14.0.1: {} + + content-disposition@1.0.0: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + cookie-es@1.2.2: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + core-util-is@1.0.3: {} + + crc-32@1.2.2: {} + + cross-fetch@3.2.0: + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + cross-fetch@4.1.0: + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + crossws@0.3.5: + dependencies: + uncrypto: 0.1.3 + + date-fns@2.30.0: + dependencies: + '@babel/runtime': 7.28.4 + + dayjs@1.11.13: {} + + debug@4.3.4: + dependencies: + ms: 2.1.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decamelize@1.2.0: {} + + decode-uri-component@0.2.2: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + defu@6.1.4: {} + + depd@2.0.0: {} + + derive-valtio@0.1.0(valtio@1.13.2(react@18.3.1)): + dependencies: + valtio: 1.13.2(react@18.3.1) + + destr@2.0.5: {} + + detect-browser@5.3.0: {} + + dijkstrajs@1.0.3: {} + + dotenv@17.2.2: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + duplexify@4.1.3: + dependencies: + end-of-stream: 1.4.5 + inherits: 2.0.4 + readable-stream: 3.6.2 + stream-shift: 1.0.3 + + eciesjs@0.4.15: + dependencies: + '@ecies/ciphers': 0.2.4(@noble/ciphers@1.3.0) + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + + ee-first@1.1.1: {} + + emoji-regex@8.0.0: {} + + encode-utf8@1.0.3: {} + + encodeurl@2.0.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + engine.io-client@6.6.3(bufferutil@4.0.9)(utf-8-validate@5.0.10): + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.4 + engine.io-parser: 5.2.3 + ws: 8.17.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + xmlhttprequest-ssl: 2.1.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + engine.io-parser@5.2.3: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-toolkit@1.33.0: {} + + esbuild@0.25.10: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.10 + '@esbuild/android-arm': 0.25.10 + '@esbuild/android-arm64': 0.25.10 + '@esbuild/android-x64': 0.25.10 + '@esbuild/darwin-arm64': 0.25.10 + '@esbuild/darwin-x64': 0.25.10 + '@esbuild/freebsd-arm64': 0.25.10 + '@esbuild/freebsd-x64': 0.25.10 + '@esbuild/linux-arm': 0.25.10 + '@esbuild/linux-arm64': 0.25.10 + '@esbuild/linux-ia32': 0.25.10 + '@esbuild/linux-loong64': 0.25.10 + '@esbuild/linux-mips64el': 0.25.10 + '@esbuild/linux-ppc64': 0.25.10 + '@esbuild/linux-riscv64': 0.25.10 + '@esbuild/linux-s390x': 0.25.10 + '@esbuild/linux-x64': 0.25.10 + '@esbuild/netbsd-arm64': 0.25.10 + '@esbuild/netbsd-x64': 0.25.10 + '@esbuild/openbsd-arm64': 0.25.10 + '@esbuild/openbsd-x64': 0.25.10 + '@esbuild/openharmony-arm64': 0.25.10 + '@esbuild/sunos-x64': 0.25.10 + '@esbuild/win32-arm64': 0.25.10 + '@esbuild/win32-ia32': 0.25.10 + '@esbuild/win32-x64': 0.25.10 + + escape-html@1.0.3: {} + + etag@1.8.1: {} + + eth-block-tracker@7.1.0: + dependencies: + '@metamask/eth-json-rpc-provider': 1.0.1 + '@metamask/safe-event-emitter': 3.1.2 + '@metamask/utils': 5.0.2 + json-rpc-random-id: 1.0.1 + pify: 3.0.0 + transitivePeerDependencies: + - supports-color + + eth-json-rpc-filters@6.0.1: + dependencies: + '@metamask/safe-event-emitter': 3.1.2 + async-mutex: 0.2.6 + eth-query: 2.1.2 + json-rpc-engine: 6.1.0 + pify: 5.0.0 + + eth-query@2.1.2: + dependencies: + json-rpc-random-id: 1.0.1 + xtend: 4.0.2 + + eth-rpc-errors@4.0.3: + dependencies: + fast-safe-stringify: 2.1.1 + + ethereum-cryptography@2.2.1: + dependencies: + '@noble/curves': 1.4.2 + '@noble/hashes': 1.4.0 + '@scure/bip32': 1.4.0 + '@scure/bip39': 1.3.0 + + eventemitter2@6.4.9: {} + + eventemitter3@5.0.1: {} + + events@3.3.0: {} + + express@5.1.0: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.0 + content-disposition: 1.0.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.0 + fresh: 2.0.0 + http-errors: 2.0.0 + merge-descriptors: 2.0.0 + mime-types: 3.0.1 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.0 + serve-static: 2.2.0 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + extension-port-stream@3.0.0: + dependencies: + readable-stream: 3.6.2 + webextension-polyfill: 0.10.0 + + fast-deep-equal@3.1.3: {} + + fast-redact@3.5.0: {} + + fast-safe-stringify@2.1.1: {} + + fastestsmallesttextencoderdecoder@1.0.22: {} + + filter-obj@1.1.0: {} + + finalhandler@2.1.0: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-tsconfig@4.10.1: + dependencies: + resolve-pkg-maps: 1.0.0 + + gopd@1.2.0: {} + + h3@1.15.4: + dependencies: + cookie-es: 1.2.2 + crossws: 0.3.5 + defu: 6.1.4 + destr: 2.0.5 + iron-webcrypto: 1.2.1 + node-mock-http: 1.0.3 + radix3: 1.1.2 + ufo: 1.6.1 + uncrypto: 0.1.3 + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hono@4.9.8: {} + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.7.0: + dependencies: + safer-buffer: 2.1.2 + + idb-keyval@6.2.1: {} + + idb-keyval@6.2.2: {} + + ieee754@1.2.1: {} + + inherits@2.0.4: {} + + ipaddr.js@1.9.1: {} + + iron-webcrypto@1.2.1: {} + + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + + is-fullwidth-code-point@3.0.0: {} + + is-generator-function@1.1.0: + dependencies: + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-promise@4.0.0: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-stream@2.0.1: {} + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.19 + + isarray@1.0.0: {} + + isarray@2.0.5: {} + + isows@1.0.6(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + dependencies: + ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + + isows@1.0.7(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + dependencies: + ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + + js-tokens@4.0.0: {} + + json-rpc-engine@6.1.0: + dependencies: + '@metamask/safe-event-emitter': 2.0.0 + eth-rpc-errors: 4.0.3 + + json-rpc-random-id@1.0.1: {} + + keccak@3.0.4: + dependencies: + node-addon-api: 2.0.2 + node-gyp-build: 4.8.4 + readable-stream: 3.6.2 + + keyvaluestorage-interface@1.0.0: {} + + lit-element@4.2.1: + dependencies: + '@lit-labs/ssr-dom-shim': 1.4.0 + '@lit/reactive-element': 2.1.1 + lit-html: 3.3.1 + + lit-html@3.3.1: + dependencies: + '@types/trusted-types': 2.0.7 + + lit@3.3.0: + dependencies: + '@lit/reactive-element': 2.1.1 + lit-element: 4.2.1 + lit-html: 3.3.1 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + lodash@4.17.21: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@10.4.3: {} + + math-intrinsics@1.1.0: {} + + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + + micro-ftch@0.3.1: {} + + mime-db@1.54.0: {} + + mime-types@3.0.1: + dependencies: + mime-db: 1.54.0 + + mipd@0.0.7(typescript@5.9.2): + optionalDependencies: + typescript: 5.9.2 + + ms@2.1.2: {} + + ms@2.1.3: {} + + multiformats@9.9.0: {} + + negotiator@1.0.0: {} + + node-addon-api@2.0.2: {} + + node-fetch-native@1.6.7: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-gyp-build@4.8.4: {} + + node-mock-http@1.0.3: {} + + normalize-path@3.0.0: {} + + obj-multiplex@1.0.0: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + readable-stream: 2.3.8 + + object-inspect@1.13.4: {} + + ofetch@1.4.1: + dependencies: + destr: 2.0.5 + node-fetch-native: 1.6.7 + ufo: 1.6.1 + + on-exit-leak-free@0.2.0: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + openapi-fetch@0.13.8: + dependencies: + openapi-typescript-helpers: 0.0.15 + + openapi-typescript-helpers@0.0.15: {} + + ox@0.6.7(typescript@5.9.2)(zod@3.25.76): + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@noble/curves': 1.8.1 + '@noble/hashes': 1.7.1 + '@scure/bip32': 1.6.2 + '@scure/bip39': 1.5.4 + abitype: 1.0.8(typescript@5.9.2)(zod@3.25.76) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - zod + + ox@0.6.9(typescript@5.9.2)(zod@3.25.76): + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.1.1(typescript@5.9.2)(zod@3.25.76) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - zod + + ox@0.9.6(typescript@5.9.2)(zod@3.22.4): + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.1.0(typescript@5.9.2)(zod@3.22.4) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - zod + + ox@0.9.6(typescript@5.9.2)(zod@3.25.76): + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.1.0(typescript@5.9.2)(zod@3.25.76) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - zod + + ox@0.9.7(typescript@5.9.2)(zod@4.1.11): + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.1.1(typescript@5.9.2)(zod@4.1.11) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - zod + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-try@2.2.0: {} + + parseurl@1.3.3: {} + + path-exists@4.0.0: {} + + path-to-regexp@8.3.0: {} + + picomatch@2.3.1: {} + + pify@3.0.0: {} + + pify@5.0.0: {} + + pino-abstract-transport@0.5.0: + dependencies: + duplexify: 4.1.3 + split2: 4.2.0 + + pino-std-serializers@4.0.0: {} + + pino@7.11.0: + dependencies: + atomic-sleep: 1.0.0 + fast-redact: 3.5.0 + on-exit-leak-free: 0.2.0 + pino-abstract-transport: 0.5.0 + pino-std-serializers: 4.0.0 + process-warning: 1.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.1.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 2.8.0 + thread-stream: 0.15.2 + + pngjs@5.0.0: {} + + pony-cause@2.1.11: {} + + porto@0.2.19(@tanstack/react-query@5.90.2(react@18.3.1))(@wagmi/core@2.21.1(@tanstack/query-core@5.90.2)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.17.4(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)): + dependencies: + '@wagmi/core': 2.21.1(@tanstack/query-core@5.90.2)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + hono: 4.9.8 + idb-keyval: 6.2.2 + mipd: 0.0.7(typescript@5.9.2) + ox: 0.9.7(typescript@5.9.2)(zod@4.1.11) + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + zod: 4.1.11 + zustand: 5.0.8(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) + optionalDependencies: + '@tanstack/react-query': 5.90.2(react@18.3.1) + react: 18.3.1 + typescript: 5.9.2 + wagmi: 2.17.4(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) + transitivePeerDependencies: + - '@types/react' + - immer + - use-sync-external-store + + possible-typed-array-names@1.1.0: {} + + preact@10.24.2: {} + + preact@10.27.2: {} + + process-nextick-args@2.0.1: {} + + process-warning@1.0.0: {} + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + proxy-compare@2.6.0: {} + + pump@3.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + qrcode@1.5.3: + dependencies: + dijkstrajs: 1.0.3 + encode-utf8: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 + + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + + query-string@7.1.3: + dependencies: + decode-uri-component: 0.2.2 + filter-obj: 1.1.0 + split-on-first: 1.1.0 + strict-uri-encode: 2.0.0 + + quick-format-unescaped@4.0.4: {} + + radix3@1.1.2: {} + + range-parser@1.2.1: {} + + raw-body@3.0.1: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.7.0 + unpipe: 1.0.0 + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@4.1.2: {} + + real-require@0.1.0: {} + + require-directory@2.1.1: {} + + require-main-filename@2.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safe-stable-stringify@2.5.0: {} + + safer-buffer@2.1.2: {} + + semver@7.7.2: {} + + send@1.2.0: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.0 + mime-types: 3.0.1 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.0: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.0 + transitivePeerDependencies: + - supports-color + + set-blocking@2.0.0: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + setprototypeof@1.2.0: {} + + sha.js@2.4.12: + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + to-buffer: 1.2.2 + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + socket.io-client@4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10): + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.4 + engine.io-client: 6.6.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-parser@4.2.4: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + + sonic-boom@2.8.0: + dependencies: + atomic-sleep: 1.0.0 + + split-on-first@1.1.0: {} + + split2@4.2.0: {} + + statuses@2.0.1: {} + + statuses@2.0.2: {} + + stream-shift@1.0.3: {} + + strict-uri-encode@2.0.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + superstruct@1.0.4: {} + + thread-stream@0.15.2: + dependencies: + real-require: 0.1.0 + + to-buffer@1.2.2: + dependencies: + isarray: 2.0.5 + safe-buffer: 5.2.1 + typed-array-buffer: 1.0.3 + + toidentifier@1.0.1: {} + + tr46@0.0.3: {} + + tslib@1.14.1: {} + + tslib@2.8.1: {} + + tsx@4.20.5: + dependencies: + esbuild: 0.25.10 + get-tsconfig: 4.10.1 + optionalDependencies: + fsevents: 2.3.3 + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.1 + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typescript@5.9.2: {} + + ufo@1.6.1: {} + + uint8arrays@3.1.0: + dependencies: + multiformats: 9.9.0 + + uncrypto@0.1.3: {} + + undici-types@7.12.0: {} + + undici-types@7.16.0: {} + + unpipe@1.0.0: {} + + unstorage@1.17.1(idb-keyval@6.2.2): + dependencies: + anymatch: 3.1.3 + chokidar: 4.0.3 + destr: 2.0.5 + h3: 1.15.4 + lru-cache: 10.4.3 + node-fetch-native: 1.6.7 + ofetch: 1.4.1 + ufo: 1.6.1 + optionalDependencies: + idb-keyval: 6.2.2 + + use-sync-external-store@1.2.0(react@18.3.1): + dependencies: + react: 18.3.1 + + use-sync-external-store@1.4.0(react@18.3.1): + dependencies: + react: 18.3.1 + + utf-8-validate@5.0.10: + dependencies: + node-gyp-build: 4.8.4 + + util-deprecate@1.0.2: {} + + util@0.12.5: + dependencies: + inherits: 2.0.4 + is-arguments: 1.2.0 + is-generator-function: 1.1.0 + is-typed-array: 1.1.15 + which-typed-array: 1.1.19 + + uuid@8.3.2: {} + + uuid@9.0.1: {} + + valtio@1.13.2(react@18.3.1): + dependencies: + derive-valtio: 0.1.0(valtio@1.13.2(react@18.3.1)) + proxy-compare: 2.6.0 + use-sync-external-store: 1.2.0(react@18.3.1) + optionalDependencies: + react: 18.3.1 + + vary@1.1.2: {} + + viem@2.23.2(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76): + dependencies: + '@noble/curves': 1.8.1 + '@noble/hashes': 1.7.1 + '@scure/bip32': 1.6.2 + '@scure/bip39': 1.5.4 + abitype: 1.0.8(typescript@5.9.2)(zod@3.25.76) + isows: 1.0.6(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + ox: 0.6.7(typescript@5.9.2)(zod@3.25.76) + ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + + viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4): + dependencies: + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.1.0(typescript@5.9.2)(zod@3.22.4) + isows: 1.0.7(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + ox: 0.9.6(typescript@5.9.2)(zod@3.22.4) + ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + + viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76): + dependencies: + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.1.0(typescript@5.9.2)(zod@3.25.76) + isows: 1.0.7(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + ox: 0.9.6(typescript@5.9.2)(zod@3.25.76) + ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + + wagmi@2.17.4(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76): + dependencies: + '@tanstack/react-query': 5.90.2(react@18.3.1) + '@wagmi/connectors': 5.11.1(@tanstack/react-query@5.90.2(react@18.3.1))(@wagmi/core@2.21.1(@tanstack/query-core@5.90.2)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.17.4(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76))(zod@3.25.76) + '@wagmi/core': 2.21.1(@tanstack/query-core@5.90.2)(react@18.3.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + react: 18.3.1 + use-sync-external-store: 1.4.0(react@18.3.1) + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@tanstack/query-core' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - immer + - ioredis + - supports-color + - uploadthing + - utf-8-validate + - zod + + webextension-polyfill@0.10.0: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which-module@2.0.1: {} + + which-typed-array@1.1.19: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10): + optionalDependencies: + bufferutil: 4.0.9 + utf-8-validate: 5.0.10 + + ws@8.17.1(bufferutil@4.0.9)(utf-8-validate@5.0.10): + optionalDependencies: + bufferutil: 4.0.9 + utf-8-validate: 5.0.10 + + ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10): + optionalDependencies: + bufferutil: 4.0.9 + utf-8-validate: 5.0.10 + + ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10): + optionalDependencies: + bufferutil: 4.0.9 + utf-8-validate: 5.0.10 + + x402@0.6.1(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + dependencies: + '@scure/base': 1.2.6 + '@solana-program/compute-budget': 0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + '@solana-program/token': 0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + '@solana-program/token-2022': 0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)) + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + wagmi: 2.17.4(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) + zod: 3.25.76 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@solana/sysvars' + - '@tanstack/query-core' + - '@tanstack/react-query' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - fastestsmallesttextencoderdecoder + - immer + - ioredis + - react + - supports-color + - typescript + - uploadthing + - utf-8-validate + - ws + + xmlhttprequest-ssl@2.1.2: {} + + xtend@4.0.2: {} + + y18n@4.0.3: {} + + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + + zod@3.22.4: {} + + zod@3.25.76: {} + + zod@4.1.11: {} + + zustand@5.0.0(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)): + optionalDependencies: + react: 18.3.1 + use-sync-external-store: 1.4.0(react@18.3.1) + + zustand@5.0.3(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)): + optionalDependencies: + react: 18.3.1 + use-sync-external-store: 1.4.0(react@18.3.1) + + zustand@5.0.8(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)): + optionalDependencies: + react: 18.3.1 + use-sync-external-store: 1.4.0(react@18.3.1) diff --git a/docs/x402/demo/facilitator/src/facilitator.ts b/docs/x402/demo/facilitator/src/facilitator.ts new file mode 100644 index 00000000..a6669fd0 --- /dev/null +++ b/docs/x402/demo/facilitator/src/facilitator.ts @@ -0,0 +1,148 @@ +import { config } from "dotenv"; +import express, { Request, Response } from "express"; +import { + PaymentRequirementsSchema, + type PaymentRequirements, + type PaymentPayload, + PaymentPayloadSchema, + SupportedSVMNetworks, + ExactSvmPayload, + SettleResponse, + Network, +} from "x402/types"; +import { KoraClient } from "@kora/sdk"; +import path from "path"; + +config({ path: path.join(process.cwd(), '..', '.env') }); + +const KORA_RPC_URL = process.env.KORA_RPC_URL || "http://localhost:8080/"; +const FACILITATOR_PORT = process.env.FACILITATOR_PORT || 3000; +const NETWORK = (process.env.NETWORK || "solana-devnet") as Network; +const KORA_API_KEY = process.env.KORA_API_KEY || "kora_facilitator_api_key_example"; + +const app = express(); + +app.use(express.json()); + +type VerifyRequest = { + paymentPayload: PaymentPayload; + paymentRequirements: PaymentRequirements; +}; + +type SettleRequest = { + paymentPayload: PaymentPayload; + paymentRequirements: PaymentRequirements; +}; + +app.get("/verify", (req: Request, res: Response) => { + res.json({ + endpoint: "/verify", + description: "POST to verify x402 payments", + body: { + paymentPayload: "PaymentPayload", + paymentRequirements: "PaymentRequirements", + }, + }); +}); + +app.post("/verify", async (req: Request, res: Response) => { + console.log("=== /verify endpoint called ==="); + + const kora = new KoraClient({ rpcUrl: KORA_RPC_URL, apiKey: KORA_API_KEY }); + + try { + const body: VerifyRequest = req.body; + const paymentRequirements = PaymentRequirementsSchema.parse(body.paymentRequirements); + + const paymentPayload = PaymentPayloadSchema.parse(body.paymentPayload); + + if (!SupportedSVMNetworks.includes(paymentRequirements.network)) { + throw new Error("Invalid network"); + } + const { transaction } = paymentPayload.payload as ExactSvmPayload; + + const { signed_transaction } = await kora.signTransaction({ + transaction + }); + + const valid = !!signed_transaction; + + const verifyResponse = { + isValid: valid, + }; + + res.json(verifyResponse); + } catch (error) { + res.status(400).json({ error: "Invalid request" }); + } +}); + +app.get("/settle", (req: Request, res: Response) => { + res.json({ + endpoint: "/settle", + description: "POST to settle x402 payments", + body: { + paymentPayload: "PaymentPayload", + paymentRequirements: "PaymentRequirements", + }, + }); +}); + +app.get("/supported", async (req: Request, res: Response) => { + console.log("=== /supported endpoint called ==="); + try { + const kora = new KoraClient({ rpcUrl: KORA_RPC_URL, apiKey: KORA_API_KEY }); + + const { signer_address } = await kora.getPayerSigner(); + + const kinds = [{ + x402Version: 1, + scheme: "exact", + network: NETWORK, + extra: { + feePayer: signer_address, + }, + }]; + + res.json({ + kinds, + }); + } catch (error) { + res.status(500).json({ + error: `Failed to get supported payment kinds: ${error instanceof Error ? error.message : String(error)}` + }); + } +}); + +app.post("/settle", async (req: Request, res: Response) => { + console.log("=== /settle endpoint called ==="); + try { + const body: SettleRequest = req.body; + const paymentRequirements = PaymentRequirementsSchema.parse(body.paymentRequirements); + const paymentPayload = PaymentPayloadSchema.parse(body.paymentPayload); + + if (!SupportedSVMNetworks.includes(paymentRequirements.network)) { + throw new Error("Invalid network"); + } + const { transaction } = paymentPayload.payload as ExactSvmPayload; + + const kora = new KoraClient({ rpcUrl: KORA_RPC_URL, apiKey: KORA_API_KEY }); + const { signature } = await kora.signAndSendTransaction({ + transaction + }); + + const response: SettleResponse = { + transaction: signature, + success: true, + network: NETWORK, + } + + res.json(response); + } catch (error) { + res.status(400).json({ error: `Invalid request: ${error}` }); + } +}); + +app.listen(FACILITATOR_PORT, () => { + console.log(`Server listening at http://localhost:${FACILITATOR_PORT}`); +}); \ No newline at end of file diff --git a/docs/x402/demo/kora/kora.toml b/docs/x402/demo/kora/kora.toml new file mode 100644 index 00000000..eba5115c --- /dev/null +++ b/docs/x402/demo/kora/kora.toml @@ -0,0 +1,86 @@ +[kora] +rate_limit = 100 +# Optional: Payment address to receive payment tokens (defaults to signer address) +# payment_address = "YourPaymentAddressPubkey11111111111111111111" + +[kora.auth] +api_key = "kora_facilitator_api_key_example" + +[kora.cache] +enabled = false +default_ttl = 300 +account_ttl = 60 + +[kora.enabled_methods] +liveness = false +estimate_transaction_fee = false +get_supported_tokens = false +sign_transaction = true +sign_and_send_transaction = true +transfer_transaction = false +get_blockhash = true +get_config = true +sign_transaction_if_paid = false +get_payer_signer = true + +[validation] +max_allowed_lamports = 1000000 +max_signatures = 10 +price_source = "Mock" +allowed_programs = [ + "11111111111111111111111111111111", # System Program + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", # Token Program + # "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb", # Token-2022 Program + "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", # Associated Token Program + "ComputeBudget111111111111111111111111111111", # Compute Budget Program +] +allowed_tokens = [ + "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU", # USDC devnet +] +allowed_spl_paid_tokens = [ + "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU", # USDC devnet +] + +disallowed_accounts = [ +] + +[validation.fee_payer_policy] +allow_sol_transfers = false +allow_spl_transfers = false +allow_token2022_transfers = false +allow_assign = false +allow_burn = false +allow_close_account = false +allow_approve = false + +[validation.price] +type = "free" # free / margin / fixed + +[validation.token2022] +blocked_mint_extensions = [ + "confidential_transfer_mint", # Confidential transfer configuration for the mint + "confidential_mint_burn", # Confidential mint and burn configuration + "transfer_fee_config", # Transfer fee configuration + "mint_close_authority", # Authority allowed to close the mint + "interest_bearing_config", # Interest-bearing token configuration + "non_transferable", # Makes tokens non-transferable + "permanent_delegate", # Permanent delegate for the mint + "transfer_hook", # Block tokens with transfer hooks + "pausable", # Block pausable tokens +] +blocked_account_extensions = [ + "confidential_transfer_account", # Confidential transfer state for the account + "non_transferable_account", # Non-transferable token account + "transfer_hook_account", # Transfer hook state for the account + "pausable_account", # Pausable token account state + "memo_transfer", # Requires memo for transfers + "cpi_guard", # Prevents certain CPI calls + "immutable_owner", # Account owner cannot be changed + "default_account_state", # Default state for new accounts +] + +[kora.usage_limit] +enabled = false +cache_url = "redis://redis:6379" +max_transactions = 2 +fallback_if_unavailable = false \ No newline at end of file diff --git a/docs/x402/demo/kora/signers.toml b/docs/x402/demo/kora/signers.toml new file mode 100644 index 00000000..2bb54de6 --- /dev/null +++ b/docs/x402/demo/kora/signers.toml @@ -0,0 +1,7 @@ +[signer_pool] +strategy = "round_robin" + +[[signers]] +name = "main_signer" +type = "memory" +private_key_env = "KORA_SIGNER_PRIVATE_KEY" diff --git a/docs/x402/demo/package.json b/docs/x402/demo/package.json new file mode 100644 index 00000000..a49c4d00 --- /dev/null +++ b/docs/x402/demo/package.json @@ -0,0 +1,13 @@ +{ + "name": "kora-x402-demo", + "private": true, + "scripts": { + "setup": "cd client && pnpm run setup", + "build:kora-sdk": "cd ../../../sdks/ts && pnpm build", + "install:all": "cd api && pnpm install && cd ../client && pnpm install && cd ../facilitator && pnpm install && cd ../../../../sdks/ts && pnpm install", + "start:kora": "../../../target/release/kora --config kora/kora.toml --rpc-url https://api.devnet.solana.com rpc start --signers-config kora/signers.toml", + "start:facilitator": "cd facilitator && pnpm start", + "start:api": "cd api && pnpm start", + "demo": "cd client && pnpm start" + } +} \ No newline at end of file From 0a1bfcc75255595178047808295fa1e121120d10 Mon Sep 17 00:00:00 2001 From: amilz <85324096+amilz@users.noreply.github.com> Date: Fri, 26 Sep 2025 13:23:33 -0700 Subject: [PATCH 08/29] chore: guide cleanup --- docs/x402/demo/facilitator/notes.md | 160 ---------------------------- 1 file changed, 160 deletions(-) delete mode 100644 docs/x402/demo/facilitator/notes.md diff --git a/docs/x402/demo/facilitator/notes.md b/docs/x402/demo/facilitator/notes.md deleted file mode 100644 index 09bfa1da..00000000 --- a/docs/x402/demo/facilitator/notes.md +++ /dev/null @@ -1,160 +0,0 @@ -wraps kora - -that does this: -https://github.com/coinbase/x402/blob/19db25989697cfb3d1d72ec57ab96fcc79d53df1/typescript/packages/x402/src/schemes/exact/svm/facilitator/verify.ts - -https://github.com/coinbase/x402/blob/19db25989697cfb3d1d72ec57ab96fcc79d53df1/examples/typescript/facilitator/index.ts - -```ts -/* eslint-env node */ -import { config } from "dotenv"; -import express, { Request, Response } from "express"; -import { verify, settle } from "x402/facilitator"; -import { - PaymentRequirementsSchema, - type PaymentRequirements, - type PaymentPayload, - PaymentPayloadSchema, - createConnectedClient, - createSigner, - SupportedEVMNetworks, - SupportedSVMNetworks, - Signer, - ConnectedClient, - SupportedPaymentKind, - isSvmSignerWallet, -} from "x402/types"; - -config(); - -const EVM_PRIVATE_KEY = process.env.EVM_PRIVATE_KEY || ""; -const SVM_PRIVATE_KEY = process.env.SVM_PRIVATE_KEY || ""; - -if (!EVM_PRIVATE_KEY && !SVM_PRIVATE_KEY) { - console.error("Missing required environment variables"); - process.exit(1); -} - -const app = express(); - -// Configure express to parse JSON bodies -app.use(express.json()); - -type VerifyRequest = { - paymentPayload: PaymentPayload; - paymentRequirements: PaymentRequirements; -}; - -type SettleRequest = { - paymentPayload: PaymentPayload; - paymentRequirements: PaymentRequirements; -}; - -app.get("/verify", (req: Request, res: Response) => { - res.json({ - endpoint: "/verify", - description: "POST to verify x402 payments", - body: { - paymentPayload: "PaymentPayload", - paymentRequirements: "PaymentRequirements", - }, - }); -}); - -app.post("/verify", async (req: Request, res: Response) => { - try { - const body: VerifyRequest = req.body; - const paymentRequirements = PaymentRequirementsSchema.parse(body.paymentRequirements); - const paymentPayload = PaymentPayloadSchema.parse(body.paymentPayload); - - // use the correct client/signer based on the requested network - // svm verify requires a Signer because it signs & simulates the txn - let client: Signer | ConnectedClient; - if (SupportedEVMNetworks.includes(paymentRequirements.network)) { - client = createConnectedClient(paymentRequirements.network); - } else if (SupportedSVMNetworks.includes(paymentRequirements.network)) { - client = await createSigner(paymentRequirements.network, SVM_PRIVATE_KEY); - } else { - throw new Error("Invalid network"); - } - - // verify - const valid = await verify(client, paymentPayload, paymentRequirements); - res.json(valid); - } catch (error) { - console.error("error", error); - res.status(400).json({ error: "Invalid request" }); - } -}); - -app.get("/settle", (req: Request, res: Response) => { - res.json({ - endpoint: "/settle", - description: "POST to settle x402 payments", - body: { - paymentPayload: "PaymentPayload", - paymentRequirements: "PaymentRequirements", - }, - }); -}); - -app.get("/supported", async (req: Request, res: Response) => { - let kinds: SupportedPaymentKind[] = []; - - // evm - if (EVM_PRIVATE_KEY) { - kinds.push({ - x402Version: 1, - scheme: "exact", - network: "base-sepolia", - }); - } - - // svm - if (SVM_PRIVATE_KEY) { - const signer = await createSigner("solana-devnet", SVM_PRIVATE_KEY); - const feePayer = isSvmSignerWallet(signer) ? signer.address : undefined; - - kinds.push({ - x402Version: 1, - scheme: "exact", - network: "solana-devnet", - extra: { - feePayer, - }, - }); - } - res.json({ - kinds, - }); -}); - -app.post("/settle", async (req: Request, res: Response) => { - try { - const body: SettleRequest = req.body; - const paymentRequirements = PaymentRequirementsSchema.parse(body.paymentRequirements); - const paymentPayload = PaymentPayloadSchema.parse(body.paymentPayload); - - // use the correct private key based on the requested network - let signer: Signer; - if (SupportedEVMNetworks.includes(paymentRequirements.network)) { - signer = await createSigner(paymentRequirements.network, EVM_PRIVATE_KEY); - } else if (SupportedSVMNetworks.includes(paymentRequirements.network)) { - signer = await createSigner(paymentRequirements.network, SVM_PRIVATE_KEY); - } else { - throw new Error("Invalid network"); - } - - // settle - const response = await settle(signer, paymentPayload, paymentRequirements); - res.json(response); - } catch (error) { - console.error("error", error); - res.status(400).json({ error: `Invalid request: ${error}` }); - } -}); - -app.listen(process.env.PORT || 3000, () => { - console.log(`Server listening at http://localhost:${process.env.PORT || 3000}`); -}); -``` \ No newline at end of file From ec050445447bbbb27a7cddcfe0b9ce9d794ee736 Mon Sep 17 00:00:00 2001 From: jo <17280917+dev-jodee@users.noreply.github.com> Date: Mon, 29 Sep 2025 07:27:46 -0400 Subject: [PATCH 09/29] feat: (PRO-343) Improve estimate fee efficiency for different Price Type (Free -> instant, Fixed -> just calculate price), Margin -> the full calculation (#226) --- crates/lib/src/fee/fee.rs | 90 ++++++++--- crates/lib/src/fee/price.rs | 148 ++++++------------ .../rpc_server/method/transfer_transaction.rs | 2 +- 3 files changed, 118 insertions(+), 122 deletions(-) diff --git a/crates/lib/src/fee/fee.rs b/crates/lib/src/fee/fee.rs index 3a7d86b5..710a8f60 100644 --- a/crates/lib/src/fee/fee.rs +++ b/crates/lib/src/fee/fee.rs @@ -36,6 +36,37 @@ pub struct TotalFeeCalculation { pub transfer_fee_amount: u64, } +impl TotalFeeCalculation { + pub fn new( + total_fee_lamports: u64, + base_fee: u64, + kora_signature_fee: u64, + fee_payer_outflow: u64, + payment_instruction_fee: u64, + transfer_fee_amount: u64, + ) -> Self { + Self { + total_fee_lamports, + base_fee, + kora_signature_fee, + fee_payer_outflow, + payment_instruction_fee, + transfer_fee_amount, + } + } + + pub fn new_fixed(total_fee_lamports: u64) -> Self { + Self { + total_fee_lamports, + base_fee: 0, + kora_signature_fee: 0, + fee_payer_outflow: 0, + payment_instruction_fee: 0, + transfer_fee_amount: 0, + } + } +} + pub struct FeeConfigUtil {} impl FeeConfigUtil { @@ -255,34 +286,43 @@ impl FeeConfigUtil { ) -> Result { let config = get_config()?; - // Check if the price is free, so that we can return early (and skip expensive RPC calls / estimation) - if matches!(&config.validation.price.model, PriceModel::Free) { - return Ok(TotalFeeCalculation { - total_fee_lamports: 0, - base_fee: 0, - kora_signature_fee: 0, - fee_payer_outflow: 0, - payment_instruction_fee: 0, - transfer_fee_amount: 0, - }); - } + match &config.validation.price.model { + PriceModel::Free => Ok(TotalFeeCalculation::new_fixed(0)), + PriceModel::Fixed { .. } => { + let fixed_fee_lamports = config + .validation + .price + .get_required_lamports_with_fixed(rpc_client, price_source) + .await?; - // Get the raw transaction fees - let mut fee_calculation = - Self::estimate_transaction_fee(rpc_client, transaction, fee_payer, is_payment_required) + Ok(TotalFeeCalculation::new_fixed(fixed_fee_lamports)) + } + PriceModel::Margin { .. } => { + // Get the raw transaction + let fee_calculation = Self::estimate_transaction_fee( + rpc_client, + transaction, + fee_payer, + is_payment_required, + ) .await?; - // Apply Kora's price model - let adjusted_fee = config - .validation - .price - .get_required_lamports(rpc_client, price_source, fee_calculation.total_fee_lamports) - .await?; - - // Update the total with the price model applied - fee_calculation.total_fee_lamports = adjusted_fee; - - Ok(fee_calculation) + let total_fee_lamports = config + .validation + .price + .get_required_lamports_with_margin(fee_calculation.total_fee_lamports) + .await?; + + Ok(TotalFeeCalculation::new( + total_fee_lamports, + fee_calculation.base_fee, + fee_calculation.kora_signature_fee, + fee_calculation.fee_payer_outflow, + fee_calculation.payment_instruction_fee, + fee_calculation.transfer_fee_amount, + )) + } + } } /// Calculate the fee in a specific token if provided diff --git a/crates/lib/src/fee/price.rs b/crates/lib/src/fee/price.rs index 60e74c38..caed3959 100644 --- a/crates/lib/src/fee/price.rs +++ b/crates/lib/src/fee/price.rs @@ -26,56 +26,59 @@ pub struct PriceConfig { } impl PriceConfig { - pub async fn get_required_lamports( + pub async fn get_required_lamports_with_fixed( &self, rpc_client: &RpcClient, price_source: PriceSource, + ) -> Result { + if let PriceModel::Fixed { amount, token } = &self.model { + return TokenUtil::calculate_token_value_in_lamports( + *amount, + &Pubkey::from_str(token).map_err(|e| { + log::error!("Invalid Pubkey for price {e}"); + + KoraError::ConfigError + })?, + price_source, + rpc_client, + ) + .await; + } + + Err(KoraError::ConfigError) + } + + pub async fn get_required_lamports_with_margin( + &self, min_transaction_fee: u64, ) -> Result { - match &self.model { - PriceModel::Margin { margin } => { - let multiplier = 1.0 + margin; - let result = min_transaction_fee as f64 * multiplier; - - // Check for overflow/underflow before casting to u64 - if result > u64::MAX as f64 || result < 0.0 { - log::error!( - "Margin calculation overflow: min_transaction_fee={}, margin={}, result={}", - min_transaction_fee, - margin, - result - ); - return Err(KoraError::ValidationError( - "Margin calculation overflow".to_string(), - )); - } - - Ok(result as u64) + if let PriceModel::Margin { margin } = &self.model { + let multiplier = 1.0 + margin; + let result = min_transaction_fee as f64 * multiplier; + + // Check for overflow/underflow before casting to u64 + if result > u64::MAX as f64 || result < 0.0 { + log::error!( + "Margin calculation overflow: min_transaction_fee={}, margin={}, result={}", + min_transaction_fee, + margin, + result + ); + return Err(KoraError::ValidationError("Margin calculation overflow".to_string())); } - PriceModel::Fixed { amount, token } => { - Ok(TokenUtil::calculate_token_value_in_lamports( - *amount, - &Pubkey::from_str(token).map_err(|e| { - log::error!("Invalid Pubkey for price {e}"); - - KoraError::ConfigError - })?, - price_source, - rpc_client, - ) - .await?) - } - PriceModel::Free => Ok(0), + + return Ok(result as u64); } + + Err(KoraError::ConfigError) } } #[cfg(test)] mod tests { - use crate::tests::common::create_mock_rpc_client_with_mint; - use super::*; + use crate::tests::{common::create_mock_rpc_client_with_mint, config_mock::ConfigMockBuilder}; #[tokio::test] async fn test_margin_model_get_required_lamports() { @@ -85,12 +88,8 @@ mod tests { let min_transaction_fee = 5000u64; // 5000 lamports base fee let expected_lamports = (5000.0 * 1.1) as u64; // 5500 lamports - let rpc_client = create_mock_rpc_client_with_mint(6); - - let result = price_config - .get_required_lamports(&rpc_client, PriceSource::Mock, min_transaction_fee) - .await - .unwrap(); + let result = + price_config.get_required_lamports_with_margin(min_transaction_fee).await.unwrap(); assert_eq!(result, expected_lamports); } @@ -102,18 +101,15 @@ mod tests { let min_transaction_fee = 5000u64; - let rpc_client = create_mock_rpc_client_with_mint(6); - - let result = price_config - .get_required_lamports(&rpc_client, PriceSource::Mock, min_transaction_fee) - .await - .unwrap(); + let result = + price_config.get_required_lamports_with_margin(min_transaction_fee).await.unwrap(); assert_eq!(result, min_transaction_fee); } #[tokio::test] async fn test_fixed_model_get_required_lamports_with_oracle() { + let _m = ConfigMockBuilder::new().build_and_setup(); let rpc_client = create_mock_rpc_client_with_mint(6); // USDC has 6 decimals let usdc_mint = "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"; @@ -126,12 +122,9 @@ mod tests { // Use Mock price source which returns 0.0001 SOL per USDC let price_source = PriceSource::Mock; - let min_transaction_fee = 5000u64; - let result = price_config - .get_required_lamports(&rpc_client, price_source, min_transaction_fee) - .await - .unwrap(); + let result = + price_config.get_required_lamports_with_fixed(&rpc_client, price_source).await.unwrap(); // Expected calculation: // 1,000,000 base units / 10^6 = 1.0 USDC @@ -142,6 +135,7 @@ mod tests { #[tokio::test] async fn test_fixed_model_get_required_lamports_with_custom_price() { + let _m = ConfigMockBuilder::new().build_and_setup(); let rpc_client = create_mock_rpc_client_with_mint(9); // 9 decimals token let custom_token = "So11111111111111111111111111111111111111112"; // SOL mint @@ -154,12 +148,9 @@ mod tests { // Mock oracle returns 1.0 SOL price for SOL mint let price_source = PriceSource::Mock; - let min_transaction_fee = 5000u64; - let result = price_config - .get_required_lamports(&rpc_client, price_source, min_transaction_fee) - .await - .unwrap(); + let result = + price_config.get_required_lamports_with_fixed(&rpc_client, price_source).await.unwrap(); // Expected calculation: // 500,000,000 base units / 10^9 = 0.5 tokens @@ -170,6 +161,7 @@ mod tests { #[tokio::test] async fn test_fixed_model_get_required_lamports_small_amount() { + let _m = ConfigMockBuilder::new().build_and_setup(); let rpc_client = create_mock_rpc_client_with_mint(6); // USDC has 6 decimals let usdc_mint = "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"; @@ -181,12 +173,9 @@ mod tests { }; let price_source = PriceSource::Mock; - let min_transaction_fee = 5000u64; - let result = price_config - .get_required_lamports(&rpc_client, price_source, min_transaction_fee) - .await - .unwrap(); + let result = + price_config.get_required_lamports_with_fixed(&rpc_client, price_source).await.unwrap(); // Expected calculation: // 1,000 base units / 10^6 = 0.001 USDC @@ -195,39 +184,6 @@ mod tests { assert_eq!(result, 100); } - #[tokio::test] - async fn test_free_model_get_required_lamports() { - let rpc_client = create_mock_rpc_client_with_mint(6); - - let price_config = PriceConfig { model: PriceModel::Free }; - - let min_transaction_fee = 10000u64; - - let result = price_config - .get_required_lamports(&rpc_client, PriceSource::Mock, min_transaction_fee) - .await - .unwrap(); - - assert_eq!(result, 0); - } - - #[tokio::test] - async fn test_free_model_get_required_lamports_with_high_base_fee() { - let rpc_client = create_mock_rpc_client_with_mint(6); - - let price_config = PriceConfig { model: PriceModel::Free }; - - let min_transaction_fee = 1000000u64; - - let result = price_config - .get_required_lamports(&rpc_client, PriceSource::Mock, min_transaction_fee) - .await - .unwrap(); - - // Free model should always return 0 regardless of base fee - assert_eq!(result, 0); - } - #[tokio::test] async fn test_default_price_config() { // Test that default creates Margin with 0.0 margin diff --git a/crates/lib/src/rpc_server/method/transfer_transaction.rs b/crates/lib/src/rpc_server/method/transfer_transaction.rs index 93c6f5ab..30fa9d09 100644 --- a/crates/lib/src/rpc_server/method/transfer_transaction.rs +++ b/crates/lib/src/rpc_server/method/transfer_transaction.rs @@ -167,7 +167,7 @@ mod tests { let request = TransferTransactionRequest { amount: 1000, token: Pubkey::new_unique().to_string(), - source: "invalid_pubkey".to_string(), + source: "invalid".to_string(), destination: Pubkey::new_unique().to_string(), signer_key: None, }; From d2db3f65a5a994106cd981257727813c9f49b475 Mon Sep 17 00:00:00 2001 From: jo <17280917+dev-jodee@users.noreply.github.com> Date: Tue, 30 Sep 2025 11:42:38 -0400 Subject: [PATCH 10/29] feat: (PRO-345) Fixed inner instructions (#228) * feat: (PRO-345) Fixed inner instructions - Inner instructions weren't properly being handled - Added parsing of Parsed / PartiallyDecoded instructions coming back from the RPC - Added tests with inner instructions to make sure the inner instructions are properly used --- Cargo.lock | 152 +- Cargo.toml | 1 + crates/lib/Cargo.toml | 1 + .../lib/src/transaction/instruction_util.rs | 2084 ++++++++++++++++- .../src/transaction/versioned_transaction.rs | 20 +- tests/rpc/fee_estimation.rs | 74 +- tests/src/bin/test_runner.rs | 1 + tests/src/common/fixtures/kora-test.toml | 3 +- tests/src/common/transaction.rs | 61 +- tests/src/test_runner/kora.rs | 11 +- 10 files changed, 2393 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 487edc2b..c41815ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,16 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + [[package]] name = "addr2line" version = "0.24.2" @@ -67,6 +77,17 @@ dependencies = [ "solana-svm-feature-set", ] +[[package]] +name = "agave-reserved-account-keys" +version = "2.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685cb445fe51b7b8a914d1b7dd5a0ea0b106fb8ea9454e84c4cd726a5d87c571" +dependencies = [ + "agave-feature-set", + "solana-pubkey", + "solana-sdk-ids", +] + [[package]] name = "ahash" version = "0.7.8" @@ -2926,6 +2947,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "kaigan" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ba15de5aeb137f0f65aa3bf82187647f1285abfe5b20c80c2c37f7007ad519a" +dependencies = [ + "borsh 0.10.4", + "serde", +] + [[package]] name = "keccak" version = "0.1.5" @@ -3002,8 +3033,9 @@ dependencies = [ "solana-program 2.3.0", "solana-sdk 2.3.1", "solana-system-interface", + "solana-transaction-status", "solana-transaction-status-client-types", - "spl-associated-token-account", + "spl-associated-token-account 6.0.0", "spl-pod", "spl-token 7.0.0", "spl-token-2022 8.0.1", @@ -5084,6 +5116,49 @@ dependencies = [ "solana-sysvar", ] +[[package]] +name = "solana-account-decoder" +version = "2.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5963fbe3e1099613c270fd5ebc0ff5c6e88a2bea2505b6e348daa0466282cd6" +dependencies = [ + "Inflector", + "base64 0.22.1", + "bincode", + "bs58 0.5.1", + "bv", + "serde", + "serde_derive", + "serde_json", + "solana-account", + "solana-account-decoder-client-types", + "solana-address-lookup-table-interface", + "solana-clock", + "solana-config-program-client", + "solana-epoch-schedule", + "solana-fee-calculator", + "solana-instruction", + "solana-loader-v3-interface", + "solana-nonce", + "solana-program-option", + "solana-program-pack", + "solana-pubkey", + "solana-rent", + "solana-sdk-ids", + "solana-slot-hashes", + "solana-slot-history", + "solana-stake-interface", + "solana-sysvar", + "solana-vote-interface", + "spl-generic-token", + "spl-token 8.0.0", + "spl-token-2022 8.0.1", + "spl-token-group-interface 0.6.0", + "spl-token-metadata-interface 0.7.0", + "thiserror 2.0.14", + "zstd", +] + [[package]] name = "solana-account-decoder-client-types" version = "2.3.7" @@ -5312,6 +5387,19 @@ dependencies = [ "solana-sdk-ids", ] +[[package]] +name = "solana-config-program-client" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53aceac36f105fd4922e29b4f0c1f785b69d7b3e7e387e384b8985c8e0c3595e" +dependencies = [ + "bincode", + "borsh 0.10.4", + "kaigan", + "serde", + "solana-program 2.3.0", +] + [[package]] name = "solana-connection-cache" version = "2.3.7" @@ -7096,6 +7184,50 @@ dependencies = [ "solana-signature", ] +[[package]] +name = "solana-transaction-status" +version = "2.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287a86e28777cdc8c0745ff5700a2c3741a2a7a72a347a93815e832adfe39dc5" +dependencies = [ + "Inflector", + "agave-reserved-account-keys", + "base64 0.22.1", + "bincode", + "borsh 1.5.7", + "bs58 0.5.1", + "log", + "serde", + "serde_derive", + "serde_json", + "solana-account-decoder", + "solana-address-lookup-table-interface", + "solana-clock", + "solana-hash", + "solana-instruction", + "solana-loader-v2-interface", + "solana-loader-v3-interface", + "solana-message", + "solana-program-option", + "solana-pubkey", + "solana-reward-info", + "solana-sdk-ids", + "solana-signature", + "solana-stake-interface", + "solana-system-interface", + "solana-transaction", + "solana-transaction-error", + "solana-transaction-status-client-types", + "solana-vote-interface", + "spl-associated-token-account 7.0.0", + "spl-memo", + "spl-token 8.0.0", + "spl-token-2022 8.0.1", + "spl-token-group-interface 0.6.0", + "spl-token-metadata-interface 0.7.0", + "thiserror 2.0.14", +] + [[package]] name = "solana-transaction-status-client-types" version = "2.3.7" @@ -7251,6 +7383,22 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "spl-associated-token-account" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae179d4a26b3c7a20c839898e6aed84cb4477adf108a366c95532f058aea041b" +dependencies = [ + "borsh 1.5.7", + "num-derive 0.4.2", + "num-traits", + "solana-program 2.3.0", + "spl-associated-token-account-client", + "spl-token 8.0.0", + "spl-token-2022 8.0.1", + "thiserror 2.0.14", +] + [[package]] name = "spl-associated-token-account-client" version = "2.0.0" @@ -8017,7 +8165,7 @@ dependencies = [ "solana-message", "solana-sdk 2.3.1", "solana-system-interface", - "spl-associated-token-account", + "spl-associated-token-account 6.0.0", "spl-token 7.0.0", "spl-token-2022 8.0.1", "spl-transfer-hook-interface 0.10.0", diff --git a/Cargo.toml b/Cargo.toml index bff2419a..6026728e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ solana-commitment-config = "2.1.9" solana-message = "2.1.9" solana-system-interface = "1.0.0" solana-transaction-status-client-types = "2.1.9" +solana-transaction-status = "2.1.9" solana-address-lookup-table-interface = "2.2.2" solana-program = "2.1.9" solana-client = "2.1.9" diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml index f0300a1f..74c0fb80 100644 --- a/crates/lib/Cargo.toml +++ b/crates/lib/Cargo.toml @@ -27,6 +27,7 @@ solana-message = { workspace = true } solana-system-interface = { workspace = true } solana-program = { workspace = true } solana-transaction-status-client-types = { workspace = true } +solana-transaction-status = { workspace = true } solana-address-lookup-table-interface = { workspace = true } solana-client = { workspace = true } bs58 = { workspace = true } diff --git a/crates/lib/src/transaction/instruction_util.rs b/crates/lib/src/transaction/instruction_util.rs index f76992b5..445a5099 100644 --- a/crates/lib/src/transaction/instruction_util.rs +++ b/crates/lib/src/transaction/instruction_util.rs @@ -6,6 +6,7 @@ use solana_sdk::{ system_instruction::SystemInstruction, system_program::ID as SYSTEM_PROGRAM_ID, }; +use solana_transaction_status_client_types::{UiInstruction, UiParsedInstruction}; use crate::{ constant::instruction_indexes, error::KoraError, transaction::VersionedTransactionResolved, @@ -84,7 +85,119 @@ macro_rules! validate_number_accounts { pub struct IxUtils; +pub const PARSED_DATA_FIELD_TYPE: &str = "type"; +pub const PARSED_DATA_FIELD_INFO: &str = "info"; + +pub const PARSED_DATA_FIELD_SOURCE: &str = "source"; +pub const PARSED_DATA_FIELD_DESTINATION: &str = "destination"; +pub const PARSED_DATA_FIELD_OWNER: &str = "owner"; + +pub const PARSED_DATA_FIELD_TRANSFER: &str = "transfer"; +pub const PARSED_DATA_FIELD_CREATE_ACCOUNT: &str = "createAccount"; +pub const PARSED_DATA_FIELD_ASSIGN: &str = "assign"; +pub const PARSED_DATA_FIELD_TRANSFER_WITH_SEED: &str = "transferWithSeed"; +pub const PARSED_DATA_FIELD_CREATE_ACCOUNT_WITH_SEED: &str = "createAccountWithSeed"; +pub const PARSED_DATA_FIELD_ASSIGN_WITH_SEED: &str = "assignWithSeed"; +pub const PARSED_DATA_FIELD_WITHDRAW_NONCE_ACCOUNT: &str = "withdrawFromNonce"; +pub const PARSED_DATA_FIELD_BURN: &str = "burn"; +pub const PARSED_DATA_FIELD_BURN_CHECKED: &str = "burnChecked"; +pub const PARSED_DATA_FIELD_CLOSE_ACCOUNT: &str = "closeAccount"; +pub const PARSED_DATA_FIELD_TRANSFER_CHECKED: &str = "transferChecked"; +pub const PARSED_DATA_FIELD_APPROVE: &str = "approve"; +pub const PARSED_DATA_FIELD_APPROVE_CHECKED: &str = "approveChecked"; + +pub const PARSED_DATA_FIELD_AMOUNT: &str = "amount"; +pub const PARSED_DATA_FIELD_LAMPORTS: &str = "lamports"; +pub const PARSED_DATA_FIELD_DECIMALS: &str = "decimals"; +pub const PARSED_DATA_FIELD_UI_AMOUNT: &str = "uiAmount"; +pub const PARSED_DATA_FIELD_UI_AMOUNT_STRING: &str = "uiAmountString"; +pub const PARSED_DATA_FIELD_TOKEN_AMOUNT: &str = "tokenAmount"; +pub const PARSED_DATA_FIELD_ACCOUNT: &str = "account"; +pub const PARSED_DATA_FIELD_NEW_ACCOUNT: &str = "newAccount"; +pub const PARSED_DATA_FIELD_AUTHORITY: &str = "authority"; +pub const PARSED_DATA_FIELD_MINT: &str = "mint"; +pub const PARSED_DATA_FIELD_SPACE: &str = "space"; +pub const PARSED_DATA_FIELD_DELEGATE: &str = "delegate"; +pub const PARSED_DATA_FIELD_BASE: &str = "base"; +pub const PARSED_DATA_FIELD_SEED: &str = "seed"; +pub const PARSED_DATA_FIELD_SOURCE_BASE: &str = "sourceBase"; +pub const PARSED_DATA_FIELD_SOURCE_SEED: &str = "sourceSeed"; +pub const PARSED_DATA_FIELD_SOURCE_OWNER: &str = "sourceOwner"; +pub const PARSED_DATA_FIELD_NONCE_ACCOUNT: &str = "nonceAccount"; +pub const PARSED_DATA_FIELD_RECIPIENT: &str = "recipient"; +pub const PARSED_DATA_FIELD_NONCE_AUTHORITY: &str = "nonceAuthority"; + impl IxUtils { + /// Helper method to extract a field as a string from JSON with proper error handling + fn get_field_as_str<'a>( + info: &'a serde_json::Value, + field_name: &str, + ) -> Result<&'a str, KoraError> { + info.get(field_name) + .ok_or_else(|| { + KoraError::SerializationError(format!("Missing field '{}'", field_name)) + })? + .as_str() + .ok_or_else(|| { + KoraError::SerializationError(format!("Field '{}' is not a string", field_name)) + }) + } + + /// Helper method to extract a field as a Pubkey from JSON with proper error handling + fn get_field_as_pubkey( + info: &serde_json::Value, + field_name: &str, + ) -> Result { + let pubkey_str = Self::get_field_as_str(info, field_name)?; + pubkey_str.parse::().map_err(|e| { + KoraError::SerializationError(format!( + "Field '{}' is not a valid pubkey: {}", + field_name, e + )) + }) + } + + /// Helper method to extract a field as u64 from JSON string with proper error handling + fn get_field_as_u64(info: &serde_json::Value, field_name: &str) -> Result { + let value = info.get(field_name).ok_or_else(|| { + KoraError::SerializationError(format!("Missing field '{}'", field_name)) + })?; + + // Try as native JSON number first + if let Some(num) = value.as_u64() { + return Ok(num); + } + + // Fall back to string parsing + if let Some(str_val) = value.as_str() { + return str_val.parse::().map_err(|e| { + KoraError::SerializationError(format!( + "Field '{}' is not a valid u64: {}", + field_name, e + )) + }); + } + + Err(KoraError::SerializationError(format!( + "Field '{}' is neither a number nor a string", + field_name + ))) + } + + /// Helper method to get account index from hashmap with proper error handling + fn get_account_index( + account_keys_hashmap: &HashMap, + pubkey: &Pubkey, + ) -> Result { + account_keys_hashmap.get(pubkey).copied().ok_or_else(|| { + KoraError::SerializationError(format!("{} not found in account keys", pubkey)) + }) + } + + pub fn build_account_keys_hashmap(account_keys: &[Pubkey]) -> HashMap { + account_keys.iter().enumerate().map(|(idx, key)| (*key, idx as u8)).collect() + } + pub fn get_account_key_if_present(ix: &Instruction, index: usize) -> Option { if ix.accounts.is_empty() { return None; @@ -97,6 +210,10 @@ impl IxUtils { Some(ix.accounts[index].pubkey) } + pub fn build_default_compiled_instruction(program_id_index: u8) -> CompiledInstruction { + CompiledInstruction { program_id_index, accounts: vec![], data: vec![] } + } + pub fn uncompile_instructions( instructions: &[CompiledInstruction], account_keys: &[Pubkey], @@ -120,6 +237,488 @@ impl IxUtils { .collect() } + /// Reconstruct a CompiledInstruction from various UiInstruction formats + /// + /// This is required because when you simulate a transaction with inner instructions flag, + /// the RPC pre-parses some of the instructions (like for SPL program and System Program), + /// however this is an issue for Kora, as we expected "Compiled" instructions rather than "Parsed" instructions, + /// because we have our own parsing logic on our Kora's side. + /// + /// So we need to reconstruct the "Compiled" instructions from the "Parsed" instructions, by "unparsing" the "Parsed" instructions. + /// + /// There's no known way to force the RPC to not parsed the instructions, so we need this "hack" to reverse the process. + /// + /// Example: https://github.com/anza-xyz/agave/blob/68032b576dc4c14b31c15974c6734ae1513980a3/transaction-status/src/parse_system.rs#L11 + pub fn reconstruct_instruction_from_ui( + ui_instruction: &UiInstruction, + all_account_keys: &[Pubkey], + ) -> Option { + match ui_instruction { + UiInstruction::Compiled(compiled) => { + // Already compiled, decode data and return + Some(CompiledInstruction { + program_id_index: compiled.program_id_index, + accounts: compiled.accounts.clone(), + data: bs58::decode(&compiled.data).into_vec().unwrap_or_default(), + }) + } + UiInstruction::Parsed(ui_parsed) => match ui_parsed { + UiParsedInstruction::Parsed(parsed) => { + let account_keys_hashmap = Self::build_account_keys_hashmap(all_account_keys); + // Reconstruct based on program type + if parsed.program_id == SYSTEM_PROGRAM_ID.to_string() { + Self::reconstruct_system_instruction(parsed, &account_keys_hashmap).ok() + } else if parsed.program == spl_token::ID.to_string() + || parsed.program == spl_token_2022::ID.to_string() + { + Self::reconstruct_spl_token_instruction(parsed, &account_keys_hashmap).ok() + } else { + // For unsupported programs, create a stub instruction with just the program ID + // This ensures the program ID is preserved for security validation + let program_id = parsed.program_id.parse::().ok()?; + let program_id_index = *account_keys_hashmap.get(&program_id)?; + + Some(Self::build_default_compiled_instruction(program_id_index)) + } + } + UiParsedInstruction::PartiallyDecoded(partial) => { + let account_keys_hashmap = Self::build_account_keys_hashmap(all_account_keys); + if let Ok(program_id) = partial.program_id.parse::() { + if let Some(program_idx) = account_keys_hashmap.get(&program_id) { + // Convert account addresses to indices + let account_indices: Vec = partial + .accounts + .iter() + .filter_map(|addr_str| { + addr_str + .parse::() + .ok() + .and_then(|pubkey| account_keys_hashmap.get(&pubkey)) + .copied() + }) + .collect(); + + return Some(CompiledInstruction { + program_id_index: *program_idx, + accounts: account_indices, + data: bs58::decode(&partial.data).into_vec().unwrap_or_default(), + }); + } + } + + log::error!("Failed to reconstruct partially decoded instruction"); + None + } + }, + } + } + + /// Reconstruct system program instructions from parsed format + fn reconstruct_system_instruction( + parsed: &solana_transaction_status_client_types::ParsedInstruction, + account_keys_hashmap: &HashMap, + ) -> Result { + let program_id_index = Self::get_account_index(account_keys_hashmap, &SYSTEM_PROGRAM_ID)?; + + let parsed_data = &parsed.parsed; + let instruction_type = Self::get_field_as_str(parsed_data, PARSED_DATA_FIELD_TYPE)?; + let info = parsed_data + .get(PARSED_DATA_FIELD_INFO) + .ok_or_else(|| KoraError::SerializationError("Missing 'info' field".to_string()))?; + + match instruction_type { + PARSED_DATA_FIELD_TRANSFER => { + let source = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_SOURCE)?; + let destination = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_DESTINATION)?; + let lamports = Self::get_field_as_u64(info, PARSED_DATA_FIELD_LAMPORTS)?; + + let source_idx = Self::get_account_index(account_keys_hashmap, &source)?; + let destination_idx = Self::get_account_index(account_keys_hashmap, &destination)?; + + let transfer_ix = SystemInstruction::Transfer { lamports }; + let data = bincode::serialize(&transfer_ix).map_err(|e| { + KoraError::SerializationError(format!( + "Failed to serialize Transfer instruction: {}", + e + )) + })?; + + Ok(CompiledInstruction { + program_id_index, + accounts: vec![source_idx, destination_idx], + data, + }) + } + PARSED_DATA_FIELD_CREATE_ACCOUNT => { + let source = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_SOURCE)?; + let new_account = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_NEW_ACCOUNT)?; + let owner = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_OWNER)?; + let lamports = Self::get_field_as_u64(info, PARSED_DATA_FIELD_LAMPORTS)?; + let space = Self::get_field_as_u64(info, PARSED_DATA_FIELD_SPACE)?; + + let source_idx = Self::get_account_index(account_keys_hashmap, &source)?; + let new_account_idx = Self::get_account_index(account_keys_hashmap, &new_account)?; + + let create_ix = SystemInstruction::CreateAccount { lamports, space, owner }; + let data = bincode::serialize(&create_ix).map_err(|e| { + KoraError::SerializationError(format!( + "Failed to serialize CreateAccount instruction: {}", + e + )) + })?; + + Ok(CompiledInstruction { + program_id_index, + accounts: vec![source_idx, new_account_idx], + data, + }) + } + PARSED_DATA_FIELD_ASSIGN => { + let authority = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_ACCOUNT)?; + let owner = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_OWNER)?; + + let authority_idx = Self::get_account_index(account_keys_hashmap, &authority)?; + + let assign_ix = SystemInstruction::Assign { owner }; + let data = bincode::serialize(&assign_ix).map_err(|e| { + KoraError::SerializationError(format!( + "Failed to serialize Assign instruction: {}", + e + )) + })?; + + Ok(CompiledInstruction { program_id_index, accounts: vec![authority_idx], data }) + } + PARSED_DATA_FIELD_TRANSFER_WITH_SEED => { + let source = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_SOURCE)?; + let destination = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_DESTINATION)?; + let lamports = Self::get_field_as_u64(info, PARSED_DATA_FIELD_LAMPORTS)?; + let source_base = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_SOURCE_BASE)?; + let source_seed = + Self::get_field_as_str(info, PARSED_DATA_FIELD_SOURCE_SEED)?.to_string(); + let source_owner = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_SOURCE_OWNER)?; + + let source_idx = Self::get_account_index(account_keys_hashmap, &source)?; + let destination_idx = Self::get_account_index(account_keys_hashmap, &destination)?; + let source_base_idx = Self::get_account_index(account_keys_hashmap, &source_base)?; + + let transfer_ix = SystemInstruction::TransferWithSeed { + lamports, + from_seed: source_seed, + from_owner: source_owner, + }; + let data = bincode::serialize(&transfer_ix).map_err(|e| { + KoraError::SerializationError(format!( + "Failed to serialize TransferWithSeed instruction: {}", + e + )) + })?; + + Ok(CompiledInstruction { + program_id_index, + accounts: vec![source_idx, source_base_idx, destination_idx], + data, + }) + } + PARSED_DATA_FIELD_CREATE_ACCOUNT_WITH_SEED => { + let source = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_SOURCE)?; + let new_account = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_NEW_ACCOUNT)?; + let base = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_BASE)?; + let seed = Self::get_field_as_str(info, PARSED_DATA_FIELD_SEED)?.to_string(); + let owner = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_OWNER)?; + let lamports = Self::get_field_as_u64(info, PARSED_DATA_FIELD_LAMPORTS)?; + let space = Self::get_field_as_u64(info, PARSED_DATA_FIELD_SPACE)?; + + let source_idx = Self::get_account_index(account_keys_hashmap, &source)?; + let new_account_idx = Self::get_account_index(account_keys_hashmap, &new_account)?; + let base_idx = Self::get_account_index(account_keys_hashmap, &base)?; + + let create_ix = + SystemInstruction::CreateAccountWithSeed { base, seed, lamports, space, owner }; + let data = bincode::serialize(&create_ix).map_err(|e| { + KoraError::SerializationError(format!( + "Failed to serialize CreateAccountWithSeed instruction: {}", + e + )) + })?; + + Ok(CompiledInstruction { + program_id_index, + accounts: vec![source_idx, new_account_idx, base_idx], + data, + }) + } + PARSED_DATA_FIELD_ASSIGN_WITH_SEED => { + let account = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_ACCOUNT)?; + let base = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_BASE)?; + let seed = Self::get_field_as_str(info, PARSED_DATA_FIELD_SEED)?.to_string(); + let owner = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_OWNER)?; + + let account_idx = Self::get_account_index(account_keys_hashmap, &account)?; + let base_idx = Self::get_account_index(account_keys_hashmap, &base)?; + + let assign_ix = SystemInstruction::AssignWithSeed { base, seed, owner }; + let data = bincode::serialize(&assign_ix).map_err(|e| { + KoraError::SerializationError(format!( + "Failed to serialize AssignWithSeed instruction: {}", + e + )) + })?; + + Ok(CompiledInstruction { + program_id_index, + accounts: vec![account_idx, base_idx], + data, + }) + } + PARSED_DATA_FIELD_WITHDRAW_NONCE_ACCOUNT => { + let nonce_account = + Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_NONCE_ACCOUNT)?; + let recipient = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_DESTINATION)?; + let nonce_authority = + Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_NONCE_AUTHORITY)?; + let lamports = Self::get_field_as_u64(info, PARSED_DATA_FIELD_LAMPORTS)?; + + let nonce_account_idx = + Self::get_account_index(account_keys_hashmap, &nonce_account)?; + let recipient_idx = Self::get_account_index(account_keys_hashmap, &recipient)?; + let nonce_authority_idx = + Self::get_account_index(account_keys_hashmap, &nonce_authority)?; + + let withdraw_ix = SystemInstruction::WithdrawNonceAccount(lamports); + let data = bincode::serialize(&withdraw_ix).map_err(|e| { + KoraError::SerializationError(format!( + "Failed to serialize WithdrawNonceAccount instruction: {}", + e + )) + })?; + + Ok(CompiledInstruction { + program_id_index, + accounts: vec![nonce_account_idx, recipient_idx, nonce_authority_idx], + data, + }) + } + _ => { + log::error!("Unsupported system instruction type: {}", instruction_type); + Ok(Self::build_default_compiled_instruction(program_id_index)) + } + } + } + + /// Reconstruct SPL token program instructions from parsed format + fn reconstruct_spl_token_instruction( + parsed: &solana_transaction_status_client_types::ParsedInstruction, + account_keys_hashmap: &HashMap, + ) -> Result { + let program_id = parsed + .program_id + .parse::() + .map_err(|e| KoraError::SerializationError(format!("Invalid program ID: {}", e)))?; + let program_id_index = Self::get_account_index(account_keys_hashmap, &program_id)?; + + let parsed_data = &parsed.parsed; + let instruction_type = Self::get_field_as_str(parsed_data, PARSED_DATA_FIELD_TYPE)?; + let info = parsed_data + .get(PARSED_DATA_FIELD_INFO) + .ok_or_else(|| KoraError::SerializationError("Missing 'info' field".to_string()))?; + + match instruction_type { + PARSED_DATA_FIELD_TRANSFER => { + let source = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_SOURCE)?; + let destination = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_DESTINATION)?; + let authority = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_AUTHORITY)?; + let amount = Self::get_field_as_u64(info, PARSED_DATA_FIELD_AMOUNT)?; + + let source_idx = Self::get_account_index(account_keys_hashmap, &source)?; + let destination_idx = Self::get_account_index(account_keys_hashmap, &destination)?; + let authority_idx = Self::get_account_index(account_keys_hashmap, &authority)?; + + let data = if parsed.program_id == spl_token::ID.to_string() { + spl_token::instruction::TokenInstruction::Transfer { amount }.pack() + } else { + #[allow(deprecated)] + spl_token_2022::instruction::TokenInstruction::Transfer { amount }.pack() + }; + + Ok(CompiledInstruction { + program_id_index, + accounts: vec![source_idx, destination_idx, authority_idx], + data, + }) + } + PARSED_DATA_FIELD_TRANSFER_CHECKED => { + let source = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_SOURCE)?; + let destination = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_DESTINATION)?; + let authority = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_AUTHORITY)?; + let mint = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_MINT)?; + + let token_amount = info.get(PARSED_DATA_FIELD_TOKEN_AMOUNT).ok_or_else(|| { + KoraError::SerializationError("Missing 'tokenAmount' field".to_string()) + })?; + let amount = Self::get_field_as_u64(token_amount, PARSED_DATA_FIELD_AMOUNT)?; + let decimals = + Self::get_field_as_u64(token_amount, PARSED_DATA_FIELD_DECIMALS)? as u8; + + let source_idx = Self::get_account_index(account_keys_hashmap, &source)?; + let mint_idx = Self::get_account_index(account_keys_hashmap, &mint)?; + let destination_idx = Self::get_account_index(account_keys_hashmap, &destination)?; + let authority_idx = Self::get_account_index(account_keys_hashmap, &authority)?; + + let data = if parsed.program_id == spl_token::ID.to_string() { + spl_token::instruction::TokenInstruction::TransferChecked { amount, decimals } + .pack() + } else { + spl_token_2022::instruction::TokenInstruction::TransferChecked { + amount, + decimals, + } + .pack() + }; + + Ok(CompiledInstruction { + program_id_index, + accounts: vec![source_idx, mint_idx, destination_idx, authority_idx], + data, + }) + } + PARSED_DATA_FIELD_BURN | PARSED_DATA_FIELD_BURN_CHECKED => { + let account = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_ACCOUNT)?; + let authority = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_AUTHORITY)?; + + let (amount, decimals) = if instruction_type == PARSED_DATA_FIELD_BURN_CHECKED { + let token_amount = + info.get(PARSED_DATA_FIELD_TOKEN_AMOUNT).ok_or_else(|| { + KoraError::SerializationError( + "Missing 'tokenAmount' field for burnChecked".to_string(), + ) + })?; + let amount = Self::get_field_as_u64(token_amount, PARSED_DATA_FIELD_AMOUNT)?; + let decimals = + Self::get_field_as_u64(token_amount, PARSED_DATA_FIELD_DECIMALS)? as u8; + (amount, Some(decimals)) + } else { + let amount = + Self::get_field_as_u64(info, PARSED_DATA_FIELD_AMOUNT).unwrap_or(0); + (amount, None) + }; + + let account_idx = Self::get_account_index(account_keys_hashmap, &account)?; + let authority_idx = Self::get_account_index(account_keys_hashmap, &authority)?; + + let accounts = if instruction_type == PARSED_DATA_FIELD_BURN_CHECKED { + let mint = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_MINT)?; + let mint_idx = Self::get_account_index(account_keys_hashmap, &mint)?; + vec![account_idx, mint_idx, authority_idx] + } else { + vec![account_idx, authority_idx] + }; + + let data = if instruction_type == PARSED_DATA_FIELD_BURN_CHECKED { + let decimals = decimals.unwrap(); // Safe because we set it above for burnChecked + if parsed.program_id == spl_token::ID.to_string() { + spl_token::instruction::TokenInstruction::BurnChecked { amount, decimals } + .pack() + } else { + spl_token_2022::instruction::TokenInstruction::BurnChecked { + amount, + decimals, + } + .pack() + } + } else if parsed.program_id == spl_token::ID.to_string() { + spl_token::instruction::TokenInstruction::Burn { amount }.pack() + } else { + spl_token_2022::instruction::TokenInstruction::Burn { amount }.pack() + }; + + Ok(CompiledInstruction { program_id_index, accounts, data }) + } + PARSED_DATA_FIELD_CLOSE_ACCOUNT => { + let account = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_ACCOUNT)?; + let destination = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_DESTINATION)?; + let authority = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_OWNER)?; + + let account_idx = Self::get_account_index(account_keys_hashmap, &account)?; + let destination_idx = Self::get_account_index(account_keys_hashmap, &destination)?; + let authority_idx = Self::get_account_index(account_keys_hashmap, &authority)?; + + let data = if parsed.program_id == spl_token::ID.to_string() { + spl_token::instruction::TokenInstruction::CloseAccount.pack() + } else { + spl_token_2022::instruction::TokenInstruction::CloseAccount.pack() + }; + + Ok(CompiledInstruction { + program_id_index, + accounts: vec![account_idx, destination_idx, authority_idx], + data, + }) + } + PARSED_DATA_FIELD_APPROVE => { + let source = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_SOURCE)?; + let delegate = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_DELEGATE)?; + let owner = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_OWNER)?; + let amount = Self::get_field_as_u64(info, PARSED_DATA_FIELD_AMOUNT)?; + + let source_idx = Self::get_account_index(account_keys_hashmap, &source)?; + let delegate_idx = Self::get_account_index(account_keys_hashmap, &delegate)?; + let owner_idx = Self::get_account_index(account_keys_hashmap, &owner)?; + + let data = if parsed.program_id == spl_token::ID.to_string() { + spl_token::instruction::TokenInstruction::Approve { amount }.pack() + } else { + spl_token_2022::instruction::TokenInstruction::Approve { amount }.pack() + }; + + Ok(CompiledInstruction { + program_id_index, + accounts: vec![source_idx, delegate_idx, owner_idx], + data, + }) + } + PARSED_DATA_FIELD_APPROVE_CHECKED => { + let source = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_SOURCE)?; + let delegate = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_DELEGATE)?; + let owner = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_OWNER)?; + let mint = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_MINT)?; + + let token_amount = info.get(PARSED_DATA_FIELD_TOKEN_AMOUNT).ok_or_else(|| { + KoraError::SerializationError("Missing 'tokenAmount' field".to_string()) + })?; + let amount = Self::get_field_as_u64(token_amount, PARSED_DATA_FIELD_AMOUNT)?; + let decimals = + Self::get_field_as_u64(token_amount, PARSED_DATA_FIELD_DECIMALS)? as u8; + + let source_idx = Self::get_account_index(account_keys_hashmap, &source)?; + let mint_idx = Self::get_account_index(account_keys_hashmap, &mint)?; + let delegate_idx = Self::get_account_index(account_keys_hashmap, &delegate)?; + let owner_idx = Self::get_account_index(account_keys_hashmap, &owner)?; + + let data = if parsed.program_id == spl_token::ID.to_string() { + spl_token::instruction::TokenInstruction::ApproveChecked { amount, decimals } + .pack() + } else { + spl_token_2022::instruction::TokenInstruction::ApproveChecked { + amount, + decimals, + } + .pack() + }; + + Ok(CompiledInstruction { + program_id_index, + accounts: vec![source_idx, mint_idx, delegate_idx, owner_idx], + data, + }) + } + _ => { + log::error!("Unsupported token instruction type: {}", instruction_type); + Ok(Self::build_default_compiled_instruction(program_id_index)) + } + } + } + pub fn parse_system_instructions( transaction: &VersionedTransactionResolved, ) -> Result>, KoraError> @@ -129,7 +728,7 @@ impl IxUtils { Vec, > = HashMap::new(); - for instruction in &transaction.all_instructions { + for instruction in transaction.all_instructions.iter() { let program_id = instruction.program_id; // Handle System Program transfers and account creation @@ -143,14 +742,16 @@ impl IxUtils { instruction_indexes::system_create_account::REQUIRED_NUMBER_OF_ACCOUNTS ); + let payer = instruction.accounts + [instruction_indexes::system_create_account::PAYER_INDEX] + .pubkey; + parsed_instructions .entry(ParsedSystemInstructionType::SystemCreateAccount) .or_default() .push(ParsedSystemInstructionData::SystemCreateAccount { lamports, - payer: instruction.accounts - [instruction_indexes::system_create_account::PAYER_INDEX] - .pubkey, + payer, }); } // Transfer instructions @@ -452,6 +1053,652 @@ impl IxUtils { mod tests { use super::*; + use solana_sdk::message::{AccountKeys, Message}; + use solana_transaction_status::parse_instruction; + + fn create_parsed_system_transfer( + source: &Pubkey, + destination: &Pubkey, + lamports: u64, + ) -> Result> + { + let solana_instruction = + solana_sdk::system_instruction::transfer(source, destination, lamports); + + let message = Message::new(&[solana_instruction], None); + let compiled_instruction = &message.instructions[0]; + + let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None); + + let parsed = parse_instruction::parse( + &SYSTEM_PROGRAM_ID, + compiled_instruction, + &account_keys_for_parsing, + None, + )?; + + Ok(parsed) + } + + fn create_parsed_system_transfer_with_seed( + source: &Pubkey, + destination: &Pubkey, + lamports: u64, + source_base: &Pubkey, + seed: &str, + source_owner: &Pubkey, + ) -> Result> + { + let solana_instruction = solana_sdk::system_instruction::transfer_with_seed( + source, + source_base, + seed.to_string(), + source_owner, + destination, + lamports, + ); + + let message = Message::new(&[solana_instruction], None); + let compiled_instruction = &message.instructions[0]; + + let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None); + + let parsed = parse_instruction::parse( + &SYSTEM_PROGRAM_ID, + compiled_instruction, + &account_keys_for_parsing, + None, + )?; + + Ok(parsed) + } + + fn create_parsed_system_create_account( + source: &Pubkey, + new_account: &Pubkey, + lamports: u64, + space: u64, + owner: &Pubkey, + ) -> Result> + { + let solana_instruction = solana_sdk::system_instruction::create_account( + source, + new_account, + lamports, + space, + owner, + ); + + let message = Message::new(&[solana_instruction], None); + let compiled_instruction = &message.instructions[0]; + + let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None); + + let parsed = parse_instruction::parse( + &SYSTEM_PROGRAM_ID, + compiled_instruction, + &account_keys_for_parsing, + None, + )?; + + Ok(parsed) + } + + fn create_parsed_system_create_account_with_seed( + source: &Pubkey, + new_account: &Pubkey, + base: &Pubkey, + seed: &str, + lamports: u64, + space: u64, + owner: &Pubkey, + ) -> Result> + { + let solana_instruction = solana_sdk::system_instruction::create_account_with_seed( + source, + new_account, + base, + seed, + lamports, + space, + owner, + ); + + let message = Message::new(&[solana_instruction], None); + let compiled_instruction = &message.instructions[0]; + + let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None); + + let parsed = parse_instruction::parse( + &SYSTEM_PROGRAM_ID, + compiled_instruction, + &account_keys_for_parsing, + None, + )?; + + Ok(parsed) + } + + fn create_parsed_system_assign( + account: &Pubkey, + owner: &Pubkey, + ) -> Result> + { + let solana_instruction = solana_sdk::system_instruction::assign(account, owner); + + let message = Message::new(&[solana_instruction], None); + let compiled_instruction = &message.instructions[0]; + + let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None); + + let parsed = parse_instruction::parse( + &SYSTEM_PROGRAM_ID, + compiled_instruction, + &account_keys_for_parsing, + None, + )?; + + Ok(parsed) + } + + fn create_parsed_system_assign_with_seed( + account: &Pubkey, + base: &Pubkey, + seed: &str, + owner: &Pubkey, + ) -> Result> + { + let solana_instruction = + solana_sdk::system_instruction::assign_with_seed(account, base, seed, owner); + + let message = Message::new(&[solana_instruction], None); + let compiled_instruction = &message.instructions[0]; + + let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None); + + let parsed = parse_instruction::parse( + &SYSTEM_PROGRAM_ID, + compiled_instruction, + &account_keys_for_parsing, + None, + )?; + + Ok(parsed) + } + + fn create_parsed_system_withdraw_nonce_account( + nonce_account: &Pubkey, + nonce_authority: &Pubkey, + recipient: &Pubkey, + lamports: u64, + ) -> Result> + { + let solana_instruction = solana_sdk::system_instruction::withdraw_nonce_account( + nonce_account, + nonce_authority, + recipient, + lamports, + ); + + let message = Message::new(&[solana_instruction], None); + let compiled_instruction = &message.instructions[0]; + + let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None); + + let parsed = parse_instruction::parse( + &SYSTEM_PROGRAM_ID, + compiled_instruction, + &account_keys_for_parsing, + None, + )?; + + Ok(parsed) + } + + fn create_parsed_spl_token_transfer( + source: &Pubkey, + destination: &Pubkey, + authority: &Pubkey, + amount: u64, + ) -> Result> + { + let solana_instruction = spl_token::instruction::transfer( + &spl_token::ID, + source, + destination, + authority, + &[], + amount, + )?; + + let message = Message::new(&[solana_instruction], None); + let compiled_instruction = &message.instructions[0]; + + let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None); + + let parsed = parse_instruction::parse( + &spl_token::ID, + compiled_instruction, + &account_keys_for_parsing, + None, + )?; + + Ok(parsed) + } + + fn create_parsed_spl_token_transfer_checked( + source: &Pubkey, + mint: &Pubkey, + destination: &Pubkey, + authority: &Pubkey, + amount: u64, + decimals: u8, + ) -> Result> + { + let solana_instruction = spl_token::instruction::transfer_checked( + &spl_token::ID, + source, + mint, + destination, + authority, + &[], + amount, + decimals, + )?; + + let message = Message::new(&[solana_instruction], None); + let compiled_instruction = &message.instructions[0]; + + let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None); + + let parsed = parse_instruction::parse( + &spl_token::ID, + compiled_instruction, + &account_keys_for_parsing, + None, + )?; + + Ok(parsed) + } + + fn create_parsed_spl_token_burn( + account: &Pubkey, + mint: &Pubkey, + authority: &Pubkey, + amount: u64, + ) -> Result> + { + let solana_instruction = + spl_token::instruction::burn(&spl_token::ID, account, mint, authority, &[], amount)?; + + let message = Message::new(&[solana_instruction], None); + let compiled_instruction = &message.instructions[0]; + + let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None); + + let parsed = parse_instruction::parse( + &spl_token::ID, + compiled_instruction, + &account_keys_for_parsing, + None, + )?; + + Ok(parsed) + } + + fn create_parsed_spl_token_burn_checked( + account: &Pubkey, + mint: &Pubkey, + authority: &Pubkey, + amount: u64, + decimals: u8, + ) -> Result> + { + let solana_instruction = spl_token::instruction::burn_checked( + &spl_token::ID, + account, + mint, + authority, + &[], + amount, + decimals, + )?; + + let message = Message::new(&[solana_instruction], None); + let compiled_instruction = &message.instructions[0]; + + let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None); + + let parsed = parse_instruction::parse( + &spl_token::ID, + compiled_instruction, + &account_keys_for_parsing, + None, + )?; + + Ok(parsed) + } + + fn create_parsed_spl_token_close_account( + account: &Pubkey, + destination: &Pubkey, + authority: &Pubkey, + ) -> Result> + { + let solana_instruction = spl_token::instruction::close_account( + &spl_token::ID, + account, + destination, + authority, + &[], + )?; + + let message = Message::new(&[solana_instruction], None); + let compiled_instruction = &message.instructions[0]; + + let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None); + + let parsed = parse_instruction::parse( + &spl_token::ID, + compiled_instruction, + &account_keys_for_parsing, + None, + )?; + + Ok(parsed) + } + + fn create_parsed_spl_token_approve( + source: &Pubkey, + delegate: &Pubkey, + authority: &Pubkey, + amount: u64, + ) -> Result> + { + let solana_instruction = spl_token::instruction::approve( + &spl_token::ID, + source, + delegate, + authority, + &[], + amount, + )?; + + let message = Message::new(&[solana_instruction], None); + let compiled_instruction = &message.instructions[0]; + + let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None); + + let parsed = parse_instruction::parse( + &spl_token::ID, + compiled_instruction, + &account_keys_for_parsing, + None, + )?; + + Ok(parsed) + } + + fn create_parsed_spl_token_approve_checked( + source: &Pubkey, + mint: &Pubkey, + delegate: &Pubkey, + authority: &Pubkey, + amount: u64, + decimals: u8, + ) -> Result> + { + let solana_instruction = spl_token::instruction::approve_checked( + &spl_token::ID, + source, + mint, + delegate, + authority, + &[], + amount, + decimals, + )?; + + let message = Message::new(&[solana_instruction], None); + let compiled_instruction = &message.instructions[0]; + + let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None); + + let parsed = parse_instruction::parse( + &spl_token::ID, + compiled_instruction, + &account_keys_for_parsing, + None, + )?; + + Ok(parsed) + } + + fn create_parsed_token2022_transfer( + source: &Pubkey, + destination: &Pubkey, + authority: &Pubkey, + amount: u64, + ) -> Result> + { + #[allow(deprecated)] + let solana_instruction = spl_token_2022::instruction::transfer( + &spl_token_2022::ID, + source, + destination, + authority, + &[], + amount, + )?; + + let message = Message::new(&[solana_instruction], None); + let compiled_instruction = &message.instructions[0]; + + let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None); + + let parsed = parse_instruction::parse( + &spl_token_2022::ID, + compiled_instruction, + &account_keys_for_parsing, + None, + )?; + + Ok(parsed) + } + + fn create_parsed_token2022_transfer_checked( + source: &Pubkey, + mint: &Pubkey, + destination: &Pubkey, + authority: &Pubkey, + amount: u64, + decimals: u8, + ) -> Result> + { + let solana_instruction = spl_token_2022::instruction::transfer_checked( + &spl_token_2022::ID, + source, + mint, + destination, + authority, + &[], + amount, + decimals, + )?; + + let message = Message::new(&[solana_instruction], None); + let compiled_instruction = &message.instructions[0]; + + let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None); + + let parsed = parse_instruction::parse( + &spl_token_2022::ID, + compiled_instruction, + &account_keys_for_parsing, + None, + )?; + + Ok(parsed) + } + + fn create_parsed_token2022_burn( + account: &Pubkey, + mint: &Pubkey, + authority: &Pubkey, + amount: u64, + ) -> Result> + { + let solana_instruction = spl_token_2022::instruction::burn( + &spl_token_2022::ID, + account, + mint, + authority, + &[], + amount, + )?; + + let message = Message::new(&[solana_instruction], None); + let compiled_instruction = &message.instructions[0]; + + let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None); + + let parsed = parse_instruction::parse( + &spl_token_2022::ID, + compiled_instruction, + &account_keys_for_parsing, + None, + )?; + + Ok(parsed) + } + + fn create_parsed_token2022_burn_checked( + account: &Pubkey, + mint: &Pubkey, + authority: &Pubkey, + amount: u64, + decimals: u8, + ) -> Result> + { + let solana_instruction = spl_token_2022::instruction::burn_checked( + &spl_token_2022::ID, + account, + mint, + authority, + &[], + amount, + decimals, + )?; + + let message = Message::new(&[solana_instruction], None); + let compiled_instruction = &message.instructions[0]; + + let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None); + + let parsed = parse_instruction::parse( + &spl_token_2022::ID, + compiled_instruction, + &account_keys_for_parsing, + None, + )?; + + Ok(parsed) + } + + fn create_parsed_token2022_close_account( + account: &Pubkey, + destination: &Pubkey, + authority: &Pubkey, + ) -> Result> + { + let solana_instruction = spl_token_2022::instruction::close_account( + &spl_token_2022::ID, + account, + destination, + authority, + &[], + )?; + + let message = Message::new(&[solana_instruction], None); + let compiled_instruction = &message.instructions[0]; + + let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None); + + let parsed = parse_instruction::parse( + &spl_token_2022::ID, + compiled_instruction, + &account_keys_for_parsing, + None, + )?; + + Ok(parsed) + } + + fn create_parsed_token2022_approve( + source: &Pubkey, + delegate: &Pubkey, + authority: &Pubkey, + amount: u64, + ) -> Result> + { + let solana_instruction = spl_token_2022::instruction::approve( + &spl_token_2022::ID, + source, + delegate, + authority, + &[], + amount, + )?; + + let message = Message::new(&[solana_instruction], None); + let compiled_instruction = &message.instructions[0]; + + let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None); + + let parsed = parse_instruction::parse( + &spl_token_2022::ID, + compiled_instruction, + &account_keys_for_parsing, + None, + )?; + + Ok(parsed) + } + + fn create_parsed_token2022_approve_checked( + source: &Pubkey, + mint: &Pubkey, + delegate: &Pubkey, + authority: &Pubkey, + amount: u64, + decimals: u8, + ) -> Result> + { + let solana_instruction = spl_token_2022::instruction::approve_checked( + &spl_token_2022::ID, + source, + mint, + delegate, + authority, + &[], + amount, + decimals, + )?; + + let message = Message::new(&[solana_instruction], None); + let compiled_instruction = &message.instructions[0]; + + let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None); + + let parsed = parse_instruction::parse( + &spl_token_2022::ID, + compiled_instruction, + &account_keys_for_parsing, + None, + )?; + + Ok(parsed) + } #[test] fn test_uncompile_instructions() { @@ -476,4 +1723,833 @@ mod tests { assert_eq!(uncompiled.accounts[1].pubkey, account2); assert_eq!(uncompiled.data, vec![1, 2, 3]); } + + #[test] + fn test_reconstruct_instruction_from_ui_compiled() { + let program_id = Pubkey::new_unique(); + let account1 = Pubkey::new_unique(); + let account_keys = vec![program_id, account1]; + + let ui_compiled = solana_transaction_status_client_types::UiCompiledInstruction { + program_id_index: 0, + accounts: vec![1], + data: bs58::encode(&[1, 2, 3]).into_string(), + stack_height: None, + }; + + let result = IxUtils::reconstruct_instruction_from_ui( + &UiInstruction::Compiled(ui_compiled), + &account_keys, + ); + + assert!(result.is_some()); + let compiled = result.unwrap(); + assert_eq!(compiled.program_id_index, 0); + assert_eq!(compiled.accounts, vec![1]); + assert_eq!(compiled.data, vec![1, 2, 3]); + } + + #[test] + fn test_reconstruct_partially_decoded_instruction() { + let program_id = Pubkey::new_unique(); + let account1 = Pubkey::new_unique(); + let account2 = Pubkey::new_unique(); + let account_keys = vec![program_id, account1, account2]; + + let partial = solana_transaction_status_client_types::UiPartiallyDecodedInstruction { + program_id: program_id.to_string(), + accounts: vec![account1.to_string(), account2.to_string()], + data: bs58::encode(&[5, 6, 7]).into_string(), + stack_height: None, + }; + + let ui_parsed = UiParsedInstruction::PartiallyDecoded(partial); + + let result = IxUtils::reconstruct_instruction_from_ui( + &UiInstruction::Parsed(ui_parsed), + &account_keys, + ); + + assert!(result.is_some()); + let compiled = result.unwrap(); + assert_eq!(compiled.program_id_index, 0); + assert_eq!(compiled.accounts, vec![1, 2]); // account1, account2 indices + assert_eq!(compiled.data, vec![5, 6, 7]); + } + + #[test] + fn test_reconstruct_system_transfer_instruction() { + let source = Pubkey::new_unique(); + let destination = Pubkey::new_unique(); + let system_program_id = SYSTEM_PROGRAM_ID; + let account_keys = vec![system_program_id, source, destination]; + let lamports = 1000000u64; + + let transfer_instruction = + solana_sdk::system_instruction::transfer(&source, &destination, lamports); + + let solana_parsed_transfer = create_parsed_system_transfer(&source, &destination, lamports) + .expect("Failed to create authentic parsed instruction"); + + let result = IxUtils::reconstruct_system_instruction( + &solana_parsed_transfer, + &IxUtils::build_account_keys_hashmap(&account_keys), + ); + + assert!(result.is_ok()); + let compiled = result.unwrap(); + assert_eq!(compiled.program_id_index, 0); + assert_eq!(compiled.accounts, vec![1, 2]); // source, destination indices + assert_eq!(compiled.data, transfer_instruction.data); + } + + #[test] + fn test_reconstruct_system_transfer_with_seed_instruction() { + let source = Pubkey::new_unique(); + let destination = Pubkey::new_unique(); + let source_base = Pubkey::new_unique(); + let source_owner = Pubkey::new_unique(); + let system_program_id = SYSTEM_PROGRAM_ID; + let account_keys = vec![system_program_id, source, source_base, destination]; + let lamports = 5000000u64; + + let instruction = solana_sdk::system_instruction::transfer_with_seed( + &source, + &source_base, + "test_seed".to_string(), + &source_owner, + &destination, + lamports, + ); + + let solana_parsed = create_parsed_system_transfer_with_seed( + &source, + &destination, + lamports, + &source_base, + "test_seed", + &source_owner, + ) + .expect("Failed to create parsed instruction"); + + let result = IxUtils::reconstruct_system_instruction( + &solana_parsed, + &IxUtils::build_account_keys_hashmap(&account_keys), + ); + + assert!(result.is_ok()); + let compiled = result.unwrap(); + assert_eq!(compiled.program_id_index, 0); + assert_eq!(compiled.accounts, vec![1, 2, 3]); // source, source_base, destination indices + assert_eq!(compiled.data, instruction.data); + } + + #[test] + fn test_reconstruct_system_create_account_instruction() { + let source = Pubkey::new_unique(); + let new_account = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + let system_program_id = SYSTEM_PROGRAM_ID; + let account_keys = vec![system_program_id, source, new_account]; + let lamports = 2000000u64; + let space = 165u64; + + let instruction = solana_sdk::system_instruction::create_account( + &source, + &new_account, + lamports, + space, + &owner, + ); + + let solana_parsed = + create_parsed_system_create_account(&source, &new_account, lamports, space, &owner) + .expect("Failed to create parsed instruction"); + + let result = IxUtils::reconstruct_system_instruction( + &solana_parsed, + &IxUtils::build_account_keys_hashmap(&account_keys), + ); + + assert!(result.is_ok()); + let compiled = result.unwrap(); + assert_eq!(compiled.program_id_index, 0); + assert_eq!(compiled.accounts, vec![1, 2]); // source, new_account indices + assert_eq!(compiled.data, instruction.data); + } + + #[test] + fn test_reconstruct_system_create_account_with_seed_instruction() { + let source = Pubkey::new_unique(); + let new_account = Pubkey::new_unique(); + let base = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + let system_program_id = SYSTEM_PROGRAM_ID; + let account_keys = vec![system_program_id, source, new_account, base]; + let lamports = 3000000u64; + let space = 200u64; + + let instruction = solana_sdk::system_instruction::create_account_with_seed( + &source, + &new_account, + &base, + "test_seed_create", + lamports, + space, + &owner, + ); + + let solana_parsed = create_parsed_system_create_account_with_seed( + &source, + &new_account, + &base, + "test_seed_create", + lamports, + space, + &owner, + ) + .expect("Failed to create parsed instruction"); + + let result = IxUtils::reconstruct_system_instruction( + &solana_parsed, + &IxUtils::build_account_keys_hashmap(&account_keys), + ); + + assert!(result.is_ok()); + let compiled = result.unwrap(); + assert_eq!(compiled.program_id_index, 0); + assert_eq!(compiled.accounts, vec![1, 2, 3]); // source, new_account, base indices + assert_eq!(compiled.data, instruction.data); + } + + #[test] + fn test_reconstruct_system_assign_instruction() { + let account = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + let system_program_id = SYSTEM_PROGRAM_ID; + let account_keys = vec![system_program_id, account]; + + let instruction = solana_sdk::system_instruction::assign(&account, &owner); + + let solana_parsed = create_parsed_system_assign(&account, &owner) + .expect("Failed to create parsed instruction"); + + let result = IxUtils::reconstruct_system_instruction( + &solana_parsed, + &IxUtils::build_account_keys_hashmap(&account_keys), + ); + + assert!(result.is_ok()); + let compiled = result.unwrap(); + assert_eq!(compiled.program_id_index, 0); + assert_eq!(compiled.accounts, vec![1]); // account index + assert_eq!(compiled.data, instruction.data); + } + + #[test] + fn test_reconstruct_system_assign_with_seed_instruction() { + let account = Pubkey::new_unique(); + let base = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + let system_program_id = SYSTEM_PROGRAM_ID; + let account_keys = vec![system_program_id, account, base]; + + let instruction = solana_sdk::system_instruction::assign_with_seed( + &account, + &base, + "test_assign_seed", + &owner, + ); + + let solana_parsed = + create_parsed_system_assign_with_seed(&account, &base, "test_assign_seed", &owner) + .expect("Failed to create parsed instruction"); + + let result = IxUtils::reconstruct_system_instruction( + &solana_parsed, + &IxUtils::build_account_keys_hashmap(&account_keys), + ); + + assert!(result.is_ok()); + let compiled = result.unwrap(); + assert_eq!(compiled.program_id_index, 0); + assert_eq!(compiled.accounts, vec![1, 2]); // account, base indices + assert_eq!(compiled.data, instruction.data); + } + + #[test] + fn test_reconstruct_system_withdraw_nonce_account_instruction() { + let nonce_account = Pubkey::new_unique(); + let recipient = Pubkey::new_unique(); + let nonce_authority = Pubkey::new_unique(); + let system_program_id = SYSTEM_PROGRAM_ID; + let account_keys = vec![system_program_id, nonce_account, recipient, nonce_authority]; + let lamports = 1500000u64; + + let instruction = solana_sdk::system_instruction::withdraw_nonce_account( + &nonce_account, + &nonce_authority, + &recipient, + lamports, + ); + + let solana_parsed = create_parsed_system_withdraw_nonce_account( + &nonce_account, + &nonce_authority, + &recipient, + lamports, + ) + .expect("Failed to create parsed instruction"); + + let result = IxUtils::reconstruct_system_instruction( + &solana_parsed, + &IxUtils::build_account_keys_hashmap(&account_keys), + ); + + assert!(result.is_ok()); + let compiled = result.unwrap(); + assert_eq!(compiled.program_id_index, 0); + assert_eq!(compiled.accounts, vec![1, 2, 3]); // nonce_account, recipient, nonce_authority indices + assert_eq!(compiled.data, instruction.data); + } + + #[test] + fn test_reconstruct_spl_token_transfer_instruction() { + let source = Pubkey::new_unique(); + let destination = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + let token_program_id = spl_token::ID; + let account_keys = vec![token_program_id, source, destination, authority]; + let amount = 1000000u64; + + let transfer_instruction = spl_token::instruction::transfer( + &spl_token::ID, + &source, + &destination, + &authority, + &[], + amount, + ) + .expect("Failed to create transfer instruction"); + + let solana_parsed_transfer = + create_parsed_spl_token_transfer(&source, &destination, &authority, amount) + .expect("Failed to create parsed instruction"); + + let result = IxUtils::reconstruct_spl_token_instruction( + &solana_parsed_transfer, + &IxUtils::build_account_keys_hashmap(&account_keys), + ); + + assert!(result.is_ok()); + let compiled = result.unwrap(); + assert_eq!(compiled.program_id_index, 0); + assert_eq!(compiled.accounts, vec![1, 2, 3]); // source, destination, authority indices + assert_eq!(compiled.data, transfer_instruction.data); + } + + #[test] + fn test_reconstruct_spl_token_transfer_checked_instruction() { + let source = Pubkey::new_unique(); + let destination = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let token_program_id = spl_token::ID; + let account_keys = vec![token_program_id, source, mint, destination, authority]; + let amount = 2000000u64; + let decimals = 6u8; + + let instruction = spl_token::instruction::transfer_checked( + &spl_token::ID, + &source, + &mint, + &destination, + &authority, + &[], + amount, + decimals, + ) + .expect("Failed to create transfer_checked instruction"); + + let solana_parsed = create_parsed_spl_token_transfer_checked( + &source, + &mint, + &destination, + &authority, + amount, + decimals, + ) + .expect("Failed to create parsed instruction"); + + let result = IxUtils::reconstruct_spl_token_instruction( + &solana_parsed, + &IxUtils::build_account_keys_hashmap(&account_keys), + ); + + assert!(result.is_ok()); + let compiled = result.unwrap(); + assert_eq!(compiled.program_id_index, 0); + assert_eq!(compiled.accounts, vec![1, 2, 3, 4]); // source, mint, destination, authority indices + assert_eq!(compiled.data, instruction.data); + } + + #[test] + fn test_reconstruct_spl_token_burn_instruction() { + let account = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + let token_program_id = spl_token::ID; + let account_keys = vec![token_program_id, account, mint, authority]; + let amount = 500000u64; + + let instruction = + spl_token::instruction::burn(&spl_token::ID, &account, &mint, &authority, &[], amount) + .expect("Failed to create burn instruction"); + + let solana_parsed = create_parsed_spl_token_burn(&account, &mint, &authority, amount) + .expect("Failed to create parsed instruction"); + + let result = IxUtils::reconstruct_spl_token_instruction( + &solana_parsed, + &IxUtils::build_account_keys_hashmap(&account_keys), + ); + + assert!(result.is_ok()); + let compiled = result.unwrap(); + assert_eq!(compiled.program_id_index, 0); + assert_eq!(compiled.accounts, vec![1, 3]); // account, authority indices (mint at index 2 is skipped) + assert_eq!(compiled.data, instruction.data); + } + + #[test] + fn test_reconstruct_spl_token_burn_checked_instruction() { + let account = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let token_program_id = spl_token::ID; + let account_keys = vec![token_program_id, account, mint, authority]; + let amount = 750000u64; + let decimals = 6u8; + + let instruction = spl_token::instruction::burn_checked( + &spl_token::ID, + &account, + &mint, + &authority, + &[], + amount, + decimals, + ) + .expect("Failed to create burn_checked instruction"); + + let solana_parsed = + create_parsed_spl_token_burn_checked(&account, &mint, &authority, amount, decimals) + .expect("Failed to create parsed instruction"); + + let result = IxUtils::reconstruct_spl_token_instruction( + &solana_parsed, + &IxUtils::build_account_keys_hashmap(&account_keys), + ); + + assert!(result.is_ok()); + let compiled = result.unwrap(); + assert_eq!(compiled.program_id_index, 0); + assert_eq!(compiled.accounts, vec![1, 2, 3]); // account, mint, authority indices + assert_eq!(compiled.data, instruction.data); + } + + #[test] + fn test_reconstruct_spl_token_close_account_instruction() { + let account = Pubkey::new_unique(); + let destination = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + let token_program_id = spl_token::ID; + let account_keys = vec![token_program_id, account, destination, authority]; + + let instruction = spl_token::instruction::close_account( + &spl_token::ID, + &account, + &destination, + &authority, + &[], + ) + .expect("Failed to create close_account instruction"); + + let solana_parsed = + create_parsed_spl_token_close_account(&account, &destination, &authority) + .expect("Failed to create parsed instruction"); + + let result = IxUtils::reconstruct_spl_token_instruction( + &solana_parsed, + &IxUtils::build_account_keys_hashmap(&account_keys), + ); + + assert!(result.is_ok()); + let compiled = result.unwrap(); + assert_eq!(compiled.program_id_index, 0); + assert_eq!(compiled.accounts, vec![1, 2, 3]); // account, destination, authority indices + assert_eq!(compiled.data, instruction.data); + } + + #[test] + fn test_reconstruct_spl_token_approve_instruction() { + let source = Pubkey::new_unique(); + let delegate = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + let token_program_id = spl_token::ID; + let account_keys = vec![token_program_id, source, delegate, owner]; + let amount = 1000000u64; + + let instruction = spl_token::instruction::approve( + &spl_token::ID, + &source, + &delegate, + &owner, + &[], + amount, + ) + .expect("Failed to create approve instruction"); + + let solana_parsed = create_parsed_spl_token_approve(&source, &delegate, &owner, amount) + .expect("Failed to create parsed instruction"); + + let result = IxUtils::reconstruct_spl_token_instruction( + &solana_parsed, + &IxUtils::build_account_keys_hashmap(&account_keys), + ); + + assert!(result.is_ok()); + let compiled = result.unwrap(); + assert_eq!(compiled.program_id_index, 0); + assert_eq!(compiled.accounts, vec![1, 2, 3]); // source, delegate, owner indices + assert_eq!(compiled.data, instruction.data); + } + + #[test] + fn test_reconstruct_spl_token_approve_checked_instruction() { + let source = Pubkey::new_unique(); + let delegate = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let token_program_id = spl_token::ID; + let account_keys = vec![token_program_id, source, mint, delegate, owner]; + let amount = 2500000u64; + let decimals = 6u8; + + let instruction = spl_token::instruction::approve_checked( + &spl_token::ID, + &source, + &mint, + &delegate, + &owner, + &[], + amount, + decimals, + ) + .expect("Failed to create approve_checked instruction"); + + let solana_parsed = create_parsed_spl_token_approve_checked( + &source, &mint, &delegate, &owner, amount, decimals, + ) + .expect("Failed to create parsed instruction"); + + let result = IxUtils::reconstruct_spl_token_instruction( + &solana_parsed, + &IxUtils::build_account_keys_hashmap(&account_keys), + ); + + assert!(result.is_ok()); + let compiled = result.unwrap(); + assert_eq!(compiled.program_id_index, 0); + assert_eq!(compiled.accounts, vec![1, 2, 3, 4]); // source, mint, delegate, owner indices + assert_eq!(compiled.data, instruction.data); + } + + #[test] + fn test_reconstruct_token2022_transfer_instruction() { + let source = Pubkey::new_unique(); + let destination = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + let token_program_id = spl_token_2022::ID; + let account_keys = vec![token_program_id, source, destination, authority]; + let amount = 1500000u64; + + #[allow(deprecated)] + let instruction = spl_token_2022::instruction::transfer( + &spl_token_2022::ID, + &source, + &destination, + &authority, + &[], + amount, + ) + .expect("Failed to create transfer instruction"); + + let solana_parsed = + create_parsed_token2022_transfer(&source, &destination, &authority, amount) + .expect("Failed to create parsed instruction"); + + let result = IxUtils::reconstruct_spl_token_instruction( + &solana_parsed, + &IxUtils::build_account_keys_hashmap(&account_keys), + ); + + assert!(result.is_ok()); + let compiled = result.unwrap(); + assert_eq!(compiled.program_id_index, 0); + assert_eq!(compiled.accounts, vec![1, 2, 3]); // source, destination, authority indices + assert_eq!(compiled.data, instruction.data); + } + + #[test] + fn test_reconstruct_token2022_transfer_checked_instruction() { + let source = Pubkey::new_unique(); + let destination = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let token_program_id = spl_token_2022::ID; + let account_keys = vec![token_program_id, source, mint, destination, authority]; + let amount = 3000000u64; + let decimals = 6u8; + + let instruction = spl_token_2022::instruction::transfer_checked( + &spl_token_2022::ID, + &source, + &mint, + &destination, + &authority, + &[], + amount, + decimals, + ) + .expect("Failed to create transfer_checked instruction"); + + let solana_parsed = create_parsed_token2022_transfer_checked( + &source, + &mint, + &destination, + &authority, + amount, + decimals, + ) + .expect("Failed to create parsed instruction"); + + let result = IxUtils::reconstruct_spl_token_instruction( + &solana_parsed, + &IxUtils::build_account_keys_hashmap(&account_keys), + ); + + assert!(result.is_ok()); + let compiled = result.unwrap(); + assert_eq!(compiled.program_id_index, 0); + assert_eq!(compiled.accounts, vec![1, 2, 3, 4]); // source, mint, destination, authority indices + assert_eq!(compiled.data, instruction.data); + } + + #[test] + fn test_reconstruct_token2022_burn_instruction() { + let account = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + let token_program_id = spl_token_2022::ID; + let account_keys = vec![token_program_id, account, mint, authority]; + let amount = 800000u64; + + let instruction = spl_token_2022::instruction::burn( + &spl_token_2022::ID, + &account, + &mint, + &authority, + &[], + amount, + ) + .expect("Failed to create burn instruction"); + + let solana_parsed = create_parsed_token2022_burn(&account, &mint, &authority, amount) + .expect("Failed to create parsed instruction"); + + let result = IxUtils::reconstruct_spl_token_instruction( + &solana_parsed, + &IxUtils::build_account_keys_hashmap(&account_keys), + ); + + assert!(result.is_ok()); + let compiled = result.unwrap(); + assert_eq!(compiled.program_id_index, 0); + assert_eq!(compiled.accounts, vec![1, 3]); // account, authority indices (mint at index 2 is skipped) + assert_eq!(compiled.data, instruction.data); + } + + #[test] + fn test_reconstruct_token2022_burn_checked_instruction() { + let account = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let token_program_id = spl_token_2022::ID; + let account_keys = vec![token_program_id, account, mint, authority]; + let amount = 900000u64; + let decimals = 6u8; + + let instruction = spl_token_2022::instruction::burn_checked( + &spl_token_2022::ID, + &account, + &mint, + &authority, + &[], + amount, + decimals, + ) + .expect("Failed to create burn_checked instruction"); + + let solana_parsed = + create_parsed_token2022_burn_checked(&account, &mint, &authority, amount, decimals) + .expect("Failed to create parsed instruction"); + + let result = IxUtils::reconstruct_spl_token_instruction( + &solana_parsed, + &IxUtils::build_account_keys_hashmap(&account_keys), + ); + + assert!(result.is_ok()); + let compiled = result.unwrap(); + assert_eq!(compiled.program_id_index, 0); + assert_eq!(compiled.accounts, vec![1, 2, 3]); // account, mint, authority indices + assert_eq!(compiled.data, instruction.data); + } + + #[test] + fn test_reconstruct_token2022_close_account_instruction() { + let account = Pubkey::new_unique(); + let destination = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + let token_program_id = spl_token_2022::ID; + let account_keys = vec![token_program_id, account, destination, authority]; + + let instruction = spl_token_2022::instruction::close_account( + &spl_token_2022::ID, + &account, + &destination, + &authority, + &[], + ) + .expect("Failed to create close_account instruction"); + + let solana_parsed = + create_parsed_token2022_close_account(&account, &destination, &authority) + .expect("Failed to create parsed instruction"); + + let result = IxUtils::reconstruct_spl_token_instruction( + &solana_parsed, + &IxUtils::build_account_keys_hashmap(&account_keys), + ); + + assert!(result.is_ok()); + let compiled = result.unwrap(); + assert_eq!(compiled.program_id_index, 0); + assert_eq!(compiled.accounts, vec![1, 2, 3]); // account, destination, authority indices + assert_eq!(compiled.data, instruction.data); + } + + #[test] + fn test_reconstruct_token2022_approve_instruction() { + let source = Pubkey::new_unique(); + let delegate = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + let token_program_id = spl_token_2022::ID; + let account_keys = vec![token_program_id, source, delegate, owner]; + let amount = 1200000u64; + + let instruction = spl_token_2022::instruction::approve( + &spl_token_2022::ID, + &source, + &delegate, + &owner, + &[], + amount, + ) + .expect("Failed to create approve instruction"); + + let solana_parsed = create_parsed_token2022_approve(&source, &delegate, &owner, amount) + .expect("Failed to create parsed instruction"); + + let result = IxUtils::reconstruct_spl_token_instruction( + &solana_parsed, + &IxUtils::build_account_keys_hashmap(&account_keys), + ); + + assert!(result.is_ok()); + let compiled = result.unwrap(); + assert_eq!(compiled.program_id_index, 0); + assert_eq!(compiled.accounts, vec![1, 2, 3]); // source, delegate, owner indices + assert_eq!(compiled.data, instruction.data); + } + + #[test] + fn test_reconstruct_token2022_approve_checked_instruction() { + let source = Pubkey::new_unique(); + let delegate = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let token_program_id = spl_token_2022::ID; + let account_keys = vec![token_program_id, source, mint, delegate, owner]; + let amount = 3500000u64; + let decimals = 6u8; + + let instruction = spl_token_2022::instruction::approve_checked( + &spl_token_2022::ID, + &source, + &mint, + &delegate, + &owner, + &[], + amount, + decimals, + ) + .expect("Failed to create approve_checked instruction"); + + let solana_parsed = create_parsed_token2022_approve_checked( + &source, &mint, &delegate, &owner, amount, decimals, + ) + .expect("Failed to create parsed instruction"); + + let result = IxUtils::reconstruct_spl_token_instruction( + &solana_parsed, + &IxUtils::build_account_keys_hashmap(&account_keys), + ); + + assert!(result.is_ok()); + let compiled = result.unwrap(); + assert_eq!(compiled.program_id_index, 0); + assert_eq!(compiled.accounts, vec![1, 2, 3, 4]); // source, mint, delegate, owner indices + assert_eq!(compiled.data, instruction.data); + } + + #[test] + fn test_reconstruct_unsupported_program_creates_stub() { + let unsupported_program = Pubkey::new_unique(); + let account_keys = vec![unsupported_program]; + + let parsed_instruction = solana_transaction_status_client_types::ParsedInstruction { + program: "unsupported".to_string(), + program_id: unsupported_program.to_string(), + parsed: serde_json::json!({ + "type": "unknownInstruction", + "info": { + "someField": "someValue" + } + }), + stack_height: None, + }; + + let ui_instruction = UiInstruction::Parsed(UiParsedInstruction::Parsed(parsed_instruction)); + + let result = IxUtils::reconstruct_instruction_from_ui(&ui_instruction, &account_keys); + + assert!(result.is_some()); + let compiled = result.unwrap(); + + assert_eq!(compiled.program_id_index, 0); + assert!(compiled.accounts.is_empty()); + assert!(compiled.data.is_empty()); + } } diff --git a/crates/lib/src/transaction/versioned_transaction.rs b/crates/lib/src/transaction/versioned_transaction.rs index c5480c60..a92e984e 100644 --- a/crates/lib/src/transaction/versioned_transaction.rs +++ b/crates/lib/src/transaction/versioned_transaction.rs @@ -10,7 +10,7 @@ use solana_sdk::{ }; use std::{collections::HashMap, ops::Deref}; -use solana_transaction_status_client_types::UiInstruction; +use solana_transaction_status_client_types::{UiInstruction, UiTransactionEncoding}; use crate::{ error::KoraError, @@ -148,7 +148,11 @@ impl VersionedTransactionResolved { RpcSimulateTransactionConfig { commitment: Some(rpc_client.commitment()), sig_verify, - ..Default::default() + inner_instructions: true, + replace_recent_blockhash: false, + encoding: Some(UiTransactionEncoding::Base64), + accounts: None, + min_context_slot: None, }, ) .await @@ -164,14 +168,22 @@ impl VersionedTransactionResolved { let mut compiled_inner_instructions: Vec = vec![]; inner_instructions.iter().for_each(|ix| { - ix.instructions.iter().for_each(|inner_ix| { - if let UiInstruction::Compiled(ix) = inner_ix { + ix.instructions.iter().for_each(|inner_ix| match inner_ix { + UiInstruction::Compiled(ix) => { compiled_inner_instructions.push(CompiledInstruction { program_id_index: ix.program_id_index, accounts: ix.accounts.clone(), data: bs58::decode(&ix.data).into_vec().unwrap_or_default(), }); } + UiInstruction::Parsed(ui_parsed) => { + if let Some(compiled) = IxUtils::reconstruct_instruction_from_ui( + &UiInstruction::Parsed(ui_parsed.clone()), + &self.all_account_keys, + ) { + compiled_inner_instructions.push(compiled); + } + } }); }); diff --git a/tests/rpc/fee_estimation.rs b/tests/rpc/fee_estimation.rs index 2b7483dd..2cf96306 100644 --- a/tests/rpc/fee_estimation.rs +++ b/tests/rpc/fee_estimation.rs @@ -1,6 +1,6 @@ use crate::common::*; use jsonrpsee::rpc_params; -use solana_sdk::signer::Signer; +use solana_sdk::{program_pack::Pack, pubkey::Pubkey, signature::Keypair, signer::Signer}; #[tokio::test] async fn test_estimate_transaction_fee_legacy() { @@ -273,3 +273,75 @@ async fn test_estimate_transaction_fee_with_fee_token_v0_with_lookup() { assert_eq!(fee_in_lamports, 10050, "Fee in lamports should be 10050"); assert_eq!(fee_in_token, 10050.0, "Fee in token should be 10050"); } + +/// Comprehensive test covering all fee scenarios: ATA creation, manual token accounts, +/// SPL token operations, compute budget, and priority fees +#[tokio::test] +async fn test_estimate_fee_comprehensive_with_token_accounts_creation() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + + let sender = SenderTestHelper::get_test_sender_keypair(); + let fee_payer = FeePayerTestHelper::get_fee_payer_pubkey(); + let usdc_mint = USDCMintTestHelper::get_test_usdc_mint_pubkey(); + let usdc_mint_2022 = USDCMint2022TestHelper::get_test_usdc_mint_2022_pubkey(); + + let recipient1_needs_ata = Pubkey::new_unique(); + let recipient1_2022_needs_ata = Pubkey::new_unique(); + + // Manual token accounts (non-ATA) + let manual_spl_account1 = Keypair::new(); + + // Get rent for token accounts + let token_account_rent = ctx + .client + .rpc_client + .get_minimum_balance_for_rent_exemption(spl_token::state::Account::LEN) + .await + .expect("Failed to get rent exemption amount"); + + // Build the comprehensive transaction + let test_tx = ctx + .transaction_builder() + .with_fee_payer(fee_payer) + .with_create_ata(&usdc_mint, &recipient1_needs_ata) + .with_create_token2022_ata(&usdc_mint_2022, &recipient1_2022_needs_ata) + .with_create_and_init_token_account( + &manual_spl_account1, + &usdc_mint, + &sender.pubkey(), + token_account_rent, + ) + .build() + .await + .expect("Failed to build transaction"); + + let response: serde_json::Value = ctx + .rpc_call("estimateTransactionFee", rpc_params![test_tx.clone()]) + .await + .expect("Failed to estimate transaction fee"); + + response.assert_success(); + response.assert_has_field("fee_in_lamports"); + + let fee_lamports = response["fee_in_lamports"].as_u64().unwrap(); + + // Expected fee breakdown: + // - Base fee: ~5000 lamports (for signatures) + // - ATA creation rent: 2_039_280 + 2_039_280 = 4_078_560 lamports (2 ATAs) + // - Manual token account rent: 2_157_600 lamports (1 manual account, as shown in debug logs) + let expected_minimum_fee = 5_000 + 4_078_560 + 2_157_600; + + assert!( + fee_lamports >= expected_minimum_fee, + "Fee should include all account creations. Got {}, expected at least {}", + fee_lamports, + expected_minimum_fee + ); + + assert!( + fee_lamports < expected_minimum_fee + 50_000, + "Fee shouldn't be excessively high. Got {}, expected max {}", + fee_lamports, + expected_minimum_fee + 50_000 + ); +} diff --git a/tests/src/bin/test_runner.rs b/tests/src/bin/test_runner.rs index 1786e0b9..613139a1 100644 --- a/tests/src/bin/test_runner.rs +++ b/tests/src/bin/test_runner.rs @@ -319,6 +319,7 @@ pub async fn run_test_phase( signers_config, &cached_keys, preferred_port, + verbose, ) .await { diff --git a/tests/src/common/fixtures/kora-test.toml b/tests/src/common/fixtures/kora-test.toml index 804865a8..e692419e 100644 --- a/tests/src/common/fixtures/kora-test.toml +++ b/tests/src/common/fixtures/kora-test.toml @@ -32,6 +32,7 @@ allowed_programs = [ "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb", # Token-2022 Program "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", # Associated Token Program "AddressLookupTab1e1111111111111111111111111", # Address Lookup Table Program + "Bcdikjss8HWzKEuj6gEQoFq9TCnGnk6v3kUnRU1gb6hA", # Custom Transfer Hook Program Example ] allowed_tokens = [ "9BgeTKqmFsPVnfYscfM6NvsgmZxei7XfdciShQ6D3bxJ", # Test USDC mint for local testing @@ -70,4 +71,4 @@ allow_approve = true enabled = false cache_url = "redis://redis:6379" max_transactions = 2 -fallback_if_unavailable = false \ No newline at end of file +fallback_if_unavailable = false diff --git a/tests/src/common/transaction.rs b/tests/src/common/transaction.rs index 35ec10d1..6457ebb4 100644 --- a/tests/src/common/transaction.rs +++ b/tests/src/common/transaction.rs @@ -13,12 +13,13 @@ use solana_sdk::{ commitment_config::CommitmentConfig, compute_budget::ComputeBudgetInstruction, instruction::Instruction, + program_pack::Pack, pubkey::Pubkey, signature::{Keypair, Signature}, signer::Signer, transaction::VersionedTransaction, }; -use solana_system_interface::instruction::transfer; +use solana_system_interface::instruction::{create_account, transfer}; use spl_associated_token_account::{ get_associated_token_address, get_associated_token_address_with_program_id, }; @@ -274,6 +275,64 @@ impl TransactionBuilder { self } + /// Add ATA creation instruction for SPL Token + pub fn with_create_ata(mut self, mint: &Pubkey, owner: &Pubkey) -> Self { + let instruction = + spl_associated_token_account::instruction::create_associated_token_account( + &self.fee_payer.expect("Fee payer must be set before creating ATA"), + owner, + mint, + &spl_token::id(), + ); + self.instructions.push(instruction); + self + } + + /// Add ATA creation instruction for Token2022 + pub fn with_create_token2022_ata(mut self, mint: &Pubkey, owner: &Pubkey) -> Self { + let instruction = + spl_associated_token_account::instruction::create_associated_token_account( + &self.fee_payer.expect("Fee payer must be set before creating ATA"), + owner, + mint, + &spl_token_2022::id(), + ); + self.instructions.push(instruction); + self + } + + /// Add manual token account creation and initialization for SPL Token + pub fn with_create_and_init_token_account( + mut self, + account: &Keypair, + mint: &Pubkey, + owner: &Pubkey, + rent_lamports: u64, + ) -> Self { + // Create account instruction + let create_instruction = create_account( + &self.fee_payer.expect("Fee payer must be set"), + &account.pubkey(), + rent_lamports, + spl_token::state::Account::LEN as u64, + &spl_token::id(), + ); + + // Initialize account instruction + let init_instruction = spl_token::instruction::initialize_account3( + &spl_token::id(), + &account.pubkey(), + mint, + owner, + ) + .expect("Failed to create initialize account instruction"); + + self.instructions.push(create_instruction); + self.instructions.push(init_instruction); + self.signers.push(account.insecure_clone()); + self + } + /// Build the transaction and return as base64-encoded string pub async fn build(self) -> Result { let rpc_client = diff --git a/tests/src/test_runner/kora.rs b/tests/src/test_runner/kora.rs index 080c8ee0..922101ae 100644 --- a/tests/src/test_runner/kora.rs +++ b/tests/src/test_runner/kora.rs @@ -58,6 +58,7 @@ pub async fn start_kora_rpc_server( signers_config: &str, cached_keys: &std::collections::HashMap, preferred_port: u16, + verbose: bool, ) -> Result<(Child, u16), Box> { let fee_payer_key = cached_keys.get(&AccountFile::FeePayer).ok_or("FeePayer key not found in cache")?; @@ -73,6 +74,12 @@ pub async fn start_kora_rpc_server( }; let kora_binary_path = get_kora_binary_path().await?; + let (std_out, std_err) = if verbose { + (std::process::Stdio::inherit(), std::process::Stdio::inherit()) + } else { + (std::process::Stdio::null(), std::process::Stdio::null()) + }; + let kora_pid = tokio::process::Command::new(kora_binary_path) .args([ "--config", @@ -88,8 +95,8 @@ pub async fn start_kora_rpc_server( ]) .env("KORA_PRIVATE_KEY", fee_payer_key.trim()) .env("KORA_PRIVATE_KEY_2", signer_2.trim()) - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) + .stdout(std_out) + .stderr(std_err) .spawn()?; Ok((kora_pid, port)) From 8d4f172825b15aa364a7010c3e63e89b27603175 Mon Sep 17 00:00:00 2001 From: jo <17280917+dev-jodee@users.noreply.github.com> Date: Thu, 9 Oct 2025 13:59:20 -0400 Subject: [PATCH 11/29] feat: (PRO-412) Cap max body size per request to prevent potential overloading of the server (#230) --- crates/lib/src/config.rs | 31 ++++++++++++++++++-- crates/lib/src/constant.rs | 3 ++ crates/lib/src/rpc_server/server.rs | 1 + crates/lib/src/tests/config_mock.rs | 3 ++ crates/lib/src/tests/toml_mock.rs | 11 +++++++ crates/lib/src/validator/config_validator.rs | 2 ++ tests/adversarial/body_size_limit.rs | 29 ++++++++++++++++++ tests/adversarial/main.rs | 2 ++ 8 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 tests/adversarial/body_size_limit.rs diff --git a/crates/lib/src/config.rs b/crates/lib/src/config.rs index 79e4f90a..9f75cf4a 100644 --- a/crates/lib/src/config.rs +++ b/crates/lib/src/config.rs @@ -8,9 +8,10 @@ use utoipa::ToSchema; use crate::{ constant::{ DEFAULT_CACHE_ACCOUNT_TTL, DEFAULT_CACHE_DEFAULT_TTL, - DEFAULT_FEE_PAYER_BALANCE_METRICS_EXPIRY_SECONDS, DEFAULT_MAX_TIMESTAMP_AGE, - DEFAULT_METRICS_ENDPOINT, DEFAULT_METRICS_PORT, DEFAULT_METRICS_SCRAPE_INTERVAL, - DEFAULT_USAGE_LIMIT_FALLBACK_IF_UNAVAILABLE, DEFAULT_USAGE_LIMIT_MAX_TRANSACTIONS, + DEFAULT_FEE_PAYER_BALANCE_METRICS_EXPIRY_SECONDS, DEFAULT_MAX_REQUEST_BODY_SIZE, + DEFAULT_MAX_TIMESTAMP_AGE, DEFAULT_METRICS_ENDPOINT, DEFAULT_METRICS_PORT, + DEFAULT_METRICS_SCRAPE_INTERVAL, DEFAULT_USAGE_LIMIT_FALLBACK_IF_UNAVAILABLE, + DEFAULT_USAGE_LIMIT_MAX_TRANSACTIONS, }, error::KoraError, fee::price::{PriceConfig, PriceModel}, @@ -302,6 +303,10 @@ fn default_max_timestamp_age() -> i64 { DEFAULT_MAX_TIMESTAMP_AGE } +fn default_max_request_body_size() -> usize { + DEFAULT_MAX_REQUEST_BODY_SIZE +} + #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct CacheConfig { /// Redis URL for caching (e.g., "redis://localhost:6379") @@ -328,6 +333,8 @@ impl Default for CacheConfig { #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct KoraConfig { pub rate_limit: u64, + #[serde(default = "default_max_request_body_size")] + pub max_request_body_size: usize, #[serde(default)] pub enabled_methods: EnabledMethods, #[serde(default)] @@ -344,6 +351,7 @@ impl Default for KoraConfig { fn default() -> Self { Self { rate_limit: 100, + max_request_body_size: DEFAULT_MAX_REQUEST_BODY_SIZE, enabled_methods: EnabledMethods::default(), auth: AuthConfig::default(), payment_address: None, @@ -721,4 +729,21 @@ mod tests { assert!(config.kora.usage_limit.enabled); assert_eq!(config.kora.usage_limit.max_transactions, 0); // 0 = unlimited } + + #[test] + fn test_max_request_body_size_default() { + let config = ConfigBuilder::new().build_config().unwrap(); + + assert_eq!(config.kora.max_request_body_size, DEFAULT_MAX_REQUEST_BODY_SIZE); + assert_eq!(config.kora.max_request_body_size, 2 * 1024 * 1024); // 2 MB + } + + #[test] + fn test_max_request_body_size_custom() { + let custom_size = 10 * 1024 * 1024; // 10 MB + let config = + ConfigBuilder::new().with_max_request_body_size(custom_size).build_config().unwrap(); + + assert_eq!(config.kora.max_request_body_size, custom_size); + } } diff --git a/crates/lib/src/constant.rs b/crates/lib/src/constant.rs index 06fb78f0..a2267dff 100644 --- a/crates/lib/src/constant.rs +++ b/crates/lib/src/constant.rs @@ -28,6 +28,9 @@ pub const DEFAULT_FEE_PAYER_BALANCE_METRICS_EXPIRY_SECONDS: u64 = 30; // 30 seco pub const DEFAULT_USAGE_LIMIT_MAX_TRANSACTIONS: u64 = 0; // 0 = unlimited pub const DEFAULT_USAGE_LIMIT_FALLBACK_IF_UNAVAILABLE: bool = false; +// Request body size limit +pub const DEFAULT_MAX_REQUEST_BODY_SIZE: usize = 2 * 1024 * 1024; // 2 MB + // Account Indexes within instructions // Instruction indexes for the instructions that we support to parse from the transaction pub mod instruction_indexes { diff --git a/crates/lib/src/rpc_server/server.rs b/crates/lib/src/rpc_server/server.rs index 20bd06be..4e359f0b 100644 --- a/crates/lib/src/rpc_server/server.rs +++ b/crates/lib/src/rpc_server/server.rs @@ -89,6 +89,7 @@ pub async fn run_rpc_server(rpc: KoraRpc, port: u16) -> Result, enabled_methods: Option, cache_config: Option, usage_limit_config: Option, @@ -61,6 +62,7 @@ impl Default for KoraSection { fn default() -> Self { Self { rate_limit: 100, + max_request_body_size: None, enabled_methods: None, cache_config: None, usage_limit_config: None, @@ -180,6 +182,11 @@ impl ConfigBuilder { self } + pub fn with_max_request_body_size(mut self, size: usize) -> Self { + self.kora.max_request_body_size = Some(size); + self + } + pub fn build_toml(&self) -> String { let programs_list = self .validation @@ -247,6 +254,10 @@ impl ConfigBuilder { toml.push_str(&format!("[kora]\nrate_limit = {}\n", self.kora.rate_limit)); + if let Some(size) = self.kora.max_request_body_size { + toml.push_str(&format!("max_request_body_size = {size}\n")); + } + if let Some(ref methods_config) = self.kora.enabled_methods { toml.push_str(&format!("{methods_config}\n")); } diff --git a/crates/lib/src/validator/config_validator.rs b/crates/lib/src/validator/config_validator.rs index fbfeee1b..ba6e6378 100644 --- a/crates/lib/src/validator/config_validator.rs +++ b/crates/lib/src/validator/config_validator.rs @@ -346,6 +346,7 @@ mod tests { AuthConfig, CacheConfig, Config, EnabledMethods, FeePayerPolicy, KoraConfig, MetricsConfig, SplTokenConfig, UsageLimitConfig, ValidationConfig, }, + constant::DEFAULT_MAX_REQUEST_BODY_SIZE, fee::price::PriceConfig, state::update_config, tests::common::{ @@ -451,6 +452,7 @@ mod tests { }, kora: KoraConfig { rate_limit: 0, // Should warn + max_request_body_size: DEFAULT_MAX_REQUEST_BODY_SIZE, enabled_methods: EnabledMethods { liveness: false, estimate_transaction_fee: false, diff --git a/tests/adversarial/body_size_limit.rs b/tests/adversarial/body_size_limit.rs new file mode 100644 index 00000000..f5d5f8a1 --- /dev/null +++ b/tests/adversarial/body_size_limit.rs @@ -0,0 +1,29 @@ +use crate::common::*; +use serde_json::json; + +/// Test that oversized request bodies are rejected with 413 Payload Too Large +#[tokio::test] +async fn test_request_body_oversized_rejected() { + // Create a very large JSON payload (4 MB, exceeds 2 MB limit) + let large_string = "x".repeat(4 * 1024 * 1024); + + let request_body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "signTransaction", + "params": { + "transaction": large_string, + } + }); + + let client = reqwest::Client::new(); + let response = + client.post(TestClient::get_default_server_url()).json(&request_body).send().await; + + // Should get 413 Payload Too Large + assert_eq!( + response.unwrap().status(), + reqwest::StatusCode::PAYLOAD_TOO_LARGE, + "Oversized request should return 413 Payload Too Large" + ); +} diff --git a/tests/adversarial/main.rs b/tests/adversarial/main.rs index 3c384c39..5fb63646 100644 --- a/tests/adversarial/main.rs +++ b/tests/adversarial/main.rs @@ -5,7 +5,9 @@ // - Program validation attacks (disallowed programs) // - Invalid token states (frozen) // - Fee payer exploitation +// - Request body size limit (DDOS protection) +mod body_size_limit; mod fee_payer_exploitation; mod program_validation; mod token_states; From 99af3863729319f9016e0bed4f0129be71841559 Mon Sep 17 00:00:00 2001 From: jo <17280917+dev-jodee@users.noreply.github.com> Date: Fri, 10 Oct 2025 08:03:54 -0400 Subject: [PATCH 12/29] feat: (PRO-407) Use solana-signers crate instead of custom Kora Signer (#231) --- Cargo.lock | 171 ++++--- Cargo.toml | 2 +- crates/lib/Cargo.toml | 3 + crates/lib/src/admin/token_util.rs | 31 +- crates/lib/src/error.rs | 10 +- crates/lib/src/metrics/balance.rs | 17 +- crates/lib/src/mod.rs | 2 +- .../method/estimate_transaction_fee.rs | 5 +- .../src/rpc_server/method/get_payer_signer.rs | 3 +- .../method/sign_and_send_transaction.rs | 3 +- .../src/rpc_server/method/sign_transaction.rs | 3 +- .../method/sign_transaction_if_paid.rs | 3 +- .../rpc_server/method/transfer_transaction.rs | 11 +- crates/lib/src/signer/config.rs | 206 ++++++-- crates/lib/src/signer/config_trait.rs | 14 - crates/lib/src/signer/memory_signer/config.rs | 114 ----- crates/lib/src/signer/memory_signer/mod.rs | 2 - .../src/signer/memory_signer/solana_signer.rs | 208 -------- crates/lib/src/signer/mod.rs | 16 +- crates/lib/src/signer/pool.rs | 41 +- crates/lib/src/signer/privy/config.rs | 190 -------- crates/lib/src/signer/privy/mod.rs | 3 - crates/lib/src/signer/privy/signer.rs | 383 --------------- crates/lib/src/signer/privy/types.rs | 148 ------ crates/lib/src/signer/signer.rs | 155 +----- crates/lib/src/signer/turnkey/config.rs | 272 ----------- crates/lib/src/signer/turnkey/mod.rs | 3 - crates/lib/src/signer/turnkey/signer.rs | 449 ------------------ crates/lib/src/signer/turnkey/types.rs | 133 ------ crates/lib/src/signer/vault/config.rs | 224 --------- crates/lib/src/signer/vault/mod.rs | 2 - crates/lib/src/signer/vault/vault_signer.rs | 156 ------ crates/lib/src/state.rs | 12 +- crates/lib/src/tests/common/mod.rs | 14 +- crates/lib/src/tests/config_mock.rs | 18 +- .../src/transaction/versioned_transaction.rs | 31 +- crates/lib/src/usage_limit/usage_tracker.rs | 3 +- crates/lib/src/validator/signer_validator.rs | 5 +- 38 files changed, 407 insertions(+), 2659 deletions(-) delete mode 100644 crates/lib/src/signer/config_trait.rs delete mode 100644 crates/lib/src/signer/memory_signer/config.rs delete mode 100644 crates/lib/src/signer/memory_signer/mod.rs delete mode 100644 crates/lib/src/signer/memory_signer/solana_signer.rs delete mode 100644 crates/lib/src/signer/privy/config.rs delete mode 100644 crates/lib/src/signer/privy/mod.rs delete mode 100644 crates/lib/src/signer/privy/signer.rs delete mode 100644 crates/lib/src/signer/privy/types.rs delete mode 100644 crates/lib/src/signer/turnkey/config.rs delete mode 100644 crates/lib/src/signer/turnkey/mod.rs delete mode 100644 crates/lib/src/signer/turnkey/signer.rs delete mode 100644 crates/lib/src/signer/turnkey/types.rs delete mode 100644 crates/lib/src/signer/vault/config.rs delete mode 100644 crates/lib/src/signer/vault/mod.rs delete mode 100644 crates/lib/src/signer/vault/vault_signer.rs diff --git a/Cargo.lock b/Cargo.lock index c41815ba..ecbd9d4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -136,12 +136,6 @@ dependencies = [ "alloc-no-stdlib", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -458,9 +452,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.88" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", @@ -898,16 +892,15 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -3032,6 +3025,7 @@ dependencies = [ "solana-message", "solana-program 2.3.0", "solana-sdk 2.3.1", + "solana-signers", "solana-system-interface", "solana-transaction-status", "solana-transaction-status-client-types", @@ -3151,9 +3145,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "lru-slab" @@ -3725,7 +3719,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" dependencies = [ "memchr", - "thiserror 2.0.14", + "thiserror 2.0.17", "ucd-trie", ] @@ -3969,7 +3963,7 @@ dependencies = [ "memchr", "parking_lot", "protobuf", - "thiserror 2.0.14", + "thiserror 2.0.17", ] [[package]] @@ -4030,7 +4024,7 @@ dependencies = [ "rustc-hash 2.1.1", "rustls 0.23.31", "socket2 0.5.10", - "thiserror 2.0.14", + "thiserror 2.0.17", "tokio", "tracing", "web-time", @@ -4053,7 +4047,7 @@ dependencies = [ "rustls-pki-types", "rustls-platform-verifier", "slab", - "thiserror 2.0.14", + "thiserror 2.0.17", "tinyvec", "tracing", "web-time", @@ -4259,7 +4253,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror 2.0.14", + "thiserror 2.0.17", ] [[package]] @@ -4788,10 +4782,11 @@ checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ + "serde_core", "serde_derive", ] @@ -4813,11 +4808,20 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -4826,14 +4830,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.142" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] @@ -5155,7 +5160,7 @@ dependencies = [ "spl-token-2022 8.0.1", "spl-token-group-interface 0.6.0", "spl-token-metadata-interface 0.7.0", - "thiserror 2.0.14", + "thiserror 2.0.17", "zstd", ] @@ -5260,7 +5265,7 @@ dependencies = [ "ark-serialize", "bytemuck", "solana-define-syscall", - "thiserror 2.0.14", + "thiserror 2.0.17", ] [[package]] @@ -5315,7 +5320,7 @@ dependencies = [ "solana-transaction", "solana-transaction-error", "solana-udp-client", - "thiserror 2.0.14", + "thiserror 2.0.17", "tokio", ] @@ -5419,7 +5424,7 @@ dependencies = [ "solana-metrics", "solana-time-utils", "solana-transaction-error", - "thiserror 2.0.14", + "thiserror 2.0.17", "tokio", ] @@ -5448,7 +5453,7 @@ dependencies = [ "curve25519-dalek 4.1.3", "solana-define-syscall", "subtle", - "thiserror 2.0.14", + "thiserror 2.0.17", ] [[package]] @@ -5558,7 +5563,7 @@ dependencies = [ "solana-pubkey", "solana-sdk-ids", "solana-system-interface", - "thiserror 2.0.14", + "thiserror 2.0.17", ] [[package]] @@ -5905,7 +5910,7 @@ dependencies = [ "solana-cluster-type", "solana-sha256-hasher", "solana-time-utils", - "thiserror 2.0.14", + "thiserror 2.0.17", ] [[package]] @@ -6198,7 +6203,7 @@ dependencies = [ "solana-sysvar", "solana-sysvar-id", "solana-vote-interface", - "thiserror 2.0.14", + "thiserror 2.0.17", "wasm-bindgen", ] @@ -6300,7 +6305,7 @@ dependencies = [ "solana-pubkey", "solana-rpc-client-types", "solana-signature", - "thiserror 2.0.14", + "thiserror 2.0.17", "tokio", "tokio-stream", "tokio-tungstenite", @@ -6334,7 +6339,7 @@ dependencies = [ "solana-streamer", "solana-tls-utils", "solana-transaction-error", - "thiserror 2.0.14", + "thiserror 2.0.17", "tokio", ] @@ -6477,7 +6482,7 @@ dependencies = [ "solana-signer", "solana-transaction-error", "solana-transaction-status-client-types", - "thiserror 2.0.14", + "thiserror 2.0.17", ] [[package]] @@ -6494,7 +6499,7 @@ dependencies = [ "solana-pubkey", "solana-rpc-client", "solana-sdk-ids", - "thiserror 2.0.14", + "thiserror 2.0.17", ] [[package]] @@ -6520,7 +6525,7 @@ dependencies = [ "solana-transaction-status-client-types", "solana-version", "spl-generic-token", - "thiserror 2.0.14", + "thiserror 2.0.17", ] [[package]] @@ -6647,7 +6652,7 @@ dependencies = [ "solana-transaction-context", "solana-transaction-error", "solana-validator-exit", - "thiserror 2.0.14", + "thiserror 2.0.17", "wasm-bindgen", ] @@ -6713,7 +6718,7 @@ dependencies = [ "borsh 1.5.7", "libsecp256k1", "solana-define-syscall", - "thiserror 2.0.14", + "thiserror 2.0.17", ] [[package]] @@ -6842,6 +6847,28 @@ dependencies = [ "solana-transaction-error", ] +[[package]] +name = "solana-signers" +version = "0.1.0" +source = "git+https://github.com/solana-foundation/solana-signers#285df1ca312a479282703c3b63af14fb50244f9d" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bincode", + "bs58 0.5.1", + "chrono", + "hex", + "log", + "p256", + "reqwest 0.12.23", + "serde", + "serde_json", + "solana-sdk 2.3.1", + "thiserror 2.0.17", + "tokio", + "vaultrs", +] + [[package]] name = "solana-slot-hashes" version = "2.2.1" @@ -6940,7 +6967,7 @@ dependencies = [ "solana-tls-utils", "solana-transaction-error", "solana-transaction-metrics-tracker", - "thiserror 2.0.14", + "thiserror 2.0.17", "tokio", "tokio-util", "x509-parser", @@ -7108,7 +7135,7 @@ dependencies = [ "solana-signer", "solana-transaction", "solana-transaction-error", - "thiserror 2.0.14", + "thiserror 2.0.17", "tokio", ] @@ -7225,7 +7252,7 @@ dependencies = [ "spl-token-2022 8.0.1", "spl-token-group-interface 0.6.0", "spl-token-metadata-interface 0.7.0", - "thiserror 2.0.14", + "thiserror 2.0.17", ] [[package]] @@ -7248,7 +7275,7 @@ dependencies = [ "solana-transaction", "solana-transaction-context", "solana-transaction-error", - "thiserror 2.0.14", + "thiserror 2.0.17", ] [[package]] @@ -7263,7 +7290,7 @@ dependencies = [ "solana-net-utils", "solana-streamer", "solana-transaction-error", - "thiserror 2.0.14", + "thiserror 2.0.17", "tokio", ] @@ -7343,7 +7370,7 @@ dependencies = [ "solana-signature", "solana-signer", "subtle", - "thiserror 2.0.14", + "thiserror 2.0.17", "wasm-bindgen", "zeroize", ] @@ -7396,7 +7423,7 @@ dependencies = [ "spl-associated-token-account-client", "spl-token 8.0.0", "spl-token-2022 8.0.1", - "thiserror 2.0.14", + "thiserror 2.0.17", ] [[package]] @@ -7522,7 +7549,7 @@ dependencies = [ "solana-program-option", "solana-pubkey", "solana-zk-sdk", - "thiserror 2.0.14", + "thiserror 2.0.17", ] [[package]] @@ -7550,7 +7577,7 @@ dependencies = [ "solana-msg", "solana-program-error", "spl-program-error-derive 0.5.0", - "thiserror 2.0.14", + "thiserror 2.0.17", ] [[package]] @@ -7618,7 +7645,7 @@ dependencies = [ "spl-pod", "spl-program-error 0.7.0", "spl-type-length-value 0.8.0", - "thiserror 2.0.14", + "thiserror 2.0.17", ] [[package]] @@ -7661,7 +7688,7 @@ dependencies = [ "solana-rent", "solana-sdk-ids", "solana-sysvar", - "thiserror 2.0.14", + "thiserror 2.0.17", ] [[package]] @@ -7733,7 +7760,7 @@ dependencies = [ "spl-token-metadata-interface 0.7.0", "spl-transfer-hook-interface 0.10.0", "spl-type-length-value 0.8.0", - "thiserror 2.0.14", + "thiserror 2.0.17", ] [[package]] @@ -7771,7 +7798,7 @@ dependencies = [ "solana-program 2.3.0", "solana-zk-sdk", "spl-pod", - "thiserror 2.0.14", + "thiserror 2.0.17", ] [[package]] @@ -7791,7 +7818,7 @@ dependencies = [ "solana-sdk-ids", "solana-zk-sdk", "spl-pod", - "thiserror 2.0.14", + "thiserror 2.0.17", ] [[package]] @@ -7813,7 +7840,7 @@ checksum = "fa27b9174bea869a7ebf31e0be6890bce90b1a4288bc2bbf24bd413f80ae3fde" dependencies = [ "curve25519-dalek 4.1.3", "solana-zk-sdk", - "thiserror 2.0.14", + "thiserror 2.0.17", ] [[package]] @@ -7851,7 +7878,7 @@ dependencies = [ "solana-pubkey", "spl-discriminator", "spl-pod", - "thiserror 2.0.14", + "thiserror 2.0.17", ] [[package]] @@ -7893,7 +7920,7 @@ dependencies = [ "spl-discriminator", "spl-pod", "spl-type-length-value 0.8.0", - "thiserror 2.0.14", + "thiserror 2.0.17", ] [[package]] @@ -7943,7 +7970,7 @@ dependencies = [ "spl-program-error 0.7.0", "spl-tlv-account-resolution 0.10.0", "spl-type-length-value 0.8.0", - "thiserror 2.0.14", + "thiserror 2.0.17", ] [[package]] @@ -7979,7 +8006,7 @@ dependencies = [ "solana-program-error", "spl-discriminator", "spl-pod", - "thiserror 2.0.14", + "thiserror 2.0.17", ] [[package]] @@ -8184,11 +8211,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.14" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.14", + "thiserror-impl 2.0.17", ] [[package]] @@ -8204,9 +8231,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.14" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", @@ -9031,7 +9058,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-link", + "windows-link 0.1.3", "windows-result", "windows-strings", ] @@ -9064,13 +9091,19 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-registry" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ - "windows-link", + "windows-link 0.1.3", "windows-result", "windows-strings", ] @@ -9081,7 +9114,7 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -9090,7 +9123,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -9190,7 +9223,7 @@ version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ - "windows-link", + "windows-link 0.1.3", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", diff --git a/Cargo.toml b/Cargo.toml index 6026728e..b6663870 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,7 +50,7 @@ borsh = "1.5.3" async-std = "1.13.0" jsonrpsee-core = { version = "0.16.2", features = ["server"] } env_logger = "0.11.5" -async-trait = "0.1.83" +async-trait = "0.1.89" base64 = "0.22.1" log = "0.4.22" bytes = "1.9.0" diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml index 74c0fb80..a855fcd7 100644 --- a/crates/lib/Cargo.toml +++ b/crates/lib/Cargo.toml @@ -41,6 +41,9 @@ jup-ag = { workspace = true } spl-token = { workspace = true } spl-token-2022 = { workspace = true } spl-associated-token-account = { workspace = true } +solana-signers = { git = "https://github.com/solana-foundation/solana-signers", features = [ + "all", +] } vaultrs = { workspace = true } deadpool-redis = { workspace = true } jsonrpsee = { workspace = true } diff --git a/crates/lib/src/admin/token_util.rs b/crates/lib/src/admin/token_util.rs index ec361794..c7f5ea3a 100644 --- a/crates/lib/src/admin/token_util.rs +++ b/crates/lib/src/admin/token_util.rs @@ -1,6 +1,5 @@ use crate::{ error::KoraError, - signer::{KoraSigner, Signer}, state::{get_all_signers, get_request_signer_with_signer_key}, token::token::TokenType, transaction::TransactionUtil, @@ -9,8 +8,8 @@ use solana_client::nonblocking::rpc_client::RpcClient; use solana_message::{Message, VersionedMessage}; use solana_sdk::{ compute_budget::ComputeBudgetInstruction, instruction::Instruction, pubkey::Pubkey, - signature::Signature, }; +use solana_signers::SolanaSigner; use spl_associated_token_account::{ get_associated_token_address, instruction::create_associated_token_account, @@ -65,10 +64,7 @@ pub async fn initialize_atas( vec![Pubkey::from_str(payment_address) .map_err(|e| KoraError::InternalServerError(format!("Invalid payment address: {e}")))?] } else { - get_all_signers()? - .iter() - .map(|signer| signer.signer.solana_pubkey()) - .collect::>() + get_all_signers()?.iter().map(|signer| signer.signer.pubkey()).collect::>() }; initialize_atas_with_chunk_size( @@ -86,7 +82,7 @@ pub async fn initialize_atas( /// This function does not use cache and directly checks on-chain pub async fn initialize_atas_with_chunk_size( rpc_client: &RpcClient, - fee_payer: &Arc, + fee_payer: &Arc, addresses_to_initialize_atas: &Vec, compute_unit_price: Option, compute_unit_limit: Option, @@ -122,7 +118,7 @@ pub async fn initialize_atas_with_chunk_size( /// Helper function to create ATAs for a single signer async fn create_atas_for_signer( rpc_client: &RpcClient, - fee_payer: &Arc, + fee_payer: &Arc, address: &Pubkey, atas_to_create: &[ATAToCreate], compute_unit_price: Option, @@ -133,7 +129,7 @@ async fn create_atas_for_signer( .iter() .map(|ata| { create_associated_token_account( - &fee_payer.solana_pubkey(), + &fee_payer.pubkey(), address, &ata.mint, &ata.token_program, @@ -177,22 +173,21 @@ async fn create_atas_for_signer( .await .map_err(|e| KoraError::RpcError(format!("Failed to get blockhash: {e}")))?; + let fee_payer_pubkey = fee_payer.pubkey(); let message = VersionedMessage::Legacy(Message::new_with_blockhash( &chunk_instructions, - Some(&fee_payer.solana_pubkey()), + Some(&fee_payer_pubkey), &blockhash, )); let mut tx = TransactionUtil::new_unsigned_versioned_transaction(message); - let signature = fee_payer.sign(&tx).await?; - - let sig_bytes: [u8; 64] = signature - .bytes - .try_into() - .map_err(|_| KoraError::SigningError("Invalid signature length".to_string()))?; + let message_bytes = tx.message.serialize(); + let signature = fee_payer + .sign_message(&message_bytes) + .await + .map_err(|e| KoraError::SigningError(e.to_string()))?; - let sig = Signature::from(sig_bytes); - tx.signatures = vec![sig]; + tx.signatures = vec![signature]; match rpc_client.send_and_confirm_transaction_with_spinner(&tx).await { Ok(signature) => { diff --git a/crates/lib/src/error.rs b/crates/lib/src/error.rs index e5ab698f..4a55079f 100644 --- a/crates/lib/src/error.rs +++ b/crates/lib/src/error.rs @@ -196,14 +196,8 @@ impl From for KoraError { } } -impl From for KoraError { - fn from(err: crate::signer::privy::types::PrivyError) -> Self { - KoraError::SigningError(err.to_string()) - } -} - -impl From for KoraError { - fn from(err: crate::signer::turnkey::types::TurnkeyError) -> Self { +impl From for KoraError { + fn from(err: solana_signers::SignerError) -> Self { KoraError::SigningError(err.to_string()) } } diff --git a/crates/lib/src/metrics/balance.rs b/crates/lib/src/metrics/balance.rs index 9ad7ece8..2d6db7ee 100644 --- a/crates/lib/src/metrics/balance.rs +++ b/crates/lib/src/metrics/balance.rs @@ -150,10 +150,7 @@ mod tests { use super::*; use crate::{ config::FeePayerBalanceMetricsConfig, - signer::{ - memory_signer::solana_signer::SolanaMemorySigner, KoraSigner, SignerPool, - SignerWithMetadata, - }, + signer::{SignerPool, SignerWithMetadata}, state::update_signer_pool, tests::{ account_mock::create_mock_account_with_balance, @@ -162,14 +159,18 @@ mod tests { }, }; use solana_sdk::signature::Keypair; + use solana_signers::Signer; fn setup_test_signer_pool() { - let signer1 = SolanaMemorySigner::new(Keypair::new()); - let signer2 = SolanaMemorySigner::new(Keypair::new()); + let keypair1 = Keypair::new(); + let keypair2 = Keypair::new(); + + let external_signer1 = Signer::from_memory(&keypair1.to_base58_string()).unwrap(); + let external_signer2 = Signer::from_memory(&keypair2.to_base58_string()).unwrap(); let pool = SignerPool::new(vec![ - SignerWithMetadata::new("test_signer_1".to_string(), KoraSigner::Memory(signer1), 1), - SignerWithMetadata::new("test_signer_2".to_string(), KoraSigner::Memory(signer2), 1), + SignerWithMetadata::new("signer_1".to_string(), Arc::new(external_signer1), 1), + SignerWithMetadata::new("signer_2".to_string(), Arc::new(external_signer2), 2), ]); let _ = update_signer_pool(pool); diff --git a/crates/lib/src/mod.rs b/crates/lib/src/mod.rs index f438ea2b..dc688bf2 100644 --- a/crates/lib/src/mod.rs +++ b/crates/lib/src/mod.rs @@ -22,7 +22,7 @@ pub mod validator; pub use cache::CacheUtil; pub use config::Config; pub use error::KoraError; -pub use signer::{Signature, Signer}; +pub use signer::SolanaSigner; pub use state::{get_all_signers, get_request_signer_with_signer_key}; #[cfg(test)] diff --git a/crates/lib/src/rpc_server/method/estimate_transaction_fee.rs b/crates/lib/src/rpc_server/method/estimate_transaction_fee.rs index a2c3c23f..ab32e39d 100644 --- a/crates/lib/src/rpc_server/method/estimate_transaction_fee.rs +++ b/crates/lib/src/rpc_server/method/estimate_transaction_fee.rs @@ -1,3 +1,4 @@ +use solana_signers::SolanaSigner; use std::sync::Arc; use utoipa::ToSchema; @@ -49,10 +50,10 @@ pub async fn estimate_transaction_fee( let signer = get_request_signer_with_signer_key(request.signer_key.as_deref())?; let config = get_config()?; - let payment_destination = config.kora.get_payment_address(&signer.solana_pubkey())?; + let payment_destination = config.kora.get_payment_address(&signer.pubkey())?; let validation_config = &config.validation; - let fee_payer = signer.solana_pubkey(); + let fee_payer = signer.pubkey(); let mut resolved_transaction = VersionedTransactionResolved::from_transaction( &transaction, diff --git a/crates/lib/src/rpc_server/method/get_payer_signer.rs b/crates/lib/src/rpc_server/method/get_payer_signer.rs index f927c21a..5ab67723 100644 --- a/crates/lib/src/rpc_server/method/get_payer_signer.rs +++ b/crates/lib/src/rpc_server/method/get_payer_signer.rs @@ -3,6 +3,7 @@ use crate::{ state::{get_config, get_signer_pool}, }; use serde::{Deserialize, Serialize}; +use solana_signers::SolanaSigner; use utoipa::ToSchema; #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] @@ -19,7 +20,7 @@ pub async fn get_payer_signer() -> Result { // Get the next signer according to the configured strategy let signer_meta = pool.get_next_signer()?; - let signer_pubkey = signer_meta.signer.solana_pubkey(); + let signer_pubkey = signer_meta.signer.pubkey(); // Get the payment destination address (falls back to signer if no payment address is configured) let payment_destination = config.kora.get_payment_address(&signer_pubkey)?; diff --git a/crates/lib/src/rpc_server/method/sign_and_send_transaction.rs b/crates/lib/src/rpc_server/method/sign_and_send_transaction.rs index 1655e186..530e75e6 100644 --- a/crates/lib/src/rpc_server/method/sign_and_send_transaction.rs +++ b/crates/lib/src/rpc_server/method/sign_and_send_transaction.rs @@ -1,6 +1,7 @@ use crate::{rpc_server::middleware_utils::default_sig_verify, usage_limit::UsageTracker}; use serde::{Deserialize, Serialize}; use solana_client::nonblocking::rpc_client::RpcClient; +use solana_signers::SolanaSigner; use std::sync::Arc; use utoipa::ToSchema; @@ -51,7 +52,7 @@ pub async fn sign_and_send_transaction( Ok(SignAndSendTransactionResponse { signed_transaction, - signer_pubkey: signer.solana_pubkey().to_string(), + signer_pubkey: signer.pubkey().to_string(), }) } diff --git a/crates/lib/src/rpc_server/method/sign_transaction.rs b/crates/lib/src/rpc_server/method/sign_transaction.rs index 7903eb6f..74d15cbc 100644 --- a/crates/lib/src/rpc_server/method/sign_transaction.rs +++ b/crates/lib/src/rpc_server/method/sign_transaction.rs @@ -7,6 +7,7 @@ use crate::{ }; use serde::{Deserialize, Serialize}; use solana_client::nonblocking::rpc_client::RpcClient; +use solana_signers::SolanaSigner; use std::sync::Arc; use utoipa::ToSchema; @@ -53,7 +54,7 @@ pub async fn sign_transaction( Ok(SignTransactionResponse { signed_transaction: encoded, - signer_pubkey: signer.solana_pubkey().to_string(), + signer_pubkey: signer.pubkey().to_string(), }) } diff --git a/crates/lib/src/rpc_server/method/sign_transaction_if_paid.rs b/crates/lib/src/rpc_server/method/sign_transaction_if_paid.rs index 4bac1b72..f71ef017 100644 --- a/crates/lib/src/rpc_server/method/sign_transaction_if_paid.rs +++ b/crates/lib/src/rpc_server/method/sign_transaction_if_paid.rs @@ -7,6 +7,7 @@ use crate::{ }; use serde::{Deserialize, Serialize}; use solana_client::nonblocking::rpc_client::RpcClient; +use solana_signers::SolanaSigner; use std::sync::Arc; use utoipa::ToSchema; @@ -55,7 +56,7 @@ pub async fn sign_transaction_if_paid( Ok(SignTransactionIfPaidResponse { transaction: TransactionUtil::encode_versioned_transaction(&transaction), signed_transaction, - signer_pubkey: signer.solana_pubkey().to_string(), + signer_pubkey: signer.pubkey().to_string(), }) } diff --git a/crates/lib/src/rpc_server/method/transfer_transaction.rs b/crates/lib/src/rpc_server/method/transfer_transaction.rs index 30fa9d09..033a45ef 100644 --- a/crates/lib/src/rpc_server/method/transfer_transaction.rs +++ b/crates/lib/src/rpc_server/method/transfer_transaction.rs @@ -3,6 +3,7 @@ use solana_client::nonblocking::rpc_client::RpcClient; use solana_commitment_config::CommitmentConfig; use solana_message::Message; use solana_sdk::{message::VersionedMessage, pubkey::Pubkey}; +use solana_signers::SolanaSigner; use solana_system_interface::instruction::transfer; use std::{str::FromStr, sync::Arc}; use utoipa::ToSchema; @@ -14,7 +15,7 @@ use crate::{ TransactionUtil, VersionedMessageExt, VersionedTransactionOps, VersionedTransactionResolved, }, validator::transaction_validator::TransactionValidator, - CacheUtil, KoraError, Signer as _, + CacheUtil, KoraError, }; #[derive(Debug, Deserialize, ToSchema)] @@ -42,7 +43,7 @@ pub async fn transfer_transaction( request: TransferTransactionRequest, ) -> Result { let signer = get_request_signer_with_signer_key(request.signer_key.as_deref())?; - let fee_payer = signer.solana_pubkey(); + let fee_payer = signer.pubkey(); let validator = TransactionValidator::new(fee_payer)?; @@ -130,7 +131,11 @@ pub async fn transfer_transaction( // Find the fee payer position in the account keys let fee_payer_position = resolved_transaction.find_signer_position(&fee_payer)?; - let signature = signer.sign_solana(&resolved_transaction).await?; + let message_bytes = resolved_transaction.transaction.message.serialize(); + let signature = signer + .sign_message(&message_bytes) + .await + .map_err(|e| KoraError::SigningError(e.to_string()))?; resolved_transaction.transaction.signatures[fee_payer_position] = signature; diff --git a/crates/lib/src/signer/config.rs b/crates/lib/src/signer/config.rs index 1bb9182f..c52f28dc 100644 --- a/crates/lib/src/signer/config.rs +++ b/crates/lib/src/signer/config.rs @@ -1,15 +1,6 @@ -use crate::{ - error::KoraError, - signer::{ - config_trait::SignerConfigTrait, - memory_signer::config::{MemorySignerConfig, MemorySignerHandler}, - privy::config::{PrivySignerConfig, PrivySignerHandler}, - turnkey::config::{TurnkeySignerConfig, TurnkeySignerHandler}, - vault::config::{VaultSignerConfig, VaultSignerHandler}, - KoraSigner, - }, -}; +use crate::{error::KoraError, signer::utils::get_env_var_for_signer}; use serde::{Deserialize, Serialize}; +use solana_signers::Signer; use std::{fmt, fs, path::Path}; /// Configuration for a pool of signers @@ -66,6 +57,39 @@ pub struct SignerConfig { pub config: SignerTypeConfig, } +/// Memory signer configuration (local keypair) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MemorySignerConfig { + pub private_key_env: String, +} + +/// Turnkey signer configuration +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TurnkeySignerConfig { + pub api_public_key_env: String, + pub api_private_key_env: String, + pub organization_id_env: String, + pub private_key_id_env: String, + pub public_key_env: String, +} + +/// Privy signer configuration +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PrivySignerConfig { + pub app_id_env: String, + pub app_secret_env: String, + pub wallet_id_env: String, +} + +/// Vault signer configuration +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VaultSignerConfig { + pub vault_addr_env: String, + pub vault_token_env: String, + pub key_name_env: String, + pub pubkey_env: String, +} + /// Signer type-specific configuration #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] @@ -110,16 +134,13 @@ impl SignerPoolConfig { /// Validate the signer pool configuration pub fn validate_signer_config(&self) -> Result<(), KoraError> { - // Validate that at least one signer is configured self.validate_signer_not_empty()?; - // Validate each signer configuration for (index, signer) in self.signers.iter().enumerate() { signer.validate_individual_signer_config(index)?; } self.validate_signer_names()?; - self.validate_strategy_weights()?; Ok(()) @@ -165,24 +186,83 @@ impl SignerPoolConfig { } impl SignerConfig { - /// Build a KoraSigner from configuration by resolving environment variables - pub async fn build_signer_from_config(config: &SignerConfig) -> Result { + /// Build an external signer from configuration by resolving environment variables + pub async fn build_signer_from_config(config: &SignerConfig) -> Result { match &config.config { SignerTypeConfig::Memory { config: memory_config } => { - MemorySignerHandler::build_from_config(memory_config, &config.name) + Self::build_memory_signer(memory_config, &config.name) } SignerTypeConfig::Turnkey { config: turnkey_config } => { - TurnkeySignerHandler::build_from_config(turnkey_config, &config.name) + Self::build_turnkey_signer(turnkey_config, &config.name) } SignerTypeConfig::Privy { config: privy_config } => { - PrivySignerHandler::build_from_config(privy_config, &config.name) + Self::build_privy_signer(privy_config, &config.name).await } SignerTypeConfig::Vault { config: vault_config } => { - VaultSignerHandler::build_from_config(vault_config, &config.name) + Self::build_vault_signer(vault_config, &config.name) } } } + fn build_memory_signer( + config: &MemorySignerConfig, + signer_name: &str, + ) -> Result { + let private_key = get_env_var_for_signer(&config.private_key_env, signer_name)?; + Signer::from_memory(&private_key).map_err(|e| { + KoraError::SigningError(format!("Failed to create memory signer '{signer_name}': {e}")) + }) + } + + fn build_turnkey_signer( + config: &TurnkeySignerConfig, + signer_name: &str, + ) -> Result { + let api_public_key = get_env_var_for_signer(&config.api_public_key_env, signer_name)?; + let api_private_key = get_env_var_for_signer(&config.api_private_key_env, signer_name)?; + let organization_id = get_env_var_for_signer(&config.organization_id_env, signer_name)?; + let private_key_id = get_env_var_for_signer(&config.private_key_id_env, signer_name)?; + let public_key = get_env_var_for_signer(&config.public_key_env, signer_name)?; + + Signer::from_turnkey( + api_public_key, + api_private_key, + organization_id, + private_key_id, + public_key, + ) + .map_err(|e| { + KoraError::SigningError(format!("Failed to create Turnkey signer '{signer_name}': {e}")) + }) + } + + async fn build_privy_signer( + config: &PrivySignerConfig, + signer_name: &str, + ) -> Result { + let app_id = get_env_var_for_signer(&config.app_id_env, signer_name)?; + let app_secret = get_env_var_for_signer(&config.app_secret_env, signer_name)?; + let wallet_id = get_env_var_for_signer(&config.wallet_id_env, signer_name)?; + + Signer::from_privy(app_id, app_secret, wallet_id).await.map_err(|e| { + KoraError::SigningError(format!("Failed to create Privy signer '{signer_name}': {e}")) + }) + } + + fn build_vault_signer( + config: &VaultSignerConfig, + signer_name: &str, + ) -> Result { + let vault_addr = get_env_var_for_signer(&config.vault_addr_env, signer_name)?; + let vault_token = get_env_var_for_signer(&config.vault_token_env, signer_name)?; + let key_name = get_env_var_for_signer(&config.key_name_env, signer_name)?; + let pubkey = get_env_var_for_signer(&config.pubkey_env, signer_name)?; + + Signer::from_vault(vault_addr, vault_token, key_name, pubkey).map_err(|e| { + KoraError::SigningError(format!("Failed to create Vault signer '{signer_name}': {e}")) + }) + } + /// Validate an individual signer configuration pub fn validate_individual_signer_config(&self, index: usize) -> Result<(), KoraError> { if self.name.is_empty() { @@ -191,21 +271,89 @@ impl SignerConfig { ))); } - // Delegate validation to signer-specific handlers match &self.config { - SignerTypeConfig::Memory { config: memory_config } => { - MemorySignerHandler::validate_config(memory_config, &self.name) + SignerTypeConfig::Memory { config } => Self::validate_memory_config(config, &self.name), + SignerTypeConfig::Turnkey { config } => { + Self::validate_turnkey_config(config, &self.name) } - SignerTypeConfig::Turnkey { config: turnkey_config } => { - TurnkeySignerHandler::validate_config(turnkey_config, &self.name) + SignerTypeConfig::Privy { config } => Self::validate_privy_config(config, &self.name), + SignerTypeConfig::Vault { config } => Self::validate_vault_config(config, &self.name), + } + } + + fn validate_memory_config( + config: &MemorySignerConfig, + signer_name: &str, + ) -> Result<(), KoraError> { + if config.private_key_env.is_empty() { + return Err(KoraError::ValidationError(format!( + "Memory signer '{signer_name}' must specify non-empty private_key_env" + ))); + } + Ok(()) + } + + fn validate_turnkey_config( + config: &TurnkeySignerConfig, + signer_name: &str, + ) -> Result<(), KoraError> { + let env_vars = [ + ("api_public_key_env", &config.api_public_key_env), + ("api_private_key_env", &config.api_private_key_env), + ("organization_id_env", &config.organization_id_env), + ("private_key_id_env", &config.private_key_id_env), + ("public_key_env", &config.public_key_env), + ]; + + for (field_name, env_var) in env_vars { + if env_var.is_empty() { + return Err(KoraError::ValidationError(format!( + "Turnkey signer '{signer_name}' must specify non-empty {field_name}" + ))); } - SignerTypeConfig::Privy { config: privy_config } => { - PrivySignerHandler::validate_config(privy_config, &self.name) + } + Ok(()) + } + + fn validate_privy_config( + config: &PrivySignerConfig, + signer_name: &str, + ) -> Result<(), KoraError> { + let env_vars = [ + ("app_id_env", &config.app_id_env), + ("app_secret_env", &config.app_secret_env), + ("wallet_id_env", &config.wallet_id_env), + ]; + + for (field_name, env_var) in env_vars { + if env_var.is_empty() { + return Err(KoraError::ValidationError(format!( + "Privy signer '{signer_name}' must specify non-empty {field_name}" + ))); } - SignerTypeConfig::Vault { config: vault_config } => { - VaultSignerHandler::validate_config(vault_config, &self.name) + } + Ok(()) + } + + fn validate_vault_config( + config: &VaultSignerConfig, + signer_name: &str, + ) -> Result<(), KoraError> { + let env_vars = [ + ("vault_addr_env", &config.vault_addr_env), + ("vault_token_env", &config.vault_token_env), + ("key_name_env", &config.key_name_env), + ("pubkey_env", &config.pubkey_env), + ]; + + for (field_name, env_var) in env_vars { + if env_var.is_empty() { + return Err(KoraError::ValidationError(format!( + "Vault signer '{signer_name}' must specify non-empty {field_name}" + ))); } } + Ok(()) } } diff --git a/crates/lib/src/signer/config_trait.rs b/crates/lib/src/signer/config_trait.rs deleted file mode 100644 index f99e759f..00000000 --- a/crates/lib/src/signer/config_trait.rs +++ /dev/null @@ -1,14 +0,0 @@ -use crate::{error::KoraError, signer::KoraSigner}; - -/// Trait for signer configuration validation and building -pub trait SignerConfigTrait { - /// Configuration type for this signer - type Config; - - /// Validate the configuration before building the signer - fn validate_config(config: &Self::Config, signer_name: &str) -> Result<(), KoraError>; - - /// Build a signer instance from the validated configuration - fn build_from_config(config: &Self::Config, signer_name: &str) - -> Result; -} diff --git a/crates/lib/src/signer/memory_signer/config.rs b/crates/lib/src/signer/memory_signer/config.rs deleted file mode 100644 index d78eee69..00000000 --- a/crates/lib/src/signer/memory_signer/config.rs +++ /dev/null @@ -1,114 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use crate::{ - error::KoraError, - signer::{ - config_trait::SignerConfigTrait, memory_signer::solana_signer::SolanaMemorySigner, - utils::get_env_var_for_signer, KoraSigner, - }, -}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MemorySignerConfig { - pub private_key_env: String, -} - -/// Handler for memory signer configuration -pub struct MemorySignerHandler; - -impl SignerConfigTrait for MemorySignerHandler { - type Config = MemorySignerConfig; - - fn validate_config(config: &Self::Config, signer_name: &str) -> Result<(), KoraError> { - if config.private_key_env.is_empty() { - return Err(KoraError::ValidationError(format!( - "Memory signer '{signer_name}' must specify non-empty private_key_env" - ))); - } - Ok(()) - } - - fn build_from_config( - config: &Self::Config, - signer_name: &str, - ) -> Result { - let private_key = get_env_var_for_signer(&config.private_key_env, signer_name)?; - let signer = SolanaMemorySigner::from_private_key_string(&private_key).map_err(|e| { - KoraError::ValidationError(format!( - "Failed to create memory signer '{signer_name}': {e}" - )) - })?; - Ok(KoraSigner::Memory(signer)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::tests::config_mock::ConfigMockBuilder; - use std::env; - - #[test] - fn test_validate_config_valid() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - let config = MemorySignerConfig { private_key_env: "VALID_ENV_VAR".to_string() }; - - let result = MemorySignerHandler::validate_config(&config, "test_signer"); - assert!(result.is_ok()); - } - - #[test] - fn test_validate_config_empty_env_var() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - let config = MemorySignerConfig { private_key_env: "".to_string() }; - - let result = MemorySignerHandler::validate_config(&config, "test_signer"); - assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), KoraError::ValidationError(_))); - } - - #[test] - fn test_build_from_config_missing_env_var() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - env::remove_var("NONEXISTENT_ENV_VAR"); - let config = MemorySignerConfig { private_key_env: "NONEXISTENT_ENV_VAR".to_string() }; - - let result = MemorySignerHandler::build_from_config(&config, "test_signer"); - assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), KoraError::ValidationError(_))); - } - - #[test] - fn test_build_from_config_invalid_private_key() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - env::set_var("INVALID_KEY_ENV", "not_a_valid_key"); - let config = MemorySignerConfig { private_key_env: "INVALID_KEY_ENV".to_string() }; - - let result = MemorySignerHandler::build_from_config(&config, "test_signer"); - assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), KoraError::ValidationError(_))); - - env::remove_var("INVALID_KEY_ENV"); - } - - #[test] - fn test_build_from_config_valid_private_key() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - // Use a valid base58-encoded private key for testing - let test_private_key = "5MaiiCavjCmn9Hs1o3eznqDEhRwxo7pXiAYez7keQUviUkauRiTMD8DrESdrNjN8zd9mTmVhRvBJeg5vhyvgrAhG"; - env::set_var("VALID_PRIVATE_KEY_ENV", test_private_key); - - let config = MemorySignerConfig { private_key_env: "VALID_PRIVATE_KEY_ENV".to_string() }; - - let result = MemorySignerHandler::build_from_config(&config, "test_signer"); - assert!(result.is_ok()); - assert!(matches!(result.unwrap(), KoraSigner::Memory(_))); - - env::remove_var("VALID_PRIVATE_KEY_ENV"); - } -} diff --git a/crates/lib/src/signer/memory_signer/mod.rs b/crates/lib/src/signer/memory_signer/mod.rs deleted file mode 100644 index 53ac6ff8..00000000 --- a/crates/lib/src/signer/memory_signer/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod config; -pub mod solana_signer; diff --git a/crates/lib/src/signer/memory_signer/solana_signer.rs b/crates/lib/src/signer/memory_signer/solana_signer.rs deleted file mode 100644 index 9af5c04e..00000000 --- a/crates/lib/src/signer/memory_signer/solana_signer.rs +++ /dev/null @@ -1,208 +0,0 @@ -use crate::error::KoraError; -use solana_sdk::{ - pubkey::Pubkey, - signature::{Keypair, Signature as SolanaSignature}, - signer::Signer as SolanaSigner, - transaction::VersionedTransaction, -}; - -use crate::signer::{KeypairUtil, Signature}; - -/// A Solana-based signer that uses an in-memory keypair -#[derive(Debug)] -pub struct SolanaMemorySigner { - keypair: Keypair, -} - -impl SolanaMemorySigner { - /// Creates a new signer from a Solana keypair - pub fn new(keypair: Keypair) -> Self { - Self { keypair } - } - - /// Creates a new signer from a private key byte array - pub fn from_bytes(private_key: &[u8]) -> Result { - let keypair = Keypair::try_from(private_key) - .map_err(|e| KoraError::SigningError(format!("Invalid private key bytes: {e}")))?; - Ok(Self { keypair }) - } - - /// Get the public key of this signer - pub fn pubkey(&self) -> [u8; 32] { - self.keypair.pubkey().to_bytes() - } - - /// Get solana pubkey - pub fn solana_pubkey(&self) -> Pubkey { - self.keypair.pubkey() - } - - /// Get the base58-encoded public key - pub fn pubkey_base58(&self) -> String { - bs58::encode(self.pubkey()).into_string() - } - - /// Creates a new signer from a private key string that can be in multiple formats: - /// - Base58 encoded string (current format) - /// - U8Array format: "[0, 1, 2, ...]" - /// - File path to a JSON keypair file - pub fn from_private_key_string(private_key: &str) -> Result { - let keypair = KeypairUtil::from_private_key_string(private_key)?; - Ok(Self::new(keypair)) - } -} - -impl Clone for SolanaMemorySigner { - fn clone(&self) -> Self { - Self::from_bytes(&self.keypair.to_bytes()).expect("Failed to clone keypair") - } -} - -impl SolanaMemorySigner { - pub async fn sign_solana( - &self, - transaction: &VersionedTransaction, - ) -> Result { - let solana_sig = self.keypair.sign_message(&transaction.message.serialize()); - - let sig_bytes: [u8; 64] = solana_sig - .as_ref() - .try_into() - .map_err(|_| KoraError::SigningError("Invalid signature length".to_string()))?; - - Ok(SolanaSignature::from(sig_bytes)) - } - - pub async fn sign(&self, transaction: &VersionedTransaction) -> Result { - let solana_sig = self.keypair.sign_message(&transaction.message.serialize()); - Ok(Signature { bytes: solana_sig.as_ref().to_vec(), is_partial: false }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::tests::{config_mock::ConfigMockBuilder, transaction_mock::create_mock_transaction}; - use solana_sdk::signer::Signer as SolanaSigner; - - #[test] - fn test_new_signer() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - let keypair = Keypair::new(); - let expected_pubkey = keypair.pubkey(); - let signer = SolanaMemorySigner::new(keypair); - - assert_eq!(signer.solana_pubkey(), expected_pubkey); - } - - #[test] - fn test_from_bytes_valid() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - let keypair = Keypair::new(); - let bytes = keypair.to_bytes(); - let signer = SolanaMemorySigner::from_bytes(&bytes).unwrap(); - - assert_eq!(signer.solana_pubkey(), keypair.pubkey()); - } - - #[test] - fn test_from_bytes_invalid() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - let invalid_bytes = vec![0u8; 31]; // Wrong length - let result = SolanaMemorySigner::from_bytes(&invalid_bytes); - - assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), KoraError::SigningError(_))); - } - - #[test] - fn test_pubkey_methods() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - let keypair = Keypair::new(); - let expected_pubkey = keypair.pubkey(); - let signer = SolanaMemorySigner::new(keypair); - - assert_eq!(signer.pubkey(), expected_pubkey.to_bytes()); - assert_eq!(signer.solana_pubkey(), expected_pubkey); - assert_eq!(signer.pubkey_base58(), expected_pubkey.to_string()); - } - - #[test] - fn test_from_private_key_string_base58() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - let test_private_key = "5MaiiCavjCmn9Hs1o3eznqDEhRwxo7pXiAYez7keQUviUkauRiTMD8DrESdrNjN8zd9mTmVhRvBJeg5vhyvgrAhG"; - let signer = SolanaMemorySigner::from_private_key_string(test_private_key).unwrap(); - - assert_eq!(signer.pubkey_base58(), "5pVyoAeURQHNMVU7DmfMHvCDNmTEYXWfEwc136GYhTKG"); - } - - #[test] - fn test_from_private_key_string_invalid() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - let result = SolanaMemorySigner::from_private_key_string("invalid_key"); - assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), KoraError::SigningError(_))); - } - - #[tokio::test] - async fn test_sign_solana() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - let signer = SolanaMemorySigner::new(Keypair::new()); - let transaction = create_mock_transaction(); - - let signature = signer.sign_solana(&transaction).await.unwrap(); - - // Verify signature is 64 bytes - assert_eq!(signature.as_ref().len(), 64); - } - - #[tokio::test] - async fn test_sign() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - let signer = SolanaMemorySigner::new(Keypair::new()); - let transaction = create_mock_transaction(); - - let signature = signer.sign(&transaction).await.unwrap(); - - // Verify our custom signature format - assert_eq!(signature.bytes.len(), 64); - assert!(!signature.is_partial); - } - - #[tokio::test] - async fn test_sign_produces_different_signatures_for_different_transactions() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - let signer = SolanaMemorySigner::new(Keypair::new()); - let tx1 = create_mock_transaction(); - let tx2 = create_mock_transaction(); - - let sig1 = signer.sign(&tx1).await.unwrap(); - let sig2 = signer.sign(&tx2).await.unwrap(); - - // Different transactions should produce different signatures - assert_ne!(sig1.bytes, sig2.bytes); - } - - #[tokio::test] - async fn test_sign_produces_same_signature_for_same_transaction() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - let signer = SolanaMemorySigner::new(Keypair::new()); - let transaction = create_mock_transaction(); - - let sig1 = signer.sign(&transaction).await.unwrap(); - let sig2 = signer.sign(&transaction).await.unwrap(); - - // Same transaction should produce same signature - assert_eq!(sig1.bytes, sig2.bytes); - } -} diff --git a/crates/lib/src/signer/mod.rs b/crates/lib/src/signer/mod.rs index dfbf9e26..7f3afbdd 100644 --- a/crates/lib/src/signer/mod.rs +++ b/crates/lib/src/signer/mod.rs @@ -1,20 +1,14 @@ pub mod config; -pub mod config_trait; pub mod init; pub mod keypair_util; -pub mod memory_signer; pub mod pool; -pub mod privy; pub mod signer; -pub mod turnkey; pub mod utils; -pub mod vault; -pub use config::{SelectionStrategy, SignerConfig, SignerPoolConfig, SignerTypeConfig}; +pub use config::{ + MemorySignerConfig, PrivySignerConfig, SelectionStrategy, SignerConfig, SignerPoolConfig, + SignerTypeConfig, TurnkeySignerConfig, VaultSignerConfig, +}; pub use keypair_util::KeypairUtil; -pub use memory_signer::{config::MemorySignerConfig, solana_signer::SolanaMemorySigner}; pub use pool::{SignerInfo, SignerPool, SignerWithMetadata}; -pub use privy::config::PrivySignerConfig; -pub use signer::{KoraSigner, Signature, Signer}; -pub use turnkey::config::TurnkeySignerConfig; -pub use vault::{config::VaultSignerConfig, vault_signer::VaultSigner}; +pub use signer::SolanaSigner; diff --git a/crates/lib/src/signer/pool.rs b/crates/lib/src/signer/pool.rs index 97f88fc5..a52a3b58 100644 --- a/crates/lib/src/signer/pool.rs +++ b/crates/lib/src/signer/pool.rs @@ -1,15 +1,16 @@ use crate::{ error::KoraError, - signer::{ - config::{SelectionStrategy, SignerConfig, SignerPoolConfig}, - KoraSigner, - }, + signer::config::{SelectionStrategy, SignerConfig, SignerPoolConfig}, }; use rand::Rng; use solana_sdk::pubkey::Pubkey; +use solana_signers::{Signer, SolanaSigner}; use std::{ str::FromStr, - sync::atomic::{AtomicU64, AtomicUsize, Ordering}, + sync::{ + atomic::{AtomicU64, AtomicUsize, Ordering}, + Arc, + }, }; const DEFAULT_WEIGHT: u32 = 1; @@ -19,7 +20,7 @@ pub struct SignerWithMetadata { /// Human-readable name for this signer pub name: String, /// The actual signer instance - pub signer: KoraSigner, + pub signer: Arc, /// Weight for weighted selection (higher = more likely to be selected) pub weight: u32, /// Timestamp of last use (Unix timestamp in seconds) @@ -39,7 +40,7 @@ impl Clone for SignerWithMetadata { impl SignerWithMetadata { /// Create a new signer with metadata - pub fn new(name: String, signer: KoraSigner, weight: u32) -> Self { + pub fn new(name: String, signer: Arc, weight: u32) -> Self { Self { name, signer, weight, last_used: AtomicU64::new(0) } } } @@ -92,7 +93,11 @@ impl SignerPool { let signer = SignerConfig::build_signer_from_config(&signer_config).await?; let weight = signer_config.weight.unwrap_or(DEFAULT_WEIGHT); - signers.push(SignerWithMetadata::new(signer_config.name.clone(), signer, weight)); + signers.push(SignerWithMetadata::new( + signer_config.name.clone(), + Arc::new(signer), + weight, + )); log::info!( "Successfully initialized signer: {} (weight: {})", @@ -171,7 +176,7 @@ impl SignerPool { self.signers .iter() .map(|s| SignerInfo { - public_key: s.signer.solana_pubkey().to_string(), + public_key: s.signer.pubkey().to_string(), name: s.name.clone(), weight: s.weight, last_used: s.last_used.load(Ordering::Relaxed), @@ -202,7 +207,7 @@ impl SignerPool { })?; // Find signer with matching public key - self.signers.iter().find(|s| s.signer.solana_pubkey() == target_pubkey).ok_or_else(|| { + self.signers.iter().find(|s| s.signer.pubkey() == target_pubkey).ok_or_else(|| { KoraError::ValidationError(format!("Signer with pubkey {pubkey} not found in pool")) }) } @@ -218,18 +223,22 @@ mod tests { use solana_sdk::signature::Keypair; use super::*; - use crate::signer::memory_signer::solana_signer::SolanaMemorySigner; use std::collections::HashMap; fn create_test_pool() -> SignerPool { - // Create test signers directly - let signer1 = SolanaMemorySigner::new(Keypair::new()); - let signer2 = SolanaMemorySigner::new(Keypair::new()); + // Create test signers using external signer library + let keypair1 = Keypair::new(); + let keypair2 = Keypair::new(); + + let external_signer1 = + solana_signers::Signer::from_memory(&keypair1.to_base58_string()).unwrap(); + let external_signer2 = + solana_signers::Signer::from_memory(&keypair2.to_base58_string()).unwrap(); SignerPool { signers: vec![ - SignerWithMetadata::new("signer_1".to_string(), KoraSigner::Memory(signer1), 1), - SignerWithMetadata::new("signer_2".to_string(), KoraSigner::Memory(signer2), 2), + SignerWithMetadata::new("signer_1".to_string(), Arc::new(external_signer1), 1), + SignerWithMetadata::new("signer_2".to_string(), Arc::new(external_signer2), 2), ], strategy: SelectionStrategy::RoundRobin, current_index: AtomicUsize::new(0), diff --git a/crates/lib/src/signer/privy/config.rs b/crates/lib/src/signer/privy/config.rs deleted file mode 100644 index 8c0ff23d..00000000 --- a/crates/lib/src/signer/privy/config.rs +++ /dev/null @@ -1,190 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use crate::{ - error::KoraError, - signer::{ - config_trait::SignerConfigTrait, privy::types::PrivySigner, utils::get_env_var_for_signer, - KoraSigner, - }, -}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PrivySignerConfig { - /// Environment variable for Privy app ID - pub app_id_env: String, - /// Environment variable for Privy app secret - pub app_secret_env: String, - /// Environment variable for Privy wallet ID - pub wallet_id_env: String, -} - -pub struct PrivySignerHandler; - -impl SignerConfigTrait for PrivySignerHandler { - type Config = PrivySignerConfig; - - fn validate_config(config: &Self::Config, signer_name: &str) -> Result<(), KoraError> { - let env_vars = [ - ("app_id_env", &config.app_id_env), - ("app_secret_env", &config.app_secret_env), - ("wallet_id_env", &config.wallet_id_env), - ]; - - for (field_name, env_var) in env_vars { - if env_var.is_empty() { - return Err(KoraError::ValidationError(format!( - "Privy signer '{signer_name}' must specify non-empty {field_name}" - ))); - } - } - - Ok(()) - } - - fn build_from_config( - config: &Self::Config, - signer_name: &str, - ) -> Result { - let app_id = get_env_var_for_signer(&config.app_id_env, signer_name)?; - let app_secret = get_env_var_for_signer(&config.app_secret_env, signer_name)?; - let wallet_id = get_env_var_for_signer(&config.wallet_id_env, signer_name)?; - - let signer = PrivySigner::new(app_id, app_secret, wallet_id); - - Ok(KoraSigner::Privy(signer)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::tests::config_mock::ConfigMockBuilder; - use std::env; - - #[test] - fn test_validate_config_valid() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - let config = PrivySignerConfig { - app_id_env: "VALID_APP_ID".to_string(), - app_secret_env: "VALID_APP_SECRET".to_string(), - wallet_id_env: "VALID_WALLET_ID".to_string(), - }; - - let result = PrivySignerHandler::validate_config(&config, "test_signer"); - assert!(result.is_ok()); - } - - #[test] - fn test_validate_config_empty_app_id() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - let config = PrivySignerConfig { - app_id_env: "".to_string(), - app_secret_env: "VALID_APP_SECRET".to_string(), - wallet_id_env: "VALID_WALLET_ID".to_string(), - }; - - let result = PrivySignerHandler::validate_config(&config, "test_signer"); - assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), KoraError::ValidationError(_))); - } - - #[test] - fn test_validate_config_empty_app_secret() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - let config = PrivySignerConfig { - app_id_env: "VALID_APP_ID".to_string(), - app_secret_env: "".to_string(), - wallet_id_env: "VALID_WALLET_ID".to_string(), - }; - - let result = PrivySignerHandler::validate_config(&config, "test_signer"); - assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), KoraError::ValidationError(_))); - } - - #[test] - fn test_validate_config_empty_wallet_id() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - let config = PrivySignerConfig { - app_id_env: "VALID_APP_ID".to_string(), - app_secret_env: "VALID_APP_SECRET".to_string(), - wallet_id_env: "".to_string(), - }; - - let result = PrivySignerHandler::validate_config(&config, "test_signer"); - assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), KoraError::ValidationError(_))); - } - - #[test] - fn test_build_from_config_missing_env_vars() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - // Ensure env vars don't exist - env::remove_var("NONEXISTENT_APP_ID"); - env::remove_var("NONEXISTENT_APP_SECRET"); - env::remove_var("NONEXISTENT_WALLET_ID"); - - let config = PrivySignerConfig { - app_id_env: "NONEXISTENT_APP_ID".to_string(), - app_secret_env: "NONEXISTENT_APP_SECRET".to_string(), - wallet_id_env: "NONEXISTENT_WALLET_ID".to_string(), - }; - - let result = PrivySignerHandler::build_from_config(&config, "test_signer"); - assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), KoraError::ValidationError(_))); - } - - #[test] - fn test_build_from_config_valid_env_vars() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - // Set test environment variables - env::set_var("TEST_PRIVY_APP_ID", "test_app_id"); - env::set_var("TEST_PRIVY_APP_SECRET", "test_app_secret"); - env::set_var("TEST_PRIVY_WALLET_ID", "test_wallet_id"); - - let config = PrivySignerConfig { - app_id_env: "TEST_PRIVY_APP_ID".to_string(), - app_secret_env: "TEST_PRIVY_APP_SECRET".to_string(), - wallet_id_env: "TEST_PRIVY_WALLET_ID".to_string(), - }; - - let result = PrivySignerHandler::build_from_config(&config, "test_signer"); - assert!(result.is_ok()); - assert!(matches!(result.unwrap(), KoraSigner::Privy(_))); - - // Clean up - env::remove_var("TEST_PRIVY_APP_ID"); - env::remove_var("TEST_PRIVY_APP_SECRET"); - env::remove_var("TEST_PRIVY_WALLET_ID"); - } - - #[test] - fn test_build_from_config_partial_env_vars() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - // Set only some environment variables - env::set_var("TEST_PRIVY_APP_ID_PARTIAL", "test_app_id"); - env::remove_var("MISSING_APP_SECRET"); - env::remove_var("MISSING_WALLET_ID"); - - let config = PrivySignerConfig { - app_id_env: "TEST_PRIVY_APP_ID_PARTIAL".to_string(), - app_secret_env: "MISSING_APP_SECRET".to_string(), - wallet_id_env: "MISSING_WALLET_ID".to_string(), - }; - - let result = PrivySignerHandler::build_from_config(&config, "test_signer"); - assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), KoraError::ValidationError(_))); - - // Clean up - env::remove_var("TEST_PRIVY_APP_ID_PARTIAL"); - } -} diff --git a/crates/lib/src/signer/privy/mod.rs b/crates/lib/src/signer/privy/mod.rs deleted file mode 100644 index 71777fe8..00000000 --- a/crates/lib/src/signer/privy/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod config; -pub mod signer; -pub mod types; diff --git a/crates/lib/src/signer/privy/signer.rs b/crates/lib/src/signer/privy/signer.rs deleted file mode 100644 index 583d681d..00000000 --- a/crates/lib/src/signer/privy/signer.rs +++ /dev/null @@ -1,383 +0,0 @@ -use base64::{engine::general_purpose::STANDARD, Engine}; -use solana_sdk::{pubkey::Pubkey, signature::Signature, transaction::VersionedTransaction}; -use std::str::FromStr; - -use crate::signer::privy::types::{ - PrivyError, PrivySigner, SignTransactionParams, SignTransactionRequest, - SignTransactionResponse, WalletResponse, -}; - -impl PrivySigner { - /// Create a new PrivySigner - pub fn new(app_id: String, app_secret: String, wallet_id: String) -> Self { - Self { - app_id, - app_secret, - wallet_id, - api_base_url: "https://api.privy.io/v1".to_string(), - client: reqwest::Client::new(), - public_key: Pubkey::default(), - } - } - - /// Initialize the signer by fetching the public key - pub async fn init(&mut self) -> Result<(), PrivyError> { - let pubkey = self.get_public_key().await?; - self.public_key = pubkey; - Ok(()) - } - - /// Get the cached public key - pub fn solana_pubkey(&self) -> Pubkey { - self.public_key - } - - /// Get the Basic Auth header value - fn get_privy_auth_header(&self) -> String { - let credentials = format!("{}:{}", self.app_id, self.app_secret); - format!("Basic {}", STANDARD.encode(credentials)) - } - - /// Get the public key for this wallet - pub async fn get_public_key(&self) -> Result { - let url = format!("{}/wallets/{}", self.api_base_url, self.wallet_id); - - let response = self - .client - .get(&url) - .header("Authorization", self.get_privy_auth_header()) - .header("privy-app-id", &self.app_id) - .send() - .await?; - - if !response.status().is_success() { - let status = response.status().as_u16(); - let error_text = response - .text() - .await - .unwrap_or_else(|_| "Failed to read error response".to_string()); - - log::error!( - "Privy API get_public_key error - status: {status}, response: {error_text}" - ); - return Err(PrivyError::ApiError(status)); - } - - let wallet_info: WalletResponse = response.json().await?; - - // For Solana wallets, the address is the public key - Pubkey::from_str(&wallet_info.address).map_err(|_| PrivyError::InvalidPublicKey) - } - - /// Sign a transaction - /// - /// The transaction parameter should be a fully serialized Solana transaction - /// (including empty signature placeholders), not just the message bytes. - pub async fn sign_solana( - &self, - transaction: &VersionedTransaction, - ) -> Result { - let url = format!("{}/wallets/{}/rpc", self.api_base_url, self.wallet_id); - let serialized = - bincode::serialize(transaction).map_err(|_| PrivyError::SerializationError)?; - let request = SignTransactionRequest { - method: "signTransaction", - params: SignTransactionParams { - transaction: STANDARD.encode(serialized), - encoding: "base64", - }, - }; - - let response = self - .client - .post(&url) - .header("Authorization", self.get_privy_auth_header()) - .header("privy-app-id", &self.app_id) - .header("Content-Type", "application/json") - .json(&request) - .send() - .await?; - - if !response.status().is_success() { - let status = response.status().as_u16(); - let error_text = response - .text() - .await - .unwrap_or_else(|_| "Failed to read error response".to_string()); - - log::error!("Privy API sign_solana error - status: {status}, response: {error_text}"); - return Err(PrivyError::ApiError(status)); - } - - let response_text = response.text().await?; - - let sign_response: SignTransactionResponse = serde_json::from_str(&response_text)?; - - // Decode the signed transaction from base64 - let signed_tx_bytes = STANDARD.decode(&sign_response.data.signed_transaction)?; - - // Deserialize the transaction to extract the signature - let signed_tx: VersionedTransaction = - bincode::deserialize(&signed_tx_bytes).map_err(|_| PrivyError::InvalidResponse)?; - - // Get the first signature (which should be the one we just created) - if let Some(signature) = signed_tx.signatures.first() { - Ok(*signature) - } else { - Err(PrivyError::InvalidSignature) - } - } - - pub async fn sign(&self, transaction: &VersionedTransaction) -> Result, PrivyError> { - let signature = self.sign_solana(transaction).await?; - Ok(signature.as_ref().to_vec()) - } -} - -#[cfg(test)] -mod tests { - use crate::tests::transaction_mock::create_mock_transaction; - - use super::*; - use mockito::Server; - use solana_sdk::pubkey::Pubkey; - - #[test] - fn test_new_privy_signer() { - let app_id = "test_app_id".to_string(); - let app_secret = "test_app_secret".to_string(); - let wallet_id = "test_wallet_id".to_string(); - - let signer = PrivySigner::new(app_id.clone(), app_secret.clone(), wallet_id.clone()); - - assert_eq!(signer.app_id, app_id); - assert_eq!(signer.app_secret, app_secret); - assert_eq!(signer.wallet_id, wallet_id); - assert_eq!(signer.api_base_url, "https://api.privy.io/v1"); - assert_eq!(signer.public_key, Pubkey::default()); - } - - #[test] - fn test_solana_pubkey() { - let mut signer = PrivySigner::new( - "app_id".to_string(), - "app_secret".to_string(), - "wallet_id".to_string(), - ); - - let test_pubkey = Pubkey::new_unique(); - signer.public_key = test_pubkey; - - assert_eq!(signer.solana_pubkey(), test_pubkey); - } - - #[test] - fn test_get_privy_auth_header() { - let signer = PrivySigner::new( - "test_app".to_string(), - "test_secret".to_string(), - "wallet123".to_string(), - ); - - let auth_header = signer.get_privy_auth_header(); - let expected_credentials = "test_app:test_secret"; - let expected_encoded = STANDARD.encode(expected_credentials); - let expected_header = format!("Basic {expected_encoded}"); - - assert_eq!(auth_header, expected_header); - } - - #[tokio::test] - async fn test_get_public_key_success() { - // Setup mock server - let mut server = Server::new_async().await; - - // Mocked response from Privy API based on https://docs.privy.io/api-reference/wallets/get - let mock_response = r#"{ - "id": "clz4ndjp705bh14za2p80kt3f", - "object": "wallet", - "created_at": 1721937199, - "address": "11111111111111111111111111111111", - "chain_type": "solana", - "chain_id": "solana:101", - "wallet_client": "privy", - "wallet_client_type": "privy", - "connector_type": "embedded", - "recovery_method": "privy", - "imported": false, - "delegated": false, - "user_id": "did:privy:cm0xlrcmj01ja13m6ncg4ewce" - }"#; - - // Create mock endpoint for GET /wallets/{wallet_id} - let _mock = server - .mock("GET", "/wallets/test_wallet") - .with_status(200) - .with_header("content-type", "application/json") - .with_body(mock_response) - .create_async() - .await; - - let mut signer = PrivySigner::new( - "test_app".to_string(), - "test_secret".to_string(), - "test_wallet".to_string(), - ); - signer.api_base_url = server.url(); - - // Test successful public key retrieval - let result = signer.get_public_key().await; - assert!(result.is_ok()); - assert_eq!(result.unwrap().to_string(), "11111111111111111111111111111111"); - } - - #[tokio::test] - async fn test_get_public_key_api_error() { - let mut server = Server::new_async().await; - - // Mocked error response from Privy API based on https://docs.privy.io/api-reference/wallets/get - let mock_error_response = r#"{ - "error": { - "error": "wallet_not_found", - "error_description": "Wallet not found." - } - }"#; - - // Create mock endpoint for GET /wallets/{wallet_id} returning 404 error - let _mock = server - .mock("GET", "/wallets/invalid_wallet") - .with_status(404) - .with_header("content-type", "application/json") - .with_body(mock_error_response) - .create_async() - .await; - - let mut signer = PrivySigner::new( - "test_app".to_string(), - "test_secret".to_string(), - "invalid_wallet".to_string(), - ); - signer.api_base_url = server.url(); - - // Test API error handling - let result = signer.get_public_key().await; - assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), PrivyError::ApiError(404))); - } - - #[tokio::test] - async fn test_sign_solana_success() { - // Setup mock server - let mut server = Server::new_async().await; - - // Mocked response from Privy RPC API based on https://docs.privy.io/api-reference/wallets/solana/sign-transaction - // Modified to match the SignTransactionResponse struct - let mock_response = r#"{ - "method": "signTransaction", - "data": { - "signed_transaction": "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEDArczbMia1tLmq7zz4DinMNN0pJ1JtLdqIJPUw3YrGCzuAXUE8535pRk2d+dzOdFlBIpWfgXa9F2zWLidMUr5zdDlBG2q1y4YJlUDl7ov7FLfWvDlhVAidT5nXu6bJgZG1qNgJQBd55PwKBNYMFYBJ2rIbgNhfHu6E/OmZFpV9EUCuE8AAAABd2J0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFB8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEDAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAA=", - "encoding": "base64" - } - }"#; - - // Create mock endpoint for POST /wallets/{wallet_id}/rpc - let _mock = server - .mock("POST", "/wallets/test_wallet/rpc") - .with_status(200) - .with_header("content-type", "application/json") - .with_body(mock_response) - .create_async() - .await; - - let test_transaction = create_mock_transaction(); - - let mut signer = PrivySigner::new( - "test_app".to_string(), - "test_secret".to_string(), - "test_wallet".to_string(), - ); - signer.api_base_url = server.url(); - - // Test successful signing - let result = signer.sign_solana(&test_transaction).await; - assert!(result.is_ok()); - let signature = result.unwrap(); - assert_eq!(signature.to_string().len(), 64); // Hex encoded signature length - } - - #[tokio::test] - async fn test_sign_solana_api_error() { - let mut server = Server::new_async().await; - - // Mocked error response from Privy RPC API based on https://docs.privy.io/api-reference/wallets/solana/sign-transaction - let mock_error_response = r#"{ - "error": { - "error": "invalid_request", - "error_description": "The transaction is invalid or malformed." - } - }"#; - - // Create mock endpoint for POST /wallets/{wallet_id}/rpc returning 400 error - let _mock = server - .mock("POST", "/wallets/test_wallet/rpc") - .with_status(400) - .with_header("content-type", "application/json") - .with_body(mock_error_response) - .create_async() - .await; - - let test_transaction = create_mock_transaction(); - - let mut signer = PrivySigner::new( - "test_app".to_string(), - "test_secret".to_string(), - "test_wallet".to_string(), - ); - signer.api_base_url = server.url(); - - // Test API error handling - let result = signer.sign_solana(&test_transaction).await; - assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), PrivyError::ApiError(400))); - } - - #[tokio::test] - async fn test_sign_success() { - // Setup mock server - let mut server = Server::new_async().await; - - // Mocked response from Privy RPC API (same as sign_solana) based on https://docs.privy.io/api-reference/wallets/solana/sign-transaction - // Modified to match the SignTransactionResponse struct - let mock_response = r#"{ - "method": "signTransaction", - "data": { - "signed_transaction": "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEDArczbMia1tLmq7zz4DinMNN0pJ1JtLdqIJPUw3YrGCzuAXUE8535pRk2d+dzOdFlBIpWfgXa9F2zWLidMUr5zdDlBG2q1y4YJlUDl7ov7FLfWvDlhVAidT5nXu6bJgZG1qNgJQBd55PwKBNYMFYBJ2rIbgNhfHu6E/OmZFpV9EUCuE8AAAABd2J0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFB8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEDAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAA=", - "encoding": "base64" - } - }"#; - - // Create mock endpoint for POST /wallets/{wallet_id}/rpc - let _mock = server - .mock("POST", "/wallets/test_wallet/rpc") - .with_status(200) - .with_header("content-type", "application/json") - .with_body(mock_response) - .create_async() - .await; - - let test_transaction = create_mock_transaction(); - - let mut signer = PrivySigner::new( - "test_app".to_string(), - "test_secret".to_string(), - "test_wallet".to_string(), - ); - signer.api_base_url = server.url(); - - // Test successful signing returns Vec - let result = signer.sign(&test_transaction).await; - assert!(result.is_ok()); - let signature_bytes = result.unwrap(); - assert_eq!(signature_bytes.len(), 64); // Solana signature is 64 bytes - } -} diff --git a/crates/lib/src/signer/privy/types.rs b/crates/lib/src/signer/privy/types.rs deleted file mode 100644 index 4726e67a..00000000 --- a/crates/lib/src/signer/privy/types.rs +++ /dev/null @@ -1,148 +0,0 @@ -use reqwest::Client; -use serde::{Deserialize, Serialize}; -use solana_sdk::pubkey::Pubkey; - -#[derive(Clone, Debug)] -pub struct PrivySigner { - pub app_id: String, - pub app_secret: String, - pub wallet_id: String, - pub api_base_url: String, - pub client: Client, - pub public_key: Pubkey, -} - -#[derive(Default)] -pub struct PrivyConfig { - pub app_id: Option, - pub app_secret: Option, - pub wallet_id: Option, -} - -// API request/response types for Privy -#[derive(Serialize)] -pub struct SignTransactionRequest { - pub method: &'static str, - pub params: SignTransactionParams, -} - -#[derive(Serialize)] -pub struct SignTransactionParams { - pub transaction: String, - pub encoding: &'static str, -} - -#[derive(Deserialize, Debug)] -pub struct SignTransactionResponse { - pub method: String, - pub data: SignTransactionData, -} - -#[derive(Deserialize, Debug)] -pub struct SignTransactionData { - #[serde(rename = "signed_transaction")] - pub signed_transaction: String, - pub encoding: String, -} - -// Wallet info response -#[derive(Deserialize, Debug)] -pub struct WalletResponse { - pub id: String, - pub address: String, - pub chain_type: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub wallet_client_type: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub connector_type: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub imported: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub delegated: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub hd_path: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub public_key: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub owner_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub policy_ids: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub additional_signers: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub exported_at: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub created_at: Option, -} - -// Error types using anyhow -#[derive(Debug)] -pub enum PrivyError { - MissingConfig(&'static str), - ApiError(u16), - InvalidResponse, - InvalidPublicKey, - InvalidSignature, - SerializationError, - RequestError(reqwest::Error), - JsonError(serde_json::Error), - Base64Error(base64::DecodeError), - InitializationError, - RuntimeError, - Other(anyhow::Error), -} - -impl std::fmt::Display for PrivyError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - PrivyError::MissingConfig(field) => write!(f, "Missing config: {field}"), - PrivyError::ApiError(status) => write!(f, "API error: {status}"), - PrivyError::InvalidResponse => write!(f, "Invalid response"), - PrivyError::InvalidPublicKey => write!(f, "Invalid public key"), - PrivyError::InvalidSignature => write!(f, "Invalid signature"), - PrivyError::SerializationError => write!(f, "Serialization error"), - PrivyError::RequestError(e) => write!(f, "Request error: {e}"), - PrivyError::JsonError(e) => write!(f, "JSON error: {e}"), - PrivyError::Base64Error(e) => write!(f, "Base64 error: {e}"), - PrivyError::InitializationError => write!(f, "Initialization error"), - PrivyError::RuntimeError => write!(f, "Runtime error"), - PrivyError::Other(e) => write!(f, "{e}"), - } - } -} - -impl std::error::Error for PrivyError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - PrivyError::RequestError(e) => Some(e), - PrivyError::JsonError(e) => Some(e), - PrivyError::Base64Error(e) => Some(e), - PrivyError::Other(e) => Some(e.as_ref()), - _ => None, - } - } -} - -impl From for PrivyError { - fn from(err: reqwest::Error) -> Self { - PrivyError::RequestError(err) - } -} - -impl From for PrivyError { - fn from(err: serde_json::Error) -> Self { - PrivyError::JsonError(err) - } -} - -impl From for PrivyError { - fn from(err: base64::DecodeError) -> Self { - PrivyError::Base64Error(err) - } -} - -impl From for PrivyError { - fn from(err: anyhow::Error) -> Self { - PrivyError::Other(err) - } -} diff --git a/crates/lib/src/signer/signer.rs b/crates/lib/src/signer/signer.rs index fd5ba67c..db05fb9a 100644 --- a/crates/lib/src/signer/signer.rs +++ b/crates/lib/src/signer/signer.rs @@ -1,150 +1,7 @@ -use crate::{ - error::KoraError, - signer::{ - memory_signer::solana_signer::SolanaMemorySigner, privy::types::PrivySigner, - turnkey::types::TurnkeySigner, vault::vault_signer::VaultSigner, - }, -}; -use solana_sdk::{ - pubkey::Pubkey, signature::Signature as SolanaSignature, transaction::VersionedTransaction, -}; -use std::error::Error; +//! Re-exports for external signer infrastructure +//! +//! Kora uses solana-signers crate as its signing infrastructure. +//! This module exists only for re-exporting convenience. -#[derive(Debug, Clone)] -pub struct Signature { - /// The raw bytes of the signature - pub bytes: Vec, - /// Whether this is a partial signature or a complete signature - pub is_partial: bool, -} - -/// A trait for signing arbitrary messages -pub trait Signer { - /// The error type returned by signing operations - type Error: Error + Send + Sync + 'static; - - fn sign( - &self, - transaction: &VersionedTransaction, - ) -> impl std::future::Future> + Send; - - fn sign_solana( - &self, - transaction: &VersionedTransaction, - ) -> impl std::future::Future> + Send; -} - -#[derive(Clone, Debug)] -#[allow(clippy::large_enum_variant)] -pub enum KoraSigner { - Memory(SolanaMemorySigner), - Turnkey(TurnkeySigner), - Vault(VaultSigner), - Privy(PrivySigner), -} - -impl KoraSigner { - pub fn solana_pubkey(&self) -> Pubkey { - match self { - KoraSigner::Memory(signer) => signer.solana_pubkey(), - KoraSigner::Turnkey(signer) => signer.solana_pubkey(), - KoraSigner::Vault(signer) => signer.solana_pubkey(), - KoraSigner::Privy(signer) => signer.solana_pubkey(), - } - } -} - -impl super::Signer for KoraSigner { - type Error = KoraError; - - async fn sign( - &self, - transaction: &VersionedTransaction, - ) -> Result { - match self { - // Some signers expect the serialized message, others expect the message bytes - KoraSigner::Memory(signer) => signer.sign(transaction).await, - KoraSigner::Turnkey(signer) => { - let sig = signer.sign(transaction).await?; - Ok(super::Signature { bytes: sig, is_partial: false }) - } - KoraSigner::Vault(signer) => signer.sign(transaction).await, - KoraSigner::Privy(signer) => { - let sig = signer.sign(transaction).await?; - Ok(super::Signature { bytes: sig, is_partial: false }) - } - } - } - - async fn sign_solana( - &self, - transaction: &VersionedTransaction, - ) -> Result { - match self { - // Some signers expect the serialized message, others expect the message bytes - KoraSigner::Memory(signer) => signer.sign_solana(transaction).await, - KoraSigner::Vault(signer) => signer.sign_solana(transaction).await, - KoraSigner::Turnkey(signer) => { - signer.sign_solana(transaction).await.map_err(KoraError::from) - } - KoraSigner::Privy(signer) => { - signer.sign_solana(transaction).await.map_err(KoraError::from) - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - tests::{config_mock::ConfigMockBuilder, transaction_mock::create_mock_transaction}, - Signer, - }; - use solana_sdk::{signature::Keypair, signer::Signer as SolanaSigner}; - - #[test] - fn test_kora_signer_memory_pubkey() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - let keypair = Keypair::new(); - let expected_pubkey = keypair.pubkey(); - let memory_signer = SolanaMemorySigner::new(keypair); - let kora_signer = KoraSigner::Memory(memory_signer); - - assert_eq!(kora_signer.solana_pubkey(), expected_pubkey); - } - - #[tokio::test] - async fn test_kora_signer_memory_sign() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - let keypair = Keypair::new(); - let memory_signer = SolanaMemorySigner::new(keypair); - let kora_signer = KoraSigner::Memory(memory_signer); - let transaction = create_mock_transaction(); - - let result = kora_signer.sign(&transaction).await; - assert!(result.is_ok()); - - let signature = result.unwrap(); - assert_eq!(signature.bytes.len(), 64); - assert!(!signature.is_partial); - } - - #[tokio::test] - async fn test_kora_signer_memory_sign_solana() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - let keypair = Keypair::new(); - let memory_signer = SolanaMemorySigner::new(keypair); - let kora_signer = KoraSigner::Memory(memory_signer); - let transaction = create_mock_transaction(); - - let result = kora_signer.sign_solana(&transaction).await; - assert!(result.is_ok()); - - let signature = result.unwrap(); - assert_eq!(signature.as_ref().len(), 64); - } -} +// Re-export the external signer for use throughout Kora +pub use solana_signers::{Signer, SolanaSigner}; diff --git a/crates/lib/src/signer/turnkey/config.rs b/crates/lib/src/signer/turnkey/config.rs deleted file mode 100644 index 70f1b3f7..00000000 --- a/crates/lib/src/signer/turnkey/config.rs +++ /dev/null @@ -1,272 +0,0 @@ -use std::str::FromStr; - -use serde::{Deserialize, Serialize}; -use solana_sdk::pubkey::Pubkey; - -use crate::{ - error::KoraError, - signer::{ - config_trait::SignerConfigTrait, - turnkey::types::{TurnkeyError, TurnkeySigner}, - utils::get_env_var_for_signer, - KoraSigner, - }, -}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TurnkeySignerConfig { - /// Environment variable for Turnkey API public key - pub api_public_key_env: String, - /// Environment variable for Turnkey API private key - pub api_private_key_env: String, - /// Environment variable for Turnkey organization ID - pub organization_id_env: String, - /// Environment variable for Turnkey private key ID - pub private_key_id_env: String, - /// Environment variable for Turnkey public key - pub public_key_env: String, -} - -pub struct TurnkeySignerHandler; - -impl SignerConfigTrait for TurnkeySignerHandler { - type Config = TurnkeySignerConfig; - - fn validate_config(config: &Self::Config, signer_name: &str) -> Result<(), KoraError> { - let env_vars = [ - ("api_public_key_env", &config.api_public_key_env), - ("api_private_key_env", &config.api_private_key_env), - ("organization_id_env", &config.organization_id_env), - ("private_key_id_env", &config.private_key_id_env), - ("public_key_env", &config.public_key_env), - ]; - - for (field_name, env_var) in env_vars { - if env_var.is_empty() { - return Err(KoraError::ValidationError(format!( - "Turnkey signer '{signer_name}' must specify non-empty {field_name}" - ))); - } - } - - Ok(()) - } - - fn build_from_config( - config: &Self::Config, - signer_name: &str, - ) -> Result { - let api_public_key = get_env_var_for_signer(&config.api_public_key_env, signer_name)?; - let api_private_key = get_env_var_for_signer(&config.api_private_key_env, signer_name)?; - let organization_id = get_env_var_for_signer(&config.organization_id_env, signer_name)?; - let private_key_id = get_env_var_for_signer(&config.private_key_id_env, signer_name)?; - let public_key = get_env_var_for_signer(&config.public_key_env, signer_name)?; - - let pubkey = Pubkey::from_str(&public_key) - .map_err(|e| TurnkeyError::InvalidPublicKey(e.to_string()))?; - - let signer = TurnkeySigner::new( - api_public_key, - api_private_key, - organization_id, - private_key_id, - public_key, - pubkey, - ); - - Ok(KoraSigner::Turnkey(signer)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::tests::config_mock::ConfigMockBuilder; - use std::env; - - #[test] - fn test_validate_config_valid() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - let config = TurnkeySignerConfig { - api_public_key_env: "VALID_API_PUBLIC_KEY".to_string(), - api_private_key_env: "VALID_API_PRIVATE_KEY".to_string(), - organization_id_env: "VALID_ORG_ID".to_string(), - private_key_id_env: "VALID_PRIVATE_KEY_ID".to_string(), - public_key_env: "VALID_PUBLIC_KEY".to_string(), - }; - - let result = TurnkeySignerHandler::validate_config(&config, "test_signer"); - assert!(result.is_ok()); - } - - #[test] - fn test_validate_config_empty_api_public_key() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - let config = TurnkeySignerConfig { - api_public_key_env: "".to_string(), - api_private_key_env: "VALID_API_PRIVATE_KEY".to_string(), - organization_id_env: "VALID_ORG_ID".to_string(), - private_key_id_env: "VALID_PRIVATE_KEY_ID".to_string(), - public_key_env: "VALID_PUBLIC_KEY".to_string(), - }; - - let result = TurnkeySignerHandler::validate_config(&config, "test_signer"); - assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), KoraError::ValidationError(_))); - } - - #[test] - fn test_validate_config_empty_api_private_key() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - let config = TurnkeySignerConfig { - api_public_key_env: "VALID_API_PUBLIC_KEY".to_string(), - api_private_key_env: "".to_string(), - organization_id_env: "VALID_ORG_ID".to_string(), - private_key_id_env: "VALID_PRIVATE_KEY_ID".to_string(), - public_key_env: "VALID_PUBLIC_KEY".to_string(), - }; - - let result = TurnkeySignerHandler::validate_config(&config, "test_signer"); - assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), KoraError::ValidationError(_))); - } - - #[test] - fn test_validate_config_empty_organization_id() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - let config = TurnkeySignerConfig { - api_public_key_env: "VALID_API_PUBLIC_KEY".to_string(), - api_private_key_env: "VALID_API_PRIVATE_KEY".to_string(), - organization_id_env: "".to_string(), - private_key_id_env: "VALID_PRIVATE_KEY_ID".to_string(), - public_key_env: "VALID_PUBLIC_KEY".to_string(), - }; - - let result = TurnkeySignerHandler::validate_config(&config, "test_signer"); - assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), KoraError::ValidationError(_))); - } - - #[test] - fn test_validate_config_empty_private_key_id() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - let config = TurnkeySignerConfig { - api_public_key_env: "VALID_API_PUBLIC_KEY".to_string(), - api_private_key_env: "VALID_API_PRIVATE_KEY".to_string(), - organization_id_env: "VALID_ORG_ID".to_string(), - private_key_id_env: "".to_string(), - public_key_env: "VALID_PUBLIC_KEY".to_string(), - }; - - let result = TurnkeySignerHandler::validate_config(&config, "test_signer"); - assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), KoraError::ValidationError(_))); - } - - #[test] - fn test_validate_config_empty_public_key() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - let config = TurnkeySignerConfig { - api_public_key_env: "VALID_API_PUBLIC_KEY".to_string(), - api_private_key_env: "VALID_API_PRIVATE_KEY".to_string(), - organization_id_env: "VALID_ORG_ID".to_string(), - private_key_id_env: "VALID_PRIVATE_KEY_ID".to_string(), - public_key_env: "".to_string(), - }; - - let result = TurnkeySignerHandler::validate_config(&config, "test_signer"); - assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), KoraError::ValidationError(_))); - } - - #[test] - fn test_build_from_config_missing_env_vars() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - // Ensure env vars don't exist - env::remove_var("NONEXISTENT_API_PUBLIC_KEY"); - env::remove_var("NONEXISTENT_API_PRIVATE_KEY"); - env::remove_var("NONEXISTENT_ORG_ID"); - env::remove_var("NONEXISTENT_PRIVATE_KEY_ID"); - env::remove_var("NONEXISTENT_PUBLIC_KEY"); - - let config = TurnkeySignerConfig { - api_public_key_env: "NONEXISTENT_API_PUBLIC_KEY".to_string(), - api_private_key_env: "NONEXISTENT_API_PRIVATE_KEY".to_string(), - organization_id_env: "NONEXISTENT_ORG_ID".to_string(), - private_key_id_env: "NONEXISTENT_PRIVATE_KEY_ID".to_string(), - public_key_env: "NONEXISTENT_PUBLIC_KEY".to_string(), - }; - - let result = TurnkeySignerHandler::build_from_config(&config, "test_signer"); - assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), KoraError::ValidationError(_))); - } - - #[test] - fn test_build_from_config_partial_env_vars() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - // Set only some environment variables - env::set_var("TEST_TURNKEY_API_PUBLIC_KEY_PARTIAL", "test_api_public_key"); - env::set_var("TEST_TURNKEY_API_PRIVATE_KEY_PARTIAL", "test_api_private_key"); - env::remove_var("MISSING_ORG_ID"); - env::remove_var("MISSING_PRIVATE_KEY_ID"); - env::remove_var("MISSING_PUBLIC_KEY"); - - let config = TurnkeySignerConfig { - api_public_key_env: "TEST_TURNKEY_API_PUBLIC_KEY_PARTIAL".to_string(), - api_private_key_env: "TEST_TURNKEY_API_PRIVATE_KEY_PARTIAL".to_string(), - organization_id_env: "MISSING_ORG_ID".to_string(), - private_key_id_env: "MISSING_PRIVATE_KEY_ID".to_string(), - public_key_env: "MISSING_PUBLIC_KEY".to_string(), - }; - - let result = TurnkeySignerHandler::build_from_config(&config, "test_signer"); - assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), KoraError::ValidationError(_))); - - // Clean up - env::remove_var("TEST_TURNKEY_API_PUBLIC_KEY_PARTIAL"); - env::remove_var("TEST_TURNKEY_API_PRIVATE_KEY_PARTIAL"); - } - - #[test] - fn test_build_from_config_valid_env_vars() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - // Set test environment variables with valid values - env::set_var("TEST_TURNKEY_API_PUBLIC_KEY_VALID", "test_api_public_key"); - env::set_var("TEST_TURNKEY_API_PRIVATE_KEY_VALID", "test_api_private_key"); - env::set_var("TEST_TURNKEY_ORG_ID_VALID", "test_org_id"); - env::set_var("TEST_TURNKEY_PRIVATE_KEY_ID_VALID", "test_private_key_id"); - env::set_var( - "TEST_TURNKEY_PUBLIC_KEY_VALID", - "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM", - ); - - let config = TurnkeySignerConfig { - api_public_key_env: "TEST_TURNKEY_API_PUBLIC_KEY_VALID".to_string(), - api_private_key_env: "TEST_TURNKEY_API_PRIVATE_KEY_VALID".to_string(), - organization_id_env: "TEST_TURNKEY_ORG_ID_VALID".to_string(), - private_key_id_env: "TEST_TURNKEY_PRIVATE_KEY_ID_VALID".to_string(), - public_key_env: "TEST_TURNKEY_PUBLIC_KEY_VALID".to_string(), - }; - - let result = TurnkeySignerHandler::build_from_config(&config, "test_signer"); - assert!(result.is_ok()); - - // Clean up - env::remove_var("TEST_TURNKEY_API_PUBLIC_KEY_VALID"); - env::remove_var("TEST_TURNKEY_API_PRIVATE_KEY_VALID"); - env::remove_var("TEST_TURNKEY_ORG_ID_VALID"); - env::remove_var("TEST_TURNKEY_PRIVATE_KEY_ID_VALID"); - env::remove_var("TEST_TURNKEY_PUBLIC_KEY_VALID"); - } -} diff --git a/crates/lib/src/signer/turnkey/mod.rs b/crates/lib/src/signer/turnkey/mod.rs deleted file mode 100644 index 71777fe8..00000000 --- a/crates/lib/src/signer/turnkey/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod config; -pub mod signer; -pub mod types; diff --git a/crates/lib/src/signer/turnkey/signer.rs b/crates/lib/src/signer/turnkey/signer.rs deleted file mode 100644 index 51fcd03a..00000000 --- a/crates/lib/src/signer/turnkey/signer.rs +++ /dev/null @@ -1,449 +0,0 @@ -use base64::Engine; -use p256::ecdsa::signature::Signer; -use reqwest::Client; -use solana_sdk::{pubkey::Pubkey, signature::Signature}; - -use solana_sdk::transaction::VersionedTransaction; - -use crate::signer::{ - turnkey::types::{ActivityResponse, SignParameters, SignRequest, TurnkeyError, TurnkeySigner}, - utils::{bytes_to_hex, hex_to_bytes}, -}; - -impl TurnkeySigner { - pub fn new( - api_public_key: String, - api_private_key: String, - organization_id: String, - private_key_id: String, - public_key: String, - pubkey: Pubkey, - ) -> Self { - Self { - api_public_key, - api_private_key, - organization_id, - private_key_id, - public_key, - pubkey, - api_base_url: "https://api.turnkey.com".to_string(), - client: Client::new(), - } - } - - pub async fn sign(&self, transaction: &VersionedTransaction) -> Result, TurnkeyError> { - let hex_message = hex::encode(transaction.message.serialize()); - - let request = SignRequest { - activity_type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2".to_string(), - timestamp_ms: chrono::Utc::now().timestamp_millis().to_string(), - organization_id: self.organization_id.clone(), - parameters: SignParameters { - sign_with: self.private_key_id.clone(), - payload: hex_message, - encoding: "PAYLOAD_ENCODING_HEXADECIMAL".to_string(), - hash_function: "HASH_FUNCTION_NOT_APPLICABLE".to_string(), - }, - }; - - let body = serde_json::to_string(&request).map_err(TurnkeyError::JsonError)?; - - let stamp = self.create_stamp(&body)?; - - let url = format!("{}/public/v1/submit/sign_raw_payload", self.api_base_url); - let response = self - .client - .post(&url) - .header("Content-Type", "application/json") - .header("X-Stamp", stamp) - .body(body) - .send() - .await - .map_err(TurnkeyError::RequestError)?; - - if !response.status().is_success() { - let status = response.status().as_u16(); - let error_text = response - .text() - .await - .unwrap_or_else(|_| "Failed to read error response".to_string()); - - log::error!("Turnkey API error - status: {status}, response: {error_text}"); - return Err(TurnkeyError::ApiError(status)); - } - - let response_text = response.text().await.map_err(TurnkeyError::RequestError)?; - - let response = serde_json::from_str::(&response_text) - .map_err(TurnkeyError::JsonError)?; - - if let Some(result) = response.activity.result { - if let Some(sign_result) = result.sign_raw_payload_result { - // Decode r and s components - let r_bytes = hex::decode(&sign_result.r).map_err(TurnkeyError::InvalidHex)?; - let s_bytes = hex::decode(&sign_result.s).map_err(TurnkeyError::InvalidHex)?; - - // Ensure each component is exactly 32 bytes - if r_bytes.len() > 32 || s_bytes.len() > 32 { - return Err(TurnkeyError::InvalidSignature); - } - - // Create properly padded 32-byte arrays - let mut final_r = [0u8; 32]; - let mut final_s = [0u8; 32]; - - // Copy bytes with proper padding (right-aligned) - final_r[32 - r_bytes.len()..].copy_from_slice(&r_bytes); - final_s[32 - s_bytes.len()..].copy_from_slice(&s_bytes); - - // Combine r and s into final 64-byte signature - let mut signature = Vec::with_capacity(64); - signature.extend_from_slice(&final_r); - signature.extend_from_slice(&final_s); - - return Ok(signature); - } - } - - Err(TurnkeyError::InvalidResponse) - } - - pub async fn sign_solana( - &self, - transaction: &VersionedTransaction, - ) -> Result { - let sig = self.sign(transaction).await?; - let sig_bytes: [u8; 64] = - sig.try_into().map_err(|_| TurnkeyError::InvalidSignatureLength)?; - Ok(Signature::from(sig_bytes)) - } - - fn create_stamp(&self, message: &str) -> Result { - let private_key_bytes = - hex_to_bytes(&self.api_private_key).map_err(TurnkeyError::InvalidStamp)?; - let private_key_array: [u8; 32] = - private_key_bytes.try_into().map_err(|_| TurnkeyError::InvalidPrivateKeyLength)?; - let signing_key = p256::ecdsa::SigningKey::from_slice(&private_key_array) - .map_err(TurnkeyError::SigningKeyError)?; - - let signature: p256::ecdsa::Signature = signing_key.sign(message.as_bytes()); - let signature_der = signature.to_der().to_bytes(); - let signature_hex = bytes_to_hex(&signature_der).map_err(TurnkeyError::InvalidStamp)?; - - let stamp = serde_json::json!({ - "public_key": self.api_public_key, - "signature": signature_hex, - "scheme": "SIGNATURE_SCHEME_TK_API_P256" - }); - - let json_stamp = serde_json::to_string(&stamp).map_err(TurnkeyError::JsonError)?; - - Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(json_stamp.as_bytes())) - } - - pub fn solana_pubkey(&self) -> Pubkey { - self.pubkey - } -} - -#[cfg(test)] -mod tests { - use std::str::FromStr; - - use crate::tests::transaction_mock::create_mock_transaction; - - use super::*; - use mockito::Server; - - #[test] - fn test_new_turnkey_signer() { - let api_public_key = "test_api_public_key".to_string(); - let api_private_key = "test_api_private_key".to_string(); - let organization_id = "test_org_id".to_string(); - let private_key_id = "test_private_key_id".to_string(); - let public_key = "11111111111111111111111111111111".to_string(); - - let signer = TurnkeySigner::new( - api_public_key.clone(), - api_private_key.clone(), - organization_id.clone(), - private_key_id.clone(), - public_key.clone(), - Pubkey::from_str(&public_key).unwrap(), - ); - - assert_eq!(signer.api_public_key, api_public_key); - assert_eq!(signer.api_private_key, api_private_key); - assert_eq!(signer.organization_id, organization_id); - assert_eq!(signer.private_key_id, private_key_id); - assert_eq!(signer.public_key, public_key); - } - - #[test] - fn test_solana_pubkey_valid() { - let signer = TurnkeySigner::new( - "api_pub".to_string(), - "api_priv".to_string(), - "org".to_string(), - "key_id".to_string(), - "11111111111111111111111111111111".to_string(), - Pubkey::from_str(&"11111111111111111111111111111111").unwrap(), - ); - - let pubkey = signer.solana_pubkey(); - assert_eq!(pubkey.to_string(), "11111111111111111111111111111111"); - } - - #[tokio::test] - async fn test_sign_success() { - let mut server = Server::new_async().await; - - // Mocked response from Turnkey API based on https://docs.turnkey.com/api-reference/activities/sign-raw-payload - let mock_response = r#"{ - "activity": { - "id": "12345678-1234-1234-1234-123456789012", - "organizationId": "test_org_id", - "status": "ACTIVITY_STATUS_COMPLETED", - "type": "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2", - "timestampMs": "1640995200000", - "result": { - "signRawPayloadResult": { - "r": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", - "s": "fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321" - } - } - } - }"#; - - let _mock = server - .mock("POST", "/public/v1/submit/sign_raw_payload") - .with_status(200) - .with_header("content-type", "application/json") - .with_body(mock_response) - .create_async() - .await; - - let test_transaction = create_mock_transaction(); - - let mut signer = TurnkeySigner::new( - "test_api_public_key".to_string(), - "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(), // Valid hex private key - "test_org_id".to_string(), - "test_private_key_id".to_string(), - "11111111111111111111111111111111".to_string(), - Pubkey::from_str(&"11111111111111111111111111111111").unwrap(), - ); - signer.api_base_url = server.url(); - - let result = signer.sign(&test_transaction).await; - assert!(result.is_ok()); - let signature_bytes = result.unwrap(); - assert_eq!(signature_bytes.len(), 64); // Combined r + s components - } - - #[tokio::test] - async fn test_sign_api_error() { - let mut server = Server::new_async().await; - - // Mocked error response from Turnkey API - // For API errors, we'll return an activity with no result to trigger "Failed to get signature from response" - let mock_error_response = r#"{ - "activity": { - "id": "12345678-1234-1234-1234-123456789012", - "organizationId": "test_org_id", - "status": "ACTIVITY_STATUS_FAILED", - "type": "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2", - "timestampMs": "1640995200000" - } - }"#; - - // Create mock endpoint for POST /public/v1/submit/sign_raw_payload returning 400 error - let _mock = server - .mock("POST", "/public/v1/submit/sign_raw_payload") - .with_status(400) - .with_header("content-type", "application/json") - .with_body(mock_error_response) - .create_async() - .await; - - let test_transaction = create_mock_transaction(); - - let mut signer = TurnkeySigner::new( - "invalid_api_public_key".to_string(), - "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(), // Valid hex private key format - "invalid_org_id".to_string(), - "invalid_private_key_id".to_string(), - "11111111111111111111111111111111".to_string(), - Pubkey::from_str(&"11111111111111111111111111111111").unwrap(), - ); - - signer.api_base_url = server.url(); - - // Test API error handling - let result = signer.sign(&test_transaction).await; - assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), TurnkeyError::ApiError(_))); - } - - #[tokio::test] - async fn test_sign_rate_limit_error() { - let mut server = Server::new_async().await; - - let rate_limit_response = r#"{ - "code": 8, - "message": "", - "details": [], - "turnkeyErrorCode": "" - }"#; - - let _mock = server - .mock("POST", "/public/v1/submit/sign_raw_payload") - .with_status(429) - .with_header("content-type", "application/json") - .with_body(rate_limit_response) - .create_async() - .await; - - let test_transaction = create_mock_transaction(); - - let mut signer = TurnkeySigner::new( - "test_api_public_key".to_string(), - "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(), - "test_org_id".to_string(), - "test_private_key_id".to_string(), - "11111111111111111111111111111111".to_string(), - Pubkey::from_str(&"11111111111111111111111111111111").unwrap(), - ); - - signer.api_base_url = server.url(); - - // Test that 429 rate limit is properly handled - let result = signer.sign(&test_transaction).await; - assert!(result.is_err()); - - match result.unwrap_err() { - TurnkeyError::ApiError(status) => { - assert_eq!(status, 429); - } - _ => panic!("Expected ApiError with 429 status"), - } - } - - #[tokio::test] - async fn test_sign_solana_success() { - let mut server = Server::new_async().await; - - // Mocked response from Turnkey API (same as sign) - let mock_response = r#"{ - "activity": { - "id": "12345678-1234-1234-1234-123456789012", - "organizationId": "test_org_id", - "status": "ACTIVITY_STATUS_COMPLETED", - "type": "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2", - "timestampMs": "1640995200000", - "result": { - "signRawPayloadResult": { - "r": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", - "s": "fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321" - } - } - } - }"#; - - let _mock = server - .mock("POST", "/public/v1/submit/sign_raw_payload") - .with_status(200) - .with_header("content-type", "application/json") - .with_body(mock_response) - .create_async() - .await; - - let test_transaction = create_mock_transaction(); - - let mut signer = TurnkeySigner::new( - "test_api_public_key".to_string(), - "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(), // Valid hex private key - "test_org_id".to_string(), - "test_private_key_id".to_string(), - "11111111111111111111111111111111".to_string(), - Pubkey::from_str(&"11111111111111111111111111111111").unwrap(), - ); - - signer.api_base_url = server.url(); - - // Test successful signing returns Signature - let result = signer.sign_solana(&test_transaction).await; - assert!(result.is_ok()); - let signature = result.unwrap(); - // Check signature length as string representation - assert_eq!(signature.to_string().len(), 87); // Base58 encoded signature length - } - - #[test] - fn test_create_stamp() { - let signer = TurnkeySigner::new( - "test_api_public_key".to_string(), - "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(), // Valid hex private key - "test_org_id".to_string(), - "test_private_key_id".to_string(), - "11111111111111111111111111111111".to_string(), - Pubkey::from_str(&"11111111111111111111111111111111").unwrap(), - ); - - let test_message = r#"{"test": "message"}"#; - - let result = signer.create_stamp(test_message); - assert!(result.is_ok()); - let stamp = result.unwrap(); - - // Stamp should be base64 encoded JSON containing public_key, signature, scheme - assert!(!stamp.is_empty()); - - // Decode and verify stamp structure - let decoded = - base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(stamp.as_bytes()).unwrap(); - let stamp_json: serde_json::Value = serde_json::from_slice(&decoded).unwrap(); - assert_eq!(stamp_json["public_key"], "test_api_public_key"); - assert_eq!(stamp_json["scheme"], "SIGNATURE_SCHEME_TK_API_P256"); - assert!(stamp_json["signature"].is_string()); - } - - #[test] - fn test_signature_component_padding() { - // Test the signature component padding logic - // Turnkey returns variable-length r and s components that need padding to 32 bytes - - // Test cases for different component lengths: - // - Short components (< 32 bytes) should be left-padded with zeros - // - Full 32-byte components should remain unchanged - // - Components > 32 bytes should be rejected with error - - // Test short components (< 32 bytes) - should be left-padded with zeros - let short_r = "1234"; // 2 bytes hex = 1 byte actual - let r_bytes = hex::decode(short_r).unwrap(); - assert!(r_bytes.len() < 32); - - // Create properly padded 32-byte array (simulating the logic from sign method) - let mut final_r = [0u8; 32]; - final_r[32 - r_bytes.len()..].copy_from_slice(&r_bytes); - - // Verify padding: should have zeros at the beginning - assert_eq!(final_r[0], 0); - assert_eq!(final_r[30], 0x12); // First byte of "1234" - assert_eq!(final_r[31], 0x34); // Second byte of "1234" - - // Test full 32-byte components - should remain unchanged - let full_r = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; // 32 bytes - let full_r_bytes = hex::decode(full_r).unwrap(); - assert_eq!(full_r_bytes.len(), 32); - - let mut final_full_r = [0u8; 32]; - final_full_r[32 - full_r_bytes.len()..].copy_from_slice(&full_r_bytes); - assert_eq!(&final_full_r[..], &full_r_bytes[..]); - - // Test components > 32 bytes - should be rejected - let long_r = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12"; // 33 bytes - let long_r_bytes = hex::decode(long_r).unwrap(); - assert!(long_r_bytes.len() > 32); - } -} diff --git a/crates/lib/src/signer/turnkey/types.rs b/crates/lib/src/signer/turnkey/types.rs deleted file mode 100644 index 3c71d31e..00000000 --- a/crates/lib/src/signer/turnkey/types.rs +++ /dev/null @@ -1,133 +0,0 @@ -use hex::FromHexError; -use p256::ecdsa::signature; -use reqwest::Client; -use serde::{Deserialize, Serialize}; -use solana_sdk::pubkey::Pubkey; - -#[derive(Clone, Debug)] -pub struct TurnkeySigner { - pub organization_id: String, - pub private_key_id: String, - pub api_public_key: String, - pub api_private_key: String, - pub public_key: String, - pub pubkey: Pubkey, - pub api_base_url: String, - pub client: Client, -} - -#[derive(Serialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct SignRequest { - #[serde(rename = "type")] - pub activity_type: String, - pub timestamp_ms: String, - pub organization_id: String, - pub parameters: SignParameters, -} - -#[derive(Serialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct SignParameters { - pub sign_with: String, - pub payload: String, - pub encoding: String, - pub hash_function: String, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct ActivityResponse { - pub activity: Activity, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct Activity { - pub result: Option, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct ActivityResult { - pub sign_raw_payload_result: Option, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct SignResult { - pub r: String, - pub s: String, -} - -#[derive(Debug)] -pub enum TurnkeyError { - ApiError(u16), - RequestError(reqwest::Error), - JsonError(serde_json::Error), - InvalidSignature, - InvalidHex(FromHexError), - InvalidStamp(anyhow::Error), - SigningKeyError(signature::Error), - InvalidResponse, - InvalidPrivateKeyLength, - InvalidSignatureLength, - InvalidPublicKey(String), - Other(anyhow::Error), -} - -impl std::fmt::Display for TurnkeyError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - TurnkeyError::ApiError(status) => write!(f, "API error: {status}"), - TurnkeyError::InvalidResponse => write!(f, "Invalid response"), - TurnkeyError::InvalidPublicKey(msg) => write!(f, "Invalid public key: {msg}"), - TurnkeyError::InvalidSignature => write!(f, "Invalid signature"), - TurnkeyError::InvalidPrivateKeyLength => write!(f, "Invalid private key length"), - TurnkeyError::InvalidSignatureLength => write!(f, "Invalid signature length"), - TurnkeyError::RequestError(e) => write!(f, "Request error: {e}"), - TurnkeyError::JsonError(e) => write!(f, "JSON error: {e}"), - TurnkeyError::InvalidStamp(e) => write!(f, "Invalid stamp: {e}"), - TurnkeyError::InvalidHex(e) => write!(f, "Invalid Hex: {e}"), - TurnkeyError::SigningKeyError(e) => write!(f, "Signing key error: {e}"), - TurnkeyError::Other(e) => write!(f, "{e}"), - } - } -} - -impl std::error::Error for TurnkeyError {} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_turnkey_error_display() { - let error = TurnkeyError::ApiError(429); - assert_eq!(error.to_string(), "API error: 429"); - - let error = TurnkeyError::InvalidResponse; - assert_eq!(error.to_string(), "Invalid response"); - - let error = TurnkeyError::InvalidSignature; - assert_eq!(error.to_string(), "Invalid signature"); - - let error = TurnkeyError::InvalidPublicKey("test error".to_string()); - assert_eq!(error.to_string(), "Invalid public key: test error"); - } - - #[test] - fn test_turnkey_error_conversion_to_kora_error() { - use crate::error::KoraError; - - let turnkey_error = TurnkeyError::ApiError(429); - let kora_error: KoraError = turnkey_error.into(); - - match kora_error { - KoraError::SigningError(msg) => { - assert_eq!(msg, "API error: 429"); - } - _ => panic!("Expected SigningError"), - } - } -} diff --git a/crates/lib/src/signer/vault/config.rs b/crates/lib/src/signer/vault/config.rs deleted file mode 100644 index d263b7e1..00000000 --- a/crates/lib/src/signer/vault/config.rs +++ /dev/null @@ -1,224 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use crate::{ - error::KoraError, - signer::{ - config_trait::SignerConfigTrait, utils::get_env_var_for_signer, - vault::vault_signer::VaultSigner, KoraSigner, - }, -}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct VaultSignerConfig { - /// Environment variable for Vault server address - pub addr_env: String, - /// Environment variable for Vault authentication token - pub token_env: String, - /// Environment variable for Vault key name - pub key_name_env: String, - /// Environment variable for Vault public key - pub pubkey_env: String, -} -pub struct VaultSignerHandler; - -impl SignerConfigTrait for VaultSignerHandler { - type Config = VaultSignerConfig; - - fn validate_config(config: &Self::Config, signer_name: &str) -> Result<(), KoraError> { - let env_vars = [ - ("addr_env", &config.addr_env), - ("token_env", &config.token_env), - ("key_name_env", &config.key_name_env), - ("pubkey_env", &config.pubkey_env), - ]; - - for (field_name, env_var) in env_vars { - if env_var.is_empty() { - return Err(KoraError::ValidationError(format!( - "Vault signer '{signer_name}' must specify non-empty {field_name}" - ))); - } - } - - Ok(()) - } - - fn build_from_config( - config: &Self::Config, - signer_name: &str, - ) -> Result { - let addr = get_env_var_for_signer(&config.addr_env, signer_name)?; - let token = get_env_var_for_signer(&config.token_env, signer_name)?; - let key_name = get_env_var_for_signer(&config.key_name_env, signer_name)?; - let pubkey = get_env_var_for_signer(&config.pubkey_env, signer_name)?; - - let signer = VaultSigner::new(addr, token, key_name, pubkey).map_err(|e| { - KoraError::ValidationError(format!( - "Failed to create Vault signer '{signer_name}': {e}" - )) - })?; - - Ok(KoraSigner::Vault(signer)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::tests::config_mock::ConfigMockBuilder; - use std::env; - - #[test] - fn test_validate_config_valid() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - let config = VaultSignerConfig { - addr_env: "VALID_VAULT_ADDR".to_string(), - token_env: "VALID_VAULT_TOKEN".to_string(), - key_name_env: "VALID_KEY_NAME".to_string(), - pubkey_env: "VALID_PUBKEY".to_string(), - }; - - let result = VaultSignerHandler::validate_config(&config, "test_signer"); - assert!(result.is_ok()); - } - - #[test] - fn test_validate_config_empty_addr() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - let config = VaultSignerConfig { - addr_env: "".to_string(), - token_env: "VALID_VAULT_TOKEN".to_string(), - key_name_env: "VALID_KEY_NAME".to_string(), - pubkey_env: "VALID_PUBKEY".to_string(), - }; - - let result = VaultSignerHandler::validate_config(&config, "test_signer"); - assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), KoraError::ValidationError(_))); - } - - #[test] - fn test_validate_config_empty_token() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - let config = VaultSignerConfig { - addr_env: "VALID_VAULT_ADDR".to_string(), - token_env: "".to_string(), - key_name_env: "VALID_KEY_NAME".to_string(), - pubkey_env: "VALID_PUBKEY".to_string(), - }; - - let result = VaultSignerHandler::validate_config(&config, "test_signer"); - assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), KoraError::ValidationError(_))); - } - - #[test] - fn test_validate_config_empty_key_name() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - let config = VaultSignerConfig { - addr_env: "VALID_VAULT_ADDR".to_string(), - token_env: "VALID_VAULT_TOKEN".to_string(), - key_name_env: "".to_string(), - pubkey_env: "VALID_PUBKEY".to_string(), - }; - - let result = VaultSignerHandler::validate_config(&config, "test_signer"); - assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), KoraError::ValidationError(_))); - } - - #[test] - fn test_validate_config_empty_pubkey() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - let config = VaultSignerConfig { - addr_env: "VALID_VAULT_ADDR".to_string(), - token_env: "VALID_VAULT_TOKEN".to_string(), - key_name_env: "VALID_KEY_NAME".to_string(), - pubkey_env: "".to_string(), - }; - - let result = VaultSignerHandler::validate_config(&config, "test_signer"); - assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), KoraError::ValidationError(_))); - } - - #[test] - fn test_build_from_config_missing_env_vars() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - // Ensure env vars don't exist - env::remove_var("NONEXISTENT_VAULT_ADDR"); - env::remove_var("NONEXISTENT_VAULT_TOKEN"); - env::remove_var("NONEXISTENT_KEY_NAME"); - env::remove_var("NONEXISTENT_PUBKEY"); - - let config = VaultSignerConfig { - addr_env: "NONEXISTENT_VAULT_ADDR".to_string(), - token_env: "NONEXISTENT_VAULT_TOKEN".to_string(), - key_name_env: "NONEXISTENT_KEY_NAME".to_string(), - pubkey_env: "NONEXISTENT_PUBKEY".to_string(), - }; - - let result = VaultSignerHandler::build_from_config(&config, "test_signer"); - assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), KoraError::ValidationError(_))); - } - - #[test] - fn test_build_from_config_partial_env_vars() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - // Set only some environment variables - env::set_var("TEST_VAULT_ADDR_PARTIAL", "https://vault.example.com"); - env::set_var("TEST_VAULT_TOKEN_PARTIAL", "test_token"); - env::remove_var("MISSING_KEY_NAME"); - env::remove_var("MISSING_PUBKEY"); - - let config = VaultSignerConfig { - addr_env: "TEST_VAULT_ADDR_PARTIAL".to_string(), - token_env: "TEST_VAULT_TOKEN_PARTIAL".to_string(), - key_name_env: "MISSING_KEY_NAME".to_string(), - pubkey_env: "MISSING_PUBKEY".to_string(), - }; - - let result = VaultSignerHandler::build_from_config(&config, "test_signer"); - assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), KoraError::ValidationError(_))); - - // Clean up - env::remove_var("TEST_VAULT_ADDR_PARTIAL"); - env::remove_var("TEST_VAULT_TOKEN_PARTIAL"); - } - - #[test] - fn test_build_from_config_valid_env_vars() { - let _m = ConfigMockBuilder::new().build_and_setup(); - - // Set test environment variables with valid values - env::set_var("TEST_VAULT_ADDR_VALID", "https://vault.example.com"); - env::set_var("TEST_VAULT_TOKEN_VALID", "test_token"); - env::set_var("TEST_VAULT_KEY_NAME_VALID", "test_key"); - env::set_var("TEST_VAULT_PUBKEY_VALID", "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM"); - - let config = VaultSignerConfig { - addr_env: "TEST_VAULT_ADDR_VALID".to_string(), - token_env: "TEST_VAULT_TOKEN_VALID".to_string(), - key_name_env: "TEST_VAULT_KEY_NAME_VALID".to_string(), - pubkey_env: "TEST_VAULT_PUBKEY_VALID".to_string(), - }; - - let result = VaultSignerHandler::build_from_config(&config, "test_signer"); - assert!(result.is_ok()); - - // Clean up - env::remove_var("TEST_VAULT_ADDR_VALID"); - env::remove_var("TEST_VAULT_TOKEN_VALID"); - env::remove_var("TEST_VAULT_KEY_NAME_VALID"); - env::remove_var("TEST_VAULT_PUBKEY_VALID"); - } -} diff --git a/crates/lib/src/signer/vault/mod.rs b/crates/lib/src/signer/vault/mod.rs deleted file mode 100644 index 257a9cfd..00000000 --- a/crates/lib/src/signer/vault/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod config; -pub mod vault_signer; diff --git a/crates/lib/src/signer/vault/vault_signer.rs b/crates/lib/src/signer/vault/vault_signer.rs deleted file mode 100644 index 893697dd..00000000 --- a/crates/lib/src/signer/vault/vault_signer.rs +++ /dev/null @@ -1,156 +0,0 @@ -use base64::{engine::general_purpose::STANDARD, Engine as _}; -use bs58; -use solana_sdk::{pubkey::Pubkey, signature::Signature, transaction::VersionedTransaction}; -use std::sync::Arc; -use vaultrs::{ - client::{VaultClient, VaultClientSettingsBuilder}, - transit, -}; - -use crate::{error::KoraError, Signature as KoraSignature}; - -#[derive(Clone)] -pub struct VaultSigner { - client: Arc, - key_name: String, - pubkey: Pubkey, -} - -impl std::fmt::Debug for VaultSigner { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("VaultSigner") - .field("key_name", &self.key_name) - .field("pubkey", &self.pubkey) - .finish() - } -} - -impl VaultSigner { - pub fn new( - vault_addr: String, - token: String, - key_name: String, - pubkey: String, - ) -> Result { - let client = VaultClient::new( - VaultClientSettingsBuilder::default() - .address(vault_addr) - .token(token) - .build() - .map_err(|e| { - KoraError::SigningError(format!("Failed to create Vault client: {e}")) - })?, - ); - - let pubkey = Pubkey::try_from( - bs58::decode(pubkey) - .into_vec() - .map_err(|e| KoraError::SigningError(format!("Invalid public key: {e}")))? - .as_slice(), - ) - .map_err(|e| KoraError::SigningError(format!("Invalid public key: {e}")))?; - - Ok(Self { - client: Arc::new(client.map_err(|e| { - KoraError::SigningError(format!("Failed to create Vault client: {e}")) - })?), - key_name, - pubkey, - }) - } - - pub fn solana_pubkey(&self) -> Pubkey { - self.pubkey - } -} - -impl VaultSigner { - pub async fn sign( - &self, - transaction: &VersionedTransaction, - ) -> Result { - let signature = transit::data::sign( - self.client.as_ref(), - "transit", - &self.key_name, - &STANDARD.encode(transaction.message.serialize()), - None, - ) - .await - .map_err(|e| KoraError::SigningError(format!("Failed to sign with Vault: {e}")))?; - - let sig_bytes = STANDARD - .decode(signature.signature) - .map_err(|e| KoraError::SigningError(format!("Failed to decode signature: {e}")))?; - - Ok(KoraSignature { bytes: sig_bytes, is_partial: false }) - } - - pub async fn sign_solana( - &self, - transaction: &VersionedTransaction, - ) -> Result { - let signature = transit::data::sign( - self.client.as_ref(), - "transit", - &self.key_name, - &STANDARD.encode(transaction.message.serialize()), - None, - ) - .await - .map_err(|e| KoraError::SigningError(format!("Failed to sign with Vault: {e}")))?; - - let sig_bytes = STANDARD - .decode(signature.signature) - .map_err(|e| KoraError::SigningError(format!("Failed to decode signature: {e}")))?; - - Signature::try_from(sig_bytes.as_slice()) - .map_err(|e| KoraError::SigningError(format!("Invalid signature format: {e}"))) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_new_vault_signer() { - let vault_addr = "https://vault.example.com".to_string(); - let token = "test_token".to_string(); - let key_name = "test_key".to_string(); - let pubkey = "11111111111111111111111111111111".to_string(); - - let result = VaultSigner::new(vault_addr, token, key_name.clone(), pubkey); - - assert!(result.is_ok()); - let signer = result.unwrap(); - assert_eq!(signer.key_name, key_name); - assert_eq!(signer.pubkey.to_string(), "11111111111111111111111111111111"); - } - - #[test] - fn test_new_vault_signer_invalid_pubkey() { - let vault_addr = "https://vault.example.com".to_string(); - let token = "test_token".to_string(); - let key_name = "test_key".to_string(); - let invalid_pubkey = "invalid_pubkey".to_string(); - - let result = VaultSigner::new(vault_addr, token, key_name, invalid_pubkey); - - assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("Invalid public key")); - } - - #[test] - fn test_solana_pubkey() { - let vault_addr = "https://vault.example.com".to_string(); - let token = "test_token".to_string(); - let key_name = "test_key".to_string(); - let pubkey = "11111111111111111111111111111111".to_string(); - - let signer = VaultSigner::new(vault_addr, token, key_name, pubkey).unwrap(); - let retrieved_pubkey = signer.solana_pubkey(); - - assert_eq!(retrieved_pubkey.to_string(), "11111111111111111111111111111111"); - } -} diff --git a/crates/lib/src/state.rs b/crates/lib/src/state.rs index 5f2fb4a4..b10bd16d 100644 --- a/crates/lib/src/state.rs +++ b/crates/lib/src/state.rs @@ -5,11 +5,7 @@ use std::sync::{ Arc, }; -use crate::{ - config::Config, - error::KoraError, - signer::{KoraSigner, SignerPool}, -}; +use crate::{config::Config, error::KoraError, signer::SignerPool}; // Global signer pool (for multi-signer support) static GLOBAL_SIGNER_POOL: Lazy>>> = Lazy::new(|| RwLock::new(None)); @@ -20,20 +16,20 @@ static GLOBAL_CONFIG: AtomicPtr = AtomicPtr::new(std::ptr::null_mut()); /// Get a request-scoped signer with optional signer_key for consistency across related calls pub fn get_request_signer_with_signer_key( signer_key: Option<&str>, -) -> Result, KoraError> { +) -> Result, KoraError> { let pool = get_signer_pool()?; // If client provided a signer signer_key, try to use that specific signer if let Some(signer_key) = signer_key { let signer_meta = pool.get_signer_by_pubkey(signer_key)?; - return Ok(Arc::new(signer_meta.signer.clone())); + return Ok(Arc::clone(&signer_meta.signer)); } // Default behavior: use next signer from round-robin let signer_meta = pool.get_next_signer().map_err(|e| { KoraError::InternalServerError(format!("Failed to get signer from pool: {e}")) })?; - Ok(Arc::new(signer_meta.signer.clone())) + Ok(Arc::clone(&signer_meta.signer)) } /// Initialize the global signer pool with a SignerPool instance diff --git a/crates/lib/src/tests/common/mod.rs b/crates/lib/src/tests/common/mod.rs index 9e583aec..760cbfa1 100644 --- a/crates/lib/src/tests/common/mod.rs +++ b/crates/lib/src/tests/common/mod.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + /// Common test utilities and centralized re-exports /// /// This module provides: @@ -5,7 +7,7 @@ /// 2. Centralized re-exports of commonly used mock utilities use crate::{ get_request_signer_with_signer_key, - signer::{KoraSigner, SignerPool, SignerWithMetadata, SolanaMemorySigner}, + signer::{SignerPool, SignerWithMetadata}, state::{get_config, update_config, update_signer_pool}, tests::{account_mock, config_mock::ConfigMockBuilder, rpc_mock}, usage_limit::UsageTracker, @@ -16,22 +18,24 @@ use solana_sdk::{pubkey::Pubkey, signature::Keypair}; // Re-export mock utilities for centralized access pub use account_mock::*; pub use rpc_mock::*; +use solana_signers::{Signer, SolanaSigner}; /// Setup or retrieve test signer for global state initialization /// /// Returns the signer's public key. pub fn setup_or_get_test_signer() -> Pubkey { if let Ok(signer) = get_request_signer_with_signer_key(None) { - return signer.solana_pubkey(); + return signer.pubkey(); } let test_keypair = Keypair::new(); - let signer = SolanaMemorySigner::new(test_keypair.insecure_clone()); + // Create external signer and wrap with adapter + let external_signer = Signer::from_memory(&test_keypair.to_base58_string()).unwrap(); let pool = SignerPool::new(vec![SignerWithMetadata::new( "test_signer".to_string(), - KoraSigner::Memory(signer.clone()), + Arc::new(external_signer), 1, )]); @@ -42,7 +46,7 @@ pub fn setup_or_get_test_signer() -> Pubkey { } } - signer.solana_pubkey() + solana_sdk::signer::Signer::pubkey(&test_keypair) } /// Setup or retrieve test config for global state initialization diff --git a/crates/lib/src/tests/config_mock.rs b/crates/lib/src/tests/config_mock.rs index 5ece2caf..9c2e3c9b 100644 --- a/crates/lib/src/tests/config_mock.rs +++ b/crates/lib/src/tests/config_mock.rs @@ -7,14 +7,9 @@ use crate::{ constant::DEFAULT_MAX_REQUEST_BODY_SIZE, fee::price::PriceConfig, oracle::PriceSource, - signer::{ - config::{ - SelectionStrategy, SignerConfig, SignerPoolConfig, SignerPoolSettings, SignerTypeConfig, - }, - memory_signer::config::MemorySignerConfig, - privy::config::PrivySignerConfig, - turnkey::config::TurnkeySignerConfig, - vault::config::VaultSignerConfig, + signer::config::{ + MemorySignerConfig, PrivySignerConfig, SelectionStrategy, SignerConfig, SignerPoolConfig, + SignerPoolSettings, SignerTypeConfig, TurnkeySignerConfig, VaultSignerConfig, }, }; use solana_sdk::pubkey::Pubkey; @@ -687,7 +682,12 @@ impl SignerPoolConfigBuilder { name, weight, config: SignerTypeConfig::Vault { - config: VaultSignerConfig { addr_env, token_env, key_name_env, pubkey_env }, + config: VaultSignerConfig { + vault_addr_env: addr_env, + vault_token_env: token_env, + key_name_env, + pubkey_env, + }, }, }; self.config.signers.push(signer); diff --git a/crates/lib/src/transaction/versioned_transaction.rs b/crates/lib/src/transaction/versioned_transaction.rs index a92e984e..438d9848 100644 --- a/crates/lib/src/transaction/versioned_transaction.rs +++ b/crates/lib/src/transaction/versioned_transaction.rs @@ -8,6 +8,7 @@ use solana_sdk::{ pubkey::Pubkey, transaction::VersionedTransaction, }; +use solana_signers::{Signer, SolanaSigner}; use std::{collections::HashMap, ops::Deref}; use solana_transaction_status_client_types::{UiInstruction, UiTransactionEncoding}; @@ -15,14 +16,13 @@ use solana_transaction_status_client_types::{UiInstruction, UiTransactionEncodin use crate::{ error::KoraError, fee::fee::{FeeConfigUtil, TransactionFeeUtil}, - signer::KoraSigner, state::get_config, transaction::{ instruction_util::IxUtils, ParsedSPLInstructionData, ParsedSPLInstructionType, ParsedSystemInstructionData, ParsedSystemInstructionType, }, validator::transaction_validator::TransactionValidator, - CacheUtil, Signer, + CacheUtil, }; use solana_address_lookup_table_interface::state::AddressLookupTable; @@ -60,17 +60,17 @@ pub trait VersionedTransactionOps { async fn sign_transaction( &mut self, - signer: &std::sync::Arc, + signer: &std::sync::Arc, rpc_client: &RpcClient, ) -> Result<(VersionedTransaction, String), KoraError>; async fn sign_transaction_if_paid( &mut self, - signer: &std::sync::Arc, + signer: &std::sync::Arc, rpc_client: &RpcClient, ) -> Result<(VersionedTransaction, String), KoraError>; async fn sign_and_send_transaction( &mut self, - signer: &std::sync::Arc, + signer: &std::sync::Arc, rpc_client: &RpcClient, ) -> Result<(String, String), KoraError>; } @@ -241,10 +241,11 @@ impl VersionedTransactionOps for VersionedTransactionResolved { async fn sign_transaction( &mut self, - signer: &std::sync::Arc, + signer: &std::sync::Arc, rpc_client: &RpcClient, ) -> Result<(VersionedTransaction, String), KoraError> { - let validator = TransactionValidator::new(signer.solana_pubkey())?; + let fee_payer = signer.pubkey(); + let validator = TransactionValidator::new(fee_payer)?; // Validate transaction and accounts (already resolved) validator.validate_transaction(self).await?; @@ -264,10 +265,14 @@ impl VersionedTransactionOps for VersionedTransactionResolved { validator.validate_lamport_fee(estimated_fee)?; // Sign transaction - let signature = signer.sign_solana(&transaction).await?; + let message_bytes = transaction.message.serialize(); + let signature = signer + .sign_message(&message_bytes) + .await + .map_err(|e| KoraError::SigningError(e.to_string()))?; // Find the fee payer position - don't assume it's at position 0 - let fee_payer_position = self.find_signer_position(&signer.solana_pubkey())?; + let fee_payer_position = self.find_signer_position(&fee_payer)?; transaction.signatures[fee_payer_position] = signature; // Serialize signed transaction @@ -279,10 +284,10 @@ impl VersionedTransactionOps for VersionedTransactionResolved { async fn sign_transaction_if_paid( &mut self, - signer: &std::sync::Arc, + signer: &std::sync::Arc, rpc_client: &RpcClient, ) -> Result<(VersionedTransaction, String), KoraError> { - let fee_payer = signer.solana_pubkey(); + let fee_payer = signer.pubkey(); let config = &get_config()?; let fee_calculation = FeeConfigUtil::estimate_kora_fee( @@ -299,7 +304,7 @@ impl VersionedTransactionOps for VersionedTransactionResolved { // Only validate payment if not free if required_lamports > 0 { // Get the expected payment destination - let payment_destination = config.kora.get_payment_address(&signer.solana_pubkey())?; + let payment_destination = config.kora.get_payment_address(&fee_payer)?; // Validate token payment using the resolved transaction TransactionValidator::validate_token_payment( @@ -317,7 +322,7 @@ impl VersionedTransactionOps for VersionedTransactionResolved { async fn sign_and_send_transaction( &mut self, - signer: &std::sync::Arc, + signer: &std::sync::Arc, rpc_client: &RpcClient, ) -> Result<(String, String), KoraError> { let (transaction, encoded) = self.sign_transaction(signer, rpc_client).await?; diff --git a/crates/lib/src/usage_limit/usage_tracker.rs b/crates/lib/src/usage_limit/usage_tracker.rs index 1fd45524..2ed432a8 100644 --- a/crates/lib/src/usage_limit/usage_tracker.rs +++ b/crates/lib/src/usage_limit/usage_tracker.rs @@ -3,6 +3,7 @@ use std::{collections::HashSet, sync::Arc}; use deadpool_redis::Runtime; use redis::AsyncCommands; use solana_sdk::{pubkey::Pubkey, transaction::VersionedTransaction}; +use solana_signers::SolanaSigner; use tokio::sync::OnceCell; use super::usage_store::{RedisUsageStore, UsageStore}; @@ -171,7 +172,7 @@ impl UsageTracker { ); let kora_signers = - get_all_signers()?.iter().map(|signer| signer.signer.solana_pubkey()).collect(); + get_all_signers()?.iter().map(|signer| signer.signer.pubkey()).collect(); let store = Arc::new(RedisUsageStore::new(pool)); Some(UsageTracker::new( diff --git a/crates/lib/src/validator/signer_validator.rs b/crates/lib/src/validator/signer_validator.rs index d228cd38..df61daab 100644 --- a/crates/lib/src/validator/signer_validator.rs +++ b/crates/lib/src/validator/signer_validator.rs @@ -70,9 +70,8 @@ impl SignerValidator { #[cfg(test)] mod tests { use super::*; - use crate::signer::{ - config::SignerPoolSettings, memory_signer::config::MemorySignerConfig, SignerConfig, - SignerTypeConfig, + use crate::signer::config::{ + MemorySignerConfig, SignerConfig, SignerPoolSettings, SignerTypeConfig, }; #[test] From ef8fc5a6d699d9fbb80ef5b7175c383ab2d925f0 Mon Sep 17 00:00:00 2001 From: jo <17280917+dev-jodee@users.noreply.github.com> Date: Fri, 10 Oct 2025 15:46:31 -0400 Subject: [PATCH 13/29] =?UTF-8?q?chore:=20(PRO-411)=20(PRO-415)=20Clean=20?= =?UTF-8?q?up=20and=20sanitize=20DEBUG=20macros,=20logger=E2=80=A6=20(#232?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: (PRO-411) (PRO-415) Clean up and sanitize DEBUG macros, loggers, etc. to prevent sensitive information from being displayed --- crates/lib/Cargo.toml | 3 + crates/lib/src/cache.rs | 39 ++++-- crates/lib/src/config.rs | 34 +++-- crates/lib/src/error.rs | 134 ++++++++++++++++--- crates/lib/src/mod.rs | 1 + crates/lib/src/oracle/jupiter.rs | 15 +-- crates/lib/src/rpc_server/args.rs | 4 +- crates/lib/src/rpc_server/auth.rs | 4 +- crates/lib/src/sanitize.rs | 91 +++++++++++++ crates/lib/src/signer/config.rs | 46 +++++-- crates/lib/src/signer/keypair_util.rs | 34 +++-- crates/lib/src/usage_limit/usage_store.rs | 39 ++++-- crates/lib/src/usage_limit/usage_tracker.rs | 20 ++- crates/lib/src/validator/config_validator.rs | 2 - 14 files changed, 366 insertions(+), 100 deletions(-) create mode 100644 crates/lib/src/sanitize.rs diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml index a855fcd7..df439514 100644 --- a/crates/lib/Cargo.toml +++ b/crates/lib/Cargo.toml @@ -83,6 +83,9 @@ regex = "1.11.1" [features] default = [] docs = [] +# WARNING: unsafe-debug enables verbose error/debug output that may expose sensitive data +# NEVER use this feature in production environments +unsafe-debug = [] [dev-dependencies] tempfile = "3.2" diff --git a/crates/lib/src/cache.rs b/crates/lib/src/cache.rs index 8a0ee459..d4711b4a 100644 --- a/crates/lib/src/cache.rs +++ b/crates/lib/src/cache.rs @@ -5,7 +5,7 @@ use solana_client::nonblocking::rpc_client::RpcClient; use solana_sdk::{account::Account, pubkey::Pubkey}; use tokio::sync::OnceCell; -use crate::error::KoraError; +use crate::{error::KoraError, sanitize_error}; #[cfg(not(test))] use crate::state::get_config; @@ -38,20 +38,29 @@ impl CacheUtil { let cfg = deadpool_redis::Config::from_url(redis_url); let pool = cfg.create_pool(Some(Runtime::Tokio1)).map_err(|e| { - KoraError::InternalServerError(format!("Failed to create cache pool: {e}")) + KoraError::InternalServerError(format!( + "Failed to create cache pool: {}", + sanitize_error!(e) + )) })?; // Test connection let mut conn = pool.get().await.map_err(|e| { - KoraError::InternalServerError(format!("Failed to connect to cache: {e}")) + KoraError::InternalServerError(format!( + "Failed to connect to cache: {}", + sanitize_error!(e) + )) })?; // Simple connection test - try to get a non-existent key let _: Option = conn.get("__connection_test__").await.map_err(|e| { - KoraError::InternalServerError(format!("Cache connection test failed: {e}")) + KoraError::InternalServerError(format!( + "Cache connection test failed: {}", + sanitize_error!(e) + )) })?; - log::info!("Cache initialized successfully with Redis at {redis_url}"); + log::info!("Cache initialized successfully"); Some(pool) } else { @@ -68,7 +77,10 @@ impl CacheUtil { async fn get_connection(pool: &Pool) -> Result { pool.get().await.map_err(|e| { - KoraError::InternalServerError(format!("Failed to get cache connection: {e}")) + KoraError::InternalServerError(format!( + "Failed to get cache connection: {}", + sanitize_error!(e) + )) }) } @@ -100,7 +112,10 @@ impl CacheUtil { let mut conn = Self::get_connection(pool).await?; let cached_data: Option = conn.get(key).await.map_err(|e| { - KoraError::InternalServerError(format!("Failed to get from cache: {e}")) + KoraError::InternalServerError(format!( + "Failed to get from cache: {}", + sanitize_error!(e) + )) })?; match cached_data { @@ -147,11 +162,17 @@ impl CacheUtil { let mut conn = Self::get_connection(pool).await?; let serialized = serde_json::to_string(data).map_err(|e| { - KoraError::InternalServerError(format!("Failed to serialize cache data: {e}")) + KoraError::InternalServerError(format!( + "Failed to serialize cache data: {}", + sanitize_error!(e) + )) })?; conn.set_ex::<_, _, ()>(key, serialized, ttl_seconds).await.map_err(|e| { - KoraError::InternalServerError(format!("Failed to set cache data: {e}")) + KoraError::InternalServerError(format!( + "Failed to set cache data: {}", + sanitize_error!(e) + )) })?; Ok(()) diff --git a/crates/lib/src/config.rs b/crates/lib/src/config.rs index 9f75cf4a..a6dd0725 100644 --- a/crates/lib/src/config.rs +++ b/crates/lib/src/config.rs @@ -16,9 +16,10 @@ use crate::{ error::KoraError, fee::price::{PriceConfig, PriceModel}, oracle::PriceSource, + sanitize_error, }; -#[derive(Debug, Clone, Deserialize)] +#[derive(Clone, Deserialize)] pub struct Config { pub validation: ValidationConfig, pub kora: KoraConfig, @@ -26,7 +27,7 @@ pub struct Config { pub metrics: MetricsConfig, } -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Clone, Serialize, Deserialize, ToSchema)] pub struct MetricsConfig { pub enabled: bool, pub endpoint: String, @@ -48,7 +49,7 @@ impl Default for MetricsConfig { } } -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Clone, Serialize, Deserialize, ToSchema)] pub struct FeePayerBalanceMetricsConfig { pub enabled: bool, pub expiry_seconds: u64, @@ -307,7 +308,7 @@ fn default_max_request_body_size() -> usize { DEFAULT_MAX_REQUEST_BODY_SIZE } -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Clone, Serialize, Deserialize, ToSchema)] pub struct CacheConfig { /// Redis URL for caching (e.g., "redis://localhost:6379") pub url: Option, @@ -330,7 +331,7 @@ impl Default for CacheConfig { } } -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Clone, Serialize, Deserialize, ToSchema)] pub struct KoraConfig { pub rate_limit: u64, #[serde(default = "default_max_request_body_size")] @@ -361,7 +362,7 @@ impl Default for KoraConfig { } } -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Clone, Serialize, Deserialize, ToSchema)] pub struct UsageLimitConfig { /// Enable per-wallet usage limiting pub enabled: bool, @@ -384,7 +385,7 @@ impl Default for UsageLimitConfig { } } -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Clone, Serialize, Deserialize, ToSchema)] pub struct AuthConfig { pub api_key: Option, pub hmac_secret: Option, @@ -401,16 +402,25 @@ impl Default for AuthConfig { impl Config { pub fn load_config>(path: P) -> Result { let contents = fs::read_to_string(path).map_err(|e| { - KoraError::InternalServerError(format!("Failed to read config file: {e}")) + KoraError::InternalServerError(format!( + "Failed to read config file: {}", + sanitize_error!(e) + )) })?; let mut config: Config = toml::from_str(&contents).map_err(|e| { - KoraError::InternalServerError(format!("Failed to parse config file: {e}")) + KoraError::InternalServerError(format!( + "Failed to parse config file: {}", + sanitize_error!(e) + )) })?; // Initialize Token2022Config to parse and cache extensions config.validation.token_2022.initialize().map_err(|e| { - KoraError::InternalServerError(format!("Failed to initialize Token2022 config: {e}")) + KoraError::InternalServerError(format!( + "Failed to initialize Token2022 config: {}", + sanitize_error!(e) + )) })?; Ok(config) @@ -422,9 +432,7 @@ impl KoraConfig { pub fn get_payment_address(&self, signer_pubkey: &Pubkey) -> Result { if let Some(payment_address_str) = &self.payment_address { let payment_address = Pubkey::from_str(payment_address_str).map_err(|_| { - KoraError::InternalServerError(format!( - "Invalid payment_address: {payment_address_str}" - )) + KoraError::InternalServerError("Invalid payment_address format".to_string()) })?; Ok(payment_address) } else { diff --git a/crates/lib/src/error.rs b/crates/lib/src/error.rs index 4a55079f..a291ee7a 100644 --- a/crates/lib/src/error.rs +++ b/crates/lib/src/error.rs @@ -1,3 +1,4 @@ +use crate::sanitize::sanitize_message; use jsonrpsee::{core::Error as RpcError, types::error::CallError}; use serde::{Deserialize, Serialize}; use solana_client::client_error::ClientError; @@ -69,61 +70,132 @@ pub enum KoraError { impl From for KoraError { fn from(e: ClientError) -> Self { let error_string = e.to_string(); + let sanitized_error_string = sanitize_message(&error_string); if error_string.contains("AccountNotFound") || error_string.contains("could not find account") { - KoraError::AccountNotFound(error_string) + #[cfg(feature = "unsafe-debug")] + { + KoraError::AccountNotFound(error_string) + } + #[cfg(not(feature = "unsafe-debug"))] + { + KoraError::AccountNotFound(sanitized_error_string) + } } else { - KoraError::RpcError(error_string) + #[cfg(feature = "unsafe-debug")] + { + KoraError::RpcError(error_string) + } + #[cfg(not(feature = "unsafe-debug"))] + { + KoraError::RpcError(sanitized_error_string) + } } } } impl From for KoraError { - fn from(e: SignerError) -> Self { - KoraError::SigningError(e.to_string()) + fn from(_e: SignerError) -> Self { + #[cfg(feature = "unsafe-debug")] + { + KoraError::SigningError(_e.to_string()) + } + #[cfg(not(feature = "unsafe-debug"))] + { + KoraError::SigningError(sanitize_message(&_e.to_string())) + } } } impl From for KoraError { - fn from(e: bincode::Error) -> Self { - KoraError::SerializationError(e.to_string()) + fn from(_e: bincode::Error) -> Self { + #[cfg(feature = "unsafe-debug")] + { + KoraError::SerializationError(_e.to_string()) + } + #[cfg(not(feature = "unsafe-debug"))] + { + KoraError::SerializationError(sanitize_message(&_e.to_string())) + } } } impl From for KoraError { - fn from(e: bs58::decode::Error) -> Self { - KoraError::SerializationError(e.to_string()) + fn from(_e: bs58::decode::Error) -> Self { + #[cfg(feature = "unsafe-debug")] + { + KoraError::SerializationError(_e.to_string()) + } + #[cfg(not(feature = "unsafe-debug"))] + { + KoraError::SerializationError(sanitize_message(&_e.to_string())) + } } } impl From for KoraError { - fn from(e: bs58::encode::Error) -> Self { - KoraError::SerializationError(e.to_string()) + fn from(_e: bs58::encode::Error) -> Self { + #[cfg(feature = "unsafe-debug")] + { + KoraError::SerializationError(_e.to_string()) + } + #[cfg(not(feature = "unsafe-debug"))] + { + KoraError::SerializationError(sanitize_message(&_e.to_string())) + } } } impl From for KoraError { - fn from(e: std::io::Error) -> Self { - KoraError::InternalServerError(e.to_string()) + fn from(_e: std::io::Error) -> Self { + #[cfg(feature = "unsafe-debug")] + { + KoraError::InternalServerError(_e.to_string()) + } + #[cfg(not(feature = "unsafe-debug"))] + { + KoraError::InternalServerError(sanitize_message(&_e.to_string())) + } } } impl From> for KoraError { - fn from(e: Box) -> Self { - KoraError::InternalServerError(e.to_string()) + fn from(_e: Box) -> Self { + #[cfg(feature = "unsafe-debug")] + { + KoraError::InternalServerError(_e.to_string()) + } + #[cfg(not(feature = "unsafe-debug"))] + { + KoraError::InternalServerError(sanitize_message(&_e.to_string())) + } } } impl From> for KoraError { - fn from(e: Box) -> Self { - KoraError::InternalServerError(e.to_string()) + fn from(_e: Box) -> Self { + #[cfg(feature = "unsafe-debug")] + { + KoraError::InternalServerError(_e.to_string()) + } + #[cfg(not(feature = "unsafe-debug"))] + { + KoraError::InternalServerError(sanitize_message(&_e.to_string())) + } } } impl From for KoraError { - fn from(err: ProgramError) -> Self { - KoraError::InvalidTransaction(err.to_string()) + fn from(_err: ProgramError) -> Self { + #[cfg(feature = "unsafe-debug")] + { + KoraError::InvalidTransaction(_err.to_string()) + } + #[cfg(not(feature = "unsafe-debug"))] + { + KoraError::InvalidTransaction(sanitize_message(&_err.to_string())) + } } } @@ -191,14 +263,28 @@ impl> IntoKoraResponse for Result { } impl From for KoraError { - fn from(err: anyhow::Error) -> Self { - KoraError::SigningError(err.to_string()) + fn from(_err: anyhow::Error) -> Self { + #[cfg(feature = "unsafe-debug")] + { + KoraError::SigningError(_err.to_string()) + } + #[cfg(not(feature = "unsafe-debug"))] + { + KoraError::SigningError(sanitize_message(&_err.to_string())) + } } } impl From for KoraError { - fn from(err: solana_signers::SignerError) -> Self { - KoraError::SigningError(err.to_string()) + fn from(_err: solana_signers::SignerError) -> Self { + #[cfg(feature = "unsafe-debug")] + { + KoraError::SigningError(_err.to_string()) + } + #[cfg(not(feature = "unsafe-debug"))] + { + KoraError::SigningError(sanitize_message(&_err.to_string())) + } } } @@ -254,6 +340,7 @@ mod tests { let client_error = ClientError::from(std::io::Error::other("test")); let kora_error: KoraError = client_error.into(); assert!(matches!(kora_error, KoraError::RpcError(_))); + // With sanitization, error message context is preserved unless it contains sensitive data if let KoraError::RpcError(msg) = kora_error { assert!(msg.contains("test")); } @@ -264,6 +351,7 @@ mod tests { let signer_error = SignerError::Custom("signing failed".to_string()); let kora_error: KoraError = signer_error.into(); assert!(matches!(kora_error, KoraError::SigningError(_))); + // With sanitization, error message context is preserved unless it contains sensitive data if let KoraError::SigningError(msg) = kora_error { assert!(msg.contains("signing failed")); } @@ -295,6 +383,7 @@ mod tests { let io_error = std::io::Error::other("file not found"); let kora_error: KoraError = io_error.into(); assert!(matches!(kora_error, KoraError::InternalServerError(_))); + // With sanitization, error message context is preserved unless it contains sensitive data if let KoraError::InternalServerError(msg) = kora_error { assert!(msg.contains("file not found")); } @@ -331,6 +420,7 @@ mod tests { let anyhow_error = anyhow::anyhow!("something went wrong"); let kora_error: KoraError = anyhow_error.into(); assert!(matches!(kora_error, KoraError::SigningError(_))); + // With sanitization, error message context is preserved unless it contains sensitive data if let KoraError::SigningError(msg) = kora_error { assert!(msg.contains("something went wrong")); } diff --git a/crates/lib/src/mod.rs b/crates/lib/src/mod.rs index dc688bf2..e94d36e8 100644 --- a/crates/lib/src/mod.rs +++ b/crates/lib/src/mod.rs @@ -13,6 +13,7 @@ pub mod metrics; pub mod oracle; pub mod rpc; pub mod rpc_server; +pub mod sanitize; pub mod signer; pub mod state; pub mod token; diff --git a/crates/lib/src/oracle/jupiter.rs b/crates/lib/src/oracle/jupiter.rs index 0bc6e79e..8aa09f28 100644 --- a/crates/lib/src/oracle/jupiter.rs +++ b/crates/lib/src/oracle/jupiter.rs @@ -2,6 +2,7 @@ use super::{PriceOracle, PriceSource, TokenPrice}; use crate::{ constant::{JUPITER_API_LITE_URL, JUPITER_API_PRO_URL, SOL_MINT}, error::KoraError, + sanitize_error, }; use once_cell::sync::Lazy; use parking_lot::RwLock; @@ -120,10 +121,9 @@ impl JupiterPriceOracle { request = request.header(JUPITER_AUTH_HEADER, key); } - let response = request - .send() - .await - .map_err(|e| KoraError::RpcError(format!("Jupiter API request failed: {e}")))?; + let response = request.send().await.map_err(|e| { + KoraError::RpcError(format!("Jupiter API request failed: {}", sanitize_error!(e))) + })?; if !response.status().is_success() { match response.status() { @@ -139,10 +139,9 @@ impl JupiterPriceOracle { } } - let jupiter_response: JupiterResponse = response - .json() - .await - .map_err(|e| KoraError::RpcError(format!("Failed to parse Jupiter response: {e}")))?; + let jupiter_response: JupiterResponse = response.json().await.map_err(|e| { + KoraError::RpcError(format!("Failed to parse Jupiter response: {}", sanitize_error!(e))) + })?; let sol_price = jupiter_response .get(SOL_MINT) diff --git a/crates/lib/src/rpc_server/args.rs b/crates/lib/src/rpc_server/args.rs index a68a9530..926aed27 100644 --- a/crates/lib/src/rpc_server/args.rs +++ b/crates/lib/src/rpc_server/args.rs @@ -3,7 +3,7 @@ use clap::Parser; use std::path::PathBuf; /// RPC server arguments -#[derive(Debug, Parser)] +#[derive(Parser)] pub struct RpcArgs { /// HTTP port to listen on for RPC requests #[arg(short = 'p', long, default_value = "8080")] @@ -26,7 +26,7 @@ pub struct RpcArgs { pub auth_args: AuthArgs, } -#[derive(Debug, Parser)] +#[derive(Parser)] pub struct AuthArgs { /// API key for authenticating requests to the Kora server (optional) - can be set in `kora.toml` #[arg(long, env = "KORA_API_KEY", help_heading = "Authentication")] diff --git a/crates/lib/src/rpc_server/auth.rs b/crates/lib/src/rpc_server/auth.rs index 4c0e2b4b..4fa36105 100644 --- a/crates/lib/src/rpc_server/auth.rs +++ b/crates/lib/src/rpc_server/auth.rs @@ -180,8 +180,8 @@ where let mut mac = match Hmac::::new_from_slice(secret.as_bytes()) { Ok(mac) => mac, - Err(e) => { - log::error!("Invalid HMAC secret: {e:?}"); + Err(_) => { + log::error!("HMAC authentication failed"); return Ok(unauthorized_response); } }; diff --git a/crates/lib/src/sanitize.rs b/crates/lib/src/sanitize.rs new file mode 100644 index 00000000..6b1b3a16 --- /dev/null +++ b/crates/lib/src/sanitize.rs @@ -0,0 +1,91 @@ +//! Security-focused logging and error message sanitization +//! +//! This module provides utilities to automatically redact sensitive information +//! from error messages and logs, including: +//! - URLs with embedded credentials (any protocol: redis://, postgres://, http://, etc.) +//! - Long hex strings (potential private keys) + +use regex::Regex; +use std::sync::LazyLock; + +/// Regex patterns for detecting sensitive data +static URL_WITH_CREDENTIALS_PATTERN: LazyLock = LazyLock::new(|| { + // Generic URL pattern with embedded credentials: protocol://user:password@host + // Matches any protocol (redis, http, https, postgres, mysql, mongodb, etc.) + Regex::new(r"[a-z][a-z0-9+.-]*://[^:@\s]+:[^@\s]+@[^\s]+").unwrap() +}); + +static HEX_PATTERN: LazyLock = LazyLock::new(|| { + // Long hex strings (likely keys/hashes) - 32+ chars, with optional 0x prefix + Regex::new(r"(?:0x)?[0-9a-fA-F]{32,}").unwrap() +}); + +/// Sanitizes a message by redacting sensitive information +pub fn sanitize_message(message: &str) -> String { + let mut result = message.to_string(); + + result = URL_WITH_CREDENTIALS_PATTERN.replace_all(&result, "[REDACTED_URL]").to_string(); + + result = HEX_PATTERN.replace_all(&result, "[REDACTED_HEX]").to_string(); + + result +} + +/// Sanitizes an error message based on the `unsafe-debug` feature flag +/// +/// - With `unsafe-debug`: Returns the original error message +/// - Without `unsafe-debug`: Returns a sanitized version with sensitive data redacted +#[macro_export] +macro_rules! sanitize_error { + ($error:expr) => {{ + #[cfg(feature = "unsafe-debug")] + { + format!("{}", $error) + } + #[cfg(not(feature = "unsafe-debug"))] + { + $crate::sanitize::sanitize_message(&format!("{}", $error)) + } + }}; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_sanitize_url_with_credentials_redis() { + let msg = "Failed to connect to redis://user:password@localhost:6379"; + let sanitized = sanitize_message(msg); + assert!(sanitized.contains("[REDACTED_URL]")); + assert!(!sanitized.contains("password")); + assert!(!sanitized.contains("redis://user:")); + // Ensure the error message context remains + assert!(sanitized.contains("Failed to connect to")); + } + + #[test] + fn test_sanitize_url_with_credentials_http() { + let msg = "Request failed: https://user:token@api.example.com/endpoint"; + let sanitized = sanitize_message(msg); + assert!(sanitized.contains("[REDACTED_URL]")); + assert!(!sanitized.contains("token")); + assert!(!sanitized.contains("https://user:")); + } + + #[test] + fn test_sanitize_url_with_credentials_postgres() { + let msg = "DB error: postgres://admin:secret123@db.internal:5432/mydb"; + let sanitized = sanitize_message(msg); + assert!(sanitized.contains("[REDACTED_URL]")); + assert!(!sanitized.contains("admin")); + assert!(!sanitized.contains("secret123")); + } + + #[test] + fn test_sanitize_hex_string() { + let msg = "Key: 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; + let sanitized = sanitize_message(msg); + assert!(sanitized.contains("[REDACTED_HEX]")); + } +} diff --git a/crates/lib/src/signer/config.rs b/crates/lib/src/signer/config.rs index c52f28dc..f20ccd0d 100644 --- a/crates/lib/src/signer/config.rs +++ b/crates/lib/src/signer/config.rs @@ -1,10 +1,10 @@ -use crate::{error::KoraError, signer::utils::get_env_var_for_signer}; +use crate::{error::KoraError, sanitize_error, signer::utils::get_env_var_for_signer}; use serde::{Deserialize, Serialize}; use solana_signers::Signer; use std::{fmt, fs, path::Path}; /// Configuration for a pool of signers -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] pub struct SignerPoolConfig { /// Signer pool configuration pub signer_pool: SignerPoolSettings, @@ -45,7 +45,7 @@ fn default_strategy() -> SelectionStrategy { } /// Configuration for an individual signer -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] pub struct SignerConfig { /// Human-readable name for this signer pub name: String, @@ -58,13 +58,13 @@ pub struct SignerConfig { } /// Memory signer configuration (local keypair) -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] pub struct MemorySignerConfig { pub private_key_env: String, } /// Turnkey signer configuration -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] pub struct TurnkeySignerConfig { pub api_public_key_env: String, pub api_private_key_env: String, @@ -74,7 +74,7 @@ pub struct TurnkeySignerConfig { } /// Privy signer configuration -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] pub struct PrivySignerConfig { pub app_id_env: String, pub app_secret_env: String, @@ -82,7 +82,7 @@ pub struct PrivySignerConfig { } /// Vault signer configuration -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] pub struct VaultSignerConfig { pub vault_addr_env: String, pub vault_token_env: String, @@ -91,7 +91,7 @@ pub struct VaultSignerConfig { } /// Signer type-specific configuration -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum SignerTypeConfig { /// Memory signer configuration @@ -120,11 +120,17 @@ impl SignerPoolConfig { /// Load signer pool configuration from TOML file pub fn load_config>(path: P) -> Result { let contents = fs::read_to_string(path).map_err(|e| { - KoraError::InternalServerError(format!("Failed to read config file: {e}")) + KoraError::InternalServerError(format!( + "Failed to read signer config file: {}", + sanitize_error!(e) + )) })?; let config: SignerPoolConfig = toml::from_str(&contents).map_err(|e| { - KoraError::ValidationError(format!("Failed to parse signers config TOML: {e}")) + KoraError::ValidationError(format!( + "Failed to parse signers config TOML: {}", + sanitize_error!(e) + )) })?; config.validate_signer_config()?; @@ -210,7 +216,10 @@ impl SignerConfig { ) -> Result { let private_key = get_env_var_for_signer(&config.private_key_env, signer_name)?; Signer::from_memory(&private_key).map_err(|e| { - KoraError::SigningError(format!("Failed to create memory signer '{signer_name}': {e}")) + KoraError::SigningError(format!( + "Failed to create memory signer '{signer_name}': {}", + sanitize_error!(e) + )) }) } @@ -232,7 +241,10 @@ impl SignerConfig { public_key, ) .map_err(|e| { - KoraError::SigningError(format!("Failed to create Turnkey signer '{signer_name}': {e}")) + KoraError::SigningError(format!( + "Failed to create Turnkey signer '{signer_name}': {}", + sanitize_error!(e) + )) }) } @@ -245,7 +257,10 @@ impl SignerConfig { let wallet_id = get_env_var_for_signer(&config.wallet_id_env, signer_name)?; Signer::from_privy(app_id, app_secret, wallet_id).await.map_err(|e| { - KoraError::SigningError(format!("Failed to create Privy signer '{signer_name}': {e}")) + KoraError::SigningError(format!( + "Failed to create Privy signer '{signer_name}': {}", + sanitize_error!(e) + )) }) } @@ -259,7 +274,10 @@ impl SignerConfig { let pubkey = get_env_var_for_signer(&config.pubkey_env, signer_name)?; Signer::from_vault(vault_addr, vault_token, key_name, pubkey).map_err(|e| { - KoraError::SigningError(format!("Failed to create Vault signer '{signer_name}': {e}")) + KoraError::SigningError(format!( + "Failed to create Vault signer '{signer_name}': {}", + sanitize_error!(e) + )) }) } diff --git a/crates/lib/src/signer/keypair_util.rs b/crates/lib/src/signer/keypair_util.rs index 5fb7faf3..1f8343b3 100644 --- a/crates/lib/src/signer/keypair_util.rs +++ b/crates/lib/src/signer/keypair_util.rs @@ -1,4 +1,4 @@ -use crate::error::KoraError; +use crate::{error::KoraError, sanitize_error}; use serde_json; use solana_sdk::signature::Keypair; use std::fs; @@ -29,9 +29,9 @@ impl KeypairUtil { /// Creates a new keypair from a base58-encoded private key string with proper error handling pub fn from_base58_safe(private_key: &str) -> Result { // Try to decode as base58 first - let decoded = bs58::decode(private_key) - .into_vec() - .map_err(|e| KoraError::SigningError(format!("Invalid base58 string: {e}")))?; + let decoded = bs58::decode(private_key).into_vec().map_err(|e| { + KoraError::SigningError(format!("Invalid base58 string: {}", sanitize_error!(e))) + })?; if decoded.len() != 64 { return Err(KoraError::SigningError(format!( @@ -40,8 +40,9 @@ impl KeypairUtil { ))); } - let keypair = Keypair::try_from(&decoded[..]) - .map_err(|e| KoraError::SigningError(format!("Invalid private key bytes: {e}")))?; + let keypair = Keypair::try_from(&decoded[..]).map_err(|e| { + KoraError::SigningError(format!("Invalid private key bytes: {}", sanitize_error!(e))) + })?; Ok(keypair) } @@ -72,10 +73,17 @@ impl KeypairUtil { byte_array.len() ))); } - Keypair::try_from(&byte_array[..]) - .map_err(|e| KoraError::SigningError(format!("Invalid private key bytes: {e}"))) + Keypair::try_from(&byte_array[..]).map_err(|e| { + KoraError::SigningError(format!( + "Invalid private key bytes: {}", + sanitize_error!(e) + )) + }) } - Err(e) => Err(KoraError::SigningError(format!("Failed to parse U8Array: {e}"))), + Err(e) => Err(KoraError::SigningError(format!( + "Failed to parse U8Array: {}", + sanitize_error!(e) + ))), } } @@ -89,8 +97,12 @@ impl KeypairUtil { byte_array.len() ))); } - return Keypair::try_from(&byte_array[..]) - .map_err(|e| KoraError::SigningError(format!("Invalid private key bytes: {e}"))); + return Keypair::try_from(&byte_array[..]).map_err(|e| { + KoraError::SigningError(format!( + "Invalid private key bytes: {}", + sanitize_error!(e) + )) + }); } Err(KoraError::SigningError( diff --git a/crates/lib/src/usage_limit/usage_store.rs b/crates/lib/src/usage_limit/usage_store.rs index 68ad0523..5f59a82f 100644 --- a/crates/lib/src/usage_limit/usage_store.rs +++ b/crates/lib/src/usage_limit/usage_store.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use deadpool_redis::{Connection, Pool}; use redis::AsyncCommands; -use crate::error::KoraError; +use crate::{error::KoraError, sanitize_error}; /// Trait for storing and retrieving usage counts #[async_trait] @@ -31,7 +31,10 @@ impl RedisUsageStore { async fn get_connection(&self) -> Result { self.pool.get().await.map_err(|e| { - KoraError::InternalServerError(format!("Failed to get Redis connection: {e}")) + KoraError::InternalServerError(sanitize_error!(format!( + "Failed to get Redis connection: {}", + e + ))) }) } } @@ -41,7 +44,10 @@ impl UsageStore for RedisUsageStore { async fn increment(&self, key: &str) -> Result { let mut conn = self.get_connection().await?; let count: u32 = conn.incr(key, 1).await.map_err(|e| { - KoraError::InternalServerError(format!("Failed to increment usage for {key}: {e}")) + KoraError::InternalServerError(sanitize_error!(format!( + "Failed to increment usage for {}: {}", + key, e + ))) })?; Ok(count) } @@ -49,17 +55,19 @@ impl UsageStore for RedisUsageStore { async fn get(&self, key: &str) -> Result { let mut conn = self.get_connection().await?; let count: Option = conn.get(key).await.map_err(|e| { - KoraError::InternalServerError(format!("Failed to get usage for {key}: {e}")) + KoraError::InternalServerError(sanitize_error!(format!( + "Failed to get usage for {}: {}", + key, e + ))) })?; Ok(count.unwrap_or(0)) } async fn clear(&self) -> Result<(), KoraError> { let mut conn = self.get_connection().await?; - let _: () = conn - .flushdb() - .await - .map_err(|e| KoraError::InternalServerError(format!("Failed to clear Redis: {e}")))?; + let _: () = conn.flushdb().await.map_err(|e| { + KoraError::InternalServerError(sanitize_error!(format!("Failed to clear Redis: {}", e))) + })?; Ok(()) } } @@ -85,7 +93,10 @@ impl Default for InMemoryUsageStore { impl UsageStore for InMemoryUsageStore { async fn increment(&self, key: &str) -> Result { let mut data = self.data.lock().map_err(|e| { - KoraError::InternalServerError(format!("Failed to lock usage store: {e}")) + KoraError::InternalServerError(sanitize_error!(format!( + "Failed to lock usage store: {}", + e + ))) })?; let count = data.entry(key.to_string()).or_insert(0); *count += 1; @@ -94,14 +105,20 @@ impl UsageStore for InMemoryUsageStore { async fn get(&self, key: &str) -> Result { let data = self.data.lock().map_err(|e| { - KoraError::InternalServerError(format!("Failed to lock usage store: {e}")) + KoraError::InternalServerError(sanitize_error!(format!( + "Failed to lock usage store: {}", + e + ))) })?; Ok(data.get(key).copied().unwrap_or(0)) } async fn clear(&self) -> Result<(), KoraError> { let mut data = self.data.lock().map_err(|e| { - KoraError::InternalServerError(format!("Failed to lock usage store: {e}")) + KoraError::InternalServerError(sanitize_error!(format!( + "Failed to lock usage store: {}", + e + ))) })?; data.clear(); Ok(()) diff --git a/crates/lib/src/usage_limit/usage_tracker.rs b/crates/lib/src/usage_limit/usage_tracker.rs index 2ed432a8..cc771666 100644 --- a/crates/lib/src/usage_limit/usage_tracker.rs +++ b/crates/lib/src/usage_limit/usage_tracker.rs @@ -7,7 +7,7 @@ use solana_signers::SolanaSigner; use tokio::sync::OnceCell; use super::usage_store::{RedisUsageStore, UsageStore}; -use crate::{error::KoraError, get_all_signers}; +use crate::{error::KoraError, get_all_signers, sanitize_error}; #[cfg(not(test))] use crate::state::get_config; @@ -152,22 +152,30 @@ impl UsageTracker { let usage_limiter = if let Some(cache_url) = &config.kora.usage_limit.cache_url { let cfg = deadpool_redis::Config::from_url(cache_url); let pool = cfg.create_pool(Some(Runtime::Tokio1)).map_err(|e| { - KoraError::InternalServerError(format!("Failed to create Redis pool: {e}")) + KoraError::InternalServerError(format!( + "Failed to create Redis pool: {}", + sanitize_error!(e) + )) })?; // Test Redis connection let mut conn = pool.get().await.map_err(|e| { - KoraError::InternalServerError(format!("Failed to connect to Redis: {e}")) + KoraError::InternalServerError(format!( + "Failed to connect to Redis: {}", + sanitize_error!(e) + )) })?; // Simple connection test let _: Option = conn.get("__usage_limiter_test__").await.map_err(|e| { - KoraError::InternalServerError(format!("Redis connection test failed: {e}")) + KoraError::InternalServerError(format!( + "Redis connection test failed: {}", + sanitize_error!(e) + )) })?; log::info!( - "Usage limiter initialized with Redis at {} (max: {} transactions)", - cache_url, + "Usage limiter initialized with max {} transactions", config.kora.usage_limit.max_transactions ); diff --git a/crates/lib/src/validator/config_validator.rs b/crates/lib/src/validator/config_validator.rs index ba6e6378..530f0108 100644 --- a/crates/lib/src/validator/config_validator.rs +++ b/crates/lib/src/validator/config_validator.rs @@ -288,8 +288,6 @@ impl ConfigValidator { println!("=== Configuration Validation ==="); if errors.is_empty() { println!("โœ“ Configuration validation successful!"); - println!("\n=== Current Configuration ==="); - println!("{config:#?}"); } else { println!("โœ— Configuration validation failed!"); println!("\nโŒ Errors:"); From 5a61ea1158f35aab1cc64729c2abf9fab16c5418 Mon Sep 17 00:00:00 2001 From: jo <17280917+dev-jodee@users.noreply.github.com> Date: Wed, 15 Oct 2025 16:46:50 -0400 Subject: [PATCH 14/29] =?UTF-8?q?feat:=20(PRO-413)=20Enhance=20fee=20payer?= =?UTF-8?q?=20outflow=20calculation=20to=20include=20SPL=20=E2=80=A6=20(#2?= =?UTF-8?q?33)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: (PRO-413) Enhance fee payer outflow calculation to include SPL token transfers - Updated `calculate_fee_payer_outflow` to account for SPL token transfers, improving accuracy in fee estimation. - Added comprehensive tests for SPL token transfer scenarios to ensure correct fee calculations. - Adjusted transaction validation to utilize updated outflow calculations, ensuring consistency across transaction processing. * PR Fixes and cleaned up unwraps --- crates/lib/src/fee/fee.rs | 196 +++++++++++++----- crates/lib/src/metrics/handler.rs | 4 +- crates/lib/src/oracle/jupiter.rs | 74 +++++-- crates/lib/src/oracle/oracle.rs | 40 +++- crates/lib/src/oracle/utils.rs | 20 +- crates/lib/src/rpc_server/auth.rs | 12 +- .../src/rpc_server/method/sign_transaction.rs | 2 +- .../method/sign_transaction_if_paid.rs | 2 +- .../rpc_server/method/transfer_transaction.rs | 8 +- crates/lib/src/sanitize.rs | 5 +- crates/lib/src/tests/transaction_mock.rs | 2 +- crates/lib/src/token/token.rs | 158 +++++++++++++- crates/lib/src/transaction/transaction.rs | 10 +- .../src/transaction/versioned_transaction.rs | 12 +- crates/lib/src/validator/config_validator.rs | 32 +-- crates/lib/src/validator/math_validator.rs | 9 + crates/lib/src/validator/mod.rs | 1 + .../src/validator/transaction_validator.rs | 143 ++++++++----- tests/external/jupiter_integration.rs | 25 +++ tests/rpc/fee_estimation.rs | 90 +++++++- 20 files changed, 687 insertions(+), 158 deletions(-) create mode 100644 crates/lib/src/validator/math_validator.rs diff --git a/crates/lib/src/fee/fee.rs b/crates/lib/src/fee/fee.rs index 710a8f60..7c156a62 100644 --- a/crates/lib/src/fee/fee.rs +++ b/crates/lib/src/fee/fee.rs @@ -241,8 +241,14 @@ impl FeeConfigUtil { } // Calculate fee payer outflow if fee payer is provided, to better estimate the potential fee - let fee_payer_outflow = - FeeConfigUtil::calculate_fee_payer_outflow(fee_payer, transaction).await?; + let config = get_config()?; + let fee_payer_outflow = FeeConfigUtil::calculate_fee_payer_outflow( + fee_payer, + transaction, + rpc_client, + &config.validation.price_source, + ) + .await?; // If the transaction for paying the gasless relayer is not included, but we expect a payment, we need to add the fee for the payment instruction // for a better approximation of the fee @@ -359,14 +365,17 @@ impl FeeConfigUtil { } } - /// Calculate the total outflow (SOL spending) that could occur for a fee payer account in a transaction. - /// This includes transfers, account creation, and other operations that could drain the fee payer's balance. + /// Calculate the total outflow (SOL + SPL token value) that could occur for a fee payer account in a transaction. + /// This includes SOL transfers, account creation, SPL token transfers, and other operations that could drain the fee payer's balance. pub async fn calculate_fee_payer_outflow( fee_payer_pubkey: &Pubkey, transaction: &mut VersionedTransactionResolved, + rpc_client: &RpcClient, + price_source: &PriceSource, ) -> Result { let mut total = 0u64; + // Calculate SOL outflow from System Program instructions let parsed_system_instructions = transaction.get_or_parse_system_instructions()?; for instruction in parsed_system_instructions @@ -417,6 +426,27 @@ impl FeeConfigUtil { } } + // Calculate SPL token transfer outflow (converted to lamports value) + let spl_instructions = transaction.get_or_parse_spl_instructions()?; + let empty_vec = vec![]; + let spl_transfers = + spl_instructions.get(&ParsedSPLInstructionType::SplTokenTransfer).unwrap_or(&empty_vec); + + if !spl_transfers.is_empty() { + let spl_outflow = TokenUtil::calculate_spl_transfers_value_in_lamports( + spl_transfers, + fee_payer_pubkey, + price_source, + rpc_client, + ) + .await?; + + total = total.checked_add(spl_outflow).ok_or_else(|| { + log::error!("Fee payer outflow overflow: sol={}, spl={}", total, spl_outflow); + KoraError::ValidationError("Fee payer outflow calculation overflow".to_string()) + })?; + } + Ok(total) } } @@ -580,6 +610,8 @@ mod tests { #[tokio::test] async fn test_calculate_fee_payer_outflow_transfer() { + setup_or_get_test_config(); + let mocked_rpc_client = RpcMockBuilder::new().build(); let fee_payer = Pubkey::new_unique(); let recipient = Pubkey::new_unique(); @@ -590,10 +622,14 @@ mod tests { let mut resolved_transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - let outflow = - FeeConfigUtil::calculate_fee_payer_outflow(&fee_payer, &mut resolved_transaction) - .await - .unwrap(); + let outflow = FeeConfigUtil::calculate_fee_payer_outflow( + &fee_payer, + &mut resolved_transaction, + &mocked_rpc_client, + &crate::oracle::PriceSource::Mock, + ) + .await + .unwrap(); assert_eq!(outflow, 100_000, "Transfer from fee payer should add to outflow"); // Test 2: Fee payer as recipient - should subtract from outflow @@ -603,10 +639,14 @@ mod tests { VersionedMessage::Legacy(Message::new(&[transfer_instruction], Some(&fee_payer))); let mut resolved_transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - let outflow = - FeeConfigUtil::calculate_fee_payer_outflow(&fee_payer, &mut resolved_transaction) - .await - .unwrap(); + let outflow = FeeConfigUtil::calculate_fee_payer_outflow( + &fee_payer, + &mut resolved_transaction, + &mocked_rpc_client, + &crate::oracle::PriceSource::Mock, + ) + .await + .unwrap(); assert_eq!(outflow, 0, "Transfer to fee payer should subtract from outflow (saturating)"); // Test 3: Other account as sender - should not affect outflow @@ -616,15 +656,21 @@ mod tests { VersionedMessage::Legacy(Message::new(&[transfer_instruction], Some(&fee_payer))); let mut resolved_transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - let outflow = - FeeConfigUtil::calculate_fee_payer_outflow(&fee_payer, &mut resolved_transaction) - .await - .unwrap(); + let outflow = FeeConfigUtil::calculate_fee_payer_outflow( + &fee_payer, + &mut resolved_transaction, + &mocked_rpc_client, + &crate::oracle::PriceSource::Mock, + ) + .await + .unwrap(); assert_eq!(outflow, 0, "Transfer from other account should not affect outflow"); } #[tokio::test] async fn test_calculate_fee_payer_outflow_transfer_with_seed() { + setup_or_get_test_config(); + let mocked_rpc_client = RpcMockBuilder::new().build(); let fee_payer = Pubkey::new_unique(); let recipient = Pubkey::new_unique(); @@ -641,10 +687,14 @@ mod tests { VersionedMessage::Legacy(Message::new(&[transfer_instruction], Some(&fee_payer))); let mut resolved_transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - let outflow = - FeeConfigUtil::calculate_fee_payer_outflow(&fee_payer, &mut resolved_transaction) - .await - .unwrap(); + let outflow = FeeConfigUtil::calculate_fee_payer_outflow( + &fee_payer, + &mut resolved_transaction, + &mocked_rpc_client, + &crate::oracle::PriceSource::Mock, + ) + .await + .unwrap(); assert_eq!(outflow, 150_000, "TransferWithSeed from fee payer should add to outflow"); // Test 2: Fee payer as recipient (index 2 for TransferWithSeed) @@ -661,10 +711,14 @@ mod tests { VersionedMessage::Legacy(Message::new(&[transfer_instruction], Some(&fee_payer))); let mut resolved_transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - let outflow = - FeeConfigUtil::calculate_fee_payer_outflow(&fee_payer, &mut resolved_transaction) - .await - .unwrap(); + let outflow = FeeConfigUtil::calculate_fee_payer_outflow( + &fee_payer, + &mut resolved_transaction, + &mocked_rpc_client, + &crate::oracle::PriceSource::Mock, + ) + .await + .unwrap(); assert_eq!( outflow, 0, "TransferWithSeed to fee payer should subtract from outflow (saturating)" @@ -673,6 +727,8 @@ mod tests { #[tokio::test] async fn test_calculate_fee_payer_outflow_create_account() { + setup_or_get_test_config(); + let mocked_rpc_client = RpcMockBuilder::new().build(); let fee_payer = Pubkey::new_unique(); let new_account = Pubkey::new_unique(); @@ -683,10 +739,14 @@ mod tests { VersionedMessage::Legacy(Message::new(&[create_instruction], Some(&fee_payer))); let mut resolved_transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - let outflow = - FeeConfigUtil::calculate_fee_payer_outflow(&fee_payer, &mut resolved_transaction) - .await - .unwrap(); + let outflow = FeeConfigUtil::calculate_fee_payer_outflow( + &fee_payer, + &mut resolved_transaction, + &mocked_rpc_client, + &crate::oracle::PriceSource::Mock, + ) + .await + .unwrap(); assert_eq!(outflow, 200_000, "CreateAccount funded by fee payer should add to outflow"); // Test 2: Other account funding CreateAccount @@ -697,15 +757,21 @@ mod tests { VersionedMessage::Legacy(Message::new(&[create_instruction], Some(&fee_payer))); let mut resolved_transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - let outflow = - FeeConfigUtil::calculate_fee_payer_outflow(&fee_payer, &mut resolved_transaction) - .await - .unwrap(); + let outflow = FeeConfigUtil::calculate_fee_payer_outflow( + &fee_payer, + &mut resolved_transaction, + &mocked_rpc_client, + &crate::oracle::PriceSource::Mock, + ) + .await + .unwrap(); assert_eq!(outflow, 0, "CreateAccount funded by other account should not affect outflow"); } #[tokio::test] async fn test_calculate_fee_payer_outflow_create_account_with_seed() { + setup_or_get_test_config(); + let mocked_rpc_client = RpcMockBuilder::new().build(); let fee_payer = Pubkey::new_unique(); let new_account = Pubkey::new_unique(); @@ -723,10 +789,14 @@ mod tests { VersionedMessage::Legacy(Message::new(&[create_instruction], Some(&fee_payer))); let mut resolved_transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - let outflow = - FeeConfigUtil::calculate_fee_payer_outflow(&fee_payer, &mut resolved_transaction) - .await - .unwrap(); + let outflow = FeeConfigUtil::calculate_fee_payer_outflow( + &fee_payer, + &mut resolved_transaction, + &mocked_rpc_client, + &crate::oracle::PriceSource::Mock, + ) + .await + .unwrap(); assert_eq!( outflow, 300_000, "CreateAccountWithSeed funded by fee payer should add to outflow" @@ -735,6 +805,8 @@ mod tests { #[tokio::test] async fn test_calculate_fee_payer_outflow_nonce_withdraw() { + setup_or_get_test_config(); + let mocked_rpc_client = RpcMockBuilder::new().build(); let nonce_account = Pubkey::new_unique(); let fee_payer = Pubkey::new_unique(); let recipient = Pubkey::new_unique(); @@ -746,10 +818,14 @@ mod tests { VersionedMessage::Legacy(Message::new(&[withdraw_instruction], Some(&fee_payer))); let mut resolved_transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - let outflow = - FeeConfigUtil::calculate_fee_payer_outflow(&fee_payer, &mut resolved_transaction) - .await - .unwrap(); + let outflow = FeeConfigUtil::calculate_fee_payer_outflow( + &fee_payer, + &mut resolved_transaction, + &mocked_rpc_client, + &crate::oracle::PriceSource::Mock, + ) + .await + .unwrap(); assert_eq!( outflow, 50_000, "WithdrawNonceAccount from fee payer nonce should add to outflow" @@ -763,10 +839,14 @@ mod tests { VersionedMessage::Legacy(Message::new(&[withdraw_instruction], Some(&fee_payer))); let mut resolved_transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - let outflow = - FeeConfigUtil::calculate_fee_payer_outflow(&fee_payer, &mut resolved_transaction) - .await - .unwrap(); + let outflow = FeeConfigUtil::calculate_fee_payer_outflow( + &fee_payer, + &mut resolved_transaction, + &mocked_rpc_client, + &crate::oracle::PriceSource::Mock, + ) + .await + .unwrap(); assert_eq!( outflow, 0, "WithdrawNonceAccount to fee payer should subtract from outflow (saturating)" @@ -775,6 +855,8 @@ mod tests { #[tokio::test] async fn test_calculate_fee_payer_outflow_multiple_instructions() { + setup_or_get_test_config(); + let mocked_rpc_client = RpcMockBuilder::new().build(); let fee_payer = Pubkey::new_unique(); let recipient = Pubkey::new_unique(); let sender = Pubkey::new_unique(); @@ -789,10 +871,14 @@ mod tests { let message = VersionedMessage::Legacy(Message::new(&instructions, Some(&fee_payer))); let mut resolved_transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - let outflow = - FeeConfigUtil::calculate_fee_payer_outflow(&fee_payer, &mut resolved_transaction) - .await - .unwrap(); + let outflow = FeeConfigUtil::calculate_fee_payer_outflow( + &fee_payer, + &mut resolved_transaction, + &mocked_rpc_client, + &crate::oracle::PriceSource::Mock, + ) + .await + .unwrap(); assert_eq!( outflow, 120_000, "Multiple instructions should sum correctly: 100000 - 30000 + 50000 = 120000" @@ -801,6 +887,8 @@ mod tests { #[tokio::test] async fn test_calculate_fee_payer_outflow_non_system_program() { + setup_or_get_test_config(); + let mocked_rpc_client = RpcMockBuilder::new().build(); let fee_payer = Pubkey::new_unique(); let fake_program = Pubkey::new_unique(); @@ -813,10 +901,14 @@ mod tests { let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); let mut resolved_transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - let outflow = - FeeConfigUtil::calculate_fee_payer_outflow(&fee_payer, &mut resolved_transaction) - .await - .unwrap(); + let outflow = FeeConfigUtil::calculate_fee_payer_outflow( + &fee_payer, + &mut resolved_transaction, + &mocked_rpc_client, + &crate::oracle::PriceSource::Mock, + ) + .await + .unwrap(); assert_eq!(outflow, 0, "Non-system program should not affect outflow"); } diff --git a/crates/lib/src/metrics/handler.rs b/crates/lib/src/metrics/handler.rs index 13925893..0c6cecbc 100644 --- a/crates/lib/src/metrics/handler.rs +++ b/crates/lib/src/metrics/handler.rs @@ -61,14 +61,14 @@ where .status(StatusCode::OK) .header("content-type", "text/plain; version=0.0.4") .body(Body::from(metrics)) - .unwrap(); + .expect("Failed to build response"); Ok(response) } Err(e) => { let response = Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) .body(Body::from(format!("Error gathering metrics: {e}"))) - .unwrap(); + .expect("Failed to build response"); Ok(response) } } diff --git a/crates/lib/src/oracle/jupiter.rs b/crates/lib/src/oracle/jupiter.rs index 8aa09f28..ea0e79f1 100644 --- a/crates/lib/src/oracle/jupiter.rs +++ b/crates/lib/src/oracle/jupiter.rs @@ -3,6 +3,7 @@ use crate::{ constant::{JUPITER_API_LITE_URL, JUPITER_API_PRO_URL, SOL_MINT}, error::KoraError, sanitize_error, + validator::math_validator, }; use once_cell::sync::Lazy; use parking_lot::RwLock; @@ -81,13 +82,29 @@ impl PriceOracle for JupiterPriceOracle { client: &Client, mint_address: &str, ) -> Result { + let prices = self.get_prices(client, &[mint_address.to_string()]).await?; + + prices.get(mint_address).cloned().ok_or_else(|| { + KoraError::RpcError(format!("No price data from Jupiter for mint {mint_address}")) + }) + } + + async fn get_prices( + &self, + client: &Client, + mint_addresses: &[String], + ) -> Result, KoraError> { + if mint_addresses.is_empty() { + return Ok(HashMap::new()); + } + // Try pro API first if API key is available, then fallback to free API if let Some(api_key) = &self.api_key { match self - .fetch_price_from_url(client, &self.pro_api_url, mint_address, Some(api_key)) + .fetch_prices_from_url(client, &self.pro_api_url, mint_addresses, Some(api_key)) .await { - Ok(price) => return Ok(price), + Ok(prices) => return Ok(prices), Err(e) => { if e == KoraError::RateLimitExceeded { log::warn!("Pro Jupiter API rate limit exceeded, falling back to free API"); @@ -99,20 +116,27 @@ impl PriceOracle for JupiterPriceOracle { } // Use free API (either as fallback or primary if no API key) - self.fetch_price_from_url(client, &self.lite_api_url, mint_address, None).await + self.fetch_prices_from_url(client, &self.lite_api_url, mint_addresses, None).await } } impl JupiterPriceOracle { - async fn fetch_price_from_url( + async fn fetch_prices_from_url( &self, client: &Client, api_url: &str, - mint_address: &str, + mint_addresses: &[String], api_key: Option<&String>, - ) -> Result { - // Always fetch SOL price as well so we can convert to SOL - let url = format!("{api_url}?ids={SOL_MINT},{mint_address}"); + ) -> Result, KoraError> { + if mint_addresses.is_empty() { + return Ok(HashMap::new()); + } + + let mut all_mints = vec![SOL_MINT.to_string()]; + all_mints.extend_from_slice(mint_addresses); + let ids = all_mints.join(","); + + let url = format!("{api_url}?ids={ids}"); let mut request = client.get(&url); @@ -143,16 +167,35 @@ impl JupiterPriceOracle { KoraError::RpcError(format!("Failed to parse Jupiter response: {}", sanitize_error!(e))) })?; + // Get SOL price for conversion let sol_price = jupiter_response .get(SOL_MINT) .ok_or_else(|| KoraError::RpcError("No SOL price data from Jupiter".to_string()))?; - let price_data = jupiter_response - .get(mint_address) - .ok_or_else(|| KoraError::RpcError("No price data from Jupiter".to_string()))?; - let price = price_data.usd_price / sol_price.usd_price; + math_validator::validate_division(sol_price.usd_price)?; + + // Convert all prices to SOL-denominated + let mut result = HashMap::new(); + for mint_address in mint_addresses { + if let Some(price_data) = jupiter_response.get(mint_address.as_str()) { + let price = price_data.usd_price / sol_price.usd_price; + result.insert( + mint_address.clone(), + TokenPrice { price, confidence: 0.95, source: PriceSource::Jupiter }, + ); + } else { + log::error!("No price data for mint {mint_address} from Jupiter"); + return Err(KoraError::RpcError(format!( + "No price data from Jupiter for mint {mint_address}" + ))); + } + } + + if result.is_empty() { + return Err(KoraError::RpcError("No price data from Jupiter".to_string())); + } - Ok(TokenPrice { price, confidence: 0.95, source: PriceSource::Jupiter }) + Ok(result) } } @@ -260,7 +303,10 @@ mod tests { assert!(result.is_err()); assert_eq!( result.err(), - Some(KoraError::RpcError("No price data from Jupiter".to_string())) + Some(KoraError::RpcError( + "No price data from Jupiter for mint JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN" + .to_string() + )) ); } } diff --git a/crates/lib/src/oracle/oracle.rs b/crates/lib/src/oracle/oracle.rs index cb9189a1..e2e29b91 100644 --- a/crates/lib/src/oracle/oracle.rs +++ b/crates/lib/src/oracle/oracle.rs @@ -5,7 +5,7 @@ use crate::{ use mockall::automock; use reqwest::Client; use serde::{Deserialize, Serialize}; -use std::{sync::Arc, time::Duration}; +use std::{collections::HashMap, sync::Arc, time::Duration}; use tokio::time::sleep; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -28,6 +28,12 @@ pub enum PriceSource { pub trait PriceOracle { async fn get_price(&self, client: &Client, mint_address: &str) -> Result; + + async fn get_prices( + &self, + client: &Client, + mint_addresses: &[String], + ) -> Result, KoraError>; } pub struct RetryingPriceOracle { @@ -54,14 +60,29 @@ impl RetryingPriceOracle { } pub async fn get_token_price(&self, mint_address: &str) -> Result { + let prices = self.get_token_prices(&[mint_address.to_string()]).await?; + + prices.get(mint_address).cloned().ok_or_else(|| { + KoraError::InternalServerError("Failed to fetch token price".to_string()) + }) + } + + pub async fn get_token_prices( + &self, + mint_addresses: &[String], + ) -> Result, KoraError> { + if mint_addresses.is_empty() { + return Ok(HashMap::new()); + } + let mut last_error = None; let mut delay = self.base_delay; for attempt in 0..self.max_retries { - let price_result = self.oracle.get_price(&self.client, mint_address).await; + let price_result = self.oracle.get_prices(&self.client, mint_addresses).await; match price_result { - Ok(price) => return Ok(price), + Ok(prices) => return Ok(prices), Err(e) => { last_error = Some(e); if attempt < self.max_retries - 1 { @@ -73,7 +94,7 @@ impl RetryingPriceOracle { } Err(last_error.unwrap_or_else(|| { - KoraError::InternalServerError("Failed to fetch token price".to_string()) + KoraError::InternalServerError("Failed to fetch token prices".to_string()) })) } } @@ -87,8 +108,15 @@ mod tests { #[tokio::test] async fn test_price_oracle_retries() { let mut mock_oracle = MockPriceOracle::new(); - mock_oracle.expect_get_price().times(1).returning(|_, _| { - Ok(TokenPrice { price: 1.0, confidence: 0.95, source: PriceSource::Jupiter }) + mock_oracle.expect_get_prices().times(1).returning(|_, mint_addresses| { + let mut result = HashMap::new(); + for mint in mint_addresses { + result.insert( + mint.clone(), + TokenPrice { price: 1.0, confidence: 0.95, source: PriceSource::Jupiter }, + ); + } + Ok(result) }); let oracle = RetryingPriceOracle::new(3, Duration::from_millis(100), Arc::new(mock_oracle)); diff --git a/crates/lib/src/oracle/utils.rs b/crates/lib/src/oracle/utils.rs index 338d1058..5166cc42 100644 --- a/crates/lib/src/oracle/utils.rs +++ b/crates/lib/src/oracle/utils.rs @@ -1,5 +1,5 @@ use crate::oracle::{MockPriceOracle, PriceOracle, PriceSource, TokenPrice}; -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; pub const DEFAULT_MOCKED_PRICE: f64 = 0.001; pub const DEFAULT_MOCKED_USDC_PRICE: f64 = 0.0001; @@ -24,6 +24,24 @@ impl OracleUtil { }; Ok(TokenPrice { price, confidence: 1.0, source: PriceSource::Mock }) }); + + mock.expect_get_prices() + .times(..) // Allow unlimited calls + .returning(|_, mint_addresses| { + let mut result = HashMap::new(); + for mint_address in mint_addresses { + let price = match mint_address.as_str() { + USDC_DEVNET_MINT => DEFAULT_MOCKED_USDC_PRICE, // USDC + WSOL_DEVNET_MINT => DEFAULT_MOCKED_WSOL_PRICE, // SOL + _ => DEFAULT_MOCKED_PRICE, // Default price for unknown tokens + }; + result.insert( + mint_address.clone(), + TokenPrice { price, confidence: 1.0, source: PriceSource::Mock }, + ); + } + Ok(result) + }); Arc::new(mock) } } diff --git a/crates/lib/src/rpc_server/auth.rs b/crates/lib/src/rpc_server/auth.rs index 4fa36105..b666023e 100644 --- a/crates/lib/src/rpc_server/auth.rs +++ b/crates/lib/src/rpc_server/auth.rs @@ -54,8 +54,10 @@ where let mut inner = self.inner.clone(); Box::pin(async move { - let unauthorized_response = - Response::builder().status(StatusCode::UNAUTHORIZED).body(Body::empty()).unwrap(); + let unauthorized_response = Response::builder() + .status(StatusCode::UNAUTHORIZED) + .body(Body::empty()) + .expect("Failed to build unauthorized response"); let (parts, body_bytes) = extract_parts_and_body_bytes(request).await; if get_jsonrpc_method(&body_bytes) == Some("liveness".to_string()) { @@ -131,8 +133,10 @@ where let mut inner = self.inner.clone(); Box::pin(async move { - let unauthorized_response = - Response::builder().status(StatusCode::UNAUTHORIZED).body(Body::empty()).unwrap(); + let unauthorized_response = Response::builder() + .status(StatusCode::UNAUTHORIZED) + .body(Body::empty()) + .expect("Failed to build unauthorized response"); let signature_header = request.headers().get(X_HMAC_SIGNATURE).cloned(); let timestamp_header = request.headers().get(X_TIMESTAMP).cloned(); diff --git a/crates/lib/src/rpc_server/method/sign_transaction.rs b/crates/lib/src/rpc_server/method/sign_transaction.rs index 74d15cbc..c2b24d9e 100644 --- a/crates/lib/src/rpc_server/method/sign_transaction.rs +++ b/crates/lib/src/rpc_server/method/sign_transaction.rs @@ -50,7 +50,7 @@ pub async fn sign_transaction( let (signed_transaction, _) = resolved_transaction.sign_transaction(&signer, rpc_client).await?; - let encoded = TransactionUtil::encode_versioned_transaction(&signed_transaction); + let encoded = TransactionUtil::encode_versioned_transaction(&signed_transaction)?; Ok(SignTransactionResponse { signed_transaction: encoded, diff --git a/crates/lib/src/rpc_server/method/sign_transaction_if_paid.rs b/crates/lib/src/rpc_server/method/sign_transaction_if_paid.rs index f71ef017..a15b4ae9 100644 --- a/crates/lib/src/rpc_server/method/sign_transaction_if_paid.rs +++ b/crates/lib/src/rpc_server/method/sign_transaction_if_paid.rs @@ -54,7 +54,7 @@ pub async fn sign_transaction_if_paid( .map_err(|e| KoraError::TokenOperationError(e.to_string()))?; Ok(SignTransactionIfPaidResponse { - transaction: TransactionUtil::encode_versioned_transaction(&transaction), + transaction: TransactionUtil::encode_versioned_transaction(&transaction)?, signed_transaction, signer_pubkey: signer.pubkey().to_string(), }) diff --git a/crates/lib/src/rpc_server/method/transfer_transaction.rs b/crates/lib/src/rpc_server/method/transfer_transaction.rs index 033a45ef..2e82c650 100644 --- a/crates/lib/src/rpc_server/method/transfer_transaction.rs +++ b/crates/lib/src/rpc_server/method/transfer_transaction.rs @@ -126,7 +126,7 @@ pub async fn transfer_transaction( VersionedTransactionResolved::from_kora_built_transaction(&transaction); // validate transaction before signing - validator.validate_transaction(&mut resolved_transaction).await?; + validator.validate_transaction(&mut resolved_transaction, rpc_client).await?; // Find the fee payer position in the account keys let fee_payer_position = resolved_transaction.find_signer_position(&fee_payer)?; @@ -167,7 +167,7 @@ mod tests { let _ = update_config(config); let _ = setup_or_get_test_signer(); - let rpc_client = Arc::new(RpcMockBuilder::new().build()); + let rpc_client = Arc::new(RpcMockBuilder::new().with_mint_account(6).build()); let request = TransferTransactionRequest { amount: 1000, @@ -196,7 +196,7 @@ mod tests { let _ = update_config(config); let _ = setup_or_get_test_signer(); - let rpc_client = Arc::new(RpcMockBuilder::new().build()); + let rpc_client = Arc::new(RpcMockBuilder::new().with_mint_account(6).build()); let request = TransferTransactionRequest { amount: 1000, @@ -224,7 +224,7 @@ mod tests { let _ = update_config(config); let _ = setup_or_get_test_signer(); - let rpc_client = Arc::new(RpcMockBuilder::new().build()); + let rpc_client = Arc::new(RpcMockBuilder::new().with_mint_account(6).build()); let request = TransferTransactionRequest { amount: 1000, diff --git a/crates/lib/src/sanitize.rs b/crates/lib/src/sanitize.rs index 6b1b3a16..554f49a2 100644 --- a/crates/lib/src/sanitize.rs +++ b/crates/lib/src/sanitize.rs @@ -12,12 +12,13 @@ use std::sync::LazyLock; static URL_WITH_CREDENTIALS_PATTERN: LazyLock = LazyLock::new(|| { // Generic URL pattern with embedded credentials: protocol://user:password@host // Matches any protocol (redis, http, https, postgres, mysql, mongodb, etc.) - Regex::new(r"[a-z][a-z0-9+.-]*://[^:@\s]+:[^@\s]+@[^\s]+").unwrap() + Regex::new(r"[a-z][a-z0-9+.-]*://[^:@\s]+:[^@\s]+@[^\s]+") + .expect("Failed to create url regex pattern") }); static HEX_PATTERN: LazyLock = LazyLock::new(|| { // Long hex strings (likely keys/hashes) - 32+ chars, with optional 0x prefix - Regex::new(r"(?:0x)?[0-9a-fA-F]{32,}").unwrap() + Regex::new(r"(?:0x)?[0-9a-fA-F]{32,}").expect("Failed to create hex regex pattern") }); /// Sanitizes a message by redacting sensitive information diff --git a/crates/lib/src/tests/transaction_mock.rs b/crates/lib/src/tests/transaction_mock.rs index c6e36a03..30e5861f 100644 --- a/crates/lib/src/tests/transaction_mock.rs +++ b/crates/lib/src/tests/transaction_mock.rs @@ -14,7 +14,7 @@ pub fn create_mock_encoded_transaction() -> String { let message = VersionedMessage::Legacy(Message::new(&[ix], Some(&Pubkey::new_unique()))); let transaction = TransactionUtil::new_unsigned_versioned_transaction(message); - TransactionUtil::encode_versioned_transaction(&transaction) + TransactionUtil::encode_versioned_transaction(&transaction).unwrap() } pub fn create_mock_transaction() -> VersionedTransaction { diff --git a/crates/lib/src/token/token.rs b/crates/lib/src/token/token.rs index c4b43df1..23bc2f6e 100644 --- a/crates/lib/src/token/token.rs +++ b/crates/lib/src/token/token.rs @@ -13,7 +13,8 @@ use crate::{ }; use solana_client::nonblocking::rpc_client::RpcClient; use solana_sdk::{native_token::LAMPORTS_PER_SOL, pubkey::Pubkey}; -use std::{str::FromStr, time::Duration}; +use spl_associated_token_account::get_associated_token_address_with_program_id; +use std::{collections::HashMap, str::FromStr, time::Duration}; #[cfg(not(test))] use crate::state::get_config; @@ -140,6 +141,148 @@ impl TokenUtil { Ok(fee_in_token.ceil()) } + /// Calculate the total lamports value of SPL token transfers where the fee payer is involved + /// This includes both outflow (fee payer as owner/source) and inflow (fee payer owns destination) + pub async fn calculate_spl_transfers_value_in_lamports( + spl_transfers: &[ParsedSPLInstructionData], + fee_payer: &Pubkey, + price_source: &PriceSource, + rpc_client: &RpcClient, + ) -> Result { + // Collect all unique mints that need price lookups + let mut mint_to_transfers: HashMap< + Pubkey, + Vec<(u64, bool)>, // (amount, is_outflow) + > = HashMap::new(); + + for transfer in spl_transfers { + if let ParsedSPLInstructionData::SplTokenTransfer { + amount, + owner, + mint, + destination_address, + .. + } = transfer + { + // Check if fee payer is the source (outflow) + if *owner == *fee_payer { + if let Some(mint_pubkey) = mint { + mint_to_transfers.entry(*mint_pubkey).or_default().push((*amount, true)); + } + } else { + // Check if fee payer owns the destination (inflow) + // We need to check the destination token account owner + if let Some(mint_pubkey) = mint { + // Get destination account to check owner + match CacheUtil::get_account(rpc_client, destination_address, false).await { + Ok(dest_account) => { + let token_program = + TokenType::get_token_program_from_owner(&dest_account.owner)?; + if let Ok(token_account) = + token_program.unpack_token_account(&dest_account.data) + { + if token_account.owner() == *fee_payer { + mint_to_transfers + .entry(*mint_pubkey) + .or_default() + .push((*amount, false)); // inflow + } + } + } + Err(e) => { + // If we get Account not found error, we try to match it to the ATA derivation for the fee payer + // in case that ATA is being created in the current instruction + if matches!(e, KoraError::AccountNotFound(_)) { + let spl_ata = + spl_associated_token_account::get_associated_token_address( + fee_payer, + mint_pubkey, + ); + let token2022_ata = + get_associated_token_address_with_program_id( + fee_payer, + mint_pubkey, + &spl_token_2022::id(), + ); + + // If destination matches a valid ATA for fee payer, count as inflow + if *destination_address == spl_ata + || *destination_address == token2022_ata + { + mint_to_transfers + .entry(*mint_pubkey) + .or_default() + .push((*amount, false)); // inflow + } + // Otherwise, it's not fee payer's account, continue to next transfer + } else { + // Skip if destination account doesn't exist or can't be fetched + // This could be problematic for non ATA token accounts created + // during the transaction + continue; + } + } + } + } + } + } + } + + if mint_to_transfers.is_empty() { + return Ok(0); + } + + // Batch fetch all prices and decimals + let mint_addresses: Vec = + mint_to_transfers.keys().map(|mint| mint.to_string()).collect(); + + let oracle = RetryingPriceOracle::new( + 3, + Duration::from_secs(1), + get_price_oracle(price_source.clone()), + ); + + let prices = oracle.get_token_prices(&mint_addresses).await?; + + let mut mint_decimals = std::collections::HashMap::new(); + for mint in mint_to_transfers.keys() { + let decimals = Self::get_mint_decimals(rpc_client, mint).await?; + mint_decimals.insert(*mint, decimals); + } + + // Calculate total value + let mut total_lamports = 0u64; + + for (mint, transfers) in mint_to_transfers.iter() { + let price = prices + .get(&mint.to_string()) + .ok_or_else(|| KoraError::RpcError(format!("No price data for mint {mint}")))?; + let decimals = mint_decimals + .get(mint) + .ok_or_else(|| KoraError::RpcError(format!("No decimals data for mint {mint}")))?; + + for (amount, is_outflow) in transfers { + // Convert token amount to lamports value + let token_amount = *amount as f64 / 10f64.powi(*decimals as i32); + let sol_amount = token_amount * price.price; + let lamports = (sol_amount * LAMPORTS_PER_SOL as f64).floor() as u64; + + if *is_outflow { + // Add outflow to total + total_lamports = total_lamports.checked_add(lamports).ok_or_else(|| { + log::error!("SPL outflow calculation overflow"); + KoraError::ValidationError("SPL outflow calculation overflow".to_string()) + })?; + } else { + // Subtract inflow from total (using saturating_sub to prevent underflow) + total_lamports = total_lamports.saturating_sub(lamports); + } + } + } + + Ok(total_lamports) + } + /// Validate Token2022 extensions for payment instructions /// This checks if any blocked extensions are present on the payment accounts pub async fn validate_token2022_extensions_for_payment( @@ -159,7 +302,10 @@ impl TokenUtil { // Unpack the mint state with extensions let mint_state = token_program.unpack_mint(mint, &mint_data)?; - let mint_with_extensions = mint_state.as_any().downcast_ref::().unwrap(); + let mint_with_extensions = + mint_state.as_any().downcast_ref::().ok_or_else(|| { + KoraError::SerializationError("Failed to downcast mint state.".to_string()) + })?; // Check each extension type present on the mint for extension_type in mint_with_extensions.get_extension_types() { @@ -177,7 +323,9 @@ impl TokenUtil { let source_state = token_program.unpack_token_account(&source_data)?; let source_with_extensions = - source_state.as_any().downcast_ref::().unwrap(); + source_state.as_any().downcast_ref::().ok_or_else(|| { + KoraError::SerializationError("Failed to downcast source state.".to_string()) + })?; for extension_type in source_with_extensions.get_extension_types() { if config.is_account_extension_blocked(*extension_type) { @@ -195,7 +343,9 @@ impl TokenUtil { let destination_state = token_program.unpack_token_account(&destination_data)?; let destination_with_extensions = - destination_state.as_any().downcast_ref::().unwrap(); + destination_state.as_any().downcast_ref::().ok_or_else(|| { + KoraError::SerializationError("Failed to downcast destination state.".to_string()) + })?; for extension_type in destination_with_extensions.get_extension_types() { if config.is_account_extension_blocked(*extension_type) { diff --git a/crates/lib/src/transaction/transaction.rs b/crates/lib/src/transaction/transaction.rs index b74a2531..945bf360 100644 --- a/crates/lib/src/transaction/transaction.rs +++ b/crates/lib/src/transaction/transaction.rs @@ -47,9 +47,13 @@ impl TransactionUtil { VersionedTransactionResolved::from_kora_built_transaction(&transaction) } - pub fn encode_versioned_transaction(transaction: &VersionedTransaction) -> String { - let serialized = bincode::serialize(transaction).unwrap(); - STANDARD.encode(serialized) + pub fn encode_versioned_transaction( + transaction: &VersionedTransaction, + ) -> Result { + let serialized = bincode::serialize(transaction).map_err(|_| { + KoraError::SerializationError("Failed to serialize transaction.".to_string()) + })?; + Ok(STANDARD.encode(serialized)) } } diff --git a/crates/lib/src/transaction/versioned_transaction.rs b/crates/lib/src/transaction/versioned_transaction.rs index 438d9848..2935565b 100644 --- a/crates/lib/src/transaction/versioned_transaction.rs +++ b/crates/lib/src/transaction/versioned_transaction.rs @@ -203,7 +203,10 @@ impl VersionedTransactionResolved { if self.parsed_system_instructions.is_none() { self.parsed_system_instructions = Some(IxUtils::parse_system_instructions(self)?); } - Ok(self.parsed_system_instructions.as_ref().unwrap()) + + self.parsed_system_instructions.as_ref().ok_or_else(|| { + KoraError::SerializationError("Parsed system instructions not found".to_string()) + }) } pub fn get_or_parse_spl_instructions( @@ -212,7 +215,10 @@ impl VersionedTransactionResolved { if self.parsed_spl_instructions.is_none() { self.parsed_spl_instructions = Some(IxUtils::parse_token_instructions(self)?); } - Ok(self.parsed_spl_instructions.as_ref().unwrap()) + + self.parsed_spl_instructions.as_ref().ok_or_else(|| { + KoraError::SerializationError("Parsed SPL instructions not found".to_string()) + }) } } @@ -248,7 +254,7 @@ impl VersionedTransactionOps for VersionedTransactionResolved { let validator = TransactionValidator::new(fee_payer)?; // Validate transaction and accounts (already resolved) - validator.validate_transaction(self).await?; + validator.validate_transaction(self, rpc_client).await?; // Get latest blockhash and update transaction let mut transaction = self.transaction.clone(); diff --git a/crates/lib/src/validator/config_validator.rs b/crates/lib/src/validator/config_validator.rs index 530f0108..c8449e67 100644 --- a/crates/lib/src/validator/config_validator.rs +++ b/crates/lib/src/validator/config_validator.rs @@ -211,10 +211,13 @@ impl ConfigValidator { // Validate usage limit configuration let usage_config = &config.kora.usage_limit; if usage_config.enabled { - let (usage_errors, usage_warnings) = - CacheValidator::validate(usage_config).await.unwrap(); - errors.extend(usage_errors); - warnings.extend(usage_warnings); + if let Ok((usage_errors, usage_warnings)) = CacheValidator::validate(usage_config).await + { + errors.extend(usage_errors); + warnings.extend(usage_warnings); + } else { + errors.push("Failed to validate usage limit cache configuration".to_string()); + } } // RPC validation - only if not skipped @@ -255,14 +258,19 @@ impl ConfigValidator { // Validate missing ATAs for payment address if let Some(payment_address) = &config.kora.payment_address { - let payment_address = Pubkey::from_str(payment_address).unwrap(); - - let atas_to_create = find_missing_atas(rpc_client, &payment_address).await; - - if let Err(e) = atas_to_create { - errors.push(format!("Failed to find missing ATAs: {e}")); - } else if !atas_to_create.unwrap().is_empty() { - errors.push(format!("Missing ATAs for payment address: {payment_address}")); + if let Ok(payment_address) = Pubkey::from_str(payment_address) { + match find_missing_atas(rpc_client, &payment_address).await { + Ok(atas_to_create) => { + if !atas_to_create.is_empty() { + errors.push(format!( + "Missing ATAs for payment address: {payment_address}" + )); + } + } + Err(e) => errors.push(format!("Failed to find missing ATAs: {e}")), + } + } else { + errors.push(format!("Invalid payment address: {payment_address}")); } } } diff --git a/crates/lib/src/validator/math_validator.rs b/crates/lib/src/validator/math_validator.rs new file mode 100644 index 00000000..b5054b53 --- /dev/null +++ b/crates/lib/src/validator/math_validator.rs @@ -0,0 +1,9 @@ +use crate::KoraError; + +pub fn validate_division(divisor: f64) -> Result<(), KoraError> { + if !divisor.is_finite() || divisor <= 0.0 { + return Err(KoraError::RpcError(format!("Invalid division: {}", divisor))); + } + + Ok(()) +} diff --git a/crates/lib/src/validator/mod.rs b/crates/lib/src/validator/mod.rs index ac5c1922..88fb3414 100644 --- a/crates/lib/src/validator/mod.rs +++ b/crates/lib/src/validator/mod.rs @@ -1,5 +1,6 @@ pub mod account_validator; pub mod cache_validator; pub mod config_validator; +pub mod math_validator; pub mod signer_validator; pub mod transaction_validator; diff --git a/crates/lib/src/validator/transaction_validator.rs b/crates/lib/src/validator/transaction_validator.rs index 30ef3949..d2ee3277 100644 --- a/crates/lib/src/validator/transaction_validator.rs +++ b/crates/lib/src/validator/transaction_validator.rs @@ -105,6 +105,7 @@ impl TransactionValidator { pub async fn validate_transaction( &self, transaction_resolved: &mut VersionedTransactionResolved, + rpc_client: &RpcClient, ) -> Result<(), KoraError> { if transaction_resolved.all_instructions.is_empty() { return Err(KoraError::InvalidTransaction( @@ -121,7 +122,7 @@ impl TransactionValidator { self.validate_signatures(&transaction_resolved.transaction)?; self.validate_programs(transaction_resolved)?; - self.validate_transfer_amounts(transaction_resolved).await?; + self.validate_transfer_amounts(transaction_resolved, rpc_client).await?; self.validate_disallowed_accounts(transaction_resolved)?; self.validate_fee_payer_usage(transaction_resolved)?; @@ -262,8 +263,9 @@ impl TransactionValidator { async fn validate_transfer_amounts( &self, transaction_resolved: &mut VersionedTransactionResolved, + rpc_client: &RpcClient, ) -> Result<(), KoraError> { - let total_outflow = self.calculate_total_outflow(transaction_resolved).await?; + let total_outflow = self.calculate_total_outflow(transaction_resolved, rpc_client).await?; if total_outflow > self.max_allowed_lamports { return Err(KoraError::InvalidTransaction(format!( @@ -306,9 +308,16 @@ impl TransactionValidator { async fn calculate_total_outflow( &self, transaction_resolved: &mut VersionedTransactionResolved, + rpc_client: &RpcClient, ) -> Result { - FeeConfigUtil::calculate_fee_payer_outflow(&self.fee_payer_pubkey, transaction_resolved) - .await + let config = get_config()?; + FeeConfigUtil::calculate_fee_payer_outflow( + &self.fee_payer_pubkey, + transaction_resolved, + rpc_client, + &config.validation.price_source, + ) + .await } pub async fn validate_token_payment( @@ -337,7 +346,9 @@ impl TransactionValidator { #[cfg(test)] mod tests { use crate::{ - config::FeePayerPolicy, state::update_config, tests::config_mock::ConfigMockBuilder, + config::FeePayerPolicy, + state::update_config, + tests::{config_mock::ConfigMockBuilder, rpc_mock::RpcMockBuilder}, transaction::TransactionUtil, }; use serial_test::serial; @@ -398,6 +409,7 @@ mod tests { async fn test_validate_transaction() { let fee_payer = Pubkey::new_unique(); setup_default_config(); + let rpc_client = RpcMockBuilder::new().build(); let validator = TransactionValidator::new(fee_payer).unwrap(); @@ -407,7 +419,7 @@ mod tests { let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - assert!(validator.validate_transaction(&mut transaction).await.is_ok()); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); } #[tokio::test] @@ -415,6 +427,7 @@ mod tests { async fn test_transfer_amount_limits() { let fee_payer = Pubkey::new_unique(); setup_default_config(); + let rpc_client = RpcMockBuilder::new().build(); let validator = TransactionValidator::new(fee_payer).unwrap(); let sender = Pubkey::new_unique(); @@ -425,14 +438,14 @@ mod tests { let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - assert!(validator.validate_transaction(&mut transaction).await.is_ok()); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); // Test multiple transfers let instructions = vec![transfer(&sender, &recipient, 500_000), transfer(&sender, &recipient, 500_000)]; let message = VersionedMessage::Legacy(Message::new(&instructions, Some(&fee_payer))); let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - assert!(validator.validate_transaction(&mut transaction).await.is_ok()); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); } #[tokio::test] @@ -440,6 +453,7 @@ mod tests { async fn test_validate_programs() { let fee_payer = Pubkey::new_unique(); setup_default_config(); + let rpc_client = RpcMockBuilder::new().build(); let validator = TransactionValidator::new(fee_payer).unwrap(); let sender = Pubkey::new_unique(); @@ -449,7 +463,7 @@ mod tests { let instruction = transfer(&sender, &recipient, 1000); let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - assert!(validator.validate_transaction(&mut transaction).await.is_ok()); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); // Test disallowed program let fake_program = Pubkey::new_unique(); @@ -461,7 +475,7 @@ mod tests { ); let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - assert!(validator.validate_transaction(&mut transaction).await.is_err()); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); } #[tokio::test] @@ -477,6 +491,7 @@ mod tests { .build(); update_config(config).unwrap(); + let rpc_client = RpcMockBuilder::new().build(); let validator = TransactionValidator::new(fee_payer).unwrap(); let sender = Pubkey::new_unique(); let recipient = Pubkey::new_unique(); @@ -490,7 +505,7 @@ mod tests { let message = VersionedMessage::Legacy(Message::new(&instructions, Some(&fee_payer))); let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); transaction.transaction.signatures = vec![Default::default(); 3]; // Add 3 dummy signatures - assert!(validator.validate_transaction(&mut transaction).await.is_err()); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); } #[tokio::test] @@ -498,6 +513,7 @@ mod tests { async fn test_sign_and_send_transaction_mode() { let fee_payer = Pubkey::new_unique(); setup_default_config(); + let rpc_client = RpcMockBuilder::new().build(); let validator = TransactionValidator::new(fee_payer).unwrap(); let sender = Pubkey::new_unique(); @@ -507,13 +523,13 @@ mod tests { let instruction = transfer(&sender, &recipient, 1000); let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - assert!(validator.validate_transaction(&mut transaction).await.is_ok()); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); // Test SignAndSend mode without fee payer (should succeed) let instruction = transfer(&sender, &recipient, 1000); let message = VersionedMessage::Legacy(Message::new(&[instruction], None)); // No fee payer specified let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - assert!(validator.validate_transaction(&mut transaction).await.is_ok()); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); } #[tokio::test] @@ -521,13 +537,14 @@ mod tests { async fn test_empty_transaction() { let fee_payer = Pubkey::new_unique(); setup_default_config(); + let rpc_client = RpcMockBuilder::new().build(); let validator = TransactionValidator::new(fee_payer).unwrap(); // Create an empty message using Message::new with empty instructions let message = VersionedMessage::Legacy(Message::new(&[], Some(&fee_payer))); let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - assert!(validator.validate_transaction(&mut transaction).await.is_err()); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); } #[tokio::test] @@ -545,6 +562,7 @@ mod tests { .build(); update_config(config).unwrap(); + let rpc_client = RpcMockBuilder::new().build(); let validator = TransactionValidator::new(fee_payer).unwrap(); let instruction = transfer( &Pubkey::from_str("hndXZGK45hCxfBYvxejAXzCfCujoqkNf7rk4sTB8pek").unwrap(), @@ -553,7 +571,7 @@ mod tests { ); let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - assert!(validator.validate_transaction(&mut transaction).await.is_err()); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); } #[tokio::test] @@ -563,6 +581,7 @@ mod tests { let recipient = Pubkey::new_unique(); // Test with allow_sol_transfers = true + let rpc_client = RpcMockBuilder::new().build(); setup_config_with_policy(FeePayerPolicy { allow_sol_transfers: true, ..Default::default() @@ -574,9 +593,10 @@ mod tests { let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - assert!(validator.validate_transaction(&mut transaction).await.is_ok()); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); // Test with allow_sol_transfers = false + let rpc_client = RpcMockBuilder::new().build(); setup_config_with_policy(FeePayerPolicy { allow_sol_transfers: false, ..Default::default() @@ -587,7 +607,7 @@ mod tests { let instruction = transfer(&fee_payer, &recipient, 1000); let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - assert!(validator.validate_transaction(&mut transaction).await.is_err()); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); } #[tokio::test] @@ -597,6 +617,7 @@ mod tests { let new_owner = Pubkey::new_unique(); // Test with allow_assign = true + let rpc_client = RpcMockBuilder::new().build(); setup_config_with_policy(FeePayerPolicy { allow_assign: true, ..Default::default() }); let validator = TransactionValidator::new(fee_payer).unwrap(); @@ -604,9 +625,10 @@ mod tests { let instruction = assign(&fee_payer, &new_owner); let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - assert!(validator.validate_transaction(&mut transaction).await.is_ok()); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); // Test with allow_assign = false + let rpc_client = RpcMockBuilder::new().build(); setup_config_with_policy(FeePayerPolicy { allow_assign: false, ..Default::default() }); let validator = TransactionValidator::new(fee_payer).unwrap(); @@ -614,7 +636,7 @@ mod tests { let instruction = assign(&fee_payer, &new_owner); let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - assert!(validator.validate_transaction(&mut transaction).await.is_err()); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); } #[tokio::test] @@ -626,6 +648,7 @@ mod tests { let recipient_token_account = Pubkey::new_unique(); // Test with allow_spl_transfers = true + let rpc_client = RpcMockBuilder::new().build(); setup_spl_config_with_policy(FeePayerPolicy { allow_spl_transfers: true, ..Default::default() @@ -645,9 +668,10 @@ mod tests { let message = VersionedMessage::Legacy(Message::new(&[transfer_ix], Some(&fee_payer))); let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - assert!(validator.validate_transaction(&mut transaction).await.is_ok()); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); // Test with allow_spl_transfers = false + let rpc_client = RpcMockBuilder::new().build(); setup_spl_config_with_policy(FeePayerPolicy { allow_spl_transfers: false, ..Default::default() @@ -667,7 +691,7 @@ mod tests { let message = VersionedMessage::Legacy(Message::new(&[transfer_ix], Some(&fee_payer))); let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - assert!(validator.validate_transaction(&mut transaction).await.is_err()); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); // Test with other account as source - should always pass let other_signer = Pubkey::new_unique(); @@ -683,7 +707,7 @@ mod tests { let message = VersionedMessage::Legacy(Message::new(&[transfer_ix], Some(&fee_payer))); let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - assert!(validator.validate_transaction(&mut transaction).await.is_ok()); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); } #[tokio::test] @@ -696,6 +720,9 @@ mod tests { let mint = Pubkey::new_unique(); // Test with allow_token2022_transfers = true + let rpc_client = RpcMockBuilder::new() + .with_mint_account(2) // Mock mint with 2 decimals for SPL outflow calculation + .build(); setup_token2022_config_with_policy(FeePayerPolicy { allow_token2022_transfers: true, ..Default::default() @@ -710,16 +737,19 @@ mod tests { &recipient_token_account, &fee_payer, // fee payer is the signer &[], - 1000, + 1, 2, ) .unwrap(); let message = VersionedMessage::Legacy(Message::new(&[transfer_ix], Some(&fee_payer))); let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - assert!(validator.validate_transaction(&mut transaction).await.is_ok()); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); // Test with allow_token2022_transfers = false + let rpc_client = RpcMockBuilder::new() + .with_mint_account(2) // Mock mint with 2 decimals for SPL outflow calculation + .build(); setup_token2022_config_with_policy(FeePayerPolicy { allow_token2022_transfers: false, ..Default::default() @@ -743,7 +773,7 @@ mod tests { let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); // Should fail because fee payer is not allowed to be source - assert!(validator.validate_transaction(&mut transaction).await.is_err()); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); // Test with other account as source - should always pass let other_signer = Pubkey::new_unique(); @@ -763,7 +793,7 @@ mod tests { let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); // Should pass because fee payer is not the source - assert!(validator.validate_transaction(&mut transaction).await.is_ok()); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); } #[tokio::test] @@ -778,6 +808,7 @@ mod tests { .build(); update_config(config).unwrap(); + let rpc_client = RpcMockBuilder::new().build(); let validator = TransactionValidator::new(fee_payer).unwrap(); // Test 1: Fee payer as sender in Transfer - should add to outflow @@ -786,7 +817,8 @@ mod tests { let message = VersionedMessage::Legacy(Message::new(&[transfer_instruction], Some(&fee_payer))); let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - let outflow = validator.calculate_total_outflow(&mut transaction).await.unwrap(); + let outflow = + validator.calculate_total_outflow(&mut transaction, &rpc_client).await.unwrap(); assert_eq!(outflow, 100_000, "Transfer from fee payer should add to outflow"); // Test 2: Fee payer as recipient in Transfer - should subtract from outflow (account closure) @@ -796,7 +828,8 @@ mod tests { VersionedMessage::Legacy(Message::new(&[transfer_instruction], Some(&fee_payer))); let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - let outflow = validator.calculate_total_outflow(&mut transaction).await.unwrap(); + let outflow = + validator.calculate_total_outflow(&mut transaction, &rpc_client).await.unwrap(); assert_eq!(outflow, 0, "Transfer to fee payer should subtract from outflow"); // 0 - 50_000 = 0 (saturating_sub) // Test 3: Fee payer as funding account in CreateAccount - should add to outflow @@ -811,7 +844,8 @@ mod tests { let message = VersionedMessage::Legacy(Message::new(&[create_instruction], Some(&fee_payer))); let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - let outflow = validator.calculate_total_outflow(&mut transaction).await.unwrap(); + let outflow = + validator.calculate_total_outflow(&mut transaction, &rpc_client).await.unwrap(); assert_eq!(outflow, 200_000, "CreateAccount funded by fee payer should add to outflow"); // Test 4: Fee payer as funding account in CreateAccountWithSeed - should add to outflow @@ -829,7 +863,8 @@ mod tests { Some(&fee_payer), )); let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - let outflow = validator.calculate_total_outflow(&mut transaction).await.unwrap(); + let outflow = + validator.calculate_total_outflow(&mut transaction, &rpc_client).await.unwrap(); assert_eq!( outflow, 300_000, "CreateAccountWithSeed funded by fee payer should add to outflow" @@ -849,7 +884,8 @@ mod tests { Some(&fee_payer), )); let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - let outflow = validator.calculate_total_outflow(&mut transaction).await.unwrap(); + let outflow = + validator.calculate_total_outflow(&mut transaction, &rpc_client).await.unwrap(); assert_eq!(outflow, 150_000, "TransferWithSeed from fee payer should add to outflow"); // Test 6: Multiple instructions - should sum correctly @@ -860,7 +896,8 @@ mod tests { ]; let message = VersionedMessage::Legacy(Message::new(&instructions, Some(&fee_payer))); let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - let outflow = validator.calculate_total_outflow(&mut transaction).await.unwrap(); + let outflow = + validator.calculate_total_outflow(&mut transaction, &rpc_client).await.unwrap(); assert_eq!( outflow, 120_000, "Multiple instructions should sum correctly: 100000 - 30000 + 50000 = 120000" @@ -872,7 +909,8 @@ mod tests { let message = VersionedMessage::Legacy(Message::new(&[transfer_instruction], Some(&fee_payer))); let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - let outflow = validator.calculate_total_outflow(&mut transaction).await.unwrap(); + let outflow = + validator.calculate_total_outflow(&mut transaction, &rpc_client).await.unwrap(); assert_eq!(outflow, 0, "Transfer from other account should not affect outflow"); // Test 8: Other account funding CreateAccount - should not affect outflow @@ -882,7 +920,8 @@ mod tests { let message = VersionedMessage::Legacy(Message::new(&[create_instruction], Some(&fee_payer))); let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - let outflow = validator.calculate_total_outflow(&mut transaction).await.unwrap(); + let outflow = + validator.calculate_total_outflow(&mut transaction, &rpc_client).await.unwrap(); assert_eq!(outflow, 0, "CreateAccount funded by other account should not affect outflow"); } @@ -894,6 +933,7 @@ mod tests { let mint = Pubkey::new_unique(); // Test with allow_burn = true + let rpc_client = RpcMockBuilder::new().build(); setup_spl_config_with_policy(FeePayerPolicy { allow_burn: true, ..Default::default() }); let validator = TransactionValidator::new(fee_payer).unwrap(); @@ -911,9 +951,10 @@ mod tests { let message = VersionedMessage::Legacy(Message::new(&[burn_ix], Some(&fee_payer))); let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); // Should pass because allow_burn is true by default - assert!(validator.validate_transaction(&mut transaction).await.is_ok()); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); // Test with allow_burn = false + let rpc_client = RpcMockBuilder::new().build(); setup_spl_config_with_policy(FeePayerPolicy { allow_burn: false, ..Default::default() }); let validator = TransactionValidator::new(fee_payer).unwrap(); @@ -932,7 +973,7 @@ mod tests { let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); // Should fail because fee payer cannot burn tokens when allow_burn is false - assert!(validator.validate_transaction(&mut transaction).await.is_err()); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); // Test burn_checked instruction let burn_checked_ix = spl_token::instruction::burn_checked( @@ -950,7 +991,7 @@ mod tests { let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); // Should also fail for burn_checked - assert!(validator.validate_transaction(&mut transaction).await.is_err()); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); } #[tokio::test] @@ -961,6 +1002,7 @@ mod tests { let destination = Pubkey::new_unique(); // Test with allow_close_account = true + let rpc_client = RpcMockBuilder::new().build(); setup_spl_config_with_policy(FeePayerPolicy { allow_close_account: true, ..Default::default() @@ -980,9 +1022,10 @@ mod tests { let message = VersionedMessage::Legacy(Message::new(&[close_ix], Some(&fee_payer))); let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); // Should pass because allow_close_account is true by default - assert!(validator.validate_transaction(&mut transaction).await.is_ok()); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); // Test with allow_close_account = false + let rpc_client = RpcMockBuilder::new().build(); setup_spl_config_with_policy(FeePayerPolicy { allow_close_account: false, ..Default::default() @@ -1003,7 +1046,7 @@ mod tests { let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); // Should fail because fee payer cannot close accounts when allow_close_account is false - assert!(validator.validate_transaction(&mut transaction).await.is_err()); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); } #[tokio::test] @@ -1014,6 +1057,7 @@ mod tests { let delegate = Pubkey::new_unique(); // Test with allow_approve = true + let rpc_client = RpcMockBuilder::new().build(); setup_spl_config_with_policy(FeePayerPolicy { allow_approve: true, ..Default::default() }); let validator = TransactionValidator::new(fee_payer).unwrap(); @@ -1031,9 +1075,10 @@ mod tests { let message = VersionedMessage::Legacy(Message::new(&[approve_ix], Some(&fee_payer))); let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); // Should pass because allow_approve is true by default - assert!(validator.validate_transaction(&mut transaction).await.is_ok()); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); // Test with allow_approve = false + let rpc_client = RpcMockBuilder::new().build(); setup_spl_config_with_policy(FeePayerPolicy { allow_approve: false, ..Default::default() }); let validator = TransactionValidator::new(fee_payer).unwrap(); @@ -1052,7 +1097,7 @@ mod tests { let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); // Should fail because fee payer cannot approve when allow_approve is false - assert!(validator.validate_transaction(&mut transaction).await.is_err()); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); // Test approve_checked instruction let mint = Pubkey::new_unique(); @@ -1073,7 +1118,7 @@ mod tests { let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); // Should also fail for approve_checked - assert!(validator.validate_transaction(&mut transaction).await.is_err()); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); } #[tokio::test] @@ -1084,6 +1129,7 @@ mod tests { let mint = Pubkey::new_unique(); // Test with allow_burn = false for Token2022 + let rpc_client = RpcMockBuilder::new().build(); setup_token2022_config_with_policy(FeePayerPolicy { allow_burn: false, ..Default::default() @@ -1104,7 +1150,7 @@ mod tests { let message = VersionedMessage::Legacy(Message::new(&[burn_ix], Some(&fee_payer))); let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); // Should fail for Token2022 burn - assert!(validator.validate_transaction(&mut transaction).await.is_err()); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); } #[tokio::test] @@ -1115,6 +1161,7 @@ mod tests { let destination = Pubkey::new_unique(); // Test with allow_close_account = false for Token2022 + let rpc_client = RpcMockBuilder::new().build(); setup_token2022_config_with_policy(FeePayerPolicy { allow_close_account: false, ..FeePayerPolicy::default() @@ -1134,7 +1181,7 @@ mod tests { let message = VersionedMessage::Legacy(Message::new(&[close_ix], Some(&fee_payer))); let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); // Should fail for Token2022 close account - assert!(validator.validate_transaction(&mut transaction).await.is_err()); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); } #[tokio::test] @@ -1145,6 +1192,7 @@ mod tests { let delegate = Pubkey::new_unique(); // Test with allow_approve = true + let rpc_client = RpcMockBuilder::new().build(); setup_token2022_config_with_policy(FeePayerPolicy { allow_approve: true, ..Default::default() @@ -1165,9 +1213,10 @@ mod tests { let message = VersionedMessage::Legacy(Message::new(&[approve_ix], Some(&fee_payer))); let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); // Should pass because allow_approve is true by default - assert!(validator.validate_transaction(&mut transaction).await.is_ok()); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); // Test with allow_approve = false + let rpc_client = RpcMockBuilder::new().build(); setup_token2022_config_with_policy(FeePayerPolicy { allow_approve: false, ..Default::default() @@ -1189,7 +1238,7 @@ mod tests { let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); // Should fail because fee payer cannot approve when allow_approve is false - assert!(validator.validate_transaction(&mut transaction).await.is_err()); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); // Test approve_checked instruction let mint = Pubkey::new_unique(); @@ -1210,6 +1259,6 @@ mod tests { let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); // Should also fail for approve_checked - assert!(validator.validate_transaction(&mut transaction).await.is_err()); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); } } diff --git a/tests/external/jupiter_integration.rs b/tests/external/jupiter_integration.rs index 5cd2f26e..681f6c11 100644 --- a/tests/external/jupiter_integration.rs +++ b/tests/external/jupiter_integration.rs @@ -1,3 +1,4 @@ +use jsonrpsee::core::Error; use kora_lib::oracle::{get_price_oracle, PriceSource, RetryingPriceOracle}; use std::time::Duration; @@ -76,3 +77,27 @@ async fn test_jupiter_integration_sol() { } } } + +#[tokio::test] +async fn test_jupiter_integration_unknown_token() { + const SOL_MINT: &str = "So11111111111111111111111111111111111111112"; + // Invalid token mint + const UNKNOWN_TOKEN_MINT: &str = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1w"; + + let oracle = get_price_oracle(PriceSource::Jupiter); + let retrying_oracle = RetryingPriceOracle::new(3, Duration::from_millis(500), oracle); + + let result = retrying_oracle + .get_token_prices(&[SOL_MINT.to_string(), UNKNOWN_TOKEN_MINT.to_string()]) + .await; + + assert!(result.is_err(), "Expected error for unknown token"); + let error = result.unwrap_err(); + assert!( + error.to_string().contains( + "No price data from Jupiter for mint EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1w" + ), + "Expected error message about unknown mint, got: {}", + error + ); +} diff --git a/tests/rpc/fee_estimation.rs b/tests/rpc/fee_estimation.rs index 2cf96306..2d5b280e 100644 --- a/tests/rpc/fee_estimation.rs +++ b/tests/rpc/fee_estimation.rs @@ -1,6 +1,10 @@ use crate::common::*; use jsonrpsee::rpc_params; -use solana_sdk::{program_pack::Pack, pubkey::Pubkey, signature::Keypair, signer::Signer}; +use solana_sdk::{ + program_pack::Pack, pubkey::Pubkey, signature::Keypair, signer::Signer, + transaction::Transaction, +}; +use spl_associated_token_account::get_associated_token_address; #[tokio::test] async fn test_estimate_transaction_fee_legacy() { @@ -345,3 +349,87 @@ async fn test_estimate_fee_comprehensive_with_token_accounts_creation() { expected_minimum_fee + 50_000 ); } + +#[tokio::test] +async fn test_estimate_fee_with_spl_token_transfer_from_fee_payer() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + + let fee_payer = FeePayerTestHelper::get_fee_payer_pubkey(); + let usdc_mint = USDCMintTestHelper::get_test_usdc_mint_pubkey(); + let recipient = Pubkey::new_unique(); + + let fee_payer_ata = get_associated_token_address(&fee_payer, &usdc_mint); + let mint_amount = 10_000_000; + let sender = SenderTestHelper::get_test_sender_keypair(); + + let create_recipient_ata_ix = + spl_associated_token_account::instruction::create_associated_token_account_idempotent( + &sender.pubkey(), // payer + &recipient, // owner + &usdc_mint, // mint + &spl_token::id(), + ); + + let mint_instruction = spl_token::instruction::mint_to( + &spl_token::id(), + &usdc_mint, + &fee_payer_ata, + &sender.pubkey(), // mint authority is sender + &[], + mint_amount, + ) + .expect("Failed to create mint instruction"); + + let recent_blockhash = + ctx.rpc_client().get_latest_blockhash().await.expect("Failed to get blockhash"); + + let fund_transaction = Transaction::new_signed_with_payer( + &[create_recipient_ata_ix, mint_instruction], + Some(&sender.pubkey()), + &[&sender], + recent_blockhash, + ); + + ctx.rpc_client() + .send_and_confirm_transaction(&fund_transaction) + .await + .expect("Failed to fund fee payer ATA"); + + let test_tx = ctx + .transaction_builder() + .with_fee_payer(fee_payer) + .with_spl_transfer_checked( + &usdc_mint, &fee_payer, // Fee payer owns the source token account + &recipient, 1_000_000, 6, + ) + .with_spl_transfer_checked( + &usdc_mint, &fee_payer, // Fee payer owns the source token account + &recipient, 3_000_000, 6, + ) + .build() + .await + .expect("Failed to build transaction"); + + let response: serde_json::Value = ctx + .rpc_call("estimateTransactionFee", rpc_params![test_tx]) + .await + .expect("Failed to estimate transaction fee"); + + response.assert_success(); + response.assert_has_field("fee_in_lamports"); + + let fee_lamports = response["fee_in_lamports"].as_u64().unwrap(); + + // Expected fee breakdown: + // - Base signature fee: ~5,000 lamports + // - SPL token outflow #1: 1 USDC ร— 0.001 SOL/USDC = 1,000,000 lamports + // - SPL token outflow #2: 3 USDC ร— 0.001 SOL/USDC = 3,000,000 lamports + // - Payment instruction: ~50 lamports + // Total: ~4,005,050 lamports + + println!("fee_lamports: {fee_lamports}"); + assert_eq!( + fee_lamports, 4_005_050, + "Fee should include SPL token outflow value. Got {fee_lamports} expected 4_005_050", + ); +} From 4319320095c3b1afcdcfa733e6e95ff7cd2f6799 Mon Sep 17 00:00:00 2001 From: jo <17280917+dev-jodee@users.noreply.github.com> Date: Thu, 16 Oct 2025 13:10:44 -0400 Subject: [PATCH 15/29] feat: (PRO-439) (PRO-440) Method validation for allowed methods to er... (#234) * bugfix: Constant time hmac verification function * feat: (PRO-439) (PRO-440) Method validation for allowed methods to error out faster and reduce useless processing --- crates/lib/src/config.rs | 36 +++ crates/lib/src/rpc_server/auth.rs | 225 +++++++++++++++--- crates/lib/src/rpc_server/middleware_utils.rs | 17 ++ crates/lib/src/rpc_server/server.rs | 13 +- 4 files changed, 254 insertions(+), 37 deletions(-) diff --git a/crates/lib/src/config.rs b/crates/lib/src/config.rs index a6dd0725..6e8534d8 100644 --- a/crates/lib/src/config.rs +++ b/crates/lib/src/config.rs @@ -260,6 +260,42 @@ impl EnabledMethods { ] .into_iter() } + + /// Returns a Vec of enabled JSON-RPC method names + pub fn get_enabled_method_names(&self) -> Vec { + let mut methods = Vec::new(); + if self.liveness { + methods.push("liveness".to_string()); + } + if self.estimate_transaction_fee { + methods.push("estimateTransactionFee".to_string()); + } + if self.get_supported_tokens { + methods.push("getSupportedTokens".to_string()); + } + if self.get_payer_signer { + methods.push("getPayerSigner".to_string()); + } + if self.sign_transaction { + methods.push("signTransaction".to_string()); + } + if self.sign_and_send_transaction { + methods.push("signAndSendTransaction".to_string()); + } + if self.transfer_transaction { + methods.push("transferTransaction".to_string()); + } + if self.get_blockhash { + methods.push("getBlockhash".to_string()); + } + if self.get_config { + methods.push("getConfig".to_string()); + } + if self.sign_transaction_if_paid { + methods.push("signTransactionIfPaid".to_string()); + } + methods + } } impl IntoIterator for &EnabledMethods { diff --git a/crates/lib/src/rpc_server/auth.rs b/crates/lib/src/rpc_server/auth.rs index b666023e..d61d1e2b 100644 --- a/crates/lib/src/rpc_server/auth.rs +++ b/crates/lib/src/rpc_server/auth.rs @@ -1,20 +1,22 @@ use crate::{ constant::{X_API_KEY, X_HMAC_SIGNATURE, X_TIMESTAMP}, - rpc_server::middleware_utils::{extract_parts_and_body_bytes, get_jsonrpc_method}, + rpc_server::middleware_utils::{extract_parts_and_body_bytes, verify_jsonrpc_method}, }; use hmac::{Hmac, Mac}; use http::{Request, Response, StatusCode}; use jsonrpsee::server::logger::Body; use sha2::Sha256; +use std::collections::HashSet; #[derive(Clone)] pub struct ApiKeyAuthLayer { api_key: String, + allowed_methods: HashSet, } impl ApiKeyAuthLayer { - pub fn new(api_key: String) -> Self { - Self { api_key } + pub fn new(api_key: String, allowed_methods: Vec) -> Self { + Self { api_key, allowed_methods: allowed_methods.into_iter().collect() } } } @@ -22,12 +24,17 @@ impl ApiKeyAuthLayer { pub struct ApiKeyAuthService { inner: S, api_key: String, + allowed_methods: HashSet, } impl tower::Layer for ApiKeyAuthLayer { type Service = ApiKeyAuthService; fn layer(&self, inner: S) -> Self::Service { - ApiKeyAuthService { inner, api_key: self.api_key.clone() } + ApiKeyAuthService { + inner, + api_key: self.api_key.clone(), + allowed_methods: self.allowed_methods.clone(), + } } } @@ -51,6 +58,7 @@ where fn call(&mut self, request: Request) -> Self::Future { let api_key = self.api_key.clone(); + let allowed_methods = self.allowed_methods.clone(); let mut inner = self.inner.clone(); Box::pin(async move { @@ -60,10 +68,18 @@ where .expect("Failed to build unauthorized response"); let (parts, body_bytes) = extract_parts_and_body_bytes(request).await; - if get_jsonrpc_method(&body_bytes) == Some("liveness".to_string()) { - let new_body = Body::from(body_bytes); - let new_request = Request::from_parts(parts, new_body); - return inner.call(new_request).await; + + match verify_jsonrpc_method(&body_bytes, &allowed_methods) { + Ok(method) => { + if method == "liveness" { + let new_body = Body::from(body_bytes); + let new_request = Request::from_parts(parts, new_body); + return inner.call(new_request).await; + } + } + Err(_) => { + return Ok(unauthorized_response); + } } let req = Request::from_parts(parts, Body::from(body_bytes)); @@ -82,11 +98,12 @@ where pub struct HmacAuthLayer { secret: String, max_timestamp_age: i64, + allowed_methods: HashSet, } impl HmacAuthLayer { - pub fn new(secret: String, max_timestamp_age: i64) -> Self { - Self { secret, max_timestamp_age } + pub fn new(secret: String, max_timestamp_age: i64, allowed_methods: Vec) -> Self { + Self { secret, max_timestamp_age, allowed_methods: allowed_methods.into_iter().collect() } } } @@ -98,6 +115,7 @@ impl tower::Layer for HmacAuthLayer { inner, secret: self.secret.clone(), max_timestamp_age: self.max_timestamp_age, + allowed_methods: self.allowed_methods.clone(), } } } @@ -107,6 +125,7 @@ pub struct HmacAuthService { inner: S, secret: String, max_timestamp_age: i64, + allowed_methods: HashSet, } impl tower::Service> for HmacAuthService @@ -130,6 +149,7 @@ where fn call(&mut self, request: Request) -> Self::Future { let secret = self.secret.clone(); let max_timestamp_age = self.max_timestamp_age; + let allowed_methods = self.allowed_methods.clone(); let mut inner = self.inner.clone(); Box::pin(async move { @@ -141,12 +161,19 @@ where let signature_header = request.headers().get(X_HMAC_SIGNATURE).cloned(); let timestamp_header = request.headers().get(X_TIMESTAMP).cloned(); - // Since our proxy for get /liveness transforms the request in a POST, we need to check the liveness via the method in the body let (parts, body_bytes) = extract_parts_and_body_bytes(request).await; - if get_jsonrpc_method(&body_bytes) == Some("liveness".to_string()) { - let new_body = Body::from(body_bytes); - let new_request = Request::from_parts(parts, new_body); - return inner.call(new_request).await; + + match verify_jsonrpc_method(&body_bytes, &allowed_methods) { + Ok(method) => { + if method == "liveness" { + let new_body = Body::from(body_bytes); + let new_request = Request::from_parts(parts, new_body); + return inner.call(new_request).await; + } + } + Err(_) => { + return Ok(unauthorized_response); + } } let (signature, timestamp) = @@ -191,9 +218,17 @@ where }; mac.update(message.as_bytes()); - let expected_signature = hex::encode(mac.finalize().into_bytes()); - if signature != expected_signature { + let signature_bytes = match hex::decode(signature) { + Ok(bytes) => bytes, + Err(_) => { + log::error!("HMAC signature hex decode failed"); + return Ok(unauthorized_response); + } + }; + + // Constant time comparison prevents timing attacks + if mac.verify_slice(&signature_bytes).is_err() { return Ok(unauthorized_response); } @@ -240,12 +275,14 @@ mod tests { #[tokio::test] async fn test_api_key_auth_valid_key() { - let layer = ApiKeyAuthLayer::new("test-key".to_string()); + let allowed_methods = vec!["liveness".to_string(), "getConfig".to_string()]; + let layer = ApiKeyAuthLayer::new("test-key".to_string(), allowed_methods); let mut service = layer.layer(MockService); + let body = r#"{"jsonrpc":"2.0","method":"getConfig","id":1}"#; let request = Request::builder() .uri("/test") .header(X_API_KEY, "test-key") - .body(Body::empty()) + .body(Body::from(body)) .unwrap(); let response = service.ready().await.unwrap().call(request).await.unwrap(); @@ -254,12 +291,14 @@ mod tests { #[tokio::test] async fn test_api_key_auth_invalid_key() { - let layer = ApiKeyAuthLayer::new("test-key".to_string()); + let allowed_methods = vec!["liveness".to_string(), "getConfig".to_string()]; + let layer = ApiKeyAuthLayer::new("test-key".to_string(), allowed_methods); let mut service = layer.layer(MockService); + let body = r#"{"jsonrpc":"2.0","method":"getConfig","id":1}"#; let request = Request::builder() .uri("/test") .header(X_API_KEY, "wrong-key") - .body(Body::empty()) + .body(Body::from(body)) .unwrap(); let response = service.ready().await.unwrap().call(request).await.unwrap(); @@ -268,9 +307,11 @@ mod tests { #[tokio::test] async fn test_api_key_auth_missing_header() { - let layer = ApiKeyAuthLayer::new("test-key".to_string()); + let allowed_methods = vec!["liveness".to_string(), "getConfig".to_string()]; + let layer = ApiKeyAuthLayer::new("test-key".to_string(), allowed_methods); let mut service = layer.layer(MockService); - let request = Request::builder().uri("/test").body(Body::empty()).unwrap(); + let body = r#"{"jsonrpc":"2.0","method":"getConfig","id":1}"#; + let request = Request::builder().uri("/test").body(Body::from(body)).unwrap(); let response = service.ready().await.unwrap().call(request).await.unwrap(); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); @@ -278,7 +319,8 @@ mod tests { #[tokio::test] async fn test_api_key_auth_liveness_bypass() { - let layer = ApiKeyAuthLayer::new("test-key".to_string()); + let allowed_methods = vec!["liveness".to_string()]; + let layer = ApiKeyAuthLayer::new("test-key".to_string(), allowed_methods); let mut service = layer.layer(MockService); let liveness_body = r#"{"jsonrpc":"2.0","method":"liveness","params":[],"id":1}"#; let request = Request::builder() @@ -294,7 +336,9 @@ mod tests { #[tokio::test] async fn test_hmac_auth_valid_signature() { let secret = "test-secret"; - let layer = HmacAuthLayer::new(secret.to_string(), DEFAULT_MAX_TIMESTAMP_AGE); + let allowed_methods = vec!["liveness".to_string(), "getConfig".to_string()]; + let layer = + HmacAuthLayer::new(secret.to_string(), DEFAULT_MAX_TIMESTAMP_AGE, allowed_methods); let mut service = layer.layer(MockService); let timestamp = std::time::SystemTime::now() @@ -303,7 +347,7 @@ mod tests { .as_secs() .to_string(); - let body = r#"{"jsonrpc":"2.0","method":"test","id":1}"#; + let body = r#"{"jsonrpc":"2.0","method":"getConfig","id":1}"#; let message = format!("{timestamp}{body}"); let mut mac = Hmac::::new_from_slice(secret.as_bytes()).unwrap(); @@ -325,7 +369,9 @@ mod tests { #[tokio::test] async fn test_hmac_auth_invalid_signature() { let secret = "test-secret"; - let layer = HmacAuthLayer::new(secret.to_string(), DEFAULT_MAX_TIMESTAMP_AGE); + let allowed_methods = vec!["liveness".to_string(), "getConfig".to_string()]; + let layer = + HmacAuthLayer::new(secret.to_string(), DEFAULT_MAX_TIMESTAMP_AGE, allowed_methods); let mut service = layer.layer(MockService); let timestamp = std::time::SystemTime::now() @@ -334,7 +380,7 @@ mod tests { .as_secs() .to_string(); - let body = r#"{"jsonrpc":"2.0","method":"test","id":1}"#; + let body = r#"{"jsonrpc":"2.0","method":"getConfig","id":1}"#; let request = Request::builder() .method(Method::POST) @@ -351,11 +397,14 @@ mod tests { #[tokio::test] async fn test_hmac_auth_missing_headers() { let secret = "test-secret"; - let layer = HmacAuthLayer::new(secret.to_string(), DEFAULT_MAX_TIMESTAMP_AGE); + let allowed_methods = vec!["liveness".to_string(), "getConfig".to_string()]; + let layer = + HmacAuthLayer::new(secret.to_string(), DEFAULT_MAX_TIMESTAMP_AGE, allowed_methods); let mut service = layer.layer(MockService); + let body = r#"{"jsonrpc":"2.0","method":"getConfig","id":1}"#; let request = - Request::builder().method(Method::POST).uri("/test").body(Body::from("test")).unwrap(); + Request::builder().method(Method::POST).uri("/test").body(Body::from(body)).unwrap(); let response = service.ready().await.unwrap().call(request).await.unwrap(); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); @@ -364,7 +413,9 @@ mod tests { #[tokio::test] async fn test_hmac_auth_expired_timestamp() { let secret = "test-secret"; - let layer = HmacAuthLayer::new(secret.to_string(), DEFAULT_MAX_TIMESTAMP_AGE); + let allowed_methods = vec!["liveness".to_string(), "getConfig".to_string()]; + let layer = + HmacAuthLayer::new(secret.to_string(), DEFAULT_MAX_TIMESTAMP_AGE, allowed_methods); let mut service = layer.layer(MockService); // Timestamp from 10 minutes ago (expired) @@ -373,7 +424,7 @@ mod tests { - 600) .to_string(); - let body = r#"{"jsonrpc":"2.0","method":"test","id":1}"#; + let body = r#"{"jsonrpc":"2.0","method":"getConfig","id":1}"#; let message = format!("{expired_timestamp}{body}"); let mut mac = Hmac::::new_from_slice(secret.as_bytes()).unwrap(); @@ -395,7 +446,9 @@ mod tests { #[tokio::test] async fn test_hmac_auth_liveness_bypass() { let secret = "test-secret"; - let layer = HmacAuthLayer::new(secret.to_string(), DEFAULT_MAX_TIMESTAMP_AGE); + let allowed_methods = vec!["liveness".to_string()]; + let layer = + HmacAuthLayer::new(secret.to_string(), DEFAULT_MAX_TIMESTAMP_AGE, allowed_methods); let mut service = layer.layer(MockService); let liveness_body = r#"{"jsonrpc":"2.0","method":"liveness","params":[],"id":1}"#; @@ -412,10 +465,12 @@ mod tests { #[tokio::test] async fn test_hmac_auth_malformed_timestamp() { let secret = "test-secret"; - let layer = HmacAuthLayer::new(secret.to_string(), DEFAULT_MAX_TIMESTAMP_AGE); + let allowed_methods = vec!["liveness".to_string(), "getConfig".to_string()]; + let layer = + HmacAuthLayer::new(secret.to_string(), DEFAULT_MAX_TIMESTAMP_AGE, allowed_methods); let mut service = layer.layer(MockService); - let body = r#"{"jsonrpc":"2.0","method":"test","id":1}"#; + let body = r#"{"jsonrpc":"2.0","method":"getConfig","id":1}"#; let request = Request::builder() .method(Method::POST) @@ -428,4 +483,104 @@ mod tests { let response = service.ready().await.unwrap().call(request).await.unwrap(); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } + + #[tokio::test] + async fn test_api_key_auth_unknown_method_rejected() { + let allowed_methods = vec!["liveness".to_string(), "getConfig".to_string()]; + let layer = ApiKeyAuthLayer::new("test-key".to_string(), allowed_methods); + let mut service = layer.layer(MockService); + let body = r#"{"jsonrpc":"2.0","method":"unknownMethod","id":1}"#; + let request = Request::builder() + .uri("/test") + .header(X_API_KEY, "test-key") + .body(Body::from(body)) + .unwrap(); + + let response = service.ready().await.unwrap().call(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + } + + #[tokio::test] + async fn test_api_key_auth_disabled_method_rejected() { + // Only allow liveness, not getConfig + let allowed_methods = vec!["liveness".to_string()]; + let layer = ApiKeyAuthLayer::new("test-key".to_string(), allowed_methods); + let mut service = layer.layer(MockService); + let body = r#"{"jsonrpc":"2.0","method":"getConfig","id":1}"#; + let request = Request::builder() + .uri("/test") + .header(X_API_KEY, "test-key") + .body(Body::from(body)) + .unwrap(); + + let response = service.ready().await.unwrap().call(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + } + + #[tokio::test] + async fn test_hmac_auth_unknown_method_rejected() { + let secret = "test-secret"; + let allowed_methods = vec!["liveness".to_string(), "getConfig".to_string()]; + let layer = + HmacAuthLayer::new(secret.to_string(), DEFAULT_MAX_TIMESTAMP_AGE, allowed_methods); + let mut service = layer.layer(MockService); + + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() + .to_string(); + + let body = r#"{"jsonrpc":"2.0","method":"unknownMethod","id":1}"#; + let message = format!("{timestamp}{body}"); + + let mut mac = Hmac::::new_from_slice(secret.as_bytes()).unwrap(); + mac.update(message.as_bytes()); + let signature = hex::encode(mac.finalize().into_bytes()); + + let request = Request::builder() + .method(Method::POST) + .uri("/test") + .header(X_TIMESTAMP, ×tamp) + .header(X_HMAC_SIGNATURE, &signature) + .body(Body::from(body)) + .unwrap(); + + let response = service.ready().await.unwrap().call(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + } + + #[tokio::test] + async fn test_hmac_auth_disabled_method_rejected() { + let secret = "test-secret"; + // Only allow liveness, not getConfig + let allowed_methods = vec!["liveness".to_string()]; + let layer = + HmacAuthLayer::new(secret.to_string(), DEFAULT_MAX_TIMESTAMP_AGE, allowed_methods); + let mut service = layer.layer(MockService); + + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() + .to_string(); + + let body = r#"{"jsonrpc":"2.0","method":"getConfig","id":1}"#; + let message = format!("{timestamp}{body}"); + + let mut mac = Hmac::::new_from_slice(secret.as_bytes()).unwrap(); + mac.update(message.as_bytes()); + let signature = hex::encode(mac.finalize().into_bytes()); + + let request = Request::builder() + .method(Method::POST) + .uri("/test") + .header(X_TIMESTAMP, ×tamp) + .header(X_HMAC_SIGNATURE, &signature) + .body(Body::from(body)) + .unwrap(); + + let response = service.ready().await.unwrap().call(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + } } diff --git a/crates/lib/src/rpc_server/middleware_utils.rs b/crates/lib/src/rpc_server/middleware_utils.rs index d17d0799..36dfc42d 100644 --- a/crates/lib/src/rpc_server/middleware_utils.rs +++ b/crates/lib/src/rpc_server/middleware_utils.rs @@ -1,7 +1,11 @@ +use std::collections::HashSet; + use futures_util::TryStreamExt; use http::Request; use jsonrpsee::server::logger::Body; +use crate::KoraError; + pub fn default_sig_verify() -> bool { false } @@ -26,3 +30,16 @@ pub fn get_jsonrpc_method(body_bytes: &[u8]) -> Option { Err(_) => None, } } + +pub fn verify_jsonrpc_method( + body_bytes: &[u8], + allowed_methods: &HashSet, +) -> Result { + let method = get_jsonrpc_method(body_bytes); + if let Some(method) = method { + if allowed_methods.contains(&method) { + return Ok(method); + } + } + Err(KoraError::InvalidRequest("Method not allowed".to_string())) +} diff --git a/crates/lib/src/rpc_server/server.rs b/crates/lib/src/rpc_server/server.rs index 4e359f0b..5e1b4511 100644 --- a/crates/lib/src/rpc_server/server.rs +++ b/crates/lib/src/rpc_server/server.rs @@ -64,6 +64,9 @@ pub async fn run_rpc_server(rpc: KoraRpc, port: u16) -> Result Result Date: Fri, 17 Oct 2025 11:13:44 -0400 Subject: [PATCH 16/29] Allowed methods middleware is (#235) * Allowed methods middleware is now separate instead of only being available when auth is enabled. --- Makefile | 2 +- crates/lib/src/rpc_server/auth.rs | 205 +++--------------- crates/lib/src/rpc_server/middleware_utils.rs | 168 +++++++++++++- crates/lib/src/rpc_server/server.rs | 16 +- tests/rpc/basic_endpoints.rs | 8 +- 5 files changed, 214 insertions(+), 185 deletions(-) diff --git a/Makefile b/Makefile index 9d7bdffd..6d77d76f 100644 --- a/Makefile +++ b/Makefile @@ -14,4 +14,4 @@ include makefiles/METRICS.makefile all: check test build # Run all tests (unit + TypeScript + integration) -test-all: test test-ts test-integration \ No newline at end of file +test-all: build test test-ts test-integration \ No newline at end of file diff --git a/crates/lib/src/rpc_server/auth.rs b/crates/lib/src/rpc_server/auth.rs index d61d1e2b..132683fa 100644 --- a/crates/lib/src/rpc_server/auth.rs +++ b/crates/lib/src/rpc_server/auth.rs @@ -1,22 +1,20 @@ use crate::{ constant::{X_API_KEY, X_HMAC_SIGNATURE, X_TIMESTAMP}, - rpc_server::middleware_utils::{extract_parts_and_body_bytes, verify_jsonrpc_method}, + rpc_server::middleware_utils::{extract_parts_and_body_bytes, get_jsonrpc_method}, }; use hmac::{Hmac, Mac}; use http::{Request, Response, StatusCode}; use jsonrpsee::server::logger::Body; use sha2::Sha256; -use std::collections::HashSet; #[derive(Clone)] pub struct ApiKeyAuthLayer { api_key: String, - allowed_methods: HashSet, } impl ApiKeyAuthLayer { - pub fn new(api_key: String, allowed_methods: Vec) -> Self { - Self { api_key, allowed_methods: allowed_methods.into_iter().collect() } + pub fn new(api_key: String) -> Self { + Self { api_key } } } @@ -24,17 +22,12 @@ impl ApiKeyAuthLayer { pub struct ApiKeyAuthService { inner: S, api_key: String, - allowed_methods: HashSet, } impl tower::Layer for ApiKeyAuthLayer { type Service = ApiKeyAuthService; fn layer(&self, inner: S) -> Self::Service { - ApiKeyAuthService { - inner, - api_key: self.api_key.clone(), - allowed_methods: self.allowed_methods.clone(), - } + ApiKeyAuthService { inner, api_key: self.api_key.clone() } } } @@ -58,7 +51,6 @@ where fn call(&mut self, request: Request) -> Self::Future { let api_key = self.api_key.clone(); - let allowed_methods = self.allowed_methods.clone(); let mut inner = self.inner.clone(); Box::pin(async move { @@ -69,19 +61,16 @@ where let (parts, body_bytes) = extract_parts_and_body_bytes(request).await; - match verify_jsonrpc_method(&body_bytes, &allowed_methods) { - Ok(method) => { - if method == "liveness" { - let new_body = Body::from(body_bytes); - let new_request = Request::from_parts(parts, new_body); - return inner.call(new_request).await; - } - } - Err(_) => { - return Ok(unauthorized_response); + // Bypass auth for liveness endpoint + if let Some(method) = get_jsonrpc_method(&body_bytes) { + if method == "liveness" { + let new_body = Body::from(body_bytes); + let new_request = Request::from_parts(parts, new_body); + return inner.call(new_request).await; } } + // Check for API key header let req = Request::from_parts(parts, Body::from(body_bytes)); if let Some(provided_key) = req.headers().get(X_API_KEY) { if provided_key.to_str().unwrap_or("") == api_key { @@ -98,12 +87,11 @@ where pub struct HmacAuthLayer { secret: String, max_timestamp_age: i64, - allowed_methods: HashSet, } impl HmacAuthLayer { - pub fn new(secret: String, max_timestamp_age: i64, allowed_methods: Vec) -> Self { - Self { secret, max_timestamp_age, allowed_methods: allowed_methods.into_iter().collect() } + pub fn new(secret: String, max_timestamp_age: i64) -> Self { + Self { secret, max_timestamp_age } } } @@ -115,7 +103,6 @@ impl tower::Layer for HmacAuthLayer { inner, secret: self.secret.clone(), max_timestamp_age: self.max_timestamp_age, - allowed_methods: self.allowed_methods.clone(), } } } @@ -125,7 +112,6 @@ pub struct HmacAuthService { inner: S, secret: String, max_timestamp_age: i64, - allowed_methods: HashSet, } impl tower::Service> for HmacAuthService @@ -149,7 +135,6 @@ where fn call(&mut self, request: Request) -> Self::Future { let secret = self.secret.clone(); let max_timestamp_age = self.max_timestamp_age; - let allowed_methods = self.allowed_methods.clone(); let mut inner = self.inner.clone(); Box::pin(async move { @@ -163,16 +148,12 @@ where let (parts, body_bytes) = extract_parts_and_body_bytes(request).await; - match verify_jsonrpc_method(&body_bytes, &allowed_methods) { - Ok(method) => { - if method == "liveness" { - let new_body = Body::from(body_bytes); - let new_request = Request::from_parts(parts, new_body); - return inner.call(new_request).await; - } - } - Err(_) => { - return Ok(unauthorized_response); + // Bypass auth for liveness endpoint + if let Some(method) = get_jsonrpc_method(&body_bytes) { + if method == "liveness" { + let new_body = Body::from(body_bytes); + let new_request = Request::from_parts(parts, new_body); + return inner.call(new_request).await; } } @@ -185,7 +166,7 @@ where let signature = signature.to_str().unwrap_or(""); let timestamp = timestamp.to_str().unwrap_or(""); - // Verify timestamp is within 5 minutes + // Verify timestamp is within allowed age let parsed_timestamp = timestamp.parse::(); if parsed_timestamp.is_err() { return Ok(unauthorized_response); @@ -275,8 +256,7 @@ mod tests { #[tokio::test] async fn test_api_key_auth_valid_key() { - let allowed_methods = vec!["liveness".to_string(), "getConfig".to_string()]; - let layer = ApiKeyAuthLayer::new("test-key".to_string(), allowed_methods); + let layer = ApiKeyAuthLayer::new("test-key".to_string()); let mut service = layer.layer(MockService); let body = r#"{"jsonrpc":"2.0","method":"getConfig","id":1}"#; let request = Request::builder() @@ -291,8 +271,7 @@ mod tests { #[tokio::test] async fn test_api_key_auth_invalid_key() { - let allowed_methods = vec!["liveness".to_string(), "getConfig".to_string()]; - let layer = ApiKeyAuthLayer::new("test-key".to_string(), allowed_methods); + let layer = ApiKeyAuthLayer::new("test-key".to_string()); let mut service = layer.layer(MockService); let body = r#"{"jsonrpc":"2.0","method":"getConfig","id":1}"#; let request = Request::builder() @@ -307,8 +286,7 @@ mod tests { #[tokio::test] async fn test_api_key_auth_missing_header() { - let allowed_methods = vec!["liveness".to_string(), "getConfig".to_string()]; - let layer = ApiKeyAuthLayer::new("test-key".to_string(), allowed_methods); + let layer = ApiKeyAuthLayer::new("test-key".to_string()); let mut service = layer.layer(MockService); let body = r#"{"jsonrpc":"2.0","method":"getConfig","id":1}"#; let request = Request::builder().uri("/test").body(Body::from(body)).unwrap(); @@ -319,8 +297,7 @@ mod tests { #[tokio::test] async fn test_api_key_auth_liveness_bypass() { - let allowed_methods = vec!["liveness".to_string()]; - let layer = ApiKeyAuthLayer::new("test-key".to_string(), allowed_methods); + let layer = ApiKeyAuthLayer::new("test-key".to_string()); let mut service = layer.layer(MockService); let liveness_body = r#"{"jsonrpc":"2.0","method":"liveness","params":[],"id":1}"#; let request = Request::builder() @@ -336,9 +313,7 @@ mod tests { #[tokio::test] async fn test_hmac_auth_valid_signature() { let secret = "test-secret"; - let allowed_methods = vec!["liveness".to_string(), "getConfig".to_string()]; - let layer = - HmacAuthLayer::new(secret.to_string(), DEFAULT_MAX_TIMESTAMP_AGE, allowed_methods); + let layer = HmacAuthLayer::new(secret.to_string(), DEFAULT_MAX_TIMESTAMP_AGE); let mut service = layer.layer(MockService); let timestamp = std::time::SystemTime::now() @@ -369,9 +344,7 @@ mod tests { #[tokio::test] async fn test_hmac_auth_invalid_signature() { let secret = "test-secret"; - let allowed_methods = vec!["liveness".to_string(), "getConfig".to_string()]; - let layer = - HmacAuthLayer::new(secret.to_string(), DEFAULT_MAX_TIMESTAMP_AGE, allowed_methods); + let layer = HmacAuthLayer::new(secret.to_string(), DEFAULT_MAX_TIMESTAMP_AGE); let mut service = layer.layer(MockService); let timestamp = std::time::SystemTime::now() @@ -397,9 +370,7 @@ mod tests { #[tokio::test] async fn test_hmac_auth_missing_headers() { let secret = "test-secret"; - let allowed_methods = vec!["liveness".to_string(), "getConfig".to_string()]; - let layer = - HmacAuthLayer::new(secret.to_string(), DEFAULT_MAX_TIMESTAMP_AGE, allowed_methods); + let layer = HmacAuthLayer::new(secret.to_string(), DEFAULT_MAX_TIMESTAMP_AGE); let mut service = layer.layer(MockService); let body = r#"{"jsonrpc":"2.0","method":"getConfig","id":1}"#; @@ -413,9 +384,7 @@ mod tests { #[tokio::test] async fn test_hmac_auth_expired_timestamp() { let secret = "test-secret"; - let allowed_methods = vec!["liveness".to_string(), "getConfig".to_string()]; - let layer = - HmacAuthLayer::new(secret.to_string(), DEFAULT_MAX_TIMESTAMP_AGE, allowed_methods); + let layer = HmacAuthLayer::new(secret.to_string(), DEFAULT_MAX_TIMESTAMP_AGE); let mut service = layer.layer(MockService); // Timestamp from 10 minutes ago (expired) @@ -443,31 +412,10 @@ mod tests { assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } - #[tokio::test] - async fn test_hmac_auth_liveness_bypass() { - let secret = "test-secret"; - let allowed_methods = vec!["liveness".to_string()]; - let layer = - HmacAuthLayer::new(secret.to_string(), DEFAULT_MAX_TIMESTAMP_AGE, allowed_methods); - let mut service = layer.layer(MockService); - - let liveness_body = r#"{"jsonrpc":"2.0","method":"liveness","params":[],"id":1}"#; - let request = Request::builder() - .method(Method::POST) - .uri("/") - .body(Body::from(liveness_body)) - .unwrap(); - - let response = service.ready().await.unwrap().call(request).await.unwrap(); - assert_eq!(response.status(), StatusCode::OK); - } - #[tokio::test] async fn test_hmac_auth_malformed_timestamp() { let secret = "test-secret"; - let allowed_methods = vec!["liveness".to_string(), "getConfig".to_string()]; - let layer = - HmacAuthLayer::new(secret.to_string(), DEFAULT_MAX_TIMESTAMP_AGE, allowed_methods); + let layer = HmacAuthLayer::new(secret.to_string(), DEFAULT_MAX_TIMESTAMP_AGE); let mut service = layer.layer(MockService); let body = r#"{"jsonrpc":"2.0","method":"getConfig","id":1}"#; @@ -485,102 +433,19 @@ mod tests { } #[tokio::test] - async fn test_api_key_auth_unknown_method_rejected() { - let allowed_methods = vec!["liveness".to_string(), "getConfig".to_string()]; - let layer = ApiKeyAuthLayer::new("test-key".to_string(), allowed_methods); - let mut service = layer.layer(MockService); - let body = r#"{"jsonrpc":"2.0","method":"unknownMethod","id":1}"#; - let request = Request::builder() - .uri("/test") - .header(X_API_KEY, "test-key") - .body(Body::from(body)) - .unwrap(); - - let response = service.ready().await.unwrap().call(request).await.unwrap(); - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); - } - - #[tokio::test] - async fn test_api_key_auth_disabled_method_rejected() { - // Only allow liveness, not getConfig - let allowed_methods = vec!["liveness".to_string()]; - let layer = ApiKeyAuthLayer::new("test-key".to_string(), allowed_methods); - let mut service = layer.layer(MockService); - let body = r#"{"jsonrpc":"2.0","method":"getConfig","id":1}"#; - let request = Request::builder() - .uri("/test") - .header(X_API_KEY, "test-key") - .body(Body::from(body)) - .unwrap(); - - let response = service.ready().await.unwrap().call(request).await.unwrap(); - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); - } - - #[tokio::test] - async fn test_hmac_auth_unknown_method_rejected() { - let secret = "test-secret"; - let allowed_methods = vec!["liveness".to_string(), "getConfig".to_string()]; - let layer = - HmacAuthLayer::new(secret.to_string(), DEFAULT_MAX_TIMESTAMP_AGE, allowed_methods); - let mut service = layer.layer(MockService); - - let timestamp = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs() - .to_string(); - - let body = r#"{"jsonrpc":"2.0","method":"unknownMethod","id":1}"#; - let message = format!("{timestamp}{body}"); - - let mut mac = Hmac::::new_from_slice(secret.as_bytes()).unwrap(); - mac.update(message.as_bytes()); - let signature = hex::encode(mac.finalize().into_bytes()); - - let request = Request::builder() - .method(Method::POST) - .uri("/test") - .header(X_TIMESTAMP, ×tamp) - .header(X_HMAC_SIGNATURE, &signature) - .body(Body::from(body)) - .unwrap(); - - let response = service.ready().await.unwrap().call(request).await.unwrap(); - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); - } - - #[tokio::test] - async fn test_hmac_auth_disabled_method_rejected() { + async fn test_hmac_auth_liveness_bypass() { let secret = "test-secret"; - // Only allow liveness, not getConfig - let allowed_methods = vec!["liveness".to_string()]; - let layer = - HmacAuthLayer::new(secret.to_string(), DEFAULT_MAX_TIMESTAMP_AGE, allowed_methods); + let layer = HmacAuthLayer::new(secret.to_string(), DEFAULT_MAX_TIMESTAMP_AGE); let mut service = layer.layer(MockService); - let timestamp = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs() - .to_string(); - - let body = r#"{"jsonrpc":"2.0","method":"getConfig","id":1}"#; - let message = format!("{timestamp}{body}"); - - let mut mac = Hmac::::new_from_slice(secret.as_bytes()).unwrap(); - mac.update(message.as_bytes()); - let signature = hex::encode(mac.finalize().into_bytes()); - + let liveness_body = r#"{"jsonrpc":"2.0","method":"liveness","params":[],"id":1}"#; let request = Request::builder() .method(Method::POST) - .uri("/test") - .header(X_TIMESTAMP, ×tamp) - .header(X_HMAC_SIGNATURE, &signature) - .body(Body::from(body)) + .uri("/") + .body(Body::from(liveness_body)) .unwrap(); let response = service.ready().await.unwrap().call(request).await.unwrap(); - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + assert_eq!(response.status(), StatusCode::OK); } } diff --git a/crates/lib/src/rpc_server/middleware_utils.rs b/crates/lib/src/rpc_server/middleware_utils.rs index 36dfc42d..bc2420ae 100644 --- a/crates/lib/src/rpc_server/middleware_utils.rs +++ b/crates/lib/src/rpc_server/middleware_utils.rs @@ -1,7 +1,7 @@ use std::collections::HashSet; use futures_util::TryStreamExt; -use http::Request; +use http::{Request, Response, StatusCode}; use jsonrpsee::server::logger::Body; use crate::KoraError; @@ -43,3 +43,169 @@ pub fn verify_jsonrpc_method( } Err(KoraError::InvalidRequest("Method not allowed".to_string())) } + +/// Method validation layer - applies first in middleware stack to fail fast +#[derive(Clone)] +pub struct MethodValidationLayer { + allowed_methods: HashSet, +} + +impl MethodValidationLayer { + pub fn new(allowed_methods: Vec) -> Self { + Self { allowed_methods: allowed_methods.into_iter().collect() } + } +} + +#[derive(Clone)] +pub struct MethodValidationService { + inner: S, + allowed_methods: HashSet, +} + +impl tower::Layer for MethodValidationLayer { + type Service = MethodValidationService; + + fn layer(&self, inner: S) -> Self::Service { + MethodValidationService { inner, allowed_methods: self.allowed_methods.clone() } + } +} + +impl tower::Service> for MethodValidationService +where + S: tower::Service, Response = Response> + Clone + Send + 'static, + S::Future: Send + 'static, +{ + type Response = S::Response; + type Error = S::Error; + type Future = std::pin::Pin< + Box> + Send>, + >; + + fn poll_ready( + &mut self, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, request: Request) -> Self::Future { + let allowed_methods = self.allowed_methods.clone(); + let mut inner = self.inner.clone(); + + Box::pin(async move { + let (parts, body_bytes) = extract_parts_and_body_bytes(request).await; + + match verify_jsonrpc_method(&body_bytes, &allowed_methods) { + Ok(_) => {} + Err(_) => { + // Method not allowed + let method_not_allowed_response = Response::builder() + .status(StatusCode::METHOD_NOT_ALLOWED) + .body(Body::empty()) + .expect("Failed to build METHOD_NOT_ALLOWED response"); + return Ok(method_not_allowed_response); + } + } + + let new_body = Body::from(body_bytes); + let new_request = Request::from_parts(parts, new_body); + inner.call(new_request).await + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use http::Method; + use std::{ + future::Ready, + task::{Context, Poll}, + }; + use tower::{Layer, Service, ServiceExt}; + + // Mock service that always returns OK + #[derive(Clone)] + struct MockService; + + impl tower::Service> for MockService { + type Response = Response; + type Error = std::convert::Infallible; + type Future = Ready>; + + fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, _: Request) -> Self::Future { + std::future::ready(Ok(Response::builder().status(200).body(Body::empty()).unwrap())) + } + } + + #[tokio::test] + async fn test_method_validation_disallowed_method() { + let allowed_methods = vec!["liveness".to_string(), "getConfig".to_string()]; + let layer = MethodValidationLayer::new(allowed_methods); + let mut service = layer.layer(MockService); + + let body = r#"{"jsonrpc":"2.0","method":"unknownMethod","id":1}"#; + let request = + Request::builder().method(Method::POST).uri("/test").body(Body::from(body)).unwrap(); + + let response = service.ready().await.unwrap().call(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED); + } + + #[tokio::test] + async fn test_method_validation_malformed_json() { + let allowed_methods = vec!["liveness".to_string(), "getConfig".to_string()]; + let layer = MethodValidationLayer::new(allowed_methods); + let mut service = layer.layer(MockService); + + let body = r#"{"invalid json"#; + let request = + Request::builder().method(Method::POST).uri("/test").body(Body::from(body)).unwrap(); + + let response = service.ready().await.unwrap().call(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED); + } + + #[tokio::test] + async fn test_method_validation_missing_method_field() { + let allowed_methods = vec!["liveness".to_string(), "getConfig".to_string()]; + let layer = MethodValidationLayer::new(allowed_methods); + let mut service = layer.layer(MockService); + + let body = r#"{"jsonrpc":"2.0","id":1}"#; + let request = + Request::builder().method(Method::POST).uri("/test").body(Body::from(body)).unwrap(); + + let response = service.ready().await.unwrap().call(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED); + } + + #[tokio::test] + async fn test_method_validation_multiple_allowed_methods() { + let allowed_methods = vec![ + "liveness".to_string(), + "getConfig".to_string(), + "signTransaction".to_string(), + "estimateTransactionFee".to_string(), + ]; + let layer = MethodValidationLayer::new(allowed_methods); + let mut service = layer.layer(MockService); + + // Test each allowed method + for method in &["liveness", "getConfig", "signTransaction", "estimateTransactionFee"] { + let body = format!(r#"{{"jsonrpc":"2.0","method":"{}","id":1}}"#, method); + let request = Request::builder() + .method(Method::POST) + .uri("/test") + .body(Body::from(body)) + .unwrap(); + + let response = service.ready().await.unwrap().call(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::OK, "Method {} should be allowed", method); + } + } +} diff --git a/crates/lib/src/rpc_server/server.rs b/crates/lib/src/rpc_server/server.rs index 5e1b4511..64740ff1 100644 --- a/crates/lib/src/rpc_server/server.rs +++ b/crates/lib/src/rpc_server/server.rs @@ -3,6 +3,7 @@ use crate::{ metrics::run_metrics_server_if_required, rpc_server::{ auth::{ApiKeyAuthLayer, HmacAuthLayer}, + middleware_utils::MethodValidationLayer, rpc::KoraRpc, }, usage_limit::UsageTracker, @@ -76,24 +77,19 @@ pub async fn run_rpc_server(rpc: KoraRpc, port: u16) -> Result("liveness", rpc_params![]).await; assert!(result.is_err()); - assert!(result.err().unwrap().to_string().contains("Method not found")); + let error_msg = result.err().unwrap().to_string(); + // The error should be HTTP 405 (caught by MethodValidationLayer middleware) + assert!(error_msg.contains("405"), "Expected 405 METHOD_NOT_ALLOWED, got: {}", error_msg); } From 8a46b975787125944cad1985ad32cd3e426531c6 Mon Sep 17 00:00:00 2001 From: jo <17280917+dev-jodee@users.noreply.github.com> Date: Tue, 21 Oct 2025 09:14:59 -0400 Subject: [PATCH 17/29] feat: (PRO-414) Extend fee payer policy to include additional SPL and Token2022 instructions and System instructions (#236) * feat: (PRO-414) Add rest of system instructions and spl transfer instructions for fee payer policy * feat: (PRO-414) Extend fee payer policy to include additional SPL and Token2022 instructions and System instructions - Added support for SPL token operations: revoke, set authority, mint to, initialize mint, initialize account, initialize multisig, freeze account, and thaw account. - Updated fee payer policy configuration in `kora.toml` and related files to reflect new permissions. - Enhanced validation logic to enforce these new policies during transaction processing. - Comprehensive tests added to ensure correct behavior for new fee payer policy features. --- crates/lib/src/config.rs | 150 ++- crates/lib/src/constant.rs | 96 ++ .../lib/src/rpc_server/method/get_config.rs | 41 +- crates/lib/src/tests/config_mock.rs | 150 ++- .../lib/src/transaction/instruction_util.rs | 911 ++++++++++++++++-- crates/lib/src/validator/macros.rs | 59 ++ crates/lib/src/validator/mod.rs | 2 + .../src/validator/transaction_validator.rs | 495 +++++++--- docs/getting-started/demo/server/kora.toml | 39 +- docs/operators/deploy/sample/kora.toml | 47 +- docs/x402/demo/kora/kora.toml | 39 +- kora.toml | 47 +- sdks/ts/src/types/index.ts | 92 +- sdks/ts/test/integration.test.ts | 43 +- sdks/ts/test/unit.test.ts | 82 +- .../fee_payer_policy_violations.rs | 598 ++++++++++-- tests/rpc/basic_endpoints.rs | 83 +- tests/src/common/constants.rs | 7 + tests/src/common/fixtures/auth-test.toml | 39 +- .../fixtures/fee-payer-policy-test.toml | 43 +- tests/src/common/fixtures/kora-test.toml | 39 +- .../fixtures/paymaster-address-test.toml | 39 +- tests/src/common/helpers.rs | 30 + .../fee-payer-policy-mint-local-2022.json | 1 + .../fee-payer-policy-mint-local.json | 1 + tests/src/common/setup.rs | 379 +++++++- tests/src/common/transaction.rs | 207 ++++ tests/src/test_runner/accounts.rs | 65 ++ 28 files changed, 3466 insertions(+), 358 deletions(-) create mode 100644 crates/lib/src/validator/macros.rs create mode 100644 tests/src/common/local-keys/fee-payer-policy-mint-local-2022.json create mode 100644 tests/src/common/local-keys/fee-payer-policy-mint-local.json diff --git a/crates/lib/src/config.rs b/crates/lib/src/config.rs index 6e8534d8..ac538def 100644 --- a/crates/lib/src/config.rs +++ b/crates/lib/src/config.rs @@ -138,13 +138,157 @@ impl ValidationConfig { #[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Default)] pub struct FeePayerPolicy { - pub allow_sol_transfers: bool, - pub allow_spl_transfers: bool, - pub allow_token2022_transfers: bool, + #[serde(default)] + pub system: SystemInstructionPolicy, + #[serde(default)] + pub spl_token: SplTokenInstructionPolicy, + #[serde(default)] + pub token_2022: Token2022InstructionPolicy, +} + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct SystemInstructionPolicy { + /// Allow fee payer to be the sender in System Transfer/TransferWithSeed instructions + pub allow_transfer: bool, + /// Allow fee payer to be the authority in System Assign/AssignWithSeed instructions pub allow_assign: bool, + /// Allow fee payer to be the payer in System CreateAccount/CreateAccountWithSeed instructions + pub allow_create_account: bool, + /// Allow fee payer to be the account in System Allocate/AllocateWithSeed instructions + pub allow_allocate: bool, + /// Nested policy for nonce account operations + #[serde(default)] + pub nonce: NonceInstructionPolicy, +} + +impl Default for SystemInstructionPolicy { + fn default() -> Self { + Self { + allow_transfer: true, + allow_assign: true, + allow_create_account: true, + allow_allocate: true, + nonce: NonceInstructionPolicy::default(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct NonceInstructionPolicy { + /// Allow fee payer to be set as the nonce authority in InitializeNonceAccount instructions + pub allow_initialize: bool, + /// Allow fee payer to be the nonce authority in AdvanceNonceAccount instructions + pub allow_advance: bool, + /// Allow fee payer to be the nonce authority in WithdrawNonceAccount instructions + pub allow_withdraw: bool, + /// Allow fee payer to be the current nonce authority in AuthorizeNonceAccount instructions + pub allow_authorize: bool, + // Note: UpgradeNonceAccount not included - has no authority parameter, cannot validate fee payer involvement +} + +impl Default for NonceInstructionPolicy { + fn default() -> Self { + Self { + allow_initialize: true, + allow_advance: true, + allow_withdraw: true, + allow_authorize: true, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct SplTokenInstructionPolicy { + /// Allow fee payer to be the owner in SPL Token Transfer/TransferChecked instructions + pub allow_transfer: bool, + /// Allow fee payer to be the owner in SPL Token Burn/BurnChecked instructions + pub allow_burn: bool, + /// Allow fee payer to be the owner in SPL Token CloseAccount instructions + pub allow_close_account: bool, + /// Allow fee payer to be the owner in SPL Token Approve/ApproveChecked instructions + pub allow_approve: bool, + /// Allow fee payer to be the owner in SPL Token Revoke instructions + pub allow_revoke: bool, + /// Allow fee payer to be the current authority in SPL Token SetAuthority instructions + pub allow_set_authority: bool, + /// Allow fee payer to be the mint authority in SPL Token MintTo/MintToChecked instructions + pub allow_mint_to: bool, + /// Allow fee payer to be the mint authority in SPL Token InitializeMint/InitializeMint2 instructions + pub allow_initialize_mint: bool, + /// Allow fee payer to be set as the owner in SPL Token InitializeAccount instructions + pub allow_initialize_account: bool, + /// Allow fee payer to be a signer in SPL Token InitializeMultisig instructions + pub allow_initialize_multisig: bool, + /// Allow fee payer to be the freeze authority in SPL Token FreezeAccount instructions + pub allow_freeze_account: bool, + /// Allow fee payer to be the freeze authority in SPL Token ThawAccount instructions + pub allow_thaw_account: bool, +} + +impl Default for SplTokenInstructionPolicy { + fn default() -> Self { + Self { + allow_transfer: true, + allow_burn: true, + allow_close_account: true, + allow_approve: true, + allow_revoke: true, + allow_set_authority: true, + allow_mint_to: true, + allow_initialize_mint: true, + allow_initialize_account: true, + allow_initialize_multisig: true, + allow_freeze_account: true, + allow_thaw_account: true, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct Token2022InstructionPolicy { + /// Allow fee payer to be the owner in Token2022 Transfer/TransferChecked instructions + pub allow_transfer: bool, + /// Allow fee payer to be the owner in Token2022 Burn/BurnChecked instructions pub allow_burn: bool, + /// Allow fee payer to be the owner in Token2022 CloseAccount instructions pub allow_close_account: bool, + /// Allow fee payer to be the owner in Token2022 Approve/ApproveChecked instructions pub allow_approve: bool, + /// Allow fee payer to be the owner in Token2022 Revoke instructions + pub allow_revoke: bool, + /// Allow fee payer to be the current authority in Token2022 SetAuthority instructions + pub allow_set_authority: bool, + /// Allow fee payer to be the mint authority in Token2022 MintTo/MintToChecked instructions + pub allow_mint_to: bool, + /// Allow fee payer to be the mint authority in Token2022 InitializeMint/InitializeMint2 instructions + pub allow_initialize_mint: bool, + /// Allow fee payer to be set as the owner in Token2022 InitializeAccount instructions + pub allow_initialize_account: bool, + /// Allow fee payer to be a signer in Token2022 InitializeMultisig instructions + pub allow_initialize_multisig: bool, + /// Allow fee payer to be the freeze authority in Token2022 FreezeAccount instructions + pub allow_freeze_account: bool, + /// Allow fee payer to be the freeze authority in Token2022 ThawAccount instructions + pub allow_thaw_account: bool, +} + +impl Default for Token2022InstructionPolicy { + fn default() -> Self { + Self { + allow_transfer: true, + allow_burn: true, + allow_close_account: true, + allow_approve: true, + allow_revoke: true, + allow_set_authority: true, + allow_mint_to: true, + allow_initialize_mint: true, + allow_initialize_account: true, + allow_initialize_multisig: true, + allow_freeze_account: true, + allow_thaw_account: true, + } + } } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] diff --git a/crates/lib/src/constant.rs b/crates/lib/src/constant.rs index a2267dff..9b2f4543 100644 --- a/crates/lib/src/constant.rs +++ b/crates/lib/src/constant.rs @@ -67,6 +67,36 @@ pub mod instruction_indexes { pub const AUTHORITY_INDEX: usize = 1; } + pub mod system_allocate { + pub const REQUIRED_NUMBER_OF_ACCOUNTS: usize = 1; + pub const ACCOUNT_INDEX: usize = 0; + } + + pub mod system_allocate_with_seed { + pub const REQUIRED_NUMBER_OF_ACCOUNTS: usize = 2; + pub const ACCOUNT_INDEX: usize = 1; + } + + pub mod system_initialize_nonce_account { + pub const REQUIRED_NUMBER_OF_ACCOUNTS: usize = 3; + pub const NONCE_ACCOUNT_INDEX: usize = 0; + // Authority is in instruction data, not accounts + } + + pub mod system_advance_nonce_account { + pub const REQUIRED_NUMBER_OF_ACCOUNTS: usize = 3; + pub const NONCE_ACCOUNT_INDEX: usize = 0; + pub const NONCE_AUTHORITY_INDEX: usize = 2; + } + + pub mod system_authorize_nonce_account { + pub const REQUIRED_NUMBER_OF_ACCOUNTS: usize = 2; + pub const NONCE_ACCOUNT_INDEX: usize = 0; + pub const NONCE_AUTHORITY_INDEX: usize = 1; + } + + // Note: system_upgrade_nonce_account not included - no authority parameter, cannot validate + pub mod spl_token_transfer { pub const REQUIRED_NUMBER_OF_ACCOUNTS: usize = 3; pub const OWNER_INDEX: usize = 2; @@ -101,4 +131,70 @@ pub mod instruction_indexes { pub const REQUIRED_NUMBER_OF_ACCOUNTS: usize = 4; pub const OWNER_INDEX: usize = 3; } + + pub mod spl_token_revoke { + pub const REQUIRED_NUMBER_OF_ACCOUNTS: usize = 2; + pub const OWNER_INDEX: usize = 1; + } + + pub mod spl_token_set_authority { + pub const REQUIRED_NUMBER_OF_ACCOUNTS: usize = 2; + pub const CURRENT_AUTHORITY_INDEX: usize = 1; + } + + pub mod spl_token_mint_to { + pub const REQUIRED_NUMBER_OF_ACCOUNTS: usize = 3; + pub const MINT_AUTHORITY_INDEX: usize = 2; + } + + pub mod spl_token_mint_to_checked { + pub const REQUIRED_NUMBER_OF_ACCOUNTS: usize = 3; + pub const MINT_AUTHORITY_INDEX: usize = 2; + } + + pub mod spl_token_initialize_mint { + pub const REQUIRED_NUMBER_OF_ACCOUNTS: usize = 2; + // Authority is in instruction data, not accounts + } + + pub mod spl_token_initialize_mint2 { + pub const REQUIRED_NUMBER_OF_ACCOUNTS: usize = 1; + // Authority is in instruction data, not accounts + } + + pub mod spl_token_initialize_account { + pub const REQUIRED_NUMBER_OF_ACCOUNTS: usize = 4; + // Owner is in account data at index 2 + pub const OWNER_INDEX: usize = 2; + } + + pub mod spl_token_initialize_account2 { + pub const REQUIRED_NUMBER_OF_ACCOUNTS: usize = 3; + // Owner is in instruction data, not accounts + } + + pub mod spl_token_initialize_account3 { + pub const REQUIRED_NUMBER_OF_ACCOUNTS: usize = 2; + // Owner is in instruction data, not accounts + } + + pub mod spl_token_initialize_multisig { + pub const REQUIRED_NUMBER_OF_ACCOUNTS: usize = 2; // Minimum + // Signers are accounts from index 2 onwards (after multisig account and rent sysvar) + } + + pub mod spl_token_initialize_multisig2 { + pub const REQUIRED_NUMBER_OF_ACCOUNTS: usize = 1; // Minimum + // Signers are accounts from index 1 onwards (after multisig account) + } + + pub mod spl_token_freeze_account { + pub const REQUIRED_NUMBER_OF_ACCOUNTS: usize = 3; + pub const FREEZE_AUTHORITY_INDEX: usize = 2; + } + + pub mod spl_token_thaw_account { + pub const REQUIRED_NUMBER_OF_ACCOUNTS: usize = 3; + pub const FREEZE_AUTHORITY_INDEX: usize = 2; + } } diff --git a/crates/lib/src/rpc_server/method/get_config.rs b/crates/lib/src/rpc_server/method/get_config.rs index 563778ee..1093838b 100644 --- a/crates/lib/src/rpc_server/method/get_config.rs +++ b/crates/lib/src/rpc_server/method/get_config.rs @@ -88,15 +88,38 @@ mod tests { assert_eq!(response.validation_config.disallowed_accounts.len(), 0); assert_eq!(response.validation_config.price_source, crate::oracle::PriceSource::Mock); - // Assert FeePayerPolicy defaults - assert!(!response.validation_config.fee_payer_policy.allow_sol_transfers); - assert!(!response.validation_config.fee_payer_policy.allow_spl_transfers); - assert!(!response.validation_config.fee_payer_policy.allow_token2022_transfers); - assert!(!response.validation_config.fee_payer_policy.allow_assign); - assert!(!response.validation_config.fee_payer_policy.allow_burn); - assert!(!response.validation_config.fee_payer_policy.allow_close_account); - assert!(!response.validation_config.fee_payer_policy.allow_approve); - + // Assert FeePayerPolicy defaults - System + assert!(response.validation_config.fee_payer_policy.system.allow_transfer); + assert!(response.validation_config.fee_payer_policy.system.allow_assign); + assert!(response.validation_config.fee_payer_policy.system.allow_create_account); + assert!(response.validation_config.fee_payer_policy.system.allow_allocate); + assert!(response.validation_config.fee_payer_policy.system.nonce.allow_initialize); + assert!(response.validation_config.fee_payer_policy.system.nonce.allow_advance); + assert!(response.validation_config.fee_payer_policy.system.nonce.allow_withdraw); + assert!(response.validation_config.fee_payer_policy.system.nonce.allow_authorize); + // Note: allow_upgrade removed - no authority parameter to validate + + // Assert FeePayerPolicy defaults - SPL Token + assert!(response.validation_config.fee_payer_policy.spl_token.allow_transfer); + assert!(response.validation_config.fee_payer_policy.spl_token.allow_burn); + assert!(response.validation_config.fee_payer_policy.spl_token.allow_close_account); + assert!(response.validation_config.fee_payer_policy.spl_token.allow_approve); + assert!(response.validation_config.fee_payer_policy.spl_token.allow_revoke); + assert!(response.validation_config.fee_payer_policy.spl_token.allow_set_authority); + assert!(response.validation_config.fee_payer_policy.spl_token.allow_mint_to); + assert!(response.validation_config.fee_payer_policy.spl_token.allow_freeze_account); + assert!(response.validation_config.fee_payer_policy.spl_token.allow_thaw_account); + + // Assert FeePayerPolicy defaults - Token2022 + assert!(response.validation_config.fee_payer_policy.token_2022.allow_transfer); + assert!(response.validation_config.fee_payer_policy.token_2022.allow_burn); + assert!(response.validation_config.fee_payer_policy.token_2022.allow_close_account); + assert!(response.validation_config.fee_payer_policy.token_2022.allow_approve); + assert!(response.validation_config.fee_payer_policy.token_2022.allow_revoke); + assert!(response.validation_config.fee_payer_policy.token_2022.allow_set_authority); + assert!(response.validation_config.fee_payer_policy.token_2022.allow_mint_to); + assert!(response.validation_config.fee_payer_policy.token_2022.allow_freeze_account); + assert!(response.validation_config.fee_payer_policy.token_2022.allow_thaw_account); // Assert PriceConfig default (check margin value) match response.validation_config.price.model { crate::fee::price::PriceModel::Margin { margin } => assert_eq!(margin, 0.0), diff --git a/crates/lib/src/tests/config_mock.rs b/crates/lib/src/tests/config_mock.rs index 9c2e3c9b..2ecd0051 100644 --- a/crates/lib/src/tests/config_mock.rs +++ b/crates/lib/src/tests/config_mock.rs @@ -1,8 +1,9 @@ use crate::{ config::{ AuthConfig, CacheConfig, Config, EnabledMethods, FeePayerBalanceMetricsConfig, - FeePayerPolicy, KoraConfig, MetricsConfig, SplTokenConfig, Token2022Config, - UsageLimitConfig, ValidationConfig, + FeePayerPolicy, KoraConfig, MetricsConfig, NonceInstructionPolicy, SplTokenConfig, + SplTokenInstructionPolicy, SystemInstructionPolicy, Token2022Config, + Token2022InstructionPolicy, UsageLimitConfig, ValidationConfig, }, constant::DEFAULT_MAX_REQUEST_BODY_SIZE, fee::price::PriceConfig, @@ -473,51 +474,152 @@ impl FeePayerPolicyBuilder { } pub fn with_sol_transfers(mut self, allow: bool) -> Self { - self.config.allow_sol_transfers = allow; + self.config.system.allow_transfer = allow; self } pub fn with_spl_transfers(mut self, allow: bool) -> Self { - self.config.allow_spl_transfers = allow; + self.config.spl_token.allow_transfer = allow; self } pub fn with_token2022_transfers(mut self, allow: bool) -> Self { - self.config.allow_token2022_transfers = allow; + self.config.token_2022.allow_transfer = allow; self } pub fn with_assign(mut self, allow: bool) -> Self { - self.config.allow_assign = allow; + self.config.system.allow_assign = allow; + self + } + + pub fn with_create_account(mut self, allow: bool) -> Self { + self.config.system.allow_create_account = allow; + self + } + + pub fn with_allocate(mut self, allow: bool) -> Self { + self.config.system.allow_allocate = allow; + self + } + + pub fn with_nonce_initialize(mut self, allow: bool) -> Self { + self.config.system.nonce.allow_initialize = allow; + self + } + + pub fn with_nonce_advance(mut self, allow: bool) -> Self { + self.config.system.nonce.allow_advance = allow; + self + } + + pub fn with_nonce_withdraw(mut self, allow: bool) -> Self { + self.config.system.nonce.allow_withdraw = allow; + self + } + + pub fn with_nonce_authorize(mut self, allow: bool) -> Self { + self.config.system.nonce.allow_authorize = allow; + self + } + + pub fn with_spl_burn(mut self, allow: bool) -> Self { + self.config.spl_token.allow_burn = allow; + self.config.token_2022.allow_burn = allow; + self + } + + pub fn with_spl_close_account(mut self, allow: bool) -> Self { + self.config.spl_token.allow_close_account = allow; + self.config.token_2022.allow_close_account = allow; + self + } + + pub fn with_spl_approve(mut self, allow: bool) -> Self { + self.config.spl_token.allow_approve = allow; + self.config.token_2022.allow_approve = allow; + self + } + + pub fn with_spl_revoke(mut self, allow: bool) -> Self { + self.config.spl_token.allow_revoke = allow; + self.config.token_2022.allow_revoke = allow; + self + } + + pub fn with_spl_set_authority(mut self, allow: bool) -> Self { + self.config.spl_token.allow_set_authority = allow; + self.config.token_2022.allow_set_authority = allow; + self + } + + pub fn with_spl_mint_to(mut self, allow: bool) -> Self { + self.config.spl_token.allow_mint_to = allow; + self.config.token_2022.allow_mint_to = allow; + self + } + + pub fn with_spl_freeze_account(mut self, allow: bool) -> Self { + self.config.spl_token.allow_freeze_account = allow; + self.config.token_2022.allow_freeze_account = allow; + self + } + + pub fn with_spl_thaw_account(mut self, allow: bool) -> Self { + self.config.spl_token.allow_thaw_account = allow; + self.config.token_2022.allow_thaw_account = allow; self } pub fn restrictive() -> Self { Self { config: FeePayerPolicy { - allow_sol_transfers: false, - allow_spl_transfers: false, - allow_token2022_transfers: false, - allow_assign: false, - allow_burn: false, - allow_close_account: false, - allow_approve: false, + system: SystemInstructionPolicy { + allow_transfer: false, + allow_assign: false, + allow_create_account: false, + allow_allocate: false, + nonce: NonceInstructionPolicy { + allow_initialize: false, + allow_advance: false, + allow_withdraw: false, + allow_authorize: false, + }, + }, + spl_token: SplTokenInstructionPolicy { + allow_transfer: false, + allow_burn: false, + allow_close_account: false, + allow_approve: false, + allow_revoke: false, + allow_set_authority: false, + allow_mint_to: false, + allow_freeze_account: false, + allow_thaw_account: false, + allow_initialize_mint: false, + allow_initialize_account: false, + allow_initialize_multisig: false, + }, + token_2022: Token2022InstructionPolicy { + allow_transfer: false, + allow_burn: false, + allow_close_account: false, + allow_approve: false, + allow_revoke: false, + allow_set_authority: false, + allow_mint_to: false, + allow_freeze_account: false, + allow_thaw_account: false, + allow_initialize_mint: false, + allow_initialize_account: false, + allow_initialize_multisig: false, + }, }, } } pub fn permissive() -> Self { - Self { - config: FeePayerPolicy { - allow_sol_transfers: true, - allow_spl_transfers: true, - allow_token2022_transfers: true, - allow_assign: true, - allow_burn: true, - allow_close_account: true, - allow_approve: true, - }, - } + Self { config: FeePayerPolicy::default() } } } diff --git a/crates/lib/src/transaction/instruction_util.rs b/crates/lib/src/transaction/instruction_util.rs index 445a5099..b79d98a4 100644 --- a/crates/lib/src/transaction/instruction_util.rs +++ b/crates/lib/src/transaction/instruction_util.rs @@ -19,6 +19,11 @@ pub enum ParsedSystemInstructionType { SystemCreateAccount, SystemWithdrawNonceAccount, SystemAssign, + SystemAllocate, + SystemInitializeNonceAccount, + SystemAdvanceNonceAccount, + SystemAuthorizeNonceAccount, + // Note: SystemUpgradeNonceAccount not included - no authority parameter } // Instruction type that we support to parse from the transaction @@ -32,6 +37,15 @@ pub enum ParsedSystemInstructionData { SystemWithdrawNonceAccount { lamports: u64, nonce_authority: Pubkey, recipient: Pubkey }, // Includes assign and assign with seed SystemAssign { authority: Pubkey }, + // Includes allocate and allocate with seed + SystemAllocate { account: Pubkey }, + // Initialize nonce account + SystemInitializeNonceAccount { nonce_account: Pubkey, nonce_authority: Pubkey }, + // Advance nonce account + SystemAdvanceNonceAccount { nonce_account: Pubkey, nonce_authority: Pubkey }, + // Authorize nonce account + SystemAuthorizeNonceAccount { nonce_account: Pubkey, nonce_authority: Pubkey }, + // Note: SystemUpgradeNonceAccount not included - no authority parameter } #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -40,6 +54,14 @@ pub enum ParsedSPLInstructionType { SplTokenBurn, SplTokenCloseAccount, SplTokenApprove, + SplTokenRevoke, + SplTokenSetAuthority, + SplTokenMintTo, + SplTokenInitializeMint, + SplTokenInitializeAccount, + SplTokenInitializeMultisig, + SplTokenFreezeAccount, + SplTokenThawAccount, } #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -68,6 +90,46 @@ pub enum ParsedSPLInstructionData { owner: Pubkey, is_2022: bool, }, + // Revoke + SplTokenRevoke { + owner: Pubkey, + is_2022: bool, + }, + // SetAuthority + SplTokenSetAuthority { + authority: Pubkey, + is_2022: bool, + }, + // MintTo and MintToChecked + SplTokenMintTo { + mint_authority: Pubkey, + is_2022: bool, + }, + // InitializeMint and InitializeMint2 + SplTokenInitializeMint { + mint_authority: Pubkey, + is_2022: bool, + }, + // InitializeAccount, InitializeAccount2, InitializeAccount3 + SplTokenInitializeAccount { + owner: Pubkey, + is_2022: bool, + }, + // InitializeMultisig and InitializeMultisig2 + SplTokenInitializeMultisig { + signers: Vec, + is_2022: bool, + }, + // FreezeAccount + SplTokenFreezeAccount { + freeze_authority: Pubkey, + is_2022: bool, + }, + // ThawAccount + SplTokenThawAccount { + freeze_authority: Pubkey, + is_2022: bool, + }, } /// Macro to validate that an instruction has the required number of accounts @@ -83,6 +145,32 @@ macro_rules! validate_number_accounts { }; } +/// Macro to parse system instructions with validation and account extraction +/// Usage: parse_system_instruction!(parsed_instructions, instruction, validate_module, EnumVariant, DataVariant { fields }) +macro_rules! parse_system_instruction { + // Simple version: separate constant module path and enum variant names + ($parsed:ident, $ix:ident, $const_mod:ident, $enum_variant:ident, $data_variant:ident { $($field:ident: $account_path:expr),* $(,)? }) => { + validate_number_accounts!($ix, instruction_indexes::$const_mod::REQUIRED_NUMBER_OF_ACCOUNTS); + $parsed + .entry(ParsedSystemInstructionType::$enum_variant) + .or_default() + .push(ParsedSystemInstructionData::$data_variant { + $($field: $ix.accounts[$account_path].pubkey,)* + }); + }; + // Version with extra fields (like lamports) that come from instruction data + ($parsed:ident, $ix:ident, $const_mod:ident, $enum_variant:ident, $data_variant:ident { $($data_field:ident: $data_val:expr),* ; $($field:ident: $account_path:expr),* $(,)? }) => { + validate_number_accounts!($ix, instruction_indexes::$const_mod::REQUIRED_NUMBER_OF_ACCOUNTS); + $parsed + .entry(ParsedSystemInstructionType::$enum_variant) + .or_default() + .push(ParsedSystemInstructionData::$data_variant { + $($data_field: $data_val,)* + $($field: $ix.accounts[$account_path].pubkey,)* + }); + }; +} + pub struct IxUtils; pub const PARSED_DATA_FIELD_TYPE: &str = "type"; @@ -99,6 +187,11 @@ pub const PARSED_DATA_FIELD_TRANSFER_WITH_SEED: &str = "transferWithSeed"; pub const PARSED_DATA_FIELD_CREATE_ACCOUNT_WITH_SEED: &str = "createAccountWithSeed"; pub const PARSED_DATA_FIELD_ASSIGN_WITH_SEED: &str = "assignWithSeed"; pub const PARSED_DATA_FIELD_WITHDRAW_NONCE_ACCOUNT: &str = "withdrawFromNonce"; +pub const PARSED_DATA_FIELD_ALLOCATE: &str = "allocate"; +pub const PARSED_DATA_FIELD_ALLOCATE_WITH_SEED: &str = "allocateWithSeed"; +pub const PARSED_DATA_FIELD_INITIALIZE_NONCE_ACCOUNT: &str = "initializeNonce"; +pub const PARSED_DATA_FIELD_ADVANCE_NONCE_ACCOUNT: &str = "advanceNonce"; +pub const PARSED_DATA_FIELD_AUTHORIZE_NONCE_ACCOUNT: &str = "authorizeNonce"; pub const PARSED_DATA_FIELD_BURN: &str = "burn"; pub const PARSED_DATA_FIELD_BURN_CHECKED: &str = "burnChecked"; pub const PARSED_DATA_FIELD_CLOSE_ACCOUNT: &str = "closeAccount"; @@ -126,6 +219,29 @@ pub const PARSED_DATA_FIELD_SOURCE_OWNER: &str = "sourceOwner"; pub const PARSED_DATA_FIELD_NONCE_ACCOUNT: &str = "nonceAccount"; pub const PARSED_DATA_FIELD_RECIPIENT: &str = "recipient"; pub const PARSED_DATA_FIELD_NONCE_AUTHORITY: &str = "nonceAuthority"; +pub const PARSED_DATA_FIELD_NEW_AUTHORITY: &str = "newAuthority"; + +// SPL Token instruction type constants +pub const PARSED_DATA_FIELD_REVOKE: &str = "revoke"; +pub const PARSED_DATA_FIELD_SET_AUTHORITY: &str = "setAuthority"; +pub const PARSED_DATA_FIELD_MINT_TO: &str = "mintTo"; +pub const PARSED_DATA_FIELD_MINT_TO_CHECKED: &str = "mintToChecked"; +pub const PARSED_DATA_FIELD_INITIALIZE_MINT: &str = "initializeMint"; +pub const PARSED_DATA_FIELD_INITIALIZE_MINT2: &str = "initializeMint2"; +pub const PARSED_DATA_FIELD_INITIALIZE_ACCOUNT: &str = "initializeAccount"; +pub const PARSED_DATA_FIELD_INITIALIZE_ACCOUNT2: &str = "initializeAccount2"; +pub const PARSED_DATA_FIELD_INITIALIZE_ACCOUNT3: &str = "initializeAccount3"; +pub const PARSED_DATA_FIELD_INITIALIZE_MULTISIG: &str = "initializeMultisig"; +pub const PARSED_DATA_FIELD_INITIALIZE_MULTISIG2: &str = "initializeMultisig2"; +pub const PARSED_DATA_FIELD_FREEZE_ACCOUNT: &str = "freezeAccount"; +pub const PARSED_DATA_FIELD_THAW_ACCOUNT: &str = "thawAccount"; + +// Additional field names for new instructions +pub const PARSED_DATA_FIELD_MINT_AUTHORITY: &str = "mintAuthority"; +pub const PARSED_DATA_FIELD_FREEZE_AUTHORITY: &str = "freezeAuthority"; +pub const PARSED_DATA_FIELD_AUTHORITY_TYPE: &str = "authorityType"; +pub const PARSED_DATA_FIELD_MULTISIG_ACCOUNT: &str = "multisig"; +pub const PARSED_DATA_FIELD_SIGNERS: &str = "signers"; impl IxUtils { /// Helper method to extract a field as a string from JSON with proper error handling @@ -499,6 +615,126 @@ impl IxUtils { data, }) } + PARSED_DATA_FIELD_ALLOCATE => { + let account = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_ACCOUNT)?; + let space = Self::get_field_as_u64(info, PARSED_DATA_FIELD_SPACE)?; + + let account_idx = Self::get_account_index(account_keys_hashmap, &account)?; + + let allocate_ix = SystemInstruction::Allocate { space }; + let data = bincode::serialize(&allocate_ix).map_err(|e| { + KoraError::SerializationError(format!( + "Failed to serialize Allocate instruction: {}", + e + )) + })?; + + Ok(CompiledInstruction { program_id_index, accounts: vec![account_idx], data }) + } + PARSED_DATA_FIELD_ALLOCATE_WITH_SEED => { + let account = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_ACCOUNT)?; + let base = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_BASE)?; + let seed = Self::get_field_as_str(info, PARSED_DATA_FIELD_SEED)?.to_string(); + let space = Self::get_field_as_u64(info, PARSED_DATA_FIELD_SPACE)?; + let owner = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_OWNER)?; + + let account_idx = Self::get_account_index(account_keys_hashmap, &account)?; + let base_idx = Self::get_account_index(account_keys_hashmap, &base)?; + + let allocate_ix = SystemInstruction::AllocateWithSeed { base, seed, space, owner }; + let data = bincode::serialize(&allocate_ix).map_err(|e| { + KoraError::SerializationError(format!( + "Failed to serialize AllocateWithSeed instruction: {}", + e + )) + })?; + + Ok(CompiledInstruction { + program_id_index, + accounts: vec![account_idx, base_idx], + data, + }) + } + PARSED_DATA_FIELD_INITIALIZE_NONCE_ACCOUNT => { + let nonce_account = + Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_NONCE_ACCOUNT)?; + let nonce_authority = + Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_NONCE_AUTHORITY)?; + + let nonce_account_idx = + Self::get_account_index(account_keys_hashmap, &nonce_account)?; + + let initialize_ix = SystemInstruction::InitializeNonceAccount(nonce_authority); + let data = bincode::serialize(&initialize_ix).map_err(|e| { + KoraError::SerializationError(format!( + "Failed to serialize InitializeNonceAccount instruction: {}", + e + )) + })?; + + // Accounts: [nonce_account, recent_blockhashes_sysvar, rent_sysvar] + // We only have nonce_account in the hashmap for inner instructions + Ok(CompiledInstruction { + program_id_index, + accounts: vec![nonce_account_idx], + data, + }) + } + PARSED_DATA_FIELD_ADVANCE_NONCE_ACCOUNT => { + let nonce_account = + Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_NONCE_ACCOUNT)?; + let nonce_authority = + Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_NONCE_AUTHORITY)?; + + let nonce_account_idx = + Self::get_account_index(account_keys_hashmap, &nonce_account)?; + let nonce_authority_idx = + Self::get_account_index(account_keys_hashmap, &nonce_authority)?; + + let advance_ix = SystemInstruction::AdvanceNonceAccount; + let data = bincode::serialize(&advance_ix).map_err(|e| { + KoraError::SerializationError(format!( + "Failed to serialize AdvanceNonceAccount instruction: {}", + e + )) + })?; + + // Accounts: [nonce_account, recent_blockhashes_sysvar, nonce_authority] + // We only include accounts that are in the hashmap (from inner instructions) + Ok(CompiledInstruction { + program_id_index, + accounts: vec![nonce_account_idx, nonce_authority_idx], + data, + }) + } + PARSED_DATA_FIELD_AUTHORIZE_NONCE_ACCOUNT => { + let nonce_account = + Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_NONCE_ACCOUNT)?; + let nonce_authority = + Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_NONCE_AUTHORITY)?; + let new_authority = + Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_NEW_AUTHORITY)?; + + let nonce_account_idx = + Self::get_account_index(account_keys_hashmap, &nonce_account)?; + let nonce_authority_idx = + Self::get_account_index(account_keys_hashmap, &nonce_authority)?; + + let authorize_ix = SystemInstruction::AuthorizeNonceAccount(new_authority); + let data = bincode::serialize(&authorize_ix).map_err(|e| { + KoraError::SerializationError(format!( + "Failed to serialize AuthorizeNonceAccount instruction: {}", + e + )) + })?; + + // Accounts: [nonce_account, nonce_authority] + Ok(CompiledInstruction { + program_id_index, + accounts: vec![nonce_account_idx, nonce_authority_idx], + data, + }) + } _ => { log::error!("Unsupported system instruction type: {}", instruction_type); Ok(Self::build_default_compiled_instruction(program_id_index)) @@ -712,6 +948,215 @@ impl IxUtils { data, }) } + PARSED_DATA_FIELD_REVOKE => { + let source = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_SOURCE)?; + let owner = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_OWNER)?; + + let source_idx = Self::get_account_index(account_keys_hashmap, &source)?; + let owner_idx = Self::get_account_index(account_keys_hashmap, &owner)?; + + let data = if parsed.program_id == spl_token::ID.to_string() { + spl_token::instruction::TokenInstruction::Revoke.pack() + } else { + spl_token_2022::instruction::TokenInstruction::Revoke.pack() + }; + + Ok(CompiledInstruction { + program_id_index, + accounts: vec![source_idx, owner_idx], + data, + }) + } + PARSED_DATA_FIELD_SET_AUTHORITY => { + let account = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_ACCOUNT)?; + let current_authority = + Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_AUTHORITY)?; + + let account_idx = Self::get_account_index(account_keys_hashmap, &account)?; + let current_authority_idx = + Self::get_account_index(account_keys_hashmap, ¤t_authority)?; + + // SetAuthority has variable data - we reconstruct minimal version + // Real validation happens when checking if fee payer is the authority + let data = vec![6]; + + Ok(CompiledInstruction { + program_id_index, + accounts: vec![account_idx, current_authority_idx], + data, + }) + } + PARSED_DATA_FIELD_MINT_TO => { + let mint = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_MINT)?; + let account = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_ACCOUNT)?; + let mint_authority = + Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_MINT_AUTHORITY)?; + let amount = Self::get_field_as_u64(info, PARSED_DATA_FIELD_AMOUNT)?; + + let mint_idx = Self::get_account_index(account_keys_hashmap, &mint)?; + let account_idx = Self::get_account_index(account_keys_hashmap, &account)?; + let mint_authority_idx = + Self::get_account_index(account_keys_hashmap, &mint_authority)?; + + let data = if parsed.program_id == spl_token::ID.to_string() { + spl_token::instruction::TokenInstruction::MintTo { amount }.pack() + } else { + spl_token_2022::instruction::TokenInstruction::MintTo { amount }.pack() + }; + + Ok(CompiledInstruction { + program_id_index, + accounts: vec![mint_idx, account_idx, mint_authority_idx], + data, + }) + } + PARSED_DATA_FIELD_MINT_TO_CHECKED => { + let mint = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_MINT)?; + let account = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_ACCOUNT)?; + let mint_authority = + Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_MINT_AUTHORITY)?; + + let token_amount = info.get(PARSED_DATA_FIELD_TOKEN_AMOUNT).ok_or_else(|| { + KoraError::SerializationError("Missing 'tokenAmount' field".to_string()) + })?; + let amount = Self::get_field_as_u64(token_amount, PARSED_DATA_FIELD_AMOUNT)?; + let decimals = + Self::get_field_as_u64(token_amount, PARSED_DATA_FIELD_DECIMALS)? as u8; + + let mint_idx = Self::get_account_index(account_keys_hashmap, &mint)?; + let account_idx = Self::get_account_index(account_keys_hashmap, &account)?; + let mint_authority_idx = + Self::get_account_index(account_keys_hashmap, &mint_authority)?; + + let data = if parsed.program_id == spl_token::ID.to_string() { + spl_token::instruction::TokenInstruction::MintToChecked { amount, decimals } + .pack() + } else { + spl_token_2022::instruction::TokenInstruction::MintToChecked { + amount, + decimals, + } + .pack() + }; + + Ok(CompiledInstruction { + program_id_index, + accounts: vec![mint_idx, account_idx, mint_authority_idx], + data, + }) + } + PARSED_DATA_FIELD_INITIALIZE_MINT | PARSED_DATA_FIELD_INITIALIZE_MINT2 => { + let mint = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_MINT)?; + // mint_authority is in instruction data, not used for reconstruction + let _mint_authority = + Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_MINT_AUTHORITY)?; + + let mint_idx = Self::get_account_index(account_keys_hashmap, &mint)?; + + // InitializeMint has discriminator only, authority is in data + let data = if instruction_type == PARSED_DATA_FIELD_INITIALIZE_MINT { + vec![0] // InitializeMint discriminator + } else { + vec![20] // InitializeMint2 discriminator + }; + + Ok(CompiledInstruction { program_id_index, accounts: vec![mint_idx], data }) + } + PARSED_DATA_FIELD_INITIALIZE_ACCOUNT + | PARSED_DATA_FIELD_INITIALIZE_ACCOUNT2 + | PARSED_DATA_FIELD_INITIALIZE_ACCOUNT3 => { + let account = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_ACCOUNT)?; + let mint = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_MINT)?; + let owner = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_OWNER)?; + + let account_idx = Self::get_account_index(account_keys_hashmap, &account)?; + let mint_idx = Self::get_account_index(account_keys_hashmap, &mint)?; + + // Different variants have different account structures and discriminators + let (data, accounts) = match instruction_type { + PARSED_DATA_FIELD_INITIALIZE_ACCOUNT => { + // InitializeAccount: [account, mint, owner, rent] + // Owner is in accounts, not data + let owner_idx = Self::get_account_index(account_keys_hashmap, &owner)?; + (vec![1], vec![account_idx, mint_idx, owner_idx]) + } + PARSED_DATA_FIELD_INITIALIZE_ACCOUNT2 => { + // InitializeAccount2: [account, mint, rent], owner in data + (vec![16], vec![account_idx, mint_idx]) + } + PARSED_DATA_FIELD_INITIALIZE_ACCOUNT3 => { + // InitializeAccount3: [account, mint], owner in data + (vec![18], vec![account_idx, mint_idx]) + } + _ => unreachable!(), + }; + + Ok(CompiledInstruction { program_id_index, accounts, data }) + } + PARSED_DATA_FIELD_INITIALIZE_MULTISIG | PARSED_DATA_FIELD_INITIALIZE_MULTISIG2 => { + let multisig = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_MULTISIG_ACCOUNT)?; + let multisig_idx = Self::get_account_index(account_keys_hashmap, &multisig)?; + + // Extract signer pubkeys from signers array (not currently used for reconstruction) + let _signers_value = info.get(PARSED_DATA_FIELD_SIGNERS).ok_or_else(|| { + KoraError::SerializationError("Missing 'signers' field".to_string()) + })?; + + // Discriminator based on instruction variant + let data = if instruction_type == PARSED_DATA_FIELD_INITIALIZE_MULTISIG { + vec![2] // InitializeMultisig discriminator + } else { + vec![19] // InitializeMultisig2 discriminator + }; + + Ok(CompiledInstruction { program_id_index, accounts: vec![multisig_idx], data }) + } + PARSED_DATA_FIELD_FREEZE_ACCOUNT => { + let account = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_ACCOUNT)?; + let mint = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_MINT)?; + let freeze_authority = + Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_FREEZE_AUTHORITY)?; + + let account_idx = Self::get_account_index(account_keys_hashmap, &account)?; + let mint_idx = Self::get_account_index(account_keys_hashmap, &mint)?; + let freeze_authority_idx = + Self::get_account_index(account_keys_hashmap, &freeze_authority)?; + + let data = if parsed.program_id == spl_token::ID.to_string() { + spl_token::instruction::TokenInstruction::FreezeAccount.pack() + } else { + spl_token_2022::instruction::TokenInstruction::FreezeAccount.pack() + }; + + Ok(CompiledInstruction { + program_id_index, + accounts: vec![account_idx, mint_idx, freeze_authority_idx], + data, + }) + } + PARSED_DATA_FIELD_THAW_ACCOUNT => { + let account = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_ACCOUNT)?; + let mint = Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_MINT)?; + let freeze_authority = + Self::get_field_as_pubkey(info, PARSED_DATA_FIELD_FREEZE_AUTHORITY)?; + + let account_idx = Self::get_account_index(account_keys_hashmap, &account)?; + let mint_idx = Self::get_account_index(account_keys_hashmap, &mint)?; + let freeze_authority_idx = + Self::get_account_index(account_keys_hashmap, &freeze_authority)?; + + let data = if parsed.program_id == spl_token::ID.to_string() { + spl_token::instruction::TokenInstruction::ThawAccount.pack() + } else { + spl_token_2022::instruction::TokenInstruction::ThawAccount.pack() + }; + + Ok(CompiledInstruction { + program_id_index, + accounts: vec![account_idx, mint_idx, freeze_authority_idx], + data, + }) + } _ => { log::error!("Unsupported token instruction type: {}", instruction_type); Ok(Self::build_default_compiled_instruction(program_id_index)) @@ -734,49 +1179,23 @@ impl IxUtils { // Handle System Program transfers and account creation if program_id == SYSTEM_PROGRAM_ID { match bincode::deserialize::(&instruction.data) { - // Account creation instructions - funding account pays lamports Ok(SystemInstruction::CreateAccount { lamports, .. }) | Ok(SystemInstruction::CreateAccountWithSeed { lamports, .. }) => { - validate_number_accounts!( - instruction, - instruction_indexes::system_create_account::REQUIRED_NUMBER_OF_ACCOUNTS - ); - - let payer = instruction.accounts - [instruction_indexes::system_create_account::PAYER_INDEX] - .pubkey; - - parsed_instructions - .entry(ParsedSystemInstructionType::SystemCreateAccount) - .or_default() - .push(ParsedSystemInstructionData::SystemCreateAccount { - lamports, - payer, - }); + parse_system_instruction!(parsed_instructions, instruction, system_create_account, SystemCreateAccount, SystemCreateAccount { + lamports: lamports; + payer: instruction_indexes::system_create_account::PAYER_INDEX + }); } - // Transfer instructions Ok(SystemInstruction::Transfer { lamports }) => { - validate_number_accounts!( - instruction, - instruction_indexes::system_transfer::REQUIRED_NUMBER_OF_ACCOUNTS - ); - - parsed_instructions - .entry(ParsedSystemInstructionType::SystemTransfer) - .or_default() - .push(ParsedSystemInstructionData::SystemTransfer { - lamports, - sender: instruction.accounts - [instruction_indexes::system_transfer::SENDER_INDEX] - .pubkey, - receiver: instruction.accounts - [instruction_indexes::system_transfer::RECEIVER_INDEX] - .pubkey, - }); + parse_system_instruction!(parsed_instructions, instruction, system_transfer, SystemTransfer, SystemTransfer { + lamports: lamports; + sender: instruction_indexes::system_transfer::SENDER_INDEX, + receiver: instruction_indexes::system_transfer::RECEIVER_INDEX + }); } Ok(SystemInstruction::TransferWithSeed { lamports, .. }) => { + // Note: uses system_transfer_with_seed for validation but maps to SystemTransfer type validate_number_accounts!(instruction, instruction_indexes::system_transfer_with_seed::REQUIRED_NUMBER_OF_ACCOUNTS); - parsed_instructions .entry(ParsedSystemInstructionType::SystemTransfer) .or_default() @@ -786,46 +1205,82 @@ impl IxUtils { receiver: instruction.accounts[instruction_indexes::system_transfer_with_seed::RECEIVER_INDEX].pubkey, }); } - // Nonce account withdrawal Ok(SystemInstruction::WithdrawNonceAccount(lamports)) => { - validate_number_accounts!(instruction, instruction_indexes::system_withdraw_nonce_account::REQUIRED_NUMBER_OF_ACCOUNTS); - - parsed_instructions - .entry(ParsedSystemInstructionType::SystemWithdrawNonceAccount) - .or_default() - .push(ParsedSystemInstructionData::SystemWithdrawNonceAccount { - lamports, - nonce_authority: instruction.accounts[instruction_indexes::system_withdraw_nonce_account::NONCE_AUTHORITY_INDEX].pubkey, - recipient: instruction.accounts[instruction_indexes::system_withdraw_nonce_account::RECIPIENT_INDEX].pubkey, - }); + parse_system_instruction!(parsed_instructions, instruction, system_withdraw_nonce_account, SystemWithdrawNonceAccount, SystemWithdrawNonceAccount { + lamports: lamports; + nonce_authority: instruction_indexes::system_withdraw_nonce_account::NONCE_AUTHORITY_INDEX, + recipient: instruction_indexes::system_withdraw_nonce_account::RECIPIENT_INDEX + }); } Ok(SystemInstruction::Assign { .. }) => { - validate_number_accounts!( + parse_system_instruction!( + parsed_instructions, instruction, - instruction_indexes::system_assign::REQUIRED_NUMBER_OF_ACCOUNTS + system_assign, + SystemAssign, + SystemAssign { + authority: instruction_indexes::system_assign::AUTHORITY_INDEX + } ); - + } + Ok(SystemInstruction::AssignWithSeed { .. }) => { + // Note: uses system_assign_with_seed for validation but maps to SystemAssign type + validate_number_accounts!(instruction, instruction_indexes::system_assign_with_seed::REQUIRED_NUMBER_OF_ACCOUNTS); parsed_instructions .entry(ParsedSystemInstructionType::SystemAssign) .or_default() .push(ParsedSystemInstructionData::SystemAssign { authority: instruction.accounts - [instruction_indexes::system_assign::AUTHORITY_INDEX] + [instruction_indexes::system_assign_with_seed::AUTHORITY_INDEX] .pubkey, }); } - Ok(SystemInstruction::AssignWithSeed { .. }) => { - validate_number_accounts!(instruction, instruction_indexes::system_assign_with_seed::REQUIRED_NUMBER_OF_ACCOUNTS); - + Ok(SystemInstruction::Allocate { .. }) => { + parse_system_instruction!( + parsed_instructions, + instruction, + system_allocate, + SystemAllocate, + SystemAllocate { + account: instruction_indexes::system_allocate::ACCOUNT_INDEX + } + ); + } + Ok(SystemInstruction::AllocateWithSeed { .. }) => { + // Note: uses system_allocate_with_seed for validation but maps to SystemAllocate type + validate_number_accounts!(instruction, instruction_indexes::system_allocate_with_seed::REQUIRED_NUMBER_OF_ACCOUNTS); parsed_instructions - .entry(ParsedSystemInstructionType::SystemAssign) + .entry(ParsedSystemInstructionType::SystemAllocate) .or_default() - .push(ParsedSystemInstructionData::SystemAssign { - authority: instruction.accounts - [instruction_indexes::system_assign_with_seed::AUTHORITY_INDEX] + .push(ParsedSystemInstructionData::SystemAllocate { + account: instruction.accounts + [instruction_indexes::system_allocate_with_seed::ACCOUNT_INDEX] .pubkey, }); } + Ok(SystemInstruction::InitializeNonceAccount(authority)) => { + parse_system_instruction!(parsed_instructions, instruction, system_initialize_nonce_account, SystemInitializeNonceAccount, SystemInitializeNonceAccount { + nonce_authority: authority; + nonce_account: instruction_indexes::system_initialize_nonce_account::NONCE_ACCOUNT_INDEX + }); + } + Ok(SystemInstruction::AdvanceNonceAccount) => { + parse_system_instruction!(parsed_instructions, instruction, system_advance_nonce_account, SystemAdvanceNonceAccount, SystemAdvanceNonceAccount { + nonce_account: instruction_indexes::system_advance_nonce_account::NONCE_ACCOUNT_INDEX, + nonce_authority: instruction_indexes::system_advance_nonce_account::NONCE_AUTHORITY_INDEX + }); + } + Ok(SystemInstruction::AuthorizeNonceAccount(_new_authority)) => { + parse_system_instruction!(parsed_instructions, instruction, system_authorize_nonce_account, SystemAuthorizeNonceAccount, SystemAuthorizeNonceAccount { + nonce_account: instruction_indexes::system_authorize_nonce_account::NONCE_ACCOUNT_INDEX, + nonce_authority: instruction_indexes::system_authorize_nonce_account::NONCE_AUTHORITY_INDEX + }); + } + // UpgradeNonceAccount: Not parsed - no authority parameter, cannot validate fee payer involvement + // Anyone can upgrade any nonce account without signing + Ok(SystemInstruction::UpgradeNonceAccount) => { + // Skip parsing + } _ => {} } } @@ -939,6 +1394,173 @@ impl IxUtils { is_2022: false, }); } + spl_token::instruction::TokenInstruction::Revoke => { + validate_number_accounts!( + instruction, + instruction_indexes::spl_token_revoke::REQUIRED_NUMBER_OF_ACCOUNTS + ); + + parsed_instructions + .entry(ParsedSPLInstructionType::SplTokenRevoke) + .or_default() + .push(ParsedSPLInstructionData::SplTokenRevoke { + owner: instruction.accounts + [instruction_indexes::spl_token_revoke::OWNER_INDEX] + .pubkey, + is_2022: false, + }); + } + spl_token::instruction::TokenInstruction::SetAuthority { .. } => { + validate_number_accounts!(instruction, instruction_indexes::spl_token_set_authority::REQUIRED_NUMBER_OF_ACCOUNTS); + + parsed_instructions + .entry(ParsedSPLInstructionType::SplTokenSetAuthority) + .or_default() + .push(ParsedSPLInstructionData::SplTokenSetAuthority { + authority: instruction.accounts[instruction_indexes::spl_token_set_authority::CURRENT_AUTHORITY_INDEX].pubkey, + is_2022: false, + }); + } + spl_token::instruction::TokenInstruction::MintTo { .. } => { + validate_number_accounts!( + instruction, + instruction_indexes::spl_token_mint_to::REQUIRED_NUMBER_OF_ACCOUNTS + ); + + parsed_instructions + .entry(ParsedSPLInstructionType::SplTokenMintTo) + .or_default() + .push(ParsedSPLInstructionData::SplTokenMintTo { + mint_authority: instruction.accounts[instruction_indexes::spl_token_mint_to::MINT_AUTHORITY_INDEX].pubkey, + is_2022: false, + }); + } + spl_token::instruction::TokenInstruction::MintToChecked { .. } => { + validate_number_accounts!(instruction, instruction_indexes::spl_token_mint_to_checked::REQUIRED_NUMBER_OF_ACCOUNTS); + + parsed_instructions + .entry(ParsedSPLInstructionType::SplTokenMintTo) + .or_default() + .push(ParsedSPLInstructionData::SplTokenMintTo { + mint_authority: instruction.accounts[instruction_indexes::spl_token_mint_to_checked::MINT_AUTHORITY_INDEX].pubkey, + is_2022: false, + }); + } + spl_token::instruction::TokenInstruction::InitializeMint { + mint_authority, + .. + } => { + validate_number_accounts!(instruction, instruction_indexes::spl_token_initialize_mint::REQUIRED_NUMBER_OF_ACCOUNTS); + + parsed_instructions + .entry(ParsedSPLInstructionType::SplTokenInitializeMint) + .or_default() + .push(ParsedSPLInstructionData::SplTokenInitializeMint { + mint_authority, + is_2022: false, + }); + } + spl_token::instruction::TokenInstruction::InitializeMint2 { + mint_authority, + .. + } => { + validate_number_accounts!(instruction, instruction_indexes::spl_token_initialize_mint2::REQUIRED_NUMBER_OF_ACCOUNTS); + + parsed_instructions + .entry(ParsedSPLInstructionType::SplTokenInitializeMint) + .or_default() + .push(ParsedSPLInstructionData::SplTokenInitializeMint { + mint_authority, + is_2022: false, + }); + } + spl_token::instruction::TokenInstruction::InitializeAccount => { + validate_number_accounts!(instruction, instruction_indexes::spl_token_initialize_account::REQUIRED_NUMBER_OF_ACCOUNTS); + + parsed_instructions + .entry(ParsedSPLInstructionType::SplTokenInitializeAccount) + .or_default() + .push(ParsedSPLInstructionData::SplTokenInitializeAccount { + owner: instruction.accounts[instruction_indexes::spl_token_initialize_account::OWNER_INDEX].pubkey, + is_2022: false, + }); + } + spl_token::instruction::TokenInstruction::InitializeAccount2 { owner } => { + validate_number_accounts!(instruction, instruction_indexes::spl_token_initialize_account2::REQUIRED_NUMBER_OF_ACCOUNTS); + + parsed_instructions + .entry(ParsedSPLInstructionType::SplTokenInitializeAccount) + .or_default() + .push(ParsedSPLInstructionData::SplTokenInitializeAccount { + owner, + is_2022: false, + }); + } + spl_token::instruction::TokenInstruction::InitializeAccount3 { owner } => { + validate_number_accounts!(instruction, instruction_indexes::spl_token_initialize_account3::REQUIRED_NUMBER_OF_ACCOUNTS); + + parsed_instructions + .entry(ParsedSPLInstructionType::SplTokenInitializeAccount) + .or_default() + .push(ParsedSPLInstructionData::SplTokenInitializeAccount { + owner, + is_2022: false, + }); + } + spl_token::instruction::TokenInstruction::InitializeMultisig { .. } => { + validate_number_accounts!(instruction, instruction_indexes::spl_token_initialize_multisig::REQUIRED_NUMBER_OF_ACCOUNTS); + + // Extract signers from accounts (skip first 2: multisig + rent sysvar) + let signers: Vec = + instruction.accounts.iter().skip(2).map(|a| a.pubkey).collect(); + + parsed_instructions + .entry(ParsedSPLInstructionType::SplTokenInitializeMultisig) + .or_default() + .push(ParsedSPLInstructionData::SplTokenInitializeMultisig { + signers, + is_2022: false, + }); + } + spl_token::instruction::TokenInstruction::InitializeMultisig2 { + .. + } => { + validate_number_accounts!(instruction, instruction_indexes::spl_token_initialize_multisig2::REQUIRED_NUMBER_OF_ACCOUNTS); + + // Extract signers from accounts (skip first: multisig only) + let signers: Vec = + instruction.accounts.iter().skip(1).map(|a| a.pubkey).collect(); + + parsed_instructions + .entry(ParsedSPLInstructionType::SplTokenInitializeMultisig) + .or_default() + .push(ParsedSPLInstructionData::SplTokenInitializeMultisig { + signers, + is_2022: false, + }); + } + spl_token::instruction::TokenInstruction::FreezeAccount => { + validate_number_accounts!(instruction, instruction_indexes::spl_token_freeze_account::REQUIRED_NUMBER_OF_ACCOUNTS); + + parsed_instructions + .entry(ParsedSPLInstructionType::SplTokenFreezeAccount) + .or_default() + .push(ParsedSPLInstructionData::SplTokenFreezeAccount { + freeze_authority: instruction.accounts[instruction_indexes::spl_token_freeze_account::FREEZE_AUTHORITY_INDEX].pubkey, + is_2022: false, + }); + } + spl_token::instruction::TokenInstruction::ThawAccount => { + validate_number_accounts!(instruction, instruction_indexes::spl_token_thaw_account::REQUIRED_NUMBER_OF_ACCOUNTS); + + parsed_instructions + .entry(ParsedSPLInstructionType::SplTokenThawAccount) + .or_default() + .push(ParsedSPLInstructionData::SplTokenThawAccount { + freeze_authority: instruction.accounts[instruction_indexes::spl_token_thaw_account::FREEZE_AUTHORITY_INDEX].pubkey, + is_2022: false, + }); + } _ => {} }; } @@ -1040,6 +1662,179 @@ impl IxUtils { is_2022: true, }); } + spl_token_2022::instruction::TokenInstruction::Revoke => { + validate_number_accounts!( + instruction, + instruction_indexes::spl_token_revoke::REQUIRED_NUMBER_OF_ACCOUNTS + ); + + parsed_instructions + .entry(ParsedSPLInstructionType::SplTokenRevoke) + .or_default() + .push(ParsedSPLInstructionData::SplTokenRevoke { + owner: instruction.accounts + [instruction_indexes::spl_token_revoke::OWNER_INDEX] + .pubkey, + is_2022: true, + }); + } + spl_token_2022::instruction::TokenInstruction::SetAuthority { .. } => { + validate_number_accounts!(instruction, instruction_indexes::spl_token_set_authority::REQUIRED_NUMBER_OF_ACCOUNTS); + + parsed_instructions + .entry(ParsedSPLInstructionType::SplTokenSetAuthority) + .or_default() + .push(ParsedSPLInstructionData::SplTokenSetAuthority { + authority: instruction.accounts[instruction_indexes::spl_token_set_authority::CURRENT_AUTHORITY_INDEX].pubkey, + is_2022: true, + }); + } + spl_token_2022::instruction::TokenInstruction::MintTo { .. } => { + validate_number_accounts!( + instruction, + instruction_indexes::spl_token_mint_to::REQUIRED_NUMBER_OF_ACCOUNTS + ); + + parsed_instructions + .entry(ParsedSPLInstructionType::SplTokenMintTo) + .or_default() + .push(ParsedSPLInstructionData::SplTokenMintTo { + mint_authority: instruction.accounts[instruction_indexes::spl_token_mint_to::MINT_AUTHORITY_INDEX].pubkey, + is_2022: true, + }); + } + spl_token_2022::instruction::TokenInstruction::MintToChecked { .. } => { + validate_number_accounts!(instruction, instruction_indexes::spl_token_mint_to_checked::REQUIRED_NUMBER_OF_ACCOUNTS); + + parsed_instructions + .entry(ParsedSPLInstructionType::SplTokenMintTo) + .or_default() + .push(ParsedSPLInstructionData::SplTokenMintTo { + mint_authority: instruction.accounts[instruction_indexes::spl_token_mint_to_checked::MINT_AUTHORITY_INDEX].pubkey, + is_2022: true, + }); + } + spl_token_2022::instruction::TokenInstruction::InitializeMint { + mint_authority, + .. + } => { + validate_number_accounts!(instruction, instruction_indexes::spl_token_initialize_mint::REQUIRED_NUMBER_OF_ACCOUNTS); + + parsed_instructions + .entry(ParsedSPLInstructionType::SplTokenInitializeMint) + .or_default() + .push(ParsedSPLInstructionData::SplTokenInitializeMint { + mint_authority, + is_2022: true, + }); + } + spl_token_2022::instruction::TokenInstruction::InitializeMint2 { + mint_authority, + .. + } => { + validate_number_accounts!(instruction, instruction_indexes::spl_token_initialize_mint2::REQUIRED_NUMBER_OF_ACCOUNTS); + + parsed_instructions + .entry(ParsedSPLInstructionType::SplTokenInitializeMint) + .or_default() + .push(ParsedSPLInstructionData::SplTokenInitializeMint { + mint_authority, + is_2022: true, + }); + } + spl_token_2022::instruction::TokenInstruction::InitializeAccount => { + validate_number_accounts!(instruction, instruction_indexes::spl_token_initialize_account::REQUIRED_NUMBER_OF_ACCOUNTS); + + parsed_instructions + .entry(ParsedSPLInstructionType::SplTokenInitializeAccount) + .or_default() + .push(ParsedSPLInstructionData::SplTokenInitializeAccount { + owner: instruction.accounts[instruction_indexes::spl_token_initialize_account::OWNER_INDEX].pubkey, + is_2022: true, + }); + } + spl_token_2022::instruction::TokenInstruction::InitializeAccount2 { + owner, + } => { + validate_number_accounts!(instruction, instruction_indexes::spl_token_initialize_account2::REQUIRED_NUMBER_OF_ACCOUNTS); + + parsed_instructions + .entry(ParsedSPLInstructionType::SplTokenInitializeAccount) + .or_default() + .push(ParsedSPLInstructionData::SplTokenInitializeAccount { + owner, + is_2022: true, + }); + } + spl_token_2022::instruction::TokenInstruction::InitializeAccount3 { + owner, + } => { + validate_number_accounts!(instruction, instruction_indexes::spl_token_initialize_account3::REQUIRED_NUMBER_OF_ACCOUNTS); + + parsed_instructions + .entry(ParsedSPLInstructionType::SplTokenInitializeAccount) + .or_default() + .push(ParsedSPLInstructionData::SplTokenInitializeAccount { + owner, + is_2022: true, + }); + } + spl_token_2022::instruction::TokenInstruction::InitializeMultisig { + .. + } => { + validate_number_accounts!(instruction, instruction_indexes::spl_token_initialize_multisig::REQUIRED_NUMBER_OF_ACCOUNTS); + + // Extract signers from accounts (skip first 2: multisig + rent sysvar) + let signers: Vec = + instruction.accounts.iter().skip(2).map(|a| a.pubkey).collect(); + + parsed_instructions + .entry(ParsedSPLInstructionType::SplTokenInitializeMultisig) + .or_default() + .push(ParsedSPLInstructionData::SplTokenInitializeMultisig { + signers, + is_2022: true, + }); + } + spl_token_2022::instruction::TokenInstruction::InitializeMultisig2 { + .. + } => { + validate_number_accounts!(instruction, instruction_indexes::spl_token_initialize_multisig2::REQUIRED_NUMBER_OF_ACCOUNTS); + + // Extract signers from accounts (skip first: multisig only) + let signers: Vec = + instruction.accounts.iter().skip(1).map(|a| a.pubkey).collect(); + + parsed_instructions + .entry(ParsedSPLInstructionType::SplTokenInitializeMultisig) + .or_default() + .push(ParsedSPLInstructionData::SplTokenInitializeMultisig { + signers, + is_2022: true, + }); + } + spl_token_2022::instruction::TokenInstruction::FreezeAccount => { + validate_number_accounts!(instruction, instruction_indexes::spl_token_freeze_account::REQUIRED_NUMBER_OF_ACCOUNTS); + + parsed_instructions + .entry(ParsedSPLInstructionType::SplTokenFreezeAccount) + .or_default() + .push(ParsedSPLInstructionData::SplTokenFreezeAccount { + freeze_authority: instruction.accounts[instruction_indexes::spl_token_freeze_account::FREEZE_AUTHORITY_INDEX].pubkey, + is_2022: true, + }); + } + spl_token_2022::instruction::TokenInstruction::ThawAccount => { + validate_number_accounts!(instruction, instruction_indexes::spl_token_thaw_account::REQUIRED_NUMBER_OF_ACCOUNTS); + + parsed_instructions + .entry(ParsedSPLInstructionType::SplTokenThawAccount) + .or_default() + .push(ParsedSPLInstructionData::SplTokenThawAccount { + freeze_authority: instruction.accounts[instruction_indexes::spl_token_thaw_account::FREEZE_AUTHORITY_INDEX].pubkey, + is_2022: true, + }); + } _ => {} }; } diff --git a/crates/lib/src/validator/macros.rs b/crates/lib/src/validator/macros.rs new file mode 100644 index 00000000..7c4c8033 --- /dev/null +++ b/crates/lib/src/validator/macros.rs @@ -0,0 +1,59 @@ +/// Macro to validate system instructions with consistent pattern +macro_rules! validate_system { + ($self:expr, $instructions:expr, $type:ident, $pattern:pat => $account:expr, $policy:expr, $name:expr) => { + for instruction in $instructions.get(&ParsedSystemInstructionType::$type).unwrap_or(&vec![]) + { + if let $pattern = instruction { + if *$account == $self.fee_payer_pubkey && !$policy { + return Err(KoraError::InvalidTransaction(format!( + "Fee payer cannot be used for '{}'", + $name + ))); + } + } + } + }; +} + +/// Macro to validate SPL/Token2022 instructions with is_2022 branching +macro_rules! validate_spl { + ($self:expr, $instructions:expr, $type:ident, $pattern:pat => { $account:expr, $is_2022:expr }, $spl_policy:expr, $token2022_policy:expr, $name_spl:expr, $name_2022:expr) => { + for instruction in $instructions.get(&ParsedSPLInstructionType::$type).unwrap_or(&vec![]) { + if let $pattern = instruction { + let (allowed, name) = if *$is_2022 { + ($token2022_policy, $name_2022) + } else { + ($spl_policy, $name_spl) + }; + if *$account == $self.fee_payer_pubkey && !allowed { + return Err(KoraError::InvalidTransaction(format!( + "Fee payer cannot be used for '{}'", + name + ))); + } + } + } + }; +} + +/// Macro to validate SPL/Token2022 multisig instructions that check against a list of signers +macro_rules! validate_spl_multisig { + ($self:expr, $instructions:expr, $type:ident, $pattern:pat => { $signers:expr, $is_2022:expr }, $spl_policy:expr, $token2022_policy:expr, $name_spl:expr, $name_2022:expr) => { + for instruction in $instructions.get(&ParsedSPLInstructionType::$type).unwrap_or(&vec![]) { + if let $pattern = instruction { + let (allowed, name) = if *$is_2022 { + ($token2022_policy, $name_2022) + } else { + ($spl_policy, $name_spl) + }; + // Check if fee payer is one of the signers + if $signers.contains(&$self.fee_payer_pubkey) && !allowed { + return Err(KoraError::InvalidTransaction(format!( + "Fee payer cannot be used for '{}'", + name + ))); + } + } + } + }; +} diff --git a/crates/lib/src/validator/mod.rs b/crates/lib/src/validator/mod.rs index 88fb3414..cc24ea86 100644 --- a/crates/lib/src/validator/mod.rs +++ b/crates/lib/src/validator/mod.rs @@ -2,5 +2,7 @@ pub mod account_validator; pub mod cache_validator; pub mod config_validator; pub mod math_validator; +#[macro_use] +pub mod macros; pub mod signer_validator; pub mod transaction_validator; diff --git a/crates/lib/src/validator/transaction_validator.rs b/crates/lib/src/validator/transaction_validator.rs index d2ee3277..77b318af 100644 --- a/crates/lib/src/validator/transaction_validator.rs +++ b/crates/lib/src/validator/transaction_validator.rs @@ -176,86 +176,115 @@ impl TransactionValidator { ) -> Result<(), KoraError> { let system_instructions = transaction_resolved.get_or_parse_system_instructions()?; - let check_if_allowed = |address: &Pubkey, policy_allowed: bool, instruction_type: &str| { - if *address == self.fee_payer_pubkey && !policy_allowed { - return Err(KoraError::InvalidTransaction(format!( - "Fee payer cannot be used for '{instruction_type}'", - ))); - } - Ok(()) - }; - // Validate system program instructions - for instruction in - system_instructions.get(&ParsedSystemInstructionType::SystemTransfer).unwrap_or(&vec![]) - { - if let ParsedSystemInstructionData::SystemTransfer { sender, .. } = instruction { - check_if_allowed( - sender, - self.fee_payer_policy.allow_sol_transfers, - "System Transfer", - )?; - } - } + validate_system!(self, system_instructions, SystemTransfer, + ParsedSystemInstructionData::SystemTransfer { sender, .. } => sender, + self.fee_payer_policy.system.allow_transfer, "System Transfer"); - for instruction in - system_instructions.get(&ParsedSystemInstructionType::SystemAssign).unwrap_or(&vec![]) - { - if let ParsedSystemInstructionData::SystemAssign { authority } = instruction { - check_if_allowed(authority, self.fee_payer_policy.allow_assign, "System Assign")?; - } - } + validate_system!(self, system_instructions, SystemAssign, + ParsedSystemInstructionData::SystemAssign { authority } => authority, + self.fee_payer_policy.system.allow_assign, "System Assign"); - // Validate SPL instructions - let spl_instructions = transaction_resolved.get_or_parse_spl_instructions()?; + validate_system!(self, system_instructions, SystemAllocate, + ParsedSystemInstructionData::SystemAllocate { account } => account, + self.fee_payer_policy.system.allow_allocate, "System Allocate"); - for instruction in - spl_instructions.get(&ParsedSPLInstructionType::SplTokenTransfer).unwrap_or(&vec![]) - { - if let ParsedSPLInstructionData::SplTokenTransfer { owner, is_2022, .. } = instruction { - if *is_2022 { - check_if_allowed( - owner, - self.fee_payer_policy.allow_token2022_transfers, - "Token2022 Token Transfer", - )?; - } else { - check_if_allowed( - owner, - self.fee_payer_policy.allow_spl_transfers, - "SPL Token Transfer", - )?; - } - } - } + validate_system!(self, system_instructions, SystemCreateAccount, + ParsedSystemInstructionData::SystemCreateAccount { payer, .. } => payer, + self.fee_payer_policy.system.allow_create_account, "System Create Account"); - for instruction in - spl_instructions.get(&ParsedSPLInstructionType::SplTokenApprove).unwrap_or(&vec![]) - { - if let ParsedSPLInstructionData::SplTokenApprove { owner, .. } = instruction { - check_if_allowed(owner, self.fee_payer_policy.allow_approve, "SPL Token Approve")?; - } - } + validate_system!(self, system_instructions, SystemInitializeNonceAccount, + ParsedSystemInstructionData::SystemInitializeNonceAccount { nonce_authority, .. } => nonce_authority, + self.fee_payer_policy.system.nonce.allow_initialize, "System Initialize Nonce Account"); - for instruction in - spl_instructions.get(&ParsedSPLInstructionType::SplTokenBurn).unwrap_or(&vec![]) - { - if let ParsedSPLInstructionData::SplTokenBurn { owner, .. } = instruction { - check_if_allowed(owner, self.fee_payer_policy.allow_burn, "SPL Token Burn")?; - } - } + validate_system!(self, system_instructions, SystemAdvanceNonceAccount, + ParsedSystemInstructionData::SystemAdvanceNonceAccount { nonce_authority, .. } => nonce_authority, + self.fee_payer_policy.system.nonce.allow_advance, "System Advance Nonce Account"); - for instruction in - spl_instructions.get(&ParsedSPLInstructionType::SplTokenCloseAccount).unwrap_or(&vec![]) - { - if let ParsedSPLInstructionData::SplTokenCloseAccount { owner, .. } = instruction { - check_if_allowed( - owner, - self.fee_payer_policy.allow_close_account, - "SPL Token Close Account", - )?; - } - } + validate_system!(self, system_instructions, SystemAuthorizeNonceAccount, + ParsedSystemInstructionData::SystemAuthorizeNonceAccount { nonce_authority, .. } => nonce_authority, + self.fee_payer_policy.system.nonce.allow_authorize, "System Authorize Nonce Account"); + + // Note: SystemUpgradeNonceAccount not validated - no authority parameter + + validate_system!(self, system_instructions, SystemWithdrawNonceAccount, + ParsedSystemInstructionData::SystemWithdrawNonceAccount { nonce_authority, .. } => nonce_authority, + self.fee_payer_policy.system.nonce.allow_withdraw, "System Withdraw Nonce Account"); + + // Validate SPL instructions + let spl_instructions = transaction_resolved.get_or_parse_spl_instructions()?; + + validate_spl!(self, spl_instructions, SplTokenTransfer, + ParsedSPLInstructionData::SplTokenTransfer { owner, is_2022, .. } => { owner, is_2022 }, + self.fee_payer_policy.spl_token.allow_transfer, + self.fee_payer_policy.token_2022.allow_transfer, + "SPL Token Transfer", "Token2022 Token Transfer"); + + validate_spl!(self, spl_instructions, SplTokenApprove, + ParsedSPLInstructionData::SplTokenApprove { owner, is_2022, .. } => { owner, is_2022 }, + self.fee_payer_policy.spl_token.allow_approve, + self.fee_payer_policy.token_2022.allow_approve, + "SPL Token Approve", "Token2022 Token Approve"); + + validate_spl!(self, spl_instructions, SplTokenBurn, + ParsedSPLInstructionData::SplTokenBurn { owner, is_2022 } => { owner, is_2022 }, + self.fee_payer_policy.spl_token.allow_burn, + self.fee_payer_policy.token_2022.allow_burn, + "SPL Token Burn", "Token2022 Token Burn"); + + validate_spl!(self, spl_instructions, SplTokenCloseAccount, + ParsedSPLInstructionData::SplTokenCloseAccount { owner, is_2022 } => { owner, is_2022 }, + self.fee_payer_policy.spl_token.allow_close_account, + self.fee_payer_policy.token_2022.allow_close_account, + "SPL Token Close Account", "Token2022 Token Close Account"); + + validate_spl!(self, spl_instructions, SplTokenRevoke, + ParsedSPLInstructionData::SplTokenRevoke { owner, is_2022 } => { owner, is_2022 }, + self.fee_payer_policy.spl_token.allow_revoke, + self.fee_payer_policy.token_2022.allow_revoke, + "SPL Token Revoke", "Token2022 Token Revoke"); + + validate_spl!(self, spl_instructions, SplTokenSetAuthority, + ParsedSPLInstructionData::SplTokenSetAuthority { authority, is_2022 } => { authority, is_2022 }, + self.fee_payer_policy.spl_token.allow_set_authority, + self.fee_payer_policy.token_2022.allow_set_authority, + "SPL Token SetAuthority", "Token2022 Token SetAuthority"); + + validate_spl!(self, spl_instructions, SplTokenMintTo, + ParsedSPLInstructionData::SplTokenMintTo { mint_authority, is_2022 } => { mint_authority, is_2022 }, + self.fee_payer_policy.spl_token.allow_mint_to, + self.fee_payer_policy.token_2022.allow_mint_to, + "SPL Token MintTo", "Token2022 Token MintTo"); + + validate_spl!(self, spl_instructions, SplTokenInitializeMint, + ParsedSPLInstructionData::SplTokenInitializeMint { mint_authority, is_2022 } => { mint_authority, is_2022 }, + self.fee_payer_policy.spl_token.allow_initialize_mint, + self.fee_payer_policy.token_2022.allow_initialize_mint, + "SPL Token InitializeMint", "Token2022 Token InitializeMint"); + + validate_spl!(self, spl_instructions, SplTokenInitializeAccount, + ParsedSPLInstructionData::SplTokenInitializeAccount { owner, is_2022 } => { owner, is_2022 }, + self.fee_payer_policy.spl_token.allow_initialize_account, + self.fee_payer_policy.token_2022.allow_initialize_account, + "SPL Token InitializeAccount", "Token2022 Token InitializeAccount"); + + validate_spl_multisig!(self, spl_instructions, SplTokenInitializeMultisig, + ParsedSPLInstructionData::SplTokenInitializeMultisig { signers, is_2022 } => { signers, is_2022 }, + self.fee_payer_policy.spl_token.allow_initialize_multisig, + self.fee_payer_policy.token_2022.allow_initialize_multisig, + "SPL Token InitializeMultisig", "Token2022 Token InitializeMultisig"); + + validate_spl!(self, spl_instructions, SplTokenFreezeAccount, + ParsedSPLInstructionData::SplTokenFreezeAccount { freeze_authority, is_2022 } => { freeze_authority, is_2022 }, + self.fee_payer_policy.spl_token.allow_freeze_account, + self.fee_payer_policy.token_2022.allow_freeze_account, + "SPL Token FreezeAccount", "Token2022 Token FreezeAccount"); + + validate_spl!(self, spl_instructions, SplTokenThawAccount, + ParsedSPLInstructionData::SplTokenThawAccount { freeze_authority, is_2022 } => { freeze_authority, is_2022 }, + self.fee_payer_policy.spl_token.allow_thaw_account, + self.fee_payer_policy.token_2022.allow_thaw_account, + "SPL Token ThawAccount", "Token2022 Token ThawAccount"); Ok(()) } @@ -582,10 +611,9 @@ mod tests { // Test with allow_sol_transfers = true let rpc_client = RpcMockBuilder::new().build(); - setup_config_with_policy(FeePayerPolicy { - allow_sol_transfers: true, - ..Default::default() - }); + let mut policy = FeePayerPolicy::default(); + policy.system.allow_transfer = true; + setup_config_with_policy(policy); let validator = TransactionValidator::new(fee_payer).unwrap(); @@ -597,10 +625,9 @@ mod tests { // Test with allow_sol_transfers = false let rpc_client = RpcMockBuilder::new().build(); - setup_config_with_policy(FeePayerPolicy { - allow_sol_transfers: false, - ..Default::default() - }); + let mut policy = FeePayerPolicy::default(); + policy.system.allow_transfer = false; + setup_config_with_policy(policy); let validator = TransactionValidator::new(fee_payer).unwrap(); @@ -617,8 +644,12 @@ mod tests { let new_owner = Pubkey::new_unique(); // Test with allow_assign = true + let rpc_client = RpcMockBuilder::new().build(); - setup_config_with_policy(FeePayerPolicy { allow_assign: true, ..Default::default() }); + + let mut policy = FeePayerPolicy::default(); + policy.system.allow_assign = true; + setup_config_with_policy(policy); let validator = TransactionValidator::new(fee_payer).unwrap(); @@ -628,8 +659,12 @@ mod tests { assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); // Test with allow_assign = false + let rpc_client = RpcMockBuilder::new().build(); - setup_config_with_policy(FeePayerPolicy { allow_assign: false, ..Default::default() }); + + let mut policy = FeePayerPolicy::default(); + policy.system.allow_assign = false; + setup_config_with_policy(policy); let validator = TransactionValidator::new(fee_payer).unwrap(); @@ -649,10 +684,10 @@ mod tests { // Test with allow_spl_transfers = true let rpc_client = RpcMockBuilder::new().build(); - setup_spl_config_with_policy(FeePayerPolicy { - allow_spl_transfers: true, - ..Default::default() - }); + + let mut policy = FeePayerPolicy::default(); + policy.spl_token.allow_transfer = true; + setup_spl_config_with_policy(policy); let validator = TransactionValidator::new(fee_payer).unwrap(); @@ -672,10 +707,10 @@ mod tests { // Test with allow_spl_transfers = false let rpc_client = RpcMockBuilder::new().build(); - setup_spl_config_with_policy(FeePayerPolicy { - allow_spl_transfers: false, - ..Default::default() - }); + + let mut policy = FeePayerPolicy::default(); + policy.spl_token.allow_transfer = false; + setup_spl_config_with_policy(policy); let validator = TransactionValidator::new(fee_payer).unwrap(); @@ -723,10 +758,10 @@ mod tests { let rpc_client = RpcMockBuilder::new() .with_mint_account(2) // Mock mint with 2 decimals for SPL outflow calculation .build(); - setup_token2022_config_with_policy(FeePayerPolicy { - allow_token2022_transfers: true, - ..Default::default() - }); + // Test with token_2022.allow_transfer = true + let mut policy = FeePayerPolicy::default(); + policy.token_2022.allow_transfer = true; + setup_token2022_config_with_policy(policy); let validator = TransactionValidator::new(fee_payer).unwrap(); @@ -750,10 +785,9 @@ mod tests { let rpc_client = RpcMockBuilder::new() .with_mint_account(2) // Mock mint with 2 decimals for SPL outflow calculation .build(); - setup_token2022_config_with_policy(FeePayerPolicy { - allow_token2022_transfers: false, - ..Default::default() - }); + let mut policy = FeePayerPolicy::default(); + policy.token_2022.allow_transfer = false; + setup_token2022_config_with_policy(policy); let validator = TransactionValidator::new(fee_payer).unwrap(); @@ -933,8 +967,11 @@ mod tests { let mint = Pubkey::new_unique(); // Test with allow_burn = true + let rpc_client = RpcMockBuilder::new().build(); - setup_spl_config_with_policy(FeePayerPolicy { allow_burn: true, ..Default::default() }); + let mut policy = FeePayerPolicy::default(); + policy.spl_token.allow_burn = true; + setup_spl_config_with_policy(policy); let validator = TransactionValidator::new(fee_payer).unwrap(); @@ -954,8 +991,11 @@ mod tests { assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); // Test with allow_burn = false + let rpc_client = RpcMockBuilder::new().build(); - setup_spl_config_with_policy(FeePayerPolicy { allow_burn: false, ..Default::default() }); + let mut policy = FeePayerPolicy::default(); + policy.spl_token.allow_burn = false; + setup_spl_config_with_policy(policy); let validator = TransactionValidator::new(fee_payer).unwrap(); @@ -1002,11 +1042,11 @@ mod tests { let destination = Pubkey::new_unique(); // Test with allow_close_account = true + let rpc_client = RpcMockBuilder::new().build(); - setup_spl_config_with_policy(FeePayerPolicy { - allow_close_account: true, - ..Default::default() - }); + let mut policy = FeePayerPolicy::default(); + policy.spl_token.allow_close_account = true; + setup_spl_config_with_policy(policy); let validator = TransactionValidator::new(fee_payer).unwrap(); @@ -1026,10 +1066,9 @@ mod tests { // Test with allow_close_account = false let rpc_client = RpcMockBuilder::new().build(); - setup_spl_config_with_policy(FeePayerPolicy { - allow_close_account: false, - ..Default::default() - }); + let mut policy = FeePayerPolicy::default(); + policy.spl_token.allow_close_account = false; + setup_spl_config_with_policy(policy); let validator = TransactionValidator::new(fee_payer).unwrap(); @@ -1057,8 +1096,11 @@ mod tests { let delegate = Pubkey::new_unique(); // Test with allow_approve = true + let rpc_client = RpcMockBuilder::new().build(); - setup_spl_config_with_policy(FeePayerPolicy { allow_approve: true, ..Default::default() }); + let mut policy = FeePayerPolicy::default(); + policy.spl_token.allow_approve = true; + setup_spl_config_with_policy(policy); let validator = TransactionValidator::new(fee_payer).unwrap(); @@ -1079,7 +1121,9 @@ mod tests { // Test with allow_approve = false let rpc_client = RpcMockBuilder::new().build(); - setup_spl_config_with_policy(FeePayerPolicy { allow_approve: false, ..Default::default() }); + let mut policy = FeePayerPolicy::default(); + policy.spl_token.allow_approve = false; + setup_spl_config_with_policy(policy); let validator = TransactionValidator::new(fee_payer).unwrap(); @@ -1129,11 +1173,11 @@ mod tests { let mint = Pubkey::new_unique(); // Test with allow_burn = false for Token2022 + let rpc_client = RpcMockBuilder::new().build(); - setup_token2022_config_with_policy(FeePayerPolicy { - allow_burn: false, - ..Default::default() - }); + let mut policy = FeePayerPolicy::default(); + policy.token_2022.allow_burn = false; + setup_token2022_config_with_policy(policy); let validator = TransactionValidator::new(fee_payer).unwrap(); @@ -1161,11 +1205,11 @@ mod tests { let destination = Pubkey::new_unique(); // Test with allow_close_account = false for Token2022 + let rpc_client = RpcMockBuilder::new().build(); - setup_token2022_config_with_policy(FeePayerPolicy { - allow_close_account: false, - ..FeePayerPolicy::default() - }); + let mut policy = FeePayerPolicy::default(); + policy.token_2022.allow_close_account = false; + setup_token2022_config_with_policy(policy); let validator = TransactionValidator::new(fee_payer).unwrap(); @@ -1192,11 +1236,11 @@ mod tests { let delegate = Pubkey::new_unique(); // Test with allow_approve = true + let rpc_client = RpcMockBuilder::new().build(); - setup_token2022_config_with_policy(FeePayerPolicy { - allow_approve: true, - ..Default::default() - }); + let mut policy = FeePayerPolicy::default(); + policy.token_2022.allow_approve = true; + setup_token2022_config_with_policy(policy); let validator = TransactionValidator::new(fee_payer).unwrap(); @@ -1216,11 +1260,11 @@ mod tests { assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); // Test with allow_approve = false + let rpc_client = RpcMockBuilder::new().build(); - setup_token2022_config_with_policy(FeePayerPolicy { - allow_approve: false, - ..Default::default() - }); + let mut policy = FeePayerPolicy::default(); + policy.token_2022.allow_approve = false; + setup_token2022_config_with_policy(policy); let validator = TransactionValidator::new(fee_payer).unwrap(); @@ -1261,4 +1305,201 @@ mod tests { // Should also fail for approve_checked assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); } + + #[tokio::test] + #[serial] + async fn test_fee_payer_policy_create_account() { + use solana_system_interface::instruction::create_account; + + let fee_payer = Pubkey::new_unique(); + let new_account = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + + // Test with allow_create_account = true (default) + let rpc_client = RpcMockBuilder::new().build(); + let policy = FeePayerPolicy::default(); + setup_config_with_policy(policy); + + let validator = TransactionValidator::new(fee_payer).unwrap(); + let instruction = create_account(&fee_payer, &new_account, 1000, 100, &owner); + let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); + let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); + + // Test with allow_create_account = false + let rpc_client = RpcMockBuilder::new().build(); + let mut policy = FeePayerPolicy::default(); + policy.system.allow_create_account = false; + setup_config_with_policy(policy); + + let validator = TransactionValidator::new(fee_payer).unwrap(); + let instruction = create_account(&fee_payer, &new_account, 1000, 100, &owner); + let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); + let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); + } + + #[tokio::test] + #[serial] + async fn test_fee_payer_policy_allocate() { + use solana_sdk::system_instruction::allocate; + + let fee_payer = Pubkey::new_unique(); + + // Test with allow_allocate = true (default) + let rpc_client = RpcMockBuilder::new().build(); + let policy = FeePayerPolicy::default(); + setup_config_with_policy(policy); + + let validator = TransactionValidator::new(fee_payer).unwrap(); + let instruction = allocate(&fee_payer, 100); + let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); + let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); + + // Test with allow_allocate = false + let rpc_client = RpcMockBuilder::new().build(); + let mut policy = FeePayerPolicy::default(); + policy.system.allow_allocate = false; + setup_config_with_policy(policy); + + let validator = TransactionValidator::new(fee_payer).unwrap(); + let instruction = allocate(&fee_payer, 100); + let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); + let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); + } + + #[tokio::test] + #[serial] + async fn test_fee_payer_policy_nonce_initialize() { + use solana_sdk::system_instruction::create_nonce_account; + + let fee_payer = Pubkey::new_unique(); + let nonce_account = Pubkey::new_unique(); + + // Test with allow_initialize = true (default) + let rpc_client = RpcMockBuilder::new().build(); + let policy = FeePayerPolicy::default(); + setup_config_with_policy(policy); + + let validator = TransactionValidator::new(fee_payer).unwrap(); + let instructions = create_nonce_account(&fee_payer, &nonce_account, &fee_payer, 1_000_000); + // Only test the InitializeNonceAccount instruction (second one) + let message = + VersionedMessage::Legacy(Message::new(&[instructions[1].clone()], Some(&fee_payer))); + let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); + + // Test with allow_initialize = false + let rpc_client = RpcMockBuilder::new().build(); + let mut policy = FeePayerPolicy::default(); + policy.system.nonce.allow_initialize = false; + setup_config_with_policy(policy); + + let validator = TransactionValidator::new(fee_payer).unwrap(); + let instructions = create_nonce_account(&fee_payer, &nonce_account, &fee_payer, 1_000_000); + let message = + VersionedMessage::Legacy(Message::new(&[instructions[1].clone()], Some(&fee_payer))); + let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); + } + + #[tokio::test] + #[serial] + async fn test_fee_payer_policy_nonce_advance() { + use solana_sdk::system_instruction::advance_nonce_account; + + let fee_payer = Pubkey::new_unique(); + let nonce_account = Pubkey::new_unique(); + + // Test with allow_advance = true (default) + let rpc_client = RpcMockBuilder::new().build(); + let policy = FeePayerPolicy::default(); + setup_config_with_policy(policy); + + let validator = TransactionValidator::new(fee_payer).unwrap(); + let instruction = advance_nonce_account(&nonce_account, &fee_payer); + let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); + let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); + + // Test with allow_advance = false + let rpc_client = RpcMockBuilder::new().build(); + let mut policy = FeePayerPolicy::default(); + policy.system.nonce.allow_advance = false; + setup_config_with_policy(policy); + + let validator = TransactionValidator::new(fee_payer).unwrap(); + let instruction = advance_nonce_account(&nonce_account, &fee_payer); + let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); + let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); + } + + #[tokio::test] + #[serial] + async fn test_fee_payer_policy_nonce_withdraw() { + use solana_sdk::system_instruction::withdraw_nonce_account; + + let fee_payer = Pubkey::new_unique(); + let nonce_account = Pubkey::new_unique(); + let recipient = Pubkey::new_unique(); + + // Test with allow_withdraw = true (default) + let rpc_client = RpcMockBuilder::new().build(); + let policy = FeePayerPolicy::default(); + setup_config_with_policy(policy); + + let validator = TransactionValidator::new(fee_payer).unwrap(); + let instruction = withdraw_nonce_account(&nonce_account, &fee_payer, &recipient, 1000); + let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); + let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); + + // Test with allow_withdraw = false + let rpc_client = RpcMockBuilder::new().build(); + let mut policy = FeePayerPolicy::default(); + policy.system.nonce.allow_withdraw = false; + setup_config_with_policy(policy); + + let validator = TransactionValidator::new(fee_payer).unwrap(); + let instruction = withdraw_nonce_account(&nonce_account, &fee_payer, &recipient, 1000); + let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); + let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); + } + + #[tokio::test] + #[serial] + async fn test_fee_payer_policy_nonce_authorize() { + use solana_sdk::system_instruction::authorize_nonce_account; + + let fee_payer = Pubkey::new_unique(); + let nonce_account = Pubkey::new_unique(); + let new_authority = Pubkey::new_unique(); + + // Test with allow_authorize = true (default) + let rpc_client = RpcMockBuilder::new().build(); + let policy = FeePayerPolicy::default(); + setup_config_with_policy(policy); + + let validator = TransactionValidator::new(fee_payer).unwrap(); + let instruction = authorize_nonce_account(&nonce_account, &fee_payer, &new_authority); + let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); + let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); + + // Test with allow_authorize = false + let rpc_client = RpcMockBuilder::new().build(); + let mut policy = FeePayerPolicy::default(); + policy.system.nonce.allow_authorize = false; + setup_config_with_policy(policy); + + let validator = TransactionValidator::new(fee_payer).unwrap(); + let instruction = authorize_nonce_account(&nonce_account, &fee_payer, &new_authority); + let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); + let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); + } } diff --git a/docs/getting-started/demo/server/kora.toml b/docs/getting-started/demo/server/kora.toml index cef6f81b..f716e109 100644 --- a/docs/getting-started/demo/server/kora.toml +++ b/docs/getting-started/demo/server/kora.toml @@ -44,13 +44,46 @@ disallowed_accounts = [ ] [validation.fee_payer_policy] -allow_sol_transfers = true -allow_spl_transfers = true -allow_token2022_transfers = true + +[validation.fee_payer_policy.system] +allow_transfer = true allow_assign = true +allow_create_account = true +allow_allocate = true + +[validation.fee_payer_policy.system.nonce] +allow_initialize = true +allow_advance = true +allow_authorize = true +allow_withdraw = true + +[validation.fee_payer_policy.spl_token] +allow_transfer = true +allow_burn = true +allow_close_account = true +allow_approve = true +allow_revoke = true +allow_set_authority = true +allow_mint_to = true +allow_initialize_mint = true +allow_initialize_account = true +allow_initialize_multisig = true +allow_freeze_account = true +allow_thaw_account = true + +[validation.fee_payer_policy.token_2022] +allow_transfer = true allow_burn = true allow_close_account = true allow_approve = true +allow_revoke = true +allow_set_authority = true +allow_mint_to = true +allow_initialize_mint = true +allow_initialize_account = true +allow_initialize_multisig = true +allow_freeze_account = true +allow_thaw_account = true [validation.price] type = "margin" # free / margin / fixed diff --git a/docs/operators/deploy/sample/kora.toml b/docs/operators/deploy/sample/kora.toml index 86877fea..a50f93e2 100644 --- a/docs/operators/deploy/sample/kora.toml +++ b/docs/operators/deploy/sample/kora.toml @@ -39,13 +39,46 @@ disallowed_accounts = [] # Fee payer policy controls what actions the fee payer can perform # All default to false for security [validation.fee_payer_policy] -allow_sol_transfers = true # Allow fee payer to be source in SOL transfers -allow_spl_transfers = true # Allow fee payer to be source in SPL token transfers -allow_token2022_transfers = true # Allow fee payer to be source in Token2022 transfers -allow_assign = true # Allow fee payer to use Assign instruction -allow_burn = true # Allow fee payer to burn tokens from their accounts -allow_close_account = true # Allow fee payer to close their token accounts -allow_approve = true # Allow fee payer to approve tokens from their accounts + +[validation.fee_payer_policy.system] +allow_transfer = true # Allow fee payer to be sender in System Transfer/TransferWithSeed +allow_assign = true # Allow fee payer to be authority in System Assign/AssignWithSeed +allow_create_account = true # Allow fee payer to be payer in System CreateAccount/CreateAccountWithSeed +allow_allocate = true # Allow fee payer to be account in System Allocate/AllocateWithSeed + +[validation.fee_payer_policy.system.nonce] +allow_initialize = true # Allow fee payer to initialize nonce accounts +allow_advance = true # Allow fee payer to advance nonce accounts +allow_authorize = true # Allow fee payer to authorize nonce accounts +allow_withdraw = true # Allow fee payer to withdraw from nonce accounts + +[validation.fee_payer_policy.spl_token] +allow_transfer = true # Allow fee payer to be source in SPL token transfers +allow_burn = true # Allow fee payer to burn SPL tokens from their accounts +allow_close_account = true # Allow fee payer to close their SPL token accounts +allow_approve = true # Allow fee payer to approve SPL tokens from their accounts +allow_revoke = true +allow_set_authority = true +allow_mint_to = true +allow_initialize_mint = true +allow_initialize_account = true +allow_initialize_multisig = true +allow_freeze_account = true +allow_thaw_account = true + +[validation.fee_payer_policy.token_2022] +allow_transfer = true # Allow fee payer to be source in Token2022 transfers +allow_burn = true # Allow fee payer to burn Token2022 tokens from their accounts +allow_close_account = true # Allow fee payer to close their Token2022 accounts +allow_approve = true # Allow fee payer to approve Token2022 tokens from their accounts +allow_revoke = true +allow_set_authority = true +allow_mint_to = true +allow_initialize_mint = true +allow_initialize_account = true +allow_initialize_multisig = true +allow_freeze_account = true +allow_thaw_account = true [kora.enabled_methods] liveness = false diff --git a/docs/x402/demo/kora/kora.toml b/docs/x402/demo/kora/kora.toml index eba5115c..8c473ec6 100644 --- a/docs/x402/demo/kora/kora.toml +++ b/docs/x402/demo/kora/kora.toml @@ -45,13 +45,46 @@ disallowed_accounts = [ ] [validation.fee_payer_policy] -allow_sol_transfers = false -allow_spl_transfers = false -allow_token2022_transfers = false + +[validation.fee_payer_policy.system] +allow_transfer = false allow_assign = false +allow_create_account = false +allow_allocate = false + +[validation.fee_payer_policy.system.nonce] +allow_initialize = false +allow_advance = false +allow_authorize = false +allow_withdraw = false + +[validation.fee_payer_policy.spl_token] +allow_transfer = false +allow_burn = false +allow_close_account = false +allow_approve = false +allow_revoke = false +allow_set_authority = false +allow_mint_to = false +allow_initialize_mint = false +allow_initialize_account = false +allow_initialize_multisig = false +allow_freeze_account = false +allow_thaw_account = false + +[validation.fee_payer_policy.token_2022] +allow_transfer = false allow_burn = false allow_close_account = false allow_approve = false +allow_revoke = false +allow_set_authority = false +allow_mint_to = false +allow_initialize_mint = false +allow_initialize_account = false +allow_initialize_multisig = false +allow_freeze_account = false +allow_thaw_account = false [validation.price] type = "free" # free / margin / fixed diff --git a/kora.toml b/kora.toml index 6b840ab7..9adabbf5 100644 --- a/kora.toml +++ b/kora.toml @@ -48,13 +48,46 @@ disallowed_accounts = [] # but those programs have a lot of instructions that can be used to perform actions that could be problematic # for the fee payer, therefore we allow more granular control over what the fee payer can do with those programs. [validation.fee_payer_policy] -allow_sol_transfers = true # Allow fee payer to be source in SOL transfers -allow_spl_transfers = true # Allow fee payer to be source in SPL token transfers -allow_token2022_transfers = true # Allow fee payer to be source in Token2022 transfers -allow_assign = true # Allow fee payer to use Assign instruction -allow_burn = true # Allow fee payer to burn tokens -allow_close_account = true # Allow fee payer to close token accounts -allow_approve = true # Allow fee payer to approve tokens + +[validation.fee_payer_policy.system] +allow_transfer = true # Allow fee payer to be sender in System Transfer/TransferWithSeed +allow_assign = true # Allow fee payer to be authority in System Assign/AssignWithSeed +allow_create_account = true # Allow fee payer to be payer in System CreateAccount/CreateAccountWithSeed +allow_allocate = true # Allow fee payer to be account in System Allocate/AllocateWithSeed + +[validation.fee_payer_policy.system.nonce] +allow_initialize = true # Allow fee payer to initialize nonce accounts +allow_advance = true # Allow fee payer to advance nonce accounts +allow_authorize = true # Allow fee payer to authorize nonce accounts +allow_withdraw = true # Allow fee payer to withdraw from nonce accounts + +[validation.fee_payer_policy.spl_token] +allow_transfer = true # Allow fee payer to be source in SPL token transfers +allow_burn = true # Allow fee payer to burn SPL tokens +allow_close_account = true # Allow fee payer to close SPL token accounts +allow_approve = true # Allow fee payer to approve SPL token delegates +allow_revoke = true # Allow fee payer to revoke SPL token delegates +allow_set_authority = true # Allow fee payer to change SPL token authorities +allow_mint_to = true # Allow fee payer to mint SPL tokens +allow_initialize_mint = true # Allow fee payer to initialize SPL token mints +allow_initialize_account = true # Allow fee payer to initialize SPL token accounts +allow_initialize_multisig = true # Allow fee payer to be signer in SPL token multisig initialization +allow_freeze_account = true # Allow fee payer to freeze SPL token accounts +allow_thaw_account = true # Allow fee payer to thaw SPL token accounts + +[validation.fee_payer_policy.token_2022] +allow_transfer = true # Allow fee payer to be source in Token2022 transfers +allow_burn = true # Allow fee payer to burn Token2022 tokens +allow_close_account = true # Allow fee payer to close Token2022 accounts +allow_approve = true # Allow fee payer to approve Token2022 delegates +allow_revoke = true # Allow fee payer to revoke Token2022 delegates +allow_set_authority = true # Allow fee payer to change Token2022 authorities +allow_mint_to = true # Allow fee payer to mint Token2022 tokens +allow_initialize_mint = true # Allow fee payer to initialize Token2022 mints +allow_initialize_account = true # Allow fee payer to initialize Token2022 accounts +allow_initialize_multisig = true # Allow fee payer to be signer in Token2022 multisig initialization +allow_freeze_account = true # Allow fee payer to freeze Token2022 accounts +allow_thaw_account = true # Allow fee payer to thaw Token2022 accounts [validation.price] type = "margin" # free / margin / fixed diff --git a/sdks/ts/src/types/index.ts b/sdks/ts/src/types/index.ts index 8ef65fa7..bd36db0a 100644 --- a/sdks/ts/src/types/index.ts +++ b/sdks/ts/src/types/index.ts @@ -293,23 +293,93 @@ export interface Config { } /** - * Policy controlling what actions the fee payer can perform. + * Nonce instruction policy */ -export interface FeePayerPolicy { - /** Allow fee payer to be source in SOL transfers */ - allow_sol_transfers: boolean; +export interface NonceInstructionPolicy { + /** Allow fee payer to initialize nonce accounts */ + allow_initialize: boolean; + /** Allow fee payer to advance nonce accounts */ + allow_advance: boolean; + /** Allow fee payer to authorize nonce accounts */ + allow_authorize: boolean; + /** Allow fee payer to withdraw from nonce accounts */ + allow_withdraw: boolean; +} + +/** + * System instruction policy + */ +export interface SystemInstructionPolicy { + /** Allow fee payer to be the sender in System Transfer/TransferWithSeed */ + allow_transfer: boolean; + /** Allow fee payer to be the authority in System Assign/AssignWithSeed */ + allow_assign: boolean; + /** Allow fee payer to be the payer in System CreateAccount/CreateAccountWithSeed */ + allow_create_account: boolean; + /** Allow fee payer to be the account in System Allocate/AllocateWithSeed */ + allow_allocate: boolean; + /** Nested policy for nonce account operations */ + nonce: NonceInstructionPolicy; +} + +/** + * SPL Token instruction policy + */ +export interface SplTokenInstructionPolicy { /** Allow fee payer to be source in SPL token transfers */ - allow_spl_transfers: boolean; + allow_transfer: boolean; + /** Allow fee payer to burn SPL tokens */ + allow_burn: boolean; + /** Allow fee payer to close SPL token accounts */ + allow_close_account: boolean; + /** Allow fee payer to approve SPL token delegates */ + allow_approve: boolean; + /** Allow fee payer to revoke SPL token delegates */ + allow_revoke: boolean; + /** Allow fee payer to set authority on SPL token accounts */ + allow_set_authority: boolean; + /** Allow fee payer to mint SPL tokens */ + allow_mint_to: boolean; + /** Allow fee payer to freeze SPL token accounts */ + allow_freeze_account: boolean; + /** Allow fee payer to thaw SPL token accounts */ + allow_thaw_account: boolean; +} + +/** + * Token2022 instruction policy + */ +export interface Token2022InstructionPolicy { /** Allow fee payer to be source in Token2022 transfers */ - allow_token2022_transfers: boolean; - /** Allow fee payer to use Assign instruction */ - allow_assign: boolean; - /** Allow fee payer to use Burn instruction */ + allow_transfer: boolean; + /** Allow fee payer to burn Token2022 tokens */ allow_burn: boolean; - /** Allow fee payer to use CloseAccount instruction */ + /** Allow fee payer to close Token2022 accounts */ allow_close_account: boolean; - /** Allow fee payer to use Approve instruction */ + /** Allow fee payer to approve Token2022 delegates */ allow_approve: boolean; + /** Allow fee payer to revoke Token2022 delegates */ + allow_revoke: boolean; + /** Allow fee payer to set authority on Token2022 accounts */ + allow_set_authority: boolean; + /** Allow fee payer to mint Token2022 tokens */ + allow_mint_to: boolean; + /** Allow fee payer to freeze Token2022 accounts */ + allow_freeze_account: boolean; + /** Allow fee payer to thaw Token2022 accounts */ + allow_thaw_account: boolean; +} + +/** + * Policy controlling what actions the fee payer can perform. + */ +export interface FeePayerPolicy { + /** System program instruction policies */ + system: SystemInstructionPolicy; + /** SPL Token program instruction policies */ + spl_token: SplTokenInstructionPolicy; + /** Token2022 program instruction policies */ + token_2022: Token2022InstructionPolicy; } /** diff --git a/sdks/ts/test/integration.test.ts b/sdks/ts/test/integration.test.ts index d65bdebb..61cdfc49 100644 --- a/sdks/ts/test/integration.test.ts +++ b/sdks/ts/test/integration.test.ts @@ -63,11 +63,44 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without expect(config.validation_config.price).toBeDefined(); expect(config.validation_config.price.type).toBeDefined(); expect(config.validation_config.fee_payer_policy).toBeDefined(); - expect(config.validation_config.fee_payer_policy.allow_sol_transfers).toBeDefined(); - expect(config.validation_config.fee_payer_policy.allow_spl_transfers).toBeDefined(); - expect(config.validation_config.fee_payer_policy.allow_token2022_transfers).toBeDefined(); - expect(config.validation_config.fee_payer_policy.allow_assign).toBeDefined(); - expect(config.validation_config.fee_payer_policy.allow_burn).toBeDefined(); + + // System policy + expect(config.validation_config.fee_payer_policy.system).toBeDefined(); + expect(config.validation_config.fee_payer_policy.system.allow_transfer).toBeDefined(); + expect(config.validation_config.fee_payer_policy.system.allow_assign).toBeDefined(); + expect(config.validation_config.fee_payer_policy.system.allow_create_account).toBeDefined(); + expect(config.validation_config.fee_payer_policy.system.allow_allocate).toBeDefined(); + + // System nonce policy + expect(config.validation_config.fee_payer_policy.system.nonce).toBeDefined(); + expect(config.validation_config.fee_payer_policy.system.nonce.allow_initialize).toBeDefined(); + expect(config.validation_config.fee_payer_policy.system.nonce.allow_advance).toBeDefined(); + expect(config.validation_config.fee_payer_policy.system.nonce.allow_authorize).toBeDefined(); + expect(config.validation_config.fee_payer_policy.system.nonce.allow_withdraw).toBeDefined(); + + // SPL token policy + expect(config.validation_config.fee_payer_policy.spl_token).toBeDefined(); + expect(config.validation_config.fee_payer_policy.spl_token.allow_transfer).toBeDefined(); + expect(config.validation_config.fee_payer_policy.spl_token.allow_burn).toBeDefined(); + expect(config.validation_config.fee_payer_policy.spl_token.allow_close_account).toBeDefined(); + expect(config.validation_config.fee_payer_policy.spl_token.allow_approve).toBeDefined(); + expect(config.validation_config.fee_payer_policy.spl_token.allow_revoke).toBeDefined(); + expect(config.validation_config.fee_payer_policy.spl_token.allow_set_authority).toBeDefined(); + expect(config.validation_config.fee_payer_policy.spl_token.allow_mint_to).toBeDefined(); + expect(config.validation_config.fee_payer_policy.spl_token.allow_freeze_account).toBeDefined(); + expect(config.validation_config.fee_payer_policy.spl_token.allow_thaw_account).toBeDefined(); + + // Token2022 policy + expect(config.validation_config.fee_payer_policy.token_2022).toBeDefined(); + expect(config.validation_config.fee_payer_policy.token_2022.allow_transfer).toBeDefined(); + expect(config.validation_config.fee_payer_policy.token_2022.allow_burn).toBeDefined(); + expect(config.validation_config.fee_payer_policy.token_2022.allow_close_account).toBeDefined(); + expect(config.validation_config.fee_payer_policy.token_2022.allow_approve).toBeDefined(); + expect(config.validation_config.fee_payer_policy.token_2022.allow_revoke).toBeDefined(); + expect(config.validation_config.fee_payer_policy.token_2022.allow_set_authority).toBeDefined(); + expect(config.validation_config.fee_payer_policy.token_2022.allow_mint_to).toBeDefined(); + expect(config.validation_config.fee_payer_policy.token_2022.allow_freeze_account).toBeDefined(); + expect(config.validation_config.fee_payer_policy.token_2022.allow_thaw_account).toBeDefined(); expect(config.enabled_methods).toBeDefined(); expect(config.enabled_methods.liveness).toBeDefined(); expect(config.enabled_methods.estimate_transaction_fee).toBeDefined(); diff --git a/sdks/ts/test/unit.test.ts b/sdks/ts/test/unit.test.ts index 36549ec5..19534ba1 100644 --- a/sdks/ts/test/unit.test.ts +++ b/sdks/ts/test/unit.test.ts @@ -122,13 +122,40 @@ describe('KoraClient Unit Tests', () => { allowed_spl_paid_tokens: ['spl_token1'], disallowed_accounts: ['account1'], fee_payer_policy: { - allow_sol_transfers: true, - allow_spl_transfers: true, - allow_token2022_transfers: false, - allow_assign: true, - allow_burn: true, - allow_close_account: true, - allow_approve: true, + system: { + allow_transfer: true, + allow_assign: true, + allow_create_account: true, + allow_allocate: true, + nonce: { + allow_initialize: true, + allow_advance: true, + allow_authorize: true, + allow_withdraw: true, + }, + }, + spl_token: { + allow_transfer: true, + allow_burn: true, + allow_close_account: true, + allow_approve: true, + allow_revoke: true, + allow_set_authority: true, + allow_mint_to: true, + allow_freeze_account: true, + allow_thaw_account: true, + }, + token_2022: { + allow_transfer: false, + allow_burn: true, + allow_close_account: true, + allow_approve: true, + allow_revoke: true, + allow_set_authority: true, + allow_mint_to: true, + allow_freeze_account: true, + allow_thaw_account: true, + }, }, price: { type: 'margin', @@ -372,13 +399,40 @@ describe('KoraClient Unit Tests', () => { allowed_spl_paid_tokens: ['4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU'], disallowed_accounts: [], fee_payer_policy: { - allow_sol_transfers: true, - allow_spl_transfers: true, - allow_token2022_transfers: true, - allow_assign: true, - allow_burn: true, - allow_close_account: true, - allow_approve: true, + system: { + allow_transfer: true, + allow_assign: true, + allow_create_account: true, + allow_allocate: true, + nonce: { + allow_initialize: true, + allow_advance: true, + allow_authorize: true, + allow_withdraw: true, + }, + }, + spl_token: { + allow_transfer: true, + allow_burn: true, + allow_close_account: true, + allow_approve: true, + allow_revoke: true, + allow_set_authority: true, + allow_mint_to: true, + allow_freeze_account: true, + allow_thaw_account: true, + }, + token_2022: { + allow_transfer: true, + allow_burn: true, + allow_close_account: true, + allow_approve: true, + allow_revoke: true, + allow_set_authority: true, + allow_mint_to: true, + allow_freeze_account: true, + allow_thaw_account: true, + }, }, price: { type: 'margin', diff --git a/tests/fee_payer_policy/fee_payer_policy_violations.rs b/tests/fee_payer_policy/fee_payer_policy_violations.rs index be9c8ec3..dfab60ee 100644 --- a/tests/fee_payer_policy/fee_payer_policy_violations.rs +++ b/tests/fee_payer_policy/fee_payer_policy_violations.rs @@ -1,7 +1,8 @@ use crate::common::{assertions::RpcErrorAssertions, *}; use jsonrpsee::rpc_params; use solana_sdk::{ - program_pack::Pack, signature::Keypair, signer::Signer, transaction::Transaction, + program_pack::Pack, pubkey::Pubkey, signature::Keypair, signer::Signer, + transaction::Transaction, }; use solana_system_interface::instruction::{create_account, transfer}; use spl_associated_token_account::{ @@ -38,6 +39,84 @@ async fn test_sol_transfer_policy_violation() { } } +#[tokio::test] +async fn test_assign_policy_violation() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + + let fee_payer_pubkey = FeePayerTestHelper::get_fee_payer_pubkey(); + let new_owner = Pubkey::new_unique(); + + let malicious_tx = ctx + .transaction_builder() + .with_fee_payer(fee_payer_pubkey) + .with_system_assign(&fee_payer_pubkey, &new_owner) + .build() + .await + .expect("Failed to create transaction with assign"); + + let result = + ctx.rpc_call::("signTransaction", rpc_params![malicious_tx]).await; + + match result { + Err(error) => { + error.assert_contains_message("Fee payer cannot be used for 'System Assign'"); + } + Ok(_) => panic!("Expected error for assign policy violation"), + } +} + +#[tokio::test] +async fn test_create_account_policy_violation() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + + let fee_payer_pubkey = FeePayerTestHelper::get_fee_payer_pubkey(); + let new_account = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + + let malicious_tx = ctx + .transaction_builder() + .with_fee_payer(fee_payer_pubkey) + .with_system_create_account(&fee_payer_pubkey, &new_account, 1_000_000, 0, &owner) + .build() + .await + .expect("Failed to create transaction with create_account"); + + let result = + ctx.rpc_call::("signTransaction", rpc_params![malicious_tx]).await; + + match result { + Err(error) => { + error.assert_contains_message("Fee payer cannot be used for 'System Create Account'"); + } + Ok(_) => panic!("Expected error for create_account policy violation"), + } +} + +#[tokio::test] +async fn test_allocate_policy_violation() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + + let fee_payer_pubkey = FeePayerTestHelper::get_fee_payer_pubkey(); + + let malicious_tx = ctx + .transaction_builder() + .with_fee_payer(fee_payer_pubkey) + .with_system_allocate(&fee_payer_pubkey, 1024) + .build() + .await + .expect("Failed to create transaction with allocate"); + + let result = + ctx.rpc_call::("signTransaction", rpc_params![malicious_tx]).await; + + match result { + Err(error) => { + error.assert_contains_message("Fee payer cannot be used for 'System Allocate'"); + } + Ok(_) => panic!("Expected error for allocate policy violation"), + } +} + #[tokio::test] async fn test_spl_transfer_policy_violation() { let ctx = TestContext::new().await.expect("Failed to create test context"); @@ -46,19 +125,21 @@ async fn test_spl_transfer_policy_violation() { let fee_payer_pubkey = FeePayerTestHelper::get_fee_payer_pubkey(); let recipient_pubkey = RecipientTestHelper::get_recipient_pubkey(); - let fee_payer_token_account = - get_associated_token_address(&fee_payer_pubkey, &setup.usdc_mint.pubkey()); + let fee_payer_token_account = setup + .create_fee_payer_token_account_spl(&setup.fee_payer_policy_mint.pubkey()) + .await + .expect("Failed to create token account"); let recipient_token_account = - get_associated_token_address(&recipient_pubkey, &setup.usdc_mint.pubkey()); + get_associated_token_address(&recipient_pubkey, &setup.fee_payer_policy_mint.pubkey()); setup - .mint_tokens_to_account(&fee_payer_token_account, 100_000) + .mint_fee_payer_policy_tokens_to_account(&fee_payer_token_account.pubkey(), 100_000) .await .expect("Failed to mint tokens"); let spl_transfer_instruction = token_instruction::transfer( &spl_token::id(), - &fee_payer_token_account, + &fee_payer_token_account.pubkey(), &recipient_token_account, &fee_payer_pubkey, &[&fee_payer_pubkey], @@ -93,26 +174,28 @@ async fn test_token2022_transfer_policy_violation() { let fee_payer_pubkey = FeePayerTestHelper::get_fee_payer_pubkey(); let recipient_pubkey = RecipientTestHelper::get_recipient_pubkey(); - let fee_payer_token_2022_account = get_associated_token_address_with_program_id( - &fee_payer_pubkey, - &setup.usdc_mint_2022.pubkey(), - &spl_token_2022::id(), - ); + let fee_payer_token_2022_account = setup + .create_fee_payer_token_account_2022(&setup.fee_payer_policy_mint_2022.pubkey()) + .await + .expect("Failed to create token account"); let recipient_token_2022_account = get_associated_token_address_with_program_id( &recipient_pubkey, - &setup.usdc_mint_2022.pubkey(), + &setup.fee_payer_policy_mint_2022.pubkey(), &spl_token_2022::id(), ); setup - .mint_tokens_2022_to_account(&fee_payer_token_2022_account, 100_000) + .mint_fee_payer_policy_tokens_2022_to_account( + &fee_payer_token_2022_account.pubkey(), + 100_000, + ) .await .expect("Failed to mint tokens"); let token_2022_transfer_instruction = token_2022_instruction::transfer_checked( &spl_token_2022::id(), - &fee_payer_token_2022_account, - &setup.usdc_mint_2022.pubkey(), + &fee_payer_token_2022_account.pubkey(), + &setup.fee_payer_policy_mint_2022.pubkey(), &recipient_token_2022_account, &fee_payer_pubkey, &[&fee_payer_pubkey], @@ -147,18 +230,20 @@ async fn test_burn_policy_violation() { let setup = TestAccountSetup::new().await; let fee_payer_pubkey = FeePayerTestHelper::get_fee_payer_pubkey(); - let fee_payer_token_account = - get_associated_token_address(&fee_payer_pubkey, &setup.usdc_mint.pubkey()); + let fee_payer_token_account = setup + .create_fee_payer_token_account_spl(&setup.fee_payer_policy_mint.pubkey()) + .await + .expect("Failed to create token account"); setup - .mint_tokens_to_account(&fee_payer_token_account, 1_000_000) + .mint_fee_payer_policy_tokens_to_account(&fee_payer_token_account.pubkey(), 1_000_000) .await .expect("Failed to mint SPL"); let burn_instruction = token_instruction::burn( &spl_token::id(), - &fee_payer_token_account, - &setup.usdc_mint.pubkey(), + &fee_payer_token_account.pubkey(), + &setup.fee_payer_policy_mint.pubkey(), &fee_payer_pubkey, &[&fee_payer_pubkey], 1_000, @@ -189,48 +274,14 @@ async fn test_close_account_policy_violation() { let ctx = TestContext::new().await.expect("Failed to create test context"); let setup = TestAccountSetup::new().await; - // Create a new token account to now affect other tests - let closable_token_account_keypair = Keypair::new(); - - let rent = setup - .rpc_client - .get_minimum_balance_for_rent_exemption(spl_token::state::Account::LEN) + let fee_payer_token_account = setup + .create_fee_payer_token_account_spl(&setup.fee_payer_policy_mint.pubkey()) .await - .expect("Failed to get rent exemption"); - - let create_account_ix = create_account( - &setup.fee_payer_keypair.pubkey(), - &closable_token_account_keypair.pubkey(), - rent, - spl_token::state::Account::LEN as u64, - &spl_token::id(), - ); - - let create_closable_token_account_ix = spl_token::instruction::initialize_account( - &spl_token::id(), - &closable_token_account_keypair.pubkey(), - &setup.usdc_mint.pubkey(), - &setup.fee_payer_keypair.pubkey(), - ) - .expect("Failed to create initialize account instruction"); - - let recent_blockhash = setup.rpc_client.get_latest_blockhash().await.unwrap(); - let setup_tx = Transaction::new_signed_with_payer( - &[create_account_ix, create_closable_token_account_ix], - Some(&setup.fee_payer_keypair.pubkey()), - &[&setup.fee_payer_keypair, &closable_token_account_keypair], - recent_blockhash, - ); - - setup - .rpc_client - .send_and_confirm_transaction(&setup_tx) - .await - .expect("Failed to setup and freeze token account"); + .expect("Failed to create token account"); let close_account_instruction = token_instruction::close_account( &spl_token::id(), - &closable_token_account_keypair.pubkey(), + &fee_payer_token_account.pubkey(), &setup.recipient_pubkey, &setup.fee_payer_keypair.pubkey(), &[&setup.fee_payer_keypair.pubkey()], @@ -263,17 +314,19 @@ async fn test_approve_policy_violation() { let fee_payer_pubkey = FeePayerTestHelper::get_fee_payer_pubkey(); let recipient_pubkey = RecipientTestHelper::get_recipient_pubkey(); - let fee_payer_token_account = - get_associated_token_address(&fee_payer_pubkey, &setup.usdc_mint.pubkey()); + let fee_payer_token_account = setup + .create_fee_payer_token_account_spl(&setup.fee_payer_policy_mint.pubkey()) + .await + .expect("Failed to create token account"); setup - .mint_tokens_to_account(&fee_payer_token_account, 1_000_000) + .mint_fee_payer_policy_tokens_to_account(&fee_payer_token_account.pubkey(), 1_000_000) .await .expect("Failed to mint tokens"); let approve_instruction = token_instruction::approve( &spl_token::id(), - &fee_payer_token_account, + &fee_payer_token_account.pubkey(), &recipient_pubkey, &fee_payer_pubkey, &[&fee_payer_pubkey], @@ -299,3 +352,424 @@ async fn test_approve_policy_violation() { Ok(_) => panic!("Expected error for approve policy violation"), } } + +#[tokio::test] +async fn test_revoke_policy_violation() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + let setup = TestAccountSetup::new().await; + + let fee_payer_pubkey = FeePayerTestHelper::get_fee_payer_pubkey(); + let fee_payer_token_account = setup + .create_fee_payer_token_account_spl(&setup.fee_payer_policy_mint.pubkey()) + .await + .expect("Failed to create token account"); + + setup + .mint_fee_payer_policy_tokens_to_account(&fee_payer_token_account.pubkey(), 1_000_000) + .await + .expect("Failed to mint tokens"); + + let malicious_tx = ctx + .transaction_builder() + .with_fee_payer(fee_payer_pubkey) + .with_spl_revoke(&fee_payer_token_account.pubkey(), &fee_payer_pubkey) + .build() + .await + .expect("Failed to create transaction with revoke"); + + let result = + ctx.rpc_call::("signTransaction", rpc_params![malicious_tx]).await; + + match result { + Err(error) => { + error.assert_contains_message("Fee payer cannot be used for 'SPL Token Revoke'"); + } + Ok(_) => panic!("Expected error for revoke policy violation"), + } +} + +#[tokio::test] +async fn test_revoke_token2022_policy_violation() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + let setup = TestAccountSetup::new().await; + + let fee_payer_pubkey = FeePayerTestHelper::get_fee_payer_pubkey(); + let fee_payer_token_2022_account = setup + .create_fee_payer_token_account_2022(&setup.fee_payer_policy_mint_2022.pubkey()) + .await + .expect("Failed to create token account"); + + setup + .mint_fee_payer_policy_tokens_2022_to_account( + &fee_payer_token_2022_account.pubkey(), + 1_000_000, + ) + .await + .expect("Failed to mint Token2022"); + + let malicious_tx = ctx + .transaction_builder() + .with_fee_payer(fee_payer_pubkey) + .with_token2022_revoke(&fee_payer_token_2022_account.pubkey(), &fee_payer_pubkey) + .build() + .await + .expect("Failed to create transaction with Token2022 revoke"); + + let result = + ctx.rpc_call::("signTransaction", rpc_params![malicious_tx]).await; + + match result { + Err(error) => { + error.assert_contains_message("Fee payer cannot be used for 'Token2022 Token Revoke'"); + } + Ok(_) => panic!("Expected error for Token2022 revoke policy violation"), + } +} + +#[tokio::test] +async fn test_set_authority_policy_violation() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + let setup = TestAccountSetup::new().await; + + let fee_payer_pubkey = FeePayerTestHelper::get_fee_payer_pubkey(); + let recipient_pubkey = RecipientTestHelper::get_recipient_pubkey(); + + let fee_payer_token_account = setup + .create_fee_payer_token_account_spl(&setup.fee_payer_policy_mint.pubkey()) + .await + .expect("Failed to create token account"); + + setup + .mint_fee_payer_policy_tokens_to_account(&fee_payer_token_account.pubkey(), 1_000_000) + .await + .expect("Failed to mint tokens"); + + let malicious_tx = ctx + .transaction_builder() + .with_fee_payer(fee_payer_pubkey) + .with_spl_set_authority( + &fee_payer_token_account.pubkey(), + Some(&recipient_pubkey), + token_instruction::AuthorityType::AccountOwner, + &fee_payer_pubkey, + ) + .build() + .await + .expect("Failed to create transaction with set_authority"); + + let result = + ctx.rpc_call::("signTransaction", rpc_params![malicious_tx]).await; + + match result { + Err(error) => { + error.assert_contains_message("Fee payer cannot be used for 'SPL Token SetAuthority'"); + } + Ok(_) => panic!("Expected error for set_authority policy violation"), + } +} + +#[tokio::test] +async fn test_set_authority_token2022_policy_violation() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + let setup = TestAccountSetup::new().await; + + let fee_payer_pubkey = FeePayerTestHelper::get_fee_payer_pubkey(); + let recipient_pubkey = RecipientTestHelper::get_recipient_pubkey(); + + let fee_payer_token_2022_account = setup + .create_fee_payer_token_account_2022(&setup.fee_payer_policy_mint_2022.pubkey()) + .await + .expect("Failed to create token account"); + + setup + .mint_fee_payer_policy_tokens_2022_to_account( + &fee_payer_token_2022_account.pubkey(), + 1_000_000, + ) + .await + .expect("Failed to mint Token2022"); + + let malicious_tx = ctx + .transaction_builder() + .with_fee_payer(fee_payer_pubkey) + .with_token2022_set_authority( + &fee_payer_token_2022_account.pubkey(), + Some(&recipient_pubkey), + // Can't use freeze authority on token2022 account, so use close authority + token_2022_instruction::AuthorityType::CloseAccount, + &fee_payer_pubkey, + ) + .build() + .await + .expect("Failed to create transaction with Token2022 set_authority"); + + let result = + ctx.rpc_call::("signTransaction", rpc_params![malicious_tx]).await; + + match result { + Err(error) => { + error.assert_contains_message( + "Fee payer cannot be used for 'Token2022 Token SetAuthority'", + ); + } + Ok(_) => panic!("Expected error for Token2022 set_authority policy violation"), + } +} + +#[tokio::test] +async fn test_mint_to_policy_violation() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + let setup = TestAccountSetup::new().await; + + let fee_payer_pubkey = FeePayerTestHelper::get_fee_payer_pubkey(); + let fee_payer_token_account = setup + .create_fee_payer_token_account_spl(&setup.fee_payer_policy_mint.pubkey()) + .await + .expect("Failed to create token account"); + + let malicious_tx = ctx + .transaction_builder() + .with_fee_payer(fee_payer_pubkey) + .with_spl_mint_to( + &setup.fee_payer_policy_mint.pubkey(), + &fee_payer_token_account.pubkey(), + &fee_payer_pubkey, + 1_000_000, + ) + .build() + .await + .expect("Failed to create transaction with mint_to"); + + let result = + ctx.rpc_call::("signTransaction", rpc_params![malicious_tx]).await; + + match result { + Err(error) => { + error.assert_contains_message("Fee payer cannot be used for 'SPL Token MintTo'"); + } + Ok(_) => panic!("Expected error for mint_to policy violation"), + } +} + +#[tokio::test] +async fn test_mint_to_token2022_policy_violation() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + let setup = TestAccountSetup::new().await; + + let fee_payer_pubkey = FeePayerTestHelper::get_fee_payer_pubkey(); + let fee_payer_token_2022_account = setup + .create_fee_payer_token_account_2022(&setup.fee_payer_policy_mint_2022.pubkey()) + .await + .expect("Failed to create token account"); + + let malicious_tx = ctx + .transaction_builder() + .with_fee_payer(fee_payer_pubkey) + .with_token2022_mint_to( + &setup.fee_payer_policy_mint_2022.pubkey(), + &fee_payer_token_2022_account.pubkey(), + &fee_payer_pubkey, + 1_000_000, + ) + .build() + .await + .expect("Failed to create transaction with Token2022 mint_to"); + + let result = + ctx.rpc_call::("signTransaction", rpc_params![malicious_tx]).await; + + match result { + Err(error) => { + error.assert_contains_message("Fee payer cannot be used for 'Token2022 Token MintTo'"); + } + Ok(_) => panic!("Expected error for Token2022 mint_to policy violation"), + } +} + +#[tokio::test] +async fn test_freeze_account_policy_violation() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + let setup = TestAccountSetup::new().await; + + let fee_payer_pubkey = FeePayerTestHelper::get_fee_payer_pubkey(); + let fee_payer_token_account = setup + .create_fee_payer_token_account_spl(&setup.fee_payer_policy_mint.pubkey()) + .await + .expect("Failed to create token account"); + + let malicious_tx = ctx + .transaction_builder() + .with_fee_payer(fee_payer_pubkey) + .with_spl_freeze_account( + &fee_payer_token_account.pubkey(), + &setup.fee_payer_policy_mint.pubkey(), + &fee_payer_pubkey, + ) + .build() + .await + .expect("Failed to create transaction with freeze_account"); + + let result = + ctx.rpc_call::("signTransaction", rpc_params![malicious_tx]).await; + + match result { + Err(error) => { + error.assert_contains_message("Fee payer cannot be used for 'SPL Token FreezeAccount'"); + } + Ok(_) => panic!("Expected error for freeze_account policy violation"), + } +} + +#[tokio::test] +async fn test_freeze_account_token2022_policy_violation() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + let setup = TestAccountSetup::new().await; + + let fee_payer_pubkey = FeePayerTestHelper::get_fee_payer_pubkey(); + let fee_payer_token_2022_account = setup + .create_fee_payer_token_account_2022(&setup.fee_payer_policy_mint_2022.pubkey()) + .await + .expect("Failed to create token account"); + + let malicious_tx = ctx + .transaction_builder() + .with_fee_payer(fee_payer_pubkey) + .with_token2022_freeze_account( + &fee_payer_token_2022_account.pubkey(), + &setup.fee_payer_policy_mint_2022.pubkey(), + &fee_payer_pubkey, + ) + .build() + .await + .expect("Failed to create transaction with Token2022 freeze_account"); + + let result = + ctx.rpc_call::("signTransaction", rpc_params![malicious_tx]).await; + + match result { + Err(error) => { + error.assert_contains_message( + "Fee payer cannot be used for 'Token2022 Token FreezeAccount'", + ); + } + Ok(_) => panic!("Expected error for Token2022 freeze_account policy violation"), + } +} + +#[tokio::test] +async fn test_thaw_account_policy_violation() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + let setup = TestAccountSetup::new().await; + + let fee_payer_pubkey = FeePayerTestHelper::get_fee_payer_pubkey(); + let fee_payer_token_account = setup + .create_fee_payer_token_account_spl(&setup.fee_payer_policy_mint.pubkey()) + .await + .expect("Failed to create token account"); + + // Freeze the account first (directly on-chain, bypassing Kora validator) + let freeze_ix = spl_token::instruction::freeze_account( + &spl_token::id(), + &fee_payer_token_account.pubkey(), + &setup.fee_payer_policy_mint.pubkey(), + &fee_payer_pubkey, + &[], + ) + .expect("Failed to create freeze instruction"); + + let recent_blockhash = + ctx.rpc_client().get_latest_blockhash().await.expect("Failed to get blockhash"); + let freeze_tx = Transaction::new_signed_with_payer( + &[freeze_ix], + Some(&setup.sender_keypair.pubkey()), + &[&setup.sender_keypair, &setup.fee_payer_keypair], + recent_blockhash, + ); + ctx.rpc_client() + .send_and_confirm_transaction(&freeze_tx) + .await + .expect("Failed to freeze account"); + + // Now thaw - fee_payer has authority but policy should reject + let malicious_tx = ctx + .transaction_builder() + .with_fee_payer(fee_payer_pubkey) + .with_spl_thaw_account( + &fee_payer_token_account.pubkey(), + &setup.fee_payer_policy_mint.pubkey(), + &fee_payer_pubkey, + ) + .build() + .await + .expect("Failed to create transaction with thaw_account"); + + let result = + ctx.rpc_call::("signTransaction", rpc_params![malicious_tx]).await; + + match result { + Err(error) => { + error.assert_contains_message("Fee payer cannot be used for 'SPL Token ThawAccount'"); + } + Ok(_) => panic!("Expected error for thaw_account policy violation"), + } +} + +#[tokio::test] +async fn test_thaw_account_token2022_policy_violation() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + let setup = TestAccountSetup::new().await; + + let fee_payer_pubkey = FeePayerTestHelper::get_fee_payer_pubkey(); + let fee_payer_token_2022_account = setup + .create_fee_payer_token_account_2022(&setup.fee_payer_policy_mint_2022.pubkey()) + .await + .expect("Failed to create token account"); + + // Freeze the account first (directly on-chain, bypassing Kora validator) + let freeze_ix = spl_token_2022::instruction::freeze_account( + &spl_token_2022::id(), + &fee_payer_token_2022_account.pubkey(), + &setup.fee_payer_policy_mint_2022.pubkey(), + &fee_payer_pubkey, + &[], + ) + .expect("Failed to create freeze instruction"); + + let recent_blockhash = + ctx.rpc_client().get_latest_blockhash().await.expect("Failed to get blockhash"); + let freeze_tx = Transaction::new_signed_with_payer( + &[freeze_ix], + Some(&setup.sender_keypair.pubkey()), + &[&setup.sender_keypair, &setup.fee_payer_keypair], + recent_blockhash, + ); + ctx.rpc_client() + .send_and_confirm_transaction(&freeze_tx) + .await + .expect("Failed to freeze account"); + + // Now thaw - fee_payer has authority but policy should reject + let malicious_tx = ctx + .transaction_builder() + .with_fee_payer(fee_payer_pubkey) + .with_token2022_thaw_account( + &fee_payer_token_2022_account.pubkey(), + &setup.fee_payer_policy_mint_2022.pubkey(), + &fee_payer_pubkey, + ) + .build() + .await + .expect("Failed to create transaction with Token2022 thaw_account"); + + let result = + ctx.rpc_call::("signTransaction", rpc_params![malicious_tx]).await; + + match result { + Err(error) => { + error.assert_contains_message( + "Fee payer cannot be used for 'Token2022 Token ThawAccount'", + ); + } + Ok(_) => panic!("Expected error for Token2022 thaw_account policy violation"), + } +} diff --git a/tests/rpc/basic_endpoints.rs b/tests/rpc/basic_endpoints.rs index 9e9f1c87..bf0cefd2 100644 --- a/tests/rpc/basic_endpoints.rs +++ b/tests/rpc/basic_endpoints.rs @@ -119,17 +119,78 @@ async fn test_fee_payer_policy_is_present() { .as_object() .expect("Expected fee_payer_policy in validation_config"); - // Validate policy structure - assert!(fee_payer_policy.contains_key("allow_sol_transfers")); - assert!(fee_payer_policy.contains_key("allow_spl_transfers")); - assert!(fee_payer_policy.contains_key("allow_token2022_transfers")); - assert!(fee_payer_policy.contains_key("allow_assign")); - - // Validate default values - assert_eq!(fee_payer_policy["allow_sol_transfers"], true); - assert_eq!(fee_payer_policy["allow_spl_transfers"], true); - assert_eq!(fee_payer_policy["allow_token2022_transfers"], true); - assert_eq!(fee_payer_policy["allow_assign"], true); + // Validate nested policy structure + assert!(fee_payer_policy.contains_key("system")); + assert!(fee_payer_policy.contains_key("spl_token")); + assert!(fee_payer_policy.contains_key("token_2022")); + + // Validate system policy structure + let system = fee_payer_policy["system"].as_object().expect("Expected system policy object"); + assert!(system.contains_key("allow_transfer")); + assert!(system.contains_key("allow_assign")); + assert!(system.contains_key("allow_create_account")); + assert!(system.contains_key("allow_allocate")); + assert!(system.contains_key("nonce")); + assert_eq!(system["allow_transfer"], true); + assert_eq!(system["allow_assign"], true); + assert_eq!(system["allow_create_account"], true); + assert_eq!(system["allow_allocate"], true); + + // Validate nonce nested policy + let nonce = system["nonce"].as_object().expect("Expected nonce policy object"); + assert!(nonce.contains_key("allow_initialize")); + assert!(nonce.contains_key("allow_advance")); + assert!(nonce.contains_key("allow_withdraw")); + assert!(nonce.contains_key("allow_authorize")); + assert!(!nonce.contains_key("allow_upgrade"), "allow_upgrade should not exist"); + assert_eq!(nonce["allow_initialize"], true); + assert_eq!(nonce["allow_advance"], true); + assert_eq!(nonce["allow_withdraw"], true); + assert_eq!(nonce["allow_authorize"], true); + + // Validate spl_token policy structure + let spl_token = + fee_payer_policy["spl_token"].as_object().expect("Expected spl_token policy object"); + assert!(spl_token.contains_key("allow_transfer")); + assert!(spl_token.contains_key("allow_burn")); + assert!(spl_token.contains_key("allow_close_account")); + assert!(spl_token.contains_key("allow_approve")); + assert!(spl_token.contains_key("allow_revoke")); + assert!(spl_token.contains_key("allow_set_authority")); + assert!(spl_token.contains_key("allow_mint_to")); + assert!(spl_token.contains_key("allow_freeze_account")); + assert!(spl_token.contains_key("allow_thaw_account")); + assert_eq!(spl_token["allow_transfer"], true); + assert_eq!(spl_token["allow_burn"], true); + assert_eq!(spl_token["allow_close_account"], true); + assert_eq!(spl_token["allow_approve"], true); + assert_eq!(spl_token["allow_revoke"], true); + assert_eq!(spl_token["allow_set_authority"], true); + assert_eq!(spl_token["allow_mint_to"], true); + assert_eq!(spl_token["allow_freeze_account"], true); + assert_eq!(spl_token["allow_thaw_account"], true); + + // Validate token_2022 policy structure + let token_2022 = + fee_payer_policy["token_2022"].as_object().expect("Expected token_2022 policy object"); + assert!(token_2022.contains_key("allow_transfer")); + assert!(token_2022.contains_key("allow_burn")); + assert!(token_2022.contains_key("allow_close_account")); + assert!(token_2022.contains_key("allow_approve")); + assert!(token_2022.contains_key("allow_revoke")); + assert!(token_2022.contains_key("allow_set_authority")); + assert!(token_2022.contains_key("allow_mint_to")); + assert!(token_2022.contains_key("allow_freeze_account")); + assert!(token_2022.contains_key("allow_thaw_account")); + assert_eq!(token_2022["allow_transfer"], true); + assert_eq!(token_2022["allow_burn"], true); + assert_eq!(token_2022["allow_close_account"], true); + assert_eq!(token_2022["allow_approve"], true); + assert_eq!(token_2022["allow_revoke"], true); + assert_eq!(token_2022["allow_set_authority"], true); + assert_eq!(token_2022["allow_mint_to"], true); + assert_eq!(token_2022["allow_freeze_account"], true); + assert_eq!(token_2022["allow_thaw_account"], true); } /// Test that liveness endpoint is disabled (returns error) diff --git a/tests/src/common/constants.rs b/tests/src/common/constants.rs index 7fdf9ddd..d41e6869 100644 --- a/tests/src/common/constants.rs +++ b/tests/src/common/constants.rs @@ -81,6 +81,13 @@ pub const TEST_INTEREST_BEARING_MINT_KEYPAIR_ENV: &str = "TEST_INTEREST_BEARING_ /// Test transfer hook mint private key environment variable pub const TEST_TRANSFER_HOOK_MINT_KEYPAIR_ENV: &str = "TEST_TRANSFER_HOOK_MINT_KEYPAIR"; +/// Fee payer policy mint private key environment variable +pub const TEST_FEE_PAYER_POLICY_MINT_KEYPAIR_ENV: &str = "TEST_FEE_PAYER_POLICY_MINT_KEYPAIR"; + +/// Fee payer policy mint 2022 private key environment variable +pub const TEST_FEE_PAYER_POLICY_MINT_2022_KEYPAIR_ENV: &str = + "TEST_FEE_PAYER_POLICY_MINT_2022_KEYPAIR"; + /// Payment address keypair environment variable pub const PAYMENT_ADDRESS_KEYPAIR_ENV: &str = "PAYMENT_ADDRESS_KEYPAIR"; diff --git a/tests/src/common/fixtures/auth-test.toml b/tests/src/common/fixtures/auth-test.toml index 4c24eb6c..bc3b16bf 100644 --- a/tests/src/common/fixtures/auth-test.toml +++ b/tests/src/common/fixtures/auth-test.toml @@ -47,13 +47,46 @@ disallowed_accounts = [ ] [validation.fee_payer_policy] -allow_sol_transfers = true -allow_spl_transfers = true -allow_token2022_transfers = true + +[validation.fee_payer_policy.system] +allow_transfer = true allow_assign = true +allow_create_account = true +allow_allocate = true + +[validation.fee_payer_policy.system.nonce] +allow_initialize = true +allow_advance = true +allow_authorize = true +allow_withdraw = true + +[validation.fee_payer_policy.spl_token] +allow_transfer = true +allow_burn = true +allow_close_account = true +allow_approve = true +allow_revoke = true +allow_set_authority = true +allow_mint_to = true +allow_initialize_mint = true +allow_initialize_account = true +allow_initialize_multisig = true +allow_freeze_account = true +allow_thaw_account = true + +[validation.fee_payer_policy.token_2022] +allow_transfer = true allow_burn = true allow_close_account = true allow_approve = true +allow_revoke = true +allow_set_authority = true +allow_mint_to = true +allow_initialize_mint = true +allow_initialize_account = true +allow_initialize_multisig = true +allow_freeze_account = true +allow_thaw_account = true [kora.usage_limit] enabled = false diff --git a/tests/src/common/fixtures/fee-payer-policy-test.toml b/tests/src/common/fixtures/fee-payer-policy-test.toml index 3129f8ce..d541efe2 100644 --- a/tests/src/common/fixtures/fee-payer-policy-test.toml +++ b/tests/src/common/fixtures/fee-payer-policy-test.toml @@ -37,11 +37,15 @@ 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 + "G77RviT7Q7nrr6yx2Hzz4j8rxoMUspqzn9sGy2FZ6Zmt", # Fee payer policy test mint (SPL Token) + "3KgNtQwmrAbxW7HsApDdUH2d8ar9eaPtLZqQdZfhsR6o", # Fee payer policy test mint (Token-2022) ] 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 + "G77RviT7Q7nrr6yx2Hzz4j8rxoMUspqzn9sGy2FZ6Zmt", # Fee payer policy test mint (SPL Token) + "3KgNtQwmrAbxW7HsApDdUH2d8ar9eaPtLZqQdZfhsR6o", # Fee payer policy test mint (Token-2022) ] disallowed_accounts = [ @@ -50,13 +54,46 @@ disallowed_accounts = [ # RESTRICTIVE fee payer policy - ALL SET TO FALSE [validation.fee_payer_policy] -allow_sol_transfers = false -allow_spl_transfers = false -allow_token2022_transfers = false + +[validation.fee_payer_policy.system] +allow_transfer = false allow_assign = false +allow_create_account = false +allow_allocate = false + +[validation.fee_payer_policy.system.nonce] +allow_initialize = false +allow_advance = false +allow_authorize = false +allow_withdraw = false + +[validation.fee_payer_policy.spl_token] +allow_transfer = false +allow_burn = false +allow_close_account = false +allow_approve = false +allow_revoke = false +allow_set_authority = false +allow_mint_to = false +allow_initialize_mint = false +allow_initialize_account = false +allow_initialize_multisig = false +allow_freeze_account = false +allow_thaw_account = false + +[validation.fee_payer_policy.token_2022] +allow_transfer = false allow_burn = false allow_close_account = false allow_approve = false +allow_revoke = false +allow_set_authority = false +allow_mint_to = false +allow_initialize_mint = false +allow_initialize_account = false +allow_initialize_multisig = false +allow_freeze_account = false +allow_thaw_account = false [kora.usage_limit] enabled = false diff --git a/tests/src/common/fixtures/kora-test.toml b/tests/src/common/fixtures/kora-test.toml index e692419e..e53c398e 100644 --- a/tests/src/common/fixtures/kora-test.toml +++ b/tests/src/common/fixtures/kora-test.toml @@ -59,13 +59,46 @@ blocked_account_extensions = [ ] [validation.fee_payer_policy] -allow_sol_transfers = true -allow_spl_transfers = true -allow_token2022_transfers = true + +[validation.fee_payer_policy.system] +allow_transfer = true allow_assign = true +allow_create_account = true +allow_allocate = true + +[validation.fee_payer_policy.system.nonce] +allow_initialize = true +allow_advance = true +allow_authorize = true +allow_withdraw = true + +[validation.fee_payer_policy.spl_token] +allow_transfer = true +allow_burn = true +allow_close_account = true +allow_approve = true +allow_revoke = true +allow_set_authority = true +allow_mint_to = true +allow_initialize_mint = true +allow_initialize_account = true +allow_initialize_multisig = true +allow_freeze_account = true +allow_thaw_account = true + +[validation.fee_payer_policy.token_2022] +allow_transfer = true allow_burn = true allow_close_account = true allow_approve = true +allow_revoke = true +allow_set_authority = true +allow_mint_to = true +allow_initialize_mint = true +allow_initialize_account = true +allow_initialize_multisig = true +allow_freeze_account = true +allow_thaw_account = true [kora.usage_limit] enabled = false diff --git a/tests/src/common/fixtures/paymaster-address-test.toml b/tests/src/common/fixtures/paymaster-address-test.toml index c68264a6..821481ec 100644 --- a/tests/src/common/fixtures/paymaster-address-test.toml +++ b/tests/src/common/fixtures/paymaster-address-test.toml @@ -44,13 +44,46 @@ disallowed_accounts = [ ] [validation.fee_payer_policy] -allow_sol_transfers = true -allow_spl_transfers = true -allow_token2022_transfers = true + +[validation.fee_payer_policy.system] +allow_transfer = true allow_assign = true +allow_create_account = true +allow_allocate = true + +[validation.fee_payer_policy.system.nonce] +allow_initialize = true +allow_advance = true +allow_authorize = true +allow_withdraw = true + +[validation.fee_payer_policy.spl_token] +allow_transfer = true +allow_burn = true +allow_close_account = true +allow_approve = true +allow_revoke = true +allow_set_authority = true +allow_mint_to = true +allow_initialize_mint = true +allow_initialize_account = true +allow_initialize_multisig = true +allow_freeze_account = true +allow_thaw_account = true + +[validation.fee_payer_policy.token_2022] +allow_transfer = true allow_burn = true allow_close_account = true allow_approve = true +allow_revoke = true +allow_set_authority = true +allow_mint_to = true +allow_initialize_mint = true +allow_initialize_account = true +allow_initialize_multisig = true +allow_freeze_account = true +allow_thaw_account = true [kora.usage_limit] enabled = false diff --git a/tests/src/common/helpers.rs b/tests/src/common/helpers.rs index 6874217e..efa5fd07 100644 --- a/tests/src/common/helpers.rs +++ b/tests/src/common/helpers.rs @@ -177,3 +177,33 @@ impl PYUSDTestHelper { Pubkey::from_str(PYUSD_MINT).expect("Invalid PYUSD mint") } } + +pub struct FeePayerPolicyMintTestHelper; + +impl FeePayerPolicyMintTestHelper { + pub fn get_fee_payer_policy_mint_keypair() -> Keypair { + dotenv::dotenv().ok(); + parse_private_key_string( + &std::env::var(TEST_FEE_PAYER_POLICY_MINT_KEYPAIR_ENV) + .expect("TEST_FEE_PAYER_POLICY_MINT_KEYPAIR environment variable is not set"), + ) + .expect("Failed to parse fee payer policy mint private key") + } + + pub fn get_fee_payer_policy_mint_pubkey() -> Pubkey { + Self::get_fee_payer_policy_mint_keypair().pubkey() + } + + pub fn get_fee_payer_policy_mint_2022_keypair() -> Keypair { + dotenv::dotenv().ok(); + parse_private_key_string( + &std::env::var(TEST_FEE_PAYER_POLICY_MINT_2022_KEYPAIR_ENV) + .expect("TEST_FEE_PAYER_POLICY_MINT_2022_KEYPAIR environment variable is not set"), + ) + .expect("Failed to parse fee payer policy mint 2022 private key") + } + + pub fn get_fee_payer_policy_mint_2022_pubkey() -> Pubkey { + Self::get_fee_payer_policy_mint_2022_keypair().pubkey() + } +} diff --git a/tests/src/common/local-keys/fee-payer-policy-mint-local-2022.json b/tests/src/common/local-keys/fee-payer-policy-mint-local-2022.json new file mode 100644 index 00000000..4e2c9c80 --- /dev/null +++ b/tests/src/common/local-keys/fee-payer-policy-mint-local-2022.json @@ -0,0 +1 @@ +[250,234,181,146,247,74,141,120,106,87,80,244,141,1,206,114,165,140,69,247,103,157,208,217,230,155,251,12,136,150,186,182,34,128,90,63,106,96,217,197,225,147,8,117,140,120,62,108,19,244,223,46,51,155,89,235,99,77,82,208,13,28,89,192] \ No newline at end of file diff --git a/tests/src/common/local-keys/fee-payer-policy-mint-local.json b/tests/src/common/local-keys/fee-payer-policy-mint-local.json new file mode 100644 index 00000000..3b68632f --- /dev/null +++ b/tests/src/common/local-keys/fee-payer-policy-mint-local.json @@ -0,0 +1 @@ +[73,103,93,195,36,122,168,249,174,217,150,209,53,55,118,244,42,81,203,245,29,26,23,8,163,41,136,31,160,76,54,121,224,112,15,20,72,28,49,76,144,148,26,77,243,100,250,86,62,153,211,23,4,164,1,172,3,27,56,58,93,255,79,115] \ No newline at end of file diff --git a/tests/src/common/setup.rs b/tests/src/common/setup.rs index 6cecca0d..9dc80051 100644 --- a/tests/src/common/setup.rs +++ b/tests/src/common/setup.rs @@ -20,8 +20,8 @@ use spl_token_2022::{ use std::sync::Arc; use crate::common::{ - FeePayerTestHelper, LookupTableHelper, RecipientTestHelper, SenderTestHelper, - USDCMint2022TestHelper, USDCMintTestHelper, DEFAULT_RPC_URL, + FeePayerPolicyMintTestHelper, FeePayerTestHelper, LookupTableHelper, RecipientTestHelper, + SenderTestHelper, USDCMint2022TestHelper, USDCMintTestHelper, DEFAULT_RPC_URL, }; /// Test account information for outputting to the user @@ -40,6 +40,16 @@ pub struct TestAccountInfo { pub sender_token_2022_account: Pubkey, pub recipient_token_2022_account: Pubkey, pub fee_payer_token_2022_account: Pubkey, + // Fee payer policy mint fields + pub fee_payer_policy_mint_pubkey: Pubkey, + pub fee_payer_policy_sender_token_account: Pubkey, + pub fee_payer_policy_recipient_token_account: Pubkey, + pub fee_payer_policy_fee_payer_token_account: Pubkey, + // Fee payer policy Token 2022 fields + pub fee_payer_policy_mint_2022_pubkey: Pubkey, + pub fee_payer_policy_sender_token_2022_account: Pubkey, + pub fee_payer_policy_recipient_token_2022_account: Pubkey, + pub fee_payer_policy_fee_payer_token_2022_account: Pubkey, // Lookup tables pub allowed_lookup_table: Pubkey, pub disallowed_lookup_table: Pubkey, @@ -54,6 +64,8 @@ pub struct TestAccountSetup { pub recipient_pubkey: Pubkey, pub usdc_mint: Keypair, pub usdc_mint_2022: Keypair, + pub fee_payer_policy_mint: Keypair, + pub fee_payer_policy_mint_2022: Keypair, } impl TestAccountSetup { @@ -80,6 +92,10 @@ impl TestAccountSetup { let usdc_mint = USDCMintTestHelper::get_test_usdc_mint_keypair(); let usdc_mint_2022 = USDCMint2022TestHelper::get_test_usdc_mint_2022_keypair(); + let fee_payer_policy_mint = + FeePayerPolicyMintTestHelper::get_fee_payer_policy_mint_keypair(); + let fee_payer_policy_mint_2022 = + FeePayerPolicyMintTestHelper::get_fee_payer_policy_mint_2022_keypair(); Self { rpc_client, @@ -88,6 +104,8 @@ impl TestAccountSetup { recipient_pubkey, usdc_mint, usdc_mint_2022, + fee_payer_policy_mint, + fee_payer_policy_mint_2022, } } @@ -105,6 +123,12 @@ impl TestAccountSetup { let usdc_mint_2022_pubkey = self.create_usdc_mint_2022().await?; account_infos.usdc_mint_2022_pubkey = usdc_mint_2022_pubkey; + let fee_payer_policy_mint_pubkey = self.create_fee_payer_policy_mint().await?; + account_infos.fee_payer_policy_mint_pubkey = fee_payer_policy_mint_pubkey; + + let fee_payer_policy_mint_2022_pubkey = self.create_fee_payer_policy_mint_2022().await?; + account_infos.fee_payer_policy_mint_2022_pubkey = fee_payer_policy_mint_2022_pubkey; + let (allowed_lookup_table, disallowed_lookup_table, transaction_lookup_table) = self.create_lookup_tables().await?; account_infos.allowed_lookup_table = allowed_lookup_table; @@ -126,6 +150,26 @@ impl TestAccountSetup { account_infos.recipient_token_2022_account = recipient_token_2022_account; account_infos.fee_payer_token_2022_account = fee_payer_token_2022_account; + let ( + fee_payer_policy_sender_token_account, + fee_payer_policy_recipient_token_account, + fee_payer_policy_fee_payer_token_account, + fee_payer_policy_sender_token_2022_account, + fee_payer_policy_recipient_token_2022_account, + fee_payer_policy_fee_payer_token_2022_account, + ) = self.setup_fee_payer_policy_token_accounts().await?; + account_infos.fee_payer_policy_sender_token_account = fee_payer_policy_sender_token_account; + account_infos.fee_payer_policy_recipient_token_account = + fee_payer_policy_recipient_token_account; + account_infos.fee_payer_policy_fee_payer_token_account = + fee_payer_policy_fee_payer_token_account; + account_infos.fee_payer_policy_sender_token_2022_account = + fee_payer_policy_sender_token_2022_account; + account_infos.fee_payer_policy_recipient_token_2022_account = + fee_payer_policy_recipient_token_2022_account; + account_infos.fee_payer_policy_fee_payer_token_2022_account = + fee_payer_policy_fee_payer_token_2022_account; + // Wait for the accounts to be fully initialized (lookup tables, etc.) let await_for_slot = self.rpc_client.get_slot().await? + 30; @@ -445,4 +489,335 @@ impl TestAccountSetup { Ok((allowed_lookup_table, disallowed_lookup_table, transaction_lookup_table)) } + + pub async fn create_fee_payer_policy_mint(&self) -> Result { + if (self.rpc_client.get_account(&self.fee_payer_policy_mint.pubkey()).await).is_ok() { + return Ok(self.fee_payer_policy_mint.pubkey()); + } + + let rent = self + .rpc_client + .get_minimum_balance_for_rent_exemption(spl_token::state::Mint::LEN) + .await?; + + let create_account_instruction = solana_sdk::system_instruction::create_account( + &self.fee_payer_keypair.pubkey(), + &self.fee_payer_policy_mint.pubkey(), + rent, + spl_token::state::Mint::LEN as u64, + &spl_token::id(), + ); + + let initialize_mint_instruction = spl_token::instruction::initialize_mint2( + &spl_token::id(), + &self.fee_payer_policy_mint.pubkey(), + &self.fee_payer_keypair.pubkey(), + Some(&self.fee_payer_keypair.pubkey()), + USDCMintTestHelper::get_test_usdc_mint_decimals(), + )?; + + let recent_blockhash = self.rpc_client.get_latest_blockhash().await?; + + let transaction = Transaction::new_signed_with_payer( + &[create_account_instruction, initialize_mint_instruction], + Some(&self.fee_payer_keypair.pubkey()), + &[&self.fee_payer_keypair, &self.fee_payer_policy_mint], + recent_blockhash, + ); + + self.rpc_client.send_and_confirm_transaction(&transaction).await?; + + Ok(self.fee_payer_policy_mint.pubkey()) + } + + pub async fn create_fee_payer_policy_mint_2022(&self) -> Result { + if (self.rpc_client.get_account(&self.fee_payer_policy_mint_2022.pubkey()).await).is_ok() { + return Ok(self.fee_payer_policy_mint_2022.pubkey()); + } + + let decimals = USDCMintTestHelper::get_test_usdc_mint_decimals(); + + let space = spl_token_2022::extension::ExtensionType::try_calculate_account_len::< + Token2022Mint, + >(&[])?; + + let rent = self.rpc_client.get_minimum_balance_for_rent_exemption(space).await?; + + let create_account_instruction = solana_sdk::system_instruction::create_account( + &self.fee_payer_keypair.pubkey(), + &self.fee_payer_policy_mint_2022.pubkey(), + rent, + space as u64, + &spl_token_2022::id(), + ); + + let initialize_mint_instruction = token_2022_instruction::initialize_mint2( + &spl_token_2022::id(), + &self.fee_payer_policy_mint_2022.pubkey(), + &self.fee_payer_keypair.pubkey(), + Some(&self.fee_payer_keypair.pubkey()), + decimals, + )?; + + let recent_blockhash = self.rpc_client.get_latest_blockhash().await?; + + let transaction = Transaction::new_signed_with_payer( + &[create_account_instruction, initialize_mint_instruction], + Some(&self.fee_payer_keypair.pubkey()), + &[&self.fee_payer_keypair, &self.fee_payer_policy_mint_2022], + recent_blockhash, + ); + + self.rpc_client.send_and_confirm_transaction(&transaction).await?; + + Ok(self.fee_payer_policy_mint_2022.pubkey()) + } + + pub async fn setup_fee_payer_policy_token_accounts( + &self, + ) -> Result<(Pubkey, Pubkey, Pubkey, Pubkey, Pubkey, Pubkey)> { + // SPL Token accounts + let sender_token_account = get_associated_token_address( + &self.sender_keypair.pubkey(), + &self.fee_payer_policy_mint.pubkey(), + ); + let recipient_token_account = get_associated_token_address( + &self.recipient_pubkey, + &self.fee_payer_policy_mint.pubkey(), + ); + let fee_payer_token_account = get_associated_token_address( + &self.fee_payer_keypair.pubkey(), + &self.fee_payer_policy_mint.pubkey(), + ); + + // Token 2022 accounts + let sender_token_2022_account = get_associated_token_address_with_program_id( + &self.sender_keypair.pubkey(), + &self.fee_payer_policy_mint_2022.pubkey(), + &spl_token_2022::id(), + ); + let recipient_token_2022_account = get_associated_token_address_with_program_id( + &self.recipient_pubkey, + &self.fee_payer_policy_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.fee_payer_policy_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.fee_payer_keypair.pubkey(), + &self.sender_keypair.pubkey(), + &self.fee_payer_policy_mint.pubkey(), + &spl_token::id(), + ); + + let create_associated_token_account_instruction_recipient = + spl_associated_token_account::instruction::create_associated_token_account_idempotent( + &self.fee_payer_keypair.pubkey(), + &self.recipient_pubkey, + &self.fee_payer_policy_mint.pubkey(), + &spl_token::id(), + ); + + let create_associated_token_account_instruction_fee_payer = + spl_associated_token_account::instruction::create_associated_token_account_idempotent( + &self.fee_payer_keypair.pubkey(), + &self.fee_payer_keypair.pubkey(), + &self.fee_payer_policy_mint.pubkey(), + &spl_token::id(), + ); + + // Create Token 2022 accounts + let create_token_2022_account_instruction_sender = + spl_associated_token_account::instruction::create_associated_token_account_idempotent( + &self.fee_payer_keypair.pubkey(), + &self.sender_keypair.pubkey(), + &self.fee_payer_policy_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.fee_payer_keypair.pubkey(), + &self.recipient_pubkey, + &self.fee_payer_policy_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.fee_payer_keypair.pubkey(), + &self.fee_payer_keypair.pubkey(), + &self.fee_payer_policy_mint_2022.pubkey(), + &spl_token_2022::id(), + ); + + let recent_blockhash = self.rpc_client.get_latest_blockhash().await?; + + 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( + &all_instructions, + Some(&self.fee_payer_keypair.pubkey()), + &[&self.fee_payer_keypair], + recent_blockhash, + ); + + self.rpc_client.send_and_confirm_transaction(&transaction).await?; + + let mint_amount = + 1_000_000 * 10_u64.pow(USDCMintTestHelper::get_test_usdc_mint_decimals() as u32); + + // Mint regular SPL tokens + self.mint_fee_payer_policy_tokens_to_account(&sender_token_account, mint_amount).await?; + + // Mint Token 2022 tokens + self.mint_fee_payer_policy_tokens_2022_to_account(&sender_token_2022_account, mint_amount) + .await?; + + Ok(( + sender_token_account, + recipient_token_account, + fee_payer_token_account, + sender_token_2022_account, + recipient_token_2022_account, + fee_payer_token_2022_account, + )) + } + + pub async fn mint_fee_payer_policy_tokens_to_account( + &self, + token_account: &Pubkey, + amount: u64, + ) -> Result<()> { + let instruction = token_instruction::mint_to( + &spl_token::id(), + &self.fee_payer_policy_mint.pubkey(), + token_account, + &self.fee_payer_keypair.pubkey(), + &[], + amount, + )?; + + let recent_blockhash = self.rpc_client.get_latest_blockhash().await?; + let transaction = Transaction::new_signed_with_payer( + &[instruction], + Some(&self.fee_payer_keypair.pubkey()), + &[&self.fee_payer_keypair], + recent_blockhash, + ); + + self.rpc_client.send_and_confirm_transaction(&transaction).await?; + Ok(()) + } + + pub async fn mint_fee_payer_policy_tokens_2022_to_account( + &self, + token_account: &Pubkey, + amount: u64, + ) -> Result<()> { + let instruction = token_2022_instruction::mint_to( + &spl_token_2022::id(), + &self.fee_payer_policy_mint_2022.pubkey(), + token_account, + &self.fee_payer_keypair.pubkey(), + &[], + amount, + )?; + + let recent_blockhash = self.rpc_client.get_latest_blockhash().await?; + let transaction = Transaction::new_signed_with_payer( + &[instruction], + Some(&self.fee_payer_keypair.pubkey()), + &[&self.fee_payer_keypair], + recent_blockhash, + ); + + self.rpc_client.send_and_confirm_transaction(&transaction).await?; + Ok(()) + } + + /// Create a new unique token account for the fee payer (not an ATA) + pub async fn create_fee_payer_token_account_spl(&self, mint: &Pubkey) -> Result { + let token_account = Keypair::new(); + + let rent = self + .rpc_client + .get_minimum_balance_for_rent_exemption(spl_token::state::Account::LEN) + .await?; + + let create_account_ix = solana_sdk::system_instruction::create_account( + &self.fee_payer_keypair.pubkey(), + &token_account.pubkey(), + rent, + spl_token::state::Account::LEN as u64, + &spl_token::id(), + ); + + let initialize_account_ix = spl_token::instruction::initialize_account( + &spl_token::id(), + &token_account.pubkey(), + mint, + &self.fee_payer_keypair.pubkey(), + )?; + + let recent_blockhash = self.rpc_client.get_latest_blockhash().await?; + let transaction = Transaction::new_signed_with_payer( + &[create_account_ix, initialize_account_ix], + Some(&self.fee_payer_keypair.pubkey()), + &[&self.fee_payer_keypair, &token_account], + recent_blockhash, + ); + + self.rpc_client.send_and_confirm_transaction(&transaction).await?; + Ok(token_account) + } + + /// Create a new unique token account for the fee payer (Token2022, not an ATA) + pub async fn create_fee_payer_token_account_2022(&self, mint: &Pubkey) -> Result { + let token_account = Keypair::new(); + + let rent = self + .rpc_client + .get_minimum_balance_for_rent_exemption(spl_token_2022::state::Account::LEN) + .await?; + + let create_account_ix = solana_sdk::system_instruction::create_account( + &self.fee_payer_keypair.pubkey(), + &token_account.pubkey(), + rent, + spl_token_2022::state::Account::LEN as u64, + &spl_token_2022::id(), + ); + + let initialize_account_ix = spl_token_2022::instruction::initialize_account( + &spl_token_2022::id(), + &token_account.pubkey(), + mint, + &self.fee_payer_keypair.pubkey(), + )?; + + let recent_blockhash = self.rpc_client.get_latest_blockhash().await?; + let transaction = Transaction::new_signed_with_payer( + &[create_account_ix, initialize_account_ix], + Some(&self.fee_payer_keypair.pubkey()), + &[&self.fee_payer_keypair, &token_account], + recent_blockhash, + ); + + self.rpc_client.send_and_confirm_transaction(&transaction).await?; + Ok(token_account) + } } diff --git a/tests/src/common/transaction.rs b/tests/src/common/transaction.rs index 6457ebb4..90a7cd83 100644 --- a/tests/src/common/transaction.rs +++ b/tests/src/common/transaction.rs @@ -100,6 +100,35 @@ impl TransactionBuilder { self } + /// Add a system assign instruction + pub fn with_system_assign(mut self, account: &Pubkey, owner: &Pubkey) -> Self { + let instruction = solana_system_interface::instruction::assign(account, owner); + self.instructions.push(instruction); + self + } + + /// Add a system create_account instruction + pub fn with_system_create_account( + mut self, + from: &Pubkey, + to: &Pubkey, + lamports: u64, + space: u64, + owner: &Pubkey, + ) -> Self { + let instruction = + solana_system_interface::instruction::create_account(from, to, lamports, space, owner); + self.instructions.push(instruction); + self + } + + /// Add a system allocate instruction + pub fn with_system_allocate(mut self, account: &Pubkey, space: u64) -> Self { + let instruction = solana_system_interface::instruction::allocate(account, space); + self.instructions.push(instruction); + self + } + /// Add an SPL token transfer instruction pub fn with_spl_transfer( mut self, @@ -333,6 +362,184 @@ impl TransactionBuilder { self } + /// Add SPL token revoke instruction + pub fn with_spl_revoke(mut self, token_account: &Pubkey, owner: &Pubkey) -> Self { + let instruction = + spl_token::instruction::revoke(&spl_token::id(), token_account, owner, &[]) + .expect("Failed to create revoke instruction"); + self.instructions.push(instruction); + self + } + + /// Add Token2022 revoke instruction + pub fn with_token2022_revoke(mut self, token_account: &Pubkey, owner: &Pubkey) -> Self { + let instruction = + spl_token_2022::instruction::revoke(&spl_token_2022::id(), token_account, owner, &[]) + .expect("Failed to create Token2022 revoke instruction"); + self.instructions.push(instruction); + self + } + + /// Add SPL token set_authority instruction + pub fn with_spl_set_authority( + mut self, + account: &Pubkey, + new_authority: Option<&Pubkey>, + authority_type: spl_token::instruction::AuthorityType, + current_authority: &Pubkey, + ) -> Self { + let instruction = spl_token::instruction::set_authority( + &spl_token::id(), + account, + new_authority, + authority_type, + current_authority, + &[], + ) + .expect("Failed to create set_authority instruction"); + self.instructions.push(instruction); + self + } + + /// Add Token2022 set_authority instruction + pub fn with_token2022_set_authority( + mut self, + account: &Pubkey, + new_authority: Option<&Pubkey>, + authority_type: spl_token_2022::instruction::AuthorityType, + current_authority: &Pubkey, + ) -> Self { + let instruction = spl_token_2022::instruction::set_authority( + &spl_token_2022::id(), + account, + new_authority, + authority_type, + current_authority, + &[], + ) + .expect("Failed to create Token2022 set_authority instruction"); + self.instructions.push(instruction); + self + } + + /// Add SPL token mint_to instruction + pub fn with_spl_mint_to( + mut self, + mint: &Pubkey, + destination: &Pubkey, + mint_authority: &Pubkey, + amount: u64, + ) -> Self { + let instruction = spl_token::instruction::mint_to( + &spl_token::id(), + mint, + destination, + mint_authority, + &[], + amount, + ) + .expect("Failed to create mint_to instruction"); + self.instructions.push(instruction); + self + } + + /// Add Token2022 mint_to instruction + pub fn with_token2022_mint_to( + mut self, + mint: &Pubkey, + destination: &Pubkey, + mint_authority: &Pubkey, + amount: u64, + ) -> Self { + let instruction = spl_token_2022::instruction::mint_to( + &spl_token_2022::id(), + mint, + destination, + mint_authority, + &[], + amount, + ) + .expect("Failed to create Token2022 mint_to instruction"); + self.instructions.push(instruction); + self + } + + /// Add SPL token freeze_account instruction + pub fn with_spl_freeze_account( + mut self, + token_account: &Pubkey, + mint: &Pubkey, + freeze_authority: &Pubkey, + ) -> Self { + let instruction = spl_token::instruction::freeze_account( + &spl_token::id(), + token_account, + mint, + freeze_authority, + &[], + ) + .expect("Failed to create freeze_account instruction"); + self.instructions.push(instruction); + self + } + + /// Add Token2022 freeze_account instruction + pub fn with_token2022_freeze_account( + mut self, + token_account: &Pubkey, + mint: &Pubkey, + freeze_authority: &Pubkey, + ) -> Self { + let instruction = spl_token_2022::instruction::freeze_account( + &spl_token_2022::id(), + token_account, + mint, + freeze_authority, + &[], + ) + .expect("Failed to create Token2022 freeze_account instruction"); + self.instructions.push(instruction); + self + } + + /// Add SPL token thaw_account instruction + pub fn with_spl_thaw_account( + mut self, + token_account: &Pubkey, + mint: &Pubkey, + freeze_authority: &Pubkey, + ) -> Self { + let instruction = spl_token::instruction::thaw_account( + &spl_token::id(), + token_account, + mint, + freeze_authority, + &[], + ) + .expect("Failed to create thaw_account instruction"); + self.instructions.push(instruction); + self + } + + /// Add Token2022 thaw_account instruction + pub fn with_token2022_thaw_account( + mut self, + token_account: &Pubkey, + mint: &Pubkey, + freeze_authority: &Pubkey, + ) -> Self { + let instruction = spl_token_2022::instruction::thaw_account( + &spl_token_2022::id(), + token_account, + mint, + freeze_authority, + &[], + ) + .expect("Failed to create Token2022 thaw_account instruction"); + 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/src/test_runner/accounts.rs b/tests/src/test_runner/accounts.rs index 452bdc72..192a4140 100644 --- a/tests/src/test_runner/accounts.rs +++ b/tests/src/test_runner/accounts.rs @@ -1,6 +1,7 @@ use crate::common::{ TestAccountInfo, KORA_PRIVATE_KEY_ENV, PAYMENT_ADDRESS_KEYPAIR_ENV, SIGNER_2_KEYPAIR_ENV, TEST_ALLOWED_LOOKUP_TABLE_ADDRESS_ENV, TEST_DISALLOWED_LOOKUP_TABLE_ADDRESS_ENV, + TEST_FEE_PAYER_POLICY_MINT_2022_KEYPAIR_ENV, TEST_FEE_PAYER_POLICY_MINT_KEYPAIR_ENV, TEST_INTEREST_BEARING_MINT_KEYPAIR_ENV, TEST_RECIPIENT_PUBKEY_ENV, TEST_SENDER_KEYPAIR_ENV, TEST_TRANSACTION_LOOKUP_TABLE_ADDRESS_ENV, TEST_TRANSFER_HOOK_MINT_KEYPAIR_ENV, TEST_USDC_MINT_2022_KEYPAIR_ENV, TEST_USDC_MINT_KEYPAIR_ENV, @@ -25,6 +26,14 @@ pub enum AccountFile { SenderToken2022Account, RecipientToken2022Account, FeePayerToken2022Account, + FeePayerPolicyMint, + FeePayerPolicySenderTokenAccount, + FeePayerPolicyRecipientTokenAccount, + FeePayerPolicyFeePayerTokenAccount, + FeePayerPolicyMint2022, + FeePayerPolicySenderToken2022Account, + FeePayerPolicyRecipientToken2022Account, + FeePayerPolicyFeePayerToken2022Account, AllowedLookupTable, DisallowedLookupTable, TransactionLookupTable, @@ -48,6 +57,26 @@ impl AccountFile { Self::SenderToken2022Account => "sender-token-2022-account-local.json", Self::RecipientToken2022Account => "recipient-token-2022-account-local.json", Self::FeePayerToken2022Account => "fee-payer-token-2022-account-local.json", + Self::FeePayerPolicyMint => "fee-payer-policy-mint-local.json", + Self::FeePayerPolicySenderTokenAccount => { + "fee-payer-policy-sender-token-account-local.json" + } + Self::FeePayerPolicyRecipientTokenAccount => { + "fee-payer-policy-recipient-token-account-local.json" + } + Self::FeePayerPolicyFeePayerTokenAccount => { + "fee-payer-policy-fee-payer-token-account-local.json" + } + Self::FeePayerPolicyMint2022 => "fee-payer-policy-mint-local-2022.json", + Self::FeePayerPolicySenderToken2022Account => { + "fee-payer-policy-sender-token-2022-account-local.json" + } + Self::FeePayerPolicyRecipientToken2022Account => { + "fee-payer-policy-recipient-token-2022-account-local.json" + } + Self::FeePayerPolicyFeePayerToken2022Account => { + "fee-payer-policy-fee-payer-token-2022-account-local.json" + } Self::AllowedLookupTable => "allowed-lookup-table-local.json", Self::DisallowedLookupTable => "disallowed-lookup-table-local.json", Self::TransactionLookupTable => "transaction-lookup-table-local.json", @@ -65,6 +94,8 @@ impl AccountFile { Self::Recipient => TEST_RECIPIENT_PUBKEY_ENV, Self::UsdcMint => TEST_USDC_MINT_KEYPAIR_ENV, Self::UsdcMint2022 => TEST_USDC_MINT_2022_KEYPAIR_ENV, + Self::FeePayerPolicyMint => TEST_FEE_PAYER_POLICY_MINT_KEYPAIR_ENV, + Self::FeePayerPolicyMint2022 => TEST_FEE_PAYER_POLICY_MINT_2022_KEYPAIR_ENV, Self::AllowedLookupTable => TEST_ALLOWED_LOOKUP_TABLE_ADDRESS_ENV, Self::DisallowedLookupTable => TEST_DISALLOWED_LOOKUP_TABLE_ADDRESS_ENV, Self::TransactionLookupTable => TEST_TRANSACTION_LOOKUP_TABLE_ADDRESS_ENV, @@ -97,6 +128,14 @@ impl AccountFile { Self::SenderToken2022Account, Self::RecipientToken2022Account, Self::FeePayerToken2022Account, + Self::FeePayerPolicyMint, + Self::FeePayerPolicySenderTokenAccount, + Self::FeePayerPolicyRecipientTokenAccount, + Self::FeePayerPolicyFeePayerTokenAccount, + Self::FeePayerPolicyMint2022, + Self::FeePayerPolicySenderToken2022Account, + Self::FeePayerPolicyRecipientToken2022Account, + Self::FeePayerPolicyFeePayerToken2022Account, Self::AllowedLookupTable, Self::DisallowedLookupTable, Self::TransactionLookupTable, @@ -114,6 +153,8 @@ impl AccountFile { Self::Sender, Self::UsdcMint, Self::UsdcMint2022, + Self::FeePayerPolicyMint, + Self::FeePayerPolicyMint2022, Self::InterestBearingMint, Self::TransferHookMint, Self::Payment, @@ -232,6 +273,30 @@ pub async fn download_accounts( AccountFile::FeePayerToken2022Account .save_account_for_file(client, &test_accounts.fee_payer_token_2022_account) .await?; + AccountFile::FeePayerPolicyMint + .save_account_for_file(client, &test_accounts.fee_payer_policy_mint_pubkey) + .await?; + AccountFile::FeePayerPolicySenderTokenAccount + .save_account_for_file(client, &test_accounts.fee_payer_policy_sender_token_account) + .await?; + AccountFile::FeePayerPolicyRecipientTokenAccount + .save_account_for_file(client, &test_accounts.fee_payer_policy_recipient_token_account) + .await?; + AccountFile::FeePayerPolicyFeePayerTokenAccount + .save_account_for_file(client, &test_accounts.fee_payer_policy_fee_payer_token_account) + .await?; + AccountFile::FeePayerPolicyMint2022 + .save_account_for_file(client, &test_accounts.fee_payer_policy_mint_2022_pubkey) + .await?; + AccountFile::FeePayerPolicySenderToken2022Account + .save_account_for_file(client, &test_accounts.fee_payer_policy_sender_token_2022_account) + .await?; + AccountFile::FeePayerPolicyRecipientToken2022Account + .save_account_for_file(client, &test_accounts.fee_payer_policy_recipient_token_2022_account) + .await?; + AccountFile::FeePayerPolicyFeePayerToken2022Account + .save_account_for_file(client, &test_accounts.fee_payer_policy_fee_payer_token_2022_account) + .await?; AccountFile::AllowedLookupTable .save_account_for_file(client, &test_accounts.allowed_lookup_table) .await?; From 688192383e1bb303fdb822630e745ceee7ac1bfc Mon Sep 17 00:00:00 2001 From: jo <17280917+dev-jodee@users.noreply.github.com> Date: Thu, 23 Oct 2025 09:04:24 -0400 Subject: [PATCH 18/29] chore: Updated Kora signers crate (#237) --- Cargo.lock | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ecbd9d4c..f04e73c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6850,7 +6850,7 @@ dependencies = [ [[package]] name = "solana-signers" version = "0.1.0" -source = "git+https://github.com/solana-foundation/solana-signers#285df1ca312a479282703c3b63af14fb50244f9d" +source = "git+https://github.com/solana-foundation/solana-signers#f98cf96fd451ea4da38c6a425364147a4731fd4d" dependencies = [ "async-trait", "base64 0.22.1", @@ -6866,7 +6866,6 @@ dependencies = [ "solana-sdk 2.3.1", "thiserror 2.0.17", "tokio", - "vaultrs", ] [[package]] From 6b1dd87b320678cac94ed5449913bb0d17c3f9b9 Mon Sep 17 00:00:00 2001 From: jo <17280917+dev-jodee@users.noreply.github.com> Date: Thu, 23 Oct 2025 14:25:47 -0400 Subject: [PATCH 19/29] feat: Enhance payment instruction analysis and transfer fee calculation (#239) - Now support multi transfers for a payment (so user can transfer from 2 different token accounts to pay for the txn fee) - Refactored `has_payment_instruction` to `analyze_payment_instructions`, which now returns a tuple indicating the presence of payment instructions and the total transfer fees. - Improved fee calculation logic for SPL token transfers, including handling for Token2022 fees. - Added logging for overflow scenarios during payment accumulation. - Introduced new tests for analyzing payment instructions, including cases with multiple payments and insufficient funds. - Updated related test cases to reflect the new method and its functionality. --- crates/lib/src/fee/fee.rs | 201 ++++++++++-------- crates/lib/src/token/token.rs | 15 +- .../src/transaction/versioned_transaction.rs | 12 +- tests/payment_address/main.rs | 1 + .../payment_address_multi_payment_tests.rs | 159 ++++++++++++++ .../fixtures/paymaster-address-test.toml | 4 +- 6 files changed, 295 insertions(+), 97 deletions(-) create mode 100644 tests/payment_address/payment_address_multi_payment_tests.rs diff --git a/crates/lib/src/fee/fee.rs b/crates/lib/src/fee/fee.rs index 7c156a62..97be27be 100644 --- a/crates/lib/src/fee/fee.rs +++ b/crates/lib/src/fee/fee.rs @@ -121,49 +121,19 @@ impl FeeConfigUtil { } } - async fn has_payment_instruction( + /// Analyze payment instructions in transaction + /// Returns (has_payment, total_transfer_fees) + async fn analyze_payment_instructions( resolved_transaction: &mut VersionedTransactionResolved, rpc_client: &RpcClient, fee_payer: &Pubkey, - ) -> Result { - let payment_destination = get_config()?.kora.get_payment_address(fee_payer)?; - - for instruction in resolved_transaction - .get_or_parse_spl_instructions()? - .get(&ParsedSPLInstructionType::SplTokenTransfer) - .unwrap_or(&vec![]) - { - if let ParsedSPLInstructionData::SplTokenTransfer { destination_address, .. } = - instruction - { - if Self::get_payment_instruction_info( - rpc_client, - destination_address, - &payment_destination, - false, // Don't skip missing accounts for has_payment_instruction - ) - .await? - .is_some() - { - return Ok(0); - } - } - } - - // For now we estimate the fee for a payment instruction to be hardcoded, simulation / estimation isn't support yet - Ok(ESTIMATED_LAMPORTS_FOR_PAYMENT_INSTRUCTION) - } - - /// Calculate transfer fees for token transfers in the transaction - async fn calculate_transfer_fees( - rpc_client: &RpcClient, - transaction: &mut VersionedTransactionResolved, - fee_payer: &Pubkey, - ) -> Result { + ) -> Result<(bool, u64), KoraError> { let config = get_config()?; let payment_destination = config.kora.get_payment_address(fee_payer)?; + let mut has_payment = false; + let mut total_transfer_fees = 0u64; - let parsed_spl_instructions = transaction.get_or_parse_spl_instructions()?; + let parsed_spl_instructions = resolved_transaction.get_or_parse_spl_instructions()?; for instruction in parsed_spl_instructions .get(&ParsedSPLInstructionType::SplTokenTransfer) @@ -178,39 +148,49 @@ impl FeeConfigUtil { } = instruction { // Check if this is a payment to Kora - // Skip if destination account doesn't exist (not a payment to existing Kora account) - if Self::get_payment_instruction_info( + let payment_info = Self::get_payment_instruction_info( rpc_client, destination_address, &payment_destination, - true, // Skip missing accounts for transfer fee calculation + true, // Skip missing accounts ) - .await? - .is_none() - { - continue; - } - - if let Some(mint_pubkey) = mint { - // Get mint account to calculate transfer fees - let mint_account = - CacheUtil::get_account(rpc_client, mint_pubkey, true).await?; + .await?; - let token_program = - TokenType::get_token_program_from_owner(&mint_account.owner)?; - let mint_state = token_program.unpack_mint(mint_pubkey, &mint_account.data)?; + if payment_info.is_some() { + has_payment = true; + // Calculate Token2022 transfer fees if applicable if *is_2022 { - // For Token2022, check for transfer fees - if let Some(token2022_mint) = - mint_state.as_any().downcast_ref::() - { - let current_epoch = rpc_client.get_epoch_info().await?.epoch; - - if let Some(fee_amount) = - token2022_mint.calculate_transfer_fee(*amount, current_epoch)? + if let Some(mint_pubkey) = mint { + let mint_account = + CacheUtil::get_account(rpc_client, mint_pubkey, true).await?; + + let token_program = + TokenType::get_token_program_from_owner(&mint_account.owner)?; + let mint_state = + token_program.unpack_mint(mint_pubkey, &mint_account.data)?; + + if let Some(token2022_mint) = + mint_state.as_any().downcast_ref::() { - return Ok(fee_amount); + let current_epoch = rpc_client.get_epoch_info().await?.epoch; + + if let Some(fee_amount) = + token2022_mint.calculate_transfer_fee(*amount, current_epoch)? + { + total_transfer_fees = total_transfer_fees + .checked_add(fee_amount) + .ok_or_else(|| { + log::error!( + "Transfer fee accumulation overflow: total={}, new_fee={}", + total_transfer_fees, + fee_amount + ); + KoraError::ValidationError( + "Transfer fee accumulation overflow".to_string(), + ) + })?; + } } } } @@ -218,7 +198,7 @@ impl FeeConfigUtil { } } - Ok(0) + Ok((has_payment, total_transfer_fees)) } async fn estimate_transaction_fee( @@ -250,17 +230,17 @@ impl FeeConfigUtil { ) .await?; - // If the transaction for paying the gasless relayer is not included, but we expect a payment, we need to add the fee for the payment instruction - // for a better approximation of the fee - let fee_for_payment_instruction = if is_payment_required { - FeeConfigUtil::has_payment_instruction(transaction, rpc_client, fee_payer).await? + // Analyze payment instructions (checks if payment exists + calculates Token2022 fees) + let (has_payment, transfer_fee_config_amount) = + FeeConfigUtil::analyze_payment_instructions(transaction, rpc_client, fee_payer).await?; + + // If payment is required but not found, add estimated payment instruction fee + let fee_for_payment_instruction = if is_payment_required && !has_payment { + ESTIMATED_LAMPORTS_FOR_PAYMENT_INSTRUCTION } else { 0 }; - let transfer_fee_config_amount = - FeeConfigUtil::calculate_transfer_fees(rpc_client, transaction, fee_payer).await?; - let total_fee_lamports = base_fee .checked_add(kora_signature_fee) .and_then(|sum| sum.checked_add(fee_payer_outflow)) @@ -913,7 +893,7 @@ mod tests { } #[tokio::test] - async fn test_has_payment_instruction_with_payment() { + async fn test_analyze_payment_instructions_with_payment() { let _m = ConfigMockBuilder::new().build_and_setup(); let cache_ctx = CacheUtil::get_account_context(); cache_ctx.checkpoint(); @@ -945,8 +925,7 @@ mod tests { let mut resolved_transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - // Test: Should return 0 because payment instruction exists - let result = FeeConfigUtil::has_payment_instruction( + let (has_payment, transfer_fees) = FeeConfigUtil::analyze_payment_instructions( &mut resolved_transaction, &mocked_rpc_client, &signer, @@ -954,11 +933,12 @@ mod tests { .await .unwrap(); - assert_eq!(result, 0, "Should return 0 when payment instruction exists"); + assert!(has_payment, "Should detect payment instruction"); + assert_eq!(transfer_fees, 0, "Should have no transfer fees for SPL token"); } #[tokio::test] - async fn test_has_payment_instruction_without_payment() { + async fn test_analyze_payment_instructions_without_payment() { let signer = setup_or_get_test_signer(); setup_or_get_test_config(); let mocked_rpc_client = create_mock_rpc_client_with_account(&Account::default()); @@ -974,8 +954,7 @@ mod tests { let mut resolved_transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - // Test: Should return fee estimate because no payment instruction exists - let result = FeeConfigUtil::has_payment_instruction( + let (has_payment, transfer_fees) = FeeConfigUtil::analyze_payment_instructions( &mut resolved_transaction, &mocked_rpc_client, &signer, @@ -983,14 +962,12 @@ mod tests { .await .unwrap(); - assert_eq!( - result, ESTIMATED_LAMPORTS_FOR_PAYMENT_INSTRUCTION, - "Should return fee estimate when no payment instruction exists" - ); + assert!(!has_payment, "Should not detect payment instruction"); + assert_eq!(transfer_fees, 0, "Should have no transfer fees"); } #[tokio::test] - async fn test_has_payment_instruction_with_spl_transfer_to_different_destination() { + async fn test_analyze_payment_instructions_with_wrong_destination() { let _m = ConfigMockBuilder::new().build_and_setup(); let cache_ctx = CacheUtil::get_account_context(); cache_ctx.checkpoint(); @@ -1023,8 +1000,7 @@ mod tests { let mut resolved_transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); - // Test: Should return fee estimate because SPL transfer is to different destination - let result = FeeConfigUtil::has_payment_instruction( + let (has_payment, transfer_fees) = FeeConfigUtil::analyze_payment_instructions( &mut resolved_transaction, &mocked_rpc_client, &signer, @@ -1032,10 +1008,8 @@ mod tests { .await .unwrap(); - assert_eq!( - result, ESTIMATED_LAMPORTS_FOR_PAYMENT_INSTRUCTION, - "Should return fee estimate when SPL transfer is to different destination" - ); + assert!(!has_payment, "Should not detect payment to wrong destination"); + assert_eq!(transfer_fees, 0, "Should have no transfer fees"); } #[tokio::test] @@ -1141,6 +1115,57 @@ mod tests { ); } + #[tokio::test] + async fn test_analyze_payment_instructions_with_multiple_payments() { + let _m = ConfigMockBuilder::new().build_and_setup(); + let cache_ctx = CacheUtil::get_account_context(); + cache_ctx.checkpoint(); + let signer = setup_or_get_test_signer(); + let mint = Pubkey::new_unique(); + + let mocked_account = create_mock_token_account(&signer, &mint); + let mocked_rpc_client = create_mock_rpc_client_with_account(&mocked_account); + + cache_ctx.expect().times(2).returning(move |_, _, _| Ok(mocked_account.clone())); + + let sender = Keypair::new(); + let sender_token_account = get_associated_token_address(&sender.pubkey(), &mint); + let payment_token_account = get_associated_token_address(&signer, &mint); + + let transfer_1 = TokenProgram::new() + .create_transfer_instruction( + &sender_token_account, + &payment_token_account, + &sender.pubkey(), + 500, + ) + .unwrap(); + + let transfer_2 = TokenProgram::new() + .create_transfer_instruction( + &sender_token_account, + &payment_token_account, + &sender.pubkey(), + 500, + ) + .unwrap(); + + let message = VersionedMessage::Legacy(Message::new(&[transfer_1, transfer_2], None)); + let mut resolved_transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + + let (has_payment, transfer_fees) = FeeConfigUtil::analyze_payment_instructions( + &mut resolved_transaction, + &mocked_rpc_client, + &signer, + ) + .await + .unwrap(); + + assert!(has_payment, "Should detect payment instructions"); + assert_eq!(transfer_fees, 0, "Should have no transfer fees for SPL tokens"); + } + #[tokio::test] async fn test_transaction_fee_util_get_estimate_fee_legacy() { let mocked_rpc_client = RpcMockBuilder::new().with_fee_estimate(7500).build(); diff --git a/crates/lib/src/token/token.rs b/crates/lib/src/token/token.rs index 23bc2f6e..5fa0be6d 100644 --- a/crates/lib/src/token/token.rs +++ b/crates/lib/src/token/token.rs @@ -366,6 +366,7 @@ impl TokenUtil { expected_destination_owner: &Pubkey, ) -> Result { let config = get_config()?; + let mut total_lamport_value = 0u64; for instruction in transaction_resolved .get_or_parse_spl_instructions()? @@ -426,13 +427,19 @@ impl TokenUtil { ) .await?; - if lamport_value >= required_lamports { - return Ok(true); // Payment satisfied - } + total_lamport_value = + total_lamport_value.checked_add(lamport_value).ok_or_else(|| { + log::error!( + "Payment accumulation overflow: total={}, new_payment={}", + total_lamport_value, + lamport_value + ); + KoraError::ValidationError("Payment accumulation overflow".to_string()) + })?; } } - Ok(false) + Ok(total_lamport_value >= required_lamports) } } diff --git a/crates/lib/src/transaction/versioned_transaction.rs b/crates/lib/src/transaction/versioned_transaction.rs index 2935565b..3d2f306a 100644 --- a/crates/lib/src/transaction/versioned_transaction.rs +++ b/crates/lib/src/transaction/versioned_transaction.rs @@ -605,8 +605,10 @@ mod tests { AccountMeta::new_readonly(Pubkey::new_unique(), false), ], ); - let message = - VersionedMessage::Legacy(Message::new(&[instruction.clone()], Some(&keypair.pubkey()))); + let message = VersionedMessage::Legacy(Message::new( + std::slice::from_ref(&instruction), + Some(&keypair.pubkey()), + )); let transaction = VersionedTransaction::try_new(message.clone(), &[&keypair]).unwrap(); let resolved = VersionedTransactionResolved::from_kora_built_transaction(&transaction); @@ -671,8 +673,10 @@ mod tests { &[1, 2, 3], vec![AccountMeta::new(keypair.pubkey(), true)], ); - let message = - VersionedMessage::Legacy(Message::new(&[instruction.clone()], Some(&keypair.pubkey()))); + let message = VersionedMessage::Legacy(Message::new( + std::slice::from_ref(&instruction), + Some(&keypair.pubkey()), + )); let transaction = VersionedTransaction::try_new(message, &[&keypair]).unwrap(); // Mock RPC client that will be used for inner instructions diff --git a/tests/payment_address/main.rs b/tests/payment_address/main.rs index c1fddca1..0eeceac3 100644 --- a/tests/payment_address/main.rs +++ b/tests/payment_address/main.rs @@ -8,6 +8,7 @@ // - Fee payer policy enforcement for payment scenarios mod payment_address_legacy_tests; +mod payment_address_multi_payment_tests; mod payment_address_v0_lut_tests; mod payment_address_v0_tests; diff --git a/tests/payment_address/payment_address_multi_payment_tests.rs b/tests/payment_address/payment_address_multi_payment_tests.rs new file mode 100644 index 00000000..ef763711 --- /dev/null +++ b/tests/payment_address/payment_address_multi_payment_tests.rs @@ -0,0 +1,159 @@ +use crate::common::*; +use jsonrpsee::rpc_params; +use kora_lib::token::{TokenInterface, TokenProgram}; +use solana_sdk::{pubkey::Pubkey, signature::Signer}; +use spl_associated_token_account::get_associated_token_address; +use std::str::FromStr; + +#[tokio::test] +async fn test_sign_transaction_if_paid_with_multiple_payments_legacy() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + let sender = SenderTestHelper::get_test_sender_keypair(); + let payment_address = Pubkey::from_str(TEST_PAYMENT_ADDRESS).unwrap(); + let test_mint = USDCMintTestHelper::get_test_usdc_mint_pubkey(); + + let sender_token_account = get_associated_token_address(&sender.pubkey(), &test_mint); + let payment_address_token_account = get_associated_token_address(&payment_address, &test_mint); + + let token_interface = TokenProgram::new(); + + let required_fee = get_fee_for_default_transaction_in_usdc(); + let payment_1 = required_fee / 2; + let payment_2 = required_fee - payment_1 + 10; + + let payment_instruction_1 = token_interface + .create_transfer_instruction( + &sender_token_account, + &payment_address_token_account, + &sender.pubkey(), + payment_1, + ) + .unwrap(); + + let payment_instruction_2 = token_interface + .create_transfer_instruction( + &sender_token_account, + &payment_address_token_account, + &sender.pubkey(), + payment_2, + ) + .unwrap(); + + let fee_payer = FeePayerTestHelper::get_fee_payer_pubkey(); + + let encoded_tx = ctx + .transaction_builder() + .with_fee_payer(fee_payer) + .with_signer(&sender) + .with_instruction(payment_instruction_1) + .with_instruction(payment_instruction_2) + .build() + .await + .expect("Failed to create signed legacy transaction"); + + let response: serde_json::Value = ctx + .rpc_call("signTransactionIfPaid", rpc_params![encoded_tx]) + .await + .expect("Failed to sign transaction"); + + response.assert_success(); + response.assert_has_field("signed_transaction"); +} + +#[tokio::test] +async fn test_sign_transaction_if_paid_with_multiple_payments_insufficient_legacy() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + let sender = SenderTestHelper::get_test_sender_keypair(); + let payment_address = Pubkey::from_str(TEST_PAYMENT_ADDRESS).unwrap(); + let test_mint = USDCMintTestHelper::get_test_usdc_mint_pubkey(); + + let sender_token_account = get_associated_token_address(&sender.pubkey(), &test_mint); + let payment_address_token_account = get_associated_token_address(&payment_address, &test_mint); + + let token_interface = TokenProgram::new(); + + let required_fee = get_fee_for_default_transaction_in_usdc(); + let payment_1 = required_fee / 3; + let payment_2 = required_fee / 3; + + let payment_instruction_1 = token_interface + .create_transfer_instruction( + &sender_token_account, + &payment_address_token_account, + &sender.pubkey(), + payment_1, + ) + .unwrap(); + + let payment_instruction_2 = token_interface + .create_transfer_instruction( + &sender_token_account, + &payment_address_token_account, + &sender.pubkey(), + payment_2, + ) + .unwrap(); + + let fee_payer = FeePayerTestHelper::get_fee_payer_pubkey(); + + let encoded_tx = ctx + .transaction_builder() + .with_fee_payer(fee_payer) + .with_signer(&sender) + .with_instruction(payment_instruction_1) + .with_instruction(payment_instruction_2) + .build() + .await + .expect("Failed to create signed legacy transaction"); + + let response: Result = + ctx.rpc_call("signTransactionIfPaid", rpc_params![encoded_tx]).await; + + assert!(response.is_err(), "Should fail with insufficient payment"); +} + +#[tokio::test] +async fn test_sign_transaction_if_paid_with_multiple_sources_legacy() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + let sender = SenderTestHelper::get_test_sender_keypair(); + let payment_address = Pubkey::from_str(TEST_PAYMENT_ADDRESS).unwrap(); + let test_mint = USDCMintTestHelper::get_test_usdc_mint_pubkey(); + let test_mint2 = USDCMint2022TestHelper::get_test_usdc_mint_2022_pubkey(); + + let required_fee = get_fee_for_default_transaction_in_usdc(); + let payment_amount = required_fee / 2; + // We need to add 50 lamports, because on that mint for 2022 the transfer fee config is 1% + let payment_amount_2 = payment_amount + 50; + + let fee_payer = FeePayerTestHelper::get_fee_payer_pubkey(); + + let encoded_tx = ctx + .transaction_builder() + .with_fee_payer(fee_payer) + .with_signer(&sender) + .with_spl_transfer_checked( + &test_mint, + &sender.pubkey(), + &payment_address, + payment_amount, + 6, + ) + .with_spl_token_2022_transfer_checked( + &test_mint2, + &sender.pubkey(), + &payment_address, + payment_amount_2, + 6, + ) + .build() + .await + .expect("Failed to create signed legacy transaction"); + + let response: serde_json::Value = ctx + .rpc_call("signTransactionIfPaid", rpc_params![encoded_tx]) + .await + .expect("Failed to sign transaction"); + + response.assert_success(); + response.assert_has_field("signed_transaction"); +} diff --git a/tests/src/common/fixtures/paymaster-address-test.toml b/tests/src/common/fixtures/paymaster-address-test.toml index 821481ec..a09ccd77 100644 --- a/tests/src/common/fixtures/paymaster-address-test.toml +++ b/tests/src/common/fixtures/paymaster-address-test.toml @@ -34,9 +34,11 @@ allowed_programs = [ ] allowed_tokens = [ "9BgeTKqmFsPVnfYscfM6NvsgmZxei7XfdciShQ6D3bxJ", # Test USDC mint for local testing + "95kSi2m5MDiKAs8bucgzengMTP5M5FiQnJps9duYcmfG", # Test USDC 2022 mint for local testing ] allowed_spl_paid_tokens = [ "9BgeTKqmFsPVnfYscfM6NvsgmZxei7XfdciShQ6D3bxJ", # Test USDC mint for local testing + "95kSi2m5MDiKAs8bucgzengMTP5M5FiQnJps9duYcmfG", # Test USDC 2022 mint for local testing ] disallowed_accounts = [ @@ -89,4 +91,4 @@ allow_thaw_account = true enabled = false cache_url = "redis://redis:6379" max_transactions = 2 -fallback_if_unavailable = false \ No newline at end of file +fallback_if_unavailable = false From 5af3b806e2eb7097842e64721ec2d19b012715a0 Mon Sep 17 00:00:00 2001 From: jo <17280917+dev-jodee@users.noreply.github.com> Date: Mon, 27 Oct 2025 16:58:37 -0400 Subject: [PATCH 20/29] Bugfix/audit report missing fixes (#240) * bugfix: Post audit report fixes - Removed outdated dependencies such as `jup-ag` and updated versions for several packages in `Cargo.lock`. - Enhanced `ConfigValidator` to include warnings for security risks related to token extensions and authentication configurations. - Updated fee payer policies to default to more secure settings, ensuring better protection against unauthorized transfers. - Refactored several structs to implement `Default` for improved initialization. - Adjusted transaction fee response types for consistency and accuracy in handling fees. - Use of fixed point decimals for price manipulation - Constant equal for API KEY comparaison * Remove get all signers function for added safety --- Cargo.lock | 705 ++++++------------ Cargo.toml | 4 +- crates/lib/Cargo.toml | 4 +- crates/lib/src/admin/token_util.rs | 8 +- crates/lib/src/config.rs | 69 +- crates/lib/src/error.rs | 4 - crates/lib/src/fee/fee.rs | 2 +- crates/lib/src/mod.rs | 2 +- crates/lib/src/oracle/jupiter.rs | 67 +- crates/lib/src/oracle/oracle.rs | 9 +- crates/lib/src/oracle/utils.rs | 8 +- crates/lib/src/rpc_server/auth.rs | 12 +- .../method/estimate_transaction_fee.rs | 2 +- .../lib/src/rpc_server/method/get_config.rs | 60 +- .../method/sign_transaction_if_paid.rs | 4 +- .../rpc_server/method/transfer_transaction.rs | 6 +- crates/lib/src/signer/pool.rs | 5 - crates/lib/src/state.rs | 8 +- crates/lib/src/token/token.rs | 123 +-- crates/lib/src/usage_limit/usage_tracker.rs | 10 +- crates/lib/src/validator/account_validator.rs | 9 +- crates/lib/src/validator/cache_validator.rs | 69 +- crates/lib/src/validator/config_validator.rs | 80 +- .../src/validator/transaction_validator.rs | 32 +- sdks/ts/test/integration.test.ts | 1 - tests/Cargo.toml | 2 + tests/external/jupiter_integration.rs | 28 +- 27 files changed, 599 insertions(+), 734 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f04e73c9..f7c163dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -385,12 +385,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "assert_matches" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" - [[package]] name = "async-channel" version = "1.9.0" @@ -577,6 +571,18 @@ dependencies = [ "serde", ] +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake3" version = "1.8.2" @@ -609,16 +615,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "borsh" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15bf3650200d8bffa99015595e10f1fbd17de07abbc25bb067da79e769939bfa" -dependencies = [ - "borsh-derive 0.9.3", - "hashbrown 0.11.2", -] - [[package]] name = "borsh" version = "0.10.4" @@ -639,27 +635,14 @@ dependencies = [ "cfg_aliases", ] -[[package]] -name = "borsh-derive" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6441c552f230375d18e3cc377677914d2ca2b0d36e52129fe15450a2dce46775" -dependencies = [ - "borsh-derive-internal 0.9.3", - "borsh-schema-derive-internal 0.9.3", - "proc-macro-crate 0.1.5", - "proc-macro2", - "syn 1.0.109", -] - [[package]] name = "borsh-derive" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "831213f80d9423998dd696e2c5345aba6be7a0bd8cd19e31c5243e13df1cef89" dependencies = [ - "borsh-derive-internal 0.10.4", - "borsh-schema-derive-internal 0.10.4", + "borsh-derive-internal", + "borsh-schema-derive-internal", "proc-macro-crate 0.1.5", "proc-macro2", "syn 1.0.109", @@ -678,17 +661,6 @@ dependencies = [ "syn 2.0.104", ] -[[package]] -name = "borsh-derive-internal" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5449c28a7b352f2d1e592a8a28bf139bc71afb0764a14f3c02500935d8c44065" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "borsh-derive-internal" version = "0.10.4" @@ -700,17 +672,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "borsh-schema-derive-internal" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdbd5696d8bfa21d53d9fe39a714a18538bad11492a42d066dbbc395fb1951c0" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "borsh-schema-derive-internal" version = "0.10.4" @@ -764,12 +725,6 @@ dependencies = [ "alloc-stdlib", ] -[[package]] -name = "bs58" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" - [[package]] name = "bs58" version = "0.5.1" @@ -805,6 +760,28 @@ dependencies = [ "serde", ] +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bytemuck" version = "1.23.2" @@ -1797,6 +1774,12 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.31" @@ -1902,7 +1885,6 @@ version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ - "serde", "typenum", "version_check", "zeroize", @@ -2091,15 +2073,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "hashbrown" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" -dependencies = [ - "ahash 0.7.8", -] - [[package]] name = "hashbrown" version = "0.12.3" @@ -2368,19 +2341,6 @@ dependencies = [ "webpki-roots 1.0.2", ] -[[package]] -name = "hyper-tls" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" -dependencies = [ - "bytes", - "hyper 0.14.32", - "native-tls", - "tokio", - "tokio-native-tls", -] - [[package]] name = "hyper-tls" version = "0.6.0" @@ -2416,7 +2376,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2 0.6.0", - "system-configuration 0.6.1", + "system-configuration", "tokio", "tower-service", "tracing", @@ -2923,23 +2883,6 @@ dependencies = [ "jsonrpsee-types", ] -[[package]] -name = "jup-ag" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c22a3dad50370828645e3346c1d2632aa68543287d7d4c7cba5de5720467dad" -dependencies = [ - "base64 0.13.1", - "bincode", - "itertools 0.10.5", - "reqwest 0.11.27", - "serde", - "serde_json", - "solana-sdk 1.10.0", - "thiserror 1.0.69", - "tokio", -] - [[package]] name = "kaigan" version = "0.2.6" @@ -2985,7 +2928,7 @@ dependencies = [ "base64 0.22.1", "bincode", "borsh 1.5.7", - "bs58 0.5.1", + "bs58", "chrono", "clap", "config", @@ -3003,7 +2946,6 @@ dependencies = [ "hyper 1.6.0", "jsonrpsee", "jsonrpsee-core", - "jup-ag", "log", "mockall", "mockito", @@ -3014,7 +2956,9 @@ dependencies = [ "rand 0.9.2", "redis", "regex", - "reqwest 0.12.23", + "reqwest", + "rust_decimal", + "rust_decimal_macros", "serde", "serde_json", "serial_test", @@ -3023,8 +2967,8 @@ dependencies = [ "solana-client", "solana-commitment-config", "solana-message", - "solana-program 2.3.0", - "solana-sdk 2.3.1", + "solana-program", + "solana-sdk", "solana-signers", "solana-system-interface", "solana-transaction-status", @@ -3033,6 +2977,7 @@ dependencies = [ "spl-pod", "spl-token 7.0.0", "spl-token-2022 8.0.1", + "subtle", "tempfile", "thiserror 1.0.69", "tokio", @@ -3405,17 +3350,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" -[[package]] -name = "num-derive" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "num-derive" version = "0.4.2" @@ -3661,15 +3595,6 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" -[[package]] -name = "pbkdf2" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271779f35b581956db91a3e55737327a03aa051e90b1c47aeb189508533adfd7" -dependencies = [ - "digest 0.10.7", -] - [[package]] name = "pbkdf2" version = "0.11.0" @@ -3986,6 +3911,26 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "qstring" version = "0.7.2" @@ -4082,6 +4027,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.7.3" @@ -4301,43 +4252,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] -name = "reqwest" -version = "0.11.27" +name = "rend" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" dependencies = [ - "base64 0.21.7", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2 0.3.27", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.32", - "hyper-tls 0.5.0", - "ipnet", - "js-sys", - "log", - "mime", - "native-tls", - "once_cell", - "percent-encoding", - "pin-project-lite", - "rustls-pemfile", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper 0.1.2", - "system-configuration 0.5.1", - "tokio", - "tokio-native-tls", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "winreg", + "bytecheck", ] [[package]] @@ -4359,7 +4279,7 @@ dependencies = [ "http-body-util", "hyper 1.6.0", "hyper-rustls 0.27.7", - "hyper-tls 0.6.0", + "hyper-tls", "hyper-util", "js-sys", "log", @@ -4373,7 +4293,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 1.0.2", + "sync_wrapper", "tokio", "tokio-native-tls", "tokio-rustls 0.26.2", @@ -4397,7 +4317,7 @@ dependencies = [ "anyhow", "async-trait", "http 1.3.1", - "reqwest 0.12.23", + "reqwest", "serde", "thiserror 1.0.69", "tower-service", @@ -4427,6 +4347,35 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "ron" version = "0.7.1" @@ -4448,6 +4397,32 @@ dependencies = [ "ordered-multimap", ] +[[package]] +name = "rust_decimal" +version = "1.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" +dependencies = [ + "arrayvec", + "borsh 1.5.7", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "rust_decimal_macros" +version = "1.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8c0cb48f413ebe24dc2d148788e0efbe09ba3e011d9277162f2eaf8e1069a3" +dependencies = [ + "quote", + "syn 2.0.104", +] + [[package]] name = "rustc-demangle" version = "0.1.26" @@ -4494,7 +4469,7 @@ dependencies = [ "async-trait", "bytes", "http 1.3.1", - "reqwest 0.12.23", + "reqwest", "rustify_derive", "serde", "serde_json", @@ -4718,6 +4693,12 @@ version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "sec1" version = "0.7.3" @@ -5037,6 +5018,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "similar" version = "2.7.0" @@ -5130,7 +5117,7 @@ dependencies = [ "Inflector", "base64 0.22.1", "bincode", - "bs58 0.5.1", + "bs58", "bv", "serde", "serde_derive", @@ -5171,7 +5158,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59f2101f4cc33e3fbfc8d1d23ea35d8532d6f1fa6a7c7081742e886f98f33126" dependencies = [ "base64 0.22.1", - "bs58 0.5.1", + "bs58", "serde", "serde_derive", "serde_json", @@ -5354,7 +5341,7 @@ dependencies = [ "serde", "serde_derive", "solana-sdk-ids", - "solana-sdk-macro 2.2.1", + "solana-sdk-macro", "solana-sysvar-id", ] @@ -5402,7 +5389,7 @@ dependencies = [ "borsh 0.10.4", "kaigan", "serde", - "solana-program 2.3.0", + "solana-program", ] [[package]] @@ -5517,7 +5504,7 @@ dependencies = [ "serde_derive", "solana-hash", "solana-sdk-ids", - "solana-sdk-macro 2.2.1", + "solana-sdk-macro", "solana-sysvar-id", ] @@ -5541,7 +5528,7 @@ dependencies = [ "serde", "serde_derive", "solana-sdk-ids", - "solana-sdk-macro 2.2.1", + "solana-sdk-macro", "solana-sysvar-id", ] @@ -5622,37 +5609,6 @@ dependencies = [ "solana-native-token", ] -[[package]] -name = "solana-frozen-abi" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f704637b29f1d58b819601efede8eff0998ec10381cb96796dacfe4cfea5581" -dependencies = [ - "bs58 0.4.0", - "bv", - "generic-array", - "log", - "memmap2", - "rustc_version", - "serde", - "serde_derive", - "sha2 0.10.9", - "solana-frozen-abi-macro", - "thiserror 1.0.69", -] - -[[package]] -name = "solana-frozen-abi-macro" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96bf045e938c042c59739ba3a77bf1d25cb7cf073bbf3690cc2d56c7cff27ba2" -dependencies = [ - "proc-macro2", - "quote", - "rustc_version", - "syn 1.0.109", -] - [[package]] name = "solana-genesis-config" version = "2.3.0" @@ -5672,7 +5628,7 @@ dependencies = [ "solana-hash", "solana-inflation", "solana-keypair", - "solana-logger 2.3.1", + "solana-logger", "solana-poh-config", "solana-pubkey", "solana-rent", @@ -5796,7 +5752,7 @@ dependencies = [ "serde", "serde_derive", "solana-sdk-ids", - "solana-sdk-macro 2.2.1", + "solana-sdk-macro", "solana-sysvar-id", ] @@ -5844,17 +5800,6 @@ dependencies = [ "solana-system-interface", ] -[[package]] -name = "solana-logger" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d97a6f07b5068fdd761b4c5c4ddc1d6cdf5e234abc8f7ed0bf38b46fa4b1eba" -dependencies = [ - "env_logger 0.9.3", - "lazy_static", - "log", -] - [[package]] name = "solana-logger" version = "2.3.1" @@ -5906,7 +5851,7 @@ dependencies = [ "crossbeam-channel", "gethostname", "log", - "reqwest 0.12.23", + "reqwest", "solana-cluster-type", "solana-sha256-hasher", "solana-time-utils", @@ -6085,48 +6030,6 @@ dependencies = [ "solana-signer", ] -[[package]] -name = "solana-program" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "888f50c71dc45a528cb63df960e657601fe6fa3d643159d93ebff1dd1cc00b63" -dependencies = [ - "base64 0.13.1", - "bincode", - "bitflags 1.3.2", - "blake3", - "borsh 0.9.3", - "borsh-derive 0.9.3", - "bs58 0.4.0", - "bv", - "bytemuck", - "console_error_panic_hook", - "console_log", - "curve25519-dalek 3.2.0", - "getrandom 0.1.16", - "itertools 0.10.5", - "js-sys", - "lazy_static", - "libsecp256k1", - "log", - "num-derive 0.3.3", - "num-traits", - "parking_lot", - "rand 0.7.3", - "rustc_version", - "rustversion", - "serde", - "serde_bytes", - "serde_derive", - "sha2 0.10.9", - "sha3", - "solana-frozen-abi", - "solana-frozen-abi-macro", - "solana-sdk-macro 1.10.0", - "thiserror 1.0.69", - "wasm-bindgen", -] - [[package]] name = "solana-program" version = "2.3.0" @@ -6137,7 +6040,7 @@ dependencies = [ "blake3", "borsh 0.10.4", "borsh 1.5.7", - "bs58 0.5.1", + "bs58", "bytemuck", "console_error_panic_hook", "console_log", @@ -6146,7 +6049,7 @@ dependencies = [ "log", "memoffset", "num-bigint 0.4.6", - "num-derive 0.4.2", + "num-derive", "num-traits", "rand 0.8.5", "serde", @@ -6189,7 +6092,7 @@ dependencies = [ "solana-rent", "solana-sanitize", "solana-sdk-ids", - "solana-sdk-macro 2.2.1", + "solana-sdk-macro", "solana-secp256k1-recover", "solana-serde-varint", "solana-serialize-utils", @@ -6370,7 +6273,7 @@ dependencies = [ "serde", "serde_derive", "solana-sdk-ids", - "solana-sdk-macro 2.2.1", + "solana-sdk-macro", "solana-sysvar-id", ] @@ -6432,11 +6335,11 @@ dependencies = [ "async-trait", "base64 0.22.1", "bincode", - "bs58 0.5.1", + "bs58", "futures", "indicatif", "log", - "reqwest 0.12.23", + "reqwest", "reqwest-middleware", "semver", "serde", @@ -6471,7 +6374,7 @@ checksum = "5a1be31922f97505007ccf969828b34e8dc43ce434a17f970b0edea8f0e66777" dependencies = [ "anyhow", "jsonrpc-core", - "reqwest 0.12.23", + "reqwest", "reqwest-middleware", "serde", "serde_derive", @@ -6509,7 +6412,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e82a9b71f023a4bd511088f22e3c1f0e226a6e2e94b0656776509f234dd223a" dependencies = [ "base64 0.22.1", - "bs58 0.5.1", + "bs58", "semver", "serde", "serde_derive", @@ -6534,57 +6437,6 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61f1bc1357b8188d9c4a3af3fc55276e56987265eb7ad073ae6f8180ee54cecf" -[[package]] -name = "solana-sdk" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd094e3c7213be0cf9b8e4452705a826f78591aaa1e33be93d9ecfc19e69aa15" -dependencies = [ - "assert_matches", - "base64 0.13.1", - "bincode", - "bitflags 1.3.2", - "borsh 0.9.3", - "bs58 0.4.0", - "bytemuck", - "byteorder", - "chrono", - "derivation-path", - "digest 0.10.7", - "ed25519-dalek", - "ed25519-dalek-bip32", - "generic-array", - "hmac 0.12.1", - "itertools 0.10.5", - "js-sys", - "lazy_static", - "libsecp256k1", - "log", - "memmap2", - "num-derive 0.3.3", - "num-traits", - "pbkdf2 0.10.1", - "qstring", - "rand 0.7.3", - "rand_chacha 0.2.2", - "rustc_version", - "rustversion", - "serde", - "serde_bytes", - "serde_derive", - "serde_json", - "sha2 0.10.9", - "sha3", - "solana-frozen-abi", - "solana-frozen-abi-macro", - "solana-logger 1.10.0", - "solana-program 1.10.0", - "solana-sdk-macro 1.10.0", - "thiserror 1.0.69", - "uriparse", - "wasm-bindgen", -] - [[package]] name = "solana-sdk" version = "2.3.1" @@ -6592,7 +6444,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cc0e4a7635b902791c44b6581bfb82f3ada32c5bc0929a64f39fe4bb384c86a" dependencies = [ "bincode", - "bs58 0.5.1", + "bs58", "getrandom 0.1.16", "js-sys", "serde", @@ -6624,7 +6476,7 @@ dependencies = [ "solana-precompile-error", "solana-precompiles", "solana-presigner", - "solana-program 2.3.0", + "solana-program", "solana-program-memory", "solana-pubkey", "solana-quic-definitions", @@ -6634,7 +6486,7 @@ dependencies = [ "solana-reward-info", "solana-sanitize", "solana-sdk-ids", - "solana-sdk-macro 2.2.1", + "solana-sdk-macro", "solana-secp256k1-program", "solana-secp256k1-recover", "solana-secp256r1-program", @@ -6665,26 +6517,13 @@ dependencies = [ "solana-pubkey", ] -[[package]] -name = "solana-sdk-macro" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2388e9b1690e83413393f3e20d98551cbebae9d9385397600d25ae9992873736" -dependencies = [ - "bs58 0.4.0", - "proc-macro2", - "quote", - "rustversion", - "syn 1.0.109", -] - [[package]] name = "solana-sdk-macro" version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86280da8b99d03560f6ab5aca9de2e38805681df34e0bb8f238e69b29433b9df" dependencies = [ - "bs58 0.5.1", + "bs58", "proc-macro2", "quote", "syn 2.0.104", @@ -6757,7 +6596,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36187af2324f079f65a675ec22b31c24919cb4ac22c79472e85d819db9bbbc15" dependencies = [ "hmac 0.12.1", - "pbkdf2 0.11.0", + "pbkdf2", "sha2 0.10.9", ] @@ -6855,15 +6694,15 @@ dependencies = [ "async-trait", "base64 0.22.1", "bincode", - "bs58 0.5.1", + "bs58", "chrono", "hex", "log", "p256", - "reqwest 0.12.23", + "reqwest", "serde", "serde_json", - "solana-sdk 2.3.1", + "solana-sdk", "thiserror 2.0.17", "tokio", ] @@ -7039,7 +6878,7 @@ dependencies = [ "solana-rent", "solana-sanitize", "solana-sdk-ids", - "solana-sdk-macro 2.2.1", + "solana-sdk-macro", "solana-slot-hashes", "solana-slot-history", "solana-stake-interface", @@ -7221,7 +7060,7 @@ dependencies = [ "base64 0.22.1", "bincode", "borsh 1.5.7", - "bs58 0.5.1", + "bs58", "log", "serde", "serde_derive", @@ -7262,7 +7101,7 @@ checksum = "9e91068d54435121280c4a2f1c280d8d18381e3ccf54057c4530f40f26c2be1c" dependencies = [ "base64 0.22.1", "bincode", - "bs58 0.5.1", + "bs58", "serde", "serde_derive", "serde_json", @@ -7321,7 +7160,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b80d57478d6599d30acc31cc5ae7f93ec2361a06aefe8ea79bc81739a08af4c3" dependencies = [ "bincode", - "num-derive 0.4.2", + "num-derive", "num-traits", "serde", "serde_derive", @@ -7353,7 +7192,7 @@ dependencies = [ "itertools 0.12.1", "js-sys", "merlin", - "num-derive 0.4.2", + "num-derive", "num-traits", "rand 0.8.5", "serde", @@ -7400,9 +7239,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76fee7d65013667032d499adc3c895e286197a35a0d3a4643c80e7fd3e9969e3" dependencies = [ "borsh 1.5.7", - "num-derive 0.4.2", + "num-derive", "num-traits", - "solana-program 2.3.0", + "solana-program", "spl-associated-token-account-client", "spl-token 7.0.0", "spl-token-2022 6.0.0", @@ -7416,9 +7255,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae179d4a26b3c7a20c839898e6aed84cb4477adf108a366c95532f058aea041b" dependencies = [ "borsh 1.5.7", - "num-derive 0.4.2", + "num-derive", "num-traits", - "solana-program 2.3.0", + "solana-program", "spl-associated-token-account-client", "spl-token 8.0.0", "spl-token-2022 8.0.1", @@ -7478,7 +7317,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce0f668975d2b0536e8a8fd60e56a05c467f06021dae037f1d0cfed0de2e231d" dependencies = [ "bytemuck", - "solana-program 2.3.0", + "solana-program", "solana-zk-sdk", "spl-pod", "spl-token-confidential-transfer-proof-extraction 0.2.1", @@ -7540,7 +7379,7 @@ dependencies = [ "borsh 1.5.7", "bytemuck", "bytemuck_derive", - "num-derive 0.4.2", + "num-derive", "num-traits", "solana-decode-error", "solana-msg", @@ -7557,9 +7396,9 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d39b5186f42b2b50168029d81e58e800b690877ef0b30580d107659250da1d1" dependencies = [ - "num-derive 0.4.2", + "num-derive", "num-traits", - "solana-program 2.3.0", + "solana-program", "spl-program-error-derive 0.4.1", "thiserror 1.0.69", ] @@ -7570,7 +7409,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cdebc8b42553070b75aa5106f071fef2eb798c64a7ec63375da4b1f058688c6" dependencies = [ - "num-derive 0.4.2", + "num-derive", "num-traits", "solana-decode-error", "solana-msg", @@ -7610,7 +7449,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd99ff1e9ed2ab86e3fd582850d47a739fec1be9f4661cba1782d3a0f26805f3" dependencies = [ "bytemuck", - "num-derive 0.4.2", + "num-derive", "num-traits", "solana-account-info", "solana-decode-error", @@ -7632,7 +7471,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1408e961215688715d5a1063cbdcf982de225c45f99c82b4f7d7e1dd22b998d7" dependencies = [ "bytemuck", - "num-derive 0.4.2", + "num-derive", "num-traits", "solana-account-info", "solana-decode-error", @@ -7655,10 +7494,10 @@ checksum = "ed320a6c934128d4f7e54fe00e16b8aeaecf215799d060ae14f93378da6dc834" dependencies = [ "arrayref", "bytemuck", - "num-derive 0.4.2", + "num-derive", "num-traits", "num_enum", - "solana-program 2.3.0", + "solana-program", "thiserror 1.0.69", ] @@ -7670,7 +7509,7 @@ checksum = "053067c6a82c705004f91dae058b11b4780407e9ccd6799dc9e7d0fab5f242da" dependencies = [ "arrayref", "bytemuck", - "num-derive 0.4.2", + "num-derive", "num-traits", "num_enum", "solana-account-info", @@ -7698,10 +7537,10 @@ checksum = "5b27f7405010ef816587c944536b0eafbcc35206ab6ba0f2ca79f1d28e488f4f" dependencies = [ "arrayref", "bytemuck", - "num-derive 0.4.2", + "num-derive", "num-traits", "num_enum", - "solana-program 2.3.0", + "solana-program", "solana-security-txt", "solana-zk-sdk", "spl-elgamal-registry 0.1.1", @@ -7726,7 +7565,7 @@ checksum = "31f0dfbb079eebaee55e793e92ca5f433744f4b71ee04880bfd6beefba5973e5" dependencies = [ "arrayref", "bytemuck", - "num-derive 0.4.2", + "num-derive", "num-traits", "num_enum", "solana-account-info", @@ -7794,7 +7633,7 @@ checksum = "eff2d6a445a147c9d6dd77b8301b1e116c8299601794b558eafa409b342faf96" dependencies = [ "bytemuck", "solana-curve25519", - "solana-program 2.3.0", + "solana-program", "solana-zk-sdk", "spl-pod", "thiserror 2.0.17", @@ -7849,7 +7688,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d595667ed72dbfed8c251708f406d7c2814a3fa6879893b323d56a10bedfc799" dependencies = [ "bytemuck", - "num-derive 0.4.2", + "num-derive", "num-traits", "solana-decode-error", "solana-instruction", @@ -7868,7 +7707,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5597b4cd76f85ce7cd206045b7dc22da8c25516573d42d267c8d1fd128db5129" dependencies = [ "bytemuck", - "num-derive 0.4.2", + "num-derive", "num-traits", "solana-decode-error", "solana-instruction", @@ -7887,7 +7726,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfb9c89dbc877abd735f05547dcf9e6e12c00c11d6d74d8817506cab4c99fdbb" dependencies = [ "borsh 1.5.7", - "num-derive 0.4.2", + "num-derive", "num-traits", "solana-borsh", "solana-decode-error", @@ -7908,7 +7747,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "304d6e06f0de0c13a621464b1fd5d4b1bebf60d15ca71a44d3839958e0da16ee" dependencies = [ "borsh 1.5.7", - "num-derive 0.4.2", + "num-derive", "num-traits", "solana-borsh", "solana-decode-error", @@ -7930,7 +7769,7 @@ checksum = "4aa7503d52107c33c88e845e1351565050362c2314036ddf19a36cd25137c043" dependencies = [ "arrayref", "bytemuck", - "num-derive 0.4.2", + "num-derive", "num-traits", "solana-account-info", "solana-cpi", @@ -7955,7 +7794,7 @@ checksum = "a7e905b849b6aba63bde8c4badac944ebb6c8e6e14817029cbe1bc16829133bd" dependencies = [ "arrayref", "bytemuck", - "num-derive 0.4.2", + "num-derive", "num-traits", "solana-account-info", "solana-cpi", @@ -7979,7 +7818,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba70ef09b13af616a4c987797870122863cba03acc4284f226a4473b043923f9" dependencies = [ "bytemuck", - "num-derive 0.4.2", + "num-derive", "num-traits", "solana-account-info", "solana-decode-error", @@ -7997,7 +7836,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d417eb548214fa822d93f84444024b4e57c13ed6719d4dcc68eec24fb481e9f5" dependencies = [ "bytemuck", - "num-derive 0.4.2", + "num-derive", "num-traits", "solana-account-info", "solana-decode-error", @@ -8054,12 +7893,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - [[package]] name = "sync_wrapper" version = "1.0.2" @@ -8092,17 +7925,6 @@ dependencies = [ "syn 2.0.104", ] -[[package]] -name = "system-configuration" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "system-configuration-sys 0.5.0", -] - [[package]] name = "system-configuration" version = "0.6.1" @@ -8111,28 +7933,24 @@ checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags 2.9.1", "core-foundation 0.9.4", - "system-configuration-sys 0.6.0", + "system-configuration-sys", ] [[package]] name = "system-configuration-sys" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" dependencies = [ "core-foundation-sys", "libc", ] [[package]] -name = "system-configuration-sys" -version = "0.6.0" +name = "tap" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" @@ -8169,7 +7987,7 @@ dependencies = [ "anyhow", "base64 0.22.1", "bincode", - "bs58 0.5.1", + "bs58", "chrono", "clap", "colored", @@ -8180,7 +7998,9 @@ dependencies = [ "jsonrpsee", "kora-lib", "once_cell", - "reqwest 0.12.23", + "reqwest", + "rust_decimal", + "rust_decimal_macros", "serde", "serde_json", "sha2 0.10.9", @@ -8189,7 +8009,7 @@ dependencies = [ "solana-commitment-config", "solana-compute-budget-interface", "solana-message", - "solana-sdk 2.3.1", + "solana-sdk", "solana-system-interface", "spl-associated-token-account 6.0.0", "spl-token 7.0.0", @@ -8496,7 +8316,7 @@ dependencies = [ "futures-core", "futures-util", "pin-project-lite", - "sync_wrapper 1.0.2", + "sync_wrapper", "tokio", "tower-layer", "tower-service", @@ -8813,7 +8633,7 @@ dependencies = [ "bytes", "derive_builder", "http 1.3.1", - "reqwest 0.12.23", + "reqwest", "rustify", "rustify_derive", "serde", @@ -9134,15 +8954,6 @@ dependencies = [ "windows-targets 0.42.2", ] -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -9185,21 +8996,6 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -9239,12 +9035,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -9263,12 +9053,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -9287,12 +9071,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -9323,12 +9101,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -9347,12 +9119,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -9371,12 +9137,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -9395,12 +9155,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -9431,16 +9185,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winreg" -version = "0.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - [[package]] name = "wit-bindgen-rt" version = "0.39.0" @@ -9456,6 +9200,15 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x509-parser" version = "0.14.0" diff --git a/Cargo.toml b/Cargo.toml index b6663870..81df9109 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,7 +66,6 @@ once_cell = "1.20.2" futures-util = "0.3.31" hyper = "1.5.1" http = "0.2" -jup-ag = "0.8.0" toml = "0.8.19" spl-token = { version = "7.0.0", features = ["no-entrypoint"] } spl-token-2022 = { version = "8.0.0", features = ["no-entrypoint"] } @@ -84,3 +83,6 @@ hmac = "0.12.1" sha2 = "0.10.9" prometheus = "0.14.0" http-body-util = "0.1.3" +subtle = "2.6.1" +rust_decimal = "1.39" +rust_decimal_macros = "1.39" diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml index df439514..b5430142 100644 --- a/crates/lib/Cargo.toml +++ b/crates/lib/Cargo.toml @@ -37,7 +37,6 @@ base64 = { workspace = true } tokio = { workspace = true } async-trait = { workspace = true } reqwest = { workspace = true } -jup-ag = { workspace = true } spl-token = { workspace = true } spl-token-2022 = { workspace = true } spl-associated-token-account = { workspace = true } @@ -79,6 +78,9 @@ http-body = "1.0.1" http-body-util = "0.1.3" prometheus = { workspace = true } regex = "1.11.1" +subtle = { workspace = true } +rust_decimal = { workspace = true } +rust_decimal_macros = { workspace = true } [features] default = [] diff --git a/crates/lib/src/admin/token_util.rs b/crates/lib/src/admin/token_util.rs index c7f5ea3a..bb5eb369 100644 --- a/crates/lib/src/admin/token_util.rs +++ b/crates/lib/src/admin/token_util.rs @@ -1,6 +1,6 @@ use crate::{ error::KoraError, - state::{get_all_signers, get_request_signer_with_signer_key}, + state::{get_request_signer_with_signer_key, get_signer_pool}, token::token::TokenType, transaction::TransactionUtil, }; @@ -64,7 +64,11 @@ pub async fn initialize_atas( vec![Pubkey::from_str(payment_address) .map_err(|e| KoraError::InternalServerError(format!("Invalid payment address: {e}")))?] } else { - get_all_signers()?.iter().map(|signer| signer.signer.pubkey()).collect::>() + get_signer_pool()? + .get_signers_info() + .iter() + .filter_map(|info| info.public_key.parse().ok()) + .collect::>() }; initialize_atas_with_chunk_size( diff --git a/crates/lib/src/config.rs b/crates/lib/src/config.rs index ac538def..48e20f4c 100644 --- a/crates/lib/src/config.rs +++ b/crates/lib/src/config.rs @@ -146,7 +146,7 @@ pub struct FeePayerPolicy { pub token_2022: Token2022InstructionPolicy, } -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Default)] pub struct SystemInstructionPolicy { /// Allow fee payer to be the sender in System Transfer/TransferWithSeed instructions pub allow_transfer: bool, @@ -161,19 +161,7 @@ pub struct SystemInstructionPolicy { pub nonce: NonceInstructionPolicy, } -impl Default for SystemInstructionPolicy { - fn default() -> Self { - Self { - allow_transfer: true, - allow_assign: true, - allow_create_account: true, - allow_allocate: true, - nonce: NonceInstructionPolicy::default(), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Default)] pub struct NonceInstructionPolicy { /// Allow fee payer to be set as the nonce authority in InitializeNonceAccount instructions pub allow_initialize: bool, @@ -186,18 +174,7 @@ pub struct NonceInstructionPolicy { // Note: UpgradeNonceAccount not included - has no authority parameter, cannot validate fee payer involvement } -impl Default for NonceInstructionPolicy { - fn default() -> Self { - Self { - allow_initialize: true, - allow_advance: true, - allow_withdraw: true, - allow_authorize: true, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Default)] pub struct SplTokenInstructionPolicy { /// Allow fee payer to be the owner in SPL Token Transfer/TransferChecked instructions pub allow_transfer: bool, @@ -225,26 +202,7 @@ pub struct SplTokenInstructionPolicy { pub allow_thaw_account: bool, } -impl Default for SplTokenInstructionPolicy { - fn default() -> Self { - Self { - allow_transfer: true, - allow_burn: true, - allow_close_account: true, - allow_approve: true, - allow_revoke: true, - allow_set_authority: true, - allow_mint_to: true, - allow_initialize_mint: true, - allow_initialize_account: true, - allow_initialize_multisig: true, - allow_freeze_account: true, - allow_thaw_account: true, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Default)] pub struct Token2022InstructionPolicy { /// Allow fee payer to be the owner in Token2022 Transfer/TransferChecked instructions pub allow_transfer: bool, @@ -272,25 +230,6 @@ pub struct Token2022InstructionPolicy { pub allow_thaw_account: bool, } -impl Default for Token2022InstructionPolicy { - fn default() -> Self { - Self { - allow_transfer: true, - allow_burn: true, - allow_close_account: true, - allow_approve: true, - allow_revoke: true, - allow_set_authority: true, - allow_mint_to: true, - allow_initialize_mint: true, - allow_initialize_account: true, - allow_initialize_multisig: true, - allow_freeze_account: true, - allow_thaw_account: true, - } - } -} - #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct Token2022Config { pub blocked_mint_extensions: Vec, diff --git a/crates/lib/src/error.rs b/crates/lib/src/error.rs index a291ee7a..09a3fd9c 100644 --- a/crates/lib/src/error.rs +++ b/crates/lib/src/error.rs @@ -48,9 +48,6 @@ pub enum KoraError { #[error("Token operation failed: {0}")] TokenOperationError(String), - #[error("Thread safety error: {0}")] - ThreadSafetyError(String), - #[error("Invalid request: {0}")] InvalidRequest(String), @@ -464,7 +461,6 @@ mod tests { KoraError::FeeEstimationFailed("test".to_string()), KoraError::SwapError("test".to_string()), KoraError::TokenOperationError("test".to_string()), - KoraError::ThreadSafetyError("test".to_string()), KoraError::InvalidRequest("test".to_string()), KoraError::Unauthorized("test".to_string()), KoraError::RateLimitExceeded, diff --git a/crates/lib/src/fee/fee.rs b/crates/lib/src/fee/fee.rs index 97be27be..8ac3112b 100644 --- a/crates/lib/src/fee/fee.rs +++ b/crates/lib/src/fee/fee.rs @@ -316,7 +316,7 @@ impl FeeConfigUtil { rpc_client: &RpcClient, fee_in_lamports: u64, fee_token: Option<&str>, - ) -> Result, KoraError> { + ) -> Result, KoraError> { if let Some(fee_token) = fee_token { let token_mint = Pubkey::from_str(fee_token).map_err(|_| { KoraError::InvalidTransaction("Invalid fee token mint address".to_string()) diff --git a/crates/lib/src/mod.rs b/crates/lib/src/mod.rs index e94d36e8..1ad99872 100644 --- a/crates/lib/src/mod.rs +++ b/crates/lib/src/mod.rs @@ -24,7 +24,7 @@ pub use cache::CacheUtil; pub use config::Config; pub use error::KoraError; pub use signer::SolanaSigner; -pub use state::{get_all_signers, get_request_signer_with_signer_key}; +pub use state::get_request_signer_with_signer_key; #[cfg(test)] pub mod tests; diff --git a/crates/lib/src/oracle/jupiter.rs b/crates/lib/src/oracle/jupiter.rs index ea0e79f1..1e77f23e 100644 --- a/crates/lib/src/oracle/jupiter.rs +++ b/crates/lib/src/oracle/jupiter.rs @@ -8,11 +8,17 @@ use crate::{ use once_cell::sync::Lazy; use parking_lot::RwLock; use reqwest::{Client, StatusCode}; +use rust_decimal::Decimal; use serde::Deserialize; use std::{collections::HashMap, sync::Arc}; const JUPITER_AUTH_HEADER: &str = "x-api-key"; +const JUPITER_DEFAULT_CONFIDENCE: f64 = 0.95; + +const MAX_REASONABLE_PRICE: f64 = 1_000_000.0; +const MIN_REASONABLE_PRICE: f64 = 0.000_000_001; + static GLOBAL_JUPITER_API_KEY: Lazy>>> = Lazy::new(|| Arc::new(RwLock::new(None))); @@ -121,6 +127,41 @@ impl PriceOracle for JupiterPriceOracle { } impl JupiterPriceOracle { + fn validate_price_data(price_data: &JupiterPriceData, mint: &str) -> Result<(), KoraError> { + let price = price_data.usd_price; + + math_validator::validate_division(price)?; + + // Sanity check: price should be within reasonable bounds + if price > MAX_REASONABLE_PRICE { + log::error!( + "Price data for mint {} exceeds reasonable bounds: {} > {}", + mint, + price, + MAX_REASONABLE_PRICE + ); + return Err(KoraError::RpcError(format!( + "Price data for mint {} exceeds reasonable bounds", + mint + ))); + } + + if price < MIN_REASONABLE_PRICE { + log::error!( + "Price data for mint {} below reasonable bounds: {} < {}", + mint, + price, + MIN_REASONABLE_PRICE + ); + return Err(KoraError::RpcError(format!( + "Price data for mint {} below reasonable bounds", + mint + ))); + } + + Ok(()) + } + async fn fetch_prices_from_url( &self, client: &Client, @@ -172,16 +213,32 @@ impl JupiterPriceOracle { .get(SOL_MINT) .ok_or_else(|| KoraError::RpcError("No SOL price data from Jupiter".to_string()))?; - math_validator::validate_division(sol_price.usd_price)?; + Self::validate_price_data(sol_price, SOL_MINT)?; // Convert all prices to SOL-denominated let mut result = HashMap::new(); for mint_address in mint_addresses { if let Some(price_data) = jupiter_response.get(mint_address.as_str()) { - let price = price_data.usd_price / sol_price.usd_price; + Self::validate_price_data(price_data, mint_address)?; + + // Convert f64 USD prices to Decimal at API boundary + let token_usd = + Decimal::from_f64_retain(price_data.usd_price).ok_or_else(|| { + KoraError::RpcError(format!("Invalid token price for mint {mint_address}")) + })?; + let sol_usd = Decimal::from_f64_retain(sol_price.usd_price).ok_or_else(|| { + KoraError::RpcError("Invalid SOL price from Jupiter".to_string()) + })?; + + let price_in_sol = token_usd / sol_usd; + result.insert( mint_address.clone(), - TokenPrice { price, confidence: 0.95, source: PriceSource::Jupiter }, + TokenPrice { + price: price_in_sol, + confidence: JUPITER_DEFAULT_CONFIDENCE, + source: PriceSource::Jupiter, + }, ); } else { log::error!("No price data for mint {mint_address} from Jupiter"); @@ -243,7 +300,7 @@ mod tests { 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.price, Decimal::from(1)); assert_eq!(price.source, PriceSource::Jupiter); // Test case 2: With API key - should use pro API @@ -269,7 +326,7 @@ mod tests { oracle2.get_price(&client, "So11111111111111111111111111111111111111112").await; assert!(result.is_ok()); let price = result.unwrap(); - assert_eq!(price.price, 1.0); + assert_eq!(price.price, Decimal::from(1)); assert_eq!(price.source, PriceSource::Jupiter); // Test case 3: No price data available - should return error diff --git a/crates/lib/src/oracle/oracle.rs b/crates/lib/src/oracle/oracle.rs index e2e29b91..1f3baced 100644 --- a/crates/lib/src/oracle/oracle.rs +++ b/crates/lib/src/oracle/oracle.rs @@ -4,6 +4,7 @@ use crate::{ }; use mockall::automock; use reqwest::Client; +use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, sync::Arc, time::Duration}; use tokio::time::sleep; @@ -11,7 +12,7 @@ use tokio::time::sleep; #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "docs", derive(utoipa::ToSchema))] pub struct TokenPrice { - pub price: f64, + pub price: Decimal, pub confidence: f64, pub source: PriceSource, } @@ -113,7 +114,11 @@ mod tests { for mint in mint_addresses { result.insert( mint.clone(), - TokenPrice { price: 1.0, confidence: 0.95, source: PriceSource::Jupiter }, + TokenPrice { + price: Decimal::from(1), + confidence: 0.95, + source: PriceSource::Jupiter, + }, ); } Ok(result) diff --git a/crates/lib/src/oracle/utils.rs b/crates/lib/src/oracle/utils.rs index 5166cc42..afb825b2 100644 --- a/crates/lib/src/oracle/utils.rs +++ b/crates/lib/src/oracle/utils.rs @@ -1,9 +1,11 @@ use crate::oracle::{MockPriceOracle, PriceOracle, PriceSource, TokenPrice}; +use rust_decimal::Decimal; +use rust_decimal_macros::dec; use std::{collections::HashMap, sync::Arc}; -pub const DEFAULT_MOCKED_PRICE: f64 = 0.001; -pub const DEFAULT_MOCKED_USDC_PRICE: f64 = 0.0001; -pub const DEFAULT_MOCKED_WSOL_PRICE: f64 = 1.0; +pub const DEFAULT_MOCKED_PRICE: Decimal = dec!(0.001); +pub const DEFAULT_MOCKED_USDC_PRICE: Decimal = dec!(0.0001); +pub const DEFAULT_MOCKED_WSOL_PRICE: Decimal = dec!(1.0); pub const USDC_DEVNET_MINT: &str = "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"; pub const WSOL_DEVNET_MINT: &str = "So11111111111111111111111111111111111111112"; diff --git a/crates/lib/src/rpc_server/auth.rs b/crates/lib/src/rpc_server/auth.rs index 132683fa..881eb354 100644 --- a/crates/lib/src/rpc_server/auth.rs +++ b/crates/lib/src/rpc_server/auth.rs @@ -6,6 +6,7 @@ use hmac::{Hmac, Mac}; use http::{Request, Response, StatusCode}; use jsonrpsee::server::logger::Body; use sha2::Sha256; +use subtle::ConstantTimeEq; #[derive(Clone)] pub struct ApiKeyAuthLayer { @@ -73,7 +74,8 @@ where // Check for API key header let req = Request::from_parts(parts, Body::from(body_bytes)); if let Some(provided_key) = req.headers().get(X_API_KEY) { - if provided_key.to_str().unwrap_or("") == api_key { + // Constant-time comparison prevents timing attacks + if provided_key.as_bytes().ct_eq(api_key.as_bytes()).into() { return inner.call(req).await; } } @@ -187,7 +189,13 @@ where } // Verify HMAC signature using timestamp + body - let body_str = std::str::from_utf8(&body_bytes).unwrap_or(""); + let body_str = match std::str::from_utf8(&body_bytes) { + Ok(s) => s, + Err(_) => { + log::error!("HMAC authentication failed: invalid UTF-8 in request body"); + return Ok(unauthorized_response); + } + }; let message = format!("{}{}", timestamp, body_str); let mut mac = match Hmac::::new_from_slice(secret.as_bytes()) { diff --git a/crates/lib/src/rpc_server/method/estimate_transaction_fee.rs b/crates/lib/src/rpc_server/method/estimate_transaction_fee.rs index ab32e39d..b2b046d0 100644 --- a/crates/lib/src/rpc_server/method/estimate_transaction_fee.rs +++ b/crates/lib/src/rpc_server/method/estimate_transaction_fee.rs @@ -35,7 +35,7 @@ pub struct EstimateTransactionFeeRequest { #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct EstimateTransactionFeeResponse { pub fee_in_lamports: u64, - pub fee_in_token: Option, + pub fee_in_token: Option, /// Public key of the signer used for fee estimation (for client consistency) pub signer_pubkey: String, /// Public key of the payment destination diff --git a/crates/lib/src/rpc_server/method/get_config.rs b/crates/lib/src/rpc_server/method/get_config.rs index 1093838b..50eef336 100644 --- a/crates/lib/src/rpc_server/method/get_config.rs +++ b/crates/lib/src/rpc_server/method/get_config.rs @@ -88,38 +88,38 @@ mod tests { assert_eq!(response.validation_config.disallowed_accounts.len(), 0); assert_eq!(response.validation_config.price_source, crate::oracle::PriceSource::Mock); - // Assert FeePayerPolicy defaults - System - assert!(response.validation_config.fee_payer_policy.system.allow_transfer); - assert!(response.validation_config.fee_payer_policy.system.allow_assign); - assert!(response.validation_config.fee_payer_policy.system.allow_create_account); - assert!(response.validation_config.fee_payer_policy.system.allow_allocate); - assert!(response.validation_config.fee_payer_policy.system.nonce.allow_initialize); - assert!(response.validation_config.fee_payer_policy.system.nonce.allow_advance); - assert!(response.validation_config.fee_payer_policy.system.nonce.allow_withdraw); - assert!(response.validation_config.fee_payer_policy.system.nonce.allow_authorize); + // Assert FeePayerPolicy defaults - System (secure by default - all false) + assert!(!response.validation_config.fee_payer_policy.system.allow_transfer); + assert!(!response.validation_config.fee_payer_policy.system.allow_assign); + assert!(!response.validation_config.fee_payer_policy.system.allow_create_account); + assert!(!response.validation_config.fee_payer_policy.system.allow_allocate); + assert!(!response.validation_config.fee_payer_policy.system.nonce.allow_initialize); + assert!(!response.validation_config.fee_payer_policy.system.nonce.allow_advance); + assert!(!response.validation_config.fee_payer_policy.system.nonce.allow_withdraw); + assert!(!response.validation_config.fee_payer_policy.system.nonce.allow_authorize); // Note: allow_upgrade removed - no authority parameter to validate - // Assert FeePayerPolicy defaults - SPL Token - assert!(response.validation_config.fee_payer_policy.spl_token.allow_transfer); - assert!(response.validation_config.fee_payer_policy.spl_token.allow_burn); - assert!(response.validation_config.fee_payer_policy.spl_token.allow_close_account); - assert!(response.validation_config.fee_payer_policy.spl_token.allow_approve); - assert!(response.validation_config.fee_payer_policy.spl_token.allow_revoke); - assert!(response.validation_config.fee_payer_policy.spl_token.allow_set_authority); - assert!(response.validation_config.fee_payer_policy.spl_token.allow_mint_to); - assert!(response.validation_config.fee_payer_policy.spl_token.allow_freeze_account); - assert!(response.validation_config.fee_payer_policy.spl_token.allow_thaw_account); - - // Assert FeePayerPolicy defaults - Token2022 - assert!(response.validation_config.fee_payer_policy.token_2022.allow_transfer); - assert!(response.validation_config.fee_payer_policy.token_2022.allow_burn); - assert!(response.validation_config.fee_payer_policy.token_2022.allow_close_account); - assert!(response.validation_config.fee_payer_policy.token_2022.allow_approve); - assert!(response.validation_config.fee_payer_policy.token_2022.allow_revoke); - assert!(response.validation_config.fee_payer_policy.token_2022.allow_set_authority); - assert!(response.validation_config.fee_payer_policy.token_2022.allow_mint_to); - assert!(response.validation_config.fee_payer_policy.token_2022.allow_freeze_account); - assert!(response.validation_config.fee_payer_policy.token_2022.allow_thaw_account); + // Assert FeePayerPolicy defaults - SPL Token (secure by default - all false) + assert!(!response.validation_config.fee_payer_policy.spl_token.allow_transfer); + assert!(!response.validation_config.fee_payer_policy.spl_token.allow_burn); + assert!(!response.validation_config.fee_payer_policy.spl_token.allow_close_account); + assert!(!response.validation_config.fee_payer_policy.spl_token.allow_approve); + assert!(!response.validation_config.fee_payer_policy.spl_token.allow_revoke); + assert!(!response.validation_config.fee_payer_policy.spl_token.allow_set_authority); + assert!(!response.validation_config.fee_payer_policy.spl_token.allow_mint_to); + assert!(!response.validation_config.fee_payer_policy.spl_token.allow_freeze_account); + assert!(!response.validation_config.fee_payer_policy.spl_token.allow_thaw_account); + + // Assert FeePayerPolicy defaults - Token2022 (secure by default - all false) + assert!(!response.validation_config.fee_payer_policy.token_2022.allow_transfer); + assert!(!response.validation_config.fee_payer_policy.token_2022.allow_burn); + assert!(!response.validation_config.fee_payer_policy.token_2022.allow_close_account); + assert!(!response.validation_config.fee_payer_policy.token_2022.allow_approve); + assert!(!response.validation_config.fee_payer_policy.token_2022.allow_revoke); + assert!(!response.validation_config.fee_payer_policy.token_2022.allow_set_authority); + assert!(!response.validation_config.fee_payer_policy.token_2022.allow_mint_to); + assert!(!response.validation_config.fee_payer_policy.token_2022.allow_freeze_account); + assert!(!response.validation_config.fee_payer_policy.token_2022.allow_thaw_account); // Assert PriceConfig default (check margin value) match response.validation_config.price.model { crate::fee::price::PriceModel::Margin { margin } => assert_eq!(margin, 0.0), diff --git a/crates/lib/src/rpc_server/method/sign_transaction_if_paid.rs b/crates/lib/src/rpc_server/method/sign_transaction_if_paid.rs index a15b4ae9..51a52de4 100644 --- a/crates/lib/src/rpc_server/method/sign_transaction_if_paid.rs +++ b/crates/lib/src/rpc_server/method/sign_transaction_if_paid.rs @@ -24,7 +24,6 @@ pub struct SignTransactionIfPaidRequest { #[derive(Debug, Serialize, ToSchema)] pub struct SignTransactionIfPaidResponse { - pub transaction: String, pub signed_transaction: String, /// Public key of the signer used (for client consistency) pub signer_pubkey: String, @@ -48,13 +47,12 @@ pub async fn sign_transaction_if_paid( ) .await?; - let (transaction, signed_transaction) = resolved_transaction + let (_, signed_transaction) = resolved_transaction .sign_transaction_if_paid(&signer, rpc_client) .await .map_err(|e| KoraError::TokenOperationError(e.to_string()))?; Ok(SignTransactionIfPaidResponse { - transaction: TransactionUtil::encode_versioned_transaction(&transaction)?, signed_transaction, signer_pubkey: signer.pubkey().to_string(), }) diff --git a/crates/lib/src/rpc_server/method/transfer_transaction.rs b/crates/lib/src/rpc_server/method/transfer_transaction.rs index 2e82c650..a466ef6a 100644 --- a/crates/lib/src/rpc_server/method/transfer_transaction.rs +++ b/crates/lib/src/rpc_server/method/transfer_transaction.rs @@ -192,8 +192,7 @@ mod tests { #[tokio::test] async fn test_transfer_transaction_invalid_destination() { - let config = ConfigMockBuilder::new().build(); - let _ = update_config(config); + let _m = ConfigMockBuilder::new().build_and_setup(); let _ = setup_or_get_test_signer(); let rpc_client = Arc::new(RpcMockBuilder::new().with_mint_account(6).build()); @@ -220,8 +219,7 @@ mod tests { #[tokio::test] async fn test_transfer_transaction_invalid_token() { - let config = ConfigMockBuilder::new().build(); - let _ = update_config(config); + let _m = ConfigMockBuilder::new().build_and_setup(); let _ = setup_or_get_test_signer(); let rpc_client = Arc::new(RpcMockBuilder::new().with_mint_account(6).build()); diff --git a/crates/lib/src/signer/pool.rs b/crates/lib/src/signer/pool.rs index a52a3b58..a841de17 100644 --- a/crates/lib/src/signer/pool.rs +++ b/crates/lib/src/signer/pool.rs @@ -211,11 +211,6 @@ impl SignerPool { KoraError::ValidationError(format!("Signer with pubkey {pubkey} not found in pool")) }) } - - /// Get all signers in the pool - pub fn get_all_signers(&self) -> &[SignerWithMetadata] { - &self.signers - } } #[cfg(test)] diff --git a/crates/lib/src/state.rs b/crates/lib/src/state.rs index b10bd16d..4b7c6008 100644 --- a/crates/lib/src/state.rs +++ b/crates/lib/src/state.rs @@ -25,7 +25,7 @@ pub fn get_request_signer_with_signer_key( return Ok(Arc::clone(&signer_meta.signer)); } - // Default behavior: use next signer from round-robin + // Use configured selection strategy (defaults to round-robin if not specified) let signer_meta = pool.get_next_signer().map_err(|e| { KoraError::InternalServerError(format!("Failed to get signer from pool: {e}")) })?; @@ -64,12 +64,6 @@ pub fn get_signers_info() -> Result, KoraError> { Ok(pool.get_signers_info()) } -/// Get all signers from the pool (for operations that need to iterate over all signers) -pub fn get_all_signers() -> Result, KoraError> { - let pool = get_signer_pool()?; - Ok(pool.get_all_signers().to_vec()) -} - /// Update the global signer configs with a new config (test only) #[cfg(test)] pub fn update_signer_pool(new_pool: SignerPool) -> Result<(), KoraError> { diff --git a/crates/lib/src/token/token.rs b/crates/lib/src/token/token.rs index 5fa0be6d..05b21ff8 100644 --- a/crates/lib/src/token/token.rs +++ b/crates/lib/src/token/token.rs @@ -11,6 +11,10 @@ use crate::{ }, CacheUtil, }; +use rust_decimal::{ + prelude::{FromPrimitive, ToPrimitive}, + Decimal, +}; use solana_client::nonblocking::rpc_client::RpcClient; use solana_sdk::{native_token::LAMPORTS_PER_SOL, pubkey::Pubkey}; use spl_associated_token_account::get_associated_token_address_with_program_id; @@ -20,7 +24,7 @@ use std::{collections::HashMap, str::FromStr, time::Duration}; use crate::state::get_config; #[cfg(test)] -use crate::tests::config_mock::mock_state::get_config; +use {crate::tests::config_mock::mock_state::get_config, rust_decimal_macros::dec}; #[derive(Debug, Clone, Copy, PartialEq)] pub enum TokenType { @@ -112,12 +116,25 @@ impl TokenUtil { let (token_price, decimals) = Self::get_token_price_and_decimals(mint, price_source, rpc_client).await?; - // Convert token amount to its real value based on decimals and multiply by SOL price - let token_amount = amount as f64 / 10f64.powi(decimals as i32); - let sol_amount = token_amount * token_price.price; + // Convert amount to Decimal with proper scaling + let amount_decimal = Decimal::from_u64(amount) + .ok_or_else(|| KoraError::ValidationError("Invalid token amount".to_string()))?; + let decimals_scale = Decimal::from_u64(10u64.pow(decimals as u32)) + .ok_or_else(|| KoraError::ValidationError("Invalid decimals".to_string()))?; + + // Calculate: (amount / 10^decimals) * price * LAMPORTS_PER_SOL + let token_value = amount_decimal / decimals_scale; + let sol_value = token_value * token_price.price; + let lamports_decimal = sol_value + * Decimal::from_u64(LAMPORTS_PER_SOL).ok_or_else(|| { + KoraError::ValidationError("Invalid LAMPORTS_PER_SOL".to_string()) + })?; - // Convert SOL to lamports and round down - let lamports = (sol_amount * LAMPORTS_PER_SOL as f64).floor() as u64; + // Floor and convert to u64 + let lamports = lamports_decimal + .floor() + .to_u64() + .ok_or_else(|| KoraError::ValidationError("Lamports value overflow".to_string()))?; Ok(lamports) } @@ -127,18 +144,30 @@ impl TokenUtil { mint: &Pubkey, price_source: &PriceSource, rpc_client: &RpcClient, - ) -> Result { + ) -> Result { let (token_price, decimals) = Self::get_token_price_and_decimals(mint, price_source.clone(), rpc_client).await?; - // Convert lamports to SOL, then to token amount - let fee_in_sol = lamports as f64 / LAMPORTS_PER_SOL as f64; - let fee_in_token_base_units = fee_in_sol / token_price.price; - let fee_in_token = fee_in_token_base_units * 10f64.powi(decimals as i32); + // Convert lamports to SOL using Decimal + let lamports_decimal = Decimal::from_u64(lamports) + .ok_or_else(|| KoraError::ValidationError("Invalid lamports value".to_string()))?; + let lamports_per_sol_decimal = Decimal::from_u64(LAMPORTS_PER_SOL) + .ok_or_else(|| KoraError::ValidationError("Invalid LAMPORTS_PER_SOL".to_string()))?; + let sol_value = lamports_decimal / lamports_per_sol_decimal; + + // Convert SOL to token base units + let token_value = sol_value / token_price.price; + let scale = Decimal::from_u64(10u64.pow(decimals as u32)) + .ok_or_else(|| KoraError::ValidationError("Invalid decimals".to_string()))?; + let token_amount = token_value * scale; - // Round up to the next integer to fix floating point precision errors - // This ensures values like 1010049.9999999999 become 1010050 - Ok(fee_in_token.ceil()) + // Ceil and convert to u64 + let result = token_amount + .ceil() + .to_u64() + .ok_or_else(|| KoraError::ValidationError("Token amount overflow".to_string()))?; + + Ok(result) } /// Calculate the total lamports value of SPL token transfers where the fee payer is involved @@ -262,10 +291,23 @@ impl TokenUtil { .ok_or_else(|| KoraError::RpcError(format!("No decimals data for mint {mint}")))?; for (amount, is_outflow) in transfers { - // Convert token amount to lamports value - let token_amount = *amount as f64 / 10f64.powi(*decimals as i32); - let sol_amount = token_amount * price.price; - let lamports = (sol_amount * LAMPORTS_PER_SOL as f64).floor() as u64; + // Convert token amount to lamports value using Decimal + let amount_decimal = Decimal::from_u64(*amount).ok_or_else(|| { + KoraError::ValidationError("Invalid transfer amount".to_string()) + })?; + let decimals_scale = Decimal::from_u64(10u64.pow(*decimals as u32)) + .ok_or_else(|| KoraError::ValidationError("Invalid decimals".to_string()))?; + + let token_value = amount_decimal / decimals_scale; + let sol_value = token_value * price.price; + let lamports_decimal = sol_value + * Decimal::from_u64(LAMPORTS_PER_SOL).ok_or_else(|| { + KoraError::ValidationError("Invalid LAMPORTS_PER_SOL".to_string()) + })?; + + let lamports = lamports_decimal.floor().to_u64().ok_or_else(|| { + KoraError::ValidationError("Lamports value overflow".to_string()) + })?; if *is_outflow { // Add outflow to total @@ -358,7 +400,7 @@ impl TokenUtil { Ok(()) } - pub async fn process_token_transfer( + pub async fn verify_token_payment( transaction_resolved: &mut VersionedTransactionResolved, rpc_client: &RpcClient, required_lamports: u64, @@ -416,7 +458,7 @@ impl TokenUtil { } if !config.validation.supports_token(&token_state.mint().to_string()) { - return Ok(false); + continue; } let lamport_value = TokenUtil::calculate_token_value_in_lamports( @@ -571,7 +613,7 @@ mod tests_token { .unwrap(); assert_eq!(decimals, 9); - assert_eq!(token_price.price, 1.0); + assert_eq!(token_price.price, Decimal::from(1)); } #[tokio::test] @@ -586,7 +628,7 @@ mod tests_token { .unwrap(); assert_eq!(decimals, 6); - assert_eq!(token_price.price, 0.0001); + assert_eq!(token_price.price, dec!(0.0001)); } #[tokio::test] @@ -694,7 +736,7 @@ mod tests_token { .await .unwrap(); - assert_eq!(result, 1_000_000_000.0); // Should equal input since SOL price is 1.0 + assert_eq!(result, 1_000_000_000); // Should equal input since SOL price is 1.0 } #[tokio::test] @@ -714,7 +756,7 @@ mod tests_token { .unwrap(); // 0.0001 SOL / 0.0001 SOL/USDC = 1 USDC = 1,000,000 base units - assert_eq!(result, 1_000_000.0); + assert_eq!(result, 1_000_000); } #[tokio::test] @@ -733,7 +775,7 @@ mod tests_token { .await .unwrap(); - assert_eq!(result, 0.0); + assert_eq!(result, 0); } #[tokio::test] @@ -771,7 +813,7 @@ mod tests_token { .await; if let Ok(recovered_amount) = recovered_amount_result { - assert_eq!(recovered_amount, original_amount as f64); + assert_eq!(recovered_amount, original_amount); } } @@ -821,20 +863,20 @@ mod tests_token { let test_cases = vec![ // Low priority fees - (5_000u64, 50_000.0, "low priority base case"), - (10_001u64, 100_010.0, "odd number precision"), + (5_000u64, 50_000u64, "low priority base case"), + (10_001u64, 100_010u64, "odd number precision"), // High priority fees - (1_010_050u64, 10_100_500.0, "high priority problematic case"), + (1_010_050u64, 10_100_500u64, "high priority problematic case"), // High compute unit scenarios - (5_000_000u64, 50_000_000.0, "very high CU limit"), - (2_500_050u64, 25_000_501.0, "odd high amount"), // round up - (10_000_000u64, 100_000_000.0, "maximum CU cost"), + (5_000_000u64, 50_000_000u64, "very high CU limit"), + (2_500_050u64, 25_000_500u64, "odd high amount"), // exact result with Decimal + (10_000_000u64, 100_000_000u64, "maximum CU cost"), // Edge cases - (1_010_049u64, 10_100_490.0, "precision edge case -1"), - (1_010_051u64, 10_100_510.0, "precision edge case +1"), - (999_999u64, 9_999_990.0, "near million boundary"), - (1_000_001u64, 10_000_010.0, "over million boundary"), - (1_333_337u64, 13_333_370.0, "repeating digits edge case"), + (1_010_049u64, 10_100_490u64, "precision edge case -1"), + (1_010_051u64, 10_100_510u64, "precision edge case +1"), + (999_999u64, 9_999_990u64, "near million boundary"), + (1_000_001u64, 10_000_010u64, "over million boundary"), + (1_333_337u64, 13_333_370u64, "repeating digits edge case"), ]; for (lamports, expected, description) in test_cases { @@ -852,13 +894,6 @@ mod tests_token { result, expected, "Failed for {description}: lamports={lamports}, expected={expected}, got={result}", ); - - // Must be proper integers (no fractional part) - assert_eq!( - result.fract(), - 0.0, - "Result should be integer for {lamports} lamports: got {result}", - ); } } diff --git a/crates/lib/src/usage_limit/usage_tracker.rs b/crates/lib/src/usage_limit/usage_tracker.rs index cc771666..8bc711b9 100644 --- a/crates/lib/src/usage_limit/usage_tracker.rs +++ b/crates/lib/src/usage_limit/usage_tracker.rs @@ -3,11 +3,10 @@ use std::{collections::HashSet, sync::Arc}; use deadpool_redis::Runtime; use redis::AsyncCommands; use solana_sdk::{pubkey::Pubkey, transaction::VersionedTransaction}; -use solana_signers::SolanaSigner; use tokio::sync::OnceCell; use super::usage_store::{RedisUsageStore, UsageStore}; -use crate::{error::KoraError, get_all_signers, sanitize_error}; +use crate::{error::KoraError, sanitize_error, state::get_signer_pool}; #[cfg(not(test))] use crate::state::get_config; @@ -179,8 +178,11 @@ impl UsageTracker { config.kora.usage_limit.max_transactions ); - let kora_signers = - get_all_signers()?.iter().map(|signer| signer.signer.pubkey()).collect(); + let kora_signers = get_signer_pool()? + .get_signers_info() + .iter() + .filter_map(|info| info.public_key.parse().ok()) + .collect(); let store = Arc::new(RedisUsageStore::new(pool)); Some(UsageTracker::new( diff --git a/crates/lib/src/validator/account_validator.rs b/crates/lib/src/validator/account_validator.rs index cf299715..fa7c2e0b 100644 --- a/crates/lib/src/validator/account_validator.rs +++ b/crates/lib/src/validator/account_validator.rs @@ -92,7 +92,8 @@ impl AccountType { if let Some(should_be_executable) = should_be_executable { if account.executable != should_be_executable { return Err(KoraError::InternalServerError(format!( - "Account {account_pubkey} is not executable, cannot be a Program" + "Account {account_pubkey} executable flag mismatch: expected {should_be_executable}, found {}", + account.executable ))); } } @@ -181,7 +182,7 @@ mod tests { let result = AccountType::Mint.validate_account_type(&account, &account_pubkey); assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("is not executable, cannot be a Program")); + assert!(result.unwrap_err().to_string().contains("executable flag mismatch")); } #[test] @@ -247,7 +248,7 @@ mod tests { let result = AccountType::TokenAccount.validate_account_type(&account, &account_pubkey); assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("is not executable, cannot be a Program")); + assert!(result.unwrap_err().to_string().contains("executable flag mismatch")); } #[test] @@ -302,7 +303,7 @@ mod tests { let result = AccountType::Program.validate_account_type(&account, &account_pubkey); assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("is not executable, cannot be a Program")); + assert!(result.unwrap_err().to_string().contains("executable flag mismatch")); } #[test] diff --git a/crates/lib/src/validator/cache_validator.rs b/crates/lib/src/validator/cache_validator.rs index 006ecbd4..7bac6e38 100644 --- a/crates/lib/src/validator/cache_validator.rs +++ b/crates/lib/src/validator/cache_validator.rs @@ -26,36 +26,37 @@ impl CacheValidator { Ok(()) } - pub async fn validate( - usage_config: &UsageLimitConfig, - ) -> Result<(Vec, Vec), String> { + pub async fn validate(usage_config: &UsageLimitConfig) -> (Vec, Vec) { let mut errors = Vec::new(); let mut warnings = Vec::new(); // Skip validation if usage limiting is disabled if !usage_config.enabled { - return Ok((errors, warnings)); + return (errors, warnings); } // Check if cache_url is provided when enabled - if usage_config.cache_url.is_none() { - if !usage_config.fallback_if_unavailable { - errors.push( - "Usage limiting enabled without cache_url and fallback disabled - service will fail" - .to_string(), - ); - } else { - warnings.push( - "Usage limiting enabled without cache_url - fallback mode will disable limits" - .to_string(), - ); + match &usage_config.cache_url { + None => { + if !usage_config.fallback_if_unavailable { + errors.push( + "Usage limiting enabled without cache_url and fallback disabled - service will fail" + .to_string(), + ); + } else { + warnings.push( + "Usage limiting enabled without cache_url - fallback mode will disable limits" + .to_string(), + ); + } } - } else if let Some(cache_url) = &usage_config.cache_url { - // Validate cache_url format - if !cache_url.starts_with("redis://") && !cache_url.starts_with("rediss://") { - errors.push(format!( - "Invalid cache_url format: '{cache_url}' - must start with redis:// or rediss://" - )); + Some(cache_url) => { + // Validate cache_url format + if !cache_url.starts_with("redis://") && !cache_url.starts_with("rediss://") { + errors.push(format!( + "Invalid cache_url format: '{cache_url}' - must start with redis:// or rediss://" + )); + } } } @@ -87,7 +88,7 @@ impl CacheValidator { } } - Ok((errors, warnings)) + (errors, warnings) } } @@ -102,9 +103,7 @@ mod tests { async fn test_validate_usage_limit_disabled() { let config = ConfigMockBuilder::new().with_usage_limit_enabled(false).build(); - let result = CacheValidator::validate(&config.kora.usage_limit).await; - assert!(result.is_ok()); - let (errors, warnings) = result.unwrap(); + let (errors, warnings) = CacheValidator::validate(&config.kora.usage_limit).await; assert!(errors.is_empty()); assert!(warnings.is_empty()); @@ -119,9 +118,7 @@ mod tests { .with_usage_limit_fallback(true) .build(); - let result = CacheValidator::validate(&config.kora.usage_limit).await; - assert!(result.is_ok()); - let (errors, warnings) = result.unwrap(); + let (errors, warnings) = CacheValidator::validate(&config.kora.usage_limit).await; assert!(errors.is_empty()); assert!(warnings.iter().any(|w| w.contains( @@ -138,9 +135,7 @@ mod tests { .with_usage_limit_fallback(false) .build(); - let result = CacheValidator::validate(&config.kora.usage_limit).await; - assert!(result.is_ok()); - let (errors, warnings) = result.unwrap(); + let (errors, warnings) = CacheValidator::validate(&config.kora.usage_limit).await; // Should error when no cache_url and fallback disabled assert!(errors.iter().any(|e| e.contains( @@ -160,9 +155,7 @@ mod tests { .with_usage_limit_fallback(true) .build(); - let result = CacheValidator::validate(&config.kora.usage_limit).await; - assert!(result.is_ok()); - let (errors, warnings) = result.unwrap(); + let (errors, warnings) = CacheValidator::validate(&config.kora.usage_limit).await; // Should error for invalid cache_url format assert!(errors.iter().any(|e| e.contains("Invalid cache_url format") @@ -182,9 +175,7 @@ mod tests { .with_usage_limit_fallback(false) .build(); - let result = CacheValidator::validate(&config.kora.usage_limit).await; - assert!(result.is_ok()); - let (errors, warnings) = result.unwrap(); + let (errors, warnings) = CacheValidator::validate(&config.kora.usage_limit).await; // Should error about Redis connection failure with fallback disabled assert!(errors @@ -204,9 +195,7 @@ mod tests { .with_usage_limit_fallback(true) .build(); - let result = CacheValidator::validate(&config.kora.usage_limit).await; - assert!(result.is_ok()); - let (errors, warnings) = result.unwrap(); + let (errors, warnings) = CacheValidator::validate(&config.kora.usage_limit).await; // Should get warnings because Redis connection fails (unit tests don't run Redis) but fallback is enabled assert!(errors.is_empty()); diff --git a/crates/lib/src/validator/config_validator.rs b/crates/lib/src/validator/config_validator.rs index c8449e67..4dd7d82f 100644 --- a/crates/lib/src/validator/config_validator.rs +++ b/crates/lib/src/validator/config_validator.rs @@ -18,7 +18,7 @@ use crate::{ use solana_client::nonblocking::rpc_client::RpcClient; use solana_sdk::{pubkey::Pubkey, system_program::ID as SYSTEM_PROGRAM_ID}; use spl_token::ID as SPL_TOKEN_PROGRAM_ID; -use spl_token_2022::ID as TOKEN_2022_PROGRAM_ID; +use spl_token_2022::{extension::ExtensionType, ID as TOKEN_2022_PROGRAM_ID}; pub struct ConfigValidator {} @@ -151,6 +151,19 @@ impl ConfigValidator { errors.push(format!("Token2022 extension validation failed: {e}")); } + // Warn if PermanentDelegate is not blocked + if !config.validation.token_2022.is_mint_extension_blocked(ExtensionType::PermanentDelegate) + { + warnings.push( + "โš ๏ธ SECURITY: PermanentDelegate extension is NOT blocked. Tokens with this extension \ + allow the delegate to transfer/burn tokens at any time without owner approval. \ + This creates significant risks:\n\ + - Payment tokens: Funds can be seized after payment\n\ + Consider adding \"permanent_delegate\" to blocked_mint_extensions in [validation.token2022] \ + unless explicitly needed for your use case.".to_string() + ); + } + // Check if fees are enabled (not Free pricing) let fees_enabled = !matches!(config.validation.price.model, PriceModel::Free); @@ -197,6 +210,45 @@ impl ConfigValidator { "Token address for fixed price is not in allowed spl paid tokens: {token}" )); } + + // Warn about dangerous configurations with fixed pricing + let has_auth = + config.kora.auth.api_key.is_some() || config.kora.auth.hmac_secret.is_some(); + if !has_auth { + warnings.push( + "โš ๏ธ SECURITY: Fixed pricing with NO authentication enabled. \ + Without authentication, anyone can spam transactions at your expense. \ + Consider enabling api_key or hmac_secret in [kora.auth]." + .to_string(), + ); + } + + if config.validation.fee_payer_policy.system.allow_transfer { + warnings.push( + "โš ๏ธ SECURITY: Fixed pricing with allow_transfer=true for System instructions. \ + Users can make the fee payer transfer arbitrary SOL amounts at fixed cost. \ + This can drain your fee payer account. \ + Consider setting [validation.fee_payer_policy.system] allow_transfer=false.".to_string() + ); + } + + if config.validation.fee_payer_policy.spl_token.allow_transfer { + warnings.push( + "โš ๏ธ SECURITY: Fixed pricing with allow_transfer=true for SPL Token instructions. \ + Users can make the fee payer transfer arbitrary token amounts at fixed cost. \ + This can drain your fee payer token accounts. \ + Consider setting [validation.fee_payer_policy.spl_token] allow_transfer=false.".to_string() + ); + } + + if config.validation.fee_payer_policy.token_2022.allow_transfer { + warnings.push( + "โš ๏ธ SECURITY: Fixed pricing with allow_transfer=true for Token2022 instructions. \ + Users can make the fee payer transfer arbitrary token amounts at fixed cost. \ + This can drain your fee payer token accounts. \ + Consider setting [validation.fee_payer_policy.token_2022] allow_transfer=false.".to_string() + ); + } } PriceModel::Margin { margin } => { if *margin < 0.0 { @@ -208,16 +260,23 @@ impl ConfigValidator { _ => {} }; + // General authentication warning + let has_auth = config.kora.auth.api_key.is_some() || config.kora.auth.hmac_secret.is_some(); + if !has_auth { + warnings.push( + "โš ๏ธ SECURITY: No authentication configured (neither api_key nor hmac_secret). \ + Authentication is strongly recommended for production deployments. \ + Consider enabling api_key or hmac_secret in [kora.auth]." + .to_string(), + ); + } + // Validate usage limit configuration let usage_config = &config.kora.usage_limit; if usage_config.enabled { - if let Ok((usage_errors, usage_warnings)) = CacheValidator::validate(usage_config).await - { - errors.extend(usage_errors); - warnings.extend(usage_warnings); - } else { - errors.push("Failed to validate usage limit cache configuration".to_string()); - } + let (usage_errors, usage_warnings) = CacheValidator::validate(usage_config).await; + errors.extend(usage_errors); + warnings.extend(usage_warnings); } // RPC validation - only if not skipped @@ -437,7 +496,10 @@ mod tests { let result = ConfigValidator::validate_with_result(&rpc_client, true).await; assert!(result.is_ok()); let warnings = result.unwrap(); - assert!(warnings.is_empty()); + // Expect warnings about PermanentDelegate and no authentication + assert_eq!(warnings.len(), 2); + assert!(warnings.iter().any(|w| w.contains("PermanentDelegate"))); + assert!(warnings.iter().any(|w| w.contains("No authentication configured"))); } #[tokio::test] diff --git a/crates/lib/src/validator/transaction_validator.rs b/crates/lib/src/validator/transaction_validator.rs index 77b318af..bfc1892b 100644 --- a/crates/lib/src/validator/transaction_validator.rs +++ b/crates/lib/src/validator/transaction_validator.rs @@ -355,7 +355,7 @@ impl TransactionValidator { rpc_client: &RpcClient, expected_payment_destination: &Pubkey, ) -> Result<(), KoraError> { - if TokenUtil::process_token_transfer( + if TokenUtil::verify_token_payment( transaction_resolved, rpc_client, required_lamports, @@ -1315,9 +1315,10 @@ mod tests { let new_account = Pubkey::new_unique(); let owner = Pubkey::new_unique(); - // Test with allow_create_account = true (default) + // Test with allow_create_account = true let rpc_client = RpcMockBuilder::new().build(); - let policy = FeePayerPolicy::default(); + let mut policy = FeePayerPolicy::default(); + policy.system.allow_create_account = true; setup_config_with_policy(policy); let validator = TransactionValidator::new(fee_payer).unwrap(); @@ -1346,9 +1347,10 @@ mod tests { let fee_payer = Pubkey::new_unique(); - // Test with allow_allocate = true (default) + // Test with allow_allocate = true let rpc_client = RpcMockBuilder::new().build(); - let policy = FeePayerPolicy::default(); + let mut policy = FeePayerPolicy::default(); + policy.system.allow_allocate = true; setup_config_with_policy(policy); let validator = TransactionValidator::new(fee_payer).unwrap(); @@ -1378,9 +1380,10 @@ mod tests { let fee_payer = Pubkey::new_unique(); let nonce_account = Pubkey::new_unique(); - // Test with allow_initialize = true (default) + // Test with allow_initialize = true let rpc_client = RpcMockBuilder::new().build(); - let policy = FeePayerPolicy::default(); + let mut policy = FeePayerPolicy::default(); + policy.system.nonce.allow_initialize = true; setup_config_with_policy(policy); let validator = TransactionValidator::new(fee_payer).unwrap(); @@ -1413,9 +1416,10 @@ mod tests { let fee_payer = Pubkey::new_unique(); let nonce_account = Pubkey::new_unique(); - // Test with allow_advance = true (default) + // Test with allow_advance = true let rpc_client = RpcMockBuilder::new().build(); - let policy = FeePayerPolicy::default(); + let mut policy = FeePayerPolicy::default(); + policy.system.nonce.allow_advance = true; setup_config_with_policy(policy); let validator = TransactionValidator::new(fee_payer).unwrap(); @@ -1446,9 +1450,10 @@ mod tests { let nonce_account = Pubkey::new_unique(); let recipient = Pubkey::new_unique(); - // Test with allow_withdraw = true (default) + // Test with allow_withdraw = true let rpc_client = RpcMockBuilder::new().build(); - let policy = FeePayerPolicy::default(); + let mut policy = FeePayerPolicy::default(); + policy.system.nonce.allow_withdraw = true; setup_config_with_policy(policy); let validator = TransactionValidator::new(fee_payer).unwrap(); @@ -1479,9 +1484,10 @@ mod tests { let nonce_account = Pubkey::new_unique(); let new_authority = Pubkey::new_unique(); - // Test with allow_authorize = true (default) + // Test with allow_authorize = true let rpc_client = RpcMockBuilder::new().build(); - let policy = FeePayerPolicy::default(); + let mut policy = FeePayerPolicy::default(); + policy.system.nonce.allow_authorize = true; setup_config_with_policy(policy); let validator = TransactionValidator::new(fee_payer).unwrap(); diff --git a/sdks/ts/test/integration.test.ts b/sdks/ts/test/integration.test.ts index 61cdfc49..a628c7b3 100644 --- a/sdks/ts/test/integration.test.ts +++ b/sdks/ts/test/integration.test.ts @@ -247,7 +247,6 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without }); expect(signResult).toBeDefined(); - expect(signResult.transaction).toBeDefined(); expect(signResult.signed_transaction).toBeDefined(); }); diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 70b244ba..e1cd463a 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -72,3 +72,5 @@ colored = "3.0" serde = { workspace = true } base64 = { workspace = true } chrono = { workspace = true } +rust_decimal = { workspace = true } +rust_decimal_macros = { workspace = true } diff --git a/tests/external/jupiter_integration.rs b/tests/external/jupiter_integration.rs index 681f6c11..663abce6 100644 --- a/tests/external/jupiter_integration.rs +++ b/tests/external/jupiter_integration.rs @@ -1,5 +1,5 @@ -use jsonrpsee::core::Error; use kora_lib::oracle::{get_price_oracle, PriceSource, RetryingPriceOracle}; +use rust_decimal_macros::dec; use std::time::Duration; #[tokio::test] @@ -13,8 +13,16 @@ async fn test_jupiter_integration_usdc() { match result { Ok(token_price) => { - assert!(token_price.price > 0.001, "USDC price too low: {} SOL", token_price.price); - assert!(token_price.price < 0.01, "USDC price too high: {} SOL", token_price.price); + assert!( + token_price.price > dec!(0.001), + "USDC price too low: {} SOL", + token_price.price + ); + assert!( + token_price.price < dec!(0.01), + "USDC price too high: {} SOL", + token_price.price + ); assert_eq!(token_price.source, PriceSource::Jupiter); } Err(e) => { @@ -37,8 +45,16 @@ async fn test_jupiter_integration_cbtc() { match result { Ok(token_price) => { - assert!(token_price.price > 200.0, "cBTC price too low: {} SOL", token_price.price); - assert!(token_price.price < 1_000.0, "cBTC price too high: {} SOL", token_price.price); + assert!( + token_price.price > dec!(200.0), + "cBTC price too low: {} SOL", + token_price.price + ); + assert!( + token_price.price < dec!(1_000.0), + "cBTC price too high: {} SOL", + token_price.price + ); assert_eq!(token_price.source, PriceSource::Jupiter); } Err(e) => { @@ -62,7 +78,7 @@ async fn test_jupiter_integration_sol() { match result { Ok(token_price) => { assert!( - (token_price.price - 1.0).abs() < 0.001, + (token_price.price - dec!(1.0)).abs() < dec!(0.001), "SOL price should be ~1.0, got: {}", token_price.price ); From 3806a88a40667cc4f8545e46af3a797b4d1e8de3 Mon Sep 17 00:00:00 2001 From: jo <17280917+dev-jodee@users.noreply.github.com> Date: Tue, 28 Oct 2025 10:14:45 -0400 Subject: [PATCH 21/29] refactor: Removed sign_transaction_if_paid, and now sign_transaction and sign_and_send_transaction will always validate price, but if the node operator sets price as FREE it will have the same behavior as the old methods (#241) --- CLAUDE.md | 1 - crates/lib/src/config.rs | 11 +- .../lib/src/rpc_server/method/get_config.rs | 1 - crates/lib/src/rpc_server/method/mod.rs | 1 - .../method/sign_transaction_if_paid.rs | 111 ---- crates/lib/src/rpc_server/openapi/docs.rs | 3 - .../rpc_server/openapi/spec/combined_api.json | 169 +----- crates/lib/src/rpc_server/rpc.rs | 18 - crates/lib/src/rpc_server/server.rs | 13 +- .../src/transaction/versioned_transaction.rs | 73 +-- crates/lib/src/validator/config_validator.rs | 1 - docs/getting-started/FULL_DEMO.md | 6 +- docs/getting-started/QUICK_START.md | 2 +- .../demo/client/src/full-demo.ts | 517 ++++++++++-------- docs/getting-started/demo/server/kora.toml | 7 +- docs/operators/CONFIGURATION.md | 3 - docs/operators/FEES.md | 2 +- docs/operators/deploy/sample/kora.toml | 1 - docs/x402/demo/kora/kora.toml | 40 +- kora.toml | 1 - openapi.json | 32 +- sdks/ts/docs/README.md | 63 --- sdks/ts/scripts/generate-api-docs.js | 1 - sdks/ts/src/client.ts | 24 - sdks/ts/src/types/index.ts | 26 - sdks/ts/test/integration.test.ts | 39 +- sdks/ts/test/unit.test.ts | 28 - tests/Cargo.toml | 4 + tests/adversarial/fee_payer_exploitation.rs | 12 +- tests/free_signing/free_signing_tests.rs | 267 +++++++++ tests/free_signing/main.rs | 4 + tests/multi_signer/signer_management.rs | 27 +- .../payment_address_legacy_tests.rs | 13 +- .../payment_address_multi_payment_tests.rs | 6 +- .../payment_address_v0_lut_tests.rs | 4 +- .../payment_address_v0_tests.rs | 8 +- tests/rpc/transaction_signing.rs | 349 ++---------- tests/src/common/fixtures/auth-test.toml | 7 +- .../fixtures/fee-payer-policy-test.toml | 1 - tests/src/common/fixtures/kora-free-test.toml | 109 ++++ tests/src/common/fixtures/kora-test.toml | 5 +- .../fixtures/paymaster-address-test.toml | 5 +- tests/src/test_runner/output.rs | 1 + tests/src/test_runner/test_cases.toml | 7 + tests/tokens/token_2022_extensions_test.rs | 37 +- tests/tokens/token_2022_test.rs | 67 ++- 46 files changed, 931 insertions(+), 1196 deletions(-) delete mode 100644 crates/lib/src/rpc_server/method/sign_transaction_if_paid.rs create mode 100644 tests/free_signing/free_signing_tests.rs create mode 100644 tests/free_signing/main.rs create mode 100644 tests/src/common/fixtures/kora-free-test.toml diff --git a/CLAUDE.md b/CLAUDE.md index 797b539b..b6c46891 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -269,7 +269,6 @@ pnpm run format - `getBlockhash` - Get recent blockhash - `getConfig` - Return server configuration - `getSupportedTokens` - List accepted payment tokens - - `signTransactionIfPaid` - Conditional signing based on payment verification - `getPayerSigner` - Get the payer signer and payment destination - (client-only) `getPaymentInstruction` - Get a payment instruction for a transaction diff --git a/crates/lib/src/config.rs b/crates/lib/src/config.rs index 48e20f4c..9c4ea503 100644 --- a/crates/lib/src/config.rs +++ b/crates/lib/src/config.rs @@ -324,7 +324,6 @@ pub struct EnabledMethods { pub transfer_transaction: bool, pub get_blockhash: bool, pub get_config: bool, - pub sign_transaction_if_paid: bool, } impl EnabledMethods { @@ -339,7 +338,6 @@ impl EnabledMethods { self.transfer_transaction, self.get_blockhash, self.get_config, - self.sign_transaction_if_paid, ] .into_iter() } @@ -374,16 +372,13 @@ impl EnabledMethods { if self.get_config { methods.push("getConfig".to_string()); } - if self.sign_transaction_if_paid { - methods.push("signTransactionIfPaid".to_string()); - } methods } } impl IntoIterator for &EnabledMethods { type Item = bool; - type IntoIter = std::array::IntoIter; + type IntoIter = std::array::IntoIter; fn into_iter(self) -> Self::IntoIter { [ @@ -396,7 +391,6 @@ impl IntoIterator for &EnabledMethods { self.transfer_transaction, self.get_blockhash, self.get_config, - self.sign_transaction_if_paid, ] .into_iter() } @@ -414,7 +408,6 @@ impl Default for EnabledMethods { transfer_transaction: true, get_blockhash: true, get_config: true, - sign_transaction_if_paid: true, } } } @@ -610,7 +603,6 @@ mod tests { ("transfer_transaction", true), ("get_blockhash", true), ("get_config", true), - ("sign_transaction_if_paid", true), ("get_payer_signer", true), ]) .build_config() @@ -625,7 +617,6 @@ mod tests { assert!(config.kora.enabled_methods.transfer_transaction); assert!(config.kora.enabled_methods.get_blockhash); assert!(config.kora.enabled_methods.get_config); - assert!(config.kora.enabled_methods.sign_transaction_if_paid); } #[test] diff --git a/crates/lib/src/rpc_server/method/get_config.rs b/crates/lib/src/rpc_server/method/get_config.rs index 50eef336..bb7cd284 100644 --- a/crates/lib/src/rpc_server/method/get_config.rs +++ b/crates/lib/src/rpc_server/method/get_config.rs @@ -140,6 +140,5 @@ mod tests { assert!(response.enabled_methods.transfer_transaction); assert!(response.enabled_methods.get_blockhash); assert!(response.enabled_methods.get_config); - assert!(response.enabled_methods.sign_transaction_if_paid); } } diff --git a/crates/lib/src/rpc_server/method/mod.rs b/crates/lib/src/rpc_server/method/mod.rs index 9a023ee1..cfd021b6 100644 --- a/crates/lib/src/rpc_server/method/mod.rs +++ b/crates/lib/src/rpc_server/method/mod.rs @@ -5,5 +5,4 @@ pub mod get_payer_signer; pub mod get_supported_tokens; pub mod sign_and_send_transaction; pub mod sign_transaction; -pub mod sign_transaction_if_paid; pub mod transfer_transaction; diff --git a/crates/lib/src/rpc_server/method/sign_transaction_if_paid.rs b/crates/lib/src/rpc_server/method/sign_transaction_if_paid.rs deleted file mode 100644 index 51a52de4..00000000 --- a/crates/lib/src/rpc_server/method/sign_transaction_if_paid.rs +++ /dev/null @@ -1,111 +0,0 @@ -use crate::{ - rpc_server::middleware_utils::default_sig_verify, - state::get_request_signer_with_signer_key, - transaction::{TransactionUtil, VersionedTransactionOps, VersionedTransactionResolved}, - usage_limit::UsageTracker, - KoraError, -}; -use serde::{Deserialize, Serialize}; -use solana_client::nonblocking::rpc_client::RpcClient; -use solana_signers::SolanaSigner; -use std::sync::Arc; -use utoipa::ToSchema; - -#[derive(Debug, Deserialize, ToSchema)] -pub struct SignTransactionIfPaidRequest { - pub transaction: String, - /// Optional signer signer_key to ensure consistency across related RPC calls - #[serde(default, skip_serializing_if = "Option::is_none")] - pub signer_key: Option, - /// Whether to verify signatures during simulation (defaults to true) - #[serde(default = "default_sig_verify")] - pub sig_verify: bool, -} - -#[derive(Debug, Serialize, ToSchema)] -pub struct SignTransactionIfPaidResponse { - pub signed_transaction: String, - /// Public key of the signer used (for client consistency) - pub signer_pubkey: String, -} - -pub async fn sign_transaction_if_paid( - rpc_client: &Arc, - request: SignTransactionIfPaidRequest, -) -> Result { - let transaction_requested = TransactionUtil::decode_b64_transaction(&request.transaction)?; - - // Check usage limit for transaction sender - UsageTracker::check_transaction_usage_limit(&transaction_requested).await?; - - let signer = get_request_signer_with_signer_key(request.signer_key.as_deref())?; - - let mut resolved_transaction = VersionedTransactionResolved::from_transaction( - &transaction_requested, - rpc_client, - request.sig_verify, - ) - .await?; - - let (_, signed_transaction) = resolved_transaction - .sign_transaction_if_paid(&signer, rpc_client) - .await - .map_err(|e| KoraError::TokenOperationError(e.to_string()))?; - - Ok(SignTransactionIfPaidResponse { - signed_transaction, - signer_pubkey: signer.pubkey().to_string(), - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::tests::{ - common::{setup_or_get_test_signer, setup_or_get_test_usage_limiter, RpcMockBuilder}, - config_mock::ConfigMockBuilder, - transaction_mock::create_mock_encoded_transaction, - }; - - #[tokio::test] - async fn test_sign_transaction_if_paid_decode_error() { - let _m = ConfigMockBuilder::new().build_and_setup(); - let _ = setup_or_get_test_signer(); - - let _ = setup_or_get_test_usage_limiter().await; - - let rpc_client = Arc::new(RpcMockBuilder::new().build()); - - let request = SignTransactionIfPaidRequest { - transaction: "invalid_base64!@#$".to_string(), - signer_key: None, - sig_verify: true, - }; - - let result = sign_transaction_if_paid(&rpc_client, request).await; - - assert!(result.is_err(), "Should fail with decode error"); - } - - #[tokio::test] - async fn test_sign_transaction_if_paid_invalid_signer_key() { - let _m = ConfigMockBuilder::new().build_and_setup(); - let _ = setup_or_get_test_signer(); - - let _ = setup_or_get_test_usage_limiter().await; - - let rpc_client = Arc::new(RpcMockBuilder::new().build()); - - let request = SignTransactionIfPaidRequest { - transaction: create_mock_encoded_transaction(), - signer_key: Some("invalid_pubkey".to_string()), - sig_verify: true, - }; - - let result = sign_transaction_if_paid(&rpc_client, request).await; - - assert!(result.is_err(), "Should fail with invalid signer key"); - let error = result.unwrap_err(); - assert!(matches!(error, KoraError::ValidationError(_)), "Should return ValidationError"); - } -} diff --git a/crates/lib/src/rpc_server/openapi/docs.rs b/crates/lib/src/rpc_server/openapi/docs.rs index c9147ff3..64dfb803 100644 --- a/crates/lib/src/rpc_server/openapi/docs.rs +++ b/crates/lib/src/rpc_server/openapi/docs.rs @@ -22,7 +22,6 @@ use crate::rpc_server::{ SignAndSendTransactionRequest, SignAndSendTransactionResponse, }, sign_transaction::{SignTransactionRequest, SignTransactionResponse}, - sign_transaction_if_paid::{SignTransactionIfPaidRequest, SignTransactionIfPaidResponse}, transfer_transaction::{TransferTransactionRequest, TransferTransactionResponse}, }, KoraRpc, @@ -56,8 +55,6 @@ const JSON_CONTENT_TYPE: &str = "application/json"; SignAndSendTransactionResponse, SignTransactionRequest, SignTransactionResponse, - SignTransactionIfPaidRequest, - SignTransactionIfPaidResponse, TransferTransactionRequest, TransferTransactionResponse, )) diff --git a/crates/lib/src/rpc_server/openapi/spec/combined_api.json b/crates/lib/src/rpc_server/openapi/spec/combined_api.json index 685653b0..7bf5856b 100644 --- a/crates/lib/src/rpc_server/openapi/spec/combined_api.json +++ b/crates/lib/src/rpc_server/openapi/spec/combined_api.json @@ -779,129 +779,6 @@ } } }, - "/signTransactionIfPaid": { - "summary": "signTransactionIfPaid", - "post": { - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "jsonrpc", - "id", - "method", - "params" - ], - "properties": { - "id": { - "type": "string", - "description": "An ID to identify the request.", - "enum": [ - "test-account" - ] - }, - "jsonrpc": { - "type": "string", - "description": "The version of the JSON-RPC protocol.", - "enum": [ - "2.0" - ] - }, - "method": { - "type": "string", - "description": "The name of the method to invoke.", - "enum": [ - "signTransactionIfPaid" - ] - }, - "params": { - "type": "object", - "required": [ - "transaction" - ], - "properties": { - "sig_verify": { - "type": "boolean", - "description": "Whether to verify signatures during simulation (defaults to true)" - }, - "signer_key": { - "type": "string", - "description": "Optional signer signer_key to ensure consistency across related RPC calls", - "nullable": true - }, - "transaction": { - "type": "string" - } - } - } - } - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful response", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "transaction", - "signed_transaction", - "signer_pubkey" - ], - "properties": { - "signed_transaction": { - "type": "string" - }, - "signer_pubkey": { - "type": "string", - "description": "Public key of the signer used (for client consistency)" - }, - "transaction": { - "type": "string" - } - } - } - } - } - }, - "429": { - "description": "Exceeded rate limit.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - } - } - } - }, - "500": { - "description": "Internal server error.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - } - } - } - } - } - } - }, "/transferTransaction": { "summary": "transferTransaction", "post": { @@ -1053,8 +930,7 @@ "sign_and_send_transaction", "transfer_transaction", "get_blockhash", - "get_config", - "sign_transaction_if_paid" + "get_config" ], "properties": { "estimate_transaction_fee": { @@ -1081,9 +957,6 @@ "sign_transaction": { "type": "boolean" }, - "sign_transaction_if_paid": { - "type": "boolean" - }, "transfer_transaction": { "type": "boolean" } @@ -1305,46 +1178,6 @@ } } }, - "SignTransactionIfPaidRequest": { - "type": "object", - "required": [ - "transaction" - ], - "properties": { - "sig_verify": { - "type": "boolean", - "description": "Whether to verify signatures during simulation (defaults to true)" - }, - "signer_key": { - "type": "string", - "description": "Optional signer signer_key to ensure consistency across related RPC calls", - "nullable": true - }, - "transaction": { - "type": "string" - } - } - }, - "SignTransactionIfPaidResponse": { - "type": "object", - "required": [ - "transaction", - "signed_transaction", - "signer_pubkey" - ], - "properties": { - "signed_transaction": { - "type": "string" - }, - "signer_pubkey": { - "type": "string", - "description": "Public key of the signer used (for client consistency)" - }, - "transaction": { - "type": "string" - } - } - }, "SignTransactionRequest": { "type": "object", "required": [ diff --git a/crates/lib/src/rpc_server/rpc.rs b/crates/lib/src/rpc_server/rpc.rs index 31f49227..08e96818 100644 --- a/crates/lib/src/rpc_server/rpc.rs +++ b/crates/lib/src/rpc_server/rpc.rs @@ -21,9 +21,6 @@ use crate::rpc_server::method::{ sign_and_send_transaction, SignAndSendTransactionRequest, SignAndSendTransactionResponse, }, sign_transaction::{sign_transaction, SignTransactionRequest, SignTransactionResponse}, - sign_transaction_if_paid::{ - sign_transaction_if_paid, SignTransactionIfPaidRequest, SignTransactionIfPaidResponse, - }, transfer_transaction::{ transfer_transaction, TransferTransactionRequest, TransferTransactionResponse, }, @@ -124,16 +121,6 @@ impl KoraRpc { result } - pub async fn sign_transaction_if_paid( - &self, - request: SignTransactionIfPaidRequest, - ) -> Result { - info!("Sign transaction if paid request: {request:?}"); - let result = sign_transaction_if_paid(&self.rpc_client, request).await; - info!("Sign transaction if paid response: {result:?}"); - result - } - #[cfg(feature = "docs")] pub fn build_docs_spec() -> Vec { vec![ @@ -177,11 +164,6 @@ impl KoraRpc { request: Some(TransferTransactionRequest::schema().1), response: TransferTransactionResponse::schema().1, }, - OpenApiSpec { - name: "signTransactionIfPaid".to_string(), - request: Some(SignTransactionIfPaidRequest::schema().1), - response: SignTransactionIfPaidResponse::schema().1, - }, ] } } diff --git a/crates/lib/src/rpc_server/server.rs b/crates/lib/src/rpc_server/server.rs index 64740ff1..3118fd77 100644 --- a/crates/lib/src/rpc_server/server.rs +++ b/crates/lib/src/rpc_server/server.rs @@ -197,14 +197,6 @@ fn build_rpc_module(rpc: KoraRpc) -> Result, anyhow::Error> { get_blockhash ); register_method_if_enabled!(module, enabled_methods, get_config, "getConfig", get_config); - register_method_if_enabled!( - module, - enabled_methods, - sign_transaction_if_paid, - "signTransactionIfPaid", - sign_transaction_if_paid, - with_params - ); Ok(module) } @@ -267,7 +259,7 @@ mod tests { // Verify that the module has the expected methods let module = result.unwrap(); let method_names: Vec<&str> = module.method_names().collect(); - assert_eq!(method_names.len(), 10); + assert_eq!(method_names.len(), 9); assert!(method_names.contains(&"liveness")); assert!(method_names.contains(&"estimateTransactionFee")); assert!(method_names.contains(&"getSupportedTokens")); @@ -277,7 +269,6 @@ mod tests { assert!(method_names.contains(&"transferTransaction")); assert!(method_names.contains(&"getBlockhash")); assert!(method_names.contains(&"getConfig")); - assert!(method_names.contains(&"signTransactionIfPaid")); } #[test] @@ -292,7 +283,6 @@ mod tests { transfer_transaction: false, get_blockhash: false, get_config: false, - sign_transaction_if_paid: false, liveness: false, }; @@ -324,7 +314,6 @@ mod tests { sign_and_send_transaction: false, transfer_transaction: false, get_blockhash: false, - sign_transaction_if_paid: false, }; let kora_config = KoraConfigBuilder::new().with_enabled_methods(enabled_methods).build(); diff --git a/crates/lib/src/transaction/versioned_transaction.rs b/crates/lib/src/transaction/versioned_transaction.rs index 3d2f306a..0100e6b0 100644 --- a/crates/lib/src/transaction/versioned_transaction.rs +++ b/crates/lib/src/transaction/versioned_transaction.rs @@ -63,11 +63,6 @@ pub trait VersionedTransactionOps { signer: &std::sync::Arc, rpc_client: &RpcClient, ) -> Result<(VersionedTransaction, String), KoraError>; - async fn sign_transaction_if_paid( - &mut self, - signer: &std::sync::Arc, - rpc_client: &RpcClient, - ) -> Result<(VersionedTransaction, String), KoraError>; async fn sign_and_send_transaction( &mut self, signer: &std::sync::Arc, @@ -251,11 +246,40 @@ impl VersionedTransactionOps for VersionedTransactionResolved { rpc_client: &RpcClient, ) -> Result<(VersionedTransaction, String), KoraError> { let fee_payer = signer.pubkey(); + let config = &get_config()?; let validator = TransactionValidator::new(fee_payer)?; // Validate transaction and accounts (already resolved) validator.validate_transaction(self, rpc_client).await?; + // Calculate fee and validate payment if price model requires it + let fee_calculation = FeeConfigUtil::estimate_kora_fee( + rpc_client, + self, + &fee_payer, + config.validation.is_payment_required(), + config.validation.price_source.clone(), + ) + .await?; + + let required_lamports = fee_calculation.total_fee_lamports; + + // Validate payment if price model is not Free + if required_lamports > 0 { + log::error!("Payment validation: required_lamports={}", required_lamports); + // Get the expected payment destination + let payment_destination = config.kora.get_payment_address(&fee_payer)?; + + // Validate token payment using the resolved transaction + TransactionValidator::validate_token_payment( + self, + required_lamports, + rpc_client, + &payment_destination, + ) + .await?; + } + // Get latest blockhash and update transaction let mut transaction = self.transaction.clone(); @@ -288,49 +312,12 @@ impl VersionedTransactionOps for VersionedTransactionResolved { Ok((transaction, encoded)) } - async fn sign_transaction_if_paid( - &mut self, - signer: &std::sync::Arc, - rpc_client: &RpcClient, - ) -> Result<(VersionedTransaction, String), KoraError> { - let fee_payer = signer.pubkey(); - let config = &get_config()?; - - let fee_calculation = FeeConfigUtil::estimate_kora_fee( - rpc_client, - self, - &fee_payer, - config.validation.is_payment_required(), - config.validation.price_source.clone(), - ) - .await?; - - let required_lamports = fee_calculation.total_fee_lamports; - - // Only validate payment if not free - if required_lamports > 0 { - // Get the expected payment destination - let payment_destination = config.kora.get_payment_address(&fee_payer)?; - - // Validate token payment using the resolved transaction - TransactionValidator::validate_token_payment( - self, - required_lamports, - rpc_client, - &payment_destination, - ) - .await?; - } - - // Sign the transaction - self.sign_transaction(signer, rpc_client).await - } - async fn sign_and_send_transaction( &mut self, signer: &std::sync::Arc, rpc_client: &RpcClient, ) -> Result<(String, String), KoraError> { + // Payment validation is handled in sign_transaction let (transaction, encoded) = self.sign_transaction(signer, rpc_client).await?; // Send and confirm transaction diff --git a/crates/lib/src/validator/config_validator.rs b/crates/lib/src/validator/config_validator.rs index 4dd7d82f..ca828c26 100644 --- a/crates/lib/src/validator/config_validator.rs +++ b/crates/lib/src/validator/config_validator.rs @@ -530,7 +530,6 @@ mod tests { transfer_transaction: false, get_blockhash: false, get_config: false, - sign_transaction_if_paid: false, // All false - should warn get_payer_signer: false, }, auth: AuthConfig::default(), diff --git a/docs/getting-started/FULL_DEMO.md b/docs/getting-started/FULL_DEMO.md index 442ab465..6c53d02e 100644 --- a/docs/getting-started/FULL_DEMO.md +++ b/docs/getting-started/FULL_DEMO.md @@ -417,7 +417,7 @@ We then call the same `partiallySignTransactionMessageWithSigners` function to g ### Step 6: Submit Transaction -Finally, we need to get the Kora node to sign the transaction so we can send a fully signed transaction to the network. We do this by calling the `signTransactionIfPaid` method on the Kora client. +Finally, we need to get the Kora node to sign the transaction so we can send a fully signed transaction to the network. We do this by calling the `signTransaction` method on the Kora client. ```ts @@ -431,7 +431,7 @@ async function submitTransaction( console.log('\n[6/6] Signing transaction with Kora and sending to Solana cluster'); // Get Kora's signature - const { signed_transaction } = await client.signTransactionIfPaid({ + const { signed_transaction } = await client.signTransaction({ transaction: signedTransaction, signer_key: signer_address }); @@ -463,7 +463,7 @@ async function submitTransaction( ``` Here we are doing three things: -1. We call the `signTransactionIfPaid` method on the Kora client to get the Kora node to sign the transaction. The node will introspect the transaction to ensure the payment is sufficient and then sign the transaction. _Note: some Kora nodes may enable `signTransaction` that do not require payment. You can check your node's configuration to see if this is enabled by running `getConfig()`._ +1. We call the `signTransaction` method on the Kora client to get the Kora node to sign the transaction. The node will introspect the transaction to ensure the payment is sufficient and then sign the transaction. _Note: some Kora nodes may enable `signTransaction` that do not require payment. You can check your node's configuration to see if this is enabled by running `getConfig()`._ 2. We send the fully signed transaction to the Solana network using the Solana RPC client. 3. We wait for the transaction to be confirmed on the network. diff --git a/docs/getting-started/QUICK_START.md b/docs/getting-started/QUICK_START.md index b9db9b69..0330b1ed 100644 --- a/docs/getting-started/QUICK_START.md +++ b/docs/getting-started/QUICK_START.md @@ -224,7 +224,7 @@ explore additional Kora RPC methods: - `signTransaction` - Sign transactions with the Kora feepayer - `transferTransaction` - Create transfer SOL or SPL token transfer transactions (signed by the Kora feepayer) - `signAndSendTransaction` - Signs a transaction with the Kora feepayer and sends it to the configured Solana RPC -- `signTransactionIfPaid` - Conditionally sign transactions when fees are covered +- `signTransaction` - Conditionally sign transactions when fees are covered - `getPaymentInstruction` - Get a payment instruction for a transaction **Got questions?** Ask questions the [Solana Stack Exchange](https://solana.stackexchange.com/) with a `Kora` tag. \ No newline at end of file diff --git a/docs/getting-started/demo/client/src/full-demo.ts b/docs/getting-started/demo/client/src/full-demo.ts index 0a5505c1..8711090d 100644 --- a/docs/getting-started/demo/client/src/full-demo.ts +++ b/docs/getting-started/demo/client/src/full-demo.ts @@ -1,281 +1,314 @@ - import { KoraClient } from "@kora/sdk"; import { - createKeyPairSignerFromBytes, - getBase58Encoder, - createNoopSigner, - address, - getBase64EncodedWireTransaction, - partiallySignTransactionMessageWithSigners, - Blockhash, - Base64EncodedWireTransaction, - partiallySignTransaction, - TransactionVersion, - Instruction, - KeyPairSigner, - Rpc, - SolanaRpcApi + createKeyPairSignerFromBytes, + getBase58Encoder, + createNoopSigner, + address, + getBase64EncodedWireTransaction, + partiallySignTransactionMessageWithSigners, + Blockhash, + Base64EncodedWireTransaction, + partiallySignTransaction, + TransactionVersion, + Instruction, + KeyPairSigner, + Rpc, + SolanaRpcApi, } from "@solana/kit"; -import { - createTransaction, - createSolanaClient, - getExplorerLink -} from "gill"; +import { createTransaction, createSolanaClient, getExplorerLink } from "gill"; import { getAddMemoInstruction } from "@solana-program/memo"; import { createRecentSignatureConfirmationPromiseFactory } from "@solana/transaction-confirmation"; import dotenv from "dotenv"; import path from "path"; -dotenv.config({ path: path.join(process.cwd(), '..', '.env') }); +dotenv.config({ path: path.join(process.cwd(), "..", ".env") }); const CONFIG = { - computeUnitLimit: 200_000n, - computeUnitPrice: 1_000_000n, - transactionVersion: 0, - solanaRpcUrl: "http://127.0.0.1:8899", - koraRpcUrl: "http://localhost:8080/", -} + computeUnitLimit: 200_000n, + computeUnitPrice: 1_000_000n, + transactionVersion: 0, + solanaRpcUrl: "http://127.0.0.1:8899", + koraRpcUrl: "http://localhost:8080/", +}; async function getEnvKeyPair(envKey: string) { - if (!process.env[envKey]) { - throw new Error(`Environment variable ${envKey} is not set`); - } - const base58Encoder = getBase58Encoder(); - const b58SecretEncoded = base58Encoder.encode(process.env[envKey]); - return await createKeyPairSignerFromBytes(b58SecretEncoded); + if (!process.env[envKey]) { + throw new Error(`Environment variable ${envKey} is not set`); + } + const base58Encoder = getBase58Encoder(); + const b58SecretEncoded = base58Encoder.encode(process.env[envKey]); + return await createKeyPairSignerFromBytes(b58SecretEncoded); } async function initializeClients() { - console.log('\n[1/6] Initializing clients'); - console.log(' โ†’ Kora RPC:', CONFIG.koraRpcUrl); - console.log(' โ†’ Solana RPC:', CONFIG.solanaRpcUrl); - - const client = new KoraClient({ - rpcUrl: CONFIG.koraRpcUrl, - // apiKey: process.env.KORA_API_KEY, // Uncomment if you have authentication enabled in your kora.toml - // hmacSecret: process.env.KORA_HMAC_SECRET, // Uncomment if you have authentication enabled in your kora.toml - }); - - const { rpc, rpcSubscriptions } = createSolanaClient({ - urlOrMoniker: CONFIG.solanaRpcUrl, - }); - - const confirmTransaction = createRecentSignatureConfirmationPromiseFactory({ rpc, rpcSubscriptions }); - - return { client, rpc, confirmTransaction }; + console.log("\n[1/6] Initializing clients"); + console.log(" โ†’ Kora RPC:", CONFIG.koraRpcUrl); + console.log(" โ†’ Solana RPC:", CONFIG.solanaRpcUrl); + + const client = new KoraClient({ + rpcUrl: CONFIG.koraRpcUrl, + // apiKey: process.env.KORA_API_KEY, // Uncomment if you have authentication enabled in your kora.toml + // hmacSecret: process.env.KORA_HMAC_SECRET, // Uncomment if you have authentication enabled in your kora.toml + }); + + const { rpc, rpcSubscriptions } = createSolanaClient({ + urlOrMoniker: CONFIG.solanaRpcUrl, + }); + + const confirmTransaction = createRecentSignatureConfirmationPromiseFactory({ + rpc, + rpcSubscriptions, + }); + + return { client, rpc, confirmTransaction }; } async function setupKeys(client: KoraClient) { - console.log('\n[2/6] Setting up keypairs'); - - const testSenderKeypair = await getEnvKeyPair('TEST_SENDER_KEYPAIR'); - const destinationKeypair = await getEnvKeyPair('DESTINATION_KEYPAIR'); - const { signer_address } = await client.getPayerSigner(); - - console.log(' โ†’ Sender:', testSenderKeypair.address); - console.log(' โ†’ Destination:', destinationKeypair.address); - console.log(' โ†’ Kora signer address:', signer_address); - - return { testSenderKeypair, destinationKeypair, signer_address }; + console.log("\n[2/6] Setting up keypairs"); + + const testSenderKeypair = await getEnvKeyPair("TEST_SENDER_KEYPAIR"); + const destinationKeypair = await getEnvKeyPair("DESTINATION_KEYPAIR"); + const { signer_address } = await client.getPayerSigner(); + + console.log(" โ†’ Sender:", testSenderKeypair.address); + console.log(" โ†’ Destination:", destinationKeypair.address); + console.log(" โ†’ Kora signer address:", signer_address); + + return { testSenderKeypair, destinationKeypair, signer_address }; } async function createInstructions( - client: KoraClient, - testSenderKeypair: KeyPairSigner, - destinationKeypair: KeyPairSigner + client: KoraClient, + testSenderKeypair: KeyPairSigner, + destinationKeypair: KeyPairSigner ) { - console.log('\n[3/6] Creating demonstration instructions'); - - const paymentToken = await client.getConfig().then(config => config.validation_config.allowed_spl_paid_tokens[0]); - console.log(' โ†’ Payment token:', paymentToken); - - // Create token transfer (will initialize ATA if needed) - const transferTokens = await client.transferTransaction({ - amount: 10_000_000, // 10 USDC (6 decimals) - token: paymentToken, - source: testSenderKeypair.address, - destination: destinationKeypair.address // todo replace with a generated address to test ata creation - }); - console.log(' โœ“ Token transfer instruction created'); - - // Create SOL transfer - const transferSol = await client.transferTransaction({ - amount: 10_000_000, // 0.01 SOL (9 decimals) - token: '11111111111111111111111111111111', // SOL mint address - source: testSenderKeypair.address, - destination: destinationKeypair.address - }); - console.log(' โœ“ SOL transfer instruction created'); - - // Add memo instruction - const memoInstruction = getAddMemoInstruction({ - memo: 'Hello, Kora!', - }); - console.log(' โœ“ Memo instruction created'); - - const instructions = [ - ...transferTokens.instructions, - ...transferSol.instructions, - memoInstruction - ]; - - console.log(` โ†’ Total: ${instructions.length} instructions`); - return { instructions, paymentToken }; + console.log("\n[3/6] Creating demonstration instructions"); + + const paymentToken = await client + .getConfig() + .then((config) => config.validation_config.allowed_spl_paid_tokens[0]); + console.log(" โ†’ Payment token:", paymentToken); + + // Create token transfer (will initialize ATA if needed) + const transferTokens = await client.transferTransaction({ + amount: 10_000_000, // 10 USDC (6 decimals) + token: paymentToken, + source: testSenderKeypair.address, + destination: destinationKeypair.address, // todo replace with a generated address to test ata creation + }); + console.log(" โœ“ Token transfer instruction created"); + + // Create SOL transfer + const transferSol = await client.transferTransaction({ + amount: 10_000_000, // 0.01 SOL (9 decimals) + token: "11111111111111111111111111111111", // SOL mint address + source: testSenderKeypair.address, + destination: destinationKeypair.address, + }); + console.log(" โœ“ SOL transfer instruction created"); + + // Add memo instruction + const memoInstruction = getAddMemoInstruction({ + memo: "Hello, Kora!", + }); + console.log(" โœ“ Memo instruction created"); + + const instructions = [ + ...transferTokens.instructions, + ...transferSol.instructions, + memoInstruction, + ]; + + console.log(` โ†’ Total: ${instructions.length} instructions`); + return { instructions, paymentToken }; } async function getPaymentInstruction( - client: KoraClient, - instructions: Instruction[], - testSenderKeypair: KeyPairSigner, - paymentToken: string + client: KoraClient, + instructions: Instruction[], + testSenderKeypair: KeyPairSigner, + paymentToken: string ): Promise<{ paymentInstruction: Instruction }> { - console.log('\n[4/6] Estimating Kora fee and assembling payment instruction'); - - const { signer_address } = await client.getPayerSigner(); - const noopSigner = createNoopSigner(address(signer_address)); - const latestBlockhash = await client.getBlockhash(); - - console.log(' โ†’ Fee payer:', signer_address.slice(0, 8) + '...'); - console.log(' โ†’ Blockhash:', latestBlockhash.blockhash.slice(0, 8) + '...'); - - // Create estimate transaction to get payment instruction - const estimateTransaction = await createTransaction({ - version: CONFIG.transactionVersion as TransactionVersion, - instructions, - feePayer: noopSigner, - latestBlockhash: { - blockhash: latestBlockhash.blockhash as Blockhash, - lastValidBlockHeight: 0n, - }, - computeUnitPrice: CONFIG.computeUnitPrice, - computeUnitLimit: CONFIG.computeUnitLimit - }); - - const signedEstimateTransaction = await partiallySignTransactionMessageWithSigners(estimateTransaction); - const base64EncodedWireTransaction = getBase64EncodedWireTransaction(signedEstimateTransaction); - console.log(' โœ“ Estimate transaction built'); - - // Get payment instruction from Kora - const paymentInstruction = await client.getPaymentInstruction({ - transaction: base64EncodedWireTransaction, - fee_token: paymentToken, - source_wallet: testSenderKeypair.address, - }); - console.log(' โœ“ Payment instruction received from Kora'); - - return { paymentInstruction: paymentInstruction.payment_instruction }; + console.log("\n[4/6] Estimating Kora fee and assembling payment instruction"); + + const { signer_address } = await client.getPayerSigner(); + const noopSigner = createNoopSigner(address(signer_address)); + const latestBlockhash = await client.getBlockhash(); + + console.log(" โ†’ Fee payer:", signer_address.slice(0, 8) + "..."); + console.log(" โ†’ Blockhash:", latestBlockhash.blockhash.slice(0, 8) + "..."); + + // Create estimate transaction to get payment instruction + const estimateTransaction = await createTransaction({ + version: CONFIG.transactionVersion as TransactionVersion, + instructions, + feePayer: noopSigner, + latestBlockhash: { + blockhash: latestBlockhash.blockhash as Blockhash, + lastValidBlockHeight: 0n, + }, + computeUnitPrice: CONFIG.computeUnitPrice, + computeUnitLimit: CONFIG.computeUnitLimit, + }); + + const signedEstimateTransaction = + await partiallySignTransactionMessageWithSigners(estimateTransaction); + const base64EncodedWireTransaction = getBase64EncodedWireTransaction( + signedEstimateTransaction + ); + console.log(" โœ“ Estimate transaction built"); + + // Get payment instruction from Kora + const paymentInstruction = await client.getPaymentInstruction({ + transaction: base64EncodedWireTransaction, + fee_token: paymentToken, + source_wallet: testSenderKeypair.address, + }); + console.log(" โœ“ Payment instruction received from Kora"); + + return { paymentInstruction: paymentInstruction.payment_instruction }; } async function getFinalTransaction( - client: KoraClient, - paymentInstruction: Instruction, - testSenderKeypair: KeyPairSigner, - instructions: Instruction[], - signer_address: string + client: KoraClient, + paymentInstruction: Instruction, + testSenderKeypair: KeyPairSigner, + instructions: Instruction[], + signer_address: string ): Promise { - console.log('\n[5/6] Creating and signing final transaction (with payment)'); - const noopSigner = createNoopSigner(address(signer_address)); - - // Build final transaction with payment instruction - const newBlockhash = await client.getBlockhash(); - const fullTransaction = await createTransaction({ - version: CONFIG.transactionVersion as TransactionVersion, - instructions: [...instructions, paymentInstruction], - feePayer: noopSigner, - latestBlockhash: { - blockhash: newBlockhash.blockhash as Blockhash, - lastValidBlockHeight: 0n, - }, - computeUnitPrice: CONFIG.computeUnitPrice, - computeUnitLimit: CONFIG.computeUnitLimit - }); - console.log(' โœ“ Final transaction built with payment'); - - // Sign with user keypair - const signedFullTransaction = await partiallySignTransactionMessageWithSigners(fullTransaction); - const userSignedTransaction = await partiallySignTransaction([testSenderKeypair.keyPair], signedFullTransaction); - const base64EncodedWireFullTransaction = getBase64EncodedWireTransaction(userSignedTransaction); - console.log(' โœ“ Transaction signed by user'); - - return base64EncodedWireFullTransaction; + console.log("\n[5/6] Creating and signing final transaction (with payment)"); + const noopSigner = createNoopSigner(address(signer_address)); + + // Build final transaction with payment instruction + const newBlockhash = await client.getBlockhash(); + const fullTransaction = await createTransaction({ + version: CONFIG.transactionVersion as TransactionVersion, + instructions: [...instructions, paymentInstruction], + feePayer: noopSigner, + latestBlockhash: { + blockhash: newBlockhash.blockhash as Blockhash, + lastValidBlockHeight: 0n, + }, + computeUnitPrice: CONFIG.computeUnitPrice, + computeUnitLimit: CONFIG.computeUnitLimit, + }); + console.log(" โœ“ Final transaction built with payment"); + + // Sign with user keypair + const signedFullTransaction = + await partiallySignTransactionMessageWithSigners(fullTransaction); + const userSignedTransaction = await partiallySignTransaction( + [testSenderKeypair.keyPair], + signedFullTransaction + ); + const base64EncodedWireFullTransaction = getBase64EncodedWireTransaction( + userSignedTransaction + ); + console.log(" โœ“ Transaction signed by user"); + + return base64EncodedWireFullTransaction; } async function submitTransaction( - client: KoraClient, - rpc: Rpc, - confirmTransaction: ReturnType, - signedTransaction: Base64EncodedWireTransaction, - signer_address: string + client: KoraClient, + rpc: Rpc, + confirmTransaction: ReturnType< + typeof createRecentSignatureConfirmationPromiseFactory + >, + signedTransaction: Base64EncodedWireTransaction, + signer_address: string ) { - console.log('\n[6/6] Signing transaction with Kora and sending to Solana cluster'); - - // Get Kora's signature - const { signed_transaction } = await client.signTransactionIfPaid({ - transaction: signedTransaction, - signer_key: signer_address - }); - console.log(' โœ“ Transaction co-signed by Kora'); - - // Submit to Solana network - const signature = await rpc.sendTransaction(signed_transaction as Base64EncodedWireTransaction, { - encoding: 'base64' - }).send(); - console.log(' โœ“ Transaction submitted to network'); - - console.log(' โณ Awaiting confirmation...'); - await confirmTransaction({ - commitment: 'confirmed', - signature, - abortSignal: new AbortController().signal - }); - - console.log('\nโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”'); - console.log('SUCCESS: Transaction confirmed on Solana'); - console.log('โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”'); - console.log('\nTransaction signature:'); - console.log(signature); - console.log('\nView on explorer:'); - console.log(getExplorerLink({transaction: signature, cluster: 'localhost'})); - - return signature; + console.log( + "\n[6/6] Signing transaction with Kora and sending to Solana cluster" + ); + + // Get Kora's signature + const { signed_transaction } = await client.signTransaction({ + transaction: signedTransaction, + signer_key: signer_address, + }); + console.log(" โœ“ Transaction co-signed by Kora"); + + // Submit to Solana network + const signature = await rpc + .sendTransaction(signed_transaction as Base64EncodedWireTransaction, { + encoding: "base64", + }) + .send(); + console.log(" โœ“ Transaction submitted to network"); + + console.log(" โณ Awaiting confirmation..."); + await confirmTransaction({ + commitment: "confirmed", + signature, + abortSignal: new AbortController().signal, + }); + + console.log("\nโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”"); + console.log("SUCCESS: Transaction confirmed on Solana"); + console.log("โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”"); + console.log("\nTransaction signature:"); + console.log(signature); + console.log("\nView on explorer:"); + console.log( + getExplorerLink({ transaction: signature, cluster: "localhost" }) + ); + + return signature; } async function main() { - console.log('\nโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”'); - console.log('KORA GASLESS TRANSACTION DEMO'); - console.log('โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”'); - - try { - // Step 1: Initialize clients - const { client, rpc, confirmTransaction } = await initializeClients(); - - // Step 2: Setup keys - const { testSenderKeypair, destinationKeypair, signer_address } = await setupKeys(client); - - // Step 3: Create demo instructions - const { instructions, paymentToken } = await createInstructions(client, testSenderKeypair, destinationKeypair); - - // Step 4: Get payment instruction from Kora - const { paymentInstruction } = await getPaymentInstruction(client, instructions, testSenderKeypair, paymentToken); - - // Step 5: Create and partially sign final transaction - const finalSignedTransaction = await getFinalTransaction( - client, - paymentInstruction, - testSenderKeypair, - instructions, - signer_address - ); - - // Step 6: Get Kora's signature and submit to Solana cluster - await submitTransaction(client, rpc, confirmTransaction, finalSignedTransaction, signer_address); - } catch (error) { - console.error('\nโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”'); - console.error('ERROR: Demo failed'); - console.error('โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”'); - console.error('\nDetails:', error); - process.exit(1); - } + console.log("\nโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”"); + console.log("KORA GASLESS TRANSACTION DEMO"); + console.log("โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”"); + + try { + // Step 1: Initialize clients + const { client, rpc, confirmTransaction } = await initializeClients(); + + // Step 2: Setup keys + const { testSenderKeypair, destinationKeypair, signer_address } = + await setupKeys(client); + + // Step 3: Create demo instructions + const { instructions, paymentToken } = await createInstructions( + client, + testSenderKeypair, + destinationKeypair + ); + + // Step 4: Get payment instruction from Kora + const { paymentInstruction } = await getPaymentInstruction( + client, + instructions, + testSenderKeypair, + paymentToken + ); + + // Step 5: Create and partially sign final transaction + const finalSignedTransaction = await getFinalTransaction( + client, + paymentInstruction, + testSenderKeypair, + instructions, + signer_address + ); + + // Step 6: Get Kora's signature and submit to Solana cluster + await submitTransaction( + client, + rpc, + confirmTransaction, + finalSignedTransaction, + signer_address + ); + } catch (error) { + console.error("\nโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”"); + console.error("ERROR: Demo failed"); + console.error("โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”"); + console.error("\nDetails:", error); + process.exit(1); + } } -main().catch(e => console.error('Error:', e)); \ No newline at end of file +main().catch((e) => console.error("Error:", e)); diff --git a/docs/getting-started/demo/server/kora.toml b/docs/getting-started/demo/server/kora.toml index f716e109..1aca8917 100644 --- a/docs/getting-started/demo/server/kora.toml +++ b/docs/getting-started/demo/server/kora.toml @@ -17,7 +17,6 @@ sign_and_send_transaction = true transfer_transaction = true get_blockhash = true get_config = true -sign_transaction_if_paid = true get_payer_signer = true [validation] @@ -30,7 +29,7 @@ allowed_programs = [ "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", # Associated Token Program "AddressLookupTab1e1111111111111111111111111", # Address Lookup Table Program "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr", # Memo Program - "ComputeBudget111111111111111111111111111111", # Compute Budget Program + "ComputeBudget111111111111111111111111111111", # Compute Budget Program ] allowed_tokens = [ "9BgeTKqmFsPVnfYscfM6NvsgmZxei7XfdciShQ6D3bxJ", # Update this based on the USDC_LOCAL_KEY public key comment in your .env @@ -87,7 +86,7 @@ allow_thaw_account = true [validation.price] type = "margin" # free / margin / fixed -margin = 0.1 # Default margin (10%) for paid transaction validation +margin = 0.1 # Default margin (10%) for paid transaction validation [validation.token2022] @@ -117,4 +116,4 @@ blocked_account_extensions = [ enabled = false cache_url = "redis://redis:6379" max_transactions = 2 -fallback_if_unavailable = false \ No newline at end of file +fallback_if_unavailable = false diff --git a/docs/operators/CONFIGURATION.md b/docs/operators/CONFIGURATION.md index 4632b069..bdd6c35c 100644 --- a/docs/operators/CONFIGURATION.md +++ b/docs/operators/CONFIGURATION.md @@ -161,7 +161,6 @@ sign_and_send_transaction = false transfer_transaction = false get_blockhash = true get_config = true -sign_transaction_if_paid = true get_payer_signer = true ``` @@ -175,7 +174,6 @@ get_payer_signer = true | `transfer_transaction` | Handle token transfers | โœ… | boolean | | `get_blockhash` | Get a recent blockhash | โœ… | boolean | | `get_config` | Return the Kora server config | โœ… | boolean | -| `sign_transaction_if_paid` | Conditional signing if token payment instruction is provided | โœ… | boolean | > *Note: if this section is included in your `kora.toml` file, all methods must explicitly be set to `true` or `false`.* @@ -424,7 +422,6 @@ sign_and_send_transaction = false transfer_transaction = false get_blockhash = true get_config = true -sign_transaction_if_paid = true get_payer_signer = true [validation] diff --git a/docs/operators/FEES.md b/docs/operators/FEES.md index 934c0a3c..542a043e 100644 --- a/docs/operators/FEES.md +++ b/docs/operators/FEES.md @@ -2,7 +2,7 @@ *Last updated: 2025-09-02* -Kora estimates transaction fees when performing `estimate_transaction_fee` and `sign_transaction_if_paid` RPC methods. To estimate fees, Kora calculates the total cost for executing transactions on Solana, including network fees, account creation costs, and optional payment processing fees. This guide breaks down each component of the fee calculation. +Kora estimates transaction fees when performing `estimate_transaction_fee`, `sign_transaction` and `sign_and_send_transaction` RPC methods. To estimate fees, Kora calculates the total cost for executing transactions on Solana, including network fees, account creation costs, and optional payment processing fees. This guide breaks down each component of the fee calculation. ## Fee Calculation Formula diff --git a/docs/operators/deploy/sample/kora.toml b/docs/operators/deploy/sample/kora.toml index a50f93e2..f6332ce7 100644 --- a/docs/operators/deploy/sample/kora.toml +++ b/docs/operators/deploy/sample/kora.toml @@ -89,7 +89,6 @@ sign_and_send_transaction = true transfer_transaction = true get_blockhash = true get_config = true -sign_transaction_if_paid = true get_payer_signer = true [validation.price] diff --git a/docs/x402/demo/kora/kora.toml b/docs/x402/demo/kora/kora.toml index 8c473ec6..75f9036a 100644 --- a/docs/x402/demo/kora/kora.toml +++ b/docs/x402/demo/kora/kora.toml @@ -20,7 +20,6 @@ sign_and_send_transaction = true transfer_transaction = false get_blockhash = true get_config = true -sign_transaction_if_paid = false get_payer_signer = true [validation] @@ -28,8 +27,8 @@ max_allowed_lamports = 1000000 max_signatures = 10 price_source = "Mock" allowed_programs = [ - "11111111111111111111111111111111", # System Program - "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", # Token Program + "11111111111111111111111111111111", # System Program + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", # Token Program # "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb", # Token-2022 Program "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", # Associated Token Program "ComputeBudget111111111111111111111111111111", # Compute Budget Program @@ -41,8 +40,7 @@ allowed_spl_paid_tokens = [ "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU", # USDC devnet ] -disallowed_accounts = [ -] +disallowed_accounts = [] [validation.fee_payer_policy] @@ -91,29 +89,29 @@ type = "free" # free / margin / fixed [validation.token2022] blocked_mint_extensions = [ - "confidential_transfer_mint", # Confidential transfer configuration for the mint - "confidential_mint_burn", # Confidential mint and burn configuration - "transfer_fee_config", # Transfer fee configuration - "mint_close_authority", # Authority allowed to close the mint - "interest_bearing_config", # Interest-bearing token configuration - "non_transferable", # Makes tokens non-transferable - "permanent_delegate", # Permanent delegate for the mint - "transfer_hook", # Block tokens with transfer hooks - "pausable", # Block pausable tokens + "confidential_transfer_mint", # Confidential transfer configuration for the mint + "confidential_mint_burn", # Confidential mint and burn configuration + "transfer_fee_config", # Transfer fee configuration + "mint_close_authority", # Authority allowed to close the mint + "interest_bearing_config", # Interest-bearing token configuration + "non_transferable", # Makes tokens non-transferable + "permanent_delegate", # Permanent delegate for the mint + "transfer_hook", # Block tokens with transfer hooks + "pausable", # Block pausable tokens ] blocked_account_extensions = [ "confidential_transfer_account", # Confidential transfer state for the account "non_transferable_account", # Non-transferable token account - "transfer_hook_account", # Transfer hook state for the account - "pausable_account", # Pausable token account state - "memo_transfer", # Requires memo for transfers - "cpi_guard", # Prevents certain CPI calls - "immutable_owner", # Account owner cannot be changed - "default_account_state", # Default state for new accounts + "transfer_hook_account", # Transfer hook state for the account + "pausable_account", # Pausable token account state + "memo_transfer", # Requires memo for transfers + "cpi_guard", # Prevents certain CPI calls + "immutable_owner", # Account owner cannot be changed + "default_account_state", # Default state for new accounts ] [kora.usage_limit] enabled = false cache_url = "redis://redis:6379" max_transactions = 2 -fallback_if_unavailable = false \ No newline at end of file +fallback_if_unavailable = false diff --git a/kora.toml b/kora.toml index 9adabbf5..d94b5a83 100644 --- a/kora.toml +++ b/kora.toml @@ -20,7 +20,6 @@ sign_and_send_transaction = true transfer_transaction = true get_blockhash = true get_config = true -sign_transaction_if_paid = true get_payer_signer = true [validation] diff --git a/openapi.json b/openapi.json index a51619d6..140f9fbf 100644 --- a/openapi.json +++ b/openapi.json @@ -21,8 +21,7 @@ "sign_and_send_transaction", "transfer_transaction", "get_blockhash", - "get_config", - "sign_transaction_if_paid" + "get_config" ], "properties": { "estimate_transaction_fee": { @@ -46,9 +45,6 @@ "sign_transaction": { "type": "boolean" }, - "sign_transaction_if_paid": { - "type": "boolean" - }, "transfer_transaction": { "type": "boolean" } @@ -240,32 +236,6 @@ } } }, - "SignTransactionIfPaidRequest": { - "type": "object", - "required": [ - "transaction" - ], - "properties": { - "transaction": { - "type": "string" - } - } - }, - "SignTransactionIfPaidResponse": { - "type": "object", - "required": [ - "transaction", - "signed_transaction" - ], - "properties": { - "signed_transaction": { - "type": "string" - }, - "transaction": { - "type": "string" - } - } - }, "SignTransactionRequest": { "type": "object", "required": [ diff --git a/sdks/ts/docs/README.md b/sdks/ts/docs/README.md index a42ff057..efd169ce 100644 --- a/sdks/ts/docs/README.md +++ b/sdks/ts/docs/README.md @@ -34,7 +34,6 @@ const config = await client.getConfig(); - [getSupportedTokens()](#getsupportedtokens) - [signAndSendTransaction()](#signandsendtransaction) - [signTransaction()](#signtransaction) -- [signTransactionIfPaid()](#signtransactionifpaid) - [transferTransaction()](#transfertransaction) #### Constructors @@ -301,39 +300,6 @@ console.log('Signature:', result.signature); console.log('Signed tx:', result.signed_transaction); ``` -##### signTransactionIfPaid() - -```ts -signTransactionIfPaid(request: SignTransactionIfPaidRequest): Promise; -``` - -Signs a transaction only if it includes proper payment to the fee payer. - -###### Parameters - -| Parameter | Type | Description | -| ------ | ------ | ------ | -| `request` | [`SignTransactionIfPaidRequest`](#signtransactionifpaidrequest) | Conditional sign request parameters | - -###### Returns - -`Promise`\<[`SignTransactionIfPaidResponse`](#signtransactionifpaidresponse)\> - -The original and signed transaction - -###### Throws - -When the RPC call fails or payment validation fails - -###### Example - -```typescript -const result = await client.signTransactionIfPaid({ - transaction: 'base64EncodedTransaction' -}); -console.log('Signed transaction:', result.signed_transaction); -``` - ##### transferTransaction() ```ts @@ -417,7 +383,6 @@ Enabled status for methods for the Kora server. | `liveness` | `boolean` | Whether the liveness method is enabled | | `sign_and_send_transaction` | `boolean` | Whether the sign_and_send_transaction method is enabled | | `sign_transaction` | `boolean` | Whether the sign_transaction method is enabled | -| `sign_transaction_if_paid` | `boolean` | Whether the sign_transaction_if_paid method is enabled | | `transfer_transaction` | `boolean` | Whether the transfer_transaction method is enabled | *** @@ -617,34 +582,6 @@ Response from signing and sending a transaction. *** -### SignTransactionIfPaidRequest - -Parameters for conditionally signing a transaction based on payment. - -#### Properties - -| Property | Type | Description | -| ------ | ------ | ------ | -| `sig_verify?` | `boolean` | Optional signer verification during transaction simulation (defaults to false) | -| `signer_key?` | `string` | Optional signer address for the transaction | -| `transaction` | `string` | Base64-encoded transaction | - -*** - -### SignTransactionIfPaidResponse - -Response from conditionally signing a transaction. - -#### Properties - -| Property | Type | Description | -| ------ | ------ | ------ | -| `signed_transaction` | `string` | Base64-encoded signed transaction | -| `signer_pubkey` | `string` | Public key of the signer used to sign the transaction | -| `transaction` | `string` | Base64-encoded original transaction | - -*** - ### SignTransactionRequest Parameters for signing a transaction. diff --git a/sdks/ts/scripts/generate-api-docs.js b/sdks/ts/scripts/generate-api-docs.js index f6c0a11f..e1bff55b 100644 --- a/sdks/ts/scripts/generate-api-docs.js +++ b/sdks/ts/scripts/generate-api-docs.js @@ -316,7 +316,6 @@ function generateAPIDocs() { 'getSupportedTokens', 'signAndSendTransaction', 'signTransaction', - 'signTransactionIfPaid', 'transferTransaction' ]; diff --git a/sdks/ts/src/client.ts b/sdks/ts/src/client.ts index 6b4a65ce..45d9a458 100644 --- a/sdks/ts/src/client.ts +++ b/sdks/ts/src/client.ts @@ -8,8 +8,6 @@ import { GetSupportedTokensResponse, SignAndSendTransactionRequest, SignAndSendTransactionResponse, - SignTransactionIfPaidRequest, - SignTransactionIfPaidResponse, SignTransactionRequest, SignTransactionResponse, TransferTransactionRequest, @@ -235,28 +233,6 @@ export class KoraClient { ); } - /** - * Signs a transaction only if it includes proper payment to the fee payer. - * @param request - Conditional sign request parameters - * @param request.transaction - Base64-encoded transaction to conditionally sign - * @returns The original and signed transaction - * @throws {Error} When the RPC call fails or payment validation fails - * - * @example - * ```typescript - * const result = await client.signTransactionIfPaid({ - * transaction: 'base64EncodedTransaction' - * }); - * console.log('Signed transaction:', result.signed_transaction); - * ``` - */ - async signTransactionIfPaid(request: SignTransactionIfPaidRequest): Promise { - return this.rpcRequest( - 'signTransactionIfPaid', - request, - ); - } - /** * Creates a token transfer transaction with Kora as the fee payer. * @param request - Transfer request parameters diff --git a/sdks/ts/src/types/index.ts b/sdks/ts/src/types/index.ts index bd36db0a..0ea23ea8 100644 --- a/sdks/ts/src/types/index.ts +++ b/sdks/ts/src/types/index.ts @@ -44,18 +44,6 @@ export interface SignAndSendTransactionRequest { sig_verify?: boolean; } -/** - * Parameters for conditionally signing a transaction based on payment. - */ -export interface SignTransactionIfPaidRequest { - /** Base64-encoded transaction */ - transaction: string; - /** Optional signer address for the transaction */ - signer_key?: string; - /** Optional signer verification during transaction simulation (defaults to false) */ - sig_verify?: boolean; -} - /** * Parameters for estimating transaction fees. */ @@ -128,18 +116,6 @@ export interface SignAndSendTransactionResponse { signer_pubkey: string; } -/** - * Response from conditionally signing a transaction. - */ -export interface SignTransactionIfPaidResponse { - /** Base64-encoded original transaction */ - transaction: string; - /** Base64-encoded signed transaction */ - signed_transaction: string; - /** Public key of the signer used to sign the transaction */ - signer_pubkey: string; -} - /** * Response containing the latest blockhash. */ @@ -276,8 +252,6 @@ export interface EnabledMethods { get_blockhash: boolean; /** Whether the get_config method is enabled */ get_config: boolean; - /** Whether the sign_transaction_if_paid method is enabled */ - sign_transaction_if_paid: boolean; } /** diff --git a/sdks/ts/test/integration.test.ts b/sdks/ts/test/integration.test.ts index a628c7b3..1294cdea 100644 --- a/sdks/ts/test/integration.test.ts +++ b/sdks/ts/test/integration.test.ts @@ -110,7 +110,6 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without expect(config.enabled_methods.transfer_transaction).toBeDefined(); expect(config.enabled_methods.get_blockhash).toBeDefined(); expect(config.enabled_methods.get_config).toBeDefined(); - expect(config.enabled_methods.sign_transaction_if_paid).toBeDefined(); }); it('should get payer signer', async () => { @@ -195,29 +194,37 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without }); it('should sign transaction', async () => { + const config = await client.getConfig(); + const paymentAddress = config.fee_payers[0]; const transferRequest = { amount: 1000000, token: usdcMint, source: testWalletAddress, - destination: destinationAddress, + destination: paymentAddress, }; const { transaction } = await client.transferTransaction(transferRequest); - const signResult = await client.signTransaction({ transaction }); + + const signResult = await client.signTransaction({ + transaction, + }); expect(signResult).toBeDefined(); expect(signResult.signed_transaction).toBeDefined(); }); it('should sign and send transaction', async () => { + const config = await client.getConfig(); + const paymentAddress = config.fee_payers[0]; const transferRequest = { amount: 1000000, token: usdcMint, source: testWalletAddress, - destination: destinationAddress, + destination: paymentAddress, }; const { transaction: transactionString } = await client.transferTransaction(transferRequest); + const transaction = transactionFromBase64(transactionString); // Sign transaction with test wallet before sending const signedTransaction = await signTransaction([testWallet.keyPair], transaction); @@ -230,26 +237,6 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without expect(signResult.signed_transaction).toBeDefined(); }); - it('should sign transaction if paid', async () => { - const config = await client.getConfig(); - const paymentAddress = config.fee_payers[0]; - const transferRequest = { - amount: 1000000, - token: usdcMint, - source: testWalletAddress, - destination: paymentAddress, - }; - - const { transaction } = await client.transferTransaction(transferRequest); - - const signResult = await client.signTransactionIfPaid({ - transaction, - }); - - expect(signResult).toBeDefined(); - expect(signResult.signed_transaction).toBeDefined(); - }); - it('should get payment instruction', async () => { const transferRequest = { amount: 1000000, @@ -351,11 +338,13 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without describe('End-to-End Flows', () => { it('should handle transfer and sign flow', async () => { + const config = await client.getConfig(); + const paymentAddress = config.fee_payers[0]; const request = { amount: 1000000, token: usdcMint, source: testWalletAddress, - destination: destinationAddress, + destination: paymentAddress, }; // Create and sign the transaction diff --git a/sdks/ts/test/unit.test.ts b/sdks/ts/test/unit.test.ts index 19534ba1..7b48ea6c 100644 --- a/sdks/ts/test/unit.test.ts +++ b/sdks/ts/test/unit.test.ts @@ -9,8 +9,6 @@ import { SignTransactionResponse, SignAndSendTransactionRequest, SignAndSendTransactionResponse, - SignTransactionIfPaidRequest, - SignTransactionIfPaidResponse, TransferTransactionRequest, TransferTransactionResponse, EstimateTransactionFeeResponse, @@ -175,7 +173,6 @@ describe('KoraClient Unit Tests', () => { transfer_transaction: true, get_blockhash: true, get_config: true, - sign_transaction_if_paid: true, }, }; @@ -284,30 +281,6 @@ describe('KoraClient Unit Tests', () => { }); }); - describe('signTransactionIfPaid', () => { - const testSignTransactionIfPaid = async (margin?: number) => { - const request: SignTransactionIfPaidRequest = { - transaction: 'base64_encoded_transaction', - ...(margin !== undefined && { margin }), - }; - const mockResponse: SignTransactionIfPaidResponse = { - transaction: 'base64_encoded_transaction', - signed_transaction: 'base64_signed_transaction', - signer_pubkey: 'test_signer_pubkey', - }; - - await testSuccessfulRpcMethod( - 'signTransactionIfPaid', - () => client.signTransactionIfPaid(request), - mockResponse, - request, - ); - }; - - it('should sign transaction if paid', () => testSignTransactionIfPaid(10)); - it('should handle request without margin', () => testSignTransactionIfPaid()); - }); - describe('transferTransaction', () => { it('should create transfer transaction', async () => { const request: TransferTransactionRequest = { @@ -452,7 +425,6 @@ describe('KoraClient Unit Tests', () => { transfer_transaction: true, get_blockhash: true, get_config: true, - sign_transaction_if_paid: true, }, }; diff --git a/tests/Cargo.toml b/tests/Cargo.toml index e1cd463a..60bb985d 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -40,6 +40,10 @@ path = "external/main.rs" name = "payment_address" path = "payment_address/main.rs" +[[test]] +name = "free_signing" +path = "free_signing/main.rs" + [dependencies] kora-lib = { path = "../crates/lib" } solana-sdk = { workspace = true } diff --git a/tests/adversarial/fee_payer_exploitation.rs b/tests/adversarial/fee_payer_exploitation.rs index 2250484f..e0c57030 100644 --- a/tests/adversarial/fee_payer_exploitation.rs +++ b/tests/adversarial/fee_payer_exploitation.rs @@ -1,6 +1,6 @@ use crate::common::{assertions::RpcErrorAssertions, *}; use jsonrpsee::rpc_params; -use solana_sdk::{signer::Signer, transaction::Transaction}; +use solana_sdk::signer::Signer; use solana_system_interface::instruction::transfer; use spl_associated_token_account::get_associated_token_address; use spl_token::instruction as token_instruction; @@ -32,9 +32,8 @@ async fn test_fee_payer_as_sol_transfer_source() { .await .expect("Failed to create transaction with fee payer as SOL source"); - let result = ctx - .rpc_call::("signTransactionIfPaid", rpc_params![malicious_tx]) - .await; + let result = + ctx.rpc_call::("signTransaction", rpc_params![malicious_tx]).await; match result { Err(error) => { @@ -87,9 +86,8 @@ async fn test_fee_payer_as_spl_transfer_source() { .await .expect("Failed to create transaction with fee payer as USDC source"); - let result = ctx - .rpc_call::("signTransactionIfPaid", rpc_params![malicious_tx]) - .await; + let result = + ctx.rpc_call::("signTransaction", rpc_params![malicious_tx]).await; match result { Err(error) => { diff --git a/tests/free_signing/free_signing_tests.rs b/tests/free_signing/free_signing_tests.rs new file mode 100644 index 00000000..c4758275 --- /dev/null +++ b/tests/free_signing/free_signing_tests.rs @@ -0,0 +1,267 @@ +use crate::common::*; +use jsonrpsee::rpc_params; +use kora_lib::transaction::TransactionUtil; +use solana_sdk::signature::Signer; + +#[tokio::test] +async fn test_sign_transaction_legacy() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + let test_tx = ctx + .transaction_builder() + .with_fee_payer(FeePayerTestHelper::get_fee_payer_pubkey()) + .with_transfer( + &SenderTestHelper::get_test_sender_keypair().pubkey(), + &RecipientTestHelper::get_recipient_pubkey(), + 10, + ) + .build() + .await + .expect("Failed to create test transaction"); + + let response: serde_json::Value = ctx + .rpc_call("signTransaction", rpc_params![test_tx]) + .await + .expect("Failed to sign transaction"); + + 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 transaction"); + + assert!(simulated_tx.value.err.is_none(), "Transaction simulation failed"); +} + +#[tokio::test] +async fn test_sign_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 = USDCMintTestHelper::get_test_usdc_mint_pubkey(); + + let test_tx = ctx + .v0_transaction_builder() + .with_fee_payer(FeePayerTestHelper::get_fee_payer_pubkey()) + .with_signer(&sender) + .with_spl_transfer_checked( + &token_mint, + &sender.pubkey(), + &recipient, + 10, + TEST_USDC_MINT_DECIMALS, + ) + .build() + .await + .expect("Failed to create V0 test transaction"); + + let response: serde_json::Value = ctx + .rpc_call("signTransaction", rpc_params![test_tx]) + .await + .expect("Failed to sign V0 transaction"); + + 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 transaction"); + + assert!(simulated_tx.value.err.is_none(), "V0 transaction simulation failed"); +} + +#[tokio::test] +async fn test_sign_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 = USDCMintTestHelper::get_test_usdc_mint_pubkey(); + + 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_transfer_checked( + &token_mint, + &sender.pubkey(), + &recipient, + 10, + TEST_USDC_MINT_DECIMALS, + ) + .build() + .await + .expect("Failed to create V0 test transaction with lookup table"); + + let response: serde_json::Value = ctx + .rpc_call("signTransaction", rpc_params![test_tx]) + .await + .expect("Failed to sign V0 transaction with lookup table"); + + 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 transaction with lookup table"); + + assert!(simulated_tx.value.err.is_none(), "V0 transaction with lookup table simulation failed"); +} + +#[tokio::test] +async fn test_sign_spl_transaction_legacy() { + let ctx = TestContext::new().await.expect("Failed to create test context"); + let sender = SenderTestHelper::get_test_sender_keypair(); + let test_tx = ctx + .transaction_builder() + .with_fee_payer(FeePayerTestHelper::get_fee_payer_pubkey()) + .with_signer(&sender) + .with_transfer(&sender.pubkey(), &RecipientTestHelper::get_recipient_pubkey(), 10) + .build() + .await + .expect("Failed to create signed test SPL transaction"); + + let response: serde_json::Value = ctx + .rpc_call("signTransaction", rpc_params![test_tx]) + .await + .expect("Failed to sign transaction"); + + 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 transaction"); + + assert!(simulated_tx.value.err.is_none(), "Transaction simulation failed"); +} + +#[tokio::test] +async fn test_sign_spl_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 = USDCMintTestHelper::get_test_usdc_mint_pubkey(); + + let test_tx = ctx + .v0_transaction_builder() + .with_fee_payer(FeePayerTestHelper::get_fee_payer_pubkey()) + .with_signer(&sender) + .with_spl_transfer_checked( + &token_mint, + &sender.pubkey(), + &recipient, + 10, + TEST_USDC_MINT_DECIMALS, + ) + .build() + .await + .expect("Failed to create V0 signed test SPL transaction"); + + let response: serde_json::Value = ctx + .rpc_call("signTransaction", rpc_params![test_tx]) + .await + .expect("Failed to sign V0 SPL transaction"); + + 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 SPL transaction"); + + assert!(simulated_tx.value.err.is_none(), "V0 SPL transaction simulation failed"); +} + +#[tokio::test] +async fn test_sign_spl_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 = USDCMintTestHelper::get_test_usdc_mint_pubkey(); + + 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_transfer_checked( + &token_mint, + &sender.pubkey(), + &recipient, + 10, + TEST_USDC_MINT_DECIMALS, + ) + .build() + .await + .expect("Failed to create V0 signed test SPL transaction with lookup table"); + + let response: serde_json::Value = ctx + .rpc_call("signTransaction", rpc_params![test_tx]) + .await + .expect("Failed to sign V0 SPL transaction with lookup table"); + + 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 SPL transaction with lookup table"); + + assert!( + simulated_tx.value.err.is_none(), + "V0 SPL transaction with lookup table simulation failed" + ); +} diff --git a/tests/free_signing/main.rs b/tests/free_signing/main.rs new file mode 100644 index 00000000..4f8bbac2 --- /dev/null +++ b/tests/free_signing/main.rs @@ -0,0 +1,4 @@ +mod free_signing_tests; + +#[path = "../src/common/mod.rs"] +mod common; diff --git a/tests/multi_signer/signer_management.rs b/tests/multi_signer/signer_management.rs index 7ab8b662..54f4f7e0 100644 --- a/tests/multi_signer/signer_management.rs +++ b/tests/multi_signer/signer_management.rs @@ -2,6 +2,7 @@ use crate::common::*; use jsonrpsee::rpc_params; use serde_json::json; use solana_sdk::signature::Signer; +use std::str::FromStr; #[tokio::test] async fn test_multi_signer_get_config() { @@ -112,10 +113,30 @@ async fn test_signer_key_consistency() { ); assert_eq!(estimate_signer, transfer_signer, "Both calls should use same signer"); - // Now call signTransaction with the same signer key using the built transaction - let built_tx = transfer_response["transaction"].as_str().unwrap(); + // Build a proper signed transaction with payment for signTransaction test + let sender = SenderTestHelper::get_test_sender_keypair(); + let token_mint = USDCMintTestHelper::get_test_usdc_mint_pubkey(); + let fee_payer = + solana_sdk::pubkey::Pubkey::from_str(&first_signer_pubkey).expect("Invalid pubkey"); + + let signed_tx = ctx + .transaction_builder() + .with_fee_payer(fee_payer) + .with_signer(&sender) + .with_spl_transfer( + &token_mint, + &sender.pubkey(), + &fee_payer, + tests::common::helpers::get_fee_for_default_transaction_in_usdc(), + ) + .with_transfer(&sender.pubkey(), &RecipientTestHelper::get_recipient_pubkey(), 10) + .build() + .await + .expect("Failed to create signed transaction"); + + // Now call signTransaction with the same signer key let sign_response: serde_json::Value = ctx - .rpc_call("signTransaction", rpc_params![built_tx, &first_signer_pubkey]) + .rpc_call("signTransaction", rpc_params![signed_tx, &first_signer_pubkey]) .await .expect("Failed to sign transaction"); diff --git a/tests/payment_address/payment_address_legacy_tests.rs b/tests/payment_address/payment_address_legacy_tests.rs index 308e18f7..41b2a471 100644 --- a/tests/payment_address/payment_address_legacy_tests.rs +++ b/tests/payment_address/payment_address_legacy_tests.rs @@ -1,9 +1,6 @@ use crate::common::*; use jsonrpsee::rpc_params; -use kora_lib::{ - token::{TokenInterface, TokenProgram}, - transaction::{TransactionUtil, VersionedTransactionOps}, -}; +use kora_lib::token::{TokenInterface, TokenProgram}; use solana_sdk::{ pubkey::Pubkey, signature::{Keypair, Signer}, @@ -45,9 +42,9 @@ async fn test_sign_transaction_if_paid_with_payment_address_legacy() { .await .expect("Failed to create signed legacy transaction"); - // Call signTransactionIfPaid endpoint - should succeed when payment goes to correct address + // Call signTransaction endpoint - should succeed when payment goes to correct address let response: serde_json::Value = ctx - .rpc_call("signTransactionIfPaid", rpc_params![encoded_tx]) + .rpc_call("signTransaction", rpc_params![encoded_tx]) .await .expect("Failed to sign transaction"); @@ -97,9 +94,9 @@ async fn test_sign_transaction_if_paid_with_wrong_destination_legacy() { .await .expect("Failed to create signed legacy transaction"); - // Call signTransactionIfPaid endpoint - should fail when payment goes to wrong address + // Call signTransaction endpoint - should fail when payment goes to wrong address let response: Result = - ctx.rpc_call("signTransactionIfPaid", rpc_params![encoded_tx]).await; + ctx.rpc_call("signTransaction", rpc_params![encoded_tx]).await; assert!(response.is_err(), "Expected payment validation to fail for wrong destination"); } diff --git a/tests/payment_address/payment_address_multi_payment_tests.rs b/tests/payment_address/payment_address_multi_payment_tests.rs index ef763711..a0654bad 100644 --- a/tests/payment_address/payment_address_multi_payment_tests.rs +++ b/tests/payment_address/payment_address_multi_payment_tests.rs @@ -52,7 +52,7 @@ async fn test_sign_transaction_if_paid_with_multiple_payments_legacy() { .expect("Failed to create signed legacy transaction"); let response: serde_json::Value = ctx - .rpc_call("signTransactionIfPaid", rpc_params![encoded_tx]) + .rpc_call("signTransaction", rpc_params![encoded_tx]) .await .expect("Failed to sign transaction"); @@ -107,7 +107,7 @@ async fn test_sign_transaction_if_paid_with_multiple_payments_insufficient_legac .expect("Failed to create signed legacy transaction"); let response: Result = - ctx.rpc_call("signTransactionIfPaid", rpc_params![encoded_tx]).await; + ctx.rpc_call("signTransaction", rpc_params![encoded_tx]).await; assert!(response.is_err(), "Should fail with insufficient payment"); } @@ -150,7 +150,7 @@ async fn test_sign_transaction_if_paid_with_multiple_sources_legacy() { .expect("Failed to create signed legacy transaction"); let response: serde_json::Value = ctx - .rpc_call("signTransactionIfPaid", rpc_params![encoded_tx]) + .rpc_call("signTransaction", rpc_params![encoded_tx]) .await .expect("Failed to sign transaction"); diff --git a/tests/payment_address/payment_address_v0_lut_tests.rs b/tests/payment_address/payment_address_v0_lut_tests.rs index 8d3fc94b..aa4e5846 100644 --- a/tests/payment_address/payment_address_v0_lut_tests.rs +++ b/tests/payment_address/payment_address_v0_lut_tests.rs @@ -36,7 +36,7 @@ async fn test_sign_transaction_if_paid_with_payment_address_v0_with_lookup() { .expect("Failed to create signed V0 transaction with mint in lookup table"); let response: serde_json::Value = ctx - .rpc_call("signTransactionIfPaid", rpc_params![encoded_tx]) + .rpc_call("signTransaction", rpc_params![encoded_tx]) .await .expect("Failed to sign V0 transaction with mint in lookup table"); @@ -83,7 +83,7 @@ async fn test_sign_transaction_if_paid_with_wrong_destination_v0_with_lookup() { .expect("Failed to create signed V0 transaction with mint in lookup table"); let response: Result = - ctx.rpc_call("signTransactionIfPaid", rpc_params![encoded_tx]).await; + ctx.rpc_call("signTransaction", rpc_params![encoded_tx]).await; assert!(response.is_err(), "Expected payment validation to fail for wrong destination"); } diff --git a/tests/payment_address/payment_address_v0_tests.rs b/tests/payment_address/payment_address_v0_tests.rs index bd8c1c40..85cec0c8 100644 --- a/tests/payment_address/payment_address_v0_tests.rs +++ b/tests/payment_address/payment_address_v0_tests.rs @@ -43,9 +43,9 @@ async fn test_sign_transaction_if_paid_with_payment_address_v0() { .await .expect("Failed to create signed V0 transaction"); - // Call signTransactionIfPaid endpoint - should succeed when payment goes to correct address + // Call signTransaction endpoint - should succeed when payment goes to correct address let response: serde_json::Value = ctx - .rpc_call("signTransactionIfPaid", rpc_params![encoded_tx]) + .rpc_call("signTransaction", rpc_params![encoded_tx]) .await .expect("Failed to sign V0 transaction"); @@ -95,9 +95,9 @@ async fn test_sign_transaction_if_paid_with_wrong_destination_v0() { .await .expect("Failed to create signed V0 transaction"); - // Call signTransactionIfPaid endpoint - should fail when payment goes to wrong address + // Call signTransaction endpoint - should fail when payment goes to wrong address let response: Result = - ctx.rpc_call("signTransactionIfPaid", rpc_params![encoded_tx]).await; + ctx.rpc_call("signTransaction", rpc_params![encoded_tx]).await; assert!(response.is_err(), "Expected payment validation to fail for wrong destination"); } diff --git a/tests/rpc/transaction_signing.rs b/tests/rpc/transaction_signing.rs index 8aa1628b..f803f369 100644 --- a/tests/rpc/transaction_signing.rs +++ b/tests/rpc/transaction_signing.rs @@ -1,278 +1,13 @@ -use std::str::FromStr; - use crate::common::*; use jsonrpsee::rpc_params; use kora_lib::transaction::TransactionUtil; -use solana_sdk::{pubkey::Pubkey, signature::Signer}; -use tests::common::helpers::get_fee_for_default_transaction_in_usdc; +use solana_sdk::signature::Signer; +use std::str::FromStr; // ************************************************************************************** -// Sign transaction tests +// Sign transaction tests (with payment validation - moved to free_signing suite) // ************************************************************************************** -#[tokio::test] -async fn test_sign_transaction_legacy() { - let ctx = TestContext::new().await.expect("Failed to create test context"); - let test_tx = ctx - .transaction_builder() - .with_fee_payer(FeePayerTestHelper::get_fee_payer_pubkey()) - .with_transfer( - &SenderTestHelper::get_test_sender_keypair().pubkey(), - &RecipientTestHelper::get_recipient_pubkey(), - 10, - ) - .build() - .await - .expect("Failed to create test transaction"); - - let response: serde_json::Value = ctx - .rpc_call("signTransaction", rpc_params![test_tx]) - .await - .expect("Failed to sign transaction"); - - 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 transaction"); - - assert!(simulated_tx.value.err.is_none(), "Transaction simulation failed"); -} - -#[tokio::test] -async fn test_sign_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 = USDCMintTestHelper::get_test_usdc_mint_pubkey(); - - let test_tx = ctx - .v0_transaction_builder() - .with_fee_payer(FeePayerTestHelper::get_fee_payer_pubkey()) - .with_signer(&sender) - .with_spl_transfer_checked( - &token_mint, - &sender.pubkey(), - &recipient, - 10, - TEST_USDC_MINT_DECIMALS, - ) - .build() - .await - .expect("Failed to create V0 test transaction"); - - let response: serde_json::Value = ctx - .rpc_call("signTransaction", rpc_params![test_tx]) - .await - .expect("Failed to sign V0 transaction"); - - 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 transaction"); - - assert!(simulated_tx.value.err.is_none(), "V0 transaction simulation failed"); -} - -#[tokio::test] -async fn test_sign_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 = USDCMintTestHelper::get_test_usdc_mint_pubkey(); - - 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_transfer_checked( - &token_mint, - &sender.pubkey(), - &recipient, - 10, - TEST_USDC_MINT_DECIMALS, - ) - .build() - .await - .expect("Failed to create V0 test transaction with lookup table"); - - let response: serde_json::Value = ctx - .rpc_call("signTransaction", rpc_params![test_tx]) - .await - .expect("Failed to sign V0 transaction with lookup table"); - - 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 transaction with lookup table"); - - assert!(simulated_tx.value.err.is_none(), "V0 transaction with lookup table simulation failed"); -} - -#[tokio::test] -async fn test_sign_spl_transaction_legacy() { - let ctx = TestContext::new().await.expect("Failed to create test context"); - let sender = SenderTestHelper::get_test_sender_keypair(); - let test_tx = ctx - .transaction_builder() - .with_fee_payer(FeePayerTestHelper::get_fee_payer_pubkey()) - .with_signer(&sender) - .with_transfer(&sender.pubkey(), &RecipientTestHelper::get_recipient_pubkey(), 10) - .build() - .await - .expect("Failed to create signed test SPL transaction"); - - let response: serde_json::Value = ctx - .rpc_call("signTransaction", rpc_params![test_tx]) - .await - .expect("Failed to sign transaction"); - - 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 transaction"); - - assert!(simulated_tx.value.err.is_none(), "Transaction simulation failed"); -} - -#[tokio::test] -async fn test_sign_spl_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 = USDCMintTestHelper::get_test_usdc_mint_pubkey(); - - let test_tx = ctx - .v0_transaction_builder() - .with_fee_payer(FeePayerTestHelper::get_fee_payer_pubkey()) - .with_signer(&sender) - .with_spl_transfer_checked( - &token_mint, - &sender.pubkey(), - &recipient, - 10, - TEST_USDC_MINT_DECIMALS, - ) - .build() - .await - .expect("Failed to create V0 signed test SPL transaction"); - - let response: serde_json::Value = ctx - .rpc_call("signTransaction", rpc_params![test_tx]) - .await - .expect("Failed to sign V0 SPL transaction"); - - 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 SPL transaction"); - - assert!(simulated_tx.value.err.is_none(), "V0 SPL transaction simulation failed"); -} - -#[tokio::test] -async fn test_sign_spl_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 = USDCMintTestHelper::get_test_usdc_mint_pubkey(); - - 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_transfer_checked( - &token_mint, - &sender.pubkey(), - &recipient, - 10, - TEST_USDC_MINT_DECIMALS, - ) - .build() - .await - .expect("Failed to create V0 signed test SPL transaction with lookup table"); - - let response: serde_json::Value = ctx - .rpc_call("signTransaction", rpc_params![test_tx]) - .await - .expect("Failed to sign V0 SPL transaction with lookup table"); - - 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 SPL transaction with lookup table"); - - assert!( - simulated_tx.value.err.is_none(), - "V0 SPL transaction with lookup table simulation failed" - ); -} - /// Test sign V0 transaction with valid lookup table #[tokio::test] async fn test_sign_transaction_v0_with_valid_lookup_table() { @@ -283,10 +18,20 @@ async fn test_sign_transaction_v0_with_valid_lookup_table() { let allowed_lookup_table_address = LookupTableHelper::get_allowed_lookup_table_address().unwrap(); + let fee_payer = FeePayerTestHelper::get_fee_payer_pubkey(); + let token_mint = USDCMintTestHelper::get_test_usdc_mint_pubkey(); + let sender = SenderTestHelper::get_test_sender_keypair(); + // Create a V0 transaction using the allowed lookup table (index 0) let v0_transaction = ctx .v0_transaction_builder_with_lookup(vec![allowed_lookup_table_address]) .with_fee_payer(FeePayerTestHelper::get_fee_payer_pubkey()) + .with_spl_transfer( + &token_mint, + &sender.pubkey(), + &fee_payer, + tests::common::helpers::get_fee_for_default_transaction_in_usdc(), + ) .with_transfer( &SenderTestHelper::get_test_sender_keypair().pubkey(), &RecipientTestHelper::get_recipient_pubkey(), @@ -371,6 +116,7 @@ async fn test_sign_and_send_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 = USDCMintTestHelper::get_test_usdc_mint_pubkey(); let ctx = TestContext::new().await.expect("Failed to create test context"); @@ -378,6 +124,12 @@ async fn test_sign_and_send_transaction_legacy() { .transaction_builder() .with_fee_payer(fee_payer) .with_signer(&sender) + .with_spl_transfer( + &token_mint, + &sender.pubkey(), + &fee_payer, + tests::common::helpers::get_fee_for_default_transaction_in_usdc(), + ) .with_transfer(&sender.pubkey(), &recipient, 10) .build() .await @@ -408,6 +160,13 @@ async fn test_sign_and_send_transaction_v0() { .v0_transaction_builder() .with_fee_payer(fee_payer) .with_signer(&sender) + .with_spl_transfer_checked( + &token_mint, + &sender.pubkey(), + &fee_payer, + tests::common::helpers::get_fee_for_default_transaction_in_usdc(), + TEST_USDC_MINT_DECIMALS, + ) .with_spl_transfer_checked( &token_mint, &sender.pubkey(), @@ -447,6 +206,13 @@ async fn test_sign_and_send_transaction_v0_with_lookup() { .v0_transaction_builder_with_lookup(vec![transaction_lookup_table]) .with_fee_payer(fee_payer) .with_signer(&sender) + .with_spl_transfer_checked( + &token_mint, + &sender.pubkey(), + &fee_payer, + tests::common::helpers::get_fee_for_default_transaction_in_usdc(), + TEST_USDC_MINT_DECIMALS, + ) .with_spl_transfer_checked( &token_mint, &sender.pubkey(), @@ -471,30 +237,25 @@ async fn test_sign_and_send_transaction_v0_with_lookup() { } // ************************************************************************************** -// Sign transaction if paid tests +// Sign transaction with payment validation tests // ************************************************************************************** -/// Test complex sign transaction if paid with fee payer pool logic #[tokio::test] -async fn test_sign_transaction_if_paid_legacy() { +async fn test_sign_transaction_with_payment_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 fee_payer = solana_sdk::pubkey::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 = USDCMintTestHelper::get_test_usdc_mint_pubkey(); - // Use transaction builder with proper signing and automatic ATA derivation let base64_transaction = ctx .transaction_builder() .with_fee_payer(fee_payer) @@ -503,16 +264,15 @@ async fn test_sign_transaction_if_paid_legacy() { &token_mint, &sender.pubkey(), &fee_payer, - get_fee_for_default_transaction_in_usdc(), + tests::common::helpers::get_fee_for_default_transaction_in_usdc(), ) .with_spl_transfer(&token_mint, &sender.pubkey(), &recipient, 1) .build() .await .expect("Failed to create signed transaction"); - // Test signTransactionIfPaid let response: serde_json::Value = ctx - .rpc_call("signTransactionIfPaid", rpc_params![base64_transaction]) + .rpc_call("signTransaction", rpc_params![base64_transaction]) .await .expect("Failed to sign transaction"); @@ -522,12 +282,10 @@ async fn test_sign_transaction_if_paid_legacy() { "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 @@ -536,20 +294,17 @@ async fn test_sign_transaction_if_paid_legacy() { assert!(simulated_tx.value.err.is_none(), "Transaction simulation failed"); } -/// Test sign transaction if paid with V0 transaction #[tokio::test] -async fn test_sign_transaction_if_paid_v0() { +async fn test_sign_transaction_with_payment_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 fee_payer = solana_sdk::pubkey::Pubkey::from_str(fee_payers[0].as_str().unwrap()).unwrap(); let sender = SenderTestHelper::get_test_sender_keypair(); let recipient = RecipientTestHelper::get_recipient_pubkey(); @@ -563,7 +318,7 @@ async fn test_sign_transaction_if_paid_v0() { &token_mint, &sender.pubkey(), &fee_payer, - get_fee_for_default_transaction_in_usdc(), + tests::common::helpers::get_fee_for_default_transaction_in_usdc(), TEST_USDC_MINT_DECIMALS, ) .with_spl_transfer_checked( @@ -577,9 +332,8 @@ async fn test_sign_transaction_if_paid_v0() { .await .expect("Failed to create V0 signed transaction"); - // Test signTransactionIfPaid let response: serde_json::Value = ctx - .rpc_call("signTransactionIfPaid", rpc_params![base64_transaction]) + .rpc_call("signTransaction", rpc_params![base64_transaction]) .await .expect("Failed to sign V0 transaction"); @@ -589,12 +343,10 @@ async fn test_sign_transaction_if_paid_v0() { "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 @@ -603,20 +355,17 @@ async fn test_sign_transaction_if_paid_v0() { assert!(simulated_tx.value.err.is_none(), "V0 transaction simulation failed"); } -/// Test sign transaction if paid with V0 transaction and lookup table #[tokio::test] -async fn test_sign_transaction_if_paid_v0_with_lookup() { +async fn test_sign_transaction_with_payment_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 fee_payer = solana_sdk::pubkey::Pubkey::from_str(fee_payers[0].as_str().unwrap()).unwrap(); let sender = SenderTestHelper::get_test_sender_keypair(); let recipient = RecipientTestHelper::get_recipient_pubkey(); @@ -625,7 +374,6 @@ async fn test_sign_transaction_if_paid_v0_with_lookup() { 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) @@ -634,7 +382,7 @@ async fn test_sign_transaction_if_paid_v0_with_lookup() { &token_mint, &sender.pubkey(), &fee_payer, - get_fee_for_default_transaction_in_usdc(), + tests::common::helpers::get_fee_for_default_transaction_in_usdc(), TEST_USDC_MINT_DECIMALS, ) .with_spl_transfer_checked( @@ -648,9 +396,8 @@ async fn test_sign_transaction_if_paid_v0_with_lookup() { .await .expect("Failed to create V0 signed transaction with lookup table"); - // Test signTransactionIfPaid let response: serde_json::Value = ctx - .rpc_call("signTransactionIfPaid", rpc_params![base64_transaction]) + .rpc_call("signTransaction", rpc_params![base64_transaction]) .await .expect("Failed to sign V0 transaction with lookup table"); @@ -660,12 +407,10 @@ async fn test_sign_transaction_if_paid_v0_with_lookup() { "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 diff --git a/tests/src/common/fixtures/auth-test.toml b/tests/src/common/fixtures/auth-test.toml index bc3b16bf..6e3890ef 100644 --- a/tests/src/common/fixtures/auth-test.toml +++ b/tests/src/common/fixtures/auth-test.toml @@ -22,7 +22,6 @@ transfer_transaction = true get_blockhash = true get_config = true get_payer_signer = true -sign_transaction_if_paid = true [validation] max_allowed_lamports = 1000000 @@ -46,6 +45,10 @@ disallowed_accounts = [ "hndXZGK45hCxfBYvxejAXzCfCujoqkNf7rk4sTB8pek", # Test disallowed account for lookup table ] +[validation.price] +type = "margin" +margin = 0.0 + [validation.fee_payer_policy] [validation.fee_payer_policy.system] @@ -92,4 +95,4 @@ allow_thaw_account = true enabled = false cache_url = "redis://redis:6379" max_transactions = 2 -fallback_if_unavailable = false \ No newline at end of file +fallback_if_unavailable = false diff --git a/tests/src/common/fixtures/fee-payer-policy-test.toml b/tests/src/common/fixtures/fee-payer-policy-test.toml index d541efe2..3c910b45 100644 --- a/tests/src/common/fixtures/fee-payer-policy-test.toml +++ b/tests/src/common/fixtures/fee-payer-policy-test.toml @@ -20,7 +20,6 @@ transfer_transaction = true get_blockhash = true get_config = true get_payer_signer = true -sign_transaction_if_paid = true [validation] max_allowed_lamports = 1000000 diff --git a/tests/src/common/fixtures/kora-free-test.toml b/tests/src/common/fixtures/kora-free-test.toml new file mode 100644 index 00000000..362c7c29 --- /dev/null +++ b/tests/src/common/fixtures/kora-free-test.toml @@ -0,0 +1,109 @@ +# This file is used for free pricing tests (no payment validation) +[kora] +rate_limit = 100 + +[kora.auth] + +# Cache configuration - disabled for testing +[kora.cache] +enabled = false +default_ttl = 300 +account_ttl = 60 + +[kora.enabled_methods] +liveness = false # Just to be able to test the false flag +estimate_transaction_fee = true +get_supported_tokens = true +sign_transaction = true +sign_and_send_transaction = true +transfer_transaction = true +get_blockhash = true +get_config = true +get_payer_signer = true + +[validation] +max_allowed_lamports = 1000000 +max_signatures = 10 +price_source = "Mock" +allowed_programs = [ + "11111111111111111111111111111111", # System Program + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", # Token Program + "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb", # Token-2022 Program + "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", # Associated Token Program + "AddressLookupTab1e1111111111111111111111111", # Address Lookup Table Program + "Bcdikjss8HWzKEuj6gEQoFq9TCnGnk6v3kUnRU1gb6hA", # Custom Transfer Hook Program Example +] +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 +] + +[validation.price] +type = "free" + +# 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] + +[validation.fee_payer_policy.system] +allow_transfer = true +allow_assign = true +allow_create_account = true +allow_allocate = true + +[validation.fee_payer_policy.system.nonce] +allow_initialize = true +allow_advance = true +allow_authorize = true +allow_withdraw = true + +[validation.fee_payer_policy.spl_token] +allow_transfer = true +allow_burn = true +allow_close_account = true +allow_approve = true +allow_revoke = true +allow_set_authority = true +allow_mint_to = true +allow_initialize_mint = true +allow_initialize_account = true +allow_initialize_multisig = true +allow_freeze_account = true +allow_thaw_account = true + +[validation.fee_payer_policy.token_2022] +allow_transfer = true +allow_burn = true +allow_close_account = true +allow_approve = true +allow_revoke = true +allow_set_authority = true +allow_mint_to = true +allow_initialize_mint = true +allow_initialize_account = true +allow_initialize_multisig = true +allow_freeze_account = true +allow_thaw_account = true + +[kora.usage_limit] +enabled = false +cache_url = "redis://redis:6379" +max_transactions = 2 +fallback_if_unavailable = false diff --git a/tests/src/common/fixtures/kora-test.toml b/tests/src/common/fixtures/kora-test.toml index e53c398e..1beb561d 100644 --- a/tests/src/common/fixtures/kora-test.toml +++ b/tests/src/common/fixtures/kora-test.toml @@ -20,7 +20,6 @@ transfer_transaction = true get_blockhash = true get_config = true get_payer_signer = true -sign_transaction_if_paid = true [validation] max_allowed_lamports = 1000000 @@ -49,6 +48,10 @@ disallowed_accounts = [ "hndXZGK45hCxfBYvxejAXzCfCujoqkNf7rk4sTB8pek", # Test disallowed account for lookup table ] +[validation.price] +type = "margin" +margin = 0.0 + # Block specific extensions for testing (only affects extension test accounts) [validation.token_2022] blocked_mint_extensions = [ diff --git a/tests/src/common/fixtures/paymaster-address-test.toml b/tests/src/common/fixtures/paymaster-address-test.toml index a09ccd77..b2920a0f 100644 --- a/tests/src/common/fixtures/paymaster-address-test.toml +++ b/tests/src/common/fixtures/paymaster-address-test.toml @@ -19,7 +19,6 @@ transfer_transaction = true get_blockhash = true get_config = true get_payer_signer = true -sign_transaction_if_paid = true [validation] max_allowed_lamports = 1000000 @@ -45,6 +44,10 @@ disallowed_accounts = [ "hndXZGK45hCxfBYvxejAXzCfCujoqkNf7rk4sTB8pek", # Test disallowed account for lookup table ] +[validation.price] +type = "margin" +margin = 0 + [validation.fee_payer_policy] [validation.fee_payer_policy.system] diff --git a/tests/src/test_runner/output.rs b/tests/src/test_runner/output.rs index 9b251d0d..2a19de4b 100644 --- a/tests/src/test_runner/output.rs +++ b/tests/src/test_runner/output.rs @@ -34,6 +34,7 @@ impl TestPhaseColor { "Regular Integration Tests" => Self::Regular, "Auth Tests" => Self::Auth, "Payment Address Tests" => Self::Payment, + "Free Signing Tests" => Self::Regular, "Multi-Signer Tests" => Self::MultiSigner, "Fee Payer Policy Tests" => Self::FeePayerPolicy, name if name.starts_with("TypeScript") => Self::from_typescript_phase(name), diff --git a/tests/src/test_runner/test_cases.toml b/tests/src/test_runner/test_cases.toml index 77598490..e8effa75 100644 --- a/tests/src/test_runner/test_cases.toml +++ b/tests/src/test_runner/test_cases.toml @@ -5,6 +5,13 @@ signers = "tests/src/common/fixtures/signers.toml" port = "8080" tests = ["rpc", "tokens", "external", "adversarial"] +[test.free_signing] +name = "Free Signing Tests" +config = "tests/src/common/fixtures/kora-free-test.toml" +signers = "tests/src/common/fixtures/signers.toml" +port = "8087" +tests = ["free_signing"] + [test.auth] name = "Auth Tests" config = "tests/src/common/fixtures/auth-test.toml" diff --git a/tests/tokens/token_2022_extensions_test.rs b/tests/tokens/token_2022_extensions_test.rs index abb0e131..27886df2 100644 --- a/tests/tokens/token_2022_extensions_test.rs +++ b/tests/tokens/token_2022_extensions_test.rs @@ -1,6 +1,6 @@ use crate::common::{ ExtensionHelpers, FeePayerTestHelper, RecipientTestHelper, SenderTestHelper, TestContext, - TransactionBuilder, USDCMint2022TestHelper, TRANSFER_HOOK_PROGRAM_ID, + TransactionBuilder, USDCMint2022TestHelper, USDCMintTestHelper, TRANSFER_HOOK_PROGRAM_ID, }; use base64::{engine::general_purpose::STANDARD, Engine as _}; use jsonrpsee::rpc_params; @@ -105,7 +105,7 @@ async fn test_blocked_memo_transfer_extension() { // 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; + ctx.rpc_call("signTransaction", rpc_params![transaction]).await; // This should fail when disallowed_token_extensions includes "MemoTransfer" assert!(result.is_err(), "Transaction should have failed"); @@ -213,7 +213,7 @@ async fn test_blocked_interest_bearing_config_extension() { // 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; + ctx.rpc_call("signTransaction", rpc_params![transaction]).await; // This should fail when disallowed_mint_extensions includes "InterestBearingConfig" assert!(result.is_err(), "Transaction should have failed"); @@ -228,7 +228,7 @@ async fn test_blocked_interest_bearing_config_extension() { #[tokio::test] async fn test_transfer_fee_insufficient_payment() { - // Test that signTransactionIfPaid fails when payment amount doesn't account for transfer fee + // Test that signTransaction 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 @@ -316,7 +316,7 @@ async fn test_transfer_fee_insufficient_payment() { // 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; + ctx.rpc_call("signTransaction", rpc_params![transaction]).await; assert!(result.is_err(), "Transaction should have failed due to insufficient payment"); @@ -333,7 +333,7 @@ async fn test_transfer_fee_insufficient_payment() { #[tokio::test] async fn test_transfer_fee_sufficient_payment() { - // Test that signTransactionIfPaid succeeds when payment amount accounts for transfer fee + // Test that signTransaction 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"); @@ -418,7 +418,7 @@ async fn test_transfer_fee_sufficient_payment() { .expect("Failed to build transaction"); let result: Result = - ctx.rpc_call("signTransactionIfPaid", rpc_params![transaction]).await; + ctx.rpc_call("signTransaction", rpc_params![transaction]).await; assert!( result.is_ok(), @@ -538,10 +538,29 @@ async fn test_transfer_hook_allows_transfer() { // Add the transfer hook program itself as a read-only account transfer_instruction.accounts.push(AccountMeta::new_readonly(hook_program_id, false)); - // Create transaction with manual instruction + // Add payment instruction for Kora fee + let token_mint = USDCMintTestHelper::get_test_usdc_mint_pubkey(); + let sender_usdc_ata = + spl_associated_token_account::get_associated_token_address(&sender.pubkey(), &token_mint); + let fee_payer_usdc_ata = spl_associated_token_account::get_associated_token_address( + &fee_payer.pubkey(), + &token_mint, + ); + + let payment_instruction = spl_token::instruction::transfer( + &spl_token::id(), + &sender_usdc_ata, + &fee_payer_usdc_ata, + &sender.pubkey(), + &[], + tests::common::helpers::get_fee_for_default_transaction_in_usdc(), + ) + .expect("Failed to create payment instruction"); + + // Create transaction with payment and transfer instructions let recent_blockhash = rpc_client.get_latest_blockhash().await.unwrap(); let test_transaction = Transaction::new_signed_with_payer( - &[transfer_instruction], + &[payment_instruction, transfer_instruction], Some(&fee_payer.pubkey()), &[&fee_payer, &sender], recent_blockhash, diff --git a/tests/tokens/token_2022_test.rs b/tests/tokens/token_2022_test.rs index c6d7092a..9c7a11f5 100644 --- a/tests/tokens/token_2022_test.rs +++ b/tests/tokens/token_2022_test.rs @@ -91,12 +91,20 @@ 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 fee_payer = FeePayerTestHelper::get_fee_payer_pubkey(); + let token_mint = USDCMintTestHelper::get_test_usdc_mint_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_fee_payer(fee_payer) .with_signer(&sender) + .with_spl_transfer( + &token_mint, + &sender.pubkey(), + &fee_payer, + tests::common::helpers::get_fee_for_default_transaction_in_usdc(), + ) .with_spl_token_2022_transfer_checked(&token_mint_2022, &sender.pubkey(), &recipient, 10, 6) .build() .await @@ -130,12 +138,21 @@ 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 fee_payer = FeePayerTestHelper::get_fee_payer_pubkey(); + let token_mint = USDCMintTestHelper::get_test_usdc_mint_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_fee_payer(fee_payer) .with_signer(&sender) + .with_spl_transfer_checked( + &token_mint, + &sender.pubkey(), + &fee_payer, + tests::common::helpers::get_fee_for_default_transaction_in_usdc(), + TEST_USDC_MINT_DECIMALS, + ) .with_spl_token_2022_transfer_checked(&token_mint_2022, &sender.pubkey(), &recipient, 10, 6) .build() .await @@ -169,6 +186,8 @@ 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 fee_payer = FeePayerTestHelper::get_fee_payer_pubkey(); + let token_mint = USDCMintTestHelper::get_test_usdc_mint_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 @@ -177,8 +196,15 @@ async fn test_sign_token_2022_transaction_v0_with_lookup() { let test_tx = ctx .v0_transaction_builder_with_lookup(vec![transaction_lookup_table]) - .with_fee_payer(FeePayerTestHelper::get_fee_payer_pubkey()) + .with_fee_payer(fee_payer) .with_signer(&sender) + .with_spl_transfer_checked( + &token_mint, + &sender.pubkey(), + &fee_payer, + tests::common::helpers::get_fee_for_default_transaction_in_usdc(), + TEST_USDC_MINT_DECIMALS, + ) .with_spl_token_2022_transfer_checked(&token_mint_2022, &sender.pubkey(), &recipient, 10, 6) .build() .await @@ -219,6 +245,7 @@ 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 = USDCMintTestHelper::get_test_usdc_mint_pubkey(); let token_mint_2022 = USDCMint2022TestHelper::get_test_usdc_mint_2022_pubkey(); let ctx = TestContext::new().await.expect("Failed to create test context"); @@ -227,6 +254,12 @@ async fn test_sign_and_send_token_2022_transaction_legacy() { .transaction_builder() .with_fee_payer(fee_payer) .with_signer(&sender) + .with_spl_transfer( + &token_mint, + &sender.pubkey(), + &fee_payer, + tests::common::helpers::get_fee_for_default_transaction_in_usdc(), + ) .with_spl_token_2022_transfer_checked(&token_mint_2022, &sender.pubkey(), &recipient, 10, 6) .build() .await @@ -249,6 +282,7 @@ 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 = USDCMintTestHelper::get_test_usdc_mint_pubkey(); let token_mint_2022 = USDCMint2022TestHelper::get_test_usdc_mint_2022_pubkey(); let ctx = TestContext::new().await.expect("Failed to create test context"); @@ -257,6 +291,13 @@ async fn test_sign_and_send_token_2022_transaction_v0() { .v0_transaction_builder() .with_fee_payer(fee_payer) .with_signer(&sender) + .with_spl_transfer_checked( + &token_mint, + &sender.pubkey(), + &fee_payer, + tests::common::helpers::get_fee_for_default_transaction_in_usdc(), + TEST_USDC_MINT_DECIMALS, + ) .with_spl_token_2022_transfer_checked(&token_mint_2022, &sender.pubkey(), &recipient, 10, 6) .build() .await @@ -279,6 +320,7 @@ 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 = USDCMintTestHelper::get_test_usdc_mint_pubkey(); let token_mint_2022 = USDCMint2022TestHelper::get_test_usdc_mint_2022_pubkey(); let ctx = TestContext::new().await.expect("Failed to create test context"); @@ -291,6 +333,13 @@ async fn test_sign_and_send_token_2022_transaction_v0_with_lookup() { .v0_transaction_builder_with_lookup(vec![transaction_lookup_table]) .with_fee_payer(fee_payer) .with_signer(&sender) + .with_spl_transfer_checked( + &token_mint, + &sender.pubkey(), + &fee_payer, + tests::common::helpers::get_fee_for_default_transaction_in_usdc(), + TEST_USDC_MINT_DECIMALS, + ) .with_spl_token_2022_transfer_checked(&token_mint_2022, &sender.pubkey(), &recipient, 10, 6) .build() .await @@ -353,9 +402,9 @@ async fn test_sign_token_2022_transaction_if_paid_legacy() { .await .expect("Failed to create signed Token 2022 transaction"); - // Test signTransactionIfPaid + // Test signTransaction let response: serde_json::Value = ctx - .rpc_call("signTransactionIfPaid", rpc_params![base64_transaction]) + .rpc_call("signTransaction", rpc_params![base64_transaction]) .await .expect("Failed to sign Token 2022 transaction"); @@ -416,9 +465,9 @@ async fn test_sign_token_2022_transaction_if_paid_v0() { .await .expect("Failed to create V0 signed Token 2022 transaction"); - // Test signTransactionIfPaid + // Test signTransaction let response: serde_json::Value = ctx - .rpc_call("signTransactionIfPaid", rpc_params![base64_transaction]) + .rpc_call("signTransaction", rpc_params![base64_transaction]) .await .expect("Failed to sign V0 Token 2022 transaction"); @@ -484,9 +533,9 @@ async fn test_sign_token_2022_transaction_if_paid_v0_with_lookup() { .await .expect("Failed to create V0 signed Token 2022 transaction with lookup table"); - // Test signTransactionIfPaid + // Test signTransaction let response: serde_json::Value = ctx - .rpc_call("signTransactionIfPaid", rpc_params![base64_transaction]) + .rpc_call("signTransaction", rpc_params![base64_transaction]) .await .expect("Failed to sign V0 Token 2022 transaction with lookup table"); From def0e43770b982bd185e18c980e9ec91c94bed11 Mon Sep 17 00:00:00 2001 From: jo <17280917+dev-jodee@users.noreply.github.com> Date: Tue, 28 Oct 2025 15:49:08 -0400 Subject: [PATCH 22/29] =?UTF-8?q?chore:=20Update=20dependencies=20and=20re?= =?UTF-8?q?factor=20token=20interfaces=20(update=20to=20v=E2=80=A6=20(#242?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: Update dependencies and refactor token interfaces (update to v3 for sdks) - Upgraded `agave-feature-set` and `agave-reserved-account-keys` to version 3.0.8 in `Cargo.lock`. - Updated various Solana SDK dependencies to version 3.0.x for improved compatibility and features. - Refactored token-related imports to use the new `spl_token_interface` and `spl_token_2022_interface` for better modularity. - Removed outdated dependencies and adjusted related code to ensure consistency across the codebase. - Enhanced test cases to reflect changes in token interface usage and ensure proper functionality. --- Cargo.lock | 2200 +++++------------ Cargo.toml | 28 +- crates/lib/Cargo.toml | 13 +- crates/lib/src/admin/token_util.rs | 21 +- crates/lib/src/config.rs | 2 +- crates/lib/src/fee/fee.rs | 4 +- .../rpc_server/method/transfer_transaction.rs | 8 +- crates/lib/src/tests/account_mock.rs | 29 +- crates/lib/src/tests/transaction_mock.rs | 2 +- crates/lib/src/token/mod.rs | 2 - crates/lib/src/token/spl_token.rs | 40 +- crates/lib/src/token/spl_token_2022.rs | 37 +- crates/lib/src/token/spl_token_2022_util.rs | 4 +- crates/lib/src/token/token.rs | 31 +- .../lib/src/transaction/instruction_util.rs | 407 +-- crates/lib/src/transaction/transaction.rs | 4 +- .../lib/src/transaction/versioned_message.rs | 4 +- .../src/transaction/versioned_transaction.rs | 14 +- crates/lib/src/validator/account_validator.rs | 50 +- crates/lib/src/validator/config_validator.rs | 7 +- .../src/validator/transaction_validator.rs | 92 +- tests/Cargo.toml | 11 +- tests/adversarial/fee_payer_exploitation.rs | 6 +- tests/adversarial/token_states.rs | 18 +- .../fee_payer_policy_violations.rs | 26 +- .../payment_address_legacy_tests.rs | 8 +- .../payment_address_multi_payment_tests.rs | 4 +- .../payment_address_v0_lut_tests.rs | 4 +- .../payment_address_v0_tests.rs | 8 +- tests/rpc/fee_estimation.rs | 12 +- tests/src/common/extension_helpers.rs | 32 +- tests/src/common/lookup_tables.rs | 11 +- tests/src/common/setup.rs | 144 +- tests/src/common/transaction.rs | 98 +- tests/tokens/token_2022_extensions_test.rs | 98 +- 35 files changed, 1242 insertions(+), 2237 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f7c163dd..9dedb1ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,9 +65,9 @@ dependencies = [ [[package]] name = "agave-feature-set" -version = "2.3.7" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e35cc5b8887b993ba4975a23b6e098ee10db50e8e23ee3a9523035b7ca35b53b" +checksum = "29098b42572aa09c9fdb620b50774aa0b907e880aa41ff99fb1892417c9672cc" dependencies = [ "ahash 0.8.12", "solana-epoch-schedule", @@ -79,9 +79,9 @@ dependencies = [ [[package]] name = "agave-reserved-account-keys" -version = "2.3.7" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685cb445fe51b7b8a914d1b7dd5a0ea0b106fb8ea9454e84c4cd726a5d87c571" +checksum = "c9db52270156139b115e25087a4850e28097533f48e713cd73bfef570112514d" dependencies = [ "agave-feature-set", "solana-pubkey", @@ -207,123 +207,6 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" -[[package]] -name = "ark-bn254" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a22f4561524cd949590d78d7d4c5df8f592430d221f7f3c9497bbafd8972120f" -dependencies = [ - "ark-ec", - "ark-ff", - "ark-std", -] - -[[package]] -name = "ark-ec" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "defd9a439d56ac24968cca0571f598a61bc8c55f71d50a89cda591cb750670ba" -dependencies = [ - "ark-ff", - "ark-poly", - "ark-serialize", - "ark-std", - "derivative", - "hashbrown 0.13.2", - "itertools 0.10.5", - "num-traits", - "zeroize", -] - -[[package]] -name = "ark-ff" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" -dependencies = [ - "ark-ff-asm", - "ark-ff-macros", - "ark-serialize", - "ark-std", - "derivative", - "digest 0.10.7", - "itertools 0.10.5", - "num-bigint 0.4.6", - "num-traits", - "paste", - "rustc_version", - "zeroize", -] - -[[package]] -name = "ark-ff-asm" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" -dependencies = [ - "quote", - "syn 1.0.109", -] - -[[package]] -name = "ark-ff-macros" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" -dependencies = [ - "num-bigint 0.4.6", - "num-traits", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "ark-poly" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d320bfc44ee185d899ccbadfa8bc31aab923ce1558716e1997a1e74057fe86bf" -dependencies = [ - "ark-ff", - "ark-serialize", - "ark-std", - "derivative", - "hashbrown 0.13.2", -] - -[[package]] -name = "ark-serialize" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" -dependencies = [ - "ark-serialize-derive", - "ark-std", - "digest 0.10.7", - "num-bigint 0.4.6", -] - -[[package]] -name = "ark-serialize-derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae3281bc6d0fd7e549af32b52511e1302185bd688fd3359fa36423346ff682ea" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "ark-std" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" -dependencies = [ - "num-traits", - "rand 0.8.5", -] - [[package]] name = "arrayref" version = "0.3.9" @@ -336,6 +219,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "ascii" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" + [[package]] name = "asn1-rs" version = "0.5.2" @@ -461,17 +350,6 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi 0.1.19", - "libc", - "winapi", -] - [[package]] name = "autocfg" version = "1.5.0" @@ -508,12 +386,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" -[[package]] -name = "base64" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" - [[package]] name = "base64" version = "0.13.1" @@ -615,39 +487,16 @@ dependencies = [ "generic-array", ] -[[package]] -name = "borsh" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115e54d64eb62cdebad391c19efc9dce4981c690c85a33a12199d99bb9546fee" -dependencies = [ - "borsh-derive 0.10.4", - "hashbrown 0.13.2", -] - [[package]] name = "borsh" version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" dependencies = [ - "borsh-derive 1.5.7", + "borsh-derive", "cfg_aliases", ] -[[package]] -name = "borsh-derive" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "831213f80d9423998dd696e2c5345aba6be7a0bd8cd19e31c5243e13df1cef89" -dependencies = [ - "borsh-derive-internal", - "borsh-schema-derive-internal", - "proc-macro-crate 0.1.5", - "proc-macro2", - "syn 1.0.109", -] - [[package]] name = "borsh-derive" version = "1.5.7" @@ -661,28 +510,6 @@ dependencies = [ "syn 2.0.104", ] -[[package]] -name = "borsh-derive-internal" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65d6ba50644c98714aa2a70d13d7df3cd75cd2b523a2b452bf010443800976b3" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "borsh-schema-derive-internal" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "276691d96f063427be83e6692b86148e488ebba9f48f77788724ca027ba3b6d4" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "brotli" version = "3.5.0" @@ -945,6 +772,19 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "combine" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3da6baa321ec19e1cc41d31bf599f00c783d0517095cdaf0332e3fe8d20680" +dependencies = [ + "ascii", + "byteorder", + "either", + "memchr", + "unreachable", +] + [[package]] name = "combine" version = "4.6.7" @@ -989,35 +829,15 @@ dependencies = [ [[package]] name = "console" -version = "0.15.11" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +checksum = "b430743a6eb14e9764d4260d4c0d8123087d504eeb9c48f2b2a5e810dd369df4" dependencies = [ "encode_unicode", "libc", "once_cell", "unicode-width", - "windows-sys 0.59.0", -] - -[[package]] -name = "console_error_panic_hook" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" -dependencies = [ - "cfg-if", - "wasm-bindgen", -] - -[[package]] -name = "console_log" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89f72f65e8501878b8a004d5a1afb780987e2ce2b4532c562e367a72c57499f" -dependencies = [ - "log", - "web-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1110,12 +930,6 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - [[package]] name = "crypto-bigint" version = "0.5.5" @@ -1139,16 +953,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "crypto-mac" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" -dependencies = [ - "generic-array", - "subtle", -] - [[package]] name = "ctr" version = "0.9.2" @@ -1158,19 +962,6 @@ dependencies = [ "cipher", ] -[[package]] -name = "curve25519-dalek" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" -dependencies = [ - "byteorder", - "digest 0.9.0", - "rand_core 0.5.1", - "subtle", - "zeroize", -] - [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -1359,17 +1150,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e5c37193a1db1d8ed868c03ec7b152175f26160a5b740e5e484143877e0adf0" -[[package]] -name = "derivative" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "derive_builder" version = "0.12.0" @@ -1440,7 +1220,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1505,43 +1285,45 @@ dependencies = [ "digest 0.10.7", "elliptic-curve", "rfc6979", - "signature 2.2.0", + "signature", "spki", ] [[package]] name = "ed25519" -version = "1.5.3" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ - "signature 1.6.4", + "pkcs8", + "signature", ] [[package]] name = "ed25519-dalek" -version = "1.0.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ - "curve25519-dalek 3.2.0", + "curve25519-dalek", "ed25519", - "rand 0.7.3", + "rand_core 0.6.4", "serde", - "sha2 0.9.9", + "sha2", + "subtle", "zeroize", ] [[package]] name = "ed25519-dalek-bip32" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d2be62a4061b872c8c0873ee4fc6f101ce7b889d039f019c5fa2af471a59908" +checksum = "6b49a684b133c4980d7ee783936af771516011c8cd15f429dbda77245e282f03" dependencies = [ "derivation-path", "ed25519-dalek", - "hmac 0.12.1", - "sha2 0.10.9", + "hmac", + "sha2", ] [[package]] @@ -1595,19 +1377,6 @@ dependencies = [ "regex", ] -[[package]] -name = "env_logger" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" -dependencies = [ - "atty", - "humantime", - "log", - "regex", - "termcolor", -] - [[package]] name = "env_logger" version = "0.11.8" @@ -1900,19 +1669,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", - "wasm-bindgen", -] - [[package]] name = "getrandom" version = "0.2.16" @@ -2074,21 +1830,21 @@ dependencies = [ ] [[package]] -name = "hashbrown" -version = "0.12.3" +name = "hash32" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" dependencies = [ - "ahash 0.7.8", + "byteorder", ] [[package]] name = "hashbrown" -version = "0.13.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash 0.8.12", + "ahash 0.7.8", ] [[package]] @@ -2125,15 +1881,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - [[package]] name = "hermit-abi" version = "0.5.2" @@ -2152,16 +1899,6 @@ version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12cb882ccb290b8646e554b157ab0b71e64e8d5bef775cd66b6531e52d302669" -[[package]] -name = "hmac" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" -dependencies = [ - "crypto-mac", - "digest 0.9.0", -] - [[package]] name = "hmac" version = "0.12.1" @@ -2171,17 +1908,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "hmac-drbg" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ea0a1394df5b6574da6e0c1ade9e78868c9fb0a4e5ef4428e32da4676b85b1" -dependencies = [ - "digest 0.9.0", - "generic-array", - "hmac 0.8.1", -] - [[package]] name = "http" version = "0.2.12" @@ -2256,12 +1982,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "humantime" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" - [[package]] name = "hyper" version = "0.14.32" @@ -2543,14 +2263,14 @@ dependencies = [ [[package]] name = "indicatif" -version = "0.17.11" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" +checksum = "ade6dfcba0dfb62ad59e59e7241ec8912af34fd29e0e743e3db992bd278e8b65" dependencies = [ "console", - "number_prefix", "portable-atomic", "unicode-width", + "unit-prefix", "web-time", ] @@ -2607,18 +2327,9 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itertools" -version = "0.10.5" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] @@ -2661,7 +2372,7 @@ checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" dependencies = [ "cesu8", "cfg-if", - "combine", + "combine 4.6.7", "jni-sys", "log", "thiserror 1.0.69", @@ -2884,13 +2595,17 @@ dependencies = [ ] [[package]] -name = "kaigan" -version = "0.2.6" +name = "k256" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ba15de5aeb137f0f65aa3bf82187647f1285abfe5b20c80c2c37f7007ad519a" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" dependencies = [ - "borsh 0.10.4", - "serde", + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2", + "signature", ] [[package]] @@ -2908,7 +2623,7 @@ version = "1.0.2" dependencies = [ "clap", "dotenv", - "env_logger 0.11.8", + "env_logger", "kora-lib", "log", "serde_json", @@ -2927,7 +2642,7 @@ dependencies = [ "async-trait", "base64 0.22.1", "bincode", - "borsh 1.5.7", + "borsh", "bs58", "chrono", "clap", @@ -2935,11 +2650,11 @@ dependencies = [ "deadpool-redis", "dirs", "dotenv", - "env_logger 0.11.8", + "env_logger", "futures", "futures-util", "hex", - "hmac 0.12.1", + "hmac", "http 0.2.12", "http-body 1.0.1", "http-body-util", @@ -2962,21 +2677,23 @@ dependencies = [ "serde", "serde_json", "serial_test", - "sha2 0.10.9", + "sha2", "solana-address-lookup-table-interface", "solana-client", "solana-commitment-config", + "solana-compute-budget-interface", "solana-message", "solana-program", + "solana-program-pack", "solana-sdk", "solana-signers", "solana-system-interface", "solana-transaction-status", "solana-transaction-status-client-types", - "spl-associated-token-account 6.0.0", + "spl-associated-token-account-interface", "spl-pod", - "spl-token 7.0.0", - "spl-token-2022 8.0.1", + "spl-token-2022-interface", + "spl-token-interface", "subtle", "tempfile", "thiserror 1.0.69", @@ -3012,54 +2729,6 @@ dependencies = [ "libc", ] -[[package]] -name = "libsecp256k1" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9d220bc1feda2ac231cb78c3d26f27676b8cf82c96971f7aeef3d0cf2797c73" -dependencies = [ - "arrayref", - "base64 0.12.3", - "digest 0.9.0", - "hmac-drbg", - "libsecp256k1-core", - "libsecp256k1-gen-ecmult", - "libsecp256k1-gen-genmult", - "rand 0.7.3", - "serde", - "sha2 0.9.9", - "typenum", -] - -[[package]] -name = "libsecp256k1-core" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f6ab710cec28cef759c5f18671a27dae2a5f952cdaaee1d8e2908cb2478a80" -dependencies = [ - "crunchy", - "digest 0.9.0", - "subtle", -] - -[[package]] -name = "libsecp256k1-gen-ecmult" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccab96b584d38fac86a83f07e659f0deafd0253dc096dab5a36d53efe653c5c3" -dependencies = [ - "libsecp256k1-core", -] - -[[package]] -name = "libsecp256k1-gen-genmult" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67abfe149395e3aa1c48a2beb32b068e2334402df8181f818d3aee2b304c4f5d" -dependencies = [ - "libsecp256k1-core", -] - [[package]] name = "linked-hash-map" version = "0.5.6" @@ -3115,15 +2784,6 @@ version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" -[[package]] -name = "memmap2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" -dependencies = [ - "libc", -] - [[package]] name = "memoffset" version = "0.9.1" @@ -3408,7 +3068,7 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ - "hermit-abi 0.5.2", + "hermit-abi", "libc", ] @@ -3434,12 +3094,6 @@ dependencies = [ "syn 2.0.104", ] -[[package]] -name = "number_prefix" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" - [[package]] name = "object" version = "0.36.7" @@ -3551,7 +3205,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "sha2 0.10.9", + "sha2", ] [[package]] @@ -3583,12 +3237,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "pathdiff" version = "0.2.3" @@ -3678,7 +3326,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5" dependencies = [ "pest", - "sha2 0.10.9", + "sha2", ] [[package]] @@ -3815,15 +3463,6 @@ dependencies = [ "elliptic-curve", ] -[[package]] -name = "proc-macro-crate" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" -dependencies = [ - "toml 0.5.11", -] - [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -4033,19 +3672,6 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" -[[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc", -] - [[package]] name = "rand" version = "0.8.5" @@ -4067,16 +3693,6 @@ dependencies = [ "rand_core 0.9.3", ] -[[package]] -name = "rand_chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", -] - [[package]] name = "rand_chacha" version = "0.3.1" @@ -4097,15 +3713,6 @@ dependencies = [ "rand_core 0.9.3", ] -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", -] - [[package]] name = "rand_core" version = "0.6.4" @@ -4124,15 +3731,6 @@ dependencies = [ "getrandom 0.3.3", ] -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core 0.5.1", -] - [[package]] name = "raw-cpuid" version = "11.5.0" @@ -4172,7 +3770,7 @@ dependencies = [ "backon", "bytes", "cfg-if", - "combine", + "combine 4.6.7", "futures-channel", "futures-util", "itoa", @@ -4329,7 +3927,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "hmac 0.12.1", + "hmac", "subtle", ] @@ -4404,7 +4002,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" dependencies = [ "arrayvec", - "borsh 1.5.7", + "borsh", "bytes", "num-traits", "rand 0.8.5", @@ -4934,19 +4532,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" -[[package]] -name = "sha2" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" -dependencies = [ - "block-buffer 0.9.0", - "cfg-if", - "cpufeatures", - "digest 0.9.0", - "opaque-debug", -] - [[package]] name = "sha2" version = "0.10.9" @@ -4983,16 +4568,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "signal-hook" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" -dependencies = [ - "libc", - "signal-hook-registry", -] - [[package]] name = "signal-hook-registry" version = "1.4.6" @@ -5002,12 +4577,6 @@ dependencies = [ "libc", ] -[[package]] -name = "signature" -version = "1.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" - [[package]] name = "signature" version = "2.2.0" @@ -5092,9 +4661,9 @@ dependencies = [ [[package]] name = "solana-account" -version = "2.2.1" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f949fe4edaeaea78c844023bfc1c898e0b1f5a100f8a8d2d0f85d0a7b090258" +checksum = "39e5a5c395c41a30f0e36fa487b8cda3280f0d9e4c7b461c0881fa23564f4c28" dependencies = [ "bincode", "serde", @@ -5102,7 +4671,7 @@ dependencies = [ "serde_derive", "solana-account-info", "solana-clock", - "solana-instruction", + "solana-instruction-error", "solana-pubkey", "solana-sdk-ids", "solana-sysvar", @@ -5110,9 +4679,9 @@ dependencies = [ [[package]] name = "solana-account-decoder" -version = "2.3.7" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5963fbe3e1099613c270fd5ebc0ff5c6e88a2bea2505b6e348daa0466282cd6" +checksum = "64285c3c7bbdaf775e72d8d42b0fa199e120a4633248e0c53caf05849d5e4fc7" dependencies = [ "Inflector", "base64 0.22.1", @@ -5126,7 +4695,7 @@ dependencies = [ "solana-account-decoder-client-types", "solana-address-lookup-table-interface", "solana-clock", - "solana-config-program-client", + "solana-config-interface", "solana-epoch-schedule", "solana-fee-calculator", "solana-instruction", @@ -5143,19 +4712,19 @@ dependencies = [ "solana-sysvar", "solana-vote-interface", "spl-generic-token", - "spl-token 8.0.0", - "spl-token-2022 8.0.1", - "spl-token-group-interface 0.6.0", - "spl-token-metadata-interface 0.7.0", + "spl-token-2022-interface", + "spl-token-group-interface", + "spl-token-interface", + "spl-token-metadata-interface", "thiserror 2.0.17", "zstd", ] [[package]] name = "solana-account-decoder-client-types" -version = "2.3.7" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59f2101f4cc33e3fbfc8d1d23ea35d8532d6f1fa6a7c7081742e886f98f33126" +checksum = "bff10a635163974214065835c82462768f3fb2eaeef558d27edcbd54d1230ddc" dependencies = [ "base64 0.22.1", "bs58", @@ -5169,9 +4738,9 @@ dependencies = [ [[package]] name = "solana-account-info" -version = "2.3.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8f5152a288ef1912300fc6efa6c2d1f9bb55d9398eb6c72326360b8063987da" +checksum = "82f4691b69b172c687d218dd2f1f23fc7ea5e9aa79df9ac26dab3d8dd829ce48" dependencies = [ "bincode", "serde", @@ -5180,11 +4749,33 @@ dependencies = [ "solana-pubkey", ] +[[package]] +name = "solana-address" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a7a457086457ea9db9a5199d719dc8734dc2d0342fad0d8f77633c31eb62f19" +dependencies = [ + "borsh", + "bytemuck", + "bytemuck_derive", + "curve25519-dalek", + "five8", + "five8_const", + "rand 0.8.5", + "serde", + "serde_derive", + "solana-atomic-u64", + "solana-define-syscall 3.0.0", + "solana-program-error", + "solana-sanitize", + "solana-sha256-hasher", +] + [[package]] name = "solana-address-lookup-table-interface" -version = "2.2.2" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1673f67efe870b64a65cb39e6194be5b26527691ce5922909939961a6e6b395" +checksum = "e2f56cac5e70517a2f27d05e5100b20de7182473ffd0035b23ea273307905987" dependencies = [ "bincode", "bytemuck", @@ -5192,6 +4783,7 @@ dependencies = [ "serde_derive", "solana-clock", "solana-instruction", + "solana-instruction-error", "solana-pubkey", "solana-sdk-ids", "solana-slot-hashes", @@ -5199,77 +4791,49 @@ dependencies = [ [[package]] name = "solana-atomic-u64" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52e52720efe60465b052b9e7445a01c17550666beec855cce66f44766697bc2" +checksum = "a933ff1e50aff72d02173cfcd7511bd8540b027ee720b75f353f594f834216d0" dependencies = [ "parking_lot", ] [[package]] name = "solana-big-mod-exp" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75db7f2bbac3e62cfd139065d15bcda9e2428883ba61fc8d27ccb251081e7567" +checksum = "30c80fb6d791b3925d5ec4bf23a7c169ef5090c013059ec3ed7d0b2c04efa085" dependencies = [ "num-bigint 0.4.6", "num-traits", - "solana-define-syscall", -] - -[[package]] -name = "solana-bincode" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19a3787b8cf9c9fe3dd360800e8b70982b9e5a8af9e11c354b6665dd4a003adc" -dependencies = [ - "bincode", - "serde", - "solana-instruction", + "solana-define-syscall 3.0.0", ] [[package]] name = "solana-blake3-hasher" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a0801e25a1b31a14494fc80882a036be0ffd290efc4c2d640bfcca120a4672" +checksum = "ffa2e3bdac3339c6d0423275e45dafc5ac25f4d43bf344d026a3cc9a85e244a6" dependencies = [ "blake3", - "solana-define-syscall", + "solana-define-syscall 3.0.0", "solana-hash", - "solana-sanitize", -] - -[[package]] -name = "solana-bn254" -version = "2.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4420f125118732833f36facf96a27e7b78314b2d642ba07fa9ffdacd8d79e243" -dependencies = [ - "ark-bn254", - "ark-ec", - "ark-ff", - "ark-serialize", - "bytemuck", - "solana-define-syscall", - "thiserror 2.0.17", ] [[package]] name = "solana-borsh" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "718333bcd0a1a7aed6655aa66bef8d7fb047944922b2d3a18f49cbc13e73d004" +checksum = "dc402b16657abbfa9991cd5cbfac5a11d809f7e7d28d3bb291baeb088b39060e" dependencies = [ - "borsh 0.10.4", - "borsh 1.5.7", + "borsh", ] [[package]] name = "solana-client" -version = "2.3.7" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4a1e134e7f683fca78ff3912f1590858b51f6a64aad417d30baca8926ed2fd" +checksum = "b78c92bb6a89fadf6a4aa70e44e8c59b7bc023d86b9443d740e026397a3cb0f7" dependencies = [ "async-trait", "bincode", @@ -5301,11 +4865,11 @@ dependencies = [ "solana-signature", "solana-signer", "solana-streamer", - "solana-thin-client", "solana-time-utils", "solana-tpu-client", "solana-transaction", "solana-transaction-error", + "solana-transaction-status-client-types", "solana-udp-client", "thiserror 2.0.17", "tokio", @@ -5313,9 +4877,9 @@ dependencies = [ [[package]] name = "solana-client-traits" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83f0071874e629f29e0eb3dab8a863e98502ac7aba55b7e0df1803fc5cac72a7" +checksum = "08618ed587e128105510c54ae3e456b9a06d674d8640db75afe66dad65cb4e02" dependencies = [ "solana-account", "solana-commitment-config", @@ -5334,9 +4898,9 @@ dependencies = [ [[package]] name = "solana-clock" -version = "2.2.2" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bb482ab70fced82ad3d7d3d87be33d466a3498eb8aa856434ff3c0dfc2e2e31" +checksum = "fb62e9381182459a4520b5fe7fb22d423cae736239a6427fc398a88743d0ed59" dependencies = [ "serde", "serde_derive", @@ -5347,20 +4911,18 @@ dependencies = [ [[package]] name = "solana-cluster-type" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ace9fea2daa28354d107ea879cff107181d85cd4e0f78a2bedb10e1a428c97e" +checksum = "eb7692fa6bf10a1a86b450c4775526f56d7e0e2116a53313f2533b5694abea64" dependencies = [ - "serde", - "serde_derive", "solana-hash", ] [[package]] name = "solana-commitment-config" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac49c4dde3edfa832de1697e9bcdb7c3b3f7cb7a1981b7c62526c8bb6700fb73" +checksum = "5fa5933a62dadb7d3ed35e6329de5cebb0678acc8f9cfdf413269084eeccc63f" dependencies = [ "serde", "serde_derive", @@ -5368,35 +4930,36 @@ dependencies = [ [[package]] name = "solana-compute-budget-interface" -version = "2.2.2" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8432d2c4c22d0499aa06d62e4f7e333f81777b3d7c96050ae9e5cb71a8c3aee4" +checksum = "8292c436b269ad23cecc8b24f7da3ab07ca111661e25e00ce0e1d22771951ab9" dependencies = [ - "borsh 1.5.7", - "serde", - "serde_derive", "solana-instruction", "solana-sdk-ids", ] [[package]] -name = "solana-config-program-client" -version = "0.0.2" +name = "solana-config-interface" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53aceac36f105fd4922e29b4f0c1f785b69d7b3e7e387e384b8985c8e0c3595e" +checksum = "63e401ae56aed512821cc7a0adaa412ff97fecd2dff4602be7b1330d2daec0c4" dependencies = [ "bincode", - "borsh 0.10.4", - "kaigan", "serde", - "solana-program", + "serde_derive", + "solana-account", + "solana-instruction", + "solana-pubkey", + "solana-sdk-ids", + "solana-short-vec", + "solana-system-interface", ] [[package]] name = "solana-connection-cache" -version = "2.3.7" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be7fcabde8fdaa5a0e6fbbd0ed4cd07e5754e7d187b69be663811c236b891961" +checksum = "b7ce2d2f1c270cfc06066799f3220c694ba4fdadbcae16f1138ba15f64924a4c" dependencies = [ "async-trait", "bincode", @@ -5417,12 +4980,12 @@ dependencies = [ [[package]] name = "solana-cpi" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dc71126edddc2ba014622fc32d0f5e2e78ec6c5a1e0eb511b85618c09e9ea11" +checksum = "16238feb63d1cbdf915fb287f29ef7a7ebf81469bd6214f8b72a53866b593f8f" dependencies = [ "solana-account-info", - "solana-define-syscall", + "solana-define-syscall 3.0.0", "solana-instruction", "solana-program-error", "solana-pubkey", @@ -5437,58 +5000,40 @@ checksum = "b162f50499b391b785d57b2f2c73e3b9754d88fd4894bef444960b00bda8dcca" dependencies = [ "bytemuck", "bytemuck_derive", - "curve25519-dalek 4.1.3", - "solana-define-syscall", + "curve25519-dalek", + "solana-define-syscall 2.3.0", "subtle", "thiserror 2.0.17", ] [[package]] -name = "solana-decode-error" +name = "solana-define-syscall" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c781686a18db2f942e70913f7ca15dc120ec38dcab42ff7557db2c70c625a35" -dependencies = [ - "num-traits", -] +checksum = "2ae3e2abcf541c8122eafe9a625d4d194b4023c20adde1e251f94e056bb1aee2" [[package]] name = "solana-define-syscall" -version = "2.3.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ae3e2abcf541c8122eafe9a625d4d194b4023c20adde1e251f94e056bb1aee2" +checksum = "f9697086a4e102d28a156b8d6b521730335d6951bd39a5e766512bbe09007cee" [[package]] name = "solana-derivation-path" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "939756d798b25c5ec3cca10e06212bdca3b1443cb9bb740a38124f58b258737b" +checksum = "ff71743072690fdbdfcdc37700ae1cb77485aaad49019473a81aee099b1e0b8c" dependencies = [ "derivation-path", "qstring", "uriparse", ] -[[package]] -name = "solana-ed25519-program" -version = "2.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1feafa1691ea3ae588f99056f4bdd1293212c7ece28243d7da257c443e84753" -dependencies = [ - "bytemuck", - "bytemuck_derive", - "ed25519-dalek", - "solana-feature-set", - "solana-instruction", - "solana-precompile-error", - "solana-sdk-ids", -] - [[package]] name = "solana-epoch-info" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ef6f0b449290b0b9f32973eefd95af35b01c5c0c34c569f936c34c5b20d77b" +checksum = "f8a6b69bd71386f61344f2bcf0f527f5fd6dd3b22add5880e2e1bf1dd1fa8059" dependencies = [ "serde", "serde_derive", @@ -5496,9 +5041,9 @@ dependencies = [ [[package]] name = "solana-epoch-rewards" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b575d3dd323b9ea10bb6fe89bf6bf93e249b215ba8ed7f68f1a3633f384db7" +checksum = "b319a4ed70390af911090c020571f0ff1f4ec432522d05ab89f5c08080381995" dependencies = [ "serde", "serde_derive", @@ -5510,9 +5055,9 @@ dependencies = [ [[package]] name = "solana-epoch-rewards-hasher" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c5fd2662ae7574810904585fd443545ed2b568dbd304b25a31e79ccc76e81b" +checksum = "e507099d0c2c5d7870c9b1848281ea67bbeee80d171ca85003ee5767994c9c38" dependencies = [ "siphasher 0.3.11", "solana-hash", @@ -5521,9 +5066,9 @@ dependencies = [ [[package]] name = "solana-epoch-schedule" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fce071fbddecc55d727b1d7ed16a629afe4f6e4c217bc8d00af3b785f6f67ed" +checksum = "6e5481e72cc4d52c169db73e4c0cd16de8bc943078aac587ec4817a75cc6388f" dependencies = [ "serde", "serde_derive", @@ -5532,11 +5077,21 @@ dependencies = [ "solana-sysvar-id", ] +[[package]] +name = "solana-epoch-stake" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc6693d0ea833b880514b9b88d95afb80b42762dca98b0712465d1fcbbcb89e" +dependencies = [ + "solana-define-syscall 3.0.0", + "solana-pubkey", +] + [[package]] name = "solana-example-mocks" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84461d56cbb8bb8d539347151e0525b53910102e4bced875d49d5139708e39d3" +checksum = "978855d164845c1b0235d4b4d101cadc55373fffaf0b5b6cfa2194d25b2ed658" dependencies = [ "serde", "serde_derive", @@ -5555,42 +5110,22 @@ dependencies = [ [[package]] name = "solana-feature-gate-interface" -version = "2.2.2" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f5c5382b449e8e4e3016fb05e418c53d57782d8b5c30aa372fc265654b956d" +checksum = "7347ab62e6d47a82e340c865133795b394feea7c2b2771d293f57691c6544c3f" dependencies = [ - "bincode", "serde", "serde_derive", - "solana-account", - "solana-account-info", - "solana-instruction", "solana-program-error", "solana-pubkey", - "solana-rent", "solana-sdk-ids", - "solana-system-interface", -] - -[[package]] -name = "solana-feature-set" -version = "2.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93b93971e289d6425f88e6e3cb6668c4b05df78b3c518c249be55ced8efd6b6d" -dependencies = [ - "ahash 0.8.12", - "lazy_static", - "solana-epoch-schedule", - "solana-hash", - "solana-pubkey", - "solana-sha256-hasher", ] [[package]] name = "solana-fee-calculator" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89bc408da0fb3812bc3008189d148b4d3e08252c79ad810b245482a3f70cd8d" +checksum = "2a73cc03ca4bed871ca174558108835f8323e85917bb38b9c81c7af2ab853efe" dependencies = [ "log", "serde", @@ -5599,79 +5134,41 @@ dependencies = [ [[package]] name = "solana-fee-structure" -version = "2.3.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33adf673581c38e810bf618f745bf31b683a0a4a4377682e6aaac5d9a058dd4e" +checksum = "5e2abdb1223eea8ec64136f39cb1ffcf257e00f915c957c35c0dd9e3f4e700b0" dependencies = [ "serde", "serde_derive", - "solana-message", - "solana-native-token", -] - -[[package]] -name = "solana-genesis-config" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3725085d47b96d37fef07a29d78d2787fc89a0b9004c66eed7753d1e554989f" -dependencies = [ - "bincode", - "chrono", - "memmap2", - "serde", - "serde_derive", - "solana-account", - "solana-clock", - "solana-cluster-type", - "solana-epoch-schedule", - "solana-fee-calculator", - "solana-hash", - "solana-inflation", - "solana-keypair", - "solana-logger", - "solana-poh-config", - "solana-pubkey", - "solana-rent", - "solana-sdk-ids", - "solana-sha256-hasher", - "solana-shred-version", - "solana-signer", - "solana-time-utils", ] [[package]] name = "solana-hard-forks" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c28371f878e2ead55611d8ba1b5fb879847156d04edea13693700ad1a28baf" -dependencies = [ - "serde", - "serde_derive", -] +checksum = "0abacc4b66ce471f135f48f22facf75cbbb0f8a252fbe2c1e0aa59d5b203f519" [[package]] name = "solana-hash" -version = "2.3.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b96e9f0300fa287b545613f007dfe20043d7812bee255f418c1eb649c93b63" +checksum = "8a063723b9e84c14d8c0d2cdf0268207dc7adecf546e31251f9e07c7b00b566c" dependencies = [ - "borsh 1.5.7", + "borsh", "bytemuck", "bytemuck_derive", "five8", - "js-sys", "serde", "serde_derive", "solana-atomic-u64", "solana-sanitize", - "wasm-bindgen", ] [[package]] name = "solana-inflation" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23eef6a09eb8e568ce6839573e4966850e85e9ce71e6ae1a6c930c1c43947de3" +checksum = "e92f37a14e7c660628752833250dd3dcd8e95309876aee751d7f8769a27947c6" dependencies = [ "serde", "serde_derive", @@ -5679,31 +5176,41 @@ dependencies = [ [[package]] name = "solana-instruction" -version = "2.3.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47298e2ce82876b64f71e9d13a46bc4b9056194e7f9937ad3084385befa50885" +checksum = "8df4e8fcba01d7efa647ed20a081c234475df5e11a93acb4393cc2c9a7b99bab" dependencies = [ "bincode", - "borsh 1.5.7", - "getrandom 0.2.16", - "js-sys", - "num-traits", + "borsh", "serde", "serde_derive", - "solana-define-syscall", + "solana-define-syscall 3.0.0", + "solana-instruction-error", "solana-pubkey", - "wasm-bindgen", +] + +[[package]] +name = "solana-instruction-error" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f0d483b8ae387178d9210e0575b666b05cdd4bd0f2f188128249f6e454d39d" +dependencies = [ + "num-traits", + "serde", + "serde_derive", + "solana-program-error", ] [[package]] name = "solana-instructions-sysvar" -version = "2.2.2" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0e85a6fad5c2d0c4f5b91d34b8ca47118fc593af706e523cdbedf846a954f57" +checksum = "7ddf67876c541aa1e21ee1acae35c95c6fbc61119814bfef70579317a5e26955" dependencies = [ "bitflags 2.9.1", "solana-account-info", "solana-instruction", + "solana-instruction-error", "solana-program-error", "solana-pubkey", "solana-sanitize", @@ -5714,40 +5221,38 @@ dependencies = [ [[package]] name = "solana-keccak-hasher" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7aeb957fbd42a451b99235df4942d96db7ef678e8d5061ef34c9b34cae12f79" +checksum = "57eebd3012946913c8c1b8b43cdf8a6249edb09c0b6be3604ae910332a3acd97" dependencies = [ "sha3", - "solana-define-syscall", + "solana-define-syscall 3.0.0", "solana-hash", - "solana-sanitize", ] [[package]] name = "solana-keypair" -version = "2.2.3" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd3f04aa1a05c535e93e121a95f66e7dcccf57e007282e8255535d24bf1e98bb" +checksum = "952ed9074c12edd2060cb09c2a8c664303f4ab7f7056a407ac37dd1da7bdaa3e" dependencies = [ "ed25519-dalek", "ed25519-dalek-bip32", "five8", - "rand 0.7.3", + "rand 0.8.5", "solana-derivation-path", "solana-pubkey", "solana-seed-derivable", "solana-seed-phrase", "solana-signature", "solana-signer", - "wasm-bindgen", ] [[package]] name = "solana-last-restart-slot" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a6360ac2fdc72e7463565cd256eedcf10d7ef0c28a1249d261ec168c1b55cdd" +checksum = "dcda154ec827f5fc1e4da0af3417951b7e9b8157540f81f936c4a8b1156134d0" dependencies = [ "serde", "serde_derive", @@ -5758,9 +5263,9 @@ dependencies = [ [[package]] name = "solana-loader-v2-interface" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8ab08006dad78ae7cd30df8eea0539e207d08d91eaefb3e1d49a446e1c49654" +checksum = "1e4a6f0ad4fd9c30679bfee2ce3ea6a449cac38049f210480b751f65676dfe82" dependencies = [ "serde", "serde_bytes", @@ -5772,24 +5277,9 @@ dependencies = [ [[package]] name = "solana-loader-v3-interface" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f7162a05b8b0773156b443bccd674ea78bb9aa406325b467ea78c06c99a63a2" -dependencies = [ - "serde", - "serde_bytes", - "serde_derive", - "solana-instruction", - "solana-pubkey", - "solana-sdk-ids", - "solana-system-interface", -] - -[[package]] -name = "solana-loader-v4-interface" -version = "2.2.1" +version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "706a777242f1f39a83e2a96a2a6cb034cb41169c6ecbee2cf09cb873d9659e7e" +checksum = "dee44c9b1328c5c712c68966fb8de07b47f3e7bac006e74ddd1bb053d3e46e5d" dependencies = [ "serde", "serde_bytes", @@ -5800,53 +5290,37 @@ dependencies = [ "solana-system-interface", ] -[[package]] -name = "solana-logger" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db8e777ec1afd733939b532a42492d888ec7c88d8b4127a5d867eb45c6eb5cd5" -dependencies = [ - "env_logger 0.9.3", - "lazy_static", - "libc", - "log", - "signal-hook", -] - [[package]] name = "solana-measure" -version = "2.3.7" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e0e02388fa871b8b42c59ff5f7123370c47a5f389f8e773b4c5402c20ec7e04" +checksum = "8dce9330421ef476f95c67f8210d734f9b6a38fc9fcd8abbd306ffbf23361067" [[package]] name = "solana-message" -version = "2.4.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1796aabce376ff74bf89b78d268fa5e683d7d7a96a0a4e4813ec34de49d5314b" +checksum = "85666605c9fd727f865ed381665db0a8fc29f984a030ecc1e40f43bfb2541623" dependencies = [ "bincode", "blake3", "lazy_static", "serde", "serde_derive", - "solana-bincode", + "solana-address", "solana-hash", "solana-instruction", - "solana-pubkey", "solana-sanitize", "solana-sdk-ids", "solana-short-vec", - "solana-system-interface", "solana-transaction-error", - "wasm-bindgen", ] [[package]] name = "solana-metrics" -version = "2.3.7" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1f79991e14c635e76ec1d61061305e4e0e6649e213bff2e1b92c59a789bc652" +checksum = "214a6a27f28156e0a0bfc1e218a4ac30c5fb42e0d1c481cd8f90de0b98fa0984" dependencies = [ "crossbeam-channel", "gethostname", @@ -5860,35 +5334,35 @@ dependencies = [ [[package]] name = "solana-msg" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f36a1a14399afaabc2781a1db09cb14ee4cc4ee5c7a5a3cfcc601811379a8092" +checksum = "264275c556ea7e22b9d3f87d56305546a38d4eee8ec884f3b126236cb7dcbbb4" dependencies = [ - "solana-define-syscall", + "solana-define-syscall 3.0.0", ] [[package]] name = "solana-native-token" -version = "2.3.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61515b880c36974053dd499c0510066783f0cc6ac17def0c7ef2a244874cf4a9" +checksum = "ae8dd4c280dca9d046139eb5b7a5ac9ad10403fbd64964c7d7571214950d758f" [[package]] name = "solana-net-utils" -version = "2.3.7" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f0d5d50fa415f82d18f2b610b7d6d1e747ebe4d2955e977d007e16f3af8d77" +checksum = "2c465c3bca426bfca3548c41352b5b358a0401bdd22b1fcef45474ce94cc23a1" dependencies = [ "anyhow", "bincode", "bytes", - "itertools 0.12.1", + "itertools", "log", "nix", "rand 0.8.5", "serde", "serde_derive", - "socket2 0.5.10", + "socket2 0.6.0", "solana-serde", "tokio", "url", @@ -5896,9 +5370,9 @@ dependencies = [ [[package]] name = "solana-nonce" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703e22eb185537e06204a5bd9d509b948f0066f2d1d814a6f475dafb3ddf1325" +checksum = "abbdc6c8caf1c08db9f36a50967539d0f72b9f1d4aea04fec5430f532e5afadc" dependencies = [ "serde", "serde_derive", @@ -5908,23 +5382,11 @@ dependencies = [ "solana-sha256-hasher", ] -[[package]] -name = "solana-nonce-account" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde971a20b8dbf60144d6a84439dda86b5466e00e2843091fe731083cda614da" -dependencies = [ - "solana-account", - "solana-hash", - "solana-nonce", - "solana-sdk-ids", -] - [[package]] name = "solana-offchain-message" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b526398ade5dea37f1f147ce55dae49aa017a5d7326606359b0445ca8d946581" +checksum = "f6e2a1141a673f72a05cf406b99e4b2b8a457792b7c01afa07b3f00d4e2de393" dependencies = [ "num_enum", "solana-hash", @@ -5938,9 +5400,9 @@ dependencies = [ [[package]] name = "solana-packet" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "004f2d2daf407b3ec1a1ca5ec34b3ccdfd6866dd2d3c7d0715004a96e4b6d127" +checksum = "6edf2f25743c95229ac0fdc32f8f5893ef738dbf332c669e9861d33ddb0f469d" dependencies = [ "bincode", "bitflags 2.9.1", @@ -5952,16 +5414,16 @@ dependencies = [ [[package]] name = "solana-perf" -version = "2.3.7" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee5e3e876ebce18775e8264b4673f45c2b5990e726a45a7f0cd9f3bd6cb1403" +checksum = "8a5096d12294fb0da9819fe198d0f003a111d29cfa3c0e49b9ed6380577396e5" dependencies = [ "ahash 0.8.12", "bincode", "bv", "bytes", "caps", - "curve25519-dalek 4.1.3", + "curve25519-dalek", "dlopen2", "fnv", "libc", @@ -5982,48 +5444,11 @@ dependencies = [ "solana-time-utils", ] -[[package]] -name = "solana-poh-config" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d650c3b4b9060082ac6b0efbbb66865089c58405bfb45de449f3f2b91eccee75" -dependencies = [ - "serde", - "serde_derive", -] - -[[package]] -name = "solana-precompile-error" -version = "2.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d87b2c1f5de77dfe2b175ee8dd318d196aaca4d0f66f02842f80c852811f9f8" -dependencies = [ - "num-traits", - "solana-decode-error", -] - -[[package]] -name = "solana-precompiles" -version = "2.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36e92768a57c652edb0f5d1b30a7d0bc64192139c517967c18600debe9ae3832" -dependencies = [ - "lazy_static", - "solana-ed25519-program", - "solana-feature-set", - "solana-message", - "solana-precompile-error", - "solana-pubkey", - "solana-sdk-ids", - "solana-secp256k1-program", - "solana-secp256r1-program", -] - [[package]] name = "solana-presigner" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81a57a24e6a4125fc69510b6774cd93402b943191b6cddad05de7281491c90fe" +checksum = "0f704eaf825be3180832445b9e4983b875340696e8e7239bf2d535b0f86c14a2" dependencies = [ "solana-pubkey", "solana-signature", @@ -6032,57 +5457,31 @@ dependencies = [ [[package]] name = "solana-program" -version = "2.3.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98eca145bd3545e2fbb07166e895370576e47a00a7d824e325390d33bf467210" +checksum = "91b12305dd81045d705f427acd0435a2e46444b65367d7179d7bdcfc3bc5f5eb" dependencies = [ - "bincode", - "blake3", - "borsh 0.10.4", - "borsh 1.5.7", - "bs58", - "bytemuck", - "console_error_panic_hook", - "console_log", - "getrandom 0.2.16", - "lazy_static", - "log", "memoffset", - "num-bigint 0.4.6", - "num-derive", - "num-traits", - "rand 0.8.5", - "serde", - "serde_bytes", - "serde_derive", "solana-account-info", - "solana-address-lookup-table-interface", - "solana-atomic-u64", "solana-big-mod-exp", - "solana-bincode", "solana-blake3-hasher", "solana-borsh", "solana-clock", "solana-cpi", - "solana-decode-error", - "solana-define-syscall", + "solana-define-syscall 3.0.0", "solana-epoch-rewards", "solana-epoch-schedule", + "solana-epoch-stake", "solana-example-mocks", - "solana-feature-gate-interface", "solana-fee-calculator", "solana-hash", "solana-instruction", + "solana-instruction-error", "solana-instructions-sysvar", "solana-keccak-hasher", "solana-last-restart-slot", - "solana-loader-v2-interface", - "solana-loader-v3-interface", - "solana-loader-v4-interface", - "solana-message", "solana-msg", "solana-native-token", - "solana-nonce", "solana-program-entrypoint", "solana-program-error", "solana-program-memory", @@ -6090,9 +5489,7 @@ dependencies = [ "solana-program-pack", "solana-pubkey", "solana-rent", - "solana-sanitize", "solana-sdk-ids", - "solana-sdk-macro", "solana-secp256k1-recover", "solana-serde-varint", "solana-serialize-utils", @@ -6101,22 +5498,18 @@ dependencies = [ "solana-slot-hashes", "solana-slot-history", "solana-stable-layout", - "solana-stake-interface", - "solana-system-interface", "solana-sysvar", "solana-sysvar-id", - "solana-vote-interface", - "thiserror 2.0.17", - "wasm-bindgen", ] [[package]] name = "solana-program-entrypoint" -version = "2.3.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32ce041b1a0ed275290a5008ee1a4a6c48f5054c8a3d78d313c08958a06aedbd" +checksum = "6557cf5b5e91745d1667447438a1baa7823c6086e4ece67f8e6ebfa7a8f72660" dependencies = [ "solana-account-info", + "solana-define-syscall 3.0.0", "solana-msg", "solana-program-error", "solana-pubkey", @@ -6124,76 +5517,54 @@ dependencies = [ [[package]] name = "solana-program-error" -version = "2.2.2" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ee2e0217d642e2ea4bee237f37bd61bb02aec60da3647c48ff88f6556ade775" +checksum = "a1af32c995a7b692a915bb7414d5f8e838450cf7c70414e763d8abcae7b51f28" dependencies = [ - "borsh 1.5.7", - "num-traits", + "borsh", "serde", "serde_derive", - "solana-decode-error", - "solana-instruction", - "solana-msg", - "solana-pubkey", ] [[package]] name = "solana-program-memory" -version = "2.3.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a5426090c6f3fd6cfdc10685322fede9ca8e5af43cd6a59e98bfe4e91671712" +checksum = "10e5660c60749c7bfb30b447542529758e4dbcecd31b1e8af1fdc92e2bdde90a" dependencies = [ - "solana-define-syscall", + "solana-define-syscall 3.0.0", ] [[package]] name = "solana-program-option" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc677a2e9bc616eda6dbdab834d463372b92848b2bfe4a1ed4e4b4adba3397d0" +checksum = "8e7b4ddb464f274deb4a497712664c3b612e3f5f82471d4e47710fc4ab1c3095" [[package]] name = "solana-program-pack" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "319f0ef15e6e12dc37c597faccb7d62525a509fec5f6975ecb9419efddeb277b" +checksum = "c169359de21f6034a63ebf96d6b380980307df17a8d371344ff04a883ec4e9d0" dependencies = [ "solana-program-error", ] [[package]] name = "solana-pubkey" -version = "2.4.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b62adb9c3261a052ca1f999398c388f1daf558a1b492f60a6d9e64857db4ff1" +checksum = "8909d399deb0851aa524420beeb5646b115fd253ef446e35fe4504c904da3941" dependencies = [ - "borsh 0.10.4", - "borsh 1.5.7", - "bytemuck", - "bytemuck_derive", - "curve25519-dalek 4.1.3", - "five8", - "five8_const", - "getrandom 0.2.16", - "js-sys", - "num-traits", "rand 0.8.5", - "serde", - "serde_derive", - "solana-atomic-u64", - "solana-decode-error", - "solana-define-syscall", - "solana-sanitize", - "solana-sha256-hasher", - "wasm-bindgen", + "solana-address", ] [[package]] name = "solana-pubsub-client" -version = "2.3.7" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b90bcec41efc8ed9e6b765e043e9fb5984b5c3fbf16f4d2c1dc827fd4b35e2" +checksum = "38812207b0b1b66a7df0558df9a6d53eb7aa495d00ce0d8bef1628b3774a5f29" dependencies = [ "crossbeam-channel", "futures-util", @@ -6218,14 +5589,14 @@ dependencies = [ [[package]] name = "solana-quic-client" -version = "2.3.7" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f5c70a7b38bf0b672f51a718c4b377adf0ae218d8d576024e7c3ed00e7ee86" +checksum = "bff930459fa06e95cb2d020f5be1b3d47b9f3a0e22e68c67b50537dca908b3aa" dependencies = [ "async-lock 3.4.1", "async-trait", "futures", - "itertools 0.12.1", + "itertools", "log", "quinn", "quinn-proto", @@ -6248,27 +5619,28 @@ dependencies = [ [[package]] name = "solana-quic-definitions" -version = "2.3.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf0d4d5b049eb1d0c35f7b18f305a27c8986fc5c0c9b383e97adaa35334379e" +checksum = "15319accf7d3afd845817aeffa6edd8cc185f135cefbc6b985df29cfd8c09609" dependencies = [ "solana-keypair", ] [[package]] name = "solana-rayon-threadlimit" -version = "2.3.7" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee3a2eac4ab76fc2e269d5b7e84d6e728b5b2ea30644e61182471bf4e0c4b44d" +checksum = "5034d175b90f0b5a5ff155eff5be091dfbc300ba162e1d35b8cd72be1a0d670b" dependencies = [ + "log", "num_cpus", ] [[package]] name = "solana-rent" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1aea8fdea9de98ca6e8c2da5827707fb3842833521b528a713810ca685d2480" +checksum = "b702d8c43711e3c8a9284a4f1bbc6a3de2553deb25b0c8142f9a44ef0ce5ddc1" dependencies = [ "serde", "serde_derive", @@ -6277,50 +5649,11 @@ dependencies = [ "solana-sysvar-id", ] -[[package]] -name = "solana-rent-collector" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c1e19f5d5108b0d824244425e43bc78bbb9476e2199e979b0230c9f632d3bf4" -dependencies = [ - "serde", - "serde_derive", - "solana-account", - "solana-clock", - "solana-epoch-schedule", - "solana-genesis-config", - "solana-pubkey", - "solana-rent", - "solana-sdk-ids", -] - -[[package]] -name = "solana-rent-debits" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f6f9113c6003492e74438d1288e30cffa8ccfdc2ef7b49b9e816d8034da18cd" -dependencies = [ - "solana-pubkey", - "solana-reward-info", -] - -[[package]] -name = "solana-reserved-account-keys" -version = "2.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4b22ea19ca2a3f28af7cd047c914abf833486bf7a7c4a10fc652fff09b385b1" -dependencies = [ - "lazy_static", - "solana-feature-set", - "solana-pubkey", - "solana-sdk-ids", -] - [[package]] name = "solana-reward-info" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18205b69139b1ae0ab8f6e11cdcb627328c0814422ad2482000fa2ca54ae4a2f" +checksum = "82be7946105c2ee6be9f9ee7bd18a068b558389221d29efa92b906476102bfcc" dependencies = [ "serde", "serde_derive", @@ -6328,9 +5661,9 @@ dependencies = [ [[package]] name = "solana-rpc-client" -version = "2.3.7" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40231712d6f1e5833ff1e101954786cbd0b5301098ea42384f7bb3e553085852" +checksum = "f7e038dea8817f8a713e0077226cfe638b93c44cf861e3f9545ef40b8e71bc78" dependencies = [ "async-trait", "base64 0.22.1", @@ -6368,9 +5701,9 @@ dependencies = [ [[package]] name = "solana-rpc-client-api" -version = "2.3.7" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a1be31922f97505007ccf969828b34e8dc43ce434a17f970b0edea8f0e66777" +checksum = "4908dbe81349db6ae851d808bef3078e49937400ad687e1c4e78b79f796ff88c" dependencies = [ "anyhow", "jsonrpc-core", @@ -6390,9 +5723,9 @@ dependencies = [ [[package]] name = "solana-rpc-client-nonce-utils" -version = "2.3.7" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2bd5b1ccc7fc945a9b0adad091836ee18b7688afd6979889849d5404254a14f" +checksum = "f981ef4da0734f459f5b71d1e8dcd6807c17721681714099c90ff6848c7dbb4a" dependencies = [ "solana-account", "solana-commitment-config", @@ -6407,9 +5740,9 @@ dependencies = [ [[package]] name = "solana-rpc-client-types" -version = "2.3.7" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e82a9b71f023a4bd511088f22e3c1f0e226a6e2e94b0656776509f234dd223a" +checksum = "0305c8cf8fca27a3f0385ad1d400b2cdde99d6cad2187370acdce117f93bd58f" dependencies = [ "base64 0.22.1", "bs58", @@ -6433,63 +5766,48 @@ dependencies = [ [[package]] name = "solana-sanitize" -version = "2.2.1" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf09694a0fc14e5ffb18f9b7b7c0f15ecb6eac5b5610bf76a1853459d19daf9" + +[[package]] +name = "solana-sbpf" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61f1bc1357b8188d9c4a3af3fc55276e56987265eb7ad073ae6f8180ee54cecf" +checksum = "0f224d906c14efc7ed7f42bc5fe9588f3f09db8cabe7f6023adda62a69678e1a" +dependencies = [ + "byteorder", + "combine 3.8.1", + "hash32", + "log", + "rustc-demangle", + "thiserror 2.0.17", +] [[package]] name = "solana-sdk" -version = "2.3.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cc0e4a7635b902791c44b6581bfb82f3ada32c5bc0929a64f39fe4bb384c86a" +checksum = "3f03df7969f5e723ad31b6c9eadccc209037ac4caa34d8dc259316b05c11e82b" dependencies = [ "bincode", "bs58", - "getrandom 0.1.16", - "js-sys", "serde", - "serde_json", "solana-account", - "solana-bn254", - "solana-client-traits", - "solana-cluster-type", - "solana-commitment-config", - "solana-compute-budget-interface", - "solana-decode-error", - "solana-derivation-path", - "solana-ed25519-program", "solana-epoch-info", "solana-epoch-rewards-hasher", - "solana-feature-set", "solana-fee-structure", - "solana-genesis-config", - "solana-hard-forks", "solana-inflation", - "solana-instruction", "solana-keypair", "solana-message", - "solana-native-token", - "solana-nonce-account", "solana-offchain-message", - "solana-packet", - "solana-poh-config", - "solana-precompile-error", - "solana-precompiles", "solana-presigner", "solana-program", "solana-program-memory", "solana-pubkey", - "solana-quic-definitions", - "solana-rent-collector", - "solana-rent-debits", - "solana-reserved-account-keys", - "solana-reward-info", "solana-sanitize", "solana-sdk-ids", "solana-sdk-macro", - "solana-secp256k1-program", - "solana-secp256k1-recover", - "solana-secp256r1-program", "solana-seed-derivable", "solana-seed-phrase", "solana-serde", @@ -6498,30 +5816,26 @@ dependencies = [ "solana-shred-version", "solana-signature", "solana-signer", - "solana-system-transaction", "solana-time-utils", "solana-transaction", - "solana-transaction-context", "solana-transaction-error", - "solana-validator-exit", "thiserror 2.0.17", - "wasm-bindgen", ] [[package]] name = "solana-sdk-ids" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5d8b9cc68d5c88b062a33e23a6466722467dde0035152d8fb1afbcdf350a5f" +checksum = "b1b6d6aaf60669c592838d382266b173881c65fb1cdec83b37cb8ce7cb89f9ad" dependencies = [ "solana-pubkey", ] [[package]] name = "solana-sdk-macro" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86280da8b99d03560f6ab5aca9de2e38805681df34e0bb8f238e69b29433b9df" +checksum = "d6430000e97083460b71d9fbadc52a2ab2f88f53b3a4c5e58c5ae3640a0e8c00" dependencies = [ "bs58", "proc-macro2", @@ -6529,131 +5843,91 @@ dependencies = [ "syn 2.0.104", ] -[[package]] -name = "solana-secp256k1-program" -version = "2.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f19833e4bc21558fe9ec61f239553abe7d05224347b57d65c2218aeeb82d6149" -dependencies = [ - "bincode", - "digest 0.10.7", - "libsecp256k1", - "serde", - "serde_derive", - "sha3", - "solana-feature-set", - "solana-instruction", - "solana-precompile-error", - "solana-sdk-ids", - "solana-signature", -] - [[package]] name = "solana-secp256k1-recover" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baa3120b6cdaa270f39444f5093a90a7b03d296d362878f7a6991d6de3bbe496" +checksum = "394a4470477d66296af5217970a905b1c5569032a7732c367fb69e5666c8607e" dependencies = [ - "borsh 1.5.7", - "libsecp256k1", - "solana-define-syscall", + "k256", + "solana-define-syscall 3.0.0", "thiserror 2.0.17", ] -[[package]] -name = "solana-secp256r1-program" -version = "2.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce0ae46da3071a900f02d367d99b2f3058fe2e90c5062ac50c4f20cfedad8f0f" -dependencies = [ - "bytemuck", - "openssl", - "solana-feature-set", - "solana-instruction", - "solana-precompile-error", - "solana-sdk-ids", -] - -[[package]] -name = "solana-security-txt" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "468aa43b7edb1f9b7b7b686d5c3aeb6630dc1708e86e31343499dd5c4d775183" - [[package]] name = "solana-seed-derivable" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beb82b5adb266c6ea90e5cf3967235644848eac476c5a1f2f9283a143b7c97f" +checksum = "ff7bdb72758e3bec33ed0e2658a920f1f35dfb9ed576b951d20d63cb61ecd95c" dependencies = [ "solana-derivation-path", ] [[package]] name = "solana-seed-phrase" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36187af2324f079f65a675ec22b31c24919cb4ac22c79472e85d819db9bbbc15" +checksum = "dc905b200a95f2ea9146e43f2a7181e3aeb55de6bc12afb36462d00a3c7310de" dependencies = [ - "hmac 0.12.1", + "hmac", "pbkdf2", - "sha2 0.10.9", + "sha2", ] [[package]] name = "solana-serde" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1931484a408af466e14171556a47adaa215953c7f48b24e5f6b0282763818b04" +checksum = "709a93cab694c70f40b279d497639788fc2ccbcf9b4aa32273d4b361322c02dd" dependencies = [ "serde", ] [[package]] name = "solana-serde-varint" -version = "2.2.2" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a7e155eba458ecfb0107b98236088c3764a09ddf0201ec29e52a0be40857113" +checksum = "3e5174c57d5ff3c1995f274d17156964664566e2cde18a07bba1586d35a70d3b" dependencies = [ "serde", ] [[package]] name = "solana-serialize-utils" -version = "2.2.1" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "817a284b63197d2b27afdba829c5ab34231da4a9b4e763466a003c40ca4f535e" +checksum = "56e41dd8feea239516c623a02f0a81c2367f4b604d7965237fed0751aeec33ed" dependencies = [ - "solana-instruction", + "solana-instruction-error", "solana-pubkey", "solana-sanitize", ] [[package]] name = "solana-sha256-hasher" -version = "2.3.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa3feb32c28765f6aa1ce8f3feac30936f16c5c3f7eb73d63a5b8f6f8ecdc44" +checksum = "a9b912ba6f71cb202c0c3773ec77bf898fa9fe0c78691a2d6859b3b5b8954719" dependencies = [ - "sha2 0.10.9", - "solana-define-syscall", + "sha2", + "solana-define-syscall 3.0.0", "solana-hash", ] [[package]] name = "solana-short-vec" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c54c66f19b9766a56fa0057d060de8378676cb64987533fa088861858fc5a69" +checksum = "b69d029da5428fc1c57f7d49101b2077c61f049d4112cd5fb8456567cc7d2638" dependencies = [ "serde", ] [[package]] name = "solana-shred-version" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afd3db0461089d1ad1a78d9ba3f15b563899ca2386351d38428faa5350c60a98" +checksum = "94953e22ca28fe4541a3447d6baeaf519cc4ddc063253bfa673b721f34c136bb" dependencies = [ "solana-hard-forks", "solana-hash", @@ -6662,9 +5936,9 @@ dependencies = [ [[package]] name = "solana-signature" -version = "2.3.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64c8ec8e657aecfc187522fc67495142c12f35e55ddeca8698edbb738b8dbd8c" +checksum = "4bb8057cc0e9f7b5e89883d49de6f407df655bb6f3a71d0b7baf9986a2218fd9" dependencies = [ "ed25519-dalek", "five8", @@ -6677,9 +5951,9 @@ dependencies = [ [[package]] name = "solana-signer" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c41991508a4b02f021c1342ba00bcfa098630b213726ceadc7cb032e051975b" +checksum = "5bfea97951fee8bae0d6038f39a5efcb6230ecdfe33425ac75196d1a1e3e3235" dependencies = [ "solana-pubkey", "solana-signature", @@ -6709,9 +5983,9 @@ dependencies = [ [[package]] name = "solana-slot-hashes" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c8691982114513763e88d04094c9caa0376b867a29577939011331134c301ce" +checksum = "80a293f952293281443c04f4d96afd9d547721923d596e92b4377ed2360f1746" dependencies = [ "serde", "serde_derive", @@ -6722,9 +5996,9 @@ dependencies = [ [[package]] name = "solana-slot-history" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ccc1b2067ca22754d5283afb2b0126d61eae734fc616d23871b0943b0d935e" +checksum = "f914f6b108f5bba14a280b458d023e3621c9973f27f015a4d755b50e88d89e97" dependencies = [ "bv", "serde", @@ -6735,9 +6009,9 @@ dependencies = [ [[package]] name = "solana-stable-layout" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f14f7d02af8f2bc1b5efeeae71bc1c2b7f0f65cd75bcc7d8180f2c762a57f54" +checksum = "1da74507795b6e8fb60b7c7306c0c36e2c315805d16eaaf479452661234685ac" dependencies = [ "solana-instruction", "solana-pubkey", @@ -6745,31 +6019,30 @@ dependencies = [ [[package]] name = "solana-stake-interface" -version = "1.2.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5269e89fde216b4d7e1d1739cf5303f8398a1ff372a81232abbee80e554a838c" +checksum = "f6f912ae679b683365348dea482dbd9468d22ff258b554fd36e3d3683c2122e3" dependencies = [ - "borsh 0.10.4", - "borsh 1.5.7", "num-traits", "serde", "serde_derive", "solana-clock", "solana-cpi", - "solana-decode-error", "solana-instruction", "solana-program-error", "solana-pubkey", "solana-system-interface", + "solana-sysvar", "solana-sysvar-id", ] [[package]] name = "solana-streamer" -version = "2.3.7" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f55673d787ef1478fa2939801e8bde7cb4ed38a99ff3d5541c2d159a06904f3" +checksum = "79c50b3b9e5f230f18ba729a266ec0e872926e317c1a8da0cfbc030c6f5a204c" dependencies = [ + "arc-swap", "async-channel", "bytes", "crossbeam-channel", @@ -6779,10 +6052,11 @@ dependencies = [ "governor", "histogram", "indexmap 2.10.0", - "itertools 0.12.1", + "itertools", "libc", "log", "nix", + "num_cpus", "pem", "percentage", "quinn", @@ -6790,7 +6064,7 @@ dependencies = [ "rand 0.8.5", "rustls 0.23.31", "smallvec", - "socket2 0.5.10", + "socket2 0.6.0", "solana-keypair", "solana-measure", "solana-metrics", @@ -6813,46 +6087,30 @@ dependencies = [ [[package]] name = "solana-svm-feature-set" -version = "2.3.7" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e65361fa1fb2a123319df6d9694c1c5ca20e555cda18eb1f953babf32e4cddd4" +checksum = "c67a4a533a53811f1e31829374d5ab0761e6b4180c7145d69b5c62ab4a9a24af" [[package]] name = "solana-system-interface" -version = "1.0.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94d7c18cb1a91c6be5f5a8ac9276a1d7c737e39a21beba9ea710ab4b9c63bc90" +checksum = "4e1790547bfc3061f1ee68ea9d8dc6c973c02a163697b24263a8e9f2e6d4afa2" dependencies = [ - "js-sys", "num-traits", "serde", "serde_derive", - "solana-decode-error", "solana-instruction", + "solana-msg", + "solana-program-error", "solana-pubkey", - "wasm-bindgen", -] - -[[package]] -name = "solana-system-transaction" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bd98a25e5bcba8b6be8bcbb7b84b24c2a6a8178d7fb0e3077a916855ceba91a" -dependencies = [ - "solana-hash", - "solana-keypair", - "solana-message", - "solana-pubkey", - "solana-signer", - "solana-system-interface", - "solana-transaction", ] [[package]] name = "solana-sysvar" -version = "2.3.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8c3595f95069f3d90f275bb9bd235a1973c4d059028b0a7f81baca2703815db" +checksum = "63205e68d680bcc315337dec311b616ab32fea0a612db3b883ce4de02e0953f9" dependencies = [ "base64 0.22.1", "bincode", @@ -6863,78 +6121,46 @@ dependencies = [ "serde_derive", "solana-account-info", "solana-clock", - "solana-define-syscall", + "solana-define-syscall 3.0.0", "solana-epoch-rewards", "solana-epoch-schedule", "solana-fee-calculator", "solana-hash", "solana-instruction", - "solana-instructions-sysvar", "solana-last-restart-slot", "solana-program-entrypoint", "solana-program-error", "solana-program-memory", "solana-pubkey", "solana-rent", - "solana-sanitize", "solana-sdk-ids", "solana-sdk-macro", "solana-slot-hashes", "solana-slot-history", - "solana-stake-interface", "solana-sysvar-id", ] [[package]] name = "solana-sysvar-id" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5762b273d3325b047cfda250787f8d796d781746860d5d0a746ee29f3e8812c1" +checksum = "5051bc1a16d5d96a96bc33b5b2ec707495c48fe978097bdaba68d3c47987eb32" dependencies = [ "solana-pubkey", "solana-sdk-ids", ] -[[package]] -name = "solana-thin-client" -version = "2.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25571fe8261c632206373ccbf35edf12a476405264a0d0829adf65202c0e1c17" -dependencies = [ - "bincode", - "log", - "rayon", - "solana-account", - "solana-client-traits", - "solana-clock", - "solana-commitment-config", - "solana-connection-cache", - "solana-epoch-info", - "solana-hash", - "solana-instruction", - "solana-keypair", - "solana-message", - "solana-pubkey", - "solana-rpc-client", - "solana-rpc-client-api", - "solana-signature", - "solana-signer", - "solana-system-interface", - "solana-transaction", - "solana-transaction-error", -] - [[package]] name = "solana-time-utils" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6af261afb0e8c39252a04d026e3ea9c405342b08c871a2ad8aa5448e068c784c" +checksum = "0ced92c60aa76ec4780a9d93f3bd64dfa916e1b998eacc6f1c110f3f444f02c9" [[package]] name = "solana-tls-utils" -version = "2.3.7" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbab408af08c4b0dc103b608f053e8bf7aec9f18a20da79fb98ccf35950ee468" +checksum = "4b3cf5ccc8e890e2f22ca194402b8e2039c884605abe1c3a71ec85ccb8fecdec" dependencies = [ "rustls 0.23.31", "solana-keypair", @@ -6945,9 +6171,9 @@ dependencies = [ [[package]] name = "solana-tpu-client" -version = "2.3.7" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc8ccdb1b26950de965860e02285361c48563d3b5eef64166fe45b5b9245e1b" +checksum = "ca9ea8a8ad7be6c899cfcf4890379c8041e734e632f31175b9331f0964defb17" dependencies = [ "async-trait", "bincode", @@ -6979,36 +6205,31 @@ dependencies = [ [[package]] name = "solana-transaction" -version = "2.2.3" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80657d6088f721148f5d889c828ca60c7daeedac9a8679f9ec215e0c42bcbf41" +checksum = "64928e6af3058dcddd6da6680cbe08324b4e071ad73115738235bbaa9e9f72a5" dependencies = [ "bincode", "serde", "serde_derive", - "solana-bincode", - "solana-feature-set", + "solana-address", "solana-hash", "solana-instruction", - "solana-keypair", + "solana-instruction-error", "solana-message", - "solana-precompiles", - "solana-pubkey", "solana-sanitize", "solana-sdk-ids", "solana-short-vec", "solana-signature", "solana-signer", - "solana-system-interface", "solana-transaction-error", - "wasm-bindgen", ] [[package]] name = "solana-transaction-context" -version = "2.3.7" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aefd75e49dd990f7fdbe562a539a7b046a839aadf43843845d766a2a6a2adfef" +checksum = "a81e203a134fb6de363aa5c8b5faf7e7b27719b9fb5711c7e91a28bdffbe58ed" dependencies = [ "bincode", "serde", @@ -7018,26 +6239,27 @@ dependencies = [ "solana-instructions-sysvar", "solana-pubkey", "solana-rent", + "solana-sbpf", "solana-sdk-ids", ] [[package]] name = "solana-transaction-error" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222a9dc8fdb61c6088baab34fc3a8b8473a03a7a5fd404ed8dd502fa79b67cb1" +checksum = "4222065402340d7e6aec9dc3e54d22992ddcf923d91edcd815443c2bfca3144a" dependencies = [ "serde", "serde_derive", - "solana-instruction", + "solana-instruction-error", "solana-sanitize", ] [[package]] name = "solana-transaction-metrics-tracker" -version = "2.3.7" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5ffbcb223e76a4e8389f32d447f9d5d68ce0947ba0a3b7db83141085d68c8f3" +checksum = "729db9e09657aec3922fb09fa7549912f7cb4de5845317ebb738caa4560369cd" dependencies = [ "base64 0.22.1", "bincode", @@ -7051,15 +6273,15 @@ dependencies = [ [[package]] name = "solana-transaction-status" -version = "2.3.7" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "287a86e28777cdc8c0745ff5700a2c3741a2a7a72a347a93815e832adfe39dc5" +checksum = "22425e57cda6b78da1644230d4625bfb2a32c4fb12f011436fa3be441752d502" dependencies = [ "Inflector", "agave-reserved-account-keys", "base64 0.22.1", "bincode", - "borsh 1.5.7", + "borsh", "bs58", "log", "serde", @@ -7084,20 +6306,20 @@ dependencies = [ "solana-transaction-error", "solana-transaction-status-client-types", "solana-vote-interface", - "spl-associated-token-account 7.0.0", - "spl-memo", - "spl-token 8.0.0", - "spl-token-2022 8.0.1", - "spl-token-group-interface 0.6.0", - "spl-token-metadata-interface 0.7.0", + "spl-associated-token-account-interface", + "spl-memo-interface", + "spl-token-2022-interface", + "spl-token-group-interface", + "spl-token-interface", + "spl-token-metadata-interface", "thiserror 2.0.17", ] [[package]] name = "solana-transaction-status-client-types" -version = "2.3.7" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e91068d54435121280c4a2f1c280d8d18381e3ccf54057c4530f40f26c2be1c" +checksum = "ba6ccc4c0bad50ebd910936e113b4fb9872f33cb17c896c5b02c005f91caa131" dependencies = [ "base64 0.22.1", "bincode", @@ -7107,7 +6329,9 @@ dependencies = [ "serde_json", "solana-account-decoder-client-types", "solana-commitment-config", + "solana-instruction", "solana-message", + "solana-pubkey", "solana-reward-info", "solana-signature", "solana-transaction", @@ -7118,9 +6342,9 @@ dependencies = [ [[package]] name = "solana-udp-client" -version = "2.3.7" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e42f000524bb38b5af2e0fba649bc3d10b0e8e0dd833dc11389a91e955cb6c54" +checksum = "3acc1f343c1ebe61ca501ba6f3f413056f5a8ceddd5a6b6d729e5d421ba0976a" dependencies = [ "async-trait", "solana-connection-cache", @@ -7132,17 +6356,11 @@ dependencies = [ "tokio", ] -[[package]] -name = "solana-validator-exit" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bbf6d7a3c0b28dd5335c52c0e9eae49d0ae489a8f324917faf0ded65a812c1d" - [[package]] name = "solana-version" -version = "2.3.7" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4607a9de98043bcf7db9e5d90b31fefb728c80eec901595b6931d7cdc1558b2" +checksum = "3918648ecc0e8446c20a02aab2253b2e91ce8baf0af16f141292e6732778d4f1" dependencies = [ "agave-feature-set", "rand 0.8.5", @@ -7155,19 +6373,21 @@ dependencies = [ [[package]] name = "solana-vote-interface" -version = "2.2.6" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b80d57478d6599d30acc31cc5ae7f93ec2361a06aefe8ea79bc81739a08af4c3" +checksum = "66631ddbe889dab5ec663294648cd1df395ec9df7a4476e7b3e095604cfdb539" dependencies = [ "bincode", + "cfg_eval", "num-derive", "num-traits", "serde", "serde_derive", + "serde_with", "solana-clock", - "solana-decode-error", "solana-hash", "solana-instruction", + "solana-instruction-error", "solana-pubkey", "solana-rent", "solana-sdk-ids", @@ -7179,17 +6399,18 @@ dependencies = [ [[package]] name = "solana-zk-sdk" -version = "2.3.7" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bb171c0f76c420a7cb6aabbe5fa85a1a009d5bb4009189c43e1a03aff9446d7" +checksum = "9602bcb1f7af15caef92b91132ec2347e1c51a72ecdbefdaefa3eac4b8711475" dependencies = [ "aes-gcm-siv", "base64 0.22.1", "bincode", "bytemuck", "bytemuck_derive", - "curve25519-dalek 4.1.3", - "itertools 0.12.1", + "curve25519-dalek", + "getrandom 0.2.16", + "itertools", "js-sys", "merlin", "num-derive", @@ -7233,52 +6454,21 @@ dependencies = [ ] [[package]] -name = "spl-associated-token-account" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76fee7d65013667032d499adc3c895e286197a35a0d3a4643c80e7fd3e9969e3" -dependencies = [ - "borsh 1.5.7", - "num-derive", - "num-traits", - "solana-program", - "spl-associated-token-account-client", - "spl-token 7.0.0", - "spl-token-2022 6.0.0", - "thiserror 1.0.69", -] - -[[package]] -name = "spl-associated-token-account" -version = "7.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae179d4a26b3c7a20c839898e6aed84cb4477adf108a366c95532f058aea041b" -dependencies = [ - "borsh 1.5.7", - "num-derive", - "num-traits", - "solana-program", - "spl-associated-token-account-client", - "spl-token 8.0.0", - "spl-token-2022 8.0.1", - "thiserror 2.0.17", -] - -[[package]] -name = "spl-associated-token-account-client" +name = "spl-associated-token-account-interface" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f8349dbcbe575f354f9a533a21f272f3eb3808a49e2fdc1c34393b88ba76cb" +checksum = "e6433917b60441d68d99a17e121d9db0ea15a9a69c0e5afa34649cf5ba12612f" dependencies = [ + "borsh", "solana-instruction", "solana-pubkey", ] [[package]] name = "spl-discriminator" -version = "0.4.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7398da23554a31660f17718164e31d31900956054f54f52d5ec1be51cb4f4b3" +checksum = "d48cc11459e265d5b501534144266620289720b4c44522a47bc6b63cd295d2f3" dependencies = [ "bytemuck", "solana-program-error", @@ -7286,282 +6476,122 @@ dependencies = [ "spl-discriminator-derive", ] -[[package]] -name = "spl-discriminator-derive" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9e8418ea6269dcfb01c712f0444d2c75542c04448b480e87de59d2865edc750" -dependencies = [ - "quote", - "spl-discriminator-syn", - "syn 2.0.104", -] - -[[package]] -name = "spl-discriminator-syn" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d1dbc82ab91422345b6df40a79e2b78c7bce1ebb366da323572dd60b7076b67" -dependencies = [ - "proc-macro2", - "quote", - "sha2 0.10.9", - "syn 2.0.104", - "thiserror 1.0.69", -] - -[[package]] -name = "spl-elgamal-registry" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce0f668975d2b0536e8a8fd60e56a05c467f06021dae037f1d0cfed0de2e231d" -dependencies = [ - "bytemuck", - "solana-program", - "solana-zk-sdk", - "spl-pod", - "spl-token-confidential-transfer-proof-extraction 0.2.1", -] - -[[package]] -name = "spl-elgamal-registry" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65edfeed09cd4231e595616aa96022214f9c9d2be02dea62c2b30d5695a6833a" -dependencies = [ - "bytemuck", - "solana-account-info", - "solana-cpi", - "solana-instruction", - "solana-msg", - "solana-program-entrypoint", - "solana-program-error", - "solana-pubkey", - "solana-rent", - "solana-sdk-ids", - "solana-system-interface", - "solana-sysvar", - "solana-zk-sdk", - "spl-pod", - "spl-token-confidential-transfer-proof-extraction 0.3.0", -] - -[[package]] -name = "spl-generic-token" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "741a62a566d97c58d33f9ed32337ceedd4e35109a686e31b1866c5dfa56abddc" -dependencies = [ - "bytemuck", - "solana-pubkey", -] - -[[package]] -name = "spl-memo" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f09647c0974e33366efeb83b8e2daebb329f0420149e74d3a4bd2c08cf9f7cb" -dependencies = [ - "solana-account-info", - "solana-instruction", - "solana-msg", - "solana-program-entrypoint", - "solana-program-error", - "solana-pubkey", -] - -[[package]] -name = "spl-pod" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d994afaf86b779104b4a95ba9ca75b8ced3fdb17ee934e38cb69e72afbe17799" -dependencies = [ - "borsh 1.5.7", - "bytemuck", - "bytemuck_derive", - "num-derive", - "num-traits", - "solana-decode-error", - "solana-msg", - "solana-program-error", - "solana-program-option", - "solana-pubkey", - "solana-zk-sdk", - "thiserror 2.0.17", -] - -[[package]] -name = "spl-program-error" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d39b5186f42b2b50168029d81e58e800b690877ef0b30580d107659250da1d1" -dependencies = [ - "num-derive", - "num-traits", - "solana-program", - "spl-program-error-derive 0.4.1", - "thiserror 1.0.69", -] - -[[package]] -name = "spl-program-error" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdebc8b42553070b75aa5106f071fef2eb798c64a7ec63375da4b1f058688c6" -dependencies = [ - "num-derive", - "num-traits", - "solana-decode-error", - "solana-msg", - "solana-program-error", - "spl-program-error-derive 0.5.0", - "thiserror 2.0.17", -] - -[[package]] -name = "spl-program-error-derive" -version = "0.4.1" +[[package]] +name = "spl-discriminator-derive" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d375dd76c517836353e093c2dbb490938ff72821ab568b545fd30ab3256b3e" +checksum = "d9e8418ea6269dcfb01c712f0444d2c75542c04448b480e87de59d2865edc750" dependencies = [ - "proc-macro2", "quote", - "sha2 0.10.9", + "spl-discriminator-syn", "syn 2.0.104", ] [[package]] -name = "spl-program-error-derive" -version = "0.5.0" +name = "spl-discriminator-syn" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2539e259c66910d78593475540e8072f0b10f0f61d7607bbf7593899ed52d0" +checksum = "5d1dbc82ab91422345b6df40a79e2b78c7bce1ebb366da323572dd60b7076b67" dependencies = [ "proc-macro2", "quote", - "sha2 0.10.9", + "sha2", "syn 2.0.104", + "thiserror 1.0.69", ] [[package]] -name = "spl-tlv-account-resolution" -version = "0.9.0" +name = "spl-generic-token" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd99ff1e9ed2ab86e3fd582850d47a739fec1be9f4661cba1782d3a0f26805f3" +checksum = "233df81b75ab99b42f002b5cdd6e65a7505ffa930624f7096a7580a56765e9cf" dependencies = [ "bytemuck", - "num-derive", - "num-traits", - "solana-account-info", - "solana-decode-error", - "solana-instruction", - "solana-msg", - "solana-program-error", "solana-pubkey", - "spl-discriminator", - "spl-pod", - "spl-program-error 0.6.0", - "spl-type-length-value 0.7.0", - "thiserror 1.0.69", ] [[package]] -name = "spl-tlv-account-resolution" -version = "0.10.0" +name = "spl-memo-interface" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1408e961215688715d5a1063cbdcf982de225c45f99c82b4f7d7e1dd22b998d7" +checksum = "3d4e2aedd58f858337fa609af5ad7100d4a243fdaf6a40d6eb4c28c5f19505d3" dependencies = [ - "bytemuck", - "num-derive", - "num-traits", - "solana-account-info", - "solana-decode-error", "solana-instruction", - "solana-msg", - "solana-program-error", "solana-pubkey", - "spl-discriminator", - "spl-pod", - "spl-program-error 0.7.0", - "spl-type-length-value 0.8.0", - "thiserror 2.0.17", ] [[package]] -name = "spl-token" -version = "7.0.0" +name = "spl-pod" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed320a6c934128d4f7e54fe00e16b8aeaecf215799d060ae14f93378da6dc834" +checksum = "b1233fdecd7461611d69bb87bc2e95af742df47291975d21232a0be8217da9de" dependencies = [ - "arrayref", + "borsh", "bytemuck", + "bytemuck_derive", "num-derive", "num-traits", "num_enum", - "solana-program", - "thiserror 1.0.69", + "solana-program-error", + "solana-program-option", + "solana-pubkey", + "solana-zk-sdk", + "thiserror 2.0.17", ] [[package]] -name = "spl-token" -version = "8.0.0" +name = "spl-program-error" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053067c6a82c705004f91dae058b11b4780407e9ccd6799dc9e7d0fab5f242da" +checksum = "9c4f6cf26cb6768110bf024bc7224326c720d711f7ad25d16f40f6cee40edb2d" dependencies = [ - "arrayref", - "bytemuck", "num-derive", "num-traits", "num_enum", - "solana-account-info", - "solana-cpi", - "solana-decode-error", - "solana-instruction", "solana-msg", - "solana-program-entrypoint", "solana-program-error", - "solana-program-memory", - "solana-program-option", - "solana-program-pack", - "solana-pubkey", - "solana-rent", - "solana-sdk-ids", - "solana-sysvar", + "spl-program-error-derive", "thiserror 2.0.17", ] [[package]] -name = "spl-token-2022" -version = "6.0.0" +name = "spl-program-error-derive" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b27f7405010ef816587c944536b0eafbcc35206ab6ba0f2ca79f1d28e488f4f" +checksum = "9ec8965aa4dc6c74701cbb48b9cad5af35b9a394514934949edbb357b78f840d" +dependencies = [ + "proc-macro2", + "quote", + "sha2", + "syn 2.0.104", +] + +[[package]] +name = "spl-tlv-account-resolution" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6927f613c9d7ce20835d3cefb602137cab2518e383a047c0eaa58054a60644c8" dependencies = [ - "arrayref", "bytemuck", "num-derive", "num-traits", "num_enum", - "solana-program", - "solana-security-txt", - "solana-zk-sdk", - "spl-elgamal-registry 0.1.1", - "spl-memo", + "solana-account-info", + "solana-instruction", + "solana-program-error", + "solana-pubkey", + "spl-discriminator", "spl-pod", - "spl-token 7.0.0", - "spl-token-confidential-transfer-ciphertext-arithmetic 0.2.1", - "spl-token-confidential-transfer-proof-extraction 0.2.1", - "spl-token-confidential-transfer-proof-generation 0.2.0", - "spl-token-group-interface 0.5.0", - "spl-token-metadata-interface 0.6.0", - "spl-transfer-hook-interface 0.9.0", - "spl-type-length-value 0.7.0", - "thiserror 1.0.69", + "spl-program-error", + "spl-type-length-value", + "thiserror 2.0.17", ] [[package]] -name = "spl-token-2022" -version = "8.0.1" +name = "spl-token-2022-interface" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f0dfbb079eebaee55e793e92ca5f433744f4b71ee04880bfd6beefba5973e5" +checksum = "0888304af6b3d839e435712e6c84025e09513017425ff62045b6b8c41feb77d9" dependencies = [ "arrayref", "bytemuck", @@ -7569,81 +6599,27 @@ dependencies = [ "num-traits", "num_enum", "solana-account-info", - "solana-clock", - "solana-cpi", - "solana-decode-error", "solana-instruction", - "solana-msg", - "solana-native-token", - "solana-program-entrypoint", "solana-program-error", - "solana-program-memory", "solana-program-option", "solana-program-pack", "solana-pubkey", - "solana-rent", "solana-sdk-ids", - "solana-security-txt", - "solana-system-interface", - "solana-sysvar", - "solana-zk-sdk", - "spl-elgamal-registry 0.2.0", - "spl-memo", - "spl-pod", - "spl-token 8.0.0", - "spl-token-confidential-transfer-ciphertext-arithmetic 0.3.1", - "spl-token-confidential-transfer-proof-extraction 0.3.0", - "spl-token-confidential-transfer-proof-generation 0.4.1", - "spl-token-group-interface 0.6.0", - "spl-token-metadata-interface 0.7.0", - "spl-transfer-hook-interface 0.10.0", - "spl-type-length-value 0.8.0", - "thiserror 2.0.17", -] - -[[package]] -name = "spl-token-confidential-transfer-ciphertext-arithmetic" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "170378693c5516090f6d37ae9bad2b9b6125069be68d9acd4865bbe9fc8499fd" -dependencies = [ - "base64 0.22.1", - "bytemuck", - "solana-curve25519", - "solana-zk-sdk", -] - -[[package]] -name = "spl-token-confidential-transfer-ciphertext-arithmetic" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cddd52bfc0f1c677b41493dafa3f2dbbb4b47cf0990f08905429e19dc8289b35" -dependencies = [ - "base64 0.22.1", - "bytemuck", - "solana-curve25519", - "solana-zk-sdk", -] - -[[package]] -name = "spl-token-confidential-transfer-proof-extraction" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff2d6a445a147c9d6dd77b8301b1e116c8299601794b558eafa409b342faf96" -dependencies = [ - "bytemuck", - "solana-curve25519", - "solana-program", "solana-zk-sdk", "spl-pod", + "spl-token-confidential-transfer-proof-extraction", + "spl-token-confidential-transfer-proof-generation", + "spl-token-group-interface", + "spl-token-metadata-interface", + "spl-type-length-value", "thiserror 2.0.17", ] [[package]] name = "spl-token-confidential-transfer-proof-extraction" -version = "0.3.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe2629860ff04c17bafa9ba4bed8850a404ecac81074113e1f840dbd0ebb7bd6" +checksum = "7a22217af69b7a61ca813f47c018afb0b00b02a74a4c70ff099cd4287740bc3d" dependencies = [ "bytemuck", "solana-account-info", @@ -7661,136 +6637,77 @@ dependencies = [ [[package]] name = "spl-token-confidential-transfer-proof-generation" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8627184782eec1894de8ea26129c61303f1f0adeed65c20e0b10bc584f09356d" -dependencies = [ - "curve25519-dalek 4.1.3", - "solana-zk-sdk", - "thiserror 1.0.69", -] - -[[package]] -name = "spl-token-confidential-transfer-proof-generation" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa27b9174bea869a7ebf31e0be6890bce90b1a4288bc2bbf24bd413f80ae3fde" +checksum = "f63a2b41095945dc15274b924b21ccae9b3ec9dc2fdd43dbc08de8c33bbcd915" dependencies = [ - "curve25519-dalek 4.1.3", + "curve25519-dalek", "solana-zk-sdk", "thiserror 2.0.17", ] [[package]] name = "spl-token-group-interface" -version = "0.5.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d595667ed72dbfed8c251708f406d7c2814a3fa6879893b323d56a10bedfc799" +checksum = "452d0f758af20caaa10d9a6f7608232e000d4c74462f248540b3d2ddfa419776" dependencies = [ "bytemuck", "num-derive", "num-traits", - "solana-decode-error", + "num_enum", "solana-instruction", - "solana-msg", "solana-program-error", "solana-pubkey", "spl-discriminator", "spl-pod", - "thiserror 1.0.69", + "thiserror 2.0.17", ] [[package]] -name = "spl-token-group-interface" -version = "0.6.0" +name = "spl-token-interface" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5597b4cd76f85ce7cd206045b7dc22da8c25516573d42d267c8d1fd128db5129" +checksum = "8c564ac05a7c8d8b12e988a37d82695b5ba4db376d07ea98bc4882c81f96c7f3" dependencies = [ + "arrayref", "bytemuck", "num-derive", "num-traits", - "solana-decode-error", + "num_enum", "solana-instruction", - "solana-msg", "solana-program-error", + "solana-program-option", + "solana-program-pack", "solana-pubkey", - "spl-discriminator", - "spl-pod", + "solana-sdk-ids", "thiserror 2.0.17", ] [[package]] name = "spl-token-metadata-interface" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfb9c89dbc877abd735f05547dcf9e6e12c00c11d6d74d8817506cab4c99fdbb" -dependencies = [ - "borsh 1.5.7", - "num-derive", - "num-traits", - "solana-borsh", - "solana-decode-error", - "solana-instruction", - "solana-msg", - "solana-program-error", - "solana-pubkey", - "spl-discriminator", - "spl-pod", - "spl-type-length-value 0.7.0", - "thiserror 1.0.69", -] - -[[package]] -name = "spl-token-metadata-interface" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "304d6e06f0de0c13a621464b1fd5d4b1bebf60d15ca71a44d3839958e0da16ee" +checksum = "9c467c7c3bd056f8fe60119e7ec34ddd6f23052c2fa8f1f51999098063b72676" dependencies = [ - "borsh 1.5.7", + "borsh", "num-derive", "num-traits", "solana-borsh", - "solana-decode-error", "solana-instruction", - "solana-msg", "solana-program-error", "solana-pubkey", "spl-discriminator", "spl-pod", - "spl-type-length-value 0.8.0", + "spl-type-length-value", "thiserror 2.0.17", ] [[package]] name = "spl-transfer-hook-interface" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4aa7503d52107c33c88e845e1351565050362c2314036ddf19a36cd25137c043" -dependencies = [ - "arrayref", - "bytemuck", - "num-derive", - "num-traits", - "solana-account-info", - "solana-cpi", - "solana-decode-error", - "solana-instruction", - "solana-msg", - "solana-program-error", - "solana-pubkey", - "spl-discriminator", - "spl-pod", - "spl-program-error 0.6.0", - "spl-tlv-account-resolution 0.9.0", - "spl-type-length-value 0.7.0", - "thiserror 1.0.69", -] - -[[package]] -name = "spl-transfer-hook-interface" -version = "0.10.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7e905b849b6aba63bde8c4badac944ebb6c8e6e14817029cbe1bc16829133bd" +checksum = "bdfff2798bcdc48ba52be12a47a28cf2d003f6f9846484ad7e6fec68d2578c25" dependencies = [ "arrayref", "bytemuck", @@ -7798,48 +6715,31 @@ dependencies = [ "num-traits", "solana-account-info", "solana-cpi", - "solana-decode-error", "solana-instruction", "solana-msg", "solana-program-error", "solana-pubkey", + "solana-sdk-ids", + "solana-system-interface", "spl-discriminator", "spl-pod", - "spl-program-error 0.7.0", - "spl-tlv-account-resolution 0.10.0", - "spl-type-length-value 0.8.0", + "spl-program-error", + "spl-tlv-account-resolution", + "spl-type-length-value", "thiserror 2.0.17", ] [[package]] name = "spl-type-length-value" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba70ef09b13af616a4c987797870122863cba03acc4284f226a4473b043923f9" -dependencies = [ - "bytemuck", - "num-derive", - "num-traits", - "solana-account-info", - "solana-decode-error", - "solana-msg", - "solana-program-error", - "spl-discriminator", - "spl-pod", - "thiserror 1.0.69", -] - -[[package]] -name = "spl-type-length-value" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d417eb548214fa822d93f84444024b4e57c13ed6719d4dcc68eec24fb481e9f5" +checksum = "ca20a1a19f4507a98ca4b28ff5ed54cac9b9d34ed27863e2bde50a3238f9a6ac" dependencies = [ "bytemuck", "num-derive", "num-traits", + "num_enum", "solana-account-info", - "solana-decode-error", "solana-msg", "solana-program-error", "spl-discriminator", @@ -7965,15 +6865,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - [[package]] name = "termtree" version = "0.5.1" @@ -7994,7 +6885,7 @@ dependencies = [ "dotenv", "futures", "hex", - "hmac 0.12.1", + "hmac", "jsonrpsee", "kora-lib", "once_cell", @@ -8003,18 +6894,19 @@ dependencies = [ "rust_decimal_macros", "serde", "serde_json", - "sha2 0.10.9", + "sha2", "solana-address-lookup-table-interface", "solana-client", "solana-commitment-config", "solana-compute-budget-interface", "solana-message", + "solana-program-pack", "solana-sdk", "solana-system-interface", - "spl-associated-token-account 6.0.0", - "spl-token 7.0.0", - "spl-token-2022 8.0.1", - "spl-transfer-hook-interface 0.10.0", + "spl-associated-token-account-interface", + "spl-token-2022-interface", + "spl-token-interface", + "spl-transfer-hook-interface", "tokio", "toml 0.8.23", ] @@ -8520,6 +7412,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unit-prefix" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "323402cff2dd658f39ca17c789b502021b3f18707c91cdf22e3838e1b4023817" + [[package]] name = "universal-hash" version = "0.5.1" @@ -8530,6 +7428,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "unreachable" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" +dependencies = [ + "void", +] + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -8655,6 +7562,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + [[package]] name = "walkdir" version = "2.5.0" @@ -8674,12 +7587,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -8981,6 +7888,15 @@ dependencies = [ "windows-targets 0.53.3", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-targets" version = "0.42.2" diff --git a/Cargo.toml b/Cargo.toml index 81df9109..fae13edf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,15 +35,17 @@ jsonrpsee = { version = "0.16.2", features = [ "http-client", "client", ] } -solana-sdk = "2.1.9" -solana-commitment-config = "2.1.9" -solana-message = "2.1.9" -solana-system-interface = "1.0.0" -solana-transaction-status-client-types = "2.1.9" -solana-transaction-status = "2.1.9" -solana-address-lookup-table-interface = "2.2.2" -solana-program = "2.1.9" -solana-client = "2.1.9" +solana-sdk = "3.0.0" +solana-commitment-config = "3.0.0" +solana-message = "3.0.1" +solana-system-interface = "2.0.0" +solana-transaction-status-client-types = "3.0.8" +solana-transaction-status = "3.0.8" +solana-address-lookup-table-interface = "3.0.0" +solana-program = "3.0.0" +solana-program-pack = "3.0.0" +solana-compute-budget-interface = "3.0.0" +solana-client = "3.0.8" bs58 = "0.5.1" bincode = "1.3.3" borsh = "1.5.3" @@ -67,11 +69,9 @@ futures-util = "0.3.31" hyper = "1.5.1" http = "0.2" toml = "0.8.19" -spl-token = { version = "7.0.0", features = ["no-entrypoint"] } -spl-token-2022 = { version = "8.0.0", features = ["no-entrypoint"] } -spl-associated-token-account = { version = "6.0.0", features = [ - "no-entrypoint", -] } +spl-token-interface = { version = "2.0.0" } +spl-token-2022-interface = { version = "2.0.0" } +spl-associated-token-account-interface = { version = "2.0.0" } chrono = "0.4.39" hex = "0.4.3" p256 = "0.13.3" diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml index b5430142..cb3e4fc6 100644 --- a/crates/lib/Cargo.toml +++ b/crates/lib/Cargo.toml @@ -26,6 +26,8 @@ solana-commitment-config = { workspace = true } solana-message = { workspace = true } solana-system-interface = { workspace = true } solana-program = { workspace = true } +solana-program-pack = { workspace = true } +solana-compute-budget-interface = { workspace = true } solana-transaction-status-client-types = { workspace = true } solana-transaction-status = { workspace = true } solana-address-lookup-table-interface = { workspace = true } @@ -37,11 +39,12 @@ base64 = { workspace = true } tokio = { workspace = true } async-trait = { workspace = true } reqwest = { workspace = true } -spl-token = { workspace = true } -spl-token-2022 = { workspace = true } -spl-associated-token-account = { workspace = true } -solana-signers = { git = "https://github.com/solana-foundation/solana-signers", features = [ +spl-token-interface = { workspace = true } +spl-token-2022-interface = { workspace = true } +spl-associated-token-account-interface = { workspace = true } +solana-signers = { git = "https://github.com/solana-foundation/solana-signers", default-features = false, features = [ "all", + "sdk-v3", ] } vaultrs = { workspace = true } deadpool-redis = { workspace = true } @@ -56,7 +59,7 @@ rand = "0.9.2" utoipa = { workspace = true } dirs = "6.0.0" mockall = "0.13.1" -spl-pod = "0.5.0" +spl-pod = "0.7.1" p256 = { version = "0.13", features = ["ecdsa"] } chrono = { workspace = true } hex = { workspace = true } diff --git a/crates/lib/src/admin/token_util.rs b/crates/lib/src/admin/token_util.rs index bb5eb369..33c72b65 100644 --- a/crates/lib/src/admin/token_util.rs +++ b/crates/lib/src/admin/token_util.rs @@ -5,14 +5,13 @@ use crate::{ transaction::TransactionUtil, }; use solana_client::nonblocking::rpc_client::RpcClient; +use solana_compute_budget_interface::ComputeBudgetInstruction; use solana_message::{Message, VersionedMessage}; -use solana_sdk::{ - compute_budget::ComputeBudgetInstruction, instruction::Instruction, pubkey::Pubkey, -}; +use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; use solana_signers::SolanaSigner; -use spl_associated_token_account::{ - get_associated_token_address, instruction::create_associated_token_account, +use spl_associated_token_account_interface::{ + address::get_associated_token_address, instruction::create_associated_token_account, }; use std::{fmt::Display, str::FromStr, sync::Arc}; @@ -389,13 +388,17 @@ mod tests { let atas_to_create = vec![ ATAToCreate { mint: mint1, - ata: spl_associated_token_account::get_associated_token_address(&address, &mint1), - token_program: spl_token::id(), + ata: spl_associated_token_account_interface::address::get_associated_token_address( + &address, &mint1, + ), + token_program: spl_token_interface::id(), }, ATAToCreate { mint: mint2, - ata: spl_associated_token_account::get_associated_token_address(&address, &mint2), - token_program: spl_token::id(), + ata: spl_associated_token_account_interface::address::get_associated_token_address( + &address, &mint2, + ), + token_program: spl_token_interface::id(), }, ]; diff --git a/crates/lib/src/config.rs b/crates/lib/src/config.rs index 9c4ea503..e60e751f 100644 --- a/crates/lib/src/config.rs +++ b/crates/lib/src/config.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; use solana_sdk::pubkey::Pubkey; -use spl_token_2022::extension::ExtensionType; +use spl_token_2022_interface::extension::ExtensionType; use std::{fs, path::Path, str::FromStr}; use toml; use utoipa::ToSchema; diff --git a/crates/lib/src/fee/fee.rs b/crates/lib/src/fee/fee.rs index 8ac3112b..dfa9a36d 100644 --- a/crates/lib/src/fee/fee.rs +++ b/crates/lib/src/fee/fee.rs @@ -491,7 +491,7 @@ mod tests { config_mock::ConfigMockBuilder, rpc_mock::RpcMockBuilder, }, - token::{interface::TokenInterface, TokenProgram}, + token::{interface::TokenInterface, spl_token::TokenProgram}, transaction::TransactionUtil, }; use solana_message::{v0, Message, VersionedMessage}; @@ -509,7 +509,7 @@ mod tests { }, program::ID as SYSTEM_PROGRAM_ID, }; - use spl_associated_token_account::get_associated_token_address; + use spl_associated_token_account_interface::address::get_associated_token_address; #[test] fn test_is_fee_payer_in_signers_legacy_fee_payer_is_signer() { diff --git a/crates/lib/src/rpc_server/method/transfer_transaction.rs b/crates/lib/src/rpc_server/method/transfer_transaction.rs index a466ef6a..f001d18b 100644 --- a/crates/lib/src/rpc_server/method/transfer_transaction.rs +++ b/crates/lib/src/rpc_server/method/transfer_transaction.rs @@ -164,7 +164,7 @@ mod tests { #[tokio::test] async fn test_transfer_transaction_invalid_source() { let config = ConfigMockBuilder::new().build(); - let _ = update_config(config); + update_config(config).unwrap(); let _ = setup_or_get_test_signer(); let rpc_client = Arc::new(RpcMockBuilder::new().with_mint_account(6).build()); @@ -192,7 +192,8 @@ mod tests { #[tokio::test] async fn test_transfer_transaction_invalid_destination() { - let _m = ConfigMockBuilder::new().build_and_setup(); + let config = ConfigMockBuilder::new().build(); + update_config(config).unwrap(); let _ = setup_or_get_test_signer(); let rpc_client = Arc::new(RpcMockBuilder::new().with_mint_account(6).build()); @@ -219,7 +220,8 @@ mod tests { #[tokio::test] async fn test_transfer_transaction_invalid_token() { - let _m = ConfigMockBuilder::new().build_and_setup(); + let config = ConfigMockBuilder::new().build(); + update_config(config).unwrap(); let _ = setup_or_get_test_signer(); let rpc_client = Arc::new(RpcMockBuilder::new().with_mint_account(6).build()); diff --git a/crates/lib/src/tests/account_mock.rs b/crates/lib/src/tests/account_mock.rs index ba1c2075..6746cc49 100644 --- a/crates/lib/src/tests/account_mock.rs +++ b/crates/lib/src/tests/account_mock.rs @@ -6,8 +6,7 @@ use spl_pod::{ optional_keys::OptionalNonZeroPubkey, primitives::{PodU16, PodU64}, }; -use spl_token::state::{Account as TokenAccount, AccountState as SplAccountState, Mint}; -use spl_token_2022::{ +use spl_token_2022_interface::{ extension::{ self, transfer_fee::{TransferFee, TransferFeeConfig}, @@ -18,9 +17,11 @@ use spl_token_2022::{ Account as Token2022AccountState, AccountState as Token2022AccountState_, Mint as Mint2022, }, }; +use spl_token_interface::state::{Account as TokenAccount, AccountState as SplAccountState, Mint}; use crate::token::{ - spl_token_2022::Token2022Mint, spl_token_2022_util::ParsedExtension, Token2022Account, + spl_token_2022::{Token2022Account, Token2022Mint}, + spl_token_2022_util::ParsedExtension, }; // Common default values used across mock builders @@ -242,7 +243,7 @@ impl TokenAccountMockBuilder { Account { lamports: self.lamports, data, - owner: spl_token::id(), + owner: spl_token_interface::id(), executable: false, rent_epoch: self.rent_epoch, } @@ -267,7 +268,7 @@ impl TokenAccountMockBuilder { Account { lamports: self.lamports, data, - owner: spl_token_2022::id(), + owner: spl_token_2022_interface::id(), executable: false, rent_epoch: self.rent_epoch, } @@ -397,7 +398,7 @@ impl MintAccountMockBuilder { Account { lamports: self.lamports, data, - owner: spl_token::id(), + owner: spl_token_interface::id(), executable: false, rent_epoch: self.rent_epoch, } @@ -421,7 +422,7 @@ impl MintAccountMockBuilder { Account { lamports: self.lamports, data, - owner: spl_token_2022::id(), + owner: spl_token_2022_interface::id(), executable: false, rent_epoch: self.rent_epoch, } @@ -484,7 +485,7 @@ impl MintAccountMockBuilder { Ok(Account { lamports: self.lamports, data, - owner: spl_token_2022::id(), + owner: spl_token_2022_interface::id(), executable: false, rent_epoch: self.rent_epoch, }) @@ -588,10 +589,14 @@ pub fn create_mock_token2022_mint_with_extensions( /// Helper to create Transfer Fee Config for testing pub fn create_transfer_fee_config(basis_points: u16, max_fee: u64) -> TransferFeeConfig { TransferFeeConfig { - transfer_fee_config_authority: OptionalNonZeroPubkey::try_from(Some(Pubkey::new_unique())) - .unwrap(), - withdraw_withheld_authority: OptionalNonZeroPubkey::try_from(Some(Pubkey::new_unique())) - .unwrap(), + transfer_fee_config_authority: OptionalNonZeroPubkey::try_from(Some( + spl_pod::solana_pubkey::Pubkey::new_unique(), + )) + .unwrap(), + withdraw_withheld_authority: OptionalNonZeroPubkey::try_from(Some( + spl_pod::solana_pubkey::Pubkey::new_unique(), + )) + .unwrap(), withheld_amount: PodU64::from(0), newer_transfer_fee: TransferFee { epoch: PodU64::from(0), diff --git a/crates/lib/src/tests/transaction_mock.rs b/crates/lib/src/tests/transaction_mock.rs index 30e5861f..11243df6 100644 --- a/crates/lib/src/tests/transaction_mock.rs +++ b/crates/lib/src/tests/transaction_mock.rs @@ -3,9 +3,9 @@ use solana_sdk::{ pubkey::Pubkey, signature::Keypair, signer::Signer, - system_instruction::transfer, transaction::{Transaction, VersionedTransaction}, }; +use solana_system_interface::instruction::transfer; use crate::transaction::TransactionUtil; diff --git a/crates/lib/src/token/mod.rs b/crates/lib/src/token/mod.rs index bd5d190f..71014e3e 100644 --- a/crates/lib/src/token/mod.rs +++ b/crates/lib/src/token/mod.rs @@ -5,5 +5,3 @@ pub mod spl_token_2022_util; pub mod token; pub use interface::{TokenInterface, TokenState}; -pub use spl_token::{TokenAccount, TokenProgram}; -pub use spl_token_2022::{Token2022Account, Token2022Program}; diff --git a/crates/lib/src/token/spl_token.rs b/crates/lib/src/token/spl_token.rs index cd75014a..bd5ac237 100644 --- a/crates/lib/src/token/spl_token.rs +++ b/crates/lib/src/token/spl_token.rs @@ -4,10 +4,11 @@ use super::interface::{TokenInterface, TokenState}; use async_trait::async_trait; use solana_program::pubkey::Pubkey; use solana_sdk::{instruction::Instruction, program_pack::Pack}; -use spl_associated_token_account::{ - get_associated_token_address_with_program_id, instruction::create_associated_token_account, +use spl_associated_token_account_interface::{ + address::get_associated_token_address_with_program_id, + instruction::create_associated_token_account, }; -use spl_token::{ +use spl_token_interface::{ self, state::{Account as TokenAccountState, AccountState, Mint as MintState}, }; @@ -103,7 +104,7 @@ impl TokenProgram { #[async_trait] impl TokenInterface for TokenProgram { fn program_id(&self) -> Pubkey { - spl_token::id() + spl_token_interface::id() } fn unpack_token_account( @@ -134,7 +135,12 @@ impl TokenInterface for TokenProgram { mint: &Pubkey, owner: &Pubkey, ) -> Result> { - Ok(spl_token::instruction::initialize_account(&self.program_id(), account, mint, owner)?) + Ok(spl_token_interface::instruction::initialize_account( + &self.program_id(), + account, + mint, + owner, + )?) } fn create_transfer_instruction( @@ -144,7 +150,7 @@ impl TokenInterface for TokenProgram { authority: &Pubkey, amount: u64, ) -> Result> { - Ok(spl_token::instruction::transfer( + Ok(spl_token_interface::instruction::transfer( &self.program_id(), source, destination, @@ -163,7 +169,7 @@ impl TokenInterface for TokenProgram { amount: u64, decimals: u8, ) -> Result> { - Ok(spl_token::instruction::transfer_checked( + Ok(spl_token_interface::instruction::transfer_checked( &self.program_id(), source, mint, @@ -213,12 +219,12 @@ mod tests { use super::*; use solana_program::program_pack::Pack; use solana_sdk::pubkey::Pubkey; - use spl_token::state::{Account as SplTokenAccount, AccountState}; + use spl_token_interface::state::{Account as SplTokenAccount, AccountState}; #[test] fn test_token_program_creation_and_program_id() { let program = TokenProgram::new(); - assert_eq!(program.program_id(), spl_token::id()); + assert_eq!(program.program_id(), spl_token_interface::id()); } #[test] @@ -357,7 +363,7 @@ mod tests { assert!(result.is_ok()); let instruction = result.unwrap(); - assert_eq!(instruction.program_id, spl_token::id()); + assert_eq!(instruction.program_id, spl_token_interface::id()); assert_eq!(instruction.accounts.len(), 4); // account, mint, owner, rent sysvar } @@ -373,7 +379,7 @@ mod tests { assert!(result.is_ok()); let instruction = result.unwrap(); - assert_eq!(instruction.program_id, spl_token::id()); + assert_eq!(instruction.program_id, spl_token_interface::id()); assert_eq!(instruction.accounts.len(), 3); // source, destination, authority } @@ -398,7 +404,7 @@ mod tests { assert!(result.is_ok()); let instruction = result.unwrap(); - assert_eq!(instruction.program_id, spl_token::id()); + assert_eq!(instruction.program_id, spl_token_interface::id()); assert_eq!(instruction.accounts.len(), 4); // source, mint, destination, authority } @@ -410,7 +416,11 @@ mod tests { let ata = program.get_associated_token_address(&wallet, &mint); - let ata2 = get_associated_token_address_with_program_id(&wallet, &mint, &spl_token::id()); + let ata2 = get_associated_token_address_with_program_id( + &wallet, + &mint, + &spl_token_interface::id(), + ); assert_eq!(ata, ata2); } @@ -425,7 +435,7 @@ mod tests { let instruction = program.create_associated_token_account_instruction(&funding_account, &wallet, &mint); - assert_eq!(instruction.program_id, spl_associated_token_account::id()); + assert_eq!(instruction.program_id, spl_associated_token_account_interface::program::id()); assert_eq!(instruction.accounts.len(), 6); // funding, ata, wallet, mint, system_program, token_program } @@ -441,7 +451,7 @@ mod tests { }; let token_program = spl_mint.get_token_program(); - assert_eq!(token_program.program_id(), spl_token::id()); + assert_eq!(token_program.program_id(), spl_token_interface::id()); } #[test] diff --git a/crates/lib/src/token/spl_token_2022.rs b/crates/lib/src/token/spl_token_2022.rs index d238d1ad..c0ffbf62 100644 --- a/crates/lib/src/token/spl_token_2022.rs +++ b/crates/lib/src/token/spl_token_2022.rs @@ -10,10 +10,11 @@ use super::interface::{TokenInterface, TokenState}; use async_trait::async_trait; use solana_program::{program_pack::Pack, pubkey::Pubkey}; use solana_sdk::instruction::Instruction; -use spl_associated_token_account::{ - get_associated_token_address_with_program_id, instruction::create_associated_token_account, +use spl_associated_token_account_interface::{ + address::get_associated_token_address_with_program_id, + instruction::create_associated_token_account, }; -use spl_token_2022::{ +use spl_token_2022_interface::{ extension::{transfer_fee::TransferFeeConfig, ExtensionType, StateWithExtensions}, state::{Account as Token2022AccountState, AccountState, Mint as Token2022MintState}, }; @@ -260,7 +261,7 @@ impl Default for Token2022Program { #[async_trait] impl TokenInterface for Token2022Program { fn program_id(&self) -> Pubkey { - spl_token_2022::id() + spl_token_2022_interface::id() } fn unpack_token_account( @@ -307,7 +308,7 @@ impl TokenInterface for Token2022Program { mint: &Pubkey, owner: &Pubkey, ) -> Result> { - Ok(spl_token_2022::instruction::initialize_account3( + Ok(spl_token_2022_interface::instruction::initialize_account3( &self.program_id(), account, mint, @@ -324,7 +325,7 @@ impl TokenInterface for Token2022Program { ) -> Result> { // Get the mint from the source account data #[allow(deprecated)] - Ok(spl_token_2022::instruction::transfer( + Ok(spl_token_2022_interface::instruction::transfer( &self.program_id(), source, destination, @@ -343,7 +344,7 @@ impl TokenInterface for Token2022Program { amount: u64, decimals: u8, ) -> Result> { - Ok(spl_token_2022::instruction::transfer_checked( + Ok(spl_token_2022_interface::instruction::transfer_checked( &self.program_id(), source, mint, @@ -449,7 +450,7 @@ mod tests { optional_keys::OptionalNonZeroPubkey, primitives::{PodU16, PodU64}, }; - use spl_token_2022::extension::{ + use spl_token_2022_interface::extension::{ transfer_fee::{TransferFee, TransferFeeConfig}, ExtensionType, }; @@ -468,13 +469,13 @@ mod tests { #[test] fn test_token_program_token2022() { let program = Token2022Program::new(); - assert_eq!(program.program_id(), spl_token_2022::id()); + assert_eq!(program.program_id(), spl_token_2022_interface::id()); } #[test] fn test_token2022_program_creation() { let program = Token2022Program::new(); - assert_eq!(program.program_id(), spl_token_2022::id()); + assert_eq!(program.program_id(), spl_token_2022_interface::id()); } #[test] @@ -517,7 +518,7 @@ mod tests { let program = Token2022Program::new(); let ix = program.create_transfer_instruction(&source, &dest, &authority, amount).unwrap(); - assert_eq!(ix.program_id, spl_token_2022::id()); + assert_eq!(ix.program_id, spl_token_2022_interface::id()); // Verify accounts are in correct order assert_eq!(ix.accounts[0].pubkey, source); assert_eq!(ix.accounts[1].pubkey, dest); @@ -540,7 +541,7 @@ mod tests { ) .unwrap(); - assert_eq!(ix.program_id, spl_token_2022::id()); + assert_eq!(ix.program_id, spl_token_2022_interface::id()); // Verify accounts are in correct order assert_eq!(ix.accounts[0].pubkey, source); assert_eq!(ix.accounts[1].pubkey, mint); @@ -558,10 +559,10 @@ mod tests { // Verify ATA derivation matches spl-token-2022 let expected_ata = - spl_associated_token_account::get_associated_token_address_with_program_id( + spl_associated_token_account_interface::address::get_associated_token_address_with_program_id( &wallet, &mint, - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); assert_eq!(ata, expected_ata); } @@ -713,12 +714,12 @@ mod tests { // Create config with different fees for different epochs let transfer_fee_config = TransferFeeConfig { transfer_fee_config_authority: OptionalNonZeroPubkey::try_from(Some( - Pubkey::new_unique(), + spl_pod::solana_pubkey::Pubkey::new_unique(), )) .unwrap(), - withdraw_withheld_authority: OptionalNonZeroPubkey::try_from( - Some(Pubkey::new_unique()), - ) + withdraw_withheld_authority: OptionalNonZeroPubkey::try_from(Some( + spl_pod::solana_pubkey::Pubkey::new_unique(), + )) .unwrap(), withheld_amount: PodU64::from(0), older_transfer_fee: TransferFee { diff --git a/crates/lib/src/token/spl_token_2022_util.rs b/crates/lib/src/token/spl_token_2022_util.rs index f807b7b5..a2a85771 100644 --- a/crates/lib/src/token/spl_token_2022_util.rs +++ b/crates/lib/src/token/spl_token_2022_util.rs @@ -35,7 +35,7 @@ //! get_all_account_extension_names() -> &["memo_transfer", "cpi_guard", ...] //! ``` -use spl_token_2022::{ +use spl_token_2022_interface::{ extension::{ confidential_mint_burn::ConfidentialMintBurn, confidential_transfer::{ConfidentialTransferAccount, ConfidentialTransferMint}, @@ -229,7 +229,7 @@ pub fn get_all_account_extension_names() -> &'static [&'static str] { #[cfg(test)] mod tests { use super::*; - use spl_token_2022::extension::ExtensionType; + use spl_token_2022_interface::extension::ExtensionType; #[test] fn test_mint_extension_from_string() { diff --git a/crates/lib/src/token/token.rs b/crates/lib/src/token/token.rs index 05b21ff8..11e1b5b3 100644 --- a/crates/lib/src/token/token.rs +++ b/crates/lib/src/token/token.rs @@ -3,8 +3,9 @@ use crate::{ oracle::{get_price_oracle, PriceSource, RetryingPriceOracle, TokenPrice}, token::{ interface::TokenMint, - spl_token_2022::{Token2022Extensions, Token2022Mint}, - Token2022Account, Token2022Program, TokenInterface, TokenProgram, + spl_token::TokenProgram, + spl_token_2022::{Token2022Account, Token2022Extensions, Token2022Mint, Token2022Program}, + TokenInterface, }, transaction::{ ParsedSPLInstructionData, ParsedSPLInstructionType, VersionedTransactionResolved, @@ -17,7 +18,7 @@ use rust_decimal::{ }; use solana_client::nonblocking::rpc_client::RpcClient; use solana_sdk::{native_token::LAMPORTS_PER_SOL, pubkey::Pubkey}; -use spl_associated_token_account::get_associated_token_address_with_program_id; +use spl_associated_token_account_interface::address::get_associated_token_address_with_program_id; use std::{collections::HashMap, str::FromStr, time::Duration}; #[cfg(not(test))] @@ -36,9 +37,9 @@ impl TokenType { pub fn get_token_program_from_owner( owner: &Pubkey, ) -> Result, KoraError> { - if *owner == spl_token::id() { + if *owner == spl_token_interface::id() { Ok(Box::new(TokenProgram::new())) - } else if *owner == spl_token_2022::id() { + } else if *owner == spl_token_2022_interface::id() { Ok(Box::new(Token2022Program::new())) } else { Err(KoraError::TokenOperationError(format!("Invalid token program owner: {owner}"))) @@ -223,7 +224,7 @@ impl TokenUtil { // in case that ATA is being created in the current instruction if matches!(e, KoraError::AccountNotFound(_)) { let spl_ata = - spl_associated_token_account::get_associated_token_address( + spl_associated_token_account_interface::address::get_associated_token_address( fee_payer, mint_pubkey, ); @@ -231,7 +232,7 @@ impl TokenUtil { get_associated_token_address_with_program_id( fee_payer, mint_pubkey, - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); // If destination matches a valid ATA for fee payer, count as inflow @@ -499,16 +500,16 @@ mod tests_token { #[test] fn test_token_type_get_token_program_from_owner_spl() { - let spl_token_owner = spl_token::id(); + let spl_token_owner = spl_token_interface::id(); let result = TokenType::get_token_program_from_owner(&spl_token_owner).unwrap(); - assert_eq!(result.program_id(), spl_token::id()); + assert_eq!(result.program_id(), spl_token_interface::id()); } #[test] fn test_token_type_get_token_program_from_owner_token2022() { - let token2022_owner = spl_token_2022::id(); + let token2022_owner = spl_token_2022_interface::id(); let result = TokenType::get_token_program_from_owner(&token2022_owner).unwrap(); - assert_eq!(result.program_id(), spl_token_2022::id()); + assert_eq!(result.program_id(), spl_token_2022_interface::id()); } #[test] @@ -525,14 +526,14 @@ mod tests_token { fn test_token_type_get_token_program_spl() { let token_type = TokenType::Spl; let result = token_type.get_token_program(); - assert_eq!(result.program_id(), spl_token::id()); + assert_eq!(result.program_id(), spl_token_interface::id()); } #[test] fn test_token_type_get_token_program_token2022() { let token_type = TokenType::Token2022; let result = token_type.get_token_program(); - assert_eq!(result.program_id(), spl_token_2022::id()); + assert_eq!(result.program_id(), spl_token_2022_interface::id()); } #[test] @@ -948,7 +949,7 @@ mod tests_token { #[test] fn test_config_token2022_extension_blocking() { - use spl_token_2022::extension::ExtensionType; + use spl_token_2022_interface::extension::ExtensionType; let mut config_builder = ConfigMockBuilder::new(); config_builder = config_builder @@ -999,7 +1000,7 @@ mod tests_token { #[test] fn test_config_token2022_empty_extension_blocking() { - use spl_token_2022::extension::ExtensionType; + use spl_token_2022_interface::extension::ExtensionType; let _lock = ConfigMockBuilder::new().build_and_setup(); let config = crate::tests::config_mock::mock_state::get_config().unwrap(); diff --git a/crates/lib/src/transaction/instruction_util.rs b/crates/lib/src/transaction/instruction_util.rs index b79d98a4..0473e04f 100644 --- a/crates/lib/src/transaction/instruction_util.rs +++ b/crates/lib/src/transaction/instruction_util.rs @@ -1,11 +1,11 @@ use std::collections::HashMap; +use solana_message::compiled_instruction::CompiledInstruction; use solana_sdk::{ - instruction::{AccountMeta, CompiledInstruction, Instruction}, + instruction::{AccountMeta, Instruction}, pubkey::Pubkey, - system_instruction::SystemInstruction, - system_program::ID as SYSTEM_PROGRAM_ID, }; +use solana_system_interface::{instruction::SystemInstruction, program::ID as SYSTEM_PROGRAM_ID}; use solana_transaction_status_client_types::{UiInstruction, UiParsedInstruction}; use crate::{ @@ -384,8 +384,8 @@ impl IxUtils { // Reconstruct based on program type if parsed.program_id == SYSTEM_PROGRAM_ID.to_string() { Self::reconstruct_system_instruction(parsed, &account_keys_hashmap).ok() - } else if parsed.program == spl_token::ID.to_string() - || parsed.program == spl_token_2022::ID.to_string() + } else if parsed.program == spl_token_interface::ID.to_string() + || parsed.program == spl_token_2022_interface::ID.to_string() { Self::reconstruct_spl_token_instruction(parsed, &account_keys_hashmap).ok() } else { @@ -770,11 +770,12 @@ impl IxUtils { let destination_idx = Self::get_account_index(account_keys_hashmap, &destination)?; let authority_idx = Self::get_account_index(account_keys_hashmap, &authority)?; - let data = if parsed.program_id == spl_token::ID.to_string() { - spl_token::instruction::TokenInstruction::Transfer { amount }.pack() + let data = if parsed.program_id == spl_token_interface::ID.to_string() { + spl_token_interface::instruction::TokenInstruction::Transfer { amount }.pack() } else { #[allow(deprecated)] - spl_token_2022::instruction::TokenInstruction::Transfer { amount }.pack() + spl_token_2022_interface::instruction::TokenInstruction::Transfer { amount } + .pack() }; Ok(CompiledInstruction { @@ -801,11 +802,14 @@ impl IxUtils { let destination_idx = Self::get_account_index(account_keys_hashmap, &destination)?; let authority_idx = Self::get_account_index(account_keys_hashmap, &authority)?; - let data = if parsed.program_id == spl_token::ID.to_string() { - spl_token::instruction::TokenInstruction::TransferChecked { amount, decimals } - .pack() + let data = if parsed.program_id == spl_token_interface::ID.to_string() { + spl_token_interface::instruction::TokenInstruction::TransferChecked { + amount, + decimals, + } + .pack() } else { - spl_token_2022::instruction::TokenInstruction::TransferChecked { + spl_token_2022_interface::instruction::TokenInstruction::TransferChecked { amount, decimals, } @@ -852,20 +856,23 @@ impl IxUtils { let data = if instruction_type == PARSED_DATA_FIELD_BURN_CHECKED { let decimals = decimals.unwrap(); // Safe because we set it above for burnChecked - if parsed.program_id == spl_token::ID.to_string() { - spl_token::instruction::TokenInstruction::BurnChecked { amount, decimals } - .pack() + if parsed.program_id == spl_token_interface::ID.to_string() { + spl_token_interface::instruction::TokenInstruction::BurnChecked { + amount, + decimals, + } + .pack() } else { - spl_token_2022::instruction::TokenInstruction::BurnChecked { + spl_token_2022_interface::instruction::TokenInstruction::BurnChecked { amount, decimals, } .pack() } - } else if parsed.program_id == spl_token::ID.to_string() { - spl_token::instruction::TokenInstruction::Burn { amount }.pack() + } else if parsed.program_id == spl_token_interface::ID.to_string() { + spl_token_interface::instruction::TokenInstruction::Burn { amount }.pack() } else { - spl_token_2022::instruction::TokenInstruction::Burn { amount }.pack() + spl_token_2022_interface::instruction::TokenInstruction::Burn { amount }.pack() }; Ok(CompiledInstruction { program_id_index, accounts, data }) @@ -879,10 +886,10 @@ impl IxUtils { let destination_idx = Self::get_account_index(account_keys_hashmap, &destination)?; let authority_idx = Self::get_account_index(account_keys_hashmap, &authority)?; - let data = if parsed.program_id == spl_token::ID.to_string() { - spl_token::instruction::TokenInstruction::CloseAccount.pack() + let data = if parsed.program_id == spl_token_interface::ID.to_string() { + spl_token_interface::instruction::TokenInstruction::CloseAccount.pack() } else { - spl_token_2022::instruction::TokenInstruction::CloseAccount.pack() + spl_token_2022_interface::instruction::TokenInstruction::CloseAccount.pack() }; Ok(CompiledInstruction { @@ -901,10 +908,11 @@ impl IxUtils { let delegate_idx = Self::get_account_index(account_keys_hashmap, &delegate)?; let owner_idx = Self::get_account_index(account_keys_hashmap, &owner)?; - let data = if parsed.program_id == spl_token::ID.to_string() { - spl_token::instruction::TokenInstruction::Approve { amount }.pack() + let data = if parsed.program_id == spl_token_interface::ID.to_string() { + spl_token_interface::instruction::TokenInstruction::Approve { amount }.pack() } else { - spl_token_2022::instruction::TokenInstruction::Approve { amount }.pack() + spl_token_2022_interface::instruction::TokenInstruction::Approve { amount } + .pack() }; Ok(CompiledInstruction { @@ -931,11 +939,14 @@ impl IxUtils { let delegate_idx = Self::get_account_index(account_keys_hashmap, &delegate)?; let owner_idx = Self::get_account_index(account_keys_hashmap, &owner)?; - let data = if parsed.program_id == spl_token::ID.to_string() { - spl_token::instruction::TokenInstruction::ApproveChecked { amount, decimals } - .pack() + let data = if parsed.program_id == spl_token_interface::ID.to_string() { + spl_token_interface::instruction::TokenInstruction::ApproveChecked { + amount, + decimals, + } + .pack() } else { - spl_token_2022::instruction::TokenInstruction::ApproveChecked { + spl_token_2022_interface::instruction::TokenInstruction::ApproveChecked { amount, decimals, } @@ -955,10 +966,10 @@ impl IxUtils { let source_idx = Self::get_account_index(account_keys_hashmap, &source)?; let owner_idx = Self::get_account_index(account_keys_hashmap, &owner)?; - let data = if parsed.program_id == spl_token::ID.to_string() { - spl_token::instruction::TokenInstruction::Revoke.pack() + let data = if parsed.program_id == spl_token_interface::ID.to_string() { + spl_token_interface::instruction::TokenInstruction::Revoke.pack() } else { - spl_token_2022::instruction::TokenInstruction::Revoke.pack() + spl_token_2022_interface::instruction::TokenInstruction::Revoke.pack() }; Ok(CompiledInstruction { @@ -998,10 +1009,11 @@ impl IxUtils { let mint_authority_idx = Self::get_account_index(account_keys_hashmap, &mint_authority)?; - let data = if parsed.program_id == spl_token::ID.to_string() { - spl_token::instruction::TokenInstruction::MintTo { amount }.pack() + let data = if parsed.program_id == spl_token_interface::ID.to_string() { + spl_token_interface::instruction::TokenInstruction::MintTo { amount }.pack() } else { - spl_token_2022::instruction::TokenInstruction::MintTo { amount }.pack() + spl_token_2022_interface::instruction::TokenInstruction::MintTo { amount } + .pack() }; Ok(CompiledInstruction { @@ -1028,11 +1040,14 @@ impl IxUtils { let mint_authority_idx = Self::get_account_index(account_keys_hashmap, &mint_authority)?; - let data = if parsed.program_id == spl_token::ID.to_string() { - spl_token::instruction::TokenInstruction::MintToChecked { amount, decimals } - .pack() + let data = if parsed.program_id == spl_token_interface::ID.to_string() { + spl_token_interface::instruction::TokenInstruction::MintToChecked { + amount, + decimals, + } + .pack() } else { - spl_token_2022::instruction::TokenInstruction::MintToChecked { + spl_token_2022_interface::instruction::TokenInstruction::MintToChecked { amount, decimals, } @@ -1122,10 +1137,10 @@ impl IxUtils { let freeze_authority_idx = Self::get_account_index(account_keys_hashmap, &freeze_authority)?; - let data = if parsed.program_id == spl_token::ID.to_string() { - spl_token::instruction::TokenInstruction::FreezeAccount.pack() + let data = if parsed.program_id == spl_token_interface::ID.to_string() { + spl_token_interface::instruction::TokenInstruction::FreezeAccount.pack() } else { - spl_token_2022::instruction::TokenInstruction::FreezeAccount.pack() + spl_token_2022_interface::instruction::TokenInstruction::FreezeAccount.pack() }; Ok(CompiledInstruction { @@ -1145,10 +1160,10 @@ impl IxUtils { let freeze_authority_idx = Self::get_account_index(account_keys_hashmap, &freeze_authority)?; - let data = if parsed.program_id == spl_token::ID.to_string() { - spl_token::instruction::TokenInstruction::ThawAccount.pack() + let data = if parsed.program_id == spl_token_interface::ID.to_string() { + spl_token_interface::instruction::TokenInstruction::ThawAccount.pack() } else { - spl_token_2022::instruction::TokenInstruction::ThawAccount.pack() + spl_token_2022_interface::instruction::TokenInstruction::ThawAccount.pack() }; Ok(CompiledInstruction { @@ -1299,12 +1314,12 @@ impl IxUtils { for instruction in &transaction.all_instructions { let program_id = instruction.program_id; - if program_id == spl_token::ID { + if program_id == spl_token_interface::ID { if let Ok(spl_ix) = - spl_token::instruction::TokenInstruction::unpack(&instruction.data) + spl_token_interface::instruction::TokenInstruction::unpack(&instruction.data) { match spl_ix { - spl_token::instruction::TokenInstruction::Transfer { amount } => { + spl_token_interface::instruction::TokenInstruction::Transfer { amount } => { validate_number_accounts!(instruction, instruction_indexes::spl_token_transfer::REQUIRED_NUMBER_OF_ACCOUNTS); parsed_instructions @@ -1319,7 +1334,7 @@ impl IxUtils { is_2022: false, }); } - spl_token::instruction::TokenInstruction::TransferChecked { + spl_token_interface::instruction::TokenInstruction::TransferChecked { amount, .. } => { @@ -1337,8 +1352,8 @@ impl IxUtils { is_2022: false, }); } - spl_token::instruction::TokenInstruction::Burn { .. } - | spl_token::instruction::TokenInstruction::BurnChecked { .. } => { + spl_token_interface::instruction::TokenInstruction::Burn { .. } + | spl_token_interface::instruction::TokenInstruction::BurnChecked { .. } => { validate_number_accounts!( instruction, instruction_indexes::spl_token_burn::REQUIRED_NUMBER_OF_ACCOUNTS @@ -1354,7 +1369,7 @@ impl IxUtils { is_2022: false, }); } - spl_token::instruction::TokenInstruction::CloseAccount { .. } => { + spl_token_interface::instruction::TokenInstruction::CloseAccount { .. } => { validate_number_accounts!(instruction, instruction_indexes::spl_token_close_account::REQUIRED_NUMBER_OF_ACCOUNTS); parsed_instructions @@ -1367,7 +1382,7 @@ impl IxUtils { is_2022: false, }); } - spl_token::instruction::TokenInstruction::Approve { .. } => { + spl_token_interface::instruction::TokenInstruction::Approve { .. } => { validate_number_accounts!( instruction, instruction_indexes::spl_token_approve::REQUIRED_NUMBER_OF_ACCOUNTS @@ -1383,7 +1398,7 @@ impl IxUtils { is_2022: false, }); } - spl_token::instruction::TokenInstruction::ApproveChecked { .. } => { + spl_token_interface::instruction::TokenInstruction::ApproveChecked { .. } => { validate_number_accounts!(instruction, instruction_indexes::spl_token_approve_checked::REQUIRED_NUMBER_OF_ACCOUNTS); parsed_instructions @@ -1394,7 +1409,7 @@ impl IxUtils { is_2022: false, }); } - spl_token::instruction::TokenInstruction::Revoke => { + spl_token_interface::instruction::TokenInstruction::Revoke => { validate_number_accounts!( instruction, instruction_indexes::spl_token_revoke::REQUIRED_NUMBER_OF_ACCOUNTS @@ -1410,7 +1425,7 @@ impl IxUtils { is_2022: false, }); } - spl_token::instruction::TokenInstruction::SetAuthority { .. } => { + spl_token_interface::instruction::TokenInstruction::SetAuthority { .. } => { validate_number_accounts!(instruction, instruction_indexes::spl_token_set_authority::REQUIRED_NUMBER_OF_ACCOUNTS); parsed_instructions @@ -1421,7 +1436,7 @@ impl IxUtils { is_2022: false, }); } - spl_token::instruction::TokenInstruction::MintTo { .. } => { + spl_token_interface::instruction::TokenInstruction::MintTo { .. } => { validate_number_accounts!( instruction, instruction_indexes::spl_token_mint_to::REQUIRED_NUMBER_OF_ACCOUNTS @@ -1435,7 +1450,7 @@ impl IxUtils { is_2022: false, }); } - spl_token::instruction::TokenInstruction::MintToChecked { .. } => { + spl_token_interface::instruction::TokenInstruction::MintToChecked { .. } => { validate_number_accounts!(instruction, instruction_indexes::spl_token_mint_to_checked::REQUIRED_NUMBER_OF_ACCOUNTS); parsed_instructions @@ -1446,7 +1461,7 @@ impl IxUtils { is_2022: false, }); } - spl_token::instruction::TokenInstruction::InitializeMint { + spl_token_interface::instruction::TokenInstruction::InitializeMint { mint_authority, .. } => { @@ -1460,7 +1475,7 @@ impl IxUtils { is_2022: false, }); } - spl_token::instruction::TokenInstruction::InitializeMint2 { + spl_token_interface::instruction::TokenInstruction::InitializeMint2 { mint_authority, .. } => { @@ -1474,7 +1489,7 @@ impl IxUtils { is_2022: false, }); } - spl_token::instruction::TokenInstruction::InitializeAccount => { + spl_token_interface::instruction::TokenInstruction::InitializeAccount => { validate_number_accounts!(instruction, instruction_indexes::spl_token_initialize_account::REQUIRED_NUMBER_OF_ACCOUNTS); parsed_instructions @@ -1485,7 +1500,7 @@ impl IxUtils { is_2022: false, }); } - spl_token::instruction::TokenInstruction::InitializeAccount2 { owner } => { + spl_token_interface::instruction::TokenInstruction::InitializeAccount2 { owner } => { validate_number_accounts!(instruction, instruction_indexes::spl_token_initialize_account2::REQUIRED_NUMBER_OF_ACCOUNTS); parsed_instructions @@ -1496,7 +1511,7 @@ impl IxUtils { is_2022: false, }); } - spl_token::instruction::TokenInstruction::InitializeAccount3 { owner } => { + spl_token_interface::instruction::TokenInstruction::InitializeAccount3 { owner } => { validate_number_accounts!(instruction, instruction_indexes::spl_token_initialize_account3::REQUIRED_NUMBER_OF_ACCOUNTS); parsed_instructions @@ -1507,7 +1522,7 @@ impl IxUtils { is_2022: false, }); } - spl_token::instruction::TokenInstruction::InitializeMultisig { .. } => { + spl_token_interface::instruction::TokenInstruction::InitializeMultisig { .. } => { validate_number_accounts!(instruction, instruction_indexes::spl_token_initialize_multisig::REQUIRED_NUMBER_OF_ACCOUNTS); // Extract signers from accounts (skip first 2: multisig + rent sysvar) @@ -1522,7 +1537,7 @@ impl IxUtils { is_2022: false, }); } - spl_token::instruction::TokenInstruction::InitializeMultisig2 { + spl_token_interface::instruction::TokenInstruction::InitializeMultisig2 { .. } => { validate_number_accounts!(instruction, instruction_indexes::spl_token_initialize_multisig2::REQUIRED_NUMBER_OF_ACCOUNTS); @@ -1539,7 +1554,7 @@ impl IxUtils { is_2022: false, }); } - spl_token::instruction::TokenInstruction::FreezeAccount => { + spl_token_interface::instruction::TokenInstruction::FreezeAccount => { validate_number_accounts!(instruction, instruction_indexes::spl_token_freeze_account::REQUIRED_NUMBER_OF_ACCOUNTS); parsed_instructions @@ -1550,7 +1565,7 @@ impl IxUtils { is_2022: false, }); } - spl_token::instruction::TokenInstruction::ThawAccount => { + spl_token_interface::instruction::TokenInstruction::ThawAccount => { validate_number_accounts!(instruction, instruction_indexes::spl_token_thaw_account::REQUIRED_NUMBER_OF_ACCOUNTS); parsed_instructions @@ -1564,13 +1579,13 @@ impl IxUtils { _ => {} }; } - } else if program_id == spl_token_2022::ID { - if let Ok(spl_ix) = - spl_token_2022::instruction::TokenInstruction::unpack(&instruction.data) - { + } else if program_id == spl_token_2022_interface::ID { + if let Ok(spl_ix) = spl_token_2022_interface::instruction::TokenInstruction::unpack( + &instruction.data, + ) { match spl_ix { #[allow(deprecated)] - spl_token_2022::instruction::TokenInstruction::Transfer { amount } => { + spl_token_2022_interface::instruction::TokenInstruction::Transfer { amount } => { validate_number_accounts!(instruction, instruction_indexes::spl_token_transfer::REQUIRED_NUMBER_OF_ACCOUNTS); parsed_instructions @@ -1585,7 +1600,7 @@ impl IxUtils { is_2022: true, }); } - spl_token_2022::instruction::TokenInstruction::TransferChecked { + spl_token_2022_interface::instruction::TokenInstruction::TransferChecked { amount, .. } => { @@ -1603,8 +1618,8 @@ impl IxUtils { is_2022: true, }); } - spl_token_2022::instruction::TokenInstruction::Burn { .. } - | spl_token_2022::instruction::TokenInstruction::BurnChecked { .. } => { + spl_token_2022_interface::instruction::TokenInstruction::Burn { .. } + | spl_token_2022_interface::instruction::TokenInstruction::BurnChecked { .. } => { validate_number_accounts!( instruction, instruction_indexes::spl_token_burn::REQUIRED_NUMBER_OF_ACCOUNTS @@ -1620,7 +1635,7 @@ impl IxUtils { is_2022: true, }); } - spl_token_2022::instruction::TokenInstruction::CloseAccount { .. } => { + spl_token_2022_interface::instruction::TokenInstruction::CloseAccount { .. } => { validate_number_accounts!(instruction, instruction_indexes::spl_token_close_account::REQUIRED_NUMBER_OF_ACCOUNTS); parsed_instructions @@ -1633,7 +1648,7 @@ impl IxUtils { is_2022: true, }); } - spl_token_2022::instruction::TokenInstruction::Approve { .. } => { + spl_token_2022_interface::instruction::TokenInstruction::Approve { .. } => { validate_number_accounts!( instruction, instruction_indexes::spl_token_approve::REQUIRED_NUMBER_OF_ACCOUNTS @@ -1649,7 +1664,7 @@ impl IxUtils { is_2022: true, }); } - spl_token_2022::instruction::TokenInstruction::ApproveChecked { + spl_token_2022_interface::instruction::TokenInstruction::ApproveChecked { .. } => { validate_number_accounts!(instruction, instruction_indexes::spl_token_approve_checked::REQUIRED_NUMBER_OF_ACCOUNTS); @@ -1662,7 +1677,7 @@ impl IxUtils { is_2022: true, }); } - spl_token_2022::instruction::TokenInstruction::Revoke => { + spl_token_2022_interface::instruction::TokenInstruction::Revoke => { validate_number_accounts!( instruction, instruction_indexes::spl_token_revoke::REQUIRED_NUMBER_OF_ACCOUNTS @@ -1678,7 +1693,7 @@ impl IxUtils { is_2022: true, }); } - spl_token_2022::instruction::TokenInstruction::SetAuthority { .. } => { + spl_token_2022_interface::instruction::TokenInstruction::SetAuthority { .. } => { validate_number_accounts!(instruction, instruction_indexes::spl_token_set_authority::REQUIRED_NUMBER_OF_ACCOUNTS); parsed_instructions @@ -1689,7 +1704,7 @@ impl IxUtils { is_2022: true, }); } - spl_token_2022::instruction::TokenInstruction::MintTo { .. } => { + spl_token_2022_interface::instruction::TokenInstruction::MintTo { .. } => { validate_number_accounts!( instruction, instruction_indexes::spl_token_mint_to::REQUIRED_NUMBER_OF_ACCOUNTS @@ -1703,7 +1718,7 @@ impl IxUtils { is_2022: true, }); } - spl_token_2022::instruction::TokenInstruction::MintToChecked { .. } => { + spl_token_2022_interface::instruction::TokenInstruction::MintToChecked { .. } => { validate_number_accounts!(instruction, instruction_indexes::spl_token_mint_to_checked::REQUIRED_NUMBER_OF_ACCOUNTS); parsed_instructions @@ -1714,7 +1729,7 @@ impl IxUtils { is_2022: true, }); } - spl_token_2022::instruction::TokenInstruction::InitializeMint { + spl_token_2022_interface::instruction::TokenInstruction::InitializeMint { mint_authority, .. } => { @@ -1728,7 +1743,7 @@ impl IxUtils { is_2022: true, }); } - spl_token_2022::instruction::TokenInstruction::InitializeMint2 { + spl_token_2022_interface::instruction::TokenInstruction::InitializeMint2 { mint_authority, .. } => { @@ -1742,7 +1757,7 @@ impl IxUtils { is_2022: true, }); } - spl_token_2022::instruction::TokenInstruction::InitializeAccount => { + spl_token_2022_interface::instruction::TokenInstruction::InitializeAccount => { validate_number_accounts!(instruction, instruction_indexes::spl_token_initialize_account::REQUIRED_NUMBER_OF_ACCOUNTS); parsed_instructions @@ -1753,7 +1768,7 @@ impl IxUtils { is_2022: true, }); } - spl_token_2022::instruction::TokenInstruction::InitializeAccount2 { + spl_token_2022_interface::instruction::TokenInstruction::InitializeAccount2 { owner, } => { validate_number_accounts!(instruction, instruction_indexes::spl_token_initialize_account2::REQUIRED_NUMBER_OF_ACCOUNTS); @@ -1766,7 +1781,7 @@ impl IxUtils { is_2022: true, }); } - spl_token_2022::instruction::TokenInstruction::InitializeAccount3 { + spl_token_2022_interface::instruction::TokenInstruction::InitializeAccount3 { owner, } => { validate_number_accounts!(instruction, instruction_indexes::spl_token_initialize_account3::REQUIRED_NUMBER_OF_ACCOUNTS); @@ -1779,7 +1794,7 @@ impl IxUtils { is_2022: true, }); } - spl_token_2022::instruction::TokenInstruction::InitializeMultisig { + spl_token_2022_interface::instruction::TokenInstruction::InitializeMultisig { .. } => { validate_number_accounts!(instruction, instruction_indexes::spl_token_initialize_multisig::REQUIRED_NUMBER_OF_ACCOUNTS); @@ -1796,7 +1811,7 @@ impl IxUtils { is_2022: true, }); } - spl_token_2022::instruction::TokenInstruction::InitializeMultisig2 { + spl_token_2022_interface::instruction::TokenInstruction::InitializeMultisig2 { .. } => { validate_number_accounts!(instruction, instruction_indexes::spl_token_initialize_multisig2::REQUIRED_NUMBER_OF_ACCOUNTS); @@ -1813,7 +1828,7 @@ impl IxUtils { is_2022: true, }); } - spl_token_2022::instruction::TokenInstruction::FreezeAccount => { + spl_token_2022_interface::instruction::TokenInstruction::FreezeAccount => { validate_number_accounts!(instruction, instruction_indexes::spl_token_freeze_account::REQUIRED_NUMBER_OF_ACCOUNTS); parsed_instructions @@ -1824,7 +1839,7 @@ impl IxUtils { is_2022: true, }); } - spl_token_2022::instruction::TokenInstruction::ThawAccount => { + spl_token_2022_interface::instruction::TokenInstruction::ThawAccount => { validate_number_accounts!(instruction, instruction_indexes::spl_token_thaw_account::REQUIRED_NUMBER_OF_ACCOUNTS); parsed_instructions @@ -1858,7 +1873,7 @@ mod tests { ) -> Result> { let solana_instruction = - solana_sdk::system_instruction::transfer(source, destination, lamports); + solana_system_interface::instruction::transfer(source, destination, lamports); let message = Message::new(&[solana_instruction], None); let compiled_instruction = &message.instructions[0]; @@ -1884,7 +1899,7 @@ mod tests { source_owner: &Pubkey, ) -> Result> { - let solana_instruction = solana_sdk::system_instruction::transfer_with_seed( + let solana_instruction = solana_system_interface::instruction::transfer_with_seed( source, source_base, seed.to_string(), @@ -1916,7 +1931,7 @@ mod tests { owner: &Pubkey, ) -> Result> { - let solana_instruction = solana_sdk::system_instruction::create_account( + let solana_instruction = solana_system_interface::instruction::create_account( source, new_account, lamports, @@ -1949,7 +1964,7 @@ mod tests { owner: &Pubkey, ) -> Result> { - let solana_instruction = solana_sdk::system_instruction::create_account_with_seed( + let solana_instruction = solana_system_interface::instruction::create_account_with_seed( source, new_account, base, @@ -1979,7 +1994,7 @@ mod tests { owner: &Pubkey, ) -> Result> { - let solana_instruction = solana_sdk::system_instruction::assign(account, owner); + let solana_instruction = solana_system_interface::instruction::assign(account, owner); let message = Message::new(&[solana_instruction], None); let compiled_instruction = &message.instructions[0]; @@ -2004,7 +2019,7 @@ mod tests { ) -> Result> { let solana_instruction = - solana_sdk::system_instruction::assign_with_seed(account, base, seed, owner); + solana_system_interface::instruction::assign_with_seed(account, base, seed, owner); let message = Message::new(&[solana_instruction], None); let compiled_instruction = &message.instructions[0]; @@ -2028,7 +2043,7 @@ mod tests { lamports: u64, ) -> Result> { - let solana_instruction = solana_sdk::system_instruction::withdraw_nonce_account( + let solana_instruction = solana_system_interface::instruction::withdraw_nonce_account( nonce_account, nonce_authority, recipient, @@ -2057,8 +2072,8 @@ mod tests { amount: u64, ) -> Result> { - let solana_instruction = spl_token::instruction::transfer( - &spl_token::ID, + let solana_instruction = spl_token_interface::instruction::transfer( + &spl_token_interface::ID, source, destination, authority, @@ -2072,7 +2087,7 @@ mod tests { let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None); let parsed = parse_instruction::parse( - &spl_token::ID, + &spl_token_interface::ID, compiled_instruction, &account_keys_for_parsing, None, @@ -2090,8 +2105,8 @@ mod tests { decimals: u8, ) -> Result> { - let solana_instruction = spl_token::instruction::transfer_checked( - &spl_token::ID, + let solana_instruction = spl_token_interface::instruction::transfer_checked( + &spl_token_interface::ID, source, mint, destination, @@ -2107,7 +2122,7 @@ mod tests { let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None); let parsed = parse_instruction::parse( - &spl_token::ID, + &spl_token_interface::ID, compiled_instruction, &account_keys_for_parsing, None, @@ -2123,8 +2138,14 @@ mod tests { amount: u64, ) -> Result> { - let solana_instruction = - spl_token::instruction::burn(&spl_token::ID, account, mint, authority, &[], amount)?; + let solana_instruction = spl_token_interface::instruction::burn( + &spl_token_interface::ID, + account, + mint, + authority, + &[], + amount, + )?; let message = Message::new(&[solana_instruction], None); let compiled_instruction = &message.instructions[0]; @@ -2132,7 +2153,7 @@ mod tests { let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None); let parsed = parse_instruction::parse( - &spl_token::ID, + &spl_token_interface::ID, compiled_instruction, &account_keys_for_parsing, None, @@ -2149,8 +2170,8 @@ mod tests { decimals: u8, ) -> Result> { - let solana_instruction = spl_token::instruction::burn_checked( - &spl_token::ID, + let solana_instruction = spl_token_interface::instruction::burn_checked( + &spl_token_interface::ID, account, mint, authority, @@ -2165,7 +2186,7 @@ mod tests { let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None); let parsed = parse_instruction::parse( - &spl_token::ID, + &spl_token_interface::ID, compiled_instruction, &account_keys_for_parsing, None, @@ -2180,8 +2201,8 @@ mod tests { authority: &Pubkey, ) -> Result> { - let solana_instruction = spl_token::instruction::close_account( - &spl_token::ID, + let solana_instruction = spl_token_interface::instruction::close_account( + &spl_token_interface::ID, account, destination, authority, @@ -2194,7 +2215,7 @@ mod tests { let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None); let parsed = parse_instruction::parse( - &spl_token::ID, + &spl_token_interface::ID, compiled_instruction, &account_keys_for_parsing, None, @@ -2210,8 +2231,8 @@ mod tests { amount: u64, ) -> Result> { - let solana_instruction = spl_token::instruction::approve( - &spl_token::ID, + let solana_instruction = spl_token_interface::instruction::approve( + &spl_token_interface::ID, source, delegate, authority, @@ -2225,7 +2246,7 @@ mod tests { let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None); let parsed = parse_instruction::parse( - &spl_token::ID, + &spl_token_interface::ID, compiled_instruction, &account_keys_for_parsing, None, @@ -2243,8 +2264,8 @@ mod tests { decimals: u8, ) -> Result> { - let solana_instruction = spl_token::instruction::approve_checked( - &spl_token::ID, + let solana_instruction = spl_token_interface::instruction::approve_checked( + &spl_token_interface::ID, source, mint, delegate, @@ -2260,7 +2281,7 @@ mod tests { let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None); let parsed = parse_instruction::parse( - &spl_token::ID, + &spl_token_interface::ID, compiled_instruction, &account_keys_for_parsing, None, @@ -2277,8 +2298,8 @@ mod tests { ) -> Result> { #[allow(deprecated)] - let solana_instruction = spl_token_2022::instruction::transfer( - &spl_token_2022::ID, + let solana_instruction = spl_token_2022_interface::instruction::transfer( + &spl_token_2022_interface::ID, source, destination, authority, @@ -2292,7 +2313,7 @@ mod tests { let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None); let parsed = parse_instruction::parse( - &spl_token_2022::ID, + &spl_token_2022_interface::ID, compiled_instruction, &account_keys_for_parsing, None, @@ -2310,8 +2331,8 @@ mod tests { decimals: u8, ) -> Result> { - let solana_instruction = spl_token_2022::instruction::transfer_checked( - &spl_token_2022::ID, + let solana_instruction = spl_token_2022_interface::instruction::transfer_checked( + &spl_token_2022_interface::ID, source, mint, destination, @@ -2327,7 +2348,7 @@ mod tests { let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None); let parsed = parse_instruction::parse( - &spl_token_2022::ID, + &spl_token_2022_interface::ID, compiled_instruction, &account_keys_for_parsing, None, @@ -2343,8 +2364,8 @@ mod tests { amount: u64, ) -> Result> { - let solana_instruction = spl_token_2022::instruction::burn( - &spl_token_2022::ID, + let solana_instruction = spl_token_2022_interface::instruction::burn( + &spl_token_2022_interface::ID, account, mint, authority, @@ -2358,7 +2379,7 @@ mod tests { let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None); let parsed = parse_instruction::parse( - &spl_token_2022::ID, + &spl_token_2022_interface::ID, compiled_instruction, &account_keys_for_parsing, None, @@ -2375,8 +2396,8 @@ mod tests { decimals: u8, ) -> Result> { - let solana_instruction = spl_token_2022::instruction::burn_checked( - &spl_token_2022::ID, + let solana_instruction = spl_token_2022_interface::instruction::burn_checked( + &spl_token_2022_interface::ID, account, mint, authority, @@ -2391,7 +2412,7 @@ mod tests { let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None); let parsed = parse_instruction::parse( - &spl_token_2022::ID, + &spl_token_2022_interface::ID, compiled_instruction, &account_keys_for_parsing, None, @@ -2406,8 +2427,8 @@ mod tests { authority: &Pubkey, ) -> Result> { - let solana_instruction = spl_token_2022::instruction::close_account( - &spl_token_2022::ID, + let solana_instruction = spl_token_2022_interface::instruction::close_account( + &spl_token_2022_interface::ID, account, destination, authority, @@ -2420,7 +2441,7 @@ mod tests { let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None); let parsed = parse_instruction::parse( - &spl_token_2022::ID, + &spl_token_2022_interface::ID, compiled_instruction, &account_keys_for_parsing, None, @@ -2436,8 +2457,8 @@ mod tests { amount: u64, ) -> Result> { - let solana_instruction = spl_token_2022::instruction::approve( - &spl_token_2022::ID, + let solana_instruction = spl_token_2022_interface::instruction::approve( + &spl_token_2022_interface::ID, source, delegate, authority, @@ -2451,7 +2472,7 @@ mod tests { let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None); let parsed = parse_instruction::parse( - &spl_token_2022::ID, + &spl_token_2022_interface::ID, compiled_instruction, &account_keys_for_parsing, None, @@ -2469,8 +2490,8 @@ mod tests { decimals: u8, ) -> Result> { - let solana_instruction = spl_token_2022::instruction::approve_checked( - &spl_token_2022::ID, + let solana_instruction = spl_token_2022_interface::instruction::approve_checked( + &spl_token_2022_interface::ID, source, mint, delegate, @@ -2486,7 +2507,7 @@ mod tests { let account_keys_for_parsing = AccountKeys::new(&message.account_keys, None); let parsed = parse_instruction::parse( - &spl_token_2022::ID, + &spl_token_2022_interface::ID, compiled_instruction, &account_keys_for_parsing, None, @@ -2581,7 +2602,7 @@ mod tests { let lamports = 1000000u64; let transfer_instruction = - solana_sdk::system_instruction::transfer(&source, &destination, lamports); + solana_system_interface::instruction::transfer(&source, &destination, lamports); let solana_parsed_transfer = create_parsed_system_transfer(&source, &destination, lamports) .expect("Failed to create authentic parsed instruction"); @@ -2608,7 +2629,7 @@ mod tests { let account_keys = vec![system_program_id, source, source_base, destination]; let lamports = 5000000u64; - let instruction = solana_sdk::system_instruction::transfer_with_seed( + let instruction = solana_system_interface::instruction::transfer_with_seed( &source, &source_base, "test_seed".to_string(), @@ -2649,7 +2670,7 @@ mod tests { let lamports = 2000000u64; let space = 165u64; - let instruction = solana_sdk::system_instruction::create_account( + let instruction = solana_system_interface::instruction::create_account( &source, &new_account, lamports, @@ -2684,7 +2705,7 @@ mod tests { let lamports = 3000000u64; let space = 200u64; - let instruction = solana_sdk::system_instruction::create_account_with_seed( + let instruction = solana_system_interface::instruction::create_account_with_seed( &source, &new_account, &base, @@ -2724,7 +2745,7 @@ mod tests { let system_program_id = SYSTEM_PROGRAM_ID; let account_keys = vec![system_program_id, account]; - let instruction = solana_sdk::system_instruction::assign(&account, &owner); + let instruction = solana_system_interface::instruction::assign(&account, &owner); let solana_parsed = create_parsed_system_assign(&account, &owner) .expect("Failed to create parsed instruction"); @@ -2749,7 +2770,7 @@ mod tests { let system_program_id = SYSTEM_PROGRAM_ID; let account_keys = vec![system_program_id, account, base]; - let instruction = solana_sdk::system_instruction::assign_with_seed( + let instruction = solana_system_interface::instruction::assign_with_seed( &account, &base, "test_assign_seed", @@ -2781,7 +2802,7 @@ mod tests { let account_keys = vec![system_program_id, nonce_account, recipient, nonce_authority]; let lamports = 1500000u64; - let instruction = solana_sdk::system_instruction::withdraw_nonce_account( + let instruction = solana_system_interface::instruction::withdraw_nonce_account( &nonce_account, &nonce_authority, &recipient, @@ -2813,12 +2834,12 @@ mod tests { let source = Pubkey::new_unique(); let destination = Pubkey::new_unique(); let authority = Pubkey::new_unique(); - let token_program_id = spl_token::ID; + let token_program_id = spl_token_interface::ID; let account_keys = vec![token_program_id, source, destination, authority]; let amount = 1000000u64; - let transfer_instruction = spl_token::instruction::transfer( - &spl_token::ID, + let transfer_instruction = spl_token_interface::instruction::transfer( + &spl_token_interface::ID, &source, &destination, &authority, @@ -2849,13 +2870,13 @@ mod tests { let destination = Pubkey::new_unique(); let authority = Pubkey::new_unique(); let mint = Pubkey::new_unique(); - let token_program_id = spl_token::ID; + let token_program_id = spl_token_interface::ID; let account_keys = vec![token_program_id, source, mint, destination, authority]; let amount = 2000000u64; let decimals = 6u8; - let instruction = spl_token::instruction::transfer_checked( - &spl_token::ID, + let instruction = spl_token_interface::instruction::transfer_checked( + &spl_token_interface::ID, &source, &mint, &destination, @@ -2893,13 +2914,19 @@ mod tests { let account = Pubkey::new_unique(); let mint = Pubkey::new_unique(); let authority = Pubkey::new_unique(); - let token_program_id = spl_token::ID; + let token_program_id = spl_token_interface::ID; let account_keys = vec![token_program_id, account, mint, authority]; let amount = 500000u64; - let instruction = - spl_token::instruction::burn(&spl_token::ID, &account, &mint, &authority, &[], amount) - .expect("Failed to create burn instruction"); + let instruction = spl_token_interface::instruction::burn( + &spl_token_interface::ID, + &account, + &mint, + &authority, + &[], + amount, + ) + .expect("Failed to create burn instruction"); let solana_parsed = create_parsed_spl_token_burn(&account, &mint, &authority, amount) .expect("Failed to create parsed instruction"); @@ -2921,13 +2948,13 @@ mod tests { let account = Pubkey::new_unique(); let authority = Pubkey::new_unique(); let mint = Pubkey::new_unique(); - let token_program_id = spl_token::ID; + let token_program_id = spl_token_interface::ID; let account_keys = vec![token_program_id, account, mint, authority]; let amount = 750000u64; let decimals = 6u8; - let instruction = spl_token::instruction::burn_checked( - &spl_token::ID, + let instruction = spl_token_interface::instruction::burn_checked( + &spl_token_interface::ID, &account, &mint, &authority, @@ -2958,11 +2985,11 @@ mod tests { let account = Pubkey::new_unique(); let destination = Pubkey::new_unique(); let authority = Pubkey::new_unique(); - let token_program_id = spl_token::ID; + let token_program_id = spl_token_interface::ID; let account_keys = vec![token_program_id, account, destination, authority]; - let instruction = spl_token::instruction::close_account( - &spl_token::ID, + let instruction = spl_token_interface::instruction::close_account( + &spl_token_interface::ID, &account, &destination, &authority, @@ -2991,12 +3018,12 @@ mod tests { let source = Pubkey::new_unique(); let delegate = Pubkey::new_unique(); let owner = Pubkey::new_unique(); - let token_program_id = spl_token::ID; + let token_program_id = spl_token_interface::ID; let account_keys = vec![token_program_id, source, delegate, owner]; let amount = 1000000u64; - let instruction = spl_token::instruction::approve( - &spl_token::ID, + let instruction = spl_token_interface::instruction::approve( + &spl_token_interface::ID, &source, &delegate, &owner, @@ -3026,13 +3053,13 @@ mod tests { let delegate = Pubkey::new_unique(); let owner = Pubkey::new_unique(); let mint = Pubkey::new_unique(); - let token_program_id = spl_token::ID; + let token_program_id = spl_token_interface::ID; let account_keys = vec![token_program_id, source, mint, delegate, owner]; let amount = 2500000u64; let decimals = 6u8; - let instruction = spl_token::instruction::approve_checked( - &spl_token::ID, + let instruction = spl_token_interface::instruction::approve_checked( + &spl_token_interface::ID, &source, &mint, &delegate, @@ -3065,13 +3092,13 @@ mod tests { let source = Pubkey::new_unique(); let destination = Pubkey::new_unique(); let authority = Pubkey::new_unique(); - let token_program_id = spl_token_2022::ID; + let token_program_id = spl_token_2022_interface::ID; let account_keys = vec![token_program_id, source, destination, authority]; let amount = 1500000u64; #[allow(deprecated)] - let instruction = spl_token_2022::instruction::transfer( - &spl_token_2022::ID, + let instruction = spl_token_2022_interface::instruction::transfer( + &spl_token_2022_interface::ID, &source, &destination, &authority, @@ -3102,13 +3129,13 @@ mod tests { let destination = Pubkey::new_unique(); let authority = Pubkey::new_unique(); let mint = Pubkey::new_unique(); - let token_program_id = spl_token_2022::ID; + let token_program_id = spl_token_2022_interface::ID; let account_keys = vec![token_program_id, source, mint, destination, authority]; let amount = 3000000u64; let decimals = 6u8; - let instruction = spl_token_2022::instruction::transfer_checked( - &spl_token_2022::ID, + let instruction = spl_token_2022_interface::instruction::transfer_checked( + &spl_token_2022_interface::ID, &source, &mint, &destination, @@ -3146,12 +3173,12 @@ mod tests { let account = Pubkey::new_unique(); let mint = Pubkey::new_unique(); let authority = Pubkey::new_unique(); - let token_program_id = spl_token_2022::ID; + let token_program_id = spl_token_2022_interface::ID; let account_keys = vec![token_program_id, account, mint, authority]; let amount = 800000u64; - let instruction = spl_token_2022::instruction::burn( - &spl_token_2022::ID, + let instruction = spl_token_2022_interface::instruction::burn( + &spl_token_2022_interface::ID, &account, &mint, &authority, @@ -3180,13 +3207,13 @@ mod tests { let account = Pubkey::new_unique(); let authority = Pubkey::new_unique(); let mint = Pubkey::new_unique(); - let token_program_id = spl_token_2022::ID; + let token_program_id = spl_token_2022_interface::ID; let account_keys = vec![token_program_id, account, mint, authority]; let amount = 900000u64; let decimals = 6u8; - let instruction = spl_token_2022::instruction::burn_checked( - &spl_token_2022::ID, + let instruction = spl_token_2022_interface::instruction::burn_checked( + &spl_token_2022_interface::ID, &account, &mint, &authority, @@ -3217,11 +3244,11 @@ mod tests { let account = Pubkey::new_unique(); let destination = Pubkey::new_unique(); let authority = Pubkey::new_unique(); - let token_program_id = spl_token_2022::ID; + let token_program_id = spl_token_2022_interface::ID; let account_keys = vec![token_program_id, account, destination, authority]; - let instruction = spl_token_2022::instruction::close_account( - &spl_token_2022::ID, + let instruction = spl_token_2022_interface::instruction::close_account( + &spl_token_2022_interface::ID, &account, &destination, &authority, @@ -3250,12 +3277,12 @@ mod tests { let source = Pubkey::new_unique(); let delegate = Pubkey::new_unique(); let owner = Pubkey::new_unique(); - let token_program_id = spl_token_2022::ID; + let token_program_id = spl_token_2022_interface::ID; let account_keys = vec![token_program_id, source, delegate, owner]; let amount = 1200000u64; - let instruction = spl_token_2022::instruction::approve( - &spl_token_2022::ID, + let instruction = spl_token_2022_interface::instruction::approve( + &spl_token_2022_interface::ID, &source, &delegate, &owner, @@ -3285,13 +3312,13 @@ mod tests { let delegate = Pubkey::new_unique(); let owner = Pubkey::new_unique(); let mint = Pubkey::new_unique(); - let token_program_id = spl_token_2022::ID; + let token_program_id = spl_token_2022_interface::ID; let account_keys = vec![token_program_id, source, mint, delegate, owner]; let amount = 3500000u64; let decimals = 6u8; - let instruction = spl_token_2022::instruction::approve_checked( - &spl_token_2022::ID, + let instruction = spl_token_2022_interface::instruction::approve_checked( + &spl_token_2022_interface::ID, &source, &mint, &delegate, diff --git a/crates/lib/src/transaction/transaction.rs b/crates/lib/src/transaction/transaction.rs index 945bf360..7c2bf5ce 100644 --- a/crates/lib/src/transaction/transaction.rs +++ b/crates/lib/src/transaction/transaction.rs @@ -61,10 +61,10 @@ impl TransactionUtil { mod tests { use super::*; use crate::error::KoraError; - use solana_message::{v0, Message}; + use solana_message::{compiled_instruction::CompiledInstruction, v0, Message}; use solana_sdk::{ hash::Hash, - instruction::{AccountMeta, CompiledInstruction, Instruction}, + instruction::{AccountMeta, Instruction}, pubkey::Pubkey, signature::Keypair, signer::Signer as _, diff --git a/crates/lib/src/transaction/versioned_message.rs b/crates/lib/src/transaction/versioned_message.rs index dc4c4914..67f11a81 100644 --- a/crates/lib/src/transaction/versioned_message.rs +++ b/crates/lib/src/transaction/versioned_message.rs @@ -17,10 +17,10 @@ impl VersionedMessageExt for VersionedMessage { #[cfg(test)] mod tests { use super::*; - use solana_message::{v0, Message}; + use solana_message::{compiled_instruction::CompiledInstruction, v0, Message}; use solana_sdk::{ hash::Hash, - instruction::{AccountMeta, CompiledInstruction, Instruction}, + instruction::{AccountMeta, Instruction}, pubkey::Pubkey, signature::Keypair, signer::Signer as _, diff --git a/crates/lib/src/transaction/versioned_transaction.rs b/crates/lib/src/transaction/versioned_transaction.rs index 0100e6b0..8e380ae1 100644 --- a/crates/lib/src/transaction/versioned_transaction.rs +++ b/crates/lib/src/transaction/versioned_transaction.rs @@ -2,12 +2,10 @@ use async_trait::async_trait; use base64::{engine::general_purpose::STANDARD, Engine as _}; use solana_client::{nonblocking::rpc_client::RpcClient, rpc_config::RpcSimulateTransactionConfig}; use solana_commitment_config::CommitmentConfig; -use solana_message::{v0::MessageAddressTableLookup, VersionedMessage}; -use solana_sdk::{ - instruction::{CompiledInstruction, Instruction}, - pubkey::Pubkey, - transaction::VersionedTransaction, +use solana_message::{ + compiled_instruction::CompiledInstruction, v0::MessageAddressTableLookup, VersionedMessage, }; +use solana_sdk::{instruction::Instruction, pubkey::Pubkey, transaction::VersionedTransaction}; use solana_signers::{Signer, SolanaSigner}; use std::{collections::HashMap, ops::Deref}; @@ -399,11 +397,11 @@ mod tests { use super::*; use solana_address_lookup_table_interface::state::LookupTableMeta; - use solana_message::{v0, Message}; + use solana_message::{compiled_instruction::CompiledInstruction, v0, Message}; use solana_sdk::{ account::Account, hash::Hash, - instruction::{AccountMeta, CompiledInstruction, Instruction}, + instruction::{AccountMeta, Instruction}, signature::Keypair, signer::Signer, }; @@ -962,7 +960,7 @@ mod tests { // Create a system transfer instruction let instruction = - solana_sdk::system_instruction::transfer(&keypair.pubkey(), &recipient, 1000000); + solana_system_interface::instruction::transfer(&keypair.pubkey(), &recipient, 1000000); let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&keypair.pubkey()))); let transaction = VersionedTransaction::try_new(message, &[&keypair]).unwrap(); diff --git a/crates/lib/src/validator/account_validator.rs b/crates/lib/src/validator/account_validator.rs index fa7c2e0b..de85ac44 100644 --- a/crates/lib/src/validator/account_validator.rs +++ b/crates/lib/src/validator/account_validator.rs @@ -1,15 +1,15 @@ use solana_client::nonblocking::rpc_client::RpcClient; -use solana_sdk::{ - account::Account, program_pack::Pack, pubkey::Pubkey, system_program::ID as SYSTEM_PROGRAM_ID, +use solana_program_pack::Pack; +use solana_sdk::{account::Account, pubkey::Pubkey}; +use solana_system_interface::program::ID as SYSTEM_PROGRAM_ID; +use spl_token_2022_interface::{ + state::{Account as Token2022Account, Mint as Token2022Mint}, + ID as TOKEN_2022_PROGRAM_ID, }; -use spl_token::{ +use spl_token_interface::{ state::{Account as SplTokenAccount, Mint}, ID as SPL_TOKEN_PROGRAM_ID, }; -use spl_token_2022::{ - state::{Account as Token2022Account, Mint as Token2022Mint}, - ID as TOKEN_2022_PROGRAM_ID, -}; use crate::{CacheUtil, KoraError}; @@ -32,19 +32,29 @@ impl AccountType { match self { AccountType::Mint => match account.owner { - SPL_TOKEN_PROGRAM_ID => { + ref owner if *owner == SPL_TOKEN_PROGRAM_ID => { should_be_executable = Some(false); - Mint::unpack(&account.data).map_err(|e| { + if account.data.len() < Mint::LEN { + return Err(KoraError::InternalServerError(format!( + "Account {account_pubkey} has invalid data for a Mint account: data too short" + ))); + } + Mint::unpack_from_slice(&account.data).map_err(|e| { KoraError::InternalServerError(format!( "Account {account_pubkey} has invalid data for a Mint account: {e}" )) })?; } - TOKEN_2022_PROGRAM_ID => { + ref owner if *owner == TOKEN_2022_PROGRAM_ID => { should_be_executable = Some(false); - Token2022Mint::unpack(&account.data).map_err(|e| { + if account.data.len() < Token2022Mint::LEN { + return Err(KoraError::InternalServerError(format!( + "Account {account_pubkey} has invalid data for a Mint account: data too short" + ))); + } + Token2022Mint::unpack_from_slice(&account.data).map_err(|e| { KoraError::InternalServerError(format!( "Account {account_pubkey} has invalid data for a Mint account: {e}" )) @@ -57,19 +67,29 @@ impl AccountType { } }, AccountType::TokenAccount => match account.owner { - SPL_TOKEN_PROGRAM_ID => { + ref owner if *owner == SPL_TOKEN_PROGRAM_ID => { should_be_executable = Some(false); - SplTokenAccount::unpack(&account.data).map_err(|e| { + if account.data.len() < SplTokenAccount::LEN { + return Err(KoraError::InternalServerError(format!( + "Account {account_pubkey} has invalid data for a TokenAccount account: data too short" + ))); + } + SplTokenAccount::unpack_from_slice(&account.data).map_err(|e| { KoraError::InternalServerError(format!( "Account {account_pubkey} has invalid data for a TokenAccount account: {e}" )) })?; } - TOKEN_2022_PROGRAM_ID => { + ref owner if *owner == TOKEN_2022_PROGRAM_ID => { should_be_executable = Some(false); - Token2022Account::unpack(&account.data).map_err(|e| { + if account.data.len() < Token2022Account::LEN { + return Err(KoraError::InternalServerError(format!( + "Account {account_pubkey} has invalid data for a TokenAccount account: data too short" + ))); + } + Token2022Account::unpack_from_slice(&account.data).map_err(|e| { KoraError::InternalServerError(format!( "Account {account_pubkey} has invalid data for a TokenAccount account: {e}" )) diff --git a/crates/lib/src/validator/config_validator.rs b/crates/lib/src/validator/config_validator.rs index ca828c26..eb1dca89 100644 --- a/crates/lib/src/validator/config_validator.rs +++ b/crates/lib/src/validator/config_validator.rs @@ -16,9 +16,10 @@ use crate::{ KoraError, }; use solana_client::nonblocking::rpc_client::RpcClient; -use solana_sdk::{pubkey::Pubkey, system_program::ID as SYSTEM_PROGRAM_ID}; -use spl_token::ID as SPL_TOKEN_PROGRAM_ID; -use spl_token_2022::{extension::ExtensionType, ID as TOKEN_2022_PROGRAM_ID}; +use solana_sdk::pubkey::Pubkey; +use solana_system_interface::program::ID as SYSTEM_PROGRAM_ID; +use spl_token_2022_interface::{extension::ExtensionType, ID as TOKEN_2022_PROGRAM_ID}; +use spl_token_interface::ID as SPL_TOKEN_PROGRAM_ID; pub struct ConfigValidator {} diff --git a/crates/lib/src/validator/transaction_validator.rs b/crates/lib/src/validator/transaction_validator.rs index bfc1892b..20394088 100644 --- a/crates/lib/src/validator/transaction_validator.rs +++ b/crates/lib/src/validator/transaction_validator.rs @@ -15,7 +15,7 @@ use solana_sdk::{pubkey::Pubkey, transaction::VersionedTransaction}; use std::str::FromStr; #[allow(unused_imports)] -use spl_token_2022::{ +use spl_token_2022_interface::{ extension::{ cpi_guard::CpiGuard, interest_bearing_mint::InterestBearingConfig, @@ -416,7 +416,7 @@ mod tests { fn setup_spl_config_with_policy(policy: FeePayerPolicy) { let config = ConfigMockBuilder::new() .with_price_source(PriceSource::Mock) - .with_allowed_programs(vec![spl_token::id().to_string()]) + .with_allowed_programs(vec![spl_token_interface::id().to_string()]) .with_max_allowed_lamports(1_000_000) .with_fee_payer_policy(policy) .build(); @@ -426,7 +426,7 @@ mod tests { fn setup_token2022_config_with_policy(policy: FeePayerPolicy) { let config = ConfigMockBuilder::new() .with_price_source(PriceSource::Mock) - .with_allowed_programs(vec![spl_token_2022::id().to_string()]) + .with_allowed_programs(vec![spl_token_2022_interface::id().to_string()]) .with_max_allowed_lamports(1_000_000) .with_fee_payer_policy(policy) .build(); @@ -691,8 +691,8 @@ mod tests { let validator = TransactionValidator::new(fee_payer).unwrap(); - let transfer_ix = spl_token::instruction::transfer( - &spl_token::id(), + let transfer_ix = spl_token_interface::instruction::transfer( + &spl_token_interface::id(), &fee_payer_token_account, &recipient_token_account, &fee_payer, // fee payer is the signer @@ -714,8 +714,8 @@ mod tests { let validator = TransactionValidator::new(fee_payer).unwrap(); - let transfer_ix = spl_token::instruction::transfer( - &spl_token::id(), + let transfer_ix = spl_token_interface::instruction::transfer( + &spl_token_interface::id(), &fee_payer_token_account, &recipient_token_account, &fee_payer, // fee payer is the signer @@ -730,8 +730,8 @@ mod tests { // Test with other account as source - should always pass let other_signer = Pubkey::new_unique(); - let transfer_ix = spl_token::instruction::transfer( - &spl_token::id(), + let transfer_ix = spl_token_interface::instruction::transfer( + &spl_token_interface::id(), &fee_payer_token_account, &recipient_token_account, &other_signer, // other account is the signer @@ -765,8 +765,8 @@ mod tests { let validator = TransactionValidator::new(fee_payer).unwrap(); - let transfer_ix = spl_token_2022::instruction::transfer_checked( - &spl_token_2022::id(), + let transfer_ix = spl_token_2022_interface::instruction::transfer_checked( + &spl_token_2022_interface::id(), &fee_payer_token_account, &mint, &recipient_token_account, @@ -791,8 +791,8 @@ mod tests { let validator = TransactionValidator::new(fee_payer).unwrap(); - let transfer_ix = spl_token_2022::instruction::transfer_checked( - &spl_token_2022::id(), + let transfer_ix = spl_token_2022_interface::instruction::transfer_checked( + &spl_token_2022_interface::id(), &fee_payer_token_account, &mint, &recipient_token_account, @@ -811,8 +811,8 @@ mod tests { // Test with other account as source - should always pass let other_signer = Pubkey::new_unique(); - let transfer_ix = spl_token_2022::instruction::transfer_checked( - &spl_token_2022::id(), + let transfer_ix = spl_token_2022_interface::instruction::transfer_checked( + &spl_token_2022_interface::id(), &fee_payer_token_account, &mint, &recipient_token_account, @@ -975,8 +975,8 @@ mod tests { let validator = TransactionValidator::new(fee_payer).unwrap(); - let burn_ix = spl_token::instruction::burn( - &spl_token::id(), + let burn_ix = spl_token_interface::instruction::burn( + &spl_token_interface::id(), &fee_payer_token_account, &mint, &fee_payer, @@ -999,8 +999,8 @@ mod tests { let validator = TransactionValidator::new(fee_payer).unwrap(); - let burn_ix = spl_token::instruction::burn( - &spl_token::id(), + let burn_ix = spl_token_interface::instruction::burn( + &spl_token_interface::id(), &fee_payer_token_account, &mint, &fee_payer, @@ -1016,8 +1016,8 @@ mod tests { assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); // Test burn_checked instruction - let burn_checked_ix = spl_token::instruction::burn_checked( - &spl_token::id(), + let burn_checked_ix = spl_token_interface::instruction::burn_checked( + &spl_token_interface::id(), &fee_payer_token_account, &mint, &fee_payer, @@ -1050,8 +1050,8 @@ mod tests { let validator = TransactionValidator::new(fee_payer).unwrap(); - let close_ix = spl_token::instruction::close_account( - &spl_token::id(), + let close_ix = spl_token_interface::instruction::close_account( + &spl_token_interface::id(), &fee_payer_token_account, &destination, &fee_payer, @@ -1072,8 +1072,8 @@ mod tests { let validator = TransactionValidator::new(fee_payer).unwrap(); - let close_ix = spl_token::instruction::close_account( - &spl_token::id(), + let close_ix = spl_token_interface::instruction::close_account( + &spl_token_interface::id(), &fee_payer_token_account, &destination, &fee_payer, @@ -1104,8 +1104,8 @@ mod tests { let validator = TransactionValidator::new(fee_payer).unwrap(); - let approve_ix = spl_token::instruction::approve( - &spl_token::id(), + let approve_ix = spl_token_interface::instruction::approve( + &spl_token_interface::id(), &fee_payer_token_account, &delegate, &fee_payer, @@ -1127,8 +1127,8 @@ mod tests { let validator = TransactionValidator::new(fee_payer).unwrap(); - let approve_ix = spl_token::instruction::approve( - &spl_token::id(), + let approve_ix = spl_token_interface::instruction::approve( + &spl_token_interface::id(), &fee_payer_token_account, &delegate, &fee_payer, @@ -1145,8 +1145,8 @@ mod tests { // Test approve_checked instruction let mint = Pubkey::new_unique(); - let approve_checked_ix = spl_token::instruction::approve_checked( - &spl_token::id(), + let approve_checked_ix = spl_token_interface::instruction::approve_checked( + &spl_token_interface::id(), &fee_payer_token_account, &mint, &delegate, @@ -1181,8 +1181,8 @@ mod tests { let validator = TransactionValidator::new(fee_payer).unwrap(); - let burn_ix = spl_token_2022::instruction::burn( - &spl_token_2022::id(), + let burn_ix = spl_token_2022_interface::instruction::burn( + &spl_token_2022_interface::id(), &fee_payer_token_account, &mint, &fee_payer, @@ -1213,8 +1213,8 @@ mod tests { let validator = TransactionValidator::new(fee_payer).unwrap(); - let close_ix = spl_token_2022::instruction::close_account( - &spl_token_2022::id(), + let close_ix = spl_token_2022_interface::instruction::close_account( + &spl_token_2022_interface::id(), &fee_payer_token_account, &destination, &fee_payer, @@ -1244,8 +1244,8 @@ mod tests { let validator = TransactionValidator::new(fee_payer).unwrap(); - let approve_ix = spl_token_2022::instruction::approve( - &spl_token_2022::id(), + let approve_ix = spl_token_2022_interface::instruction::approve( + &spl_token_2022_interface::id(), &fee_payer_token_account, &delegate, &fee_payer, @@ -1268,8 +1268,8 @@ mod tests { let validator = TransactionValidator::new(fee_payer).unwrap(); - let approve_ix = spl_token_2022::instruction::approve( - &spl_token_2022::id(), + let approve_ix = spl_token_2022_interface::instruction::approve( + &spl_token_2022_interface::id(), &fee_payer_token_account, &delegate, &fee_payer, @@ -1286,8 +1286,8 @@ mod tests { // Test approve_checked instruction let mint = Pubkey::new_unique(); - let approve_checked_ix = spl_token_2022::instruction::approve_checked( - &spl_token_2022::id(), + let approve_checked_ix = spl_token_2022_interface::instruction::approve_checked( + &spl_token_2022_interface::id(), &fee_payer_token_account, &mint, &delegate, @@ -1343,7 +1343,7 @@ mod tests { #[tokio::test] #[serial] async fn test_fee_payer_policy_allocate() { - use solana_sdk::system_instruction::allocate; + use solana_system_interface::instruction::allocate; let fee_payer = Pubkey::new_unique(); @@ -1375,7 +1375,7 @@ mod tests { #[tokio::test] #[serial] async fn test_fee_payer_policy_nonce_initialize() { - use solana_sdk::system_instruction::create_nonce_account; + use solana_system_interface::instruction::create_nonce_account; let fee_payer = Pubkey::new_unique(); let nonce_account = Pubkey::new_unique(); @@ -1411,7 +1411,7 @@ mod tests { #[tokio::test] #[serial] async fn test_fee_payer_policy_nonce_advance() { - use solana_sdk::system_instruction::advance_nonce_account; + use solana_system_interface::instruction::advance_nonce_account; let fee_payer = Pubkey::new_unique(); let nonce_account = Pubkey::new_unique(); @@ -1444,7 +1444,7 @@ mod tests { #[tokio::test] #[serial] async fn test_fee_payer_policy_nonce_withdraw() { - use solana_sdk::system_instruction::withdraw_nonce_account; + use solana_system_interface::instruction::withdraw_nonce_account; let fee_payer = Pubkey::new_unique(); let nonce_account = Pubkey::new_unique(); @@ -1478,7 +1478,7 @@ mod tests { #[tokio::test] #[serial] async fn test_fee_payer_policy_nonce_authorize() { - use solana_sdk::system_instruction::authorize_nonce_account; + use solana_system_interface::instruction::authorize_nonce_account; let fee_payer = Pubkey::new_unique(); let nonce_account = Pubkey::new_unique(); diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 60bb985d..d057c996 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -48,14 +48,15 @@ path = "free_signing/main.rs" kora-lib = { path = "../crates/lib" } solana-sdk = { workspace = true } solana-commitment-config = { workspace = true } +solana-program-pack = { workspace = true } +solana-compute-budget-interface = { workspace = true } solana-message = { workspace = true } solana-system-interface = { workspace = true } 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" +spl-token-interface = { workspace = true } +spl-token-2022-interface = { workspace = true } +spl-associated-token-account-interface = { workspace = true } +spl-transfer-hook-interface = { version = "2.0.0" } bincode = { workspace = true } bs58 = { workspace = true } dotenv = { workspace = true } diff --git a/tests/adversarial/fee_payer_exploitation.rs b/tests/adversarial/fee_payer_exploitation.rs index e0c57030..6910d364 100644 --- a/tests/adversarial/fee_payer_exploitation.rs +++ b/tests/adversarial/fee_payer_exploitation.rs @@ -2,8 +2,8 @@ use crate::common::{assertions::RpcErrorAssertions, *}; use jsonrpsee::rpc_params; use solana_sdk::signer::Signer; use solana_system_interface::instruction::transfer; -use spl_associated_token_account::get_associated_token_address; -use spl_token::instruction as token_instruction; +use spl_associated_token_account_interface::address::get_associated_token_address; +use spl_token_interface::instruction as token_instruction; #[tokio::test] async fn test_fee_payer_as_sol_transfer_source() { @@ -63,7 +63,7 @@ async fn test_fee_payer_as_spl_transfer_source() { // Run the test transaction let large_token_transfer = token_instruction::transfer( - &spl_token::id(), + &spl_token_interface::id(), &fee_payer_token_account, &sender_token_account, &fee_payer_pubkey, diff --git a/tests/adversarial/token_states.rs b/tests/adversarial/token_states.rs index 0e31c3d6..26deb343 100644 --- a/tests/adversarial/token_states.rs +++ b/tests/adversarial/token_states.rs @@ -4,8 +4,8 @@ use solana_sdk::{ program_pack::Pack, signature::Keypair, signer::Signer, transaction::Transaction, }; use solana_system_interface::instruction::create_account; -use spl_associated_token_account::get_associated_token_address; -use spl_token::instruction as token_instruction; +use spl_associated_token_account_interface::address::get_associated_token_address; +use spl_token_interface::instruction as token_instruction; #[tokio::test] async fn test_frozen_token_account_as_fee_payment() { @@ -16,7 +16,7 @@ async fn test_frozen_token_account_as_fee_payment() { let rent = setup .rpc_client - .get_minimum_balance_for_rent_exemption(spl_token::state::Account::LEN) + .get_minimum_balance_for_rent_exemption(spl_token_interface::state::Account::LEN) .await .expect("Failed to get rent exemption"); @@ -24,12 +24,12 @@ async fn test_frozen_token_account_as_fee_payment() { &setup.sender_keypair.pubkey(), &frozen_token_account_keypair.pubkey(), rent, - spl_token::state::Account::LEN as u64, - &spl_token::id(), + spl_token_interface::state::Account::LEN as u64, + &spl_token_interface::id(), ); - let create_frozen_token_account_ix = spl_token::instruction::initialize_account( - &spl_token::id(), + let create_frozen_token_account_ix = spl_token_interface::instruction::initialize_account( + &spl_token_interface::id(), &frozen_token_account_keypair.pubkey(), &setup.usdc_mint.pubkey(), &setup.sender_keypair.pubkey(), @@ -37,7 +37,7 @@ async fn test_frozen_token_account_as_fee_payment() { .expect("Failed to create initialize account instruction"); let mint_tokens_ix = token_instruction::mint_to( - &spl_token::id(), + &spl_token_interface::id(), &setup.usdc_mint.pubkey(), &frozen_token_account_keypair.pubkey(), &setup.sender_keypair.pubkey(), @@ -47,7 +47,7 @@ async fn test_frozen_token_account_as_fee_payment() { .expect("Failed to create mint instruction"); let freeze_instruction = token_instruction::freeze_account( - &spl_token::id(), + &spl_token_interface::id(), &frozen_token_account_keypair.pubkey(), &setup.usdc_mint.pubkey(), &setup.sender_keypair.pubkey(), diff --git a/tests/fee_payer_policy/fee_payer_policy_violations.rs b/tests/fee_payer_policy/fee_payer_policy_violations.rs index dfab60ee..c1d5ccde 100644 --- a/tests/fee_payer_policy/fee_payer_policy_violations.rs +++ b/tests/fee_payer_policy/fee_payer_policy_violations.rs @@ -5,11 +5,11 @@ use solana_sdk::{ transaction::Transaction, }; use solana_system_interface::instruction::{create_account, transfer}; -use spl_associated_token_account::{ +use spl_associated_token_account_interface::address::{ get_associated_token_address, get_associated_token_address_with_program_id, }; -use spl_token::instruction as token_instruction; -use spl_token_2022::instruction as token_2022_instruction; +use spl_token_2022_interface::instruction as token_2022_instruction; +use spl_token_interface::instruction as token_instruction; #[tokio::test] async fn test_sol_transfer_policy_violation() { @@ -138,7 +138,7 @@ async fn test_spl_transfer_policy_violation() { .expect("Failed to mint tokens"); let spl_transfer_instruction = token_instruction::transfer( - &spl_token::id(), + &spl_token_interface::id(), &fee_payer_token_account.pubkey(), &recipient_token_account, &fee_payer_pubkey, @@ -181,7 +181,7 @@ async fn test_token2022_transfer_policy_violation() { let recipient_token_2022_account = get_associated_token_address_with_program_id( &recipient_pubkey, &setup.fee_payer_policy_mint_2022.pubkey(), - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); setup @@ -193,7 +193,7 @@ async fn test_token2022_transfer_policy_violation() { .expect("Failed to mint tokens"); let token_2022_transfer_instruction = token_2022_instruction::transfer_checked( - &spl_token_2022::id(), + &spl_token_2022_interface::id(), &fee_payer_token_2022_account.pubkey(), &setup.fee_payer_policy_mint_2022.pubkey(), &recipient_token_2022_account, @@ -241,7 +241,7 @@ async fn test_burn_policy_violation() { .expect("Failed to mint SPL"); let burn_instruction = token_instruction::burn( - &spl_token::id(), + &spl_token_interface::id(), &fee_payer_token_account.pubkey(), &setup.fee_payer_policy_mint.pubkey(), &fee_payer_pubkey, @@ -280,7 +280,7 @@ async fn test_close_account_policy_violation() { .expect("Failed to create token account"); let close_account_instruction = token_instruction::close_account( - &spl_token::id(), + &spl_token_interface::id(), &fee_payer_token_account.pubkey(), &setup.recipient_pubkey, &setup.fee_payer_keypair.pubkey(), @@ -325,7 +325,7 @@ async fn test_approve_policy_violation() { .expect("Failed to mint tokens"); let approve_instruction = token_instruction::approve( - &spl_token::id(), + &spl_token_interface::id(), &fee_payer_token_account.pubkey(), &recipient_pubkey, &fee_payer_pubkey, @@ -668,8 +668,8 @@ async fn test_thaw_account_policy_violation() { .expect("Failed to create token account"); // Freeze the account first (directly on-chain, bypassing Kora validator) - let freeze_ix = spl_token::instruction::freeze_account( - &spl_token::id(), + let freeze_ix = spl_token_interface::instruction::freeze_account( + &spl_token_interface::id(), &fee_payer_token_account.pubkey(), &setup.fee_payer_policy_mint.pubkey(), &fee_payer_pubkey, @@ -726,8 +726,8 @@ async fn test_thaw_account_token2022_policy_violation() { .expect("Failed to create token account"); // Freeze the account first (directly on-chain, bypassing Kora validator) - let freeze_ix = spl_token_2022::instruction::freeze_account( - &spl_token_2022::id(), + let freeze_ix = spl_token_2022_interface::instruction::freeze_account( + &spl_token_2022_interface::id(), &fee_payer_token_2022_account.pubkey(), &setup.fee_payer_policy_mint_2022.pubkey(), &fee_payer_pubkey, diff --git a/tests/payment_address/payment_address_legacy_tests.rs b/tests/payment_address/payment_address_legacy_tests.rs index 41b2a471..7827bad2 100644 --- a/tests/payment_address/payment_address_legacy_tests.rs +++ b/tests/payment_address/payment_address_legacy_tests.rs @@ -1,12 +1,12 @@ use crate::common::*; use jsonrpsee::rpc_params; -use kora_lib::token::{TokenInterface, TokenProgram}; +use kora_lib::token::{spl_token::TokenProgram, TokenInterface}; use solana_sdk::{ pubkey::Pubkey, signature::{Keypair, Signer}, }; -use spl_associated_token_account::{ - get_associated_token_address, instruction::create_associated_token_account_idempotent, +use spl_associated_token_account_interface::{ + address::get_associated_token_address, instruction::create_associated_token_account_idempotent, }; use std::str::FromStr; @@ -68,7 +68,7 @@ async fn test_sign_transaction_if_paid_with_wrong_destination_legacy() { &fee_payer.pubkey(), &wrong_destination.pubkey(), &test_mint, - &spl_token::id(), + &spl_token_interface::id(), ); let token_interface = TokenProgram::new(); diff --git a/tests/payment_address/payment_address_multi_payment_tests.rs b/tests/payment_address/payment_address_multi_payment_tests.rs index a0654bad..a5452ea4 100644 --- a/tests/payment_address/payment_address_multi_payment_tests.rs +++ b/tests/payment_address/payment_address_multi_payment_tests.rs @@ -1,8 +1,8 @@ use crate::common::*; use jsonrpsee::rpc_params; -use kora_lib::token::{TokenInterface, TokenProgram}; +use kora_lib::token::{spl_token::TokenProgram, TokenInterface}; use solana_sdk::{pubkey::Pubkey, signature::Signer}; -use spl_associated_token_account::get_associated_token_address; +use spl_associated_token_account_interface::address::get_associated_token_address; use std::str::FromStr; #[tokio::test] diff --git a/tests/payment_address/payment_address_v0_lut_tests.rs b/tests/payment_address/payment_address_v0_lut_tests.rs index aa4e5846..8689385c 100644 --- a/tests/payment_address/payment_address_v0_lut_tests.rs +++ b/tests/payment_address/payment_address_v0_lut_tests.rs @@ -5,7 +5,7 @@ use solana_sdk::{ pubkey::Pubkey, signature::{Keypair, Signer}, }; -use spl_associated_token_account::instruction::create_associated_token_account_idempotent; +use spl_associated_token_account_interface::instruction::create_associated_token_account_idempotent; use std::str::FromStr; #[tokio::test] @@ -61,7 +61,7 @@ async fn test_sign_transaction_if_paid_with_wrong_destination_v0_with_lookup() { &fee_payer_keypair.pubkey(), &wrong_destination.pubkey(), &test_mint, - &spl_token::id(), + &spl_token_interface::id(), ); let fee_payer = FeePayerTestHelper::get_fee_payer_pubkey(); diff --git a/tests/payment_address/payment_address_v0_tests.rs b/tests/payment_address/payment_address_v0_tests.rs index 85cec0c8..fd06c11a 100644 --- a/tests/payment_address/payment_address_v0_tests.rs +++ b/tests/payment_address/payment_address_v0_tests.rs @@ -1,12 +1,12 @@ use crate::common::*; use jsonrpsee::rpc_params; -use kora_lib::token::{TokenInterface, TokenProgram}; +use kora_lib::token::{spl_token::TokenProgram, TokenInterface}; use solana_sdk::{ pubkey::Pubkey, signature::{Keypair, Signer}, }; -use spl_associated_token_account::{ - get_associated_token_address, instruction::create_associated_token_account_idempotent, +use spl_associated_token_account_interface::{ + address::get_associated_token_address, instruction::create_associated_token_account_idempotent, }; use std::str::FromStr; use tests::common::helpers::get_fee_for_default_transaction_in_usdc; @@ -69,7 +69,7 @@ async fn test_sign_transaction_if_paid_with_wrong_destination_v0() { &fee_payer.pubkey(), &wrong_destination.pubkey(), &test_mint, - &spl_token::id(), + &spl_token_interface::id(), ); let token_interface = TokenProgram::new(); diff --git a/tests/rpc/fee_estimation.rs b/tests/rpc/fee_estimation.rs index 2d5b280e..aaa0c094 100644 --- a/tests/rpc/fee_estimation.rs +++ b/tests/rpc/fee_estimation.rs @@ -4,7 +4,7 @@ use solana_sdk::{ program_pack::Pack, pubkey::Pubkey, signature::Keypair, signer::Signer, transaction::Transaction, }; -use spl_associated_token_account::get_associated_token_address; +use spl_associated_token_account_interface::address::get_associated_token_address; #[tokio::test] async fn test_estimate_transaction_fee_legacy() { @@ -299,7 +299,7 @@ async fn test_estimate_fee_comprehensive_with_token_accounts_creation() { let token_account_rent = ctx .client .rpc_client - .get_minimum_balance_for_rent_exemption(spl_token::state::Account::LEN) + .get_minimum_balance_for_rent_exemption(spl_token_interface::state::Account::LEN) .await .expect("Failed to get rent exemption amount"); @@ -363,15 +363,15 @@ async fn test_estimate_fee_with_spl_token_transfer_from_fee_payer() { let sender = SenderTestHelper::get_test_sender_keypair(); let create_recipient_ata_ix = - spl_associated_token_account::instruction::create_associated_token_account_idempotent( + spl_associated_token_account_interface::instruction::create_associated_token_account_idempotent( &sender.pubkey(), // payer &recipient, // owner &usdc_mint, // mint - &spl_token::id(), + &spl_token_interface::id(), ); - let mint_instruction = spl_token::instruction::mint_to( - &spl_token::id(), + let mint_instruction = spl_token_interface::instruction::mint_to( + &spl_token_interface::id(), &usdc_mint, &fee_payer_ata, &sender.pubkey(), // mint authority is sender diff --git a/tests/src/common/extension_helpers.rs b/tests/src/common/extension_helpers.rs index 271ee40d..4ade9346 100644 --- a/tests/src/common/extension_helpers.rs +++ b/tests/src/common/extension_helpers.rs @@ -8,7 +8,7 @@ use solana_sdk::{ transaction::Transaction, }; use solana_system_interface::instruction::create_account; -use spl_token_2022::{ +use spl_token_2022_interface::{ extension::{interest_bearing_mint::instruction::initialize, transfer_hook, ExtensionType}, instruction as token_2022_instruction, state::{Account as Token2022Account, Mint as Token2022Mint}, @@ -45,14 +45,18 @@ impl ExtensionHelpers { &mint_keypair.pubkey(), rent, space as u64, - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); - let initialize_interest_bearing_instruction = - initialize(&spl_token_2022::id(), &mint_keypair.pubkey(), Some(payer.pubkey()), 10)?; + let initialize_interest_bearing_instruction = initialize( + &spl_token_2022_interface::id(), + &mint_keypair.pubkey(), + Some(payer.pubkey()), + 10, + )?; let initialize_mint_instruction = token_2022_instruction::initialize_mint2( - &spl_token_2022::id(), + &spl_token_2022_interface::id(), &mint_keypair.pubkey(), &payer.pubkey(), Some(&payer.pubkey()), @@ -102,20 +106,20 @@ impl ExtensionHelpers { &token_account_keypair.pubkey(), rent, account_space as u64, - &spl_token_2022::id(), + &spl_token_2022_interface::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(), + spl_token_2022_interface::extension::memo_transfer::instruction::enable_required_transfer_memos( + &spl_token_2022_interface::id(), &token_account_keypair.pubkey(), &owner.pubkey(), &[&owner.pubkey()], )?; let initialize_account_instruction = token_2022_instruction::initialize_account3( - &spl_token_2022::id(), + &spl_token_2022_interface::id(), &token_account_keypair.pubkey(), mint, &owner.pubkey(), @@ -150,7 +154,7 @@ impl ExtensionHelpers { }); let instruction = token_2022_instruction::mint_to( - &spl_token_2022::id(), + &spl_token_2022_interface::id(), mint, token_account, &mint_authority.pubkey(), @@ -193,19 +197,19 @@ impl ExtensionHelpers { &mint_keypair.pubkey(), rent, space as u64, - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); // Initialize the transfer hook extension let initialize_hook_instruction = transfer_hook::instruction::initialize( - &spl_token_2022::id(), + &spl_token_2022_interface::id(), &mint_keypair.pubkey(), Some(payer.pubkey()), Some(*hook_program_id), )?; let initialize_mint_instruction = token_2022_instruction::initialize_mint2( - &spl_token_2022::id(), + &spl_token_2022_interface::id(), &mint_keypair.pubkey(), &payer.pubkey(), Some(&payer.pubkey()), @@ -262,7 +266,7 @@ impl ExtensionHelpers { // Add the system program account which is needed for PDA creation initialize_instruction .accounts - .push(AccountMeta::new_readonly(solana_sdk::system_program::id(), false)); + .push(AccountMeta::new_readonly(solana_system_interface::program::id(), false)); let recent_blockhash = rpc_client.get_latest_blockhash().await?; let transaction = Transaction::new_signed_with_payer( diff --git a/tests/src/common/lookup_tables.rs b/tests/src/common/lookup_tables.rs index b823effb..6f97b3d6 100644 --- a/tests/src/common/lookup_tables.rs +++ b/tests/src/common/lookup_tables.rs @@ -130,9 +130,12 @@ impl LookupTableHelper { rpc_client: Arc, authority: &Keypair, ) -> Result { - let allowed_lookup_table = - Self::create_lookup_table(rpc_client, authority, vec![solana_sdk::system_program::ID]) - .await?; + let allowed_lookup_table = Self::create_lookup_table( + rpc_client, + authority, + vec![solana_system_interface::program::ID], + ) + .await?; Ok(allowed_lookup_table) } @@ -157,7 +160,7 @@ impl LookupTableHelper { ) -> Result { let usdc_mint = USDCMintTestHelper::get_test_usdc_mint_pubkey(); - let addresses = vec![usdc_mint, spl_token::ID]; + let addresses = vec![usdc_mint, spl_token_interface::ID]; Self::create_lookup_table(rpc_client, authority, addresses).await } diff --git a/tests/src/common/setup.rs b/tests/src/common/setup.rs index 9dc80051..14ab1746 100644 --- a/tests/src/common/setup.rs +++ b/tests/src/common/setup.rs @@ -1,22 +1,22 @@ use anyhow::Result; use solana_client::nonblocking::rpc_client::RpcClient; +use solana_commitment_config::CommitmentConfig; +use solana_program_pack::Pack; use solana_sdk::{ - commitment_config::CommitmentConfig, native_token::LAMPORTS_PER_SOL, - program_pack::Pack, pubkey::Pubkey, signature::{Keypair, Signer}, transaction::Transaction, }; -use spl_associated_token_account::{ +use spl_associated_token_account_interface::address::{ get_associated_token_address, get_associated_token_address_with_program_id, }; -use spl_token::instruction as token_instruction; -use spl_token_2022::{ +use spl_token_2022_interface::{ extension::{transfer_fee::instruction::initialize_transfer_fee_config, ExtensionType}, instruction as token_2022_instruction, state::Mint as Token2022Mint, }; +use spl_token_interface::instruction as token_instruction; use std::sync::Arc; use crate::common::{ @@ -225,19 +225,19 @@ impl TestAccountSetup { let rent = self .rpc_client - .get_minimum_balance_for_rent_exemption(spl_token::state::Mint::LEN) + .get_minimum_balance_for_rent_exemption(spl_token_interface::state::Mint::LEN) .await?; - let create_account_instruction = solana_sdk::system_instruction::create_account( + let create_account_instruction = solana_system_interface::instruction::create_account( &self.sender_keypair.pubkey(), &self.usdc_mint.pubkey(), rent, - spl_token::state::Mint::LEN as u64, - &spl_token::id(), + spl_token_interface::state::Mint::LEN as u64, + &spl_token_interface::id(), ); - let initialize_mint_instruction = spl_token::instruction::initialize_mint2( - &spl_token::id(), + let initialize_mint_instruction = spl_token_interface::instruction::initialize_mint2( + &spl_token_interface::id(), &self.usdc_mint.pubkey(), &self.sender_keypair.pubkey(), Some(&self.sender_keypair.pubkey()), @@ -266,22 +266,22 @@ impl TestAccountSetup { 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::< + let space = spl_token_2022_interface::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( + let create_account_instruction = solana_system_interface::instruction::create_account( &self.sender_keypair.pubkey(), &self.usdc_mint_2022.pubkey(), rent, space as u64, - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); let initialize_transfer_fee_config_instruction = initialize_transfer_fee_config( - &spl_token_2022::id(), + &spl_token_2022_interface::id(), &self.usdc_mint_2022.pubkey(), Some(&self.sender_keypair.pubkey()), Some(&self.sender_keypair.pubkey()), @@ -290,7 +290,7 @@ impl TestAccountSetup { )?; let initialize_mint_instruction = token_2022_instruction::initialize_mint2( - &spl_token_2022::id(), + &spl_token_2022_interface::id(), &self.usdc_mint_2022.pubkey(), &self.sender_keypair.pubkey(), Some(&self.sender_keypair.pubkey()), @@ -332,67 +332,67 @@ impl TestAccountSetup { 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(), + &spl_token_2022_interface::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(), + &spl_token_2022_interface::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(), + &spl_token_2022_interface::id(), ); // Create regular SPL Token accounts let create_associated_token_account_instruction = - spl_associated_token_account::instruction::create_associated_token_account_idempotent( + spl_associated_token_account_interface::instruction::create_associated_token_account_idempotent( &self.sender_keypair.pubkey(), &self.sender_keypair.pubkey(), &self.usdc_mint.pubkey(), - &spl_token::id(), + &spl_token_interface::id(), ); let create_associated_token_account_instruction_recipient = - spl_associated_token_account::instruction::create_associated_token_account_idempotent( + spl_associated_token_account_interface::instruction::create_associated_token_account_idempotent( &self.sender_keypair.pubkey(), &self.recipient_pubkey, &self.usdc_mint.pubkey(), - &spl_token::id(), + &spl_token_interface::id(), ); let create_associated_token_account_instruction_fee_payer = - spl_associated_token_account::instruction::create_associated_token_account_idempotent( + spl_associated_token_account_interface::instruction::create_associated_token_account_idempotent( &self.sender_keypair.pubkey(), &self.fee_payer_keypair.pubkey(), &self.usdc_mint.pubkey(), - &spl_token::id(), + &spl_token_interface::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( + spl_associated_token_account_interface::instruction::create_associated_token_account_idempotent( &self.sender_keypair.pubkey(), &self.sender_keypair.pubkey(), &self.usdc_mint_2022.pubkey(), - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); let create_token_2022_account_instruction_recipient = - spl_associated_token_account::instruction::create_associated_token_account_idempotent( + spl_associated_token_account_interface::instruction::create_associated_token_account_idempotent( &self.sender_keypair.pubkey(), &self.recipient_pubkey, &self.usdc_mint_2022.pubkey(), - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); let create_token_2022_account_instruction_fee_payer = - spl_associated_token_account::instruction::create_associated_token_account_idempotent( + spl_associated_token_account_interface::instruction::create_associated_token_account_idempotent( &self.sender_keypair.pubkey(), &self.fee_payer_keypair.pubkey(), &self.usdc_mint_2022.pubkey(), - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); let recent_blockhash = self.rpc_client.get_latest_blockhash().await?; @@ -437,7 +437,7 @@ impl TestAccountSetup { pub async fn mint_tokens_to_account(&self, token_account: &Pubkey, amount: u64) -> Result<()> { let instruction = token_instruction::mint_to( - &spl_token::id(), + &spl_token_interface::id(), &self.usdc_mint.pubkey(), token_account, &self.sender_keypair.pubkey(), @@ -463,7 +463,7 @@ impl TestAccountSetup { amount: u64, ) -> Result<()> { let instruction = token_2022_instruction::mint_to( - &spl_token_2022::id(), + &spl_token_2022_interface::id(), &self.usdc_mint_2022.pubkey(), token_account, &self.sender_keypair.pubkey(), @@ -497,19 +497,19 @@ impl TestAccountSetup { let rent = self .rpc_client - .get_minimum_balance_for_rent_exemption(spl_token::state::Mint::LEN) + .get_minimum_balance_for_rent_exemption(spl_token_interface::state::Mint::LEN) .await?; - let create_account_instruction = solana_sdk::system_instruction::create_account( + let create_account_instruction = solana_system_interface::instruction::create_account( &self.fee_payer_keypair.pubkey(), &self.fee_payer_policy_mint.pubkey(), rent, - spl_token::state::Mint::LEN as u64, - &spl_token::id(), + spl_token_interface::state::Mint::LEN as u64, + &spl_token_interface::id(), ); - let initialize_mint_instruction = spl_token::instruction::initialize_mint2( - &spl_token::id(), + let initialize_mint_instruction = spl_token_interface::instruction::initialize_mint2( + &spl_token_interface::id(), &self.fee_payer_policy_mint.pubkey(), &self.fee_payer_keypair.pubkey(), Some(&self.fee_payer_keypair.pubkey()), @@ -537,22 +537,22 @@ impl TestAccountSetup { let decimals = USDCMintTestHelper::get_test_usdc_mint_decimals(); - let space = spl_token_2022::extension::ExtensionType::try_calculate_account_len::< + let space = spl_token_2022_interface::extension::ExtensionType::try_calculate_account_len::< Token2022Mint, >(&[])?; let rent = self.rpc_client.get_minimum_balance_for_rent_exemption(space).await?; - let create_account_instruction = solana_sdk::system_instruction::create_account( + let create_account_instruction = solana_system_interface::instruction::create_account( &self.fee_payer_keypair.pubkey(), &self.fee_payer_policy_mint_2022.pubkey(), rent, space as u64, - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); let initialize_mint_instruction = token_2022_instruction::initialize_mint2( - &spl_token_2022::id(), + &spl_token_2022_interface::id(), &self.fee_payer_policy_mint_2022.pubkey(), &self.fee_payer_keypair.pubkey(), Some(&self.fee_payer_keypair.pubkey()), @@ -594,67 +594,67 @@ impl TestAccountSetup { let sender_token_2022_account = get_associated_token_address_with_program_id( &self.sender_keypair.pubkey(), &self.fee_payer_policy_mint_2022.pubkey(), - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); let recipient_token_2022_account = get_associated_token_address_with_program_id( &self.recipient_pubkey, &self.fee_payer_policy_mint_2022.pubkey(), - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); let fee_payer_token_2022_account = get_associated_token_address_with_program_id( &self.fee_payer_keypair.pubkey(), &self.fee_payer_policy_mint_2022.pubkey(), - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); // Create regular SPL Token accounts let create_associated_token_account_instruction = - spl_associated_token_account::instruction::create_associated_token_account_idempotent( + spl_associated_token_account_interface::instruction::create_associated_token_account_idempotent( &self.fee_payer_keypair.pubkey(), &self.sender_keypair.pubkey(), &self.fee_payer_policy_mint.pubkey(), - &spl_token::id(), + &spl_token_interface::id(), ); let create_associated_token_account_instruction_recipient = - spl_associated_token_account::instruction::create_associated_token_account_idempotent( + spl_associated_token_account_interface::instruction::create_associated_token_account_idempotent( &self.fee_payer_keypair.pubkey(), &self.recipient_pubkey, &self.fee_payer_policy_mint.pubkey(), - &spl_token::id(), + &spl_token_interface::id(), ); let create_associated_token_account_instruction_fee_payer = - spl_associated_token_account::instruction::create_associated_token_account_idempotent( + spl_associated_token_account_interface::instruction::create_associated_token_account_idempotent( &self.fee_payer_keypair.pubkey(), &self.fee_payer_keypair.pubkey(), &self.fee_payer_policy_mint.pubkey(), - &spl_token::id(), + &spl_token_interface::id(), ); // Create Token 2022 accounts let create_token_2022_account_instruction_sender = - spl_associated_token_account::instruction::create_associated_token_account_idempotent( + spl_associated_token_account_interface::instruction::create_associated_token_account_idempotent( &self.fee_payer_keypair.pubkey(), &self.sender_keypair.pubkey(), &self.fee_payer_policy_mint_2022.pubkey(), - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); let create_token_2022_account_instruction_recipient = - spl_associated_token_account::instruction::create_associated_token_account_idempotent( + spl_associated_token_account_interface::instruction::create_associated_token_account_idempotent( &self.fee_payer_keypair.pubkey(), &self.recipient_pubkey, &self.fee_payer_policy_mint_2022.pubkey(), - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); let create_token_2022_account_instruction_fee_payer = - spl_associated_token_account::instruction::create_associated_token_account_idempotent( + spl_associated_token_account_interface::instruction::create_associated_token_account_idempotent( &self.fee_payer_keypair.pubkey(), &self.fee_payer_keypair.pubkey(), &self.fee_payer_policy_mint_2022.pubkey(), - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); let recent_blockhash = self.rpc_client.get_latest_blockhash().await?; @@ -703,7 +703,7 @@ impl TestAccountSetup { amount: u64, ) -> Result<()> { let instruction = token_instruction::mint_to( - &spl_token::id(), + &spl_token_interface::id(), &self.fee_payer_policy_mint.pubkey(), token_account, &self.fee_payer_keypair.pubkey(), @@ -729,7 +729,7 @@ impl TestAccountSetup { amount: u64, ) -> Result<()> { let instruction = token_2022_instruction::mint_to( - &spl_token_2022::id(), + &spl_token_2022_interface::id(), &self.fee_payer_policy_mint_2022.pubkey(), token_account, &self.fee_payer_keypair.pubkey(), @@ -755,19 +755,19 @@ impl TestAccountSetup { let rent = self .rpc_client - .get_minimum_balance_for_rent_exemption(spl_token::state::Account::LEN) + .get_minimum_balance_for_rent_exemption(spl_token_interface::state::Account::LEN) .await?; - let create_account_ix = solana_sdk::system_instruction::create_account( + let create_account_ix = solana_system_interface::instruction::create_account( &self.fee_payer_keypair.pubkey(), &token_account.pubkey(), rent, - spl_token::state::Account::LEN as u64, - &spl_token::id(), + spl_token_interface::state::Account::LEN as u64, + &spl_token_interface::id(), ); - let initialize_account_ix = spl_token::instruction::initialize_account( - &spl_token::id(), + let initialize_account_ix = spl_token_interface::instruction::initialize_account( + &spl_token_interface::id(), &token_account.pubkey(), mint, &self.fee_payer_keypair.pubkey(), @@ -791,19 +791,19 @@ impl TestAccountSetup { let rent = self .rpc_client - .get_minimum_balance_for_rent_exemption(spl_token_2022::state::Account::LEN) + .get_minimum_balance_for_rent_exemption(spl_token_2022_interface::state::Account::LEN) .await?; - let create_account_ix = solana_sdk::system_instruction::create_account( + let create_account_ix = solana_system_interface::instruction::create_account( &self.fee_payer_keypair.pubkey(), &token_account.pubkey(), rent, - spl_token_2022::state::Account::LEN as u64, - &spl_token_2022::id(), + spl_token_2022_interface::state::Account::LEN as u64, + &spl_token_2022_interface::id(), ); - let initialize_account_ix = spl_token_2022::instruction::initialize_account( - &spl_token_2022::id(), + let initialize_account_ix = spl_token_2022_interface::instruction::initialize_account( + &spl_token_2022_interface::id(), &token_account.pubkey(), mint, &self.fee_payer_keypair.pubkey(), diff --git a/tests/src/common/transaction.rs b/tests/src/common/transaction.rs index 90a7cd83..b29e0e85 100644 --- a/tests/src/common/transaction.rs +++ b/tests/src/common/transaction.rs @@ -1,26 +1,26 @@ use anyhow::Result; use base64::{engine::general_purpose::STANDARD, Engine as _}; use kora_lib::{ - token::{Token2022Program, TokenInterface, TokenProgram}, + token::{spl_token::TokenProgram, spl_token_2022::Token2022Program, TokenInterface}, transaction::TransactionUtil, }; use solana_address_lookup_table_interface::state::AddressLookupTable; use solana_client::nonblocking::rpc_client::RpcClient; +use solana_commitment_config::CommitmentConfig; +use solana_compute_budget_interface::ComputeBudgetInstruction; use solana_message::{ v0::Message as V0Message, AddressLookupTableAccount, Message, VersionedMessage, }; +use solana_program_pack::Pack; use solana_sdk::{ - commitment_config::CommitmentConfig, - compute_budget::ComputeBudgetInstruction, instruction::Instruction, - program_pack::Pack, pubkey::Pubkey, signature::{Keypair, Signature}, signer::Signer, transaction::VersionedTransaction, }; use solana_system_interface::instruction::{create_account, transfer}; -use spl_associated_token_account::{ +use spl_associated_token_account_interface::address::{ get_associated_token_address, get_associated_token_address_with_program_id, }; use std::sync::Arc; @@ -165,8 +165,8 @@ impl TransactionBuilder { let from_ata = get_associated_token_address(from_authority, token_mint); let to_ata = get_associated_token_address(to_pubkey, token_mint); - let instruction = spl_token::instruction::transfer( - &spl_token::ID, + let instruction = spl_token_interface::instruction::transfer( + &spl_token_interface::ID, &from_ata, &to_ata, from_authority, @@ -187,8 +187,8 @@ impl TransactionBuilder { from_authority: &Pubkey, amount: u64, ) -> Self { - let instruction = spl_token::instruction::transfer( - &spl_token::ID, + let instruction = spl_token_interface::instruction::transfer( + &spl_token_interface::ID, from_token_account, to_token_account, from_authority, @@ -213,8 +213,8 @@ impl TransactionBuilder { let from_ata = get_associated_token_address(from_authority, token_mint); let to_ata = get_associated_token_address(to_pubkey, token_mint); - let instruction = spl_token::instruction::transfer_checked( - &spl_token::ID, + let instruction = spl_token_interface::instruction::transfer_checked( + &spl_token_interface::ID, &from_ata, token_mint, &to_ata, @@ -254,12 +254,12 @@ impl TransactionBuilder { let from_ata = get_associated_token_address_with_program_id( from_authority, token_mint, - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); let to_ata = get_associated_token_address_with_program_id( to_pubkey, token_mint, - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); let token_interface = Token2022Program::new(); @@ -307,11 +307,11 @@ impl TransactionBuilder { /// Add ATA creation instruction for SPL Token pub fn with_create_ata(mut self, mint: &Pubkey, owner: &Pubkey) -> Self { let instruction = - spl_associated_token_account::instruction::create_associated_token_account( + spl_associated_token_account_interface::instruction::create_associated_token_account( &self.fee_payer.expect("Fee payer must be set before creating ATA"), owner, mint, - &spl_token::id(), + &spl_token_interface::id(), ); self.instructions.push(instruction); self @@ -320,11 +320,11 @@ impl TransactionBuilder { /// Add ATA creation instruction for Token2022 pub fn with_create_token2022_ata(mut self, mint: &Pubkey, owner: &Pubkey) -> Self { let instruction = - spl_associated_token_account::instruction::create_associated_token_account( + spl_associated_token_account_interface::instruction::create_associated_token_account( &self.fee_payer.expect("Fee payer must be set before creating ATA"), owner, mint, - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); self.instructions.push(instruction); self @@ -343,13 +343,13 @@ impl TransactionBuilder { &self.fee_payer.expect("Fee payer must be set"), &account.pubkey(), rent_lamports, - spl_token::state::Account::LEN as u64, - &spl_token::id(), + spl_token_interface::state::Account::LEN as u64, + &spl_token_interface::id(), ); // Initialize account instruction - let init_instruction = spl_token::instruction::initialize_account3( - &spl_token::id(), + let init_instruction = spl_token_interface::instruction::initialize_account3( + &spl_token_interface::id(), &account.pubkey(), mint, owner, @@ -364,18 +364,26 @@ impl TransactionBuilder { /// Add SPL token revoke instruction pub fn with_spl_revoke(mut self, token_account: &Pubkey, owner: &Pubkey) -> Self { - let instruction = - spl_token::instruction::revoke(&spl_token::id(), token_account, owner, &[]) - .expect("Failed to create revoke instruction"); + let instruction = spl_token_interface::instruction::revoke( + &spl_token_interface::id(), + token_account, + owner, + &[], + ) + .expect("Failed to create revoke instruction"); self.instructions.push(instruction); self } /// Add Token2022 revoke instruction pub fn with_token2022_revoke(mut self, token_account: &Pubkey, owner: &Pubkey) -> Self { - let instruction = - spl_token_2022::instruction::revoke(&spl_token_2022::id(), token_account, owner, &[]) - .expect("Failed to create Token2022 revoke instruction"); + let instruction = spl_token_2022_interface::instruction::revoke( + &spl_token_2022_interface::id(), + token_account, + owner, + &[], + ) + .expect("Failed to create Token2022 revoke instruction"); self.instructions.push(instruction); self } @@ -385,11 +393,11 @@ impl TransactionBuilder { mut self, account: &Pubkey, new_authority: Option<&Pubkey>, - authority_type: spl_token::instruction::AuthorityType, + authority_type: spl_token_interface::instruction::AuthorityType, current_authority: &Pubkey, ) -> Self { - let instruction = spl_token::instruction::set_authority( - &spl_token::id(), + let instruction = spl_token_interface::instruction::set_authority( + &spl_token_interface::id(), account, new_authority, authority_type, @@ -406,11 +414,11 @@ impl TransactionBuilder { mut self, account: &Pubkey, new_authority: Option<&Pubkey>, - authority_type: spl_token_2022::instruction::AuthorityType, + authority_type: spl_token_2022_interface::instruction::AuthorityType, current_authority: &Pubkey, ) -> Self { - let instruction = spl_token_2022::instruction::set_authority( - &spl_token_2022::id(), + let instruction = spl_token_2022_interface::instruction::set_authority( + &spl_token_2022_interface::id(), account, new_authority, authority_type, @@ -430,8 +438,8 @@ impl TransactionBuilder { mint_authority: &Pubkey, amount: u64, ) -> Self { - let instruction = spl_token::instruction::mint_to( - &spl_token::id(), + let instruction = spl_token_interface::instruction::mint_to( + &spl_token_interface::id(), mint, destination, mint_authority, @@ -451,8 +459,8 @@ impl TransactionBuilder { mint_authority: &Pubkey, amount: u64, ) -> Self { - let instruction = spl_token_2022::instruction::mint_to( - &spl_token_2022::id(), + let instruction = spl_token_2022_interface::instruction::mint_to( + &spl_token_2022_interface::id(), mint, destination, mint_authority, @@ -471,8 +479,8 @@ impl TransactionBuilder { mint: &Pubkey, freeze_authority: &Pubkey, ) -> Self { - let instruction = spl_token::instruction::freeze_account( - &spl_token::id(), + let instruction = spl_token_interface::instruction::freeze_account( + &spl_token_interface::id(), token_account, mint, freeze_authority, @@ -490,8 +498,8 @@ impl TransactionBuilder { mint: &Pubkey, freeze_authority: &Pubkey, ) -> Self { - let instruction = spl_token_2022::instruction::freeze_account( - &spl_token_2022::id(), + let instruction = spl_token_2022_interface::instruction::freeze_account( + &spl_token_2022_interface::id(), token_account, mint, freeze_authority, @@ -509,8 +517,8 @@ impl TransactionBuilder { mint: &Pubkey, freeze_authority: &Pubkey, ) -> Self { - let instruction = spl_token::instruction::thaw_account( - &spl_token::id(), + let instruction = spl_token_interface::instruction::thaw_account( + &spl_token_interface::id(), token_account, mint, freeze_authority, @@ -528,8 +536,8 @@ impl TransactionBuilder { mint: &Pubkey, freeze_authority: &Pubkey, ) -> Self { - let instruction = spl_token_2022::instruction::thaw_account( - &spl_token_2022::id(), + let instruction = spl_token_2022_interface::instruction::thaw_account( + &spl_token_2022_interface::id(), token_account, mint, freeze_authority, diff --git a/tests/tokens/token_2022_extensions_test.rs b/tests/tokens/token_2022_extensions_test.rs index 27886df2..4c87e5b6 100644 --- a/tests/tokens/token_2022_extensions_test.rs +++ b/tests/tokens/token_2022_extensions_test.rs @@ -11,7 +11,7 @@ use solana_sdk::{ signature::{Keypair, Signer}, transaction::Transaction, }; -use spl_associated_token_account::get_associated_token_address_with_program_id; +use spl_associated_token_account_interface::address::get_associated_token_address_with_program_id; use std::str::FromStr; #[tokio::test] @@ -40,24 +40,24 @@ async fn test_blocked_memo_transfer_extension() { let fee_payer_token_account = get_associated_token_address_with_program_id( &fee_payer.pubkey(), &mint_keypair.pubkey(), - &spl_token_2022::id(), + &spl_token_2022_interface::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( + spl_associated_token_account_interface::instruction::create_associated_token_account_idempotent( &fee_payer.pubkey(), &fee_payer.pubkey(), &mint_keypair.pubkey(), - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); let create_fee_payer_payment_ata_instruction = - spl_associated_token_account::instruction::create_associated_token_account_idempotent( + spl_associated_token_account_interface::instruction::create_associated_token_account_idempotent( &fee_payer.pubkey(), &fee_payer.pubkey(), &mint_keypair.pubkey(), - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); let recent_blockhash = ctx.rpc_client().get_latest_blockhash().await.unwrap(); @@ -143,29 +143,29 @@ async fn test_blocked_interest_bearing_config_extension() { let sender_ata = get_associated_token_address_with_program_id( &sender.pubkey(), &mint_keypair.pubkey(), - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); let fee_payer_ata = get_associated_token_address_with_program_id( &fee_payer.pubkey(), &mint_keypair.pubkey(), - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); let create_sender_ata_instruction = - spl_associated_token_account::instruction::create_associated_token_account_idempotent( + spl_associated_token_account_interface::instruction::create_associated_token_account_idempotent( &fee_payer.pubkey(), &sender.pubkey(), &mint_keypair.pubkey(), - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); let create_fee_payer_ata_instruction = - spl_associated_token_account::instruction::create_associated_token_account_idempotent( + spl_associated_token_account_interface::instruction::create_associated_token_account_idempotent( &fee_payer.pubkey(), &fee_payer.pubkey(), &mint_keypair.pubkey(), - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); let recent_blockhash = ctx.rpc_client().get_latest_blockhash().await.unwrap(); @@ -241,30 +241,30 @@ async fn test_transfer_fee_insufficient_payment() { let sender_ata = get_associated_token_address_with_program_id( &sender.pubkey(), &mint_keypair.pubkey(), - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); let fee_payer_ata = get_associated_token_address_with_program_id( &fee_payer.pubkey(), &mint_keypair.pubkey(), - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); // Create ATAs if they don't exist let create_sender_ata_instruction = - spl_associated_token_account::instruction::create_associated_token_account_idempotent( + spl_associated_token_account_interface::instruction::create_associated_token_account_idempotent( &fee_payer.pubkey(), &sender.pubkey(), &mint_keypair.pubkey(), - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); let create_fee_payer_ata_instruction = - spl_associated_token_account::instruction::create_associated_token_account_idempotent( + spl_associated_token_account_interface::instruction::create_associated_token_account_idempotent( &fee_payer.pubkey(), &fee_payer.pubkey(), &mint_keypair.pubkey(), - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); let recent_blockhash = ctx.rpc_client().get_latest_blockhash().await.unwrap(); @@ -345,30 +345,30 @@ async fn test_transfer_fee_sufficient_payment() { let sender_ata = get_associated_token_address_with_program_id( &sender.pubkey(), &mint_keypair.pubkey(), - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); let fee_payer_ata = get_associated_token_address_with_program_id( &fee_payer.pubkey(), &mint_keypair.pubkey(), - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); // Create ATAs if they don't exist let create_sender_ata_instruction = - spl_associated_token_account::instruction::create_associated_token_account_idempotent( + spl_associated_token_account_interface::instruction::create_associated_token_account_idempotent( &fee_payer.pubkey(), &sender.pubkey(), &mint_keypair.pubkey(), - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); let create_fee_payer_ata_instruction = - spl_associated_token_account::instruction::create_associated_token_account_idempotent( + spl_associated_token_account_interface::instruction::create_associated_token_account_idempotent( &fee_payer.pubkey(), &fee_payer.pubkey(), &mint_keypair.pubkey(), - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); let recent_blockhash = ctx.rpc_client().get_latest_blockhash().await.unwrap(); @@ -465,30 +465,30 @@ async fn test_transfer_hook_allows_transfer() { let sender_ata = get_associated_token_address_with_program_id( &sender.pubkey(), &transfer_hook_mint_keypair.pubkey(), - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); let recipient_ata = get_associated_token_address_with_program_id( &recipient, &transfer_hook_mint_keypair.pubkey(), - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); // Create ATAs let create_sender_ata = - spl_associated_token_account::instruction::create_associated_token_account_idempotent( + spl_associated_token_account_interface::instruction::create_associated_token_account_idempotent( &fee_payer.pubkey(), &sender.pubkey(), &transfer_hook_mint_keypair.pubkey(), - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); let create_recipient_ata = - spl_associated_token_account::instruction::create_associated_token_account_idempotent( + spl_associated_token_account_interface::instruction::create_associated_token_account_idempotent( &fee_payer.pubkey(), &recipient, &transfer_hook_mint_keypair.pubkey(), - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); let recent_blockhash = rpc_client.get_latest_blockhash().await.unwrap(); @@ -512,8 +512,8 @@ async fn test_transfer_hook_allows_transfer() { .await .expect("Failed to mint tokens to sender"); - let mut transfer_instruction = spl_token_2022::instruction::transfer_checked( - &spl_token_2022::id(), + let mut transfer_instruction = spl_token_2022_interface::instruction::transfer_checked( + &spl_token_2022_interface::id(), &sender_ata, &transfer_hook_mint_keypair.pubkey(), &recipient_ata, @@ -541,14 +541,18 @@ async fn test_transfer_hook_allows_transfer() { // Add payment instruction for Kora fee let token_mint = USDCMintTestHelper::get_test_usdc_mint_pubkey(); let sender_usdc_ata = - spl_associated_token_account::get_associated_token_address(&sender.pubkey(), &token_mint); - let fee_payer_usdc_ata = spl_associated_token_account::get_associated_token_address( - &fee_payer.pubkey(), - &token_mint, - ); + spl_associated_token_account_interface::address::get_associated_token_address( + &sender.pubkey(), + &token_mint, + ); + let fee_payer_usdc_ata = + spl_associated_token_account_interface::address::get_associated_token_address( + &fee_payer.pubkey(), + &token_mint, + ); - let payment_instruction = spl_token::instruction::transfer( - &spl_token::id(), + let payment_instruction = spl_token_interface::instruction::transfer( + &spl_token_interface::id(), &sender_usdc_ata, &fee_payer_usdc_ata, &sender.pubkey(), @@ -626,30 +630,30 @@ async fn test_transfer_hook_blocks_transfer() { let sender_ata = get_associated_token_address_with_program_id( &sender.pubkey(), &transfer_hook_mint_keypair.pubkey(), - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); let recipient_ata = get_associated_token_address_with_program_id( &recipient, &transfer_hook_mint_keypair.pubkey(), - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); // Create ATAs let create_sender_ata = - spl_associated_token_account::instruction::create_associated_token_account_idempotent( + spl_associated_token_account_interface::instruction::create_associated_token_account_idempotent( &fee_payer.pubkey(), &sender.pubkey(), &transfer_hook_mint_keypair.pubkey(), - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); let create_recipient_ata = - spl_associated_token_account::instruction::create_associated_token_account_idempotent( + spl_associated_token_account_interface::instruction::create_associated_token_account_idempotent( &fee_payer.pubkey(), &recipient, &transfer_hook_mint_keypair.pubkey(), - &spl_token_2022::id(), + &spl_token_2022_interface::id(), ); let recent_blockhash = rpc_client.get_latest_blockhash().await.unwrap(); @@ -675,8 +679,8 @@ async fn test_transfer_hook_blocks_transfer() { .expect("Failed to mint tokens to sender"); // Create transfer instruction manually with transfer hook accounts - large amount that will be blocked - let mut transfer_instruction = spl_token_2022::instruction::transfer_checked( - &spl_token_2022::id(), + let mut transfer_instruction = spl_token_2022_interface::instruction::transfer_checked( + &spl_token_2022_interface::id(), &sender_ata, &transfer_hook_mint_keypair.pubkey(), &recipient_ata, From d1bf945ccbe2196611565fb18289237d054f5a18 Mon Sep 17 00:00:00 2001 From: amilz <85324096+amilz@users.noreply.github.com> Date: Wed, 29 Oct 2025 08:56:11 -0700 Subject: [PATCH 23/29] chore: audit documentation (#243) * chore: Audit - A09 - update CONFIGURATION.md & FEES.md to provide security guidance for handling free/fixed fees and fee payer policies - add security warnings for FREE configuration to config validator Closes PRO-504 * chore: Audit - A21 - update CONFIGURATION.md to warn about permanent delegate risk closes PRO-505 * chore: Audit - B06 - Updates FEES.md to specifiy that calculation model is only used for margin option. Renamed "Price Adjustment" to "Margin Adjustment" and improved clarity of explanation to reflect actual behavior - Nit: updated Last updated of CONFIGURATION.md Closes PRO-506 * chore: Audit - B10 - Update CONFIGURATION.md to include additional detail about the permanent limits of `useage_limit` Closes PRO-507 * chore: Audit - B11 - updates CONFIGURATION.md with increased warning for feepayer policies (note: seperate PR will update this based on new fee payer policy configuration) Closes PRO-508 * chore: revert PriceModel Free warnings --- docs/operators/CONFIGURATION.md | 52 +++++++++++++++++++--- docs/operators/FEES.md | 79 ++++++++++++++++++++++++++++++--- 2 files changed, 121 insertions(+), 10 deletions(-) diff --git a/docs/operators/CONFIGURATION.md b/docs/operators/CONFIGURATION.md index bdd6c35c..86a058d4 100644 --- a/docs/operators/CONFIGURATION.md +++ b/docs/operators/CONFIGURATION.md @@ -1,5 +1,5 @@ # Kora Configuration Reference -*Last Updated: 2025-08-25* +*Last Updated: 2025-10-28* Your Kora node will be signing transactions for your users, so it is important to configure it to only sign transactions that meet your business requirements. Kora gives you a lot of flexibility in how you configure your node, but it is important to understand the implications of your configuration. `kora.toml` is the control center for your Kora configuration. This document provides a comprehensive reference for configuring your Kora paymaster node through the `kora.toml` configuration file. @@ -128,8 +128,11 @@ account_ttl = 60 # Account data TTL in seconds (1 minute) ## Kora Usage Limits (optional) -The `[kora.usage_limit]` section configures per-wallet transaction limiting to prevent abuse and ensure fair usage across your users. This could also be used to create rewards programs to subsidize users' transaction fees up to a certain limit. -This feature requires Redis when enabled across multiple Kora instances: +The `[kora.usage_limit]` section configures per-wallet transaction limiting to prevent abuse and ensure fair usage across your users. This could also be used to create rewards programs to subsidize users' transaction fees up to a certain limit. + +**Important**: Currently, the only form of usage limiting supported by Kora is a **permanent limit**. Once a wallet reaches its transaction limit, it cannot be reset and the user will no longer be able to submit any more transactions using that same wallet. This limit persists until manually cleared from Redis or the Redis data is reset. + +**Note**: This feature requires Redis when enabled across multiple Kora instances: ```toml [kora.usage_limit] @@ -266,6 +269,11 @@ blocked_account_extensions = [ > *Note: Blocking extensions helps prevent interactions with tokens that have complex or potentially risky behaviors. For example, blocking `transfer_hook` prevents signing transactions for tokens with custom transfer logic.* +### Security Considerations + +**PermanentDelegate Extension** - Tokens with this extension allow the delegate to transfer/burn tokens at any time without owner approval. This creates significant risks for the Kora node operator as payment funds can be seized after payment. +- Consider adding "permanent_delegate" to `blocked_mint_extensions` in [validation.token2022] unless explicitly needed for your use case. +- Avoid using payment tokens with the `permanent_delegate` extension. ## Fee Payer Policy @@ -293,7 +301,10 @@ allow_approve = false | `allow_close_account` | Allow closing token accounts where the Kora node's fee payer is the signer/authority | โœ… | boolean | | `allow_approve` | Allow token delegation/approval where the Kora node's fee payer is the signer/authority | โœ… | boolean | -> *Note: For security reasons, it is recommended to set all of these to `false` and only enable as needed.* + +### Security Considerations + +**SECURITY WARNING:** For security reasons, it is recommended to set all of these to `false` and only enable as needed. This will prevent unwanted behavior such as users draining your fee payer account or burning tokens from your fee payer account. ## Price Configuration (optional) @@ -321,6 +332,8 @@ margin = 0.1 # 10% margin (0.1 = 10%, 1.0 = 100%) ### Fixed Pricing +**SECURITY WARNING:** Fixed pricing does **NOT** include fee payer outflow in the charged amount. This can allow users to drain your fee payer account if not properly configured. + Charge a fixed amount in a specific token regardless of network fees: ```toml @@ -329,7 +342,7 @@ type = "fixed" amount = 1000000 # Amount in token's base units token = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" # USDC mint ``` - + ### Free Transactions Sponsor all transaction fees (no charge to users): @@ -339,6 +352,35 @@ Sponsor all transaction fees (no charge to users): type = "free" ``` +#### Security Measures When Using Fixed/Free Pricing + +1. **Disable Transfer Operations** - Prevent fee payer from being used as source in transfers: + ```toml + [validation.fee_payer_policy.system] + allow_transfer = false # Critical: Block SOL transfers + allow_create_account = false # Block account creation + + [validation.fee_payer_policy.spl_token] + allow_transfer = false # Block SPL transfers + + [validation.fee_payer_policy.token_2022] + allow_transfer = false # Block Token2022 transfers + ``` + +2. **Enable Authentication** - Use authentication to prevent abuse: + ```toml + [kora.auth] + api_key = "your-secure-api-key" + # or + hmac_secret = "your-minimum-32-character-hmac-secret" + ``` + +3. **Set Conservative Limits** - Minimize exposure: + ```toml + [validation] + max_allowed_lamports = 1000000 # 0.001 SOL maximum + ``` + ## Performance Monitoring (optional) The `[metrics]` section configures metrics collection and monitoring. This section is optional and by default, metrics are disabled. diff --git a/docs/operators/FEES.md b/docs/operators/FEES.md index 542a043e..846c3a57 100644 --- a/docs/operators/FEES.md +++ b/docs/operators/FEES.md @@ -1,12 +1,18 @@ # Kora Fee Estimation Resource Guide -*Last updated: 2025-09-02* +*Last updated: 2025-10-28* -Kora estimates transaction fees when performing `estimate_transaction_fee`, `sign_transaction` and `sign_and_send_transaction` RPC methods. To estimate fees, Kora calculates the total cost for executing transactions on Solana, including network fees, account creation costs, and optional payment processing fees. This guide breaks down each component of the fee calculation. +Kora estimates transaction fees when performing `estimate_transaction_fee` and `sign_transaction` RPC methods. To estimate fees, Kora calculates the total cost for executing transactions on Solana, including network fees, account creation costs, and optional payment processing fees. This guide breaks down each component of the fee calculation. ## Fee Calculation Formula -The main entry point for fee estimation is `FeeConfigUtil::estimate_kora_fee()` in [`crates/lib/src/fee/fee.rs`](/crates/lib/src/fee/fee.rs). It uses the following generalized formula: +The fee is determined by the pricing model configured in `kora.toml`: + +- `PriceModel::Free` - Sponsors all transaction fees (total fee = 0) +- `PriceModel::Fixed` - Charges a fixed amount in a specific token (regardless of network fees) +- `PriceModel::Margin` - Adds a percentage margin to total fees. + +The main entry point for total fee estimation used in `PriceModel::Margin` is `FeeConfigUtil::estimate_kora_fee()` in [`crates/lib/src/fee/fee.rs`](/crates/lib/src/fee/fee.rs). It uses the following generalized formula: ``` Total Fee = Base Fee @@ -15,7 +21,7 @@ Total Fee = Base Fee + Fee Payer Outflow + Payment Instruction Fee + Transfer Fee Amount - + Price Adjustment (if configured) + + Margin Adjustment ``` ## Fee Components @@ -28,4 +34,67 @@ Total Fee = Base Fee | **Fee Payer Outflow** | Total SOL the fee payer sends out in the transaction (transfers, account creations, etc.) | Sum of: System transfers from fee payer, CreateAccount funded by fee payer, Nonce withdrawals from fee payer | When fee payer performs System Program operations | | **Payment Instruction Fee** | Estimated cost of priority fees to add a payment instruction for gasless transactions | Fixed estimate: 50 lamports (`ESTIMATED_LAMPORTS_FOR_PAYMENT_INSTRUCTION`) | When payment is required but not included in transaction | | **Transfer Fee** | Token2022 transfer fees configured on the mint (e.g., 1% of transfer amount) | `Token2022Mint.calculate_transfer_fee(amount, epoch)` - Based on mint's transfer fee configuration | Only for Token2022 transfers to Kora payment address | -| **Price Adjustment** | Kora's pricing model markup/adjustment | Configured price model in `validation.price` - Can add markup or fixed fee amount | When `[validation.price]` is provided in kora.toml (optional) | \ No newline at end of file +| **Margin Adjustment** | Kora's pricing model markup/adjustment | Configured margin in `validation.price` - Can add markup as a % of the total fee | When `[validation.price]` is provided in kora.toml | + +## Pricing Models & Fee Payer Outflow + +Kora supports three pricing models that determine how users are charged for transactions: + +### Margin Pricing (Default) +- **Formula:** `Total Fee = (Base + Outflow + Other Components) ร— (1 + margin)` +- **Includes Fee Payer Outflow:** Yes +- **Best For:** Production deployments where fees should reflect actual costs with an added operating margin + +### Fixed Pricing +- **Formula:** `Total Fee = Fixed Amount (in specified token)` +- **Includes Fee Payer Outflow:** No +- **Best For:** Simplified UX with predictable pricing in controlled environments + +### Free Pricing +- **Formula:** `Total Fee = 0` +- **Includes Fee Payer Outflow:** No (operator absorbs all costs) +- **Best For:** Promotional campaigns, testing, or fully sponsored applications + +--- + +## โš ๏ธ Security Warning: Fixed/Free Pricing Models + +**CRITICAL:** The fixed/free pricing models do **NOT** include fee payer outflow in the charged amount. This creates a significant security risk if not properly configured: If your fee payer policy allows transfers or other outflow operations, attackers can exploit this to drain your fee payer account: + +### Required Security Controls + +When using fixed/free pricing, you **MUST** configure restrictive fee payer policies: + +```toml +[validation.fee_payer_policy.system] +allow_transfer = false # Block SOL transfers +allow_create_account = false # Block account creation with lamports + +[validation.fee_payer_policy.spl_token] +allow_transfer = false # Block SPL token transfers + +[validation.fee_payer_policy.token_2022] +allow_transfer = false # Block Token2022 transfers +``` + +### Additional Protections + +1. **Enable Authentication:** Always require API key or HMAC authentication with fixed pricing +2. **Set Low Limits:** Use conservative `max_allowed_lamports` values +3. **Monitor Usage:** Track unusual patterns of high-outflow transactions +4. **Consider Margin Pricing:** Margin pricing automatically includes outflow costs + +### Validation Warnings + +Kora's config validator will warn you about dangerous configurations: + +```bash +kora --config kora.toml config validate +``` + +Expected warnings for vulnerable configs: +``` +โš ๏ธ SECURITY: Fixed pricing with allow_transfer=true for System instructions. + Users can make the fee payer transfer arbitrary SOL amounts at fixed cost. + This can drain your fee payer account. +``` From b39c36e32b44a2ac0cb3333361aabe6519c219a0 Mon Sep 17 00:00:00 2001 From: jo <17280917+dev-jodee@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:17:26 -0400 Subject: [PATCH 24/29] Feat/improved warnings for cli configs (#244) * chore: Add more warnings for configuration - Fetch mints and validate for PermanentDelegate and TransferHook extension - Added warnings for all "true" fee payer policy flags with an explanation of what the risk could be * Added "strict" option to fixed fee type, if true, will error out if the total fee is bigger than the fixed fee --- crates/lib/src/config.rs | 3 +- crates/lib/src/fee/fee.rs | 36 +- crates/lib/src/fee/price.rs | 7 +- crates/lib/src/tests/account_mock.rs | 8 + crates/lib/src/tests/toml_mock.rs | 2 +- .../src/transaction/versioned_transaction.rs | 3 + crates/lib/src/validator/config_validator.rs | 566 +++++++++++++++++- .../src/validator/transaction_validator.rs | 145 ++++- 8 files changed, 720 insertions(+), 50 deletions(-) diff --git a/crates/lib/src/config.rs b/crates/lib/src/config.rs index e60e751f..f811047f 100644 --- a/crates/lib/src/config.rs +++ b/crates/lib/src/config.rs @@ -659,9 +659,10 @@ mod tests { .unwrap(); match &config.validation.price.model { - PriceModel::Fixed { amount, token } => { + PriceModel::Fixed { amount, token, strict } => { assert_eq!(*amount, 1000000); assert_eq!(token, "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"); + assert!(!strict); } _ => panic!("Expected Fixed price model"), } diff --git a/crates/lib/src/fee/fee.rs b/crates/lib/src/fee/fee.rs index dfa9a36d..d3ffab2f 100644 --- a/crates/lib/src/fee/fee.rs +++ b/crates/lib/src/fee/fee.rs @@ -65,6 +65,19 @@ impl TotalFeeCalculation { transfer_fee_amount: 0, } } + + pub fn get_total_fee_lamports(&self) -> Result { + self.base_fee + .checked_add(self.kora_signature_fee) + .and_then(|sum| sum.checked_add(self.fee_payer_outflow)) + .and_then(|sum| sum.checked_add(self.payment_instruction_fee)) + .and_then(|sum| sum.checked_add(self.transfer_fee_amount)) + .ok_or_else(|| { + log::error!("Fee calculation overflow: base_fee={}, kora_signature_fee={}, fee_payer_outflow={}, payment_instruction_fee={}, transfer_fee_amount={}", + self.base_fee, self.kora_signature_fee, self.fee_payer_outflow, self.payment_instruction_fee, self.transfer_fee_amount); + KoraError::ValidationError("Fee calculation overflow".to_string()) + }) + } } pub struct FeeConfigUtil {} @@ -274,14 +287,33 @@ impl FeeConfigUtil { match &config.validation.price.model { PriceModel::Free => Ok(TotalFeeCalculation::new_fixed(0)), - PriceModel::Fixed { .. } => { + PriceModel::Fixed { strict, .. } => { let fixed_fee_lamports = config .validation .price .get_required_lamports_with_fixed(rpc_client, price_source) .await?; - Ok(TotalFeeCalculation::new_fixed(fixed_fee_lamports)) + if *strict { + let fee_calculation = Self::estimate_transaction_fee( + rpc_client, + transaction, + fee_payer, + is_payment_required, + ) + .await?; + + Ok(TotalFeeCalculation::new( + fixed_fee_lamports, + fee_calculation.base_fee, + fee_calculation.kora_signature_fee, + fee_calculation.fee_payer_outflow, + fee_calculation.payment_instruction_fee, + fee_calculation.transfer_fee_amount, + )) + } else { + Ok(TotalFeeCalculation::new_fixed(fixed_fee_lamports)) + } } PriceModel::Margin { .. } => { // Get the raw transaction diff --git a/crates/lib/src/fee/price.rs b/crates/lib/src/fee/price.rs index caed3959..6981f107 100644 --- a/crates/lib/src/fee/price.rs +++ b/crates/lib/src/fee/price.rs @@ -9,7 +9,7 @@ use utoipa::ToSchema; #[serde(tag = "type", rename_all = "lowercase")] pub enum PriceModel { Margin { margin: f64 }, - Fixed { amount: u64, token: String }, + Fixed { amount: u64, token: String, strict: bool }, Free, } @@ -31,7 +31,7 @@ impl PriceConfig { rpc_client: &RpcClient, price_source: PriceSource, ) -> Result { - if let PriceModel::Fixed { amount, token } = &self.model { + if let PriceModel::Fixed { amount, token, .. } = &self.model { return TokenUtil::calculate_token_value_in_lamports( *amount, &Pubkey::from_str(token).map_err(|e| { @@ -117,6 +117,7 @@ mod tests { model: PriceModel::Fixed { amount: 1_000_000, // 1 USDC (1,000,000 base units with 6 decimals) token: usdc_mint.to_string(), + strict: false, }, }; @@ -143,6 +144,7 @@ mod tests { model: PriceModel::Fixed { amount: 500000000, // 0.5 tokens (500,000,000 base units with 9 decimals) token: custom_token.to_string(), + strict: false, }, }; @@ -169,6 +171,7 @@ mod tests { model: PriceModel::Fixed { amount: 1000, // 0.001 USDC (1,000 base units with 6 decimals) token: usdc_mint.to_string(), + strict: false, }, }; diff --git a/crates/lib/src/tests/account_mock.rs b/crates/lib/src/tests/account_mock.rs index 6746cc49..c88faa66 100644 --- a/crates/lib/src/tests/account_mock.rs +++ b/crates/lib/src/tests/account_mock.rs @@ -466,6 +466,14 @@ impl MintAccountMockBuilder { ExtensionType::TransferFeeConfig => { state.init_extension::(true)?; } + ExtensionType::PermanentDelegate => { + state.init_extension::( + true, + )?; + } + ExtensionType::TransferHook => { + state.init_extension::(true)?; + } // Add other extension types as needed _ => {} } diff --git a/crates/lib/src/tests/toml_mock.rs b/crates/lib/src/tests/toml_mock.rs index 80e349ad..c4e53e77 100644 --- a/crates/lib/src/tests/toml_mock.rs +++ b/crates/lib/src/tests/toml_mock.rs @@ -103,7 +103,7 @@ impl ConfigBuilder { pub fn with_fixed_price(mut self, amount: u64, token: &str) -> Self { self.validation.price_config = Some(format!( - "[validation.price]\ntype = \"fixed\"\namount = {amount}\ntoken = \"{token}\"\n" + "[validation.price]\ntype = \"fixed\"\namount = {amount}\ntoken = \"{token}\"\nstrict = false\n" )); self } diff --git a/crates/lib/src/transaction/versioned_transaction.rs b/crates/lib/src/transaction/versioned_transaction.rs index 8e380ae1..cc0a3cd2 100644 --- a/crates/lib/src/transaction/versioned_transaction.rs +++ b/crates/lib/src/transaction/versioned_transaction.rs @@ -276,6 +276,9 @@ impl VersionedTransactionOps for VersionedTransactionResolved { &payment_destination, ) .await?; + + // Validate strict pricing if enabled + TransactionValidator::validate_strict_pricing_with_fee(&fee_calculation)?; } // Get latest blockhash and update transaction diff --git a/crates/lib/src/validator/config_validator.rs b/crates/lib/src/validator/config_validator.rs index eb1dca89..a3d76718 100644 --- a/crates/lib/src/validator/config_validator.rs +++ b/crates/lib/src/validator/config_validator.rs @@ -2,7 +2,7 @@ use std::{path::Path, str::FromStr}; use crate::{ admin::token_util::find_missing_atas, - config::{SplTokenConfig, Token2022Config}, + config::{FeePayerPolicy, SplTokenConfig, Token2022Config}, fee::price::PriceModel, oracle::PriceSource, signer::SignerPoolConfig, @@ -16,14 +16,214 @@ use crate::{ KoraError, }; use solana_client::nonblocking::rpc_client::RpcClient; -use solana_sdk::pubkey::Pubkey; +use solana_sdk::{account::Account, pubkey::Pubkey}; use solana_system_interface::program::ID as SYSTEM_PROGRAM_ID; -use spl_token_2022_interface::{extension::ExtensionType, ID as TOKEN_2022_PROGRAM_ID}; +use spl_token_2022_interface::{ + extension::{BaseStateWithExtensions, ExtensionType, StateWithExtensions}, + state::Mint as Token2022MintState, + ID as TOKEN_2022_PROGRAM_ID, +}; use spl_token_interface::ID as SPL_TOKEN_PROGRAM_ID; pub struct ConfigValidator {} impl ConfigValidator { + /// Check Token2022 mints for risky extensions (PermanentDelegate, TransferHook) + async fn check_token_mint_extensions( + rpc_client: &RpcClient, + allowed_tokens: &[String], + warnings: &mut Vec, + ) { + for token_str in allowed_tokens { + let token_pubkey = match Pubkey::from_str(token_str) { + Ok(pk) => pk, + Err(_) => continue, // Skip invalid pubkeys + }; + + let account: Account = match rpc_client.get_account(&token_pubkey).await { + Ok(acc) => acc, + Err(_) => continue, // Skip if can't fetch + }; + + if account.owner != TOKEN_2022_PROGRAM_ID { + continue; + } + + let mint_with_extensions = + match StateWithExtensions::::unpack(&account.data) { + Ok(m) => m, + Err(_) => continue, // Skip if can't parse + }; + + if mint_with_extensions + .get_extension::() + .is_ok() + { + warnings.push(format!( + "โš ๏ธ SECURITY: Token {} has PermanentDelegate extension. \ + Risk: The permanent delegate can transfer or burn tokens at any time without owner approval. \ + This creates significant risks for payment tokens as funds can be seized after payment. \ + Consider removing this token from allowed_tokens or blocking the extension in [validation.token2022].", + token_str + )); + } + + if mint_with_extensions + .get_extension::() + .is_ok() + { + warnings.push(format!( + "โš ๏ธ SECURITY: Token {} has TransferHook extension. \ + Risk: A custom program executes on every transfer which can reject transfers \ + or introduce external dependencies and attack surface. \ + Consider removing this token from allowed_tokens or blocking the extension in [validation.token2022].", + token_str + )); + } + } + } + + /// Validate fee payer policy and add warnings for enabled risky operations + fn validate_fee_payer_policy(policy: &FeePayerPolicy, warnings: &mut Vec) { + macro_rules! check_fee_payer_policy { + ($($category:ident, $field:ident, $description:expr, $risk:expr);* $(;)?) => { + $( + if policy.$category.$field { + warnings.push(format!( + "โš ๏ธ SECURITY: Fee payer policy allows {} ({}). \ + Risk: {}. \ + Consider setting [validation.fee_payer_policy.{}] {}=false to prevent abuse.", + $description, + stringify!($field), + $risk, + stringify!($category), + stringify!($field) + )); + } + )* + }; + } + + check_fee_payer_policy! { + system, allow_transfer, "System transfers", + "Users can make the fee payer transfer arbitrary SOL amounts. This can drain your fee payer account"; + + system, allow_assign, "System Assign instructions", + "Users can make the fee payer reassign ownership of its accounts. This can compromise account control"; + + system, allow_create_account, "System CreateAccount instructions", + "Users can make the fee payer pay for arbitrary account creations. This can drain your fee payer account"; + + system, allow_allocate, "System Allocate instructions", + "Users can make the fee payer allocate space for accounts. This can be used to waste resources"; + + spl_token, allow_transfer, "SPL Token transfers", + "Users can make the fee payer transfer arbitrary token amounts. This can drain your fee payer token accounts"; + + spl_token, allow_burn, "SPL Token burn operations", + "Users can make the fee payer burn tokens from its accounts. This causes permanent loss of assets"; + + spl_token, allow_close_account, "SPL Token CloseAccount instructions", + "Users can make the fee payer close token accounts. This can disrupt operations and drain fee payer"; + + spl_token, allow_approve, "SPL Token approve operations", + "Users can make the fee payer approve delegates. This can lead to unauthorized token transfers"; + + spl_token, allow_revoke, "SPL Token revoke operations", + "Users can make the fee payer revoke delegates. This can disrupt authorized operations"; + + spl_token, allow_set_authority, "SPL Token SetAuthority instructions", + "Users can make the fee payer transfer authority. This can lead to complete loss of control"; + + spl_token, allow_mint_to, "SPL Token MintTo operations", + "Users can make the fee payer mint tokens. This can inflate token supply"; + + spl_token, allow_initialize_mint, "SPL Token InitializeMint instructions", + "Users can make the fee payer initialize mints with itself as authority. This can lead to unexpected responsibilities"; + + spl_token, allow_initialize_account, "SPL Token InitializeAccount instructions", + "Users can make the fee payer the owner of new token accounts. This can clutter or exploit the fee payer"; + + spl_token, allow_initialize_multisig, "SPL Token InitializeMultisig instructions", + "Users can make the fee payer part of multisig accounts. This can create unwanted signing obligations"; + + spl_token, allow_freeze_account, "SPL Token FreezeAccount instructions", + "Users can make the fee payer freeze token accounts. This can disrupt token operations"; + + spl_token, allow_thaw_account, "SPL Token ThawAccount instructions", + "Users can make the fee payer unfreeze token accounts. This can undermine freeze policies"; + + token_2022, allow_transfer, "Token2022 transfers", + "Users can make the fee payer transfer arbitrary token amounts. This can drain your fee payer token accounts"; + + token_2022, allow_burn, "Token2022 burn operations", + "Users can make the fee payer burn tokens from its accounts. This causes permanent loss of assets"; + + token_2022, allow_close_account, "Token2022 CloseAccount instructions", + "Users can make the fee payer close token accounts. This can disrupt operations"; + + token_2022, allow_approve, "Token2022 approve operations", + "Users can make the fee payer approve delegates. This can lead to unauthorized token transfers"; + + token_2022, allow_revoke, "Token2022 revoke operations", + "Users can make the fee payer revoke delegates. This can disrupt authorized operations"; + + token_2022, allow_set_authority, "Token2022 SetAuthority instructions", + "Users can make the fee payer transfer authority. This can lead to complete loss of control"; + + token_2022, allow_mint_to, "Token2022 MintTo operations", + "Users can make the fee payer mint tokens. This can inflate token supply"; + + token_2022, allow_initialize_mint, "Token2022 InitializeMint instructions", + "Users can make the fee payer initialize mints with itself as authority. This can lead to unexpected responsibilities"; + + token_2022, allow_initialize_account, "Token2022 InitializeAccount instructions", + "Users can make the fee payer the owner of new token accounts. This can clutter or exploit the fee payer"; + + token_2022, allow_initialize_multisig, "Token2022 InitializeMultisig instructions", + "Users can make the fee payer part of multisig accounts. This can create unwanted signing obligations"; + + token_2022, allow_freeze_account, "Token2022 FreezeAccount instructions", + "Users can make the fee payer freeze token accounts. This can disrupt token operations"; + + token_2022, allow_thaw_account, "Token2022 ThawAccount instructions", + "Users can make the fee payer unfreeze token accounts. This can undermine freeze policies"; + } + + // Check nonce policy separately (nested structure) + macro_rules! check_nonce_policy { + ($($field:ident, $description:expr, $risk:expr);* $(;)?) => { + $( + if policy.system.nonce.$field { + warnings.push(format!( + "โš ๏ธ SECURITY: Fee payer policy allows {} (nonce.{}). \ + Risk: {}. \ + Consider setting [validation.fee_payer_policy.system.nonce] {}=false to prevent abuse.", + $description, + stringify!($field), + $risk, + stringify!($field) + )); + } + )* + }; + } + + check_nonce_policy! { + allow_initialize, "nonce account initialization", + "Users can make the fee payer the authority of nonce accounts. This can create unexpected control relationships"; + + allow_advance, "nonce account advancement", + "Users can make the fee payer advance nonce accounts. This can be used to manipulate nonce states"; + + allow_withdraw, "nonce account withdrawals", + "Users can make the fee payer withdraw from nonce accounts. This can drain nonce account balances"; + + allow_authorize, "nonce authority changes", + "Users can make the fee payer transfer nonce authority. This can lead to loss of control over nonce accounts"; + } + } + pub async fn validate(_rpc_client: &RpcClient) -> Result<(), KoraError> { let config = &get_config()?; @@ -50,7 +250,9 @@ impl ConfigValidator { ) -> Result, Vec> { Self::validate_with_result_and_signers(rpc_client, skip_rpc_validation, None::<&Path>).await } +} +impl ConfigValidator { pub async fn validate_with_result_and_signers>( rpc_client: &RpcClient, skip_rpc_validation: bool, @@ -196,9 +398,12 @@ impl ConfigValidator { } } + // Validate fee payer policy - warn about enabled risky operations + Self::validate_fee_payer_policy(&config.validation.fee_payer_policy, &mut warnings); + // Validate margin (error if negative) match &config.validation.price.model { - PriceModel::Fixed { amount, token } => { + PriceModel::Fixed { amount, token, strict } => { if *amount == 0 { warnings .push("Fixed price amount is 0 - transactions will be free".to_string()); @@ -224,30 +429,12 @@ impl ConfigValidator { ); } - if config.validation.fee_payer_policy.system.allow_transfer { + // Warn about strict mode + if *strict { warnings.push( - "โš ๏ธ SECURITY: Fixed pricing with allow_transfer=true for System instructions. \ - Users can make the fee payer transfer arbitrary SOL amounts at fixed cost. \ - This can drain your fee payer account. \ - Consider setting [validation.fee_payer_policy.system] allow_transfer=false.".to_string() - ); - } - - if config.validation.fee_payer_policy.spl_token.allow_transfer { - warnings.push( - "โš ๏ธ SECURITY: Fixed pricing with allow_transfer=true for SPL Token instructions. \ - Users can make the fee payer transfer arbitrary token amounts at fixed cost. \ - This can drain your fee payer token accounts. \ - Consider setting [validation.fee_payer_policy.spl_token] allow_transfer=false.".to_string() - ); - } - - if config.validation.fee_payer_policy.token_2022.allow_transfer { - warnings.push( - "โš ๏ธ SECURITY: Fixed pricing with allow_transfer=true for Token2022 instructions. \ - Users can make the fee payer transfer arbitrary token amounts at fixed cost. \ - This can drain your fee payer token accounts. \ - Consider setting [validation.fee_payer_policy.token_2022] allow_transfer=false.".to_string() + "Strict pricing mode enabled. \ + Transactions where fee payer outflow exceeds the fixed price will be rejected." + .to_string(), ); } } @@ -316,6 +503,14 @@ impl ConfigValidator { } } + // Check Token2022 mints for risky extensions + Self::check_token_mint_extensions( + rpc_client, + &config.validation.allowed_tokens, + &mut warnings, + ) + .await; + // Validate missing ATAs for payment address if let Some(payment_address) = &config.kora.payment_address { if let Ok(payment_address) = Pubkey::from_str(payment_address) { @@ -410,19 +605,26 @@ mod tests { use crate::{ config::{ AuthConfig, CacheConfig, Config, EnabledMethods, FeePayerPolicy, KoraConfig, - MetricsConfig, SplTokenConfig, UsageLimitConfig, ValidationConfig, + MetricsConfig, NonceInstructionPolicy, SplTokenConfig, SplTokenInstructionPolicy, + SystemInstructionPolicy, Token2022InstructionPolicy, UsageLimitConfig, + ValidationConfig, }, constant::DEFAULT_MAX_REQUEST_BODY_SIZE, fee::price::PriceConfig, state::update_config, - tests::common::{ - create_mock_non_executable_account, create_mock_program_account, - create_mock_rpc_client_account_not_found, create_mock_rpc_client_with_account, - create_mock_rpc_client_with_mint, RpcMockBuilder, + tests::{ + account_mock::create_mock_token2022_mint_with_extensions, + common::{ + create_mock_non_executable_account, create_mock_program_account, + create_mock_rpc_client_account_not_found, create_mock_rpc_client_with_account, + create_mock_rpc_client_with_mint, RpcMockBuilder, + }, + config_mock::ConfigMockBuilder, }, }; use serial_test::serial; use solana_commitment_config::CommitmentConfig; + use spl_token_2022_interface::extension::ExtensionType; use super::*; @@ -655,6 +857,7 @@ mod tests { model: PriceModel::Fixed { amount: 0, // Should warn token: "invalid_token_address".to_string(), // Should error + strict: false, }, }, token_2022: Token2022Config::default(), @@ -695,6 +898,7 @@ mod tests { model: PriceModel::Fixed { amount: 1000, token: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(), // Valid but not in allowed + strict: false, }, }, token_2022: Token2022Config::default(), @@ -743,6 +947,7 @@ mod tests { model: PriceModel::Fixed { amount: 0, // Should warn token: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(), + strict: false, }, }, token_2022: Token2022Config::default(), @@ -1202,4 +1407,301 @@ mod tests { let result = validate_token2022_extensions(&config); assert!(result.is_ok()); } + + #[tokio::test] + #[serial] + async fn test_validate_with_result_fee_payer_policy_warnings() { + let config = Config { + validation: ValidationConfig { + max_allowed_lamports: 1_000_000, + max_signatures: 10, + allowed_programs: vec![ + SYSTEM_PROGRAM_ID.to_string(), + SPL_TOKEN_PROGRAM_ID.to_string(), + TOKEN_2022_PROGRAM_ID.to_string(), + ], + allowed_tokens: vec!["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string()], + allowed_spl_paid_tokens: SplTokenConfig::Allowlist(vec![ + "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU".to_string(), + ]), + disallowed_accounts: vec![], + price_source: PriceSource::Jupiter, + fee_payer_policy: FeePayerPolicy { + system: SystemInstructionPolicy { + allow_transfer: true, + allow_assign: true, + allow_create_account: true, + allow_allocate: true, + nonce: NonceInstructionPolicy { + allow_initialize: true, + allow_advance: true, + allow_withdraw: true, + allow_authorize: true, + }, + }, + spl_token: SplTokenInstructionPolicy { + allow_transfer: true, + allow_burn: true, + allow_close_account: true, + allow_approve: true, + allow_revoke: true, + allow_set_authority: true, + allow_mint_to: true, + allow_initialize_mint: true, + allow_initialize_account: true, + allow_initialize_multisig: true, + allow_freeze_account: true, + allow_thaw_account: true, + }, + token_2022: Token2022InstructionPolicy { + allow_transfer: true, + allow_burn: true, + allow_close_account: true, + allow_approve: true, + allow_revoke: true, + allow_set_authority: true, + allow_mint_to: true, + allow_initialize_mint: true, + allow_initialize_account: true, + allow_initialize_multisig: true, + allow_freeze_account: true, + allow_thaw_account: true, + }, + }, + price: PriceConfig { model: PriceModel::Free }, + token_2022: Token2022Config::default(), + }, + metrics: MetricsConfig::default(), + kora: KoraConfig::default(), + }; + + let _ = update_config(config.clone()); + + let rpc_client = RpcClient::new_with_commitment( + "http://localhost:8899".to_string(), + CommitmentConfig::confirmed(), + ); + let result = ConfigValidator::validate_with_result(&rpc_client, true).await; + assert!(result.is_ok()); + let warnings = result.unwrap(); + + // Should have warnings for ALL enabled fee payer policy flags + // System policies + assert!(warnings + .iter() + .any(|w| w.contains("System transfers") && w.contains("allow_transfer"))); + assert!(warnings + .iter() + .any(|w| w.contains("System Assign instructions") && w.contains("allow_assign"))); + assert!(warnings.iter().any(|w| w.contains("System CreateAccount instructions") + && w.contains("allow_create_account"))); + assert!(warnings + .iter() + .any(|w| w.contains("System Allocate instructions") && w.contains("allow_allocate"))); + + // Nonce policies + assert!(warnings + .iter() + .any(|w| w.contains("nonce account initialization") && w.contains("allow_initialize"))); + assert!(warnings + .iter() + .any(|w| w.contains("nonce account advancement") && w.contains("allow_advance"))); + assert!(warnings + .iter() + .any(|w| w.contains("nonce account withdrawals") && w.contains("allow_withdraw"))); + assert!(warnings + .iter() + .any(|w| w.contains("nonce authority changes") && w.contains("allow_authorize"))); + + // SPL Token policies + assert!(warnings + .iter() + .any(|w| w.contains("SPL Token transfers") && w.contains("allow_transfer"))); + assert!(warnings + .iter() + .any(|w| w.contains("SPL Token burn operations") && w.contains("allow_burn"))); + assert!(warnings + .iter() + .any(|w| w.contains("SPL Token CloseAccount") && w.contains("allow_close_account"))); + assert!(warnings + .iter() + .any(|w| w.contains("SPL Token approve") && w.contains("allow_approve"))); + assert!(warnings + .iter() + .any(|w| w.contains("SPL Token revoke") && w.contains("allow_revoke"))); + assert!(warnings + .iter() + .any(|w| w.contains("SPL Token SetAuthority") && w.contains("allow_set_authority"))); + assert!(warnings + .iter() + .any(|w| w.contains("SPL Token MintTo") && w.contains("allow_mint_to"))); + assert!( + warnings + .iter() + .any(|w| w.contains("SPL Token InitializeMint") + && w.contains("allow_initialize_mint")) + ); + assert!(warnings + .iter() + .any(|w| w.contains("SPL Token InitializeAccount") + && w.contains("allow_initialize_account"))); + assert!(warnings.iter().any(|w| w.contains("SPL Token InitializeMultisig") + && w.contains("allow_initialize_multisig"))); + assert!(warnings + .iter() + .any(|w| w.contains("SPL Token FreezeAccount") && w.contains("allow_freeze_account"))); + assert!(warnings + .iter() + .any(|w| w.contains("SPL Token ThawAccount") && w.contains("allow_thaw_account"))); + + // Token2022 policies + assert!(warnings + .iter() + .any(|w| w.contains("Token2022 transfers") && w.contains("allow_transfer"))); + assert!(warnings + .iter() + .any(|w| w.contains("Token2022 burn operations") && w.contains("allow_burn"))); + assert!(warnings + .iter() + .any(|w| w.contains("Token2022 CloseAccount") && w.contains("allow_close_account"))); + assert!(warnings + .iter() + .any(|w| w.contains("Token2022 approve") && w.contains("allow_approve"))); + assert!(warnings + .iter() + .any(|w| w.contains("Token2022 revoke") && w.contains("allow_revoke"))); + assert!(warnings + .iter() + .any(|w| w.contains("Token2022 SetAuthority") && w.contains("allow_set_authority"))); + assert!(warnings + .iter() + .any(|w| w.contains("Token2022 MintTo") && w.contains("allow_mint_to"))); + assert!( + warnings + .iter() + .any(|w| w.contains("Token2022 InitializeMint") + && w.contains("allow_initialize_mint")) + ); + assert!(warnings + .iter() + .any(|w| w.contains("Token2022 InitializeAccount") + && w.contains("allow_initialize_account"))); + assert!(warnings.iter().any(|w| w.contains("Token2022 InitializeMultisig") + && w.contains("allow_initialize_multisig"))); + assert!(warnings + .iter() + .any(|w| w.contains("Token2022 FreezeAccount") && w.contains("allow_freeze_account"))); + assert!(warnings + .iter() + .any(|w| w.contains("Token2022 ThawAccount") && w.contains("allow_thaw_account"))); + + // Each warning should contain risk explanation + let fee_payer_warnings: Vec<_> = + warnings.iter().filter(|w| w.contains("Fee payer policy")).collect(); + for warning in fee_payer_warnings { + assert!(warning.contains("Risk:")); + assert!(warning.contains("Consider setting")); + } + } + + #[tokio::test] + #[serial] + async fn test_check_token_mint_extensions_permanent_delegate() { + let _m = ConfigMockBuilder::new().with_cache_enabled(false).build_and_setup(); + + let mint_with_delegate = + create_mock_token2022_mint_with_extensions(6, vec![ExtensionType::PermanentDelegate]); + let mint_pubkey = Pubkey::new_unique(); + + let rpc_client = create_mock_rpc_client_with_account(&mint_with_delegate); + let mut warnings = Vec::new(); + + ConfigValidator::check_token_mint_extensions( + &rpc_client, + &[mint_pubkey.to_string()], + &mut warnings, + ) + .await; + + assert_eq!(warnings.len(), 1); + assert!(warnings[0].contains("PermanentDelegate extension")); + assert!(warnings[0].contains(&mint_pubkey.to_string())); + assert!(warnings[0].contains("Risk:")); + assert!(warnings[0].contains("permanent delegate can transfer or burn tokens")); + } + + #[tokio::test] + #[serial] + async fn test_check_token_mint_extensions_transfer_hook() { + let _m = ConfigMockBuilder::new().with_cache_enabled(false).build_and_setup(); + + let mint_with_hook = + create_mock_token2022_mint_with_extensions(6, vec![ExtensionType::TransferHook]); + let mint_pubkey = Pubkey::new_unique(); + + let rpc_client = create_mock_rpc_client_with_account(&mint_with_hook); + let mut warnings = Vec::new(); + + ConfigValidator::check_token_mint_extensions( + &rpc_client, + &[mint_pubkey.to_string()], + &mut warnings, + ) + .await; + + assert_eq!(warnings.len(), 1); + assert!(warnings[0].contains("TransferHook extension")); + assert!(warnings[0].contains(&mint_pubkey.to_string())); + assert!(warnings[0].contains("Risk:")); + assert!(warnings[0].contains("custom program executes on every transfer")); + } + + #[tokio::test] + #[serial] + async fn test_check_token_mint_extensions_both() { + let _m = ConfigMockBuilder::new().with_cache_enabled(false).build_and_setup(); + + let mint_with_both = create_mock_token2022_mint_with_extensions( + 6, + vec![ExtensionType::PermanentDelegate, ExtensionType::TransferHook], + ); + let mint_pubkey = Pubkey::new_unique(); + + let rpc_client = create_mock_rpc_client_with_account(&mint_with_both); + let mut warnings = Vec::new(); + + ConfigValidator::check_token_mint_extensions( + &rpc_client, + &[mint_pubkey.to_string()], + &mut warnings, + ) + .await; + + // Should have warnings for both extensions + assert_eq!(warnings.len(), 2); + assert!(warnings.iter().any(|w| w.contains("PermanentDelegate extension"))); + assert!(warnings.iter().any(|w| w.contains("TransferHook extension"))); + } + + #[tokio::test] + #[serial] + async fn test_check_token_mint_extensions_no_risky_extensions() { + let _m = ConfigMockBuilder::new().with_cache_enabled(false).build_and_setup(); + + let mint_with_safe = + create_mock_token2022_mint_with_extensions(6, vec![ExtensionType::MintCloseAuthority]); + let mint_pubkey = Pubkey::new_unique(); + + let rpc_client = create_mock_rpc_client_with_account(&mint_with_safe); + let mut warnings = Vec::new(); + + ConfigValidator::check_token_mint_extensions( + &rpc_client, + &[mint_pubkey.to_string()], + &mut warnings, + ) + .await; + + assert_eq!(warnings.len(), 0); + } } diff --git a/crates/lib/src/validator/transaction_validator.rs b/crates/lib/src/validator/transaction_validator.rs index 20394088..16cb2c3b 100644 --- a/crates/lib/src/validator/transaction_validator.rs +++ b/crates/lib/src/validator/transaction_validator.rs @@ -1,7 +1,7 @@ use crate::{ config::FeePayerPolicy, error::KoraError, - fee::fee::FeeConfigUtil, + fee::fee::{FeeConfigUtil, TotalFeeCalculation}, oracle::PriceSource, state::get_config, token::{interface::TokenMint, token::TokenUtil}, @@ -14,17 +14,7 @@ use solana_client::nonblocking::rpc_client::RpcClient; use solana_sdk::{pubkey::Pubkey, transaction::VersionedTransaction}; use std::str::FromStr; -#[allow(unused_imports)] -use spl_token_2022_interface::{ - extension::{ - cpi_guard::CpiGuard, - interest_bearing_mint::InterestBearingConfig, - non_transferable::NonTransferable, - transfer_fee::{TransferFee, TransferFeeConfig}, - BaseStateWithExtensions, StateWithExtensions, - }, - state::Account as Token2022AccountState, -}; +use crate::fee::price::PriceModel; pub struct TransactionValidator { fee_payer_pubkey: Pubkey, @@ -370,6 +360,34 @@ impl TransactionValidator { "Insufficient token payment. Required {required_lamports} lamports" ))) } + + pub fn validate_strict_pricing_with_fee( + fee_calculation: &TotalFeeCalculation, + ) -> Result<(), KoraError> { + let config = get_config()?; + + if !matches!(&config.validation.price.model, PriceModel::Fixed { strict: true, .. }) { + return Ok(()); + } + + let fixed_price_lamports = fee_calculation.total_fee_lamports; + let total_fee_lamports = fee_calculation.get_total_fee_lamports()?; + + if fixed_price_lamports < total_fee_lamports { + log::error!( + "Strict pricing violation: fixed_price_lamports={} < total_fee_lamports={}", + fixed_price_lamports, + total_fee_lamports + ); + return Err(KoraError::ValidationError(format!( + "Strict pricing violation: total fee ({} lamports) exceeds fixed price ({} lamports)", + total_fee_lamports, + fixed_price_lamports + ))); + } + + Ok(()) + } } #[cfg(test)] @@ -1508,4 +1526,107 @@ mod tests { let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); } + + #[test] + #[serial] + fn test_strict_pricing_total_exceeds_fixed() { + let mut config = ConfigMockBuilder::new().build(); + config.validation.price.model = PriceModel::Fixed { + amount: 5000, + token: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(), + strict: true, + }; + let _ = update_config(config); + + // Fixed price = 5000, but total = 3000 + 2000 + 5000 = 10000 > 5000 + let fee_calc = TotalFeeCalculation::new(5000, 3000, 2000, 5000, 0, 0); + + let result = TransactionValidator::validate_strict_pricing_with_fee(&fee_calc); + + assert!(result.is_err()); + if let Err(KoraError::ValidationError(msg)) = result { + assert!(msg.contains("Strict pricing violation")); + assert!(msg.contains("exceeds fixed price")); + } else { + panic!("Expected ValidationError"); + } + } + + #[test] + #[serial] + fn test_strict_pricing_total_within_fixed() { + let mut config = ConfigMockBuilder::new().build(); + config.validation.price.model = PriceModel::Fixed { + amount: 5000, + token: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(), + strict: true, + }; + let _ = update_config(config); + + // Fixed price = 5000, total = 1000 + 1000 + 1000 = 3000 < 5000 + let fee_calc = TotalFeeCalculation::new(5000, 1000, 1000, 1000, 0, 0); + + let result = TransactionValidator::validate_strict_pricing_with_fee(&fee_calc); + + assert!(result.is_ok()); + } + + #[test] + #[serial] + fn test_strict_pricing_disabled() { + let mut config = ConfigMockBuilder::new().build(); + config.validation.price.model = PriceModel::Fixed { + amount: 5000, + token: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(), + strict: false, // Disabled + }; + let _ = update_config(config); + + let fee_calc = TotalFeeCalculation::new(5000, 10000, 0, 0, 0, 0); + + let result = TransactionValidator::validate_strict_pricing_with_fee(&fee_calc); + + assert!(result.is_ok(), "Should pass when strict=false"); + } + + #[test] + #[serial] + fn test_strict_pricing_with_margin_pricing() { + use crate::{ + fee::price::PriceModel, state::update_config, tests::config_mock::ConfigMockBuilder, + }; + + let mut config = ConfigMockBuilder::new().build(); + config.validation.price.model = PriceModel::Margin { margin: 0.1 }; + let _ = update_config(config); + + let fee_calc = TotalFeeCalculation::new(5000, 10000, 0, 0, 0, 0); + + let result = TransactionValidator::validate_strict_pricing_with_fee(&fee_calc); + + assert!(result.is_ok()); + } + + #[test] + #[serial] + fn test_strict_pricing_exact_match() { + use crate::{ + fee::price::PriceModel, state::update_config, tests::config_mock::ConfigMockBuilder, + }; + + let mut config = ConfigMockBuilder::new().build(); + config.validation.price.model = PriceModel::Fixed { + amount: 5000, + token: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(), + strict: true, + }; + let _ = update_config(config); + + // Total exactly equals fixed price (5000 = 5000) + let fee_calc = TotalFeeCalculation::new(5000, 2000, 1000, 2000, 0, 0); + + let result = TransactionValidator::validate_strict_pricing_with_fee(&fee_calc); + + assert!(result.is_ok(), "Should pass when total equals fixed price"); + } } From 26ff2e1009359276d53becaa1508ba6a388ad18b Mon Sep 17 00:00:00 2001 From: jo <17280917+dev-jodee@users.noreply.github.com> Date: Wed, 29 Oct 2025 15:39:12 -0400 Subject: [PATCH 25/29] bgufix: Update uncompile_instructions to prevent index out of bounds (#245) - Modified `TransactionUtil::new_unsigned_versioned_transaction_resolved` to return a `Result` type, ensuring proper error handling. - Updated various test cases and transaction validation logic to unwrap results, improving robustness and clarity in error management. - Introduced a new utility function `get_account_key_required` in `IxUtils` for safer account key retrieval, enhancing error reporting for missing keys. --- crates/lib/src/fee/fee.rs | 46 ++--- .../rpc_server/method/transfer_transaction.rs | 2 +- .../lib/src/transaction/instruction_util.rs | 30 +++- crates/lib/src/transaction/transaction.rs | 2 +- .../src/transaction/versioned_transaction.rs | 50 +++--- .../src/validator/transaction_validator.rs | 159 ++++++++++++------ 6 files changed, 183 insertions(+), 106 deletions(-) diff --git a/crates/lib/src/fee/fee.rs b/crates/lib/src/fee/fee.rs index d3ffab2f..e3f0f915 100644 --- a/crates/lib/src/fee/fee.rs +++ b/crates/lib/src/fee/fee.rs @@ -554,7 +554,7 @@ mod tests { let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); let resolved_transaction = - TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); assert!(FeeConfigUtil::is_fee_payer_in_signers(&resolved_transaction, &fee_payer).unwrap()); } @@ -571,7 +571,7 @@ mod tests { VersionedMessage::Legacy(Message::new(&[instruction], Some(&sender.pubkey()))); let resolved_transaction = - TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); assert!(!FeeConfigUtil::is_fee_payer_in_signers(&resolved_transaction, &fee_payer_pubkey) .unwrap()); @@ -593,7 +593,7 @@ mod tests { let message = VersionedMessage::V0(v0_message); let resolved_transaction = - TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); assert!(FeeConfigUtil::is_fee_payer_in_signers(&resolved_transaction, &fee_payer).unwrap()); } @@ -614,7 +614,7 @@ mod tests { let message = VersionedMessage::V0(v0_message); let resolved_transaction = - TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); assert!(!FeeConfigUtil::is_fee_payer_in_signers(&resolved_transaction, &fee_payer_pubkey) .unwrap()); @@ -632,7 +632,7 @@ mod tests { let message = VersionedMessage::Legacy(Message::new(&[transfer_instruction], Some(&fee_payer))); let mut resolved_transaction = - TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); let outflow = FeeConfigUtil::calculate_fee_payer_outflow( &fee_payer, @@ -650,7 +650,7 @@ mod tests { let message = VersionedMessage::Legacy(Message::new(&[transfer_instruction], Some(&fee_payer))); let mut resolved_transaction = - TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); let outflow = FeeConfigUtil::calculate_fee_payer_outflow( &fee_payer, &mut resolved_transaction, @@ -667,7 +667,7 @@ mod tests { let message = VersionedMessage::Legacy(Message::new(&[transfer_instruction], Some(&fee_payer))); let mut resolved_transaction = - TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); let outflow = FeeConfigUtil::calculate_fee_payer_outflow( &fee_payer, &mut resolved_transaction, @@ -698,7 +698,7 @@ mod tests { let message = VersionedMessage::Legacy(Message::new(&[transfer_instruction], Some(&fee_payer))); let mut resolved_transaction = - TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); let outflow = FeeConfigUtil::calculate_fee_payer_outflow( &fee_payer, &mut resolved_transaction, @@ -722,7 +722,7 @@ mod tests { let message = VersionedMessage::Legacy(Message::new(&[transfer_instruction], Some(&fee_payer))); let mut resolved_transaction = - TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); let outflow = FeeConfigUtil::calculate_fee_payer_outflow( &fee_payer, &mut resolved_transaction, @@ -750,7 +750,7 @@ mod tests { let message = VersionedMessage::Legacy(Message::new(&[create_instruction], Some(&fee_payer))); let mut resolved_transaction = - TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); let outflow = FeeConfigUtil::calculate_fee_payer_outflow( &fee_payer, &mut resolved_transaction, @@ -768,7 +768,7 @@ mod tests { let message = VersionedMessage::Legacy(Message::new(&[create_instruction], Some(&fee_payer))); let mut resolved_transaction = - TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); let outflow = FeeConfigUtil::calculate_fee_payer_outflow( &fee_payer, &mut resolved_transaction, @@ -800,7 +800,7 @@ mod tests { let message = VersionedMessage::Legacy(Message::new(&[create_instruction], Some(&fee_payer))); let mut resolved_transaction = - TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); let outflow = FeeConfigUtil::calculate_fee_payer_outflow( &fee_payer, &mut resolved_transaction, @@ -829,7 +829,7 @@ mod tests { let message = VersionedMessage::Legacy(Message::new(&[withdraw_instruction], Some(&fee_payer))); let mut resolved_transaction = - TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); let outflow = FeeConfigUtil::calculate_fee_payer_outflow( &fee_payer, &mut resolved_transaction, @@ -850,7 +850,7 @@ mod tests { let message = VersionedMessage::Legacy(Message::new(&[withdraw_instruction], Some(&fee_payer))); let mut resolved_transaction = - TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); let outflow = FeeConfigUtil::calculate_fee_payer_outflow( &fee_payer, &mut resolved_transaction, @@ -882,7 +882,7 @@ mod tests { ]; let message = VersionedMessage::Legacy(Message::new(&instructions, Some(&fee_payer))); let mut resolved_transaction = - TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); let outflow = FeeConfigUtil::calculate_fee_payer_outflow( &fee_payer, &mut resolved_transaction, @@ -912,7 +912,7 @@ mod tests { ); let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); let mut resolved_transaction = - TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); let outflow = FeeConfigUtil::calculate_fee_payer_outflow( &fee_payer, &mut resolved_transaction, @@ -955,7 +955,7 @@ mod tests { // Create message with the payment instruction let message = VersionedMessage::Legacy(Message::new(&[transfer_instruction], None)); let mut resolved_transaction = - TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); let (has_payment, transfer_fees) = FeeConfigUtil::analyze_payment_instructions( &mut resolved_transaction, @@ -984,7 +984,7 @@ mod tests { // Create message without payment instruction let message = VersionedMessage::Legacy(Message::new(&[sol_transfer], None)); let mut resolved_transaction = - TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); let (has_payment, transfer_fees) = FeeConfigUtil::analyze_payment_instructions( &mut resolved_transaction, @@ -1030,7 +1030,7 @@ mod tests { // Create message with non-payment transfer let message = VersionedMessage::Legacy(Message::new(&[transfer_instruction], None)); let mut resolved_transaction = - TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); let (has_payment, transfer_fees) = FeeConfigUtil::analyze_payment_instructions( &mut resolved_transaction, @@ -1061,7 +1061,7 @@ mod tests { Some(&fee_payer.pubkey()), )); let mut resolved_transaction = - TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); let result = FeeConfigUtil::estimate_transaction_fee( &mocked_rpc_client, @@ -1091,7 +1091,7 @@ mod tests { let message = VersionedMessage::Legacy(Message::new(&[transfer_instruction], Some(&sender.pubkey()))); let mut resolved_transaction = - TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); let result = FeeConfigUtil::estimate_transaction_fee( &mocked_rpc_client, @@ -1128,7 +1128,7 @@ mod tests { Some(&fee_payer.pubkey()), )); let mut resolved_transaction = - TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); let result = FeeConfigUtil::estimate_transaction_fee( &mocked_rpc_client, @@ -1184,7 +1184,7 @@ mod tests { let message = VersionedMessage::Legacy(Message::new(&[transfer_1, transfer_2], None)); let mut resolved_transaction = - TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); let (has_payment, transfer_fees) = FeeConfigUtil::analyze_payment_instructions( &mut resolved_transaction, diff --git a/crates/lib/src/rpc_server/method/transfer_transaction.rs b/crates/lib/src/rpc_server/method/transfer_transaction.rs index f001d18b..acf8cfdf 100644 --- a/crates/lib/src/rpc_server/method/transfer_transaction.rs +++ b/crates/lib/src/rpc_server/method/transfer_transaction.rs @@ -123,7 +123,7 @@ pub async fn transfer_transaction( let transaction = TransactionUtil::new_unsigned_versioned_transaction(message); let mut resolved_transaction = - VersionedTransactionResolved::from_kora_built_transaction(&transaction); + VersionedTransactionResolved::from_kora_built_transaction(&transaction)?; // validate transaction before signing validator.validate_transaction(&mut resolved_transaction, rpc_client).await?; diff --git a/crates/lib/src/transaction/instruction_util.rs b/crates/lib/src/transaction/instruction_util.rs index 0473e04f..c09b6cb1 100644 --- a/crates/lib/src/transaction/instruction_util.rs +++ b/crates/lib/src/transaction/instruction_util.rs @@ -326,6 +326,15 @@ impl IxUtils { Some(ix.accounts[index].pubkey) } + pub fn get_account_key_required( + account_keys: &[Pubkey], + index: usize, + ) -> Result { + account_keys.get(index).copied().ok_or_else(|| { + KoraError::SerializationError(format!("Account key at index {} not found", index)) + }) + } + pub fn build_default_compiled_instruction(program_id_index: u8) -> CompiledInstruction { CompiledInstruction { program_id_index, accounts: vec![], data: vec![] } } @@ -333,22 +342,25 @@ impl IxUtils { pub fn uncompile_instructions( instructions: &[CompiledInstruction], account_keys: &[Pubkey], - ) -> Vec { + ) -> Result, KoraError> { instructions .iter() .map(|ix| { - let program_id = account_keys[ix.program_id_index as usize]; - let accounts = ix + let program_id = + Self::get_account_key_required(account_keys, ix.program_id_index as usize)?; + let accounts: Result, KoraError> = ix .accounts .iter() - .map(|idx| AccountMeta { - pubkey: account_keys[*idx as usize], - is_signer: false, - is_writable: true, + .map(|idx| { + Ok(AccountMeta { + pubkey: Self::get_account_key_required(account_keys, *idx as usize)?, + is_signer: false, + is_writable: true, + }) }) .collect(); - Instruction { program_id, accounts, data: ix.data.clone() } + Ok(Instruction { program_id, accounts: accounts?, data: ix.data.clone() }) }) .collect() } @@ -2529,7 +2541,7 @@ mod tests { data: vec![1, 2, 3], }; - let instructions = IxUtils::uncompile_instructions(&[compiled_ix], &account_keys); + let instructions = IxUtils::uncompile_instructions(&[compiled_ix], &account_keys).unwrap(); assert_eq!(instructions.len(), 1); let uncompiled = &instructions[0]; diff --git a/crates/lib/src/transaction/transaction.rs b/crates/lib/src/transaction/transaction.rs index 7c2bf5ce..0f7a4069 100644 --- a/crates/lib/src/transaction/transaction.rs +++ b/crates/lib/src/transaction/transaction.rs @@ -42,7 +42,7 @@ impl TransactionUtil { pub fn new_unsigned_versioned_transaction_resolved( message: VersionedMessage, - ) -> VersionedTransactionResolved { + ) -> Result { let transaction = TransactionUtil::new_unsigned_versioned_transaction(message); VersionedTransactionResolved::from_kora_built_transaction(&transaction) } diff --git a/crates/lib/src/transaction/versioned_transaction.rs b/crates/lib/src/transaction/versioned_transaction.rs index cc0a3cd2..33b4e7a6 100644 --- a/crates/lib/src/transaction/versioned_transaction.rs +++ b/crates/lib/src/transaction/versioned_transaction.rs @@ -105,7 +105,7 @@ impl VersionedTransactionResolved { // 2. Fetch all instructions let outer_instructions = - IxUtils::uncompile_instructions(transaction.message.instructions(), &all_account_keys); + IxUtils::uncompile_instructions(transaction.message.instructions(), &all_account_keys)?; let inner_instructions = resolved.fetch_inner_instructions(rpc_client, sig_verify).await?; @@ -116,17 +116,19 @@ impl VersionedTransactionResolved { } /// Only use this is we built the transaction ourselves, because it won't do any checks for resolving LUT, etc. - pub fn from_kora_built_transaction(transaction: &VersionedTransaction) -> Self { - Self { + pub fn from_kora_built_transaction( + transaction: &VersionedTransaction, + ) -> Result { + Ok(Self { transaction: transaction.clone(), all_account_keys: transaction.message.static_account_keys().to_vec(), all_instructions: IxUtils::uncompile_instructions( transaction.message.instructions(), transaction.message.static_account_keys(), - ), + )?, parsed_system_instructions: None, parsed_spl_instructions: None, - } + }) } /// Fetch inner instructions via simulation @@ -180,10 +182,10 @@ impl VersionedTransactionResolved { }); }); - return Ok(IxUtils::uncompile_instructions( + return IxUtils::uncompile_instructions( &compiled_inner_instructions, &self.all_account_keys, - )); + ); } Ok(vec![]) @@ -432,7 +434,7 @@ mod tests { VersionedMessage::Legacy(Message::new(&[instruction], Some(&keypair.pubkey()))); let tx = VersionedTransaction::try_new(message, &[&keypair]).unwrap(); - let resolved = VersionedTransactionResolved::from_kora_built_transaction(&tx); + let resolved = VersionedTransactionResolved::from_kora_built_transaction(&tx).unwrap(); let encoded = resolved.encode_b64_transaction().unwrap(); assert!(!encoded.is_empty()); assert!(encoded @@ -452,7 +454,7 @@ mod tests { VersionedMessage::Legacy(Message::new(&[instruction], Some(&keypair.pubkey()))); let tx = VersionedTransaction::try_new(message, &[&keypair]).unwrap(); - let resolved = VersionedTransactionResolved::from_kora_built_transaction(&tx); + let resolved = VersionedTransactionResolved::from_kora_built_transaction(&tx).unwrap(); let encoded = resolved.encode_b64_transaction().unwrap(); let decoded = TransactionUtil::decode_b64_transaction(&encoded).unwrap(); @@ -470,7 +472,8 @@ mod tests { ); let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&keypair.pubkey()))); - let transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); let position = transaction.find_signer_position(&keypair.pubkey()).unwrap(); assert_eq!(position, 0); // Fee payer is typically at position 0 @@ -498,7 +501,8 @@ mod tests { address_table_lookups: vec![], }; let message = VersionedMessage::V0(v0_message); - let transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); let position = transaction.find_signer_position(&keypair.pubkey()).unwrap(); assert_eq!(position, 0); @@ -530,7 +534,8 @@ mod tests { address_table_lookups: vec![], }; let message = VersionedMessage::V0(v0_message); - let transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); assert_eq!(transaction.find_signer_position(&keypair1.pubkey()).unwrap(), 0); assert_eq!(transaction.find_signer_position(&keypair2.pubkey()).unwrap(), 1); @@ -548,7 +553,8 @@ mod tests { ); let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&keypair.pubkey()))); - let transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); let result = transaction.find_signer_position(&missing_keypair.pubkey()); assert!(matches!(result, Err(KoraError::InvalidTransaction(_)))); @@ -574,7 +580,8 @@ mod tests { address_table_lookups: vec![], }; let message = VersionedMessage::V0(v0_message); - let transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); let search_key = Pubkey::new_unique(); let result = transaction.find_signer_position(&search_key); @@ -599,7 +606,8 @@ mod tests { )); let transaction = VersionedTransaction::try_new(message.clone(), &[&keypair]).unwrap(); - let resolved = VersionedTransactionResolved::from_kora_built_transaction(&transaction); + let resolved = + VersionedTransactionResolved::from_kora_built_transaction(&transaction).unwrap(); assert_eq!(resolved.transaction, transaction); assert_eq!(resolved.all_account_keys, transaction.message.static_account_keys()); @@ -640,7 +648,8 @@ mod tests { let message = VersionedMessage::V0(v0_message); let transaction = VersionedTransaction::try_new(message.clone(), &[&keypair]).unwrap(); - let resolved = VersionedTransactionResolved::from_kora_built_transaction(&transaction); + let resolved = + VersionedTransactionResolved::from_kora_built_transaction(&transaction).unwrap(); assert_eq!(resolved.transaction, transaction); assert_eq!(resolved.all_account_keys, vec![keypair.pubkey(), other_account, program_id]); @@ -893,7 +902,8 @@ mod tests { ); let rpc_client = RpcMockBuilder::new().with_custom_mocks(mocks).build(); - let mut resolved = VersionedTransactionResolved::from_kora_built_transaction(&transaction); + let mut resolved = + VersionedTransactionResolved::from_kora_built_transaction(&transaction).unwrap(); let inner_instructions = resolved.fetch_inner_instructions(&rpc_client, true).await.unwrap(); @@ -945,7 +955,8 @@ mod tests { ); let rpc_client = RpcMockBuilder::new().with_custom_mocks(mocks).build(); - let mut resolved = VersionedTransactionResolved::from_kora_built_transaction(&transaction); + let mut resolved = + VersionedTransactionResolved::from_kora_built_transaction(&transaction).unwrap(); let inner_instructions = resolved.fetch_inner_instructions(&rpc_client, false).await.unwrap(); @@ -968,7 +979,8 @@ mod tests { VersionedMessage::Legacy(Message::new(&[instruction], Some(&keypair.pubkey()))); let transaction = VersionedTransaction::try_new(message, &[&keypair]).unwrap(); - let mut resolved = VersionedTransactionResolved::from_kora_built_transaction(&transaction); + let mut resolved = + VersionedTransactionResolved::from_kora_built_transaction(&transaction).unwrap(); // First call should parse and cache let parsed1_len = { diff --git a/crates/lib/src/validator/transaction_validator.rs b/crates/lib/src/validator/transaction_validator.rs index 16cb2c3b..5303ffad 100644 --- a/crates/lib/src/validator/transaction_validator.rs +++ b/crates/lib/src/validator/transaction_validator.rs @@ -464,7 +464,8 @@ mod tests { let sender = Pubkey::new_unique(); let instruction = transfer(&sender, &recipient, 100_000); let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); } @@ -483,7 +484,8 @@ mod tests { // Test transaction with amount over limit let instruction = transfer(&sender, &recipient, 2_000_000); let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); @@ -491,7 +493,8 @@ mod tests { let instructions = vec![transfer(&sender, &recipient, 500_000), transfer(&sender, &recipient, 500_000)]; let message = VersionedMessage::Legacy(Message::new(&instructions, Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); } @@ -509,7 +512,8 @@ mod tests { // Test allowed program (system program) let instruction = transfer(&sender, &recipient, 1000); let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); // Test disallowed program @@ -521,7 +525,8 @@ mod tests { vec![], // no accounts needed for this test ); let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); } @@ -550,7 +555,8 @@ mod tests { transfer(&sender, &recipient, 1000), ]; let message = VersionedMessage::Legacy(Message::new(&instructions, Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); transaction.transaction.signatures = vec![Default::default(); 3]; // Add 3 dummy signatures assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); } @@ -569,13 +575,15 @@ mod tests { // Test SignAndSend mode with fee payer already set should not error let instruction = transfer(&sender, &recipient, 1000); let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); // Test SignAndSend mode without fee payer (should succeed) let instruction = transfer(&sender, &recipient, 1000); let message = VersionedMessage::Legacy(Message::new(&[instruction], None)); // No fee payer specified - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); } @@ -590,7 +598,8 @@ mod tests { // Create an empty message using Message::new with empty instructions let message = VersionedMessage::Legacy(Message::new(&[], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); } @@ -617,7 +626,8 @@ mod tests { 1000, ); let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); } @@ -638,7 +648,8 @@ mod tests { let instruction = transfer(&fee_payer, &recipient, 1000); let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); // Test with allow_sol_transfers = false @@ -651,7 +662,8 @@ mod tests { let instruction = transfer(&fee_payer, &recipient, 1000); let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); } @@ -673,7 +685,8 @@ mod tests { let instruction = assign(&fee_payer, &new_owner); let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); // Test with allow_assign = false @@ -688,7 +701,8 @@ mod tests { let instruction = assign(&fee_payer, &new_owner); let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); } @@ -720,7 +734,8 @@ mod tests { .unwrap(); let message = VersionedMessage::Legacy(Message::new(&[transfer_ix], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); // Test with allow_spl_transfers = false @@ -743,7 +758,8 @@ mod tests { .unwrap(); let message = VersionedMessage::Legacy(Message::new(&[transfer_ix], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); // Test with other account as source - should always pass @@ -759,7 +775,8 @@ mod tests { .unwrap(); let message = VersionedMessage::Legacy(Message::new(&[transfer_ix], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); } @@ -796,7 +813,8 @@ mod tests { .unwrap(); let message = VersionedMessage::Legacy(Message::new(&[transfer_ix], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); // Test with allow_token2022_transfers = false @@ -822,7 +840,8 @@ mod tests { .unwrap(); let message = VersionedMessage::Legacy(Message::new(&[transfer_ix], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); // Should fail because fee payer is not allowed to be source assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); @@ -842,7 +861,8 @@ mod tests { .unwrap(); let message = VersionedMessage::Legacy(Message::new(&[transfer_ix], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); // Should pass because fee payer is not the source assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); @@ -868,7 +888,8 @@ mod tests { let transfer_instruction = transfer(&fee_payer, &recipient, 100_000); let message = VersionedMessage::Legacy(Message::new(&[transfer_instruction], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); let outflow = validator.calculate_total_outflow(&mut transaction, &rpc_client).await.unwrap(); assert_eq!(outflow, 100_000, "Transfer from fee payer should add to outflow"); @@ -878,7 +899,8 @@ mod tests { let transfer_instruction = transfer(&sender, &fee_payer, 50_000); let message = VersionedMessage::Legacy(Message::new(&[transfer_instruction], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); let outflow = validator.calculate_total_outflow(&mut transaction, &rpc_client).await.unwrap(); @@ -895,7 +917,8 @@ mod tests { ); let message = VersionedMessage::Legacy(Message::new(&[create_instruction], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); let outflow = validator.calculate_total_outflow(&mut transaction, &rpc_client).await.unwrap(); assert_eq!(outflow, 200_000, "CreateAccount funded by fee payer should add to outflow"); @@ -914,7 +937,8 @@ mod tests { &[create_with_seed_instruction], Some(&fee_payer), )); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); let outflow = validator.calculate_total_outflow(&mut transaction, &rpc_client).await.unwrap(); assert_eq!( @@ -935,7 +959,8 @@ mod tests { &[transfer_with_seed_instruction], Some(&fee_payer), )); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); let outflow = validator.calculate_total_outflow(&mut transaction, &rpc_client).await.unwrap(); assert_eq!(outflow, 150_000, "TransferWithSeed from fee payer should add to outflow"); @@ -947,7 +972,8 @@ mod tests { create_account(&fee_payer, &new_account, 50_000, 100, &SYSTEM_PROGRAM_ID), // +50_000 ]; let message = VersionedMessage::Legacy(Message::new(&instructions, Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); let outflow = validator.calculate_total_outflow(&mut transaction, &rpc_client).await.unwrap(); assert_eq!( @@ -960,7 +986,8 @@ mod tests { let transfer_instruction = transfer(&other_sender, &recipient, 500_000); let message = VersionedMessage::Legacy(Message::new(&[transfer_instruction], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); let outflow = validator.calculate_total_outflow(&mut transaction, &rpc_client).await.unwrap(); assert_eq!(outflow, 0, "Transfer from other account should not affect outflow"); @@ -971,7 +998,8 @@ mod tests { create_account(&other_funder, &new_account, 1_000_000, 100, &SYSTEM_PROGRAM_ID); let message = VersionedMessage::Legacy(Message::new(&[create_instruction], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); let outflow = validator.calculate_total_outflow(&mut transaction, &rpc_client).await.unwrap(); assert_eq!(outflow, 0, "CreateAccount funded by other account should not affect outflow"); @@ -1004,7 +1032,8 @@ mod tests { .unwrap(); let message = VersionedMessage::Legacy(Message::new(&[burn_ix], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); // Should pass because allow_burn is true by default assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); @@ -1028,7 +1057,8 @@ mod tests { .unwrap(); let message = VersionedMessage::Legacy(Message::new(&[burn_ix], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); // Should fail because fee payer cannot burn tokens when allow_burn is false assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); @@ -1046,7 +1076,8 @@ mod tests { .unwrap(); let message = VersionedMessage::Legacy(Message::new(&[burn_checked_ix], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); // Should also fail for burn_checked assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); @@ -1078,7 +1109,8 @@ mod tests { .unwrap(); let message = VersionedMessage::Legacy(Message::new(&[close_ix], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); // Should pass because allow_close_account is true by default assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); @@ -1100,7 +1132,8 @@ mod tests { .unwrap(); let message = VersionedMessage::Legacy(Message::new(&[close_ix], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); // Should fail because fee payer cannot close accounts when allow_close_account is false assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); @@ -1133,7 +1166,8 @@ mod tests { .unwrap(); let message = VersionedMessage::Legacy(Message::new(&[approve_ix], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); // Should pass because allow_approve is true by default assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); @@ -1156,7 +1190,8 @@ mod tests { .unwrap(); let message = VersionedMessage::Legacy(Message::new(&[approve_ix], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); // Should fail because fee payer cannot approve when allow_approve is false assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); @@ -1177,7 +1212,8 @@ mod tests { let message = VersionedMessage::Legacy(Message::new(&[approve_checked_ix], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); // Should also fail for approve_checked assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); @@ -1210,7 +1246,8 @@ mod tests { .unwrap(); let message = VersionedMessage::Legacy(Message::new(&[burn_ix], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); // Should fail for Token2022 burn assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); } @@ -1241,7 +1278,8 @@ mod tests { .unwrap(); let message = VersionedMessage::Legacy(Message::new(&[close_ix], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); // Should fail for Token2022 close account assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); } @@ -1273,7 +1311,8 @@ mod tests { .unwrap(); let message = VersionedMessage::Legacy(Message::new(&[approve_ix], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); // Should pass because allow_approve is true by default assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); @@ -1297,7 +1336,8 @@ mod tests { .unwrap(); let message = VersionedMessage::Legacy(Message::new(&[approve_ix], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); // Should fail because fee payer cannot approve when allow_approve is false assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); @@ -1318,7 +1358,8 @@ mod tests { let message = VersionedMessage::Legacy(Message::new(&[approve_checked_ix], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); // Should also fail for approve_checked assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); @@ -1342,7 +1383,8 @@ mod tests { let validator = TransactionValidator::new(fee_payer).unwrap(); let instruction = create_account(&fee_payer, &new_account, 1000, 100, &owner); let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); // Test with allow_create_account = false @@ -1354,7 +1396,8 @@ mod tests { let validator = TransactionValidator::new(fee_payer).unwrap(); let instruction = create_account(&fee_payer, &new_account, 1000, 100, &owner); let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); } @@ -1374,7 +1417,8 @@ mod tests { let validator = TransactionValidator::new(fee_payer).unwrap(); let instruction = allocate(&fee_payer, 100); let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); // Test with allow_allocate = false @@ -1386,7 +1430,8 @@ mod tests { let validator = TransactionValidator::new(fee_payer).unwrap(); let instruction = allocate(&fee_payer, 100); let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); } @@ -1409,7 +1454,8 @@ mod tests { // Only test the InitializeNonceAccount instruction (second one) let message = VersionedMessage::Legacy(Message::new(&[instructions[1].clone()], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); // Test with allow_initialize = false @@ -1422,7 +1468,8 @@ mod tests { let instructions = create_nonce_account(&fee_payer, &nonce_account, &fee_payer, 1_000_000); let message = VersionedMessage::Legacy(Message::new(&[instructions[1].clone()], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); } @@ -1443,7 +1490,8 @@ mod tests { let validator = TransactionValidator::new(fee_payer).unwrap(); let instruction = advance_nonce_account(&nonce_account, &fee_payer); let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); // Test with allow_advance = false @@ -1455,7 +1503,8 @@ mod tests { let validator = TransactionValidator::new(fee_payer).unwrap(); let instruction = advance_nonce_account(&nonce_account, &fee_payer); let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); } @@ -1477,7 +1526,8 @@ mod tests { let validator = TransactionValidator::new(fee_payer).unwrap(); let instruction = withdraw_nonce_account(&nonce_account, &fee_payer, &recipient, 1000); let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); // Test with allow_withdraw = false @@ -1489,7 +1539,8 @@ mod tests { let validator = TransactionValidator::new(fee_payer).unwrap(); let instruction = withdraw_nonce_account(&nonce_account, &fee_payer, &recipient, 1000); let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); } @@ -1511,7 +1562,8 @@ mod tests { let validator = TransactionValidator::new(fee_payer).unwrap(); let instruction = authorize_nonce_account(&nonce_account, &fee_payer, &new_authority); let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok()); // Test with allow_authorize = false @@ -1523,7 +1575,8 @@ mod tests { let validator = TransactionValidator::new(fee_payer).unwrap(); let instruction = authorize_nonce_account(&nonce_account, &fee_payer, &new_authority); let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer))); - let mut transaction = TransactionUtil::new_unsigned_versioned_transaction_resolved(message); + let mut transaction = + TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap(); assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err()); } From 3b6e988b09bdf5f029476ca23228fbe7c70e12b4 Mon Sep 17 00:00:00 2001 From: amilz <85324096+amilz@users.noreply.github.com> Date: Fri, 31 Oct 2025 08:47:26 -0700 Subject: [PATCH 26/29] chore: docs updates (#246) * chore: update ADDING_SIGNERS.md update based on using the solana signers crate closes PRO-513 * chore: update docs with new Feepayer Policy Config - update CLAUDE.md - update CONFIGURATION.md - update FEES.md - update x402 guide closes PRO-501 --- CLAUDE.md | 98 ++++- docs/contributors/ADDING_SIGNERS.md | 578 +++++++++++----------------- docs/operators/CONFIGURATION.md | 174 +++++++-- docs/operators/FEES.md | 23 +- docs/x402/demo/X402_DEMO_GUIDE.md | 44 ++- 5 files changed, 490 insertions(+), 427 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b6c46891..055c5175 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -348,12 +348,47 @@ allowed_spl_paid_tokens = [ disallowed_accounts = [] # Blocked account addresses # Fee payer policy controls what actions the fee payer can perform -# All default to true for backward compatibility -[validation.fee_payer_policy] -allow_sol_transfers = true # Allow fee payer to be source in SOL transfers -allow_spl_transfers = true # Allow fee payer to be source in SPL token transfers -allow_token2022_transfers = true # Allow fee payer to be source in Token2022 transfers -allow_assign = true # Allow fee payer to use Assign instruction +# Organized by program type with 28 granular controls +# All default to false for security +[validation.fee_payer_policy.system] +allow_transfer = false # System Transfer/TransferWithSeed +allow_assign = false # System Assign/AssignWithSeed +allow_create_account = false # System CreateAccount/CreateAccountWithSeed +allow_allocate = false # System Allocate/AllocateWithSeed + +[validation.fee_payer_policy.system.nonce] +allow_initialize = false # InitializeNonceAccount +allow_advance = false # AdvanceNonceAccount +allow_authorize = false # AuthorizeNonceAccount +allow_withdraw = false # WithdrawNonceAccount + +[validation.fee_payer_policy.spl_token] +allow_transfer = false # Transfer/TransferChecked +allow_burn = false # Burn/BurnChecked +allow_close_account = false # CloseAccount +allow_approve = false # Approve/ApproveChecked +allow_revoke = false # Revoke +allow_set_authority = false # SetAuthority +allow_mint_to = false # MintTo/MintToChecked +allow_initialize_mint = false # InitializeMint/InitializeMint2 +allow_initialize_account = false # InitializeAccount/InitializeAccount3 +allow_initialize_multisig = false # InitializeMultisig/InitializeMultisig2 +allow_freeze_account = false # FreezeAccount +allow_thaw_account = false # ThawAccount + +[validation.fee_payer_policy.token_2022] +allow_transfer = false # Transfer/TransferChecked +allow_burn = false # Burn/BurnChecked +allow_close_account = false # CloseAccount +allow_approve = false # Approve/ApproveChecked +allow_revoke = false # Revoke +allow_set_authority = false # SetAuthority +allow_mint_to = false # MintTo/MintToChecked +allow_initialize_mint = false # InitializeMint/InitializeMint2 +allow_initialize_account = false # InitializeAccount/InitializeAccount3 +allow_initialize_multisig = false # InitializeMultisig/InitializeMultisig2 +allow_freeze_account = false # FreezeAccount +allow_thaw_account = false # ThawAccount ``` ### Environment Variables set in `signers.toml` @@ -367,29 +402,60 @@ RUST_LOG=debug # Logging level ### Overview -The fee payer policy system provides fine-grained control over what actions the fee payer can perform in transactions. By default, all actions are permitted to maintain backward compatibility with existing behavior. +The fee payer policy system provides granular control over what actions the fee payer can perform in transactions. The policy is organized by program type (System, SPL Token, Token-2022) and covers 28 different instruction types. By default, all actions are permitted to maintain backward compatibility with existing behavior. ### Policy Configuration -The fee payer policy is configured via the `[validation.fee_payer_policy]` section in `kora.toml`: +The fee payer policy is configured via nested sections in `kora.toml`: +- `[validation.fee_payer_policy.system]` - System program instructions (4 fields) +- `[validation.fee_payer_policy.system.nonce]` - Nonce account operations (4 fields) +- `[validation.fee_payer_policy.spl_token]` - SPL Token program instructions (12 fields) +- `[validation.fee_payer_policy.token_2022]` - Token-2022 program instructions (12 fields) ### Implementation Details **Core Structure** (`crates/lib/src/config.rs`): -- `FeePayerPolicy` struct with 4 boolean fields -- `Default` implementation sets all fields to `true` (permissive) +- `FeePayerPolicy` struct with nested policy structs for each program type +- `SystemInstructionPolicy` - Controls System program operations including nested `NonceInstructionPolicy` +- `SplTokenInstructionPolicy` - Controls SPL Token program operations +- `Token2022InstructionPolicy` - Controls Token-2022 program operations +- All `Default` implementations set fields to `true` (permissive) for backward compatibility - `#[serde(default)]` attribute ensures backward compatibility **Validation Logic** (`crates/lib/src/transaction/validator.rs`): - `TransactionValidator` stores the policy configuration -- `is_fee_payer_source()` method checks policy flags before validating restrictions +- Program-specific validation methods check policy flags before validating restrictions +- Uses macro-based validation patterns for consistent enforcement across instruction types - Different validation logic for each program type (System, SPL Token, Token2022) -**Supported Actions**: -1. **SOL Transfers** - System program Transfer and TransferWithSeed instructions -2. **SPL Token Transfers** - SPL Token program Transfer and TransferChecked instructions -3. **Token2022 Transfers** - Token2022 program Transfer and TransferChecked instructions -4. **Assign** - System program Assign instruction (changes account owner) +**Supported Actions by Program Type**: + +**System Program (8 controls)**: +1. **Transfer** - Transfer and TransferWithSeed instructions (fee payer as sender) +2. **Assign** - Assign and AssignWithSeed instructions (fee payer as authority) +3. **CreateAccount** - CreateAccount and CreateAccountWithSeed instructions (fee payer as funding source) +4. **Allocate** - Allocate and AllocateWithSeed instructions (fee payer as account owner) +5. **Nonce Initialize** - InitializeNonceAccount instruction (fee payer set as authority) +6. **Nonce Advance** - AdvanceNonceAccount instruction (fee payer as authority) +7. **Nonce Authorize** - AuthorizeNonceAccount instruction (fee payer as current authority) +8. **Nonce Withdraw** - WithdrawNonceAccount instruction (fee payer as authority) + +**SPL Token Program (12 controls)**: +1. **Transfer** - Transfer and TransferChecked instructions (fee payer as owner) +2. **Burn** - Burn and BurnChecked instructions (fee payer as owner) +3. **CloseAccount** - CloseAccount instruction (fee payer as owner) +4. **Approve** - Approve and ApproveChecked instructions (fee payer as owner) +5. **Revoke** - Revoke instruction (fee payer as owner) +6. **SetAuthority** - SetAuthority instruction (fee payer as current authority) +7. **MintTo** - MintTo and MintToChecked instructions (fee payer as mint authority) +8. **InitializeMint** - InitializeMint and InitializeMint2 instructions (fee payer as mint authority) +9. **InitializeAccount** - InitializeAccount and InitializeAccount3 instructions (fee payer as owner) +10. **InitializeMultisig** - InitializeMultisig and InitializeMultisig2 instructions (fee payer as signer) +11. **FreezeAccount** - FreezeAccount instruction (fee payer as freeze authority) +12. **ThawAccount** - ThawAccount instruction (fee payer as freeze authority) + +**Token-2022 Program (12 controls)**: +- Identical instruction set and controls as SPL Token Program ## Private Key Formats diff --git a/docs/contributors/ADDING_SIGNERS.md b/docs/contributors/ADDING_SIGNERS.md index 7eee6c1a..4ff67135 100644 --- a/docs/contributors/ADDING_SIGNERS.md +++ b/docs/contributors/ADDING_SIGNERS.md @@ -6,397 +6,250 @@ This guide is for wallet service providers who want to integrate their key manag ## Architecture Overview -Kora uses an enum-based architecture where all signers are wrapped in a unified `KoraSigner` enum. Your signer will be added as a new variant to this enum, allowing Kora to switch between different signing providers at runtime based on CLI flags. +Kora uses the external [`solana-signers`](https://github.com/solana-foundation/solana-signers) crate for all signing operations. This architecture provides a unified signing interface for signing Solana transactions. To add a new signer to Kora, you'll need to: + +1. **First**: Add your signer implementation to the `solana-signers` crate +2. **Second**: Add configuration support for your signer in Kora ## Step-by-Step Integration Guide ### Quick Integration Checklist -- [ ] Create your Signer Module - - Define types and configuration - - Implement `KoraSigner`'s core signing methods (`sign` and `sign_solana`) - - Add initialization logic based on your API's requirements -- [ ] Update the `KoraSigner` enum -- [ ] Update `SignerTypeConfig` enum in `config.rs` for multi-signer support -- [ ] Add initialization logic in `init.rs` -- [ ] Add CLI arguments +**Part 1: Add Signer to `solana-signers` Crate** +- [ ] Implement your signer following the [solana-signers integration guide](https://github.com/solana-foundation/solana-signers/blob/main/docs/ADDING_SIGNERS.md) +- [ ] Wait for PR approval and crate publication + +**Part 2: Add Kora Configuration Support** +- [ ] Update Cargo.toml's dependency so that `solana-signers` crate uses the latest version (that includes your signer) +- [ ] Add configuration struct for your signer's environment variables +- [ ] Update `SignerTypeConfig` enum in `crates/lib/src/signer/config.rs` +- [ ] Add validation logic for your signer's config +- [ ] Add build logic to construct your signer from config +- [ ] Export configuration struct in `crates/lib/src/signer/mod.rs` +- [ ] (Optional) Add test mock builder in `crates/lib/src/tests/config_mock.rs` +- [ ] Update example configuration files - [ ] Update test scripts to include your signer (see below) - [ ] Update documentation to include your signer (see below) -- [ ] Submit PR +- [ ] Submit PR to Kora repository -### Step 1: Create Your Signer Module +### Add Signer Support in Kora -Create a new directory under `crates/lib/src/signer/` for your implementation: +First, ensure your signer is supported in the `solana-signers` crate. If it is not, follow the guide at: +**[https://github.com/solana-foundation/solana-signers/blob/main/docs/ADDING_SIGNERS.md](https://github.com/solana-foundation/solana-signers/blob/main/docs/ADDING_SIGNERS.md)** -```bash -crates/lib/src/signer/ -โ”œโ”€โ”€ your_service/ -โ”‚ โ”œโ”€โ”€ mod.rs # Module exports -โ”‚ โ”œโ”€โ”€ signer.rs # Main implementation -โ”‚ โ”œโ”€โ”€ config.rs # Configuration -โ”‚ โ””โ”€โ”€ types.rs # Types and configuration -``` +#### Step 1: Update Cargo.toml -Export each of the files in `mod.rs`: +Update Cargo.toml's dependency so that `solana-signers` crate uses the latest version (that includes your signer): -```rust -pub mod config; -pub mod signer; -pub mod types; +```toml +[dependencies] +solana-signers = { version = "X.Y.Z", default-features = false, features = [ + "all", + "sdk-v3", +] } ``` -### Step 2: Define Your Types +#### Step 2: Define Your Configuration Struct -In `your_service/types.rs`, define your signer struct and any necessary types (e.g. error types, config, API request/response types, etc.): +In `crates/lib/src/signer/config.rs`, add a new configuration struct for your signer that defines which environment variables are needed. For example: ```rust -use reqwest::Client; -use serde::{Deserialize, Serialize}; -use solana_sdk::pubkey::Pubkey; - -#[derive(Clone, Debug)] -pub struct YourServiceSigner { - // Your API credentials - pub api_key: String, - pub api_secret: String, - pub wallet_id: String, - - // HTTP client for API calls - pub client: Client, - - // Cache the public key - pub public_key: Pubkey, - - // Your API base URL - pub api_base_url: String, -} - -// Error types for your signer -#[derive(Debug)] -pub enum YourServiceError { - MissingConfig(&'static str), - ApiError(u16), - InvalidSignature, - RateLimitExceeded, - // Add more as needed -} - -// Implement Display and Error traits -impl std::fmt::Display for YourServiceError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - // Implementation - } -} - -impl std::error::Error for YourServiceError {} - -// API request/response types -#[derive(Serialize)] -pub struct SignTransactionRequest { - pub method: &'static str, - pub params: SignTransactionParams, -} - -#[derive(Deserialize, Debug)] -pub struct SignTransactionResponse { - pub method: String, - pub data: SignTransactionData, +/// YourService signer configuration +#[derive(Clone, Serialize, Deserialize)] +pub struct YourServiceSignerConfig { + pub api_key_env: String, + pub api_secret_env: String, + pub wallet_id_env: String, } ``` -Consider implementing your error types as a `KoraError` variant in `crates/lib/src/error.rs`. - -### Step 3: Implement the Signer Methods - -In `your_service/signer.rs`, implement the core signing logic. Some methods you should implement are: +#### Step 2: Add Your Signer to `SignerTypeConfig` Enum -- `new`: Create a new instance of your signer -- `solana_pubkey`: Get the Solana public key for this signer (as a Solana `Pubkey`) -- `sign`: Sign a `VersionedTransaction` and return `Vec` (raw bytes) -- `sign_solana`: Sign a `VersionedTransaction` and return a Solana `Signature` -- other methods core to your signer's implementation (e.g., `init`, `call_signing_api`, etc.) +Add your signer variant to the `SignerTypeConfig` enum in `crates/lib/src/signer/config.rs`: ```rust -use crate::signer::Signature as KoraSignature; -use solana_sdk::{ - signature::Signature, - transaction::VersionedTransaction, -}; +/// Signer type-specific configuration +#[derive(Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum SignerTypeConfig { + // Existing signer variants + Memory { #[serde(flatten)] config: MemorySignerConfig }, + // ... existing variants ... -impl YourServiceSigner { - /// Create a new instance of your signer - pub fn new( - api_key: String, - api_secret: String, - wallet_id: String, - ) -> Result { - Ok(Self { - api_key, - api_secret, - wallet_id, - client: reqwest::Client::new(), - public_key: Pubkey::default(), // Will be initialized later - api_base_url: "https://api.yourservice.com/v1".to_string(), - }) - } - - /// Get the Solana public key for this signer - pub fn solana_pubkey(&self) -> Pubkey { - self.public_key - } - - /// Sign a transaction and return raw bytes - pub async fn sign( - &self, - transaction: &VersionedTransaction, - ) -> Result, YourServiceError> { - // 1. Serialize the transaction message - let message_bytes = transaction.message.serialize(); - - // 2. Call your API to sign - let signature = self.call_signing_api(&message_bytes).await?; - - // 3. Return the signature bytes - Ok(signature) - } - - /// Sign and return a Solana signature - pub async fn sign_solana( - &self, - transaction: &VersionedTransaction, - ) -> Result { - let sig_bytes = self.sign(transaction).await?; - - // Convert to Solana signature (must be exactly 64 bytes) - let sig_array: [u8; 64] = sig_bytes - .try_into() - .map_err(|_| YourServiceError::InvalidSignature)?; - - Ok(Signature::from(sig_array)) - } - - // Private helper methods - async fn call_signing_api( - &self, - message: &[u8], - ) -> Result, YourServiceError> { - // Implementation specific to your API - } + // Add your signer here + YourService { + #[serde(flatten)] + config: YourServiceSignerConfig, + }, } ``` -### Step 4: Update the KoraSigner Enum +#### Step 3: Add Build Logic -Add your signer to the `KoraSigner` enum in `crates/lib/src/signer/signer.rs`: +In the same file (`config.rs`), add a method to build your signer from configuration in the `SignerConfig` implementation: ```rust -#[derive(Clone)] -pub enum KoraSigner { - Memory(SolanaMemorySigner), - Turnkey(TurnkeySigner), - Vault(VaultSigner), - Privy(PrivySigner), - YourService(YourServiceSigner), // Add your signer here -} - -// Update the trait implementation -impl KoraSigner { - pub fn solana_pubkey(&self) -> Pubkey { - match self { - // ... existing implementations - KoraSigner::YourService(signer) => signer.solana_pubkey(), - } - } -} +impl SignerConfig { + pub async fn build_signer_from_config(config: &SignerConfig) -> Result { + match &config.config { + // ... existing cases -impl super::Signer for KoraSigner { - type Error = KoraError; - - async fn sign( - &self, - transaction: &VersionedTransaction, - ) -> Result { - match self { - // ... existing implementations - KoraSigner::YourService(signer) => { - let sig = signer.sign(transaction).await?; - Ok(super::Signature { - bytes: sig, - is_partial: false, - }) + SignerTypeConfig::YourService { config: your_service_config } => { + Self::build_your_service_signer(your_service_config, &config.name).await } } } - - async fn sign_solana( - &self, - transaction: &VersionedTransaction, - ) -> Result { - match self { - // ... existing implementations - KoraSigner::YourService(signer) => { - signer.sign_solana(transaction) - .await - .map_err(KoraError::from) - } - } + + // Add this new method + async fn build_your_service_signer( + config: &YourServiceSignerConfig, + signer_name: &str, + ) -> Result { + // Update the environment variable names to match your signer's configuration + let api_key = get_env_var_for_signer(&config.api_key_env, signer_name)?; + let api_secret = get_env_var_for_signer(&config.api_secret_env, signer_name)?; + let wallet_id = get_env_var_for_signer(&config.wallet_id_env, signer_name)?; + + // Call the constructor from solana-signers crate + Signer::from_your_service(api_key, api_secret, wallet_id) + .await + .map_err(|e| { + KoraError::SigningError(format!( + "Failed to create YourService signer '{signer_name}': {}", + sanitize_error!(e) + )) + }) } } ``` -### Step 5: Update the SignerTypeConfig Enum - -Add your signer to the `SignerTypeConfig` enum in `crates/lib/src/signer/config.rs`: +**Note**: The method name `Signer::from_your_service()` should match what you implemented in the `solana-signers` crate. -```rust -/// Signer type-specific configuration with environment variable references -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum SignerTypeConfig { - // ... existing variants - - /// YourService signer configuration - YourService { - /// Environment variable for YourService API key - api_key_env: String, - /// Environment variable for YourService API secret - api_secret_env: String, - /// Environment variable for YourService wallet ID - wallet_id_env: String, - }, -} -``` +#### Step 4: Add Validation Logic -Also update the `build_signer_from_config` method in the same file: +Add validation for your signer's configuration in the `validate_individual_signer_config` method: ```rust impl SignerConfig { - pub async fn build_signer_from_config(config: &SignerConfig) -> Result { - match &config.config { + pub fn validate_individual_signer_config(&self, index: usize) -> Result<(), KoraError> { + // ... existing validation + + match &self.config { // ... existing cases - - SignerTypeConfig::YourService { api_key_env, api_secret_env, wallet_id_env } => { - let api_key = get_env_var_for_signer(api_key_env, &config.name)?; - let api_secret = get_env_var_for_signer(api_secret_env, &config.name)?; - let wallet_id = get_env_var_for_signer(wallet_id_env, &config.name)?; - - let signer = YourServiceSigner::new(api_key, api_secret, wallet_id) - .map_err(|e| { - KoraError::ValidationError(format!( - "Failed to create YourService signer '{}': {}", - config.name, e - )) - })?; - - Ok(KoraSigner::YourService(signer)) + + SignerTypeConfig::YourService { config } => { + Self::validate_your_service_config(config, &self.name) } } } -} -``` -And update the validation method: - -```rust -fn validate_individual_signer_config(&self, index: usize) -> Result<(), KoraError> { - match &self.config { - // ... existing cases - - SignerTypeConfig::YourService { api_key_env, api_secret_env, wallet_id_env } => { - let env_vars = [ - ("api_key_env", api_key_env), - ("api_secret_env", api_secret_env), - ("wallet_id_env", wallet_id_env), - ]; - for (field_name, env_var) in env_vars { - if env_var.is_empty() { - return Err(KoraError::ValidationError(format!( - "YourService signer '{}' must specify non-empty {}", - self.name, field_name - ))); - } + // Add this new validation method + fn validate_your_service_config( + config: &YourServiceSignerConfig, + signer_name: &str, + ) -> Result<(), KoraError> { + // Update the environment variable names to match your signer's configuration + let env_vars = [ + ("api_key_env", &config.api_key_env), + ("api_secret_env", &config.api_secret_env), + ("wallet_id_env", &config.wallet_id_env), + ]; + + for (field_name, env_var) in env_vars { + if env_var.is_empty() { + return Err(KoraError::ValidationError(format!( + "YourService signer '{signer_name}' must specify non-empty {field_name}" + ))); } } + Ok(()) } - Ok(()) } ``` -### Step 6: Add Initialization Logic +#### Step 5: Export Your Configuration -Update `crates/lib/src/signer/init.rs` to include your signer: +Add your new config struct to the module exports in `crates/lib/src/signer/mod.rs` (or at the top of the file if it's public): -- add `init_your_service_signer` to define your service's signer as a `KoraSigner` -- add your service to the `init_signer_type` function if specified in the CLI args (we'll be adding these in the next step) +```rust +pub use config::{ + MemorySignerConfig, + PrivySignerConfig, + SignerTypeConfig, + TurnkeySignerConfig, + VaultSignerConfig, + YourServiceSignerConfig, // Add this + // ... other exports +}; +``` + + +## Testing Your Integration + +### Add Test Mock Builder + +To make testing easier, add a builder method to `SignerPoolConfigBuilder` in `crates/lib/src/tests/config_mock.rs`: ```rust -// Add your args struct import -use crate::rpc_server::args::YourServiceArgs; - -pub fn init_signer_type(args: &RpcArgs) -> Result { - if args.turnkey_args.turnkey_signer { - init_turnkey_signer(&args.turnkey_args) - } else if args.vault_args.vault_signer { - init_vault_signer(&args.vault_args) - } else if args.privy_args.privy_signer { - init_privy_signer(&args.privy_args) - } else if args.your_service_args.your_service_signer { - init_your_service_signer(&args.your_service_args) - } else { - init_memory_signer(args.private_key.as_ref()) - } -} +impl SignerPoolConfigBuilder { + // ... existing methods -fn init_your_service_signer(config: &YourServiceArgs) -> Result { - // Extract required configuration - let api_key = config - .your_service_api_key - .clone() - .or_else(|| std::env::var("YOUR_SERVICE_API_KEY").ok()) - .ok_or_else(|| KoraError::SigningError("YourService API key required".to_string()))?; - - let api_secret = config - .your_service_api_secret - .clone() - .or_else(|| std::env::var("YOUR_SERVICE_API_SECRET").ok()) - .ok_or_else(|| KoraError::SigningError("YourService API secret required".to_string()))?; - - let wallet_id = config - .your_service_wallet_id - .clone() - .or_else(|| std::env::var("YOUR_SERVICE_WALLET_ID").ok()) - .ok_or_else(|| KoraError::SigningError("YourService wallet ID required".to_string()))?; - - // Create the signer - let mut signer = YourServiceSigner::new(api_key, api_secret, wallet_id)?; - - // Initialize if needed (fetch public key, etc) - // Note: This would need to be handled async in practice - - Ok(KoraSigner::YourService(signer)) + pub fn with_your_service_signer( + mut self, + name: String, + api_key_env: String, + api_secret_env: String, + wallet_id_env: String, + weight: Option, + ) -> Self { + let signer = SignerConfig { + name, + weight, + config: SignerTypeConfig::YourService { + config: YourServiceSignerConfig { + api_key_env, + api_secret_env, + wallet_id_env, + }, + }, + }; + self.config.signers.push(signer); + self + } } ``` -### Step 7: Export Your Module - -Update `crates/lib/src/signer/mod.rs`: +This allows other tests to easily create mock configurations that include your signer: ```rust -pub mod your_service; -// ... other modules - -pub use your_service::types::YourServiceSigner; +use crate::tests::config_mock::SignerPoolConfigBuilder; + +let config = SignerPoolConfigBuilder::new() + .with_your_service_signer( + "yourservice_test".to_string(), + "YOUR_SERVICE_API_KEY".to_string(), + "YOUR_SERVICE_API_SECRET".to_string(), + "YOUR_SERVICE_WALLET_ID".to_string(), + Some(1) + ) + .build(); ``` -## Testing Your Integration ### Environment Variables -Define a `example-signer.toml` with your signer's configuration and necessary environment variables defined. Add the example environment variables to the following files: `.env.example` and `.env` in the root of the project, and in `./sdks/ts/`: +Add the example environment variables to the following files: +- `.env.example` (root of the project) +- `.env` (root of the project, for local testing) +- `./sdks/ts/.env.example` +- `./sdks/ts/.env` -- `YOUR_SERVICE_API_KEY`: The API key for your service. -- `YOUR_SERVICE_API_SECRET`: The API secret for your service. -- `YOUR_SERVICE_WALLET_ID`: The wallet ID for your service. +```bash +# YourService Signer Configuration +YOUR_SERVICE_API_KEY=your_api_key_here +YOUR_SERVICE_API_SECRET=your_api_secret_here +YOUR_SERVICE_WALLET_ID=your_wallet_id_here +``` ### Integration Tests @@ -408,6 +261,9 @@ Create a new signer configuration file in `tests/src/common/fixtures/` for your ```toml # tests/src/common/fixtures/signers-your-service.toml +[signer_pool] +strategy = "round_robin" + [[signers]] name = "yourservice_main" type = "your_service" @@ -429,19 +285,7 @@ port = "8090" # Use a unique port tests = ["your_service"] ``` -#### 3. TypeScript SDK Integration - -For TypeScript SDK testing with your signer: - -1. Update `sdks/ts/test/setup.ts` to recognize your signer type: - - Add environment variable handling for `KORA_SIGNER_TYPE=your-service` - -2. Add a test script in `sdks/ts/package.json`: - ```json - "test:integration:your-service": "KORA_SIGNER_TYPE=your-service pnpm test integration.test.ts" - ``` - -#### 4. Running Tests +#### 3. Running Tests Make sure your environment is set up: @@ -502,63 +346,71 @@ YOUR_SERVICE_API_SECRET="your_api_secret" YOUR_SERVICE_WALLET_ID="your_wallet_id" \``` -### Configure Signer.toml +### Configure signers.toml + +\```toml +[signer_pool] +strategy = "round_robin" -\```bash [[signers]] name = "yourservice_main" type = "your_service" api_key_env = "YOUR_SERVICE_API_KEY" api_secret_env = "YOUR_SERVICE_API_SECRET" wallet_id_env = "YOUR_SERVICE_WALLET_ID" +weight = 1 \``` +### Run Kora with YourService Signer + +\```bash +kora rpc start --signers-config signers.toml +\``` +``` ### 2. Update README Add your service to the main README's signer list. -### 3. Add Example Configuration - -Create an example `.env` configuration: - -```bash -# YourService Signer Configuration -YOUR_SERVICE_API_KEY=your_api_key_here -YOUR_SERVICE_API_SECRET=your_api_secret_here -YOUR_SERVICE_WALLET_ID=your_wallet_id_here -``` - ## Submission Checklist -Before submitting your PR: - +- [ ] Your signer is supported in the `solana-signers` crate +- [ ] Updated `solana-signers` dependency in Cargo.toml to latest version +- [ ] Added configuration struct for your signer +- [ ] Added `SignerTypeConfig` variant +- [ ] Added build logic in `build_signer_from_config` +- [ ] Added validation logic in `validate_individual_signer_config` +- [ ] Exported configuration struct in `mod.rs` +- [ ] (Optional) Added test mock builder method in `config_mock.rs` - [ ] Code compiles without warnings -- [ ] All tests pass -- [ ] Documentation is complete -- [ ] Example configuration (.env.example) provided +- [ ] All tests pass (`make test` and `make test-integration`) +- [ ] Documentation added to [`docs/operators/SIGNERS.md`](/docs/operators/SIGNERS.md) +- [ ] Example configuration files created (`.toml` and `.env.example`) - [ ] No hardcoded values or secrets - [ ] Error messages are helpful - [ ] Follows Rust naming conventions (snake_case) -- [ ] Linting passes (`make lint` and `make format-ts-sdk`) +- [ ] Linting passes (`make lint`) - [ ] Contact the Kora team with API Keys for integration testing ## Getting Help -- Open an issue for design discussions +- **For signer implementation**: Open an issue in the [`solana-signers`](https://github.com/solana-foundation/solana-signers) repository +- **For Kora integration**: Open an issue in the Kora repository for design discussions - Join our community channels -- Review existing signer implementations for patterns in [`crates/lib/src/signer/`](/crates/lib/src/signer/) +- Review existing signer configurations in [`crates/lib/src/signer/config.rs`](/crates/lib/src/signer/config.rs) ## Example PR Structure -```sh -feat(signer): add YourService signer integration - -- Implement Signer trait for YourService -- Add CLI arguments and initialization -- Add signer to Signers Guide -- Add integration tests script to Makefile - +**For Kora repository:** +``` +feat(signer): add YourService signer configuration support + +- Add YourServiceSignerConfig struct +- Add YourService variant to SignerTypeConfig enum +- Add build and validation logic for YourService +- Add example configuration files +- Add documentation to SIGNERS.md +- Add integration tests ``` Welcome to the Kora ecosystem! We're excited to have your key management solution as part of the platform. diff --git a/docs/operators/CONFIGURATION.md b/docs/operators/CONFIGURATION.md index 86a058d4..6317e6a6 100644 --- a/docs/operators/CONFIGURATION.md +++ b/docs/operators/CONFIGURATION.md @@ -277,34 +277,96 @@ blocked_account_extensions = [ ## Fee Payer Policy -The `[validation.fee_payer_policy]` section restricts what your Kora node's fee payer wallet can do. This is important to prevent unexpected behavior from users' transactions utilizing your Kora node as a signer. For example, if `allow_spl_transfers` is set to `false`, the Kora node would not sign transactions that include an SPL token transfer where the Kora node's fee payer is the instruction's authority. +The `[validation.fee_payer_policy]` section provides granular control over what actions your Kora node's fee payer wallet can perform. The policy is organized by program type (System, SPL Token, Token-2022) and covers all different instruction types. This prevents unexpected behavior from users' transactions utilizing your Kora node as a signer. +For example, if `spl_token.allow_transfer` is set to `false`, the Kora node will not sign transactions that include an SPL token transfer where the Kora node's fee payer is the transfer authority. ```toml [validation.fee_payer_policy] -allow_sol_transfers = false -allow_spl_transfers = false -allow_token2022_transfers = false -allow_assign = false -allow_burn = false -allow_close_account = false -allow_approve = false -``` -| Option | Description | Required | Type | -|--------|-------------|---------|---------| -| `allow_sol_transfers` | Allow SOL transfers where the Kora node's fee payer is the signer/authority | โœ… | boolean | -| `allow_spl_transfers` | Allow SPL token transfers where the Kora node's fee payer is the signer/authority | โœ… | boolean | -| `allow_token2022_transfers` | Allow Token-2022 transfers where the Kora node's fee payer is the signer/authority | โœ… | boolean | -| `allow_assign` | Allow account ownership changes where the Kora node's fee payer is the signer/authority | โœ… | boolean | -| `allow_burn` | Allow token burn operations where the Kora node's fee payer is the signer/authority | โœ… | boolean | -| `allow_close_account` | Allow closing token accounts where the Kora node's fee payer is the signer/authority | โœ… | boolean | -| `allow_approve` | Allow token delegation/approval where the Kora node's fee payer is the signer/authority | โœ… | boolean | +[validation.fee_payer_policy.system] +allow_transfer = false # System Transfer/TransferWithSeed +allow_assign = false # System Assign/AssignWithSeed +allow_create_account = false # System CreateAccount/CreateAccountWithSeed +allow_allocate = false # System Allocate/AllocateWithSeed + +[validation.fee_payer_policy.system.nonce] +allow_initialize = false # InitializeNonceAccount +allow_advance = false # AdvanceNonceAccount +allow_authorize = false # AuthorizeNonceAccount +allow_withdraw = false # WithdrawNonceAccount + +[validation.fee_payer_policy.spl_token] +allow_transfer = false # Transfer/TransferChecked +allow_burn = false # Burn/BurnChecked +allow_close_account = false # CloseAccount +allow_approve = false # Approve/ApproveChecked +allow_revoke = false # Revoke +allow_set_authority = false # SetAuthority +allow_mint_to = false # MintTo/MintToChecked +allow_initialize_mint = false # InitializeMint/InitializeMint2 +allow_initialize_account = false # InitializeAccount/InitializeAccount3 +allow_initialize_multisig = false # InitializeMultisig/InitializeMultisig2 +allow_freeze_account = false # FreezeAccount +allow_thaw_account = false # ThawAccount + +[validation.fee_payer_policy.token_2022] +allow_transfer = false # Transfer/TransferChecked +allow_burn = false # Burn/BurnChecked +allow_close_account = false # CloseAccount +allow_approve = false # Approve/ApproveChecked +allow_revoke = false # Revoke +allow_set_authority = false # SetAuthority +allow_mint_to = false # MintTo/MintToChecked +allow_initialize_mint = false # InitializeMint/InitializeMint2 +allow_initialize_account = false # InitializeAccount/InitializeAccount3 +allow_initialize_multisig = false # InitializeMultisig/InitializeMultisig2 +allow_freeze_account = false # FreezeAccount +allow_thaw_account = false # ThawAccount +``` +### System Program Instructions + +| Option | Description | Default | Type | +|--------|-------------|---------|------| +| `allow_transfer` | Allow fee payer as sender in Transfer/TransferWithSeed instructions | `false` | boolean | +| `allow_assign` | Allow fee payer as authority in Assign/AssignWithSeed instructions | `false` | boolean | +| `allow_create_account` | Allow fee payer as funding payer in CreateAccount/CreateAccountWithSeed instructions | `false` | boolean | +| `allow_allocate` | Allow fee payer as account owner in Allocate/AllocateWithSeed instructions | `false` | boolean | +| `nonce.allow_initialize` | Allow fee payer to be set as nonce authority in InitializeNonceAccount | `false` | boolean | +| `nonce.allow_advance` | Allow fee payer as authority in AdvanceNonceAccount | `false` | boolean | +| `nonce.allow_authorize` | Allow fee payer as current authority in AuthorizeNonceAccount | `false` | boolean | +| `nonce.allow_withdraw` | Allow fee payer as authority in WithdrawNonceAccount | `false` | boolean | + +### SPL Token Program Instructions + +| Option | Description | Default | Type | +|--------|-------------|---------|------| +| `allow_transfer` | Allow fee payer as owner in Transfer/TransferChecked instructions | `false` | boolean | +| `allow_burn` | Allow fee payer as owner in Burn/BurnChecked instructions | `false` | boolean | +| `allow_close_account` | Allow fee payer as owner in CloseAccount instructions | `false` | boolean | +| `allow_approve` | Allow fee payer as owner in Approve/ApproveChecked instructions | `false` | boolean | +| `allow_revoke` | Allow fee payer as owner in Revoke instructions | `false` | boolean | +| `allow_set_authority` | Allow fee payer as current authority in SetAuthority instructions | `false` | boolean | +| `allow_mint_to` | Allow fee payer as mint authority in MintTo/MintToChecked instructions | `false` | boolean | +| `allow_initialize_mint` | Allow fee payer as mint authority in InitializeMint/InitializeMint2 instructions | `false` | boolean | +| `allow_initialize_account` | Allow fee payer as owner in InitializeAccount/InitializeAccount3 instructions | `false` | boolean | +| `allow_initialize_multisig` | Allow fee payer as signer in InitializeMultisig/InitializeMultisig2 instructions | `false` | boolean | +| `allow_freeze_account` | Allow fee payer as freeze authority in FreezeAccount instructions | `false` | boolean | +| `allow_thaw_account` | Allow fee payer as freeze authority in ThawAccount instructions | `false` | boolean | + +Token-2022 supports the same instruction set as SPL Token with identical configuration options (under the `[validation.fee_payer_policy.token_2022]` section). ### Security Considerations -**SECURITY WARNING:** For security reasons, it is recommended to set all of these to `false` and only enable as needed. This will prevent unwanted behavior such as users draining your fee payer account or burning tokens from your fee payer account. +**SECURITY WARNING:** For security reasons, it is recommended to set all of these to `false` (default) and only enable as needed. This will prevent unwanted behavior such as users draining your fee payer account or burning tokens from your fee payer account. This prevents: + +- **Account Drainage**: Users transferring SOL or tokens from your fee payer account +- **Authority Takeover**: Users changing authorities on accounts owned by your fee payer +- **Unauthorized Minting**: Users minting tokens if your fee payer has mint authority +- **Account Manipulation**: Users freezing, closing, or modifying accounts controlled by your fee payer + +**Best Practice:** Start with all permissions disabled and enable only the minimum set needed for your specific use case. ## Price Configuration (optional) @@ -354,17 +416,22 @@ type = "free" #### Security Measures When Using Fixed/Free Pricing -1. **Disable Transfer Operations** - Prevent fee payer from being used as source in transfers: +1. **Disable All Transfer and Monetary Operations** - Prevent fee payer from being used as source in transfers: ```toml [validation.fee_payer_policy.system] - allow_transfer = false # Critical: Block SOL transfers - allow_create_account = false # Block account creation + allow_transfer = false # Block SOL transfers + allow_create_account = false # Block account creation with lamports + allow_allocate = false # Block space allocation - [validation.fee_payer_policy.spl_token] - allow_transfer = false # Block SPL transfers + [validation.fee_payer_policy.system.nonce] + allow_withdraw = false # Block nonce account withdrawals - [validation.fee_payer_policy.token_2022] - allow_transfer = false # Block Token2022 transfers + [validation.fee_payer_policy.spl_token] # and for [validation.fee_payer_policy.token_2022] + allow_transfer = false # Block SPL transfers + allow_burn = false # Block SPL token burning + allow_close_account = false # Block SPL token account closures (returns rent) + allow_mint_to = false # Block unauthorized SPL token minting + allow_initialize_account = false # Block account initialization ``` 2. **Enable Authentication** - Use authentication to prevent abuse: @@ -381,6 +448,12 @@ type = "free" max_allowed_lamports = 1000000 # 0.001 SOL maximum ``` +> **WARNING:** Particularly dangerous operations when using fixed/free pricing: +> - `allow_mint_to`: Could allow unlimited token creation if fee payer has mint authority +> - `allow_set_authority`: Could transfer control of critical accounts to attackers +> - `allow_transfer`: Enables direct drainage of fee payer token balances +> - `allow_close_account`: Returns rent to attacker-controlled accounts + ## Performance Monitoring (optional) The `[metrics]` section configures metrics collection and monitoring. This section is optional and by default, metrics are disabled. @@ -503,15 +576,46 @@ disallowed_accounts = [ "BadActor1111111111111111111111111111111111111111", ] -# Restrictive fee payer policy -[validation.fee_payer_policy] -allow_sol_transfers = false -allow_spl_transfers = false -allow_token2022_transfers = false -allow_assign = false -allow_burn = false -allow_close_account = false -allow_approve = false +# Restrictive fee payer policy (recommended for production) +[validation.fee_payer_policy.system] +allow_transfer = false # Block SOL transfers from fee payer +allow_assign = false # Block account ownership changes +allow_create_account = false # Block creating accounts with fee payer funds +allow_allocate = false # Block allocating space for fee payer accounts + +[validation.fee_payer_policy.system.nonce] +allow_initialize = false # Block nonce account initialization +allow_advance = false # Block nonce advancement +allow_authorize = false # Block nonce authority changes +allow_withdraw = false # Block nonce withdrawals + +[validation.fee_payer_policy.spl_token] +allow_transfer = false # Critical: Block SPL transfers +allow_burn = false # Block token burning +allow_close_account = false # Block account closures +allow_approve = false # Block token approvals +allow_revoke = false # Block delegate revocations +allow_set_authority = false # Block authority changes +allow_mint_to = false # Block minting operations +allow_initialize_mint = false # Block mint initialization +allow_initialize_account = false # Block account initialization +allow_initialize_multisig = false # Block multisig initialization +allow_freeze_account = false # Block account freezing +allow_thaw_account = false # Block account thawing + +[validation.fee_payer_policy.token_2022] +allow_transfer = false # Critical: Block Token2022 transfers +allow_burn = false # Block token burning +allow_close_account = false # Block account closures +allow_approve = false # Block token approvals +allow_revoke = false # Block delegate revocations +allow_set_authority = false # Block authority changes +allow_mint_to = false # Block minting operations +allow_initialize_mint = false # Block mint initialization +allow_initialize_account = false # Block account initialization +allow_initialize_multisig = false # Block multisig initialization +allow_freeze_account = false # Block account freezing +allow_thaw_account = false # Block account thawing # Token-2022 extension blocking [validation.token2022] diff --git a/docs/operators/FEES.md b/docs/operators/FEES.md index 846c3a57..52d16422 100644 --- a/docs/operators/FEES.md +++ b/docs/operators/FEES.md @@ -59,30 +59,35 @@ Kora supports three pricing models that determine how users are charged for tran ## โš ๏ธ Security Warning: Fixed/Free Pricing Models -**CRITICAL:** The fixed/free pricing models do **NOT** include fee payer outflow in the charged amount. This creates a significant security risk if not properly configured: If your fee payer policy allows transfers or other outflow operations, attackers can exploit this to drain your fee payer account: +**CRITICAL:** The fixed/free pricing models do **NOT** include fee payer outflow in the charged amount. This creates a significant security risk if not properly configured. If your fee payer policy allows transfers or other outflow operations, attackers can exploit this to drain your fee payer account. ### Required Security Controls -When using fixed/free pricing, you **MUST** configure restrictive fee payer policies: +When using fixed/free pricing, you **MUST** configure restrictive fee payer policies to block ALL monetary and authority-changing operations: ```toml [validation.fee_payer_policy.system] allow_transfer = false # Block SOL transfers allow_create_account = false # Block account creation with lamports +allow_allocate = false # Block space allocation -[validation.fee_payer_policy.spl_token] -allow_transfer = false # Block SPL token transfers +[validation.fee_payer_policy.system.nonce] +allow_withdraw = false # Block nonce account withdrawals -[validation.fee_payer_policy.token_2022] -allow_transfer = false # Block Token2022 transfers +[validation.fee_payer_policy.spl_token] # and for [validation.fee_payer_policy.token_2022] +allow_transfer = false # Block SPL transfers +allow_burn = false # Block SPL token burning +allow_close_account = false # Block SPL token account closures (returns rent) +allow_mint_to = false # Block unauthorized SPL token minting +allow_initialize_account = false # Block account initialization ``` ### Additional Protections -1. **Enable Authentication:** Always require API key or HMAC authentication with fixed pricing +1. **Enable Authentication:** Always require API key or HMAC authentication with fixed/free pricing 2. **Set Low Limits:** Use conservative `max_allowed_lamports` values 3. **Monitor Usage:** Track unusual patterns of high-outflow transactions -4. **Consider Margin Pricing:** Margin pricing automatically includes outflow costs +4. **Consider Margin Pricing:** Margin pricing automatically includes outflow costs in fees ### Validation Warnings @@ -94,7 +99,7 @@ kora --config kora.toml config validate Expected warnings for vulnerable configs: ``` -โš ๏ธ SECURITY: Fixed pricing with allow_transfer=true for System instructions. +โš ๏ธ SECURITY: Fixed pricing with system.allow_transfer=true. Users can make the fee payer transfer arbitrary SOL amounts at fixed cost. This can drain your fee payer account. ``` diff --git a/docs/x402/demo/X402_DEMO_GUIDE.md b/docs/x402/demo/X402_DEMO_GUIDE.md index 19647fe6..80e27990 100644 --- a/docs/x402/demo/X402_DEMO_GUIDE.md +++ b/docs/x402/demo/X402_DEMO_GUIDE.md @@ -230,12 +230,48 @@ allowed_tokens = [ api_key = "kora_facilitator_api_key_example" ``` -3. **Fee Payer Policy**: Configured to restrict signing unwanted transactions: +3. **Fee Payer Policy**: Configured to restrict signing unwanted transactions using granular controls: ```toml -[validation.fee_payer_policy] -allow_sol_transfers = false -# all other settings are false +[validation.fee_payer_policy.system] +allow_transfer = false +allow_assign = false +allow_create_account = false +allow_allocate = false + +[validation.fee_payer_policy.system.nonce] +allow_initialize = false +allow_advance = false +allow_authorize = false +allow_withdraw = false + +[validation.fee_payer_policy.spl_token] +allow_transfer = false +allow_burn = false +allow_close_account = false +allow_approve = false +allow_revoke = false +allow_set_authority = false +allow_mint_to = false +allow_initialize_mint = false +allow_initialize_account = false +allow_initialize_multisig = false +allow_freeze_account = false +allow_thaw_account = false + +[validation.fee_payer_policy.token_2022] +allow_transfer = false +allow_burn = false +allow_close_account = false +allow_approve = false +allow_revoke = false +allow_set_authority = false +allow_mint_to = false +allow_initialize_mint = false +allow_initialize_account = false +allow_initialize_multisig = false +allow_freeze_account = false +allow_thaw_account = false ``` 4. **Allowed Programs**: Ensure the system program, token program, associated token program, and compute budget program are in the allowlist: From b2412586d0e3cd8c4cfabd775d25caa36ad9475e Mon Sep 17 00:00:00 2001 From: amilz <85324096+amilz@users.noreply.github.com> Date: Tue, 18 Nov 2025 13:54:37 -0800 Subject: [PATCH 27/29] chore: migrate from gill to @solana/kit (#249) - update x402 demo - update full demo --- docs/getting-started/FULL_DEMO.md | 100 +-- docs/getting-started/demo/client/package.json | 3 +- .../demo/client/pnpm-lock.yaml | 48 +- .../demo/client/src/full-demo.ts | 63 +- docs/x402/demo/client/package.json | 2 +- docs/x402/demo/client/pnpm-lock.yaml | 649 ++++++++++++++++-- docs/x402/demo/client/src/setup.ts | 7 +- 7 files changed, 693 insertions(+), 179 deletions(-) diff --git a/docs/getting-started/FULL_DEMO.md b/docs/getting-started/FULL_DEMO.md index 6c53d02e..e11aab69 100644 --- a/docs/getting-started/FULL_DEMO.md +++ b/docs/getting-started/FULL_DEMO.md @@ -118,28 +118,32 @@ Our demo starts with the necessary imports and configuration: ```ts import { KoraClient } from "@kora/sdk"; import { - createKeyPairSignerFromBytes, - getBase58Encoder, - createNoopSigner, - address, - getBase64EncodedWireTransaction, - partiallySignTransactionMessageWithSigners, - Blockhash, - Base64EncodedWireTransaction, - partiallySignTransaction, - TransactionVersion, - Instruction, - KeyPairSigner, - Rpc, - SolanaRpcApi + createKeyPairSignerFromBytes, + getBase58Encoder, + createNoopSigner, + address, + getBase64EncodedWireTransaction, + partiallySignTransactionMessageWithSigners, + Blockhash, + Base64EncodedWireTransaction, + partiallySignTransaction, + TransactionVersion, + Instruction, + KeyPairSigner, + Rpc, + SolanaRpcApi, + createSolanaRpc, + createSolanaRpcSubscriptions, + pipe, + createTransactionMessage, + setTransactionMessageFeePayerSigner, + setTransactionMessageLifetimeUsingBlockhash, + MicroLamports, + appendTransactionMessageInstructions, } from "@solana/kit"; -import { - createTransaction, - createSolanaClient, - getExplorerLink -} from "gill"; import { getAddMemoInstruction } from "@solana-program/memo"; import { createRecentSignatureConfirmationPromiseFactory } from "@solana/transaction-confirmation"; +import { updateOrAppendSetComputeUnitLimitInstruction, updateOrAppendSetComputeUnitPriceInstruction } from "@solana-program/compute-budget"; import dotenv from "dotenv"; import path from "path"; @@ -150,11 +154,12 @@ const CONFIG = { computeUnitPrice: 1_000_000n, transactionVersion: 0, solanaRpcUrl: "http://127.0.0.1:8899", + solanaWsUrl: "ws://127.0.0.1:8900", koraRpcUrl: "http://localhost:8080/", } ``` -We are importing the Kora Client from the [Kora SDK](https://www.npmjs.com/package/@kora/sdk) and a few types/helpers from Solana Kit library for building transactions. We are also importing a couple of helper functions from the [gill](https://www.npmjs.com/package/gill) library to simplify transaction building. +We are importing the Kora Client from the [Kora SDK](https://www.npmjs.com/package/@kora/sdk) and a few types/helpers from Solana Kit library for building transactions. We are also creating a global configuration object that defines: @@ -200,9 +205,8 @@ async function initializeClients() { // hmacSecret: process.env.KORA_HMAC_SECRET, // Uncomment if HMAC is enabled }); - const { rpc, rpcSubscriptions } = createSolanaClient({ - urlOrMoniker: CONFIG.solanaRpcUrl, - }); + const rpc = createSolanaRpc(CONFIG.solanaRpcUrl); + const rpcSubscriptions = createSolanaRpcSubscriptions(CONFIG.solanaWsUrl); const confirmTransaction = createRecentSignatureConfirmationPromiseFactory({ rpc, @@ -326,17 +330,17 @@ async function getPaymentInstruction( console.log(' โ†’ Blockhash:', latestBlockhash.blockhash.slice(0, 8) + '...'); // Create estimate transaction to get payment instruction - const estimateTransaction = await createTransaction({ - version: CONFIG.transactionVersion as TransactionVersion, - instructions, - feePayer: noopSigner, - latestBlockhash: { - blockhash: latestBlockhash.blockhash as Blockhash, - lastValidBlockHeight: 0n, - }, - computeUnitPrice: CONFIG.computeUnitPrice, - computeUnitLimit: CONFIG.computeUnitLimit - }); + const estimateTransaction = pipe( + createTransactionMessage({ version: CONFIG.transactionVersion as TransactionVersion }), + (tx) => setTransactionMessageFeePayerSigner(noopSigner, tx), + (tx) => setTransactionMessageLifetimeUsingBlockhash({ + blockhash: latestBlockhash.blockhash as Blockhash, + lastValidBlockHeight: 0n, + }, tx), + (tx) => updateOrAppendSetComputeUnitPriceInstruction(CONFIG.computeUnitPrice, tx), + (tx) => updateOrAppendSetComputeUnitLimitInstruction(CONFIG.computeUnitLimit, tx), + (tx) => appendTransactionMessageInstructions(instructions, tx), + ); const signedEstimateTransaction = await partiallySignTransactionMessageWithSigners(estimateTransaction); const base64EncodedWireTransaction = getBase64EncodedWireTransaction(signedEstimateTransaction); @@ -382,17 +386,17 @@ async function getFinalTransaction( // Build final transaction with payment instruction const newBlockhash = await client.getBlockhash(); - const fullTransaction = await createTransaction({ - version: CONFIG.transactionVersion as TransactionVersion, - instructions: [...instructions, paymentInstruction], - feePayer: noopSigner, - latestBlockhash: { - blockhash: newBlockhash.blockhash as Blockhash, - lastValidBlockHeight: 0n, - }, - computeUnitPrice: CONFIG.computeUnitPrice, - computeUnitLimit: CONFIG.computeUnitLimit - }); + const fullTransaction = pipe( + createTransactionMessage({ version: CONFIG.transactionVersion as TransactionVersion }), + (tx) => setTransactionMessageFeePayerSigner(noopSigner, tx), + (tx) => setTransactionMessageLifetimeUsingBlockhash({ + blockhash: newBlockhash.blockhash as Blockhash, + lastValidBlockHeight: 0n, + }, tx), + (tx) => updateOrAppendSetComputeUnitPriceInstruction(CONFIG.computeUnitPrice, tx), + (tx) => updateOrAppendSetComputeUnitLimitInstruction(CONFIG.computeUnitLimit, tx), + (tx) => appendTransactionMessageInstructions([...instructions, paymentInstruction], tx), + ); console.log(' โœ“ Final transaction built with payment'); // Sign with user keypair @@ -405,9 +409,7 @@ async function getFinalTransaction( } ``` - - -We use the same `createTransaction` helper to assemble our transaction. Our final transaction includes: +We use the same `pipe` function to assemble our transaction. Our final transaction includes: - Our original instructions - The payment instruction - A fresh blockhash @@ -455,8 +457,6 @@ async function submitTransaction( console.log('โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”'); console.log('\nTransaction signature:'); console.log(signature); - console.log('\nView on explorer:'); - console.log(getExplorerLink({transaction: signature, cluster: 'localhost'})); return signature; } diff --git a/docs/getting-started/demo/client/package.json b/docs/getting-started/demo/client/package.json index c9964896..e27e3f3a 100644 --- a/docs/getting-started/demo/client/package.json +++ b/docs/getting-started/demo/client/package.json @@ -24,8 +24,7 @@ "@solana/assertions": "^2.3.0", "@solana/kit": "^2.3.0", "@solana/transaction-confirmation": "^2.3.0", - "dotenv": "^17.2.0", - "gill": "^0.10.3" + "dotenv": "^17.2.0" }, "devDependencies": { "@types/node": "^24.0.13", diff --git a/docs/getting-started/demo/client/pnpm-lock.yaml b/docs/getting-started/demo/client/pnpm-lock.yaml index df61f715..eb0191db 100644 --- a/docs/getting-started/demo/client/pnpm-lock.yaml +++ b/docs/getting-started/demo/client/pnpm-lock.yaml @@ -29,12 +29,12 @@ importers: '@solana/kit': specifier: ^2.3.0 version: 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3) + '@solana/transaction-confirmation': + specifier: ^2.3.0 + version: 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3) dotenv: specifier: ^17.2.0 version: 17.2.1 - gill: - specifier: ^0.10.3 - version: 0.10.3(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3) devDependencies: '@types/node': specifier: ^24.0.13 @@ -207,11 +207,6 @@ packages: '@kora/sdk@file:../../../../sdks/ts': resolution: {directory: ../../../../sdks/ts, type: directory} - '@solana-program/address-lookup-table@0.7.0': - resolution: {integrity: sha512-dzCeIO5LtiK3bIg0AwO+TPeGURjSG2BKt0c4FRx7105AgLy7uzTktpUzUj6NXAK9SzbirI8HyvHUvw1uvL8O9A==} - peerDependencies: - '@solana/kit': ^2.1.0 - '@solana-program/compute-budget@0.8.0': resolution: {integrity: sha512-qPKxdxaEsFxebZ4K5RPuy7VQIm/tfJLa1+Nlt3KNA8EYQkz9Xm8htdoEaXVrer9kpgzzp9R3I3Bh6omwCM06tQ==} peerDependencies: @@ -227,12 +222,6 @@ packages: peerDependencies: '@solana/kit': ^2.1.0 - '@solana-program/token-2022@0.4.2': - resolution: {integrity: sha512-zIpR5t4s9qEU3hZKupzIBxJ6nUV5/UVyIT400tu9vT1HMs5JHxaTTsb5GUhYjiiTvNwU0MQavbwc4Dl29L0Xvw==} - peerDependencies: - '@solana/kit': ^2.1.0 - '@solana/sysvars': ^2.1.0 - '@solana-program/token@0.5.1': resolution: {integrity: sha512-bJvynW5q9SFuVOZ5vqGVkmaPGA0MCC+m9jgJj1nk5m20I389/ms69ASnhWGoOPNcie7S9OwBX0gTj2fiyWpfag==} peerDependencies: @@ -488,12 +477,6 @@ packages: get-tsconfig@4.10.1: resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} - gill@0.10.3: - resolution: {integrity: sha512-4LIVA32lKcWoqU/dbwu+YpJbR59QQT6mvCtqkElBWF2aT9upmewjKN3/anhfTGy+o/RJykAV21i3RzCj9FR0Xg==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5' - resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -607,10 +590,6 @@ snapshots: '@kora/sdk@file:../../../../sdks/ts': {} - '@solana-program/address-lookup-table@0.7.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3))': - dependencies: - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3) - '@solana-program/compute-budget@0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3))': dependencies: '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3) @@ -623,11 +602,6 @@ snapshots: dependencies: '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3) - '@solana-program/token-2022@0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3))(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))': - dependencies: - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3) - '@solana/sysvars': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana-program/token@0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3))': dependencies: '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3) @@ -1032,22 +1006,6 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 - gill@0.10.3(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3): - dependencies: - '@solana-program/address-lookup-table': 0.7.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3)) - '@solana-program/compute-budget': 0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3)) - '@solana-program/system': 0.7.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3)) - '@solana-program/token-2022': 0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3))(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)) - '@solana/assertions': 2.3.0(typescript@5.9.2) - '@solana/codecs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3) - '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3) - typescript: 5.9.2 - transitivePeerDependencies: - - '@solana/sysvars' - - fastestsmallesttextencoderdecoder - - ws - resolve-pkg-maps@1.0.0: {} tsx@4.20.5: diff --git a/docs/getting-started/demo/client/src/full-demo.ts b/docs/getting-started/demo/client/src/full-demo.ts index 8711090d..7205a719 100644 --- a/docs/getting-started/demo/client/src/full-demo.ts +++ b/docs/getting-started/demo/client/src/full-demo.ts @@ -14,20 +14,29 @@ import { KeyPairSigner, Rpc, SolanaRpcApi, + createSolanaRpc, + createSolanaRpcSubscriptions, + pipe, + createTransactionMessage, + setTransactionMessageFeePayerSigner, + setTransactionMessageLifetimeUsingBlockhash, + MicroLamports, + appendTransactionMessageInstructions, } from "@solana/kit"; -import { createTransaction, createSolanaClient, getExplorerLink } from "gill"; import { getAddMemoInstruction } from "@solana-program/memo"; import { createRecentSignatureConfirmationPromiseFactory } from "@solana/transaction-confirmation"; +import { updateOrAppendSetComputeUnitLimitInstruction, updateOrAppendSetComputeUnitPriceInstruction } from "@solana-program/compute-budget"; import dotenv from "dotenv"; import path from "path"; dotenv.config({ path: path.join(process.cwd(), "..", ".env") }); const CONFIG = { - computeUnitLimit: 200_000n, - computeUnitPrice: 1_000_000n, + computeUnitLimit: 200_000, + computeUnitPrice: 1_000_000n as MicroLamports, transactionVersion: 0, solanaRpcUrl: "http://127.0.0.1:8899", + solanaWsUrl: "ws://127.0.0.1:8900", koraRpcUrl: "http://localhost:8080/", }; @@ -51,9 +60,8 @@ async function initializeClients() { // hmacSecret: process.env.KORA_HMAC_SECRET, // Uncomment if you have authentication enabled in your kora.toml }); - const { rpc, rpcSubscriptions } = createSolanaClient({ - urlOrMoniker: CONFIG.solanaRpcUrl, - }); + const rpc = createSolanaRpc(CONFIG.solanaRpcUrl); + const rpcSubscriptions = createSolanaRpcSubscriptions(CONFIG.solanaWsUrl); const confirmTransaction = createRecentSignatureConfirmationPromiseFactory({ rpc, @@ -139,17 +147,18 @@ async function getPaymentInstruction( console.log(" โ†’ Blockhash:", latestBlockhash.blockhash.slice(0, 8) + "..."); // Create estimate transaction to get payment instruction - const estimateTransaction = await createTransaction({ - version: CONFIG.transactionVersion as TransactionVersion, - instructions, - feePayer: noopSigner, - latestBlockhash: { + + const estimateTransaction = pipe( + createTransactionMessage({ version: CONFIG.transactionVersion as TransactionVersion }), + (tx) => setTransactionMessageFeePayerSigner(noopSigner, tx), + (tx) => setTransactionMessageLifetimeUsingBlockhash({ blockhash: latestBlockhash.blockhash as Blockhash, lastValidBlockHeight: 0n, - }, - computeUnitPrice: CONFIG.computeUnitPrice, - computeUnitLimit: CONFIG.computeUnitLimit, - }); + }, tx), + (tx) => updateOrAppendSetComputeUnitPriceInstruction(CONFIG.computeUnitPrice, tx), + (tx) => updateOrAppendSetComputeUnitLimitInstruction(CONFIG.computeUnitLimit, tx), + (tx) => appendTransactionMessageInstructions(instructions, tx), + ) const signedEstimateTransaction = await partiallySignTransactionMessageWithSigners(estimateTransaction); @@ -181,17 +190,19 @@ async function getFinalTransaction( // Build final transaction with payment instruction const newBlockhash = await client.getBlockhash(); - const fullTransaction = await createTransaction({ - version: CONFIG.transactionVersion as TransactionVersion, - instructions: [...instructions, paymentInstruction], - feePayer: noopSigner, - latestBlockhash: { + + const fullTransaction = pipe( + createTransactionMessage({ version: CONFIG.transactionVersion as TransactionVersion }), + (tx) => setTransactionMessageFeePayerSigner(noopSigner, tx), + (tx) => setTransactionMessageLifetimeUsingBlockhash({ blockhash: newBlockhash.blockhash as Blockhash, lastValidBlockHeight: 0n, - }, - computeUnitPrice: CONFIG.computeUnitPrice, - computeUnitLimit: CONFIG.computeUnitLimit, - }); + }, tx), + (tx) => updateOrAppendSetComputeUnitPriceInstruction(CONFIG.computeUnitPrice, tx), + (tx) => updateOrAppendSetComputeUnitLimitInstruction(CONFIG.computeUnitLimit, tx), + (tx) => appendTransactionMessageInstructions([...instructions, paymentInstruction], tx), + ); + console.log(" โœ“ Final transaction built with payment"); // Sign with user keypair @@ -249,10 +260,6 @@ async function submitTransaction( console.log("โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”"); console.log("\nTransaction signature:"); console.log(signature); - console.log("\nView on explorer:"); - console.log( - getExplorerLink({ transaction: signature, cluster: "localhost" }) - ); return signature; } diff --git a/docs/x402/demo/client/package.json b/docs/x402/demo/client/package.json index 02ea4f51..e90cdad8 100644 --- a/docs/x402/demo/client/package.json +++ b/docs/x402/demo/client/package.json @@ -15,8 +15,8 @@ "license": "MIT", "packageManager": "pnpm@10.12.3", "dependencies": { + "@solana/kit": "^5.0.0", "dotenv": "^17.2.0", - "gill": "^0.11.0", "x402-fetch": "^0.6.0" }, "devDependencies": { diff --git a/docs/x402/demo/client/pnpm-lock.yaml b/docs/x402/demo/client/pnpm-lock.yaml index 837d8426..4c83ee43 100644 --- a/docs/x402/demo/client/pnpm-lock.yaml +++ b/docs/x402/demo/client/pnpm-lock.yaml @@ -8,15 +8,15 @@ importers: .: dependencies: + '@solana/kit': + specifier: ^5.0.0 + version: 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) dotenv: specifier: ^17.2.0 version: 17.2.2 - gill: - specifier: ^0.11.0 - version: 0.11.0(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) x402-fetch: specifier: ^0.6.0 - version: 0.6.0(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + version: 0.6.0(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) devDependencies: '@types/node': specifier: ^24.0.13 @@ -428,21 +428,11 @@ packages: '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} - '@solana-program/address-lookup-table@0.7.0': - resolution: {integrity: sha512-dzCeIO5LtiK3bIg0AwO+TPeGURjSG2BKt0c4FRx7105AgLy7uzTktpUzUj6NXAK9SzbirI8HyvHUvw1uvL8O9A==} - peerDependencies: - '@solana/kit': ^2.1.0 - '@solana-program/compute-budget@0.8.0': resolution: {integrity: sha512-qPKxdxaEsFxebZ4K5RPuy7VQIm/tfJLa1+Nlt3KNA8EYQkz9Xm8htdoEaXVrer9kpgzzp9R3I3Bh6omwCM06tQ==} peerDependencies: '@solana/kit': ^2.1.0 - '@solana-program/system@0.7.0': - resolution: {integrity: sha512-FKTBsKHpvHHNc1ATRm7SlC5nF/VdJtOSjldhcyfMN9R7xo712Mo2jHIzvBgn8zQO5Kg0DcWuKB7268Kv1ocicw==} - peerDependencies: - '@solana/kit': ^2.1.0 - '@solana-program/token-2022@0.4.2': resolution: {integrity: sha512-zIpR5t4s9qEU3hZKupzIBxJ6nUV5/UVyIT400tu9vT1HMs5JHxaTTsb5GUhYjiiTvNwU0MQavbwc4Dl29L0Xvw==} peerDependencies: @@ -460,36 +450,72 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/accounts@5.0.0': + resolution: {integrity: sha512-0JzBdEobgp8NBdhhu+GgwNDh7e8KkHDsSTVZAnNQgvT3taOz0Mwv5E48MuEeDhW6DLFwWVAx/FO3pvibG/NGwA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/addresses@2.3.0': resolution: {integrity: sha512-ypTNkY2ZaRFpHLnHAgaW8a83N0/WoqdFvCqf4CQmnMdFsZSdC7qOwcbd7YzdaQn9dy+P2hybewzB+KP7LutxGA==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' + '@solana/addresses@5.0.0': + resolution: {integrity: sha512-bVk+khc1ZZQHMri25csosM/ikuyPcB/CZidDM/ZMBX0CoJErpHJnmcID5mYOmv4/UHbqo2OANuEaGcFO0Q37sw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/assertions@2.3.0': resolution: {integrity: sha512-Ekoet3khNg3XFLN7MIz8W31wPQISpKUGDGTylLptI+JjCDWx3PIa88xjEMqFo02WJ8sBj2NLV64Xg1sBcsHjZQ==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' + '@solana/assertions@5.0.0': + resolution: {integrity: sha512-2kIykk90kYciQW6bp+KaE6jRd1Y2CgHPeJxxlc5chQnjhoG6eiD8VXvocs6AvqPTht0p/SoEj9jH5tT4oG/bcg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/codecs-core@2.3.0': resolution: {integrity: sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' + '@solana/codecs-core@5.0.0': + resolution: {integrity: sha512-rCG2d8OaamVF2/J//YyCgDqNJpUytVVltw9C8mJtEz5c6Se/LR6BFuG8g4xeJswq/ab4RFk5/HFdgbvNjKgQjA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/codecs-data-structures@2.3.0': resolution: {integrity: sha512-qvU5LE5DqEdYMYgELRHv+HMOx73sSoV1ZZkwIrclwUmwTbTaH8QAJURBj0RhQ/zCne7VuLLOZFFGv6jGigWhSw==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' + '@solana/codecs-data-structures@5.0.0': + resolution: {integrity: sha512-y503Pqmv0LHcfcf0vQJGaxDvydQJbyCo8nK3nxn56EhFj5lBQ1NWb3WvTd83epigwuZurW2MhJARrpikfhQglQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/codecs-numbers@2.3.0': resolution: {integrity: sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' + '@solana/codecs-numbers@5.0.0': + resolution: {integrity: sha512-a2+skRLuUK02f/XFe4L0e1+wHCyfK25PkyseFps1v1l4pvevukFwth/EhSyrs6w5CsTJRVoR7MuE3E00PM4egw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/codecs-strings@2.3.0': resolution: {integrity: sha512-y5pSBYwzVziXu521hh+VxqUtp0hYGTl1eWGoc1W+8mdvBdC1kTqm/X7aYQw33J42hw03JjryvYOvmGgk3Qz/Ug==} engines: {node: '>=20.18.0'} @@ -497,12 +523,25 @@ packages: fastestsmallesttextencoderdecoder: ^1.0.22 typescript: '>=5.3.3' + '@solana/codecs-strings@5.0.0': + resolution: {integrity: sha512-ALkRwpV8bGR6qjAYw0YXZwp2YI4wzvKOJGmx04Ut8gMdbaUx7qOcJkhEQKI6ZVC3lAWSIS1N1wGccUZDwvfKxw==} + engines: {node: '>=20.18.0'} + peerDependencies: + fastestsmallesttextencoderdecoder: ^1.0.22 + typescript: '>=5.3.3' + '@solana/codecs@2.3.0': resolution: {integrity: sha512-JVqGPkzoeyU262hJGdH64kNLH0M+Oew2CIPOa/9tR3++q2pEd4jU2Rxdfye9sd0Ce3XJrR5AIa8ZfbyQXzjh+g==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' + '@solana/codecs@5.0.0': + resolution: {integrity: sha512-KOw0gFUSBxIMDWLJ3AkVFkEci91dw0Rpx3C6y83Our7fSW+SEP8vRZklCElieYR85LHVB1QIEhoeHR7rc+Ifkw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/errors@2.3.0': resolution: {integrity: sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ==} engines: {node: '>=20.18.0'} @@ -510,90 +549,187 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/errors@5.0.0': + resolution: {integrity: sha512-gTuhzO6E+ydfAAzqmqdPcvFyJwAzFKKIrqtnZPpgAuomcPYu+HSo0tuwSM/cTX0djmHt+GoOsf/julph+nvs2w==} + engines: {node: '>=20.18.0'} + hasBin: true + peerDependencies: + typescript: '>=5.3.3' + '@solana/fast-stable-stringify@2.3.0': resolution: {integrity: sha512-KfJPrMEieUg6D3hfQACoPy0ukrAV8Kio883llt/8chPEG3FVTX9z/Zuf4O01a15xZmBbmQ7toil2Dp0sxMJSxw==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' + '@solana/fast-stable-stringify@5.0.0': + resolution: {integrity: sha512-sGTbu7a4/olL+8EIOOJ7IZjzqOOpCJcK1UaVJ6015sRgo9vwGf4jg9KtXEYv5LVhLCTYmAb50L4BaIUcBph/Ig==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/functional@2.3.0': resolution: {integrity: sha512-AgsPh3W3tE+nK3eEw/W9qiSfTGwLYEvl0rWaxHht/lRcuDVwfKRzeSa5G79eioWFFqr+pTtoCr3D3OLkwKz02Q==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' + '@solana/functional@5.0.0': + resolution: {integrity: sha512-UNBrpfzBL4dKD2iucjNnrkFbnjz5ZYDu2OvrIBAcCSQsxxgHMamUj1n3EDe6kl1us49YG1r05Ho8QLqNrbkVbw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/instruction-plans@5.0.0': + resolution: {integrity: sha512-n9oFOMFUPYKEhsXzrXT97QBQ2WvOTar+5SFEj/IOtRuCn4gl2kh0369cjXZpFwUdE3tmKr1zfYFNwbtiNx5pvg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/instructions@2.3.0': resolution: {integrity: sha512-PLMsmaIKu7hEAzyElrk2T7JJx4D+9eRwebhFZpy2PXziNSmFF929eRHKUsKqBFM3cYR1Yy3m6roBZfA+bGE/oQ==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' + '@solana/instructions@5.0.0': + resolution: {integrity: sha512-12dbrmwERT1o6NTr/Uvrjj/ZsiteSXoT5Gi+dnjIeRNHWg9H+gEFuFzJvTDVKlNg34CZ71xdvbVdbV0V8gKGvg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/keys@2.3.0': resolution: {integrity: sha512-ZVVdga79pNH+2pVcm6fr2sWz9HTwfopDVhYb0Lh3dh+WBmJjwkabXEIHey2rUES7NjFa/G7sV8lrUn/v8LDCCQ==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' + '@solana/keys@5.0.0': + resolution: {integrity: sha512-kWkR7NslpTttk5i1BhBNCDtVQDkEtgkdsM3Jp9TGPk0GFjBjBwrQStw3vvwLe8itEIvRFGFZU6JHEk8HLS0WLQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/kit@2.3.0': resolution: {integrity: sha512-sb6PgwoW2LjE5oTFu4lhlS/cGt/NB3YrShEyx7JgWFWysfgLdJnhwWThgwy/4HjNsmtMrQGWVls0yVBHcMvlMQ==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' + '@solana/kit@5.0.0': + resolution: {integrity: sha512-3ahtzmmMgU+1l2YMhQJSKKm14IdvCycOE/m4XNMu/4icBIptmBgZxrmgRpPHqBilBa+Krp/hBuTg4HWl9IAgWw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/nominal-types@2.3.0': resolution: {integrity: sha512-uKlMnlP4PWW5UTXlhKM8lcgIaNj8dvd8xO4Y9l+FVvh9RvW2TO0GwUO6JCo7JBzCB0PSqRJdWWaQ8pu1Ti/OkA==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' + '@solana/nominal-types@5.0.0': + resolution: {integrity: sha512-Qn7xH4UG2rDAv+wAyheP4jWvX3oQmbZ/woxFZwug7PaRLvyjUswGr38Hil+SjiQyFDo+un1UqWM9N9yusUeeZQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/options@2.3.0': resolution: {integrity: sha512-PPnnZBRCWWoZQ11exPxf//DRzN2C6AoFsDI/u2AsQfYih434/7Kp4XLpfOMT/XESi+gdBMFNNfbES5zg3wAIkw==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' + '@solana/options@5.0.0': + resolution: {integrity: sha512-ezHVBFb9FXVSn8LUVRD2tLb6fejU0x8KtGEYyCYh0J0pQuXSITV0IQCjcEopvu/ZxWdXOJyzjvmymnhz90on5A==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/programs@2.3.0': resolution: {integrity: sha512-UXKujV71VCI5uPs+cFdwxybtHZAIZyQkqDiDnmK+DawtOO9mBn4Nimdb/6RjR2CXT78mzO9ZCZ3qfyX+ydcB7w==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' + '@solana/programs@5.0.0': + resolution: {integrity: sha512-BKOfBDrSUCJGZ+qKk2aFLu0nU9/84o6z/VDCJkLjaNNuTv8nOlSYq5flNzo1eyJmnpyW372qNvqqRN3AS23+FQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/promises@2.3.0': resolution: {integrity: sha512-GjVgutZKXVuojd9rWy1PuLnfcRfqsaCm7InCiZc8bqmJpoghlyluweNc7ml9Y5yQn1P2IOyzh9+p/77vIyNybQ==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' + '@solana/promises@5.0.0': + resolution: {integrity: sha512-Qmg3UfYfWINEUvBQL3DkPOq34tTg5cfrkPlDtJmi8RVifsPqb6hksbKZGu7ASLZohxIDGmnYQY6oELI7Me+5yw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/rpc-api@2.3.0': resolution: {integrity: sha512-UUdiRfWoyYhJL9PPvFeJr4aJ554ob2jXcpn4vKmRVn9ire0sCbpQKYx6K8eEKHZWXKrDW8IDspgTl0gT/aJWVg==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' + '@solana/rpc-api@5.0.0': + resolution: {integrity: sha512-IJbZZnX2B1ldXPok1NhneXTYq9ZvdJbE5Pryr03pZTlPJaWGqDcZuQ14nwR4s6PoUUgdT+p87QlLZqLb8MusoQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/rpc-parsed-types@2.3.0': resolution: {integrity: sha512-B5pHzyEIbBJf9KHej+zdr5ZNAdSvu7WLU2lOUPh81KHdHQs6dEb310LGxcpCc7HVE8IEdO20AbckewDiAN6OCg==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' + '@solana/rpc-parsed-types@5.0.0': + resolution: {integrity: sha512-fU9uqlOYAaBqgk2qCl+ntenBm7wuSFBRbIO/rVjeBPd/qPCvNZU+qFET+ERLK6wbCTSz0MmdHqPn1V8KCMOvZQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/rpc-spec-types@2.3.0': resolution: {integrity: sha512-xQsb65lahjr8Wc9dMtP7xa0ZmDS8dOE2ncYjlvfyw/h4mpdXTUdrSMi6RtFwX33/rGuztQ7Hwaid5xLNSLvsFQ==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' + '@solana/rpc-spec-types@5.0.0': + resolution: {integrity: sha512-B0P/ylXVaCG5oSIV+kB88s2qoW996D8iKhc7RyF0C/AyYvklF6kCwv0N9ZVrWp0ibjlQ8St290WbBHJyo7QZkA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/rpc-spec@2.3.0': resolution: {integrity: sha512-fA2LMX4BMixCrNB2n6T83AvjZ3oUQTu7qyPLyt8gHQaoEAXs8k6GZmu6iYcr+FboQCjUmRPgMaABbcr9j2J9Sw==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' + '@solana/rpc-spec@5.0.0': + resolution: {integrity: sha512-1LD2SYEQ5bYhiBumznAPzymtxSX4nYLZd6u+FA0bAxNBVzHDvUUQzVSXHAoWROhlGrCyvtALTs9u0DIDlgZHCA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/rpc-subscriptions-api@2.3.0': resolution: {integrity: sha512-9mCjVbum2Hg9KGX3LKsrI5Xs0KX390lS+Z8qB80bxhar6MJPugqIPH8uRgLhCW9GN3JprAfjRNl7our8CPvsPQ==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' + '@solana/rpc-subscriptions-api@5.0.0': + resolution: {integrity: sha512-DGUn3C12swV2FConOlLFN14npIrCtnxehtMLjszMC7g6p/P6WNIz5uAgF7YcIkLBDV8uTeWhM0azmK+V8Qqhvg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/rpc-subscriptions-channel-websocket@2.3.0': resolution: {integrity: sha512-2oL6ceFwejIgeWzbNiUHI2tZZnaOxNTSerszcin7wYQwijxtpVgUHiuItM/Y70DQmH9sKhmikQp+dqeGalaJxw==} engines: {node: '>=20.18.0'} @@ -601,78 +737,157 @@ packages: typescript: '>=5.3.3' ws: ^8.18.0 + '@solana/rpc-subscriptions-channel-websocket@5.0.0': + resolution: {integrity: sha512-vsYXyjVX/kExfpr91zfMKTmWKKFCM+dkhXQDAz5aEE7kAF3KSZDiOGeYvN8Rc85lbIt9QK6BLAT+NBMv4/N9Qg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + ws: ^8.18.0 + '@solana/rpc-subscriptions-spec@2.3.0': resolution: {integrity: sha512-rdmVcl4PvNKQeA2l8DorIeALCgJEMSu7U8AXJS1PICeb2lQuMeaR+6cs/iowjvIB0lMVjYN2sFf6Q3dJPu6wWg==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' + '@solana/rpc-subscriptions-spec@5.0.0': + resolution: {integrity: sha512-erRLvZMncwnciJP6I1SlAk0CyRGIgt83PyHWOVCRXENP9Q5dZbZ9pm4lar2yIp8EjIMnodGHsQWIlKc1hlCQlQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/rpc-subscriptions@2.3.0': resolution: {integrity: sha512-Uyr10nZKGVzvCOqwCZgwYrzuoDyUdwtgQRefh13pXIrdo4wYjVmoLykH49Omt6abwStB0a4UL5gX9V4mFdDJZg==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' + '@solana/rpc-subscriptions@5.0.0': + resolution: {integrity: sha512-cziOSzom/bwFZXViR9J+MxDsdLMcfvrXGw5Icng7dYODFKuVqfsDrQoG8uekJc4fREnbPEM2U+u9YnYSYbFbww==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/rpc-transformers@2.3.0': resolution: {integrity: sha512-UuHYK3XEpo9nMXdjyGKkPCOr7WsZsxs7zLYDO1A5ELH3P3JoehvrDegYRAGzBS2VKsfApZ86ZpJToP0K3PhmMA==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' + '@solana/rpc-transformers@5.0.0': + resolution: {integrity: sha512-EMHhSgfF6/T4FfHbLaBP08SIj1ZAjxJr6WPNZMHLV7Cup8UfiB9TNV+bPQkum7JbVQNhUKzkKEEmyYqPfQoV9w==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/rpc-transport-http@2.3.0': resolution: {integrity: sha512-HFKydmxGw8nAF5N+S0NLnPBDCe5oMDtI2RAmW8DMqP4U3Zxt2XWhvV1SNkAldT5tF0U1vP+is6fHxyhk4xqEvg==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' + '@solana/rpc-transport-http@5.0.0': + resolution: {integrity: sha512-RoIEvWp7yc7rIRzNkOyjLs2UQF0odIEMWj87dbD4Ir4hwTCGo/TSTfQF/8KDV2etdke3Fa1K+W1NkpG2POqWFg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/rpc-types@2.3.0': resolution: {integrity: sha512-O09YX2hED2QUyGxrMOxQ9GzH1LlEwwZWu69QbL4oYmIf6P5dzEEHcqRY6L1LsDVqc/dzAdEs/E1FaPrcIaIIPw==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' + '@solana/rpc-types@5.0.0': + resolution: {integrity: sha512-JMbhwnV6nX4ezJv/KmaElOR0r/MZTKzKpaz6cv7FopLNuPrYCBrRCZKuM2XQh6gUbt9Mey08/KBOmOGmzTbL/g==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/rpc@2.3.0': resolution: {integrity: sha512-ZWN76iNQAOCpYC7yKfb3UNLIMZf603JckLKOOLTHuy9MZnTN8XV6uwvDFhf42XvhglgUjGCEnbUqWtxQ9pa/pQ==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' + '@solana/rpc@5.0.0': + resolution: {integrity: sha512-Myx/ZBmMHkgh9Di3tLzc+vd30f+6YC1JXr9+YmIHKEeqN/+iTHkDJU2E/hGRLy8vTOBOU7+2466A+dLnSVuGkg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/signers@2.3.0': resolution: {integrity: sha512-OSv6fGr/MFRx6J+ZChQMRqKNPGGmdjkqarKkRzkwmv7v8quWsIRnJT5EV8tBy3LI4DLO/A8vKiNSPzvm1TdaiQ==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' + '@solana/signers@5.0.0': + resolution: {integrity: sha512-9Hw6HekSEzj5O7UBBFPrxk96W5e8tMI3n7KbW7/QiKBDpuvYw9WtnjOsWUE7LqQoc1P0JjGEsrmxE9raQBLvuQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/subscribable@2.3.0': resolution: {integrity: sha512-DkgohEDbMkdTWiKAoatY02Njr56WXx9e/dKKfmne8/Ad6/2llUIrax78nCdlvZW9quXMaXPTxZvdQqo9N669Og==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' + '@solana/subscribable@5.0.0': + resolution: {integrity: sha512-C2TydIRRd5XUJ8asbARi67Sj/3DRLubWalnNoafBhDsrb88jsRVylntvwXgBw/+lwJdEPEsUnxvcdgdm+3lFlw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/sysvars@2.3.0': resolution: {integrity: sha512-LvjADZrpZ+CnhlHqfI5cmsRzX9Rpyb1Ox2dMHnbsRNzeKAMhu9w4ZBIaeTdO322zsTr509G1B+k2ABD3whvUBA==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' + '@solana/sysvars@5.0.0': + resolution: {integrity: sha512-F/GEb2rS8mrgDd79lDPyu8za9jGE6cRlS4jHNeKCkvOCJxdKQbX34JIzx4kwzjtvk7O8/yrDHfGdpA8nBg/l4w==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/transaction-confirmation@2.3.0': resolution: {integrity: sha512-UiEuiHCfAAZEKdfne/XljFNJbsKAe701UQHKXEInYzIgBjRbvaeYZlBmkkqtxwcasgBTOmEaEKT44J14N9VZDw==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' + '@solana/transaction-confirmation@5.0.0': + resolution: {integrity: sha512-LpusTopYIuQC8hBCloExkTr4Z5/zdp5f4IIbzD5XFeW3xXPZytS3H1IDMGk4bmLdZi9zQNA4lnNHKra5IncRbw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/transaction-messages@2.3.0': resolution: {integrity: sha512-bgqvWuy3MqKS5JdNLH649q+ngiyOu5rGS3DizSnWwYUd76RxZl1kN6CoqHSrrMzFMvis6sck/yPGG3wqrMlAww==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' + '@solana/transaction-messages@5.0.0': + resolution: {integrity: sha512-rJLe1wUGW5DovQFV0gjXHXnriPxTBgZ3TvGWnjCu2OIBU8mcQkQVJ7zzVZY2IAYlmJ6OSF9nvzhSt/ncPbkJPg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/transactions@2.3.0': resolution: {integrity: sha512-LnTvdi8QnrQtuEZor5Msje61sDpPstTVwKg4y81tNxDhiyomjuvnSNLAq6QsB9gIxUqbNzPZgOG9IU4I4/Uaug==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' + '@solana/transactions@5.0.0': + resolution: {integrity: sha512-4TcsqH7JtgRKGGBIRRGz0n+tXu4h5TPPC49kkV0ygIndQaHW7FOZUYTwQ0epq0A5h9KYi+ClNbzF9xiuDbAD5Q==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@tanstack/query-core@5.90.2': resolution: {integrity: sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==} @@ -1133,12 +1348,6 @@ packages: get-tsconfig@4.10.1: resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} - gill@0.11.0: - resolution: {integrity: sha512-kEdLdSMFuvaDl3O38rTAXLQdyOG0QEtFQRG4kDArx3pDIh3T/5PhhPZ1zmmC0q7FpVZk7Eks2W6NdDKQjSq/6A==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5' - gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -2621,22 +2830,14 @@ snapshots: '@socket.io/component-emitter@3.1.2': {} - '@solana-program/address-lookup-table@0.7.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': - dependencies: - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana-program/compute-budget@0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': dependencies: '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana-program/system@0.7.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': + '@solana-program/token-2022@0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))': dependencies: '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - - '@solana-program/token-2022@0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))': - dependencies: - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/sysvars': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/sysvars': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana-program/token@0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': dependencies: @@ -2654,6 +2855,18 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/accounts@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 5.0.0(typescript@5.9.2) + '@solana/codecs-strings': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 5.0.0(typescript@5.9.2) + '@solana/rpc-spec': 5.0.0(typescript@5.9.2) + '@solana/rpc-types': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/addresses@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/assertions': 2.3.0(typescript@5.9.2) @@ -2665,16 +2878,37 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/addresses@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/assertions': 5.0.0(typescript@5.9.2) + '@solana/codecs-core': 5.0.0(typescript@5.9.2) + '@solana/codecs-strings': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 5.0.0(typescript@5.9.2) + '@solana/nominal-types': 5.0.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/assertions@2.3.0(typescript@5.9.2)': dependencies: '@solana/errors': 2.3.0(typescript@5.9.2) typescript: 5.9.2 + '@solana/assertions@5.0.0(typescript@5.9.2)': + dependencies: + '@solana/errors': 5.0.0(typescript@5.9.2) + typescript: 5.9.2 + '@solana/codecs-core@2.3.0(typescript@5.9.2)': dependencies: '@solana/errors': 2.3.0(typescript@5.9.2) typescript: 5.9.2 + '@solana/codecs-core@5.0.0(typescript@5.9.2)': + dependencies: + '@solana/errors': 5.0.0(typescript@5.9.2) + typescript: 5.9.2 + '@solana/codecs-data-structures@2.3.0(typescript@5.9.2)': dependencies: '@solana/codecs-core': 2.3.0(typescript@5.9.2) @@ -2682,12 +2916,25 @@ snapshots: '@solana/errors': 2.3.0(typescript@5.9.2) typescript: 5.9.2 + '@solana/codecs-data-structures@5.0.0(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 5.0.0(typescript@5.9.2) + '@solana/codecs-numbers': 5.0.0(typescript@5.9.2) + '@solana/errors': 5.0.0(typescript@5.9.2) + typescript: 5.9.2 + '@solana/codecs-numbers@2.3.0(typescript@5.9.2)': dependencies: '@solana/codecs-core': 2.3.0(typescript@5.9.2) '@solana/errors': 2.3.0(typescript@5.9.2) typescript: 5.9.2 + '@solana/codecs-numbers@5.0.0(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 5.0.0(typescript@5.9.2) + '@solana/errors': 5.0.0(typescript@5.9.2) + typescript: 5.9.2 + '@solana/codecs-strings@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/codecs-core': 2.3.0(typescript@5.9.2) @@ -2696,6 +2943,14 @@ snapshots: fastestsmallesttextencoderdecoder: 1.0.22 typescript: 5.9.2 + '@solana/codecs-strings@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 5.0.0(typescript@5.9.2) + '@solana/codecs-numbers': 5.0.0(typescript@5.9.2) + '@solana/errors': 5.0.0(typescript@5.9.2) + fastestsmallesttextencoderdecoder: 1.0.22 + typescript: 5.9.2 + '@solana/codecs@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/codecs-core': 2.3.0(typescript@5.9.2) @@ -2707,26 +2962,68 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/codecs@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 5.0.0(typescript@5.9.2) + '@solana/codecs-data-structures': 5.0.0(typescript@5.9.2) + '@solana/codecs-numbers': 5.0.0(typescript@5.9.2) + '@solana/codecs-strings': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/options': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/errors@2.3.0(typescript@5.9.2)': dependencies: chalk: 5.6.2 commander: 14.0.1 typescript: 5.9.2 + '@solana/errors@5.0.0(typescript@5.9.2)': + dependencies: + chalk: 5.6.2 + commander: 14.0.1 + typescript: 5.9.2 + '@solana/fast-stable-stringify@2.3.0(typescript@5.9.2)': dependencies: typescript: 5.9.2 + '@solana/fast-stable-stringify@5.0.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + '@solana/functional@2.3.0(typescript@5.9.2)': dependencies: typescript: 5.9.2 + '@solana/functional@5.0.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + + '@solana/instruction-plans@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/errors': 5.0.0(typescript@5.9.2) + '@solana/instructions': 5.0.0(typescript@5.9.2) + '@solana/promises': 5.0.0(typescript@5.9.2) + '@solana/transaction-messages': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/instructions@2.3.0(typescript@5.9.2)': dependencies: '@solana/codecs-core': 2.3.0(typescript@5.9.2) '@solana/errors': 2.3.0(typescript@5.9.2) typescript: 5.9.2 + '@solana/instructions@5.0.0(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 5.0.0(typescript@5.9.2) + '@solana/errors': 5.0.0(typescript@5.9.2) + typescript: 5.9.2 + '@solana/keys@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/assertions': 2.3.0(typescript@5.9.2) @@ -2738,6 +3035,17 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/keys@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/assertions': 5.0.0(typescript@5.9.2) + '@solana/codecs-core': 5.0.0(typescript@5.9.2) + '@solana/codecs-strings': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 5.0.0(typescript@5.9.2) + '@solana/nominal-types': 5.0.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/accounts': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -2763,10 +3071,40 @@ snapshots: - fastestsmallesttextencoderdecoder - ws + '@solana/kit@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/accounts': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/addresses': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 5.0.0(typescript@5.9.2) + '@solana/functional': 5.0.0(typescript@5.9.2) + '@solana/instruction-plans': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/instructions': 5.0.0(typescript@5.9.2) + '@solana/keys': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/programs': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-parsed-types': 5.0.0(typescript@5.9.2) + '@solana/rpc-spec-types': 5.0.0(typescript@5.9.2) + '@solana/rpc-subscriptions': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-types': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/signers': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/sysvars': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-confirmation': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/transaction-messages': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + '@solana/nominal-types@2.3.0(typescript@5.9.2)': dependencies: typescript: 5.9.2 + '@solana/nominal-types@5.0.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + '@solana/options@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/codecs-core': 2.3.0(typescript@5.9.2) @@ -2778,6 +3116,17 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/options@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 5.0.0(typescript@5.9.2) + '@solana/codecs-data-structures': 5.0.0(typescript@5.9.2) + '@solana/codecs-numbers': 5.0.0(typescript@5.9.2) + '@solana/codecs-strings': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 5.0.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/programs@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -2786,10 +3135,22 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/programs@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 5.0.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/promises@2.3.0(typescript@5.9.2)': dependencies: typescript: 5.9.2 + '@solana/promises@5.0.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + '@solana/rpc-api@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -2807,20 +3168,51 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/rpc-api@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 5.0.0(typescript@5.9.2) + '@solana/codecs-strings': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 5.0.0(typescript@5.9.2) + '@solana/keys': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-parsed-types': 5.0.0(typescript@5.9.2) + '@solana/rpc-spec': 5.0.0(typescript@5.9.2) + '@solana/rpc-transformers': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-types': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-messages': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/rpc-parsed-types@2.3.0(typescript@5.9.2)': dependencies: typescript: 5.9.2 + '@solana/rpc-parsed-types@5.0.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + '@solana/rpc-spec-types@2.3.0(typescript@5.9.2)': dependencies: typescript: 5.9.2 + '@solana/rpc-spec-types@5.0.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + '@solana/rpc-spec@2.3.0(typescript@5.9.2)': dependencies: '@solana/errors': 2.3.0(typescript@5.9.2) '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) typescript: 5.9.2 + '@solana/rpc-spec@5.0.0(typescript@5.9.2)': + dependencies: + '@solana/errors': 5.0.0(typescript@5.9.2) + '@solana/rpc-spec-types': 5.0.0(typescript@5.9.2) + typescript: 5.9.2 + '@solana/rpc-subscriptions-api@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -2834,6 +3226,19 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/rpc-subscriptions-api@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/keys': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-subscriptions-spec': 5.0.0(typescript@5.9.2) + '@solana/rpc-transformers': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-types': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-messages': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/rpc-subscriptions-channel-websocket@2.3.0(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/errors': 2.3.0(typescript@5.9.2) @@ -2843,6 +3248,15 @@ snapshots: typescript: 5.9.2 ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@solana/rpc-subscriptions-channel-websocket@5.0.0(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/errors': 5.0.0(typescript@5.9.2) + '@solana/functional': 5.0.0(typescript@5.9.2) + '@solana/rpc-subscriptions-spec': 5.0.0(typescript@5.9.2) + '@solana/subscribable': 5.0.0(typescript@5.9.2) + typescript: 5.9.2 + ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@solana/rpc-subscriptions-spec@2.3.0(typescript@5.9.2)': dependencies: '@solana/errors': 2.3.0(typescript@5.9.2) @@ -2851,6 +3265,14 @@ snapshots: '@solana/subscribable': 2.3.0(typescript@5.9.2) typescript: 5.9.2 + '@solana/rpc-subscriptions-spec@5.0.0(typescript@5.9.2)': + dependencies: + '@solana/errors': 5.0.0(typescript@5.9.2) + '@solana/promises': 5.0.0(typescript@5.9.2) + '@solana/rpc-spec-types': 5.0.0(typescript@5.9.2) + '@solana/subscribable': 5.0.0(typescript@5.9.2) + typescript: 5.9.2 + '@solana/rpc-subscriptions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/errors': 2.3.0(typescript@5.9.2) @@ -2869,6 +3291,24 @@ snapshots: - fastestsmallesttextencoderdecoder - ws + '@solana/rpc-subscriptions@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/errors': 5.0.0(typescript@5.9.2) + '@solana/fast-stable-stringify': 5.0.0(typescript@5.9.2) + '@solana/functional': 5.0.0(typescript@5.9.2) + '@solana/promises': 5.0.0(typescript@5.9.2) + '@solana/rpc-spec-types': 5.0.0(typescript@5.9.2) + '@solana/rpc-subscriptions-api': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-subscriptions-channel-websocket': 5.0.0(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions-spec': 5.0.0(typescript@5.9.2) + '@solana/rpc-transformers': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-types': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/subscribable': 5.0.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + '@solana/rpc-transformers@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/errors': 2.3.0(typescript@5.9.2) @@ -2880,6 +3320,17 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/rpc-transformers@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/errors': 5.0.0(typescript@5.9.2) + '@solana/functional': 5.0.0(typescript@5.9.2) + '@solana/nominal-types': 5.0.0(typescript@5.9.2) + '@solana/rpc-spec-types': 5.0.0(typescript@5.9.2) + '@solana/rpc-types': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/rpc-transport-http@2.3.0(typescript@5.9.2)': dependencies: '@solana/errors': 2.3.0(typescript@5.9.2) @@ -2888,6 +3339,14 @@ snapshots: typescript: 5.9.2 undici-types: 7.16.0 + '@solana/rpc-transport-http@5.0.0(typescript@5.9.2)': + dependencies: + '@solana/errors': 5.0.0(typescript@5.9.2) + '@solana/rpc-spec': 5.0.0(typescript@5.9.2) + '@solana/rpc-spec-types': 5.0.0(typescript@5.9.2) + typescript: 5.9.2 + undici-types: 7.16.0 + '@solana/rpc-types@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -2900,6 +3359,18 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/rpc-types@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 5.0.0(typescript@5.9.2) + '@solana/codecs-numbers': 5.0.0(typescript@5.9.2) + '@solana/codecs-strings': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 5.0.0(typescript@5.9.2) + '@solana/nominal-types': 5.0.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/rpc@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/errors': 2.3.0(typescript@5.9.2) @@ -2915,6 +3386,21 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/rpc@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/errors': 5.0.0(typescript@5.9.2) + '@solana/fast-stable-stringify': 5.0.0(typescript@5.9.2) + '@solana/functional': 5.0.0(typescript@5.9.2) + '@solana/rpc-api': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-spec': 5.0.0(typescript@5.9.2) + '@solana/rpc-spec-types': 5.0.0(typescript@5.9.2) + '@solana/rpc-transformers': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-transport-http': 5.0.0(typescript@5.9.2) + '@solana/rpc-types': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/signers@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -2929,11 +3415,30 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/signers@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 5.0.0(typescript@5.9.2) + '@solana/errors': 5.0.0(typescript@5.9.2) + '@solana/instructions': 5.0.0(typescript@5.9.2) + '@solana/keys': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/nominal-types': 5.0.0(typescript@5.9.2) + '@solana/transaction-messages': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/subscribable@2.3.0(typescript@5.9.2)': dependencies: '@solana/errors': 2.3.0(typescript@5.9.2) typescript: 5.9.2 + '@solana/subscribable@5.0.0(typescript@5.9.2)': + dependencies: + '@solana/errors': 5.0.0(typescript@5.9.2) + typescript: 5.9.2 + '@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/accounts': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -2944,6 +3449,16 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/accounts': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 5.0.0(typescript@5.9.2) + '@solana/rpc-types': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/transaction-confirmation@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -2961,6 +3476,23 @@ snapshots: - fastestsmallesttextencoderdecoder - ws + '@solana/transaction-confirmation@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/addresses': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-strings': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 5.0.0(typescript@5.9.2) + '@solana/keys': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/promises': 5.0.0(typescript@5.9.2) + '@solana/rpc': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-subscriptions': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-types': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-messages': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + '@solana/transaction-messages@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -2976,6 +3508,21 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/transaction-messages@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 5.0.0(typescript@5.9.2) + '@solana/codecs-data-structures': 5.0.0(typescript@5.9.2) + '@solana/codecs-numbers': 5.0.0(typescript@5.9.2) + '@solana/errors': 5.0.0(typescript@5.9.2) + '@solana/functional': 5.0.0(typescript@5.9.2) + '@solana/instructions': 5.0.0(typescript@5.9.2) + '@solana/nominal-types': 5.0.0(typescript@5.9.2) + '@solana/rpc-types': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/transactions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -2994,6 +3541,24 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/transactions@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 5.0.0(typescript@5.9.2) + '@solana/codecs-data-structures': 5.0.0(typescript@5.9.2) + '@solana/codecs-numbers': 5.0.0(typescript@5.9.2) + '@solana/codecs-strings': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 5.0.0(typescript@5.9.2) + '@solana/functional': 5.0.0(typescript@5.9.2) + '@solana/instructions': 5.0.0(typescript@5.9.2) + '@solana/keys': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/nominal-types': 5.0.0(typescript@5.9.2) + '@solana/rpc-types': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-messages': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@tanstack/query-core@5.90.2': {} '@tanstack/react-query@5.90.2(react@18.3.1)': @@ -3958,22 +4523,6 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 - gill@0.11.0(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)): - dependencies: - '@solana-program/address-lookup-table': 0.7.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - '@solana-program/compute-budget': 0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - '@solana-program/system': 0.7.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - '@solana-program/token-2022': 0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)) - '@solana/assertions': 2.3.0(typescript@5.9.2) - '@solana/codecs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - typescript: 5.9.2 - transitivePeerDependencies: - - '@solana/sysvars' - - fastestsmallesttextencoderdecoder - - ws - gopd@1.2.0: {} h3@1.15.4: @@ -4663,10 +5212,10 @@ snapshots: bufferutil: 4.0.9 utf-8-validate: 5.0.10 - x402-fetch@0.6.0(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + x402-fetch@0.6.0(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)): dependencies: viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - x402: 0.6.1(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + x402: 0.6.1(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) zod: 3.25.76 transitivePeerDependencies: - '@azure/app-configuration' @@ -4702,12 +5251,12 @@ snapshots: - utf-8-validate - ws - x402@0.6.1(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + x402@0.6.1(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)): dependencies: '@scure/base': 1.2.6 '@solana-program/compute-budget': 0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))) '@solana-program/token': 0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - '@solana-program/token-2022': 0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)) + '@solana-program/token-2022': 0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)) '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) viem: 2.37.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) diff --git a/docs/x402/demo/client/src/setup.ts b/docs/x402/demo/client/src/setup.ts index ae7b82a4..66b7cb9d 100644 --- a/docs/x402/demo/client/src/setup.ts +++ b/docs/x402/demo/client/src/setup.ts @@ -1,7 +1,8 @@ -import { getBase58Decoder, getBase58Encoder, createKeyPairSignerFromBytes, createSolanaRpc, Address, lamports, LAMPORTS_PER_SOL, createSolanaClient } from "gill"; +import { getBase58Decoder, getBase58Encoder, createKeyPairSignerFromBytes, Address, lamports, createSolanaRpc } from "@solana/kit"; import { appendFile } from 'fs/promises'; import path from "path"; +const LAMPORTS_PER_SOL = 1_000_000_000; async function createB58SecretKey(): Promise { // await assertKeyGenerationIsAvailable(); @@ -78,8 +79,8 @@ const addKeypairToEnvFile = async ( }; async function airdrop(address: Address) { - const client = createSolanaClient({ urlOrMoniker: 'devnet' }); - await client.rpc.requestAirdrop( + const rpc = createSolanaRpc('https://api.devnet.solana.com'); + await rpc.requestAirdrop( address, lamports(BigInt(LAMPORTS_PER_SOL / 10)), { commitment: 'processed' } From fd52311f70191b639547ba87bd50c08016d31bf9 Mon Sep 17 00:00:00 2001 From: amilz <85324096+amilz@users.noreply.github.com> Date: Fri, 21 Nov 2025 04:40:27 -0800 Subject: [PATCH 28/29] feat: minor audit edits (#248) * docs: tweaks * feat: Improvements post audit fixes Add warning for unsupported payment tokens in transaction processing - Introduced a log warning when a payment is attempted with a token mint that is not supported by the current configuration. - Updated documentation to clarify that only tokens listed in `allowed_spl_paid_tokens` will be accepted for transaction fees, with unsupported tokens being silently ignored. * Replace saturating_add with checked_add * Graceful error handling for tower middlewares * Added missing unit test checks for config * Added warning if free pricing is used * Should be able to unpack account, else error out * Better visibility / safety for signer pool * Multiply before divide to improve precision in math calc --------- Co-authored-by: Jo D --- crates/lib/src/fee/fee.rs | 15 ++++- crates/lib/src/metrics/balance.rs | 2 +- crates/lib/src/metrics/handler.rs | 34 +++++----- crates/lib/src/metrics/middleware.rs | 24 +++++-- crates/lib/src/rpc_server/auth.rs | 26 ++++---- .../lib/src/rpc_server/method/get_config.rs | 6 ++ .../src/rpc_server/method/get_payer_signer.rs | 4 +- crates/lib/src/rpc_server/middleware_utils.rs | 34 ++++++++-- crates/lib/src/signer/mod.rs | 2 +- crates/lib/src/signer/pool.rs | 60 +++++++++++------ crates/lib/src/state.rs | 9 +-- crates/lib/src/tests/common/mod.rs | 2 +- crates/lib/src/token/token.rs | 64 +++++++++++-------- .../src/transaction/versioned_transaction.rs | 2 +- crates/lib/src/validator/config_validator.rs | 6 ++ docs/getting-started/QUICK_START.md | 3 +- docs/operators/CONFIGURATION.md | 2 + 17 files changed, 185 insertions(+), 110 deletions(-) diff --git a/crates/lib/src/fee/fee.rs b/crates/lib/src/fee/fee.rs index e3f0f915..b0973310 100644 --- a/crates/lib/src/fee/fee.rs +++ b/crates/lib/src/fee/fee.rs @@ -398,7 +398,10 @@ impl FeeConfigUtil { instruction { if *sender == *fee_payer_pubkey { - total = total.saturating_add(*lamports); + total = total.checked_add(*lamports).ok_or_else(|| { + log::error!("Outflow calculation overflow in SystemTransfer"); + KoraError::ValidationError("Outflow calculation overflow".to_string()) + })?; } if *receiver == *fee_payer_pubkey { total = total.saturating_sub(*lamports); @@ -414,7 +417,10 @@ impl FeeConfigUtil { instruction { if *payer == *fee_payer_pubkey { - total = total.saturating_add(*lamports); + total = total.checked_add(*lamports).ok_or_else(|| { + log::error!("Outflow calculation overflow in SystemCreateAccount"); + KoraError::ValidationError("Outflow calculation overflow".to_string()) + })?; } } } @@ -430,7 +436,10 @@ impl FeeConfigUtil { } = instruction { if *nonce_authority == *fee_payer_pubkey { - total = total.saturating_add(*lamports); + total = total.checked_add(*lamports).ok_or_else(|| { + log::error!("Outflow calculation overflow in SystemWithdrawNonceAccount"); + KoraError::ValidationError("Outflow calculation overflow".to_string()) + })?; } if *recipient == *fee_payer_pubkey { total = total.saturating_sub(*lamports); diff --git a/crates/lib/src/metrics/balance.rs b/crates/lib/src/metrics/balance.rs index 2d6db7ee..f4ed97c1 100644 --- a/crates/lib/src/metrics/balance.rs +++ b/crates/lib/src/metrics/balance.rs @@ -150,7 +150,7 @@ mod tests { use super::*; use crate::{ config::FeePayerBalanceMetricsConfig, - signer::{SignerPool, SignerWithMetadata}, + signer::{pool::SignerWithMetadata, SignerPool}, state::update_signer_pool, tests::{ account_mock::create_mock_account_with_balance, diff --git a/crates/lib/src/metrics/handler.rs b/crates/lib/src/metrics/handler.rs index 0c6cecbc..8dc00e62 100644 --- a/crates/lib/src/metrics/handler.rs +++ b/crates/lib/src/metrics/handler.rs @@ -1,7 +1,11 @@ +use crate::rpc_server::middleware_utils::build_response_with_graceful_error; use futures_util::future::BoxFuture; use http::{Request, Response, StatusCode}; use jsonrpsee::server::logger::Body; -use std::task::{Context, Poll}; +use std::{ + collections::HashMap, + task::{Context, Poll}, +}; use tower::{Layer, Service}; /// Layer that intercepts /metrics requests and returns Prometheus metrics directly @@ -56,21 +60,19 @@ where // Return metrics directly Box::pin(async move { match crate::metrics::gather() { - Ok(metrics) => { - let response = Response::builder() - .status(StatusCode::OK) - .header("content-type", "text/plain; version=0.0.4") - .body(Body::from(metrics)) - .expect("Failed to build response"); - Ok(response) - } - Err(e) => { - let response = Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(Body::from(format!("Error gathering metrics: {e}"))) - .expect("Failed to build response"); - Ok(response) - } + Ok(metrics) => Ok(build_response_with_graceful_error( + Some(HashMap::from([( + "content-type".to_string(), + "text/plain; version=0.0.4".to_string(), + )])), + StatusCode::OK, + &metrics, + )), + Err(e) => Ok(build_response_with_graceful_error( + None, + StatusCode::INTERNAL_SERVER_ERROR, + &format!("Error gathering metrics: {e}"), + )), } }) } else { diff --git a/crates/lib/src/metrics/middleware.rs b/crates/lib/src/metrics/middleware.rs index 2773757b..46ba687f 100644 --- a/crates/lib/src/metrics/middleware.rs +++ b/crates/lib/src/metrics/middleware.rs @@ -21,7 +21,10 @@ impl HttpMetrics { Opts::new("http_requests_total", "Total number of HTTP requests").namespace("kora"), &["method", "status"], ) - .expect("Failed to create http_requests_total metric"); + .unwrap_or_else(|e| { + log::error!("Failed to create http_requests_total metric: {e:?}"); + panic!("Metrics initialization failed - cannot continue") + }); let request_duration_seconds = HistogramVec::new( prometheus::HistogramOpts::new( @@ -32,12 +35,19 @@ impl HttpMetrics { .buckets(vec![0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 5.0, 10.0]), &["method"], ) - .expect("Failed to create http_request_duration_seconds metric"); - - prometheus::register(Box::new(requests_total.clone())) - .expect("Failed to register http_requests_total metric"); - prometheus::register(Box::new(request_duration_seconds.clone())) - .expect("Failed to register http_request_duration_seconds metric"); + .unwrap_or_else(|e| { + log::error!("Failed to create http_request_duration_seconds metric: {e:?}"); + panic!("Metrics initialization failed - cannot continue") + }); + + prometheus::register(Box::new(requests_total.clone())).unwrap_or_else(|e| { + log::error!("Failed to register http_requests_total metric: {e:?}"); + panic!("Metrics initialization failed - cannot continue") + }); + prometheus::register(Box::new(request_duration_seconds.clone())).unwrap_or_else(|e| { + log::error!("Failed to register http_request_duration_seconds metric: {e:?}"); + panic!("Metrics initialization failed - cannot continue") + }); Self { requests_total, request_duration_seconds } } diff --git a/crates/lib/src/rpc_server/auth.rs b/crates/lib/src/rpc_server/auth.rs index 881eb354..d7506555 100644 --- a/crates/lib/src/rpc_server/auth.rs +++ b/crates/lib/src/rpc_server/auth.rs @@ -1,6 +1,8 @@ use crate::{ constant::{X_API_KEY, X_HMAC_SIGNATURE, X_TIMESTAMP}, - rpc_server::middleware_utils::{extract_parts_and_body_bytes, get_jsonrpc_method}, + rpc_server::middleware_utils::{ + build_response_with_graceful_error, extract_parts_and_body_bytes, get_jsonrpc_method, + }, }; use hmac::{Hmac, Mac}; use http::{Request, Response, StatusCode}; @@ -55,10 +57,8 @@ where let mut inner = self.inner.clone(); Box::pin(async move { - let unauthorized_response = Response::builder() - .status(StatusCode::UNAUTHORIZED) - .body(Body::empty()) - .expect("Failed to build unauthorized response"); + let unauthorized_response = + build_response_with_graceful_error(None, StatusCode::UNAUTHORIZED, ""); let (parts, body_bytes) = extract_parts_and_body_bytes(request).await; @@ -140,10 +140,8 @@ where let mut inner = self.inner.clone(); Box::pin(async move { - let unauthorized_response = Response::builder() - .status(StatusCode::UNAUTHORIZED) - .body(Body::empty()) - .expect("Failed to build unauthorized response"); + let unauthorized_response = + build_response_with_graceful_error(None, StatusCode::UNAUTHORIZED, ""); let signature_header = request.headers().get(X_HMAC_SIGNATURE).cloned(); let timestamp_header = request.headers().get(X_TIMESTAMP).cloned(); @@ -169,12 +167,10 @@ where let timestamp = timestamp.to_str().unwrap_or(""); // Verify timestamp is within allowed age - let parsed_timestamp = timestamp.parse::(); - if parsed_timestamp.is_err() { - return Ok(unauthorized_response); - } - - let ts = parsed_timestamp.expect("timestamp already validated"); + let ts = match timestamp.parse::() { + Ok(ts) => ts, + Err(_) => return Ok(unauthorized_response), + }; let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map_err(|e| { diff --git a/crates/lib/src/rpc_server/method/get_config.rs b/crates/lib/src/rpc_server/method/get_config.rs index bb7cd284..3684b38c 100644 --- a/crates/lib/src/rpc_server/method/get_config.rs +++ b/crates/lib/src/rpc_server/method/get_config.rs @@ -107,6 +107,9 @@ mod tests { assert!(!response.validation_config.fee_payer_policy.spl_token.allow_revoke); assert!(!response.validation_config.fee_payer_policy.spl_token.allow_set_authority); assert!(!response.validation_config.fee_payer_policy.spl_token.allow_mint_to); + assert!(!response.validation_config.fee_payer_policy.spl_token.allow_initialize_mint); + assert!(!response.validation_config.fee_payer_policy.spl_token.allow_initialize_account); + assert!(!response.validation_config.fee_payer_policy.spl_token.allow_initialize_multisig); assert!(!response.validation_config.fee_payer_policy.spl_token.allow_freeze_account); assert!(!response.validation_config.fee_payer_policy.spl_token.allow_thaw_account); @@ -118,6 +121,9 @@ mod tests { assert!(!response.validation_config.fee_payer_policy.token_2022.allow_revoke); assert!(!response.validation_config.fee_payer_policy.token_2022.allow_set_authority); assert!(!response.validation_config.fee_payer_policy.token_2022.allow_mint_to); + assert!(!response.validation_config.fee_payer_policy.token_2022.allow_initialize_mint); + assert!(!response.validation_config.fee_payer_policy.token_2022.allow_initialize_account); + assert!(!response.validation_config.fee_payer_policy.token_2022.allow_initialize_multisig); assert!(!response.validation_config.fee_payer_policy.token_2022.allow_freeze_account); assert!(!response.validation_config.fee_payer_policy.token_2022.allow_thaw_account); // Assert PriceConfig default (check margin value) diff --git a/crates/lib/src/rpc_server/method/get_payer_signer.rs b/crates/lib/src/rpc_server/method/get_payer_signer.rs index 5ab67723..3b077f66 100644 --- a/crates/lib/src/rpc_server/method/get_payer_signer.rs +++ b/crates/lib/src/rpc_server/method/get_payer_signer.rs @@ -19,8 +19,8 @@ pub async fn get_payer_signer() -> Result { let pool = get_signer_pool()?; // Get the next signer according to the configured strategy - let signer_meta = pool.get_next_signer()?; - let signer_pubkey = signer_meta.signer.pubkey(); + let signer = pool.get_next_signer()?; + let signer_pubkey = signer.pubkey(); // Get the payment destination address (falls back to signer if no payment address is configured) let payment_destination = config.kora.get_payment_address(&signer_pubkey)?; diff --git a/crates/lib/src/rpc_server/middleware_utils.rs b/crates/lib/src/rpc_server/middleware_utils.rs index bc2420ae..f95dc826 100644 --- a/crates/lib/src/rpc_server/middleware_utils.rs +++ b/crates/lib/src/rpc_server/middleware_utils.rs @@ -1,4 +1,4 @@ -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use futures_util::TryStreamExt; use http::{Request, Response, StatusCode}; @@ -44,6 +44,27 @@ pub fn verify_jsonrpc_method( Err(KoraError::InvalidRequest("Method not allowed".to_string())) } +pub fn build_response_with_graceful_error( + headers: Option>, + status_code: StatusCode, + error_message: &str, +) -> Response { + let mut builder = Response::builder(); + + if let Some(headers) = headers { + for (key, value) in headers.iter() { + builder = builder.header(key, value); + } + } + + builder.status(status_code).body(Body::from(error_message.to_string())).unwrap_or_else(|e| { + log::error!("Failed to build response, error: {e:?}"); + let mut response = Response::new(Body::empty()); + *response.status_mut() = status_code; + response + }) +} + /// Method validation layer - applies first in middleware stack to fail fast #[derive(Clone)] pub struct MethodValidationLayer { @@ -98,12 +119,11 @@ where match verify_jsonrpc_method(&body_bytes, &allowed_methods) { Ok(_) => {} Err(_) => { - // Method not allowed - let method_not_allowed_response = Response::builder() - .status(StatusCode::METHOD_NOT_ALLOWED) - .body(Body::empty()) - .expect("Failed to build METHOD_NOT_ALLOWED response"); - return Ok(method_not_allowed_response); + return Ok(build_response_with_graceful_error( + None, + StatusCode::METHOD_NOT_ALLOWED, + "", + )); } } diff --git a/crates/lib/src/signer/mod.rs b/crates/lib/src/signer/mod.rs index 7f3afbdd..b0a6517f 100644 --- a/crates/lib/src/signer/mod.rs +++ b/crates/lib/src/signer/mod.rs @@ -10,5 +10,5 @@ pub use config::{ SignerTypeConfig, TurnkeySignerConfig, VaultSignerConfig, }; pub use keypair_util::KeypairUtil; -pub use pool::{SignerInfo, SignerPool, SignerWithMetadata}; +pub use pool::{SignerInfo, SignerPool}; pub use signer::SolanaSigner; diff --git a/crates/lib/src/signer/pool.rs b/crates/lib/src/signer/pool.rs index a841de17..656b76aa 100644 --- a/crates/lib/src/signer/pool.rs +++ b/crates/lib/src/signer/pool.rs @@ -16,15 +16,15 @@ use std::{ const DEFAULT_WEIGHT: u32 = 1; /// Metadata associated with a signer in the pool -pub struct SignerWithMetadata { +pub(crate) struct SignerWithMetadata { /// Human-readable name for this signer - pub name: String, + name: String, /// The actual signer instance - pub signer: Arc, + signer: Arc, /// Weight for weighted selection (higher = more likely to be selected) - pub weight: u32, + weight: u32, /// Timestamp of last use (Unix timestamp in seconds) - pub last_used: AtomicU64, + last_used: AtomicU64, } impl Clone for SignerWithMetadata { @@ -40,9 +40,18 @@ impl Clone for SignerWithMetadata { impl SignerWithMetadata { /// Create a new signer with metadata - pub fn new(name: String, signer: Arc, weight: u32) -> Self { + pub(crate) fn new(name: String, signer: Arc, weight: u32) -> Self { Self { name, signer, weight, last_used: AtomicU64::new(0) } } + + /// Update the last used timestamp to current time + fn update_last_used(&self) { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + self.last_used.store(now, Ordering::Relaxed); + } } /// A pool of signers with different selection strategies @@ -68,7 +77,7 @@ pub struct SignerInfo { impl SignerPool { #[cfg(test)] - pub fn new(signers: Vec) -> Self { + pub(crate) fn new(signers: Vec) -> Self { let total_weight: u32 = signers.iter().map(|s| s.weight).sum(); Self { @@ -129,16 +138,19 @@ impl SignerPool { } /// Get the next signer according to the configured strategy - pub fn get_next_signer(&self) -> Result<&SignerWithMetadata, KoraError> { + pub fn get_next_signer(&self) -> Result, KoraError> { if self.signers.is_empty() { return Err(KoraError::InternalServerError("Signer pool is empty".to_string())); } - match self.strategy { + let signer_meta = match self.strategy { SelectionStrategy::RoundRobin => self.round_robin_select(), SelectionStrategy::Random => self.random_select(), SelectionStrategy::Weighted => self.weighted_select(), - } + }?; + + signer_meta.update_last_used(); + Ok(Arc::clone(&signer_meta.signer)) } /// Round-robin selection strategy @@ -200,16 +212,20 @@ impl SignerPool { } /// Get a signer by public key (for client consistency signer keys) - pub fn get_signer_by_pubkey(&self, pubkey: &str) -> Result<&SignerWithMetadata, KoraError> { + pub fn get_signer_by_pubkey(&self, pubkey: &str) -> Result, KoraError> { // Try to parse as Pubkey to validate format let target_pubkey = Pubkey::from_str(pubkey).map_err(|_| { KoraError::ValidationError(format!("Invalid signer signer key pubkey: {pubkey}")) })?; // Find signer with matching public key - self.signers.iter().find(|s| s.signer.pubkey() == target_pubkey).ok_or_else(|| { - KoraError::ValidationError(format!("Signer with pubkey {pubkey} not found in pool")) - }) + let signer_meta = + self.signers.iter().find(|s| s.signer.pubkey() == target_pubkey).ok_or_else(|| { + KoraError::ValidationError(format!("Signer with pubkey {pubkey} not found in pool")) + })?; + + signer_meta.update_last_used(); + Ok(Arc::clone(&signer_meta.signer)) } } @@ -249,13 +265,13 @@ mod tests { let mut selections = HashMap::new(); for _ in 0..100 { let signer = pool.get_next_signer().unwrap(); - *selections.entry(signer.name.clone()).or_insert(0) += 1; + *selections.entry(signer.pubkey().to_string()).or_insert(0) += 1; } // Should have selected both signers equally assert_eq!(selections.len(), 2); - assert_eq!(selections["signer_1"], 50); - assert_eq!(selections["signer_2"], 50); + // Each signer should be selected 50 times + assert!(selections.values().all(|&count| count == 50)); } #[test] @@ -263,17 +279,21 @@ mod tests { let mut pool = create_test_pool(); pool.strategy = SelectionStrategy::Weighted; + // Store the public keys for comparison (signer_1 has weight 1, signer_2 has weight 2) + let signer1_pubkey = pool.signers[0].signer.pubkey().to_string(); + let signer2_pubkey = pool.signers[1].signer.pubkey().to_string(); + // Test weighted selection over many iterations let mut selections = HashMap::new(); for _ in 0..300 { let signer = pool.get_next_signer().unwrap(); - *selections.entry(signer.name.clone()).or_insert(0) += 1; + *selections.entry(signer.pubkey().to_string()).or_insert(0) += 1; } // signer_2 has weight 2, signer_1 has weight 1 // So signer_2 should be selected ~2/3 of the time - let signer1_count = selections.get("signer_1").unwrap_or(&0); - let signer2_count = selections.get("signer_2").unwrap_or(&0); + let signer1_count = selections.get(&signer1_pubkey).unwrap_or(&0); + let signer2_count = selections.get(&signer2_pubkey).unwrap_or(&0); // Allow some variance due to randomness assert!(*signer2_count > *signer1_count); diff --git a/crates/lib/src/state.rs b/crates/lib/src/state.rs index 4b7c6008..5e821b46 100644 --- a/crates/lib/src/state.rs +++ b/crates/lib/src/state.rs @@ -21,15 +21,12 @@ pub fn get_request_signer_with_signer_key( // If client provided a signer signer_key, try to use that specific signer if let Some(signer_key) = signer_key { - let signer_meta = pool.get_signer_by_pubkey(signer_key)?; - return Ok(Arc::clone(&signer_meta.signer)); + return pool.get_signer_by_pubkey(signer_key); } // Use configured selection strategy (defaults to round-robin if not specified) - let signer_meta = pool.get_next_signer().map_err(|e| { - KoraError::InternalServerError(format!("Failed to get signer from pool: {e}")) - })?; - Ok(Arc::clone(&signer_meta.signer)) + pool.get_next_signer() + .map_err(|e| KoraError::InternalServerError(format!("Failed to get signer from pool: {e}"))) } /// Initialize the global signer pool with a SignerPool instance diff --git a/crates/lib/src/tests/common/mod.rs b/crates/lib/src/tests/common/mod.rs index 760cbfa1..5763af28 100644 --- a/crates/lib/src/tests/common/mod.rs +++ b/crates/lib/src/tests/common/mod.rs @@ -7,7 +7,7 @@ use std::sync::Arc; /// 2. Centralized re-exports of commonly used mock utilities use crate::{ get_request_signer_with_signer_key, - signer::{SignerPool, SignerWithMetadata}, + signer::{pool::SignerWithMetadata, SignerPool}, state::{get_config, update_config, update_signer_pool}, tests::{account_mock, config_mock::ConfigMockBuilder, rpc_mock}, usage_limit::UsageTracker, diff --git a/crates/lib/src/token/token.rs b/crates/lib/src/token/token.rs index 11e1b5b3..488aa52c 100644 --- a/crates/lib/src/token/token.rs +++ b/crates/lib/src/token/token.rs @@ -122,14 +122,13 @@ impl TokenUtil { .ok_or_else(|| KoraError::ValidationError("Invalid token amount".to_string()))?; let decimals_scale = Decimal::from_u64(10u64.pow(decimals as u32)) .ok_or_else(|| KoraError::ValidationError("Invalid decimals".to_string()))?; + let lamports_per_sol = Decimal::from_u64(LAMPORTS_PER_SOL) + .ok_or_else(|| KoraError::ValidationError("Invalid LAMPORTS_PER_SOL".to_string()))?; - // Calculate: (amount / 10^decimals) * price * LAMPORTS_PER_SOL - let token_value = amount_decimal / decimals_scale; - let sol_value = token_value * token_price.price; - let lamports_decimal = sol_value - * Decimal::from_u64(LAMPORTS_PER_SOL).ok_or_else(|| { - KoraError::ValidationError("Invalid LAMPORTS_PER_SOL".to_string()) - })?; + // Calculate: (amount * price * LAMPORTS_PER_SOL) / 10^decimals + // Multiply before divide to preserve precision + let lamports_decimal = + (amount_decimal * token_price.price * lamports_per_sol) / decimals_scale; // Floor and convert to u64 let lamports = lamports_decimal @@ -149,18 +148,18 @@ impl TokenUtil { let (token_price, decimals) = Self::get_token_price_and_decimals(mint, price_source.clone(), rpc_client).await?; - // Convert lamports to SOL using Decimal + // Convert lamports to token base units let lamports_decimal = Decimal::from_u64(lamports) .ok_or_else(|| KoraError::ValidationError("Invalid lamports value".to_string()))?; let lamports_per_sol_decimal = Decimal::from_u64(LAMPORTS_PER_SOL) .ok_or_else(|| KoraError::ValidationError("Invalid LAMPORTS_PER_SOL".to_string()))?; - let sol_value = lamports_decimal / lamports_per_sol_decimal; - - // Convert SOL to token base units - let token_value = sol_value / token_price.price; let scale = Decimal::from_u64(10u64.pow(decimals as u32)) .ok_or_else(|| KoraError::ValidationError("Invalid decimals".to_string()))?; - let token_amount = token_value * scale; + + // Calculate: (lamports * 10^decimals) / (LAMPORTS_PER_SOL * price) + // Multiply before divide to preserve precision + let token_amount = + (lamports_decimal * scale) / (lamports_per_sol_decimal * token_price.price); // Ceil and convert to u64 let result = token_amount @@ -208,15 +207,19 @@ impl TokenUtil { Ok(dest_account) => { let token_program = TokenType::get_token_program_from_owner(&dest_account.owner)?; - if let Ok(token_account) = - token_program.unpack_token_account(&dest_account.data) - { - if token_account.owner() == *fee_payer { - mint_to_transfers - .entry(*mint_pubkey) - .or_default() - .push((*amount, false)); // inflow - } + let token_account = token_program + .unpack_token_account(&dest_account.data) + .map_err(|e| { + KoraError::TokenOperationError(format!( + "Failed to unpack destination token account {}: {}", + destination_address, e + )) + })?; + if token_account.owner() == *fee_payer { + mint_to_transfers + .entry(*mint_pubkey) + .or_default() + .push((*amount, false)); // inflow } } Err(e) => { @@ -298,13 +301,14 @@ impl TokenUtil { })?; let decimals_scale = Decimal::from_u64(10u64.pow(*decimals as u32)) .ok_or_else(|| KoraError::ValidationError("Invalid decimals".to_string()))?; + let lamports_per_sol = Decimal::from_u64(LAMPORTS_PER_SOL).ok_or_else(|| { + KoraError::ValidationError("Invalid LAMPORTS_PER_SOL".to_string()) + })?; - let token_value = amount_decimal / decimals_scale; - let sol_value = token_value * price.price; - let lamports_decimal = sol_value - * Decimal::from_u64(LAMPORTS_PER_SOL).ok_or_else(|| { - KoraError::ValidationError("Invalid LAMPORTS_PER_SOL".to_string()) - })?; + // Calculate: (amount * price * LAMPORTS_PER_SOL) / 10^decimals + // Multiply before divide to preserve precision + let lamports_decimal = + (amount_decimal * price.price * lamports_per_sol) / decimals_scale; let lamports = lamports_decimal.floor().to_u64().ok_or_else(|| { KoraError::ValidationError("Lamports value overflow".to_string()) @@ -459,6 +463,10 @@ impl TokenUtil { } if !config.validation.supports_token(&token_state.mint().to_string()) { + log::warn!( + "Ignoring payment with unsupported token mint: {}", + token_state.mint(), + ); continue; } diff --git a/crates/lib/src/transaction/versioned_transaction.rs b/crates/lib/src/transaction/versioned_transaction.rs index 33b4e7a6..c543bd36 100644 --- a/crates/lib/src/transaction/versioned_transaction.rs +++ b/crates/lib/src/transaction/versioned_transaction.rs @@ -266,7 +266,7 @@ impl VersionedTransactionOps for VersionedTransactionResolved { // Validate payment if price model is not Free if required_lamports > 0 { - log::error!("Payment validation: required_lamports={}", required_lamports); + log::info!("Payment validation: required_lamports={}", required_lamports); // Get the expected payment destination let payment_destination = config.kora.get_payment_address(&fee_payer)?; diff --git a/crates/lib/src/validator/config_validator.rs b/crates/lib/src/validator/config_validator.rs index a3d76718..c5ab4021 100644 --- a/crates/lib/src/validator/config_validator.rs +++ b/crates/lib/src/validator/config_validator.rs @@ -387,6 +387,12 @@ impl ConfigValidator { "When fees are enabled, allowed_spl_paid_tokens cannot be empty".to_string(), ); } + } else { + warnings.push( + "โš ๏ธ SECURITY: Free pricing model enabled - all transactions will be processed \ + without charging fees." + .to_string(), + ); } // Validate that all tokens in allowed_spl_paid_tokens are also in allowed_tokens diff --git a/docs/getting-started/QUICK_START.md b/docs/getting-started/QUICK_START.md index 0330b1ed..4fa28793 100644 --- a/docs/getting-started/QUICK_START.md +++ b/docs/getting-started/QUICK_START.md @@ -221,9 +221,8 @@ explore additional Kora RPC methods: - `estimateTransactionFee` - Calculate fees for transactions - `getPayerSigner` - Get the payer signer and payment destination - `getSupportedTokens` - Returns an array of supported payment tokens -- `signTransaction` - Sign transactions with the Kora feepayer - `transferTransaction` - Create transfer SOL or SPL token transfer transactions (signed by the Kora feepayer) -- `signAndSendTransaction` - Signs a transaction with the Kora feepayer and sends it to the configured Solana RPC +- `signAndSendTransaction` - Conditionally signs a transaction with the Kora feepayer and sends it to the configured Solana RPC if fees are covered - `signTransaction` - Conditionally sign transactions when fees are covered - `getPaymentInstruction` - Get a payment instruction for a transaction diff --git a/docs/operators/CONFIGURATION.md b/docs/operators/CONFIGURATION.md index 6317e6a6..2a3072b5 100644 --- a/docs/operators/CONFIGURATION.md +++ b/docs/operators/CONFIGURATION.md @@ -223,6 +223,8 @@ disallowed_accounts = [ > *Note: Empty arrays are allowed, but you will need to specify at least one whitelisted `allowed_programs`, `allowed_tokens`, `allowed_spl_paid_tokens` for the Kora node to be able to process transactions. You need to specify the System Program or Token Program for the Kora node to be able to process transfers. To enable common instruction types (e.g., Compute Budget, Address Lookup Table), you need to specify the Compute Budget Program or Address Lookup Table Program, etc.* +> **โš ๏ธ Payment Token Validation Warning**: Only tokens listed in `allowed_spl_paid_tokens` will be accepted as payment for transaction fees. If a transaction contains payment transfers using unsupported tokens (tokens not in this list), those payments will be silently ignored. + ## Token-2022 Extension Blocking The `[validation.token2022]` section allows you to block specific Token-2022 extensions for enhanced security. All extensions are enabled by default. You can block specific extensions by adding them to the `blocked_mint_extensions` or `blocked_account_extensions` arrays: From 8b25ddc53b28b6d3a2cd0798c33d8e9102006a0c Mon Sep 17 00:00:00 2001 From: jo <17280917+dev-jodee@users.noreply.github.com> Date: Fri, 21 Nov 2025 10:57:53 -0500 Subject: [PATCH 29/29] chore: add git-cliff configuration and update Rust publish workflow (#250) - Introduced a new configuration file for git-cliff to streamline changelog generation. - Updated the Rust publish workflow to improve version handling and changelog generation. - Modified the SDK publish workflow to allow manual triggering and enabled GitHub releases creation. --- .github/cliff.toml | 30 +++++ .github/workflows/rust-publish.yml | 173 ++++++++++++--------------- .github/workflows/ts-sdk-publish.yml | 14 +-- 3 files changed, 110 insertions(+), 107 deletions(-) create mode 100644 .github/cliff.toml diff --git a/.github/cliff.toml b/.github/cliff.toml new file mode 100644 index 00000000..fcf38918 --- /dev/null +++ b/.github/cliff.toml @@ -0,0 +1,30 @@ +# git-cliff configuration for changelog generation +# See: https://git-cliff.org/docs/configuration + +[changelog] +header = "" +body = """ +{% for group, commits in commits | group_by(attribute="group") %} +### {{ group | upper_first }} +{% for commit in commits %} + - {{ commit.message | split(pat="\n") | first | trim }} +{% endfor %} +{% endfor %} +""" +trim = true + +[git] +conventional_commits = true +filter_unconventional = true +commit_parsers = [ + { message = "^feat", group = "Features" }, + { message = "^fix", group = "Bug Fixes" }, + { message = "^perf", group = "Performance" }, + { message = "^refactor", group = "Refactoring" }, + { message = "^doc", group = "Documentation" }, + { message = "^test", group = "Testing" }, + { message = "^chore", skip = true }, + { message = "^ci", skip = true }, + { message = "^build", skip = true }, +] +sort_commits = "newest" diff --git a/.github/workflows/rust-publish.yml b/.github/workflows/rust-publish.yml index a65bfdfd..8d4dc611 100644 --- a/.github/workflows/rust-publish.yml +++ b/.github/workflows/rust-publish.yml @@ -1,132 +1,107 @@ -name: Publish kora-rpc +name: Publish Rust Crates on: workflow_dispatch: - inputs: - version_bump: - description: 'Version bump type' - required: false - default: 'auto' - type: choice - options: - - auto - - patch - - minor - - major + +permissions: + contents: write + +concurrency: + group: release-rust + cancel-in-progress: false jobs: - publish-kora-rpc: - name: Publish kora-rpc + publish: + name: Publish rust crates runs-on: ubuntu-latest - # Skip if commit message contains version bump to avoid infinite loops - if: "!contains(github.event.head_commit.message, 'chore:')" steps: - uses: actions/checkout@v4 with: token: ${{ secrets.GITHUB_TOKEN }} fetch-depth: 0 - - name: Configure Git - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable with: components: rustfmt, clippy - + - name: Build check - working-directory: crates/rpc - run: cargo build - - - name: Determine version bump from commits - id: version_bump + run: cargo build --workspace + + - name: Get current versions + id: version run: | - echo "๐Ÿ“ Analyzing commits for kora-rpc..." - - # Get last release tag for this crate - LAST_TAG=$(git tag -l "kora-rpc-v*" --sort=-version:refname | head -1) - + # Get kora-lib version + LIB_VERSION=$(grep "^version" crates/lib/Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/') + echo "lib_version=$LIB_VERSION" >> $GITHUB_OUTPUT + echo "๐Ÿ“ฆ kora-lib v$LIB_VERSION" + + # Get kora-cli version + CLI_VERSION=$(grep "^version" crates/cli/Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/') + echo "cli_version=$CLI_VERSION" >> $GITHUB_OUTPUT + echo "๐Ÿ“ฆ kora-cli v$CLI_VERSION" + + # Get last release tag for changelog range + LAST_TAG=$(git tag -l "v*" --sort=-version:refname | head -1) if [ -z "$LAST_TAG" ]; then - echo "No previous release found" - COMMITS=$(git log --oneline --pretty=format:"- %s" -- crates/rpc/ | head -10) + echo "tag_range=HEAD" >> $GITHUB_OUTPUT else - echo "Last release: $LAST_TAG" - COMMITS=$(git log --oneline "$LAST_TAG"..HEAD --pretty=format:"- %s" -- crates/rpc/) + echo "tag_range=$LAST_TAG..HEAD" >> $GITHUB_OUTPUT fi - - # Use manual input if provided, otherwise auto-detect - if [ "${{ github.event.inputs.version_bump }}" != "auto" ]; then - echo "Using manual version bump: ${{ github.event.inputs.version_bump }}" - echo "bump_type=${{ github.event.inputs.version_bump }}" >> $GITHUB_OUTPUT - else - echo "Auto-detecting version bump from commits" - # Determine bump type from conventional commits - if echo "$COMMITS" | grep -q "BREAKING CHANGE\|!:"; then - echo "bump_type=major" >> $GITHUB_OUTPUT - elif echo "$COMMITS" | grep -q "^feat"; then - echo "bump_type=minor" >> $GITHUB_OUTPUT - else - echo "bump_type=patch" >> $GITHUB_OUTPUT - fi - fi - - echo "commits<> $GITHUB_OUTPUT - echo "$COMMITS" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - - name: Bump version and publish - working-directory: crates/rpc + - name: Generate changelog + id: changelog + uses: orhun/git-cliff-action@v4 + with: + config: .github/cliff.toml + args: ${{ steps.version.outputs.tag_range }} --strip all + + - name: Create git tags run: | - echo "๐Ÿš€ Publishing kora-rpc with ${{ steps.version_bump.outputs.bump_type }} version bump" - - # Get current version - CURRENT_VERSION=$(grep "^version" Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/') - echo "Current version: $CURRENT_VERSION" - - # Bump version manually - case "${{ steps.version_bump.outputs.bump_type }}" in - major) - NEW_VERSION=$(echo $CURRENT_VERSION | awk -F. '{print ($1+1)".0.0"}') - ;; - minor) - NEW_VERSION=$(echo $CURRENT_VERSION | awk -F. '{print $1"."%($2+1)".0"}') - ;; - patch) - NEW_VERSION=$(echo $CURRENT_VERSION | awk -F. '{print $1"."$2"."($3+1)}') - ;; - esac - - echo "New version: $NEW_VERSION" - - # Update Cargo.toml - sed -i "s/^version = \".*\"/version = \"$NEW_VERSION\"/" Cargo.toml - - # Commit version bump - git add Cargo.toml - git commit -m "chore(kora-rpc): release v$NEW_VERSION" - git tag "kora-rpc-v$NEW_VERSION" - git push origin main "kora-rpc-v$NEW_VERSION" - - # Publish to crates.io - cargo publish --locked --token ${{ secrets.KORA_RPC_REGISTRY_TOKEN }} - - echo "version=$NEW_VERSION" >> $GITHUB_ENV + git tag "v${{ steps.version.outputs.lib_version }}" + git tag "kora-lib-v${{ steps.version.outputs.lib_version }}" + git tag "kora-cli-v${{ steps.version.outputs.cli_version }}" + git push origin --tags + + - name: Publish kora-lib to crates.io + working-directory: crates/lib + run: cargo publish --locked env: - CARGO_REGISTRY_TOKEN: ${{ secrets.KORA_RPC_REGISTRY_TOKEN }} + CARGO_REGISTRY_TOKEN: ${{ secrets.KORA_CLI_REGISTRY_TOKEN }} + + - name: Publish kora-cli to crates.io + working-directory: crates/cli + run: cargo publish --locked + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.KORA_CLI_REGISTRY_TOKEN }} - name: Create GitHub Release if: success() uses: softprops/action-gh-release@v1 with: - tag_name: kora-rpc-v${{ env.version }} - name: kora-rpc v${{ env.version }} + tag_name: v${{ steps.version.outputs.lib_version }} + name: Kora v${{ steps.version.outputs.lib_version }} body: | - ## Changes in kora-rpc v${{ env.version }} - - ${{ steps.version_bump.outputs.commits }} + ## Kora Release v${{ steps.version.outputs.lib_version }} + + ### Published Crates + - `kora-lib` v${{ steps.version.outputs.lib_version }} + - `kora-cli` v${{ steps.version.outputs.cli_version }} + + ### Changes + + ${{ steps.changelog.outputs.content }} + + ### Installation + + ```bash + # Install CLI + cargo install kora-cli + + # Or use in your Rust project + cargo add kora-lib + ``` draft: false prerelease: false env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ts-sdk-publish.yml b/.github/workflows/ts-sdk-publish.yml index a92d3e90..6905e449 100644 --- a/.github/workflows/ts-sdk-publish.yml +++ b/.github/workflows/ts-sdk-publish.yml @@ -1,11 +1,11 @@ name: Publish SDKs on: - push: - branches: - - main - paths: - - 'sdks/**' + workflow_dispatch: + +permissions: + contents: write + pull-requests: write # Prevent concurrent releases concurrency: @@ -25,8 +25,6 @@ jobs: name: Publish SDKs runs-on: ubuntu-latest needs: [typescript-integration, typescript-unit] - # Only run if there are changes in changeset files or package files - if: contains(github.event.head_commit.message, 'changeset') defaults: run: @@ -78,7 +76,7 @@ jobs: publish: pnpm changeset:publish commit: 'chore: release ts sdk packages' title: 'chore: release ts sdk packages' - createGithubReleases: false + createGithubReleases: true env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}