From 118ee805faf77be41adfdd65313ccafc3b278407 Mon Sep 17 00:00:00 2001 From: Jo D Date: Thu, 9 Oct 2025 15:13:04 -0400 Subject: [PATCH] feat: (PRO-407) Use solana-signers crate instead of custom Kora Signer --- 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]