From 500f760ff7c42918d6e76d504640e13b4f549fb0 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Mon, 19 May 2025 15:08:01 +0200 Subject: [PATCH 01/21] XC-317: Add client method to sign a transaction --- Cargo.lock | 7 +- end_to_end_tests/Cargo.toml | 1 + end_to_end_tests/tests/end_to_end.rs | 1 + examples/basic_solana/src/ed25519.rs | 50 +-------- examples/basic_solana/src/lib.rs | 26 +---- examples/basic_solana/src/main.rs | 13 +-- examples/basic_solana/src/solana_wallet.rs | 23 ++-- examples/basic_solana/src/state.rs | 12 +-- examples/basic_solana/tests/tests.rs | 8 +- libs/client/Cargo.toml | 6 +- libs/client/src/lib.rs | 118 ++++++++++++++++++++- libs/types/src/lib.rs | 11 +- libs/types/src/solana/request/mod.rs | 65 +++++++++++- 13 files changed, 237 insertions(+), 104 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 72708aca..d6bb05b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4462,10 +4462,14 @@ dependencies = [ "sol_rpc_types", "solana-account", "solana-account-decoder-client-types", - "solana-clock", + "solana-hash", "solana-instruction", + "solana-keypair", + "solana-message", + "solana-program", "solana-pubkey", "solana-signature", + "solana-transaction", "solana-transaction-error", "solana-transaction-status-client-types", "strum 0.27.1", @@ -4489,6 +4493,7 @@ dependencies = [ "solana-compute-budget-interface", "solana-hash", "solana-keypair", + "solana-message", "solana-program", "solana-pubkey", "solana-signature", diff --git a/end_to_end_tests/Cargo.toml b/end_to_end_tests/Cargo.toml index 33ec86db..c2b3db07 100644 --- a/end_to_end_tests/Cargo.toml +++ b/end_to_end_tests/Cargo.toml @@ -19,6 +19,7 @@ sol_rpc_types = { path = "../libs/types" } sol_rpc_int_tests = { path = "../integration_tests" } solana-commitment-config = { workspace = true } solana-hash = { workspace = true } +solana-message = { workspace = true } solana-pubkey = { workspace = true } solana-signature = { workspace = true } solana-signer = { workspace = true } diff --git a/end_to_end_tests/tests/end_to_end.rs b/end_to_end_tests/tests/end_to_end.rs index 513cb405..a91a07ea 100644 --- a/end_to_end_tests/tests/end_to_end.rs +++ b/end_to_end_tests/tests/end_to_end.rs @@ -71,6 +71,7 @@ async fn should_send_transaction() { .expect("Block not found"); let blockhash = Hash::from_str(&block.blockhash).expect("Failed to parse blockhash"); + // TODO XC-317: Use tEDdSA let transaction = Transaction::new_signed_with_payer( &[set_cu_limit_ix, add_priority_fee_ix, transfer_ix], Some(&sender.pubkey()), diff --git a/examples/basic_solana/src/ed25519.rs b/examples/basic_solana/src/ed25519.rs index 096da98e..7cbe65af 100644 --- a/examples/basic_solana/src/ed25519.rs +++ b/examples/basic_solana/src/ed25519.rs @@ -1,25 +1,8 @@ -use crate::Ed25519KeyName; use ic_cdk::api::management_canister::schnorr::{ SchnorrAlgorithm, SchnorrKeyId, SchnorrPublicKeyArgument, SchnorrPublicKeyResponse, - SignWithSchnorrArgument, SignWithSchnorrResponse, }; use ic_ed25519::PublicKey; - -#[derive(Clone, Debug, PartialEq, Eq, Default)] -pub struct DerivationPath(Vec>); - -impl From<&[u8]> for DerivationPath { - fn from(bytes: &[u8]) -> Self { - const SCHEMA_V1: u8 = 1; - Self([vec![SCHEMA_V1], bytes.to_vec()].into_iter().collect()) - } -} - -impl From<&DerivationPath> for Vec> { - fn from(derivation_path: &DerivationPath) -> Self { - derivation_path.0.clone() - } -} +use sol_rpc_types::{DerivationPath, Ed25519KeyId}; #[derive(Clone, Debug, PartialEq, Eq)] pub struct Ed25519ExtendedPublicKey { @@ -28,7 +11,7 @@ pub struct Ed25519ExtendedPublicKey { } impl Ed25519ExtendedPublicKey { - pub fn derive_public_key(&self, derivation_path: &DerivationPath) -> Ed25519ExtendedPublicKey { + pub fn derive_public_key(&self, derivation_path: DerivationPath) -> Ed25519ExtendedPublicKey { let derivation_path = ic_ed25519::DerivationPath::new( >>::from(derivation_path) .into_iter() @@ -55,13 +38,13 @@ impl From for Ed25519ExtendedPublicKey { } pub async fn get_ed25519_public_key( - key_name: &Ed25519KeyName, + key_name: &Ed25519KeyId, derivation_path: &DerivationPath, ) -> Ed25519ExtendedPublicKey { let (response,): (SchnorrPublicKeyResponse,) = ic_cdk::api::management_canister::schnorr::schnorr_public_key(SchnorrPublicKeyArgument { canister_id: None, - derivation_path: derivation_path.into(), + derivation_path: derivation_path.clone().into(), key_id: SchnorrKeyId { algorithm: SchnorrAlgorithm::Ed25519, name: key_name.to_string(), @@ -76,28 +59,3 @@ pub async fn get_ed25519_public_key( }); Ed25519ExtendedPublicKey::from(response) } - -pub async fn sign_with_ed25519( - message: Vec, - derivation_path: &DerivationPath, - key_name: &Ed25519KeyName, -) -> [u8; 64] { - let (response,): (SignWithSchnorrResponse,) = - ic_cdk::api::management_canister::schnorr::sign_with_schnorr(SignWithSchnorrArgument { - message, - derivation_path: derivation_path.into(), - key_id: SchnorrKeyId { - algorithm: SchnorrAlgorithm::Ed25519, - name: key_name.to_string(), - }, - }) - .await - .expect("failed to sign with ed25519"); - let signature_length = response.signature.len(); - <[u8; 64]>::try_from(response.signature).unwrap_or_else(|_| { - panic!( - "BUG: invalid signature from management canister. Expected 64 bytes but got {} bytes", - signature_length - ) - }) -} diff --git a/examples/basic_solana/src/lib.rs b/examples/basic_solana/src/lib.rs index 43c77148..82bcf14b 100644 --- a/examples/basic_solana/src/lib.rs +++ b/examples/basic_solana/src/lib.rs @@ -6,9 +6,9 @@ pub mod state; use crate::state::{read_state, State}; use candid::{CandidType, Deserialize, Principal}; use sol_rpc_client::{IcRuntime, SolRpcClient}; -use sol_rpc_types::{CommitmentLevel, MultiRpcResult, RpcSources, SolanaCluster}; +use sol_rpc_types::{CommitmentLevel, Ed25519KeyId, MultiRpcResult, RpcSources, SolanaCluster}; use solana_hash::Hash; -use std::{fmt::Display, str::FromStr}; +use std::str::FromStr; // Fetch a recent blockhash using the Solana `getSlot` and `getBlock` methods. // Since the `getSlot` method might fail due to Solana's fast blocktime, and some slots do not @@ -68,7 +68,7 @@ pub fn client() -> SolRpcClient { pub struct InitArg { pub sol_rpc_canister_id: Option, pub solana_network: Option, - pub ed25519_key_name: Option, + pub ed25519_key_id: Option, pub solana_commitment_level: Option, } @@ -90,26 +90,6 @@ impl From for SolanaCluster { } } -#[derive(CandidType, Deserialize, Debug, Default, PartialEq, Eq, Clone, Copy)] -pub enum Ed25519KeyName { - #[default] - TestKeyLocalDevelopment, - TestKey1, - ProductionKey1, -} - -impl Display for Ed25519KeyName { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let str = match self { - Ed25519KeyName::TestKeyLocalDevelopment => "dfx_test_key", - Ed25519KeyName::TestKey1 => "test_key_1", - Ed25519KeyName::ProductionKey1 => "key_1", - } - .to_string(); - write!(f, "{}", str) - } -} - pub fn validate_caller_not_anonymous() -> Principal { let principal = ic_cdk::caller(); if principal == Principal::anonymous() { diff --git a/examples/basic_solana/src/main.rs b/examples/basic_solana/src/main.rs index 712f5890..deff0b54 100644 --- a/examples/basic_solana/src/main.rs +++ b/examples/basic_solana/src/main.rs @@ -156,9 +156,10 @@ pub async fn create_nonce_account(owner: Option) -> String { ); let signatures = vec![ - wallet.sign_with_ed25519(&message, &payer).await, - wallet.sign_with_ed25519(&message, &nonce_account).await, + SolanaWallet::sign_message(&message, &payer).await, + SolanaWallet::sign_message(&message, &nonce_account).await, ]; + let transaction = Transaction { message, signatures, @@ -212,7 +213,7 @@ pub async fn create_associated_token_account( &get_recent_blockhash(&client).await, ); - let signatures = vec![wallet.sign_with_ed25519(&message, &payer).await]; + let signatures = vec![SolanaWallet::sign_message(&message, &payer).await]; let transaction = Transaction { message, signatures, @@ -251,7 +252,7 @@ pub async fn send_sol(owner: Option, to: String, amount: Nat) -> Stri Some(payer.as_ref()), &get_recent_blockhash(&client).await, ); - let signatures = vec![wallet.sign_with_ed25519(&message, &payer).await]; + let signatures = vec![SolanaWallet::sign_message(&message, &payer).await]; let transaction = Transaction { message, signatures, @@ -290,7 +291,7 @@ pub async fn send_sol_with_durable_nonce( let blockhash = Hash::from(get_nonce(Some(nonce_account.as_ref().into())).await); let message = Message::new_with_blockhash(instructions, Some(payer.as_ref()), &blockhash); - let signatures = vec![wallet.sign_with_ed25519(&message, &payer).await]; + let signatures = vec![SolanaWallet::sign_message(&message, &payer).await]; let transaction = Transaction { message, signatures, @@ -332,7 +333,7 @@ pub async fn send_spl_token( Some(payer.as_ref()), &get_recent_blockhash(&client).await, ); - let signatures = vec![wallet.sign_with_ed25519(&message, &payer).await]; + let signatures = vec![SolanaWallet::sign_message(&message, &payer).await]; let transaction = Transaction { message, signatures, diff --git a/examples/basic_solana/src/solana_wallet.rs b/examples/basic_solana/src/solana_wallet.rs index 5bb86735..5e41ef4c 100644 --- a/examples/basic_solana/src/solana_wallet.rs +++ b/examples/basic_solana/src/solana_wallet.rs @@ -5,13 +5,16 @@ //! such as error handling, access-control, caching, etc. use crate::{ - ed25519::{sign_with_ed25519, DerivationPath, Ed25519ExtendedPublicKey}, + ed25519::Ed25519ExtendedPublicKey, state::{lazy_call_ed25519_public_key, read_state}, }; use candid::Principal; +use sol_rpc_client::{sign_transaction, IcRuntime}; +use sol_rpc_types::{DerivationPath, SignTransactionRequestParams}; use solana_message::Message; use solana_pubkey::Pubkey; use solana_signature::Signature; +use solana_transaction::Transaction; use std::fmt::Display; #[derive(Clone)] @@ -26,7 +29,7 @@ impl SolanaAccount { derivation_path: DerivationPath, ) -> Self { let ed25519_public_key = root_public_key - .derive_public_key(&derivation_path) + .derive_public_key(derivation_path.clone()) .public_key .serialize_raw() .into(); @@ -85,10 +88,16 @@ impl SolanaWallet { ) } - pub async fn sign_with_ed25519(&self, message: &Message, signer: &SolanaAccount) -> Signature { - let message = message.serialize(); - let derivation_path = &signer.derivation_path; - let key_name = &read_state(|s| s.ed25519_key_name()); - Signature::from(sign_with_ed25519(message, derivation_path, key_name).await) + pub async fn sign_message(message: &Message, signer: &SolanaAccount) -> Signature { + sign_transaction( + &IcRuntime, + SignTransactionRequestParams { + transaction: Transaction::new_unsigned(message.clone()), + key_id: read_state(|s| s.ed25519_key_id()), + derivation_path: Some(signer.derivation_path.clone()), + }, + ) + .await + .expect("Failed to sign transaction") } } diff --git a/examples/basic_solana/src/state.rs b/examples/basic_solana/src/state.rs index f15753c7..69320328 100644 --- a/examples/basic_solana/src/state.rs +++ b/examples/basic_solana/src/state.rs @@ -1,9 +1,9 @@ use crate::{ ed25519::{get_ed25519_public_key, Ed25519ExtendedPublicKey}, - Ed25519KeyName, InitArg, SolanaNetwork, + InitArg, SolanaNetwork, }; use candid::Principal; -use sol_rpc_types::CommitmentLevel; +use sol_rpc_types::{CommitmentLevel, Ed25519KeyId}; use std::{ cell::RefCell, ops::{Deref, DerefMut}, @@ -34,11 +34,11 @@ pub struct State { solana_network: SolanaNetwork, solana_commitment_level: CommitmentLevel, ed25519_public_key: Option, - ed25519_key_name: Ed25519KeyName, + ed25519_key_name: Ed25519KeyId, } impl State { - pub fn ed25519_key_name(&self) -> Ed25519KeyName { + pub fn ed25519_key_id(&self) -> Ed25519KeyId { self.ed25519_key_name } @@ -62,7 +62,7 @@ impl From for State { solana_network: init_arg.solana_network.unwrap_or_default(), solana_commitment_level: init_arg.solana_commitment_level.unwrap_or_default(), ed25519_public_key: None, - ed25519_key_name: init_arg.ed25519_key_name.unwrap_or_default(), + ed25519_key_name: init_arg.ed25519_key_id.unwrap_or_default(), } } } @@ -72,7 +72,7 @@ pub async fn lazy_call_ed25519_public_key() -> Ed25519ExtendedPublicKey { return public_key; } let public_key = - get_ed25519_public_key(&read_state(|s| s.ed25519_key_name()), &Default::default()).await; + get_ed25519_public_key(&read_state(|s| s.ed25519_key_id()), &Default::default()).await; mutate_state(|s| s.ed25519_public_key = Some(public_key.clone())); public_key } diff --git a/examples/basic_solana/tests/tests.rs b/examples/basic_solana/tests/tests.rs index 4b1d12c3..b1b4a73e 100644 --- a/examples/basic_solana/tests/tests.rs +++ b/examples/basic_solana/tests/tests.rs @@ -1,4 +1,4 @@ -use basic_solana::{Ed25519KeyName, SolanaNetwork}; +use basic_solana::SolanaNetwork; use candid::{ decode_args, encode_args, utils::ArgumentEncoder, CandidType, Encode, Nat, Principal, }; @@ -8,8 +8,8 @@ use pocket_ic::{ }; use serde::de::DeserializeOwned; use sol_rpc_types::{ - CommitmentLevel, OverrideProvider, RegexSubstitution, RpcAccess, SupportedRpcProvider, - SupportedRpcProviderId, TokenAmount, + CommitmentLevel, Ed25519KeyId, OverrideProvider, RegexSubstitution, RpcAccess, + SupportedRpcProvider, SupportedRpcProviderId, TokenAmount, }; use solana_client::{rpc_client::RpcClient as SolanaRpcClient, rpc_config::RpcTransactionConfig}; use solana_commitment_config::CommitmentConfig; @@ -292,7 +292,7 @@ impl Setup { let basic_solana_install_args = basic_solana::InitArg { sol_rpc_canister_id: Some(sol_rpc_canister_id), solana_network: Some(SolanaNetwork::Devnet), - ed25519_key_name: Some(Ed25519KeyName::ProductionKey1), + ed25519_key_id: Some(Ed25519KeyId::ProductionKey1), solana_commitment_level: Some(CommitmentLevel::Confirmed), }; env.install_canister( diff --git a/libs/client/Cargo.toml b/libs/client/Cargo.toml index 909bd8aa..9a85e78e 100644 --- a/libs/client/Cargo.toml +++ b/libs/client/Cargo.toml @@ -19,10 +19,14 @@ serde = { workspace = true } serde_json = { workspace = true } solana-account = { workspace = true } solana-account-decoder-client-types = { workspace = true } -solana-clock = { workspace = true } +solana-hash = { workspace = true } solana-instruction = { workspace = true } +solana-keypair = { workspace = true } +solana-message = { workspace = true } +solana-program = { workspace = true } solana-pubkey = { workspace = true } solana-signature = { workspace = true } +solana-transaction = { workspace = true } solana-transaction-error = { workspace = true } solana-transaction-status-client-types = { workspace = true } sol_rpc_types = { version = "0.1.0", path = "../types" } diff --git a/libs/client/src/lib.rs b/libs/client/src/lib.rs index 42117a72..e03fa98c 100644 --- a/libs/client/src/lib.rs +++ b/libs/client/src/lib.rs @@ -130,20 +130,29 @@ use crate::request::{ }; use async_trait::async_trait; use candid::{utils::ArgumentEncoder, CandidType, Principal}; -use ic_cdk::api::call::RejectionCode; +use ic_cdk::api::{ + call::RejectionCode, + management_canister::schnorr::{ + SchnorrAlgorithm, SchnorrKeyId, SignWithSchnorrArgument, SignWithSchnorrResponse, + }, +}; pub use request::{Request, RequestBuilder, SolRpcEndpoint, SolRpcRequest}; use serde::de::DeserializeOwned; use sol_rpc_types::{ CommitmentLevel, GetAccountInfoParams, GetBalanceParams, GetBlockParams, GetRecentPrioritizationFeesParams, GetSignatureStatusesParams, GetSlotParams, GetSlotRpcConfig, - GetTokenAccountBalanceParams, GetTransactionParams, Lamport, Pubkey, RpcConfig, RpcResult, - RpcSources, SendTransactionParams, Signature, Slot, SolanaCluster, SupportedRpcProvider, - SupportedRpcProviderId, TokenAmount, TransactionDetails, TransactionInfo, + GetTokenAccountBalanceParams, GetTransactionParams, Lamport, Pubkey, RpcConfig, RpcError, + RpcResult, RpcSources, SendTransactionParams, SignTransactionRequestParams, Signature, Slot, + SolanaCluster, SupportedRpcProvider, SupportedRpcProviderId, TokenAmount, TransactionDetails, + TransactionInfo, }; use solana_account_decoder_client_types::token::UiTokenAmount; use solana_transaction_status_client_types::EncodedConfirmedTransactionWithStatusMeta; use std::{fmt::Debug, sync::Arc}; +// Source: https://internetcomputer.org/docs/current/references/t-sigs-how-it-works/#fees-for-the-t-schnorr-production-key +const SIGN_WITH_SCHNORR_FEE: u128 = 26_153_846_153; + /// The principal identifying the productive Solana RPC canister under NNS control. /// /// ```rust @@ -203,6 +212,11 @@ impl SolRpcClient { pub fn builder(runtime: R, sol_rpc_canister: Principal) -> ClientBuilder { ClientBuilder::new(runtime, sol_rpc_canister) } + + /// Returns a reference to the client's runtime. + pub fn runtime(&self) -> &R { + &self.config.runtime + } } impl SolRpcClient { @@ -908,6 +922,102 @@ impl SolRpcClient { } } +/// Sign an unsigned Solana transaction with threshold EdDSA, see threshold Schnorr documentation +/// [here](https://internetcomputer.org/docs/building-apps/network-features/signatures/t-schnorr). +/// +/// # Examples +/// +/// ```rust +/// use solana_hash::Hash; +/// use solana_message::legacy::Message; +/// use solana_program::system_instruction::transfer; +/// use solana_pubkey::pubkey; +/// use solana_transaction::Transaction; +/// use sol_rpc_client::{IcRuntime, sign_transaction, SolRpcClient}; +/// use sol_rpc_types::{DerivationPath, Ed25519KeyId, SignTransactionRequestParams}; +/// +/// # #[tokio::main] +/// # async fn main() -> Result<(), Box> { +/// # use sol_rpc_client::fixtures::MockRuntime; +/// # use std::str::FromStr; +/// use candid::Principal; +/// # use ic_cdk::api::management_canister::schnorr::SignWithSchnorrResponse; +/// let runtime = IcRuntime; +/// # let runtime = MockRuntime::same_response(SignWithSchnorrResponse { +/// # signature: "ityU6OGhNgvUXCL8gOy9p0LNThE8eKn4LUPNFwpeQVyXiUmNOzohl0VkcwEQnTqg".to_string().into_bytes(), +/// # }); +/// +/// // TODO XC-317: Use pubkey that is actually derived from the given derivation path +/// let key_id = Ed25519KeyId::TestKey1; +/// let derivation_path = Some(DerivationPath::from("un4fu-tqaaa-aaaab-qadjq-cai".as_bytes())); +/// // This pubkey should be derived from the root key `key_id` with `derivation_path`, see: +/// // https://internetcomputer.org/docs/references/ic-interface-spec#ic-schnorr_public_key +/// let payer = pubkey!("3EdRSc7CnKUGxGUSZwJ58rd7haBM8CR2Xh87KheEX7iS"); +/// +/// let recipient = pubkey!("BPebStjcgCPnWTK3FXZJ8KhqwNYLk9aubC9b4Cgqb6oE"); +/// +/// // TODO XC-317: Use client method to fetch recent blockhash +/// let recent_blockhash = Hash::new_unique(); +/// +/// let message = Message::new_with_blockhash( +/// &[transfer(&payer, &recipient, 1_000_000)], +/// Some(&payer), +/// &recent_blockhash, +/// ); +/// +/// let mut transaction = Transaction::new_unsigned(message); +/// let signature = sign_transaction( +/// &runtime, +/// SignTransactionRequestParams { +/// transaction, +/// derivation_path, +/// key_id, +/// }, +/// ).await; +/// +/// assert_eq!( +/// signature, +/// Ok(solana_signature::Signature::from_str("").unwrap()) +/// ); +/// +/// // The transaction is now signed and can be submitted with the `sendTransaction` RPC method. +/// transaction.signatures = vec![signature.unwrap()]; +/// # Ok(()) +/// # } +/// ``` +pub async fn sign_transaction( + runtime: &R, + params: SignTransactionRequestParams, +) -> RpcResult { + let arg = SignWithSchnorrArgument { + message: params.transaction.message_data(), + derivation_path: params.derivation_path.unwrap_or_default().into(), + key_id: SchnorrKeyId { + algorithm: SchnorrAlgorithm::Ed25519, + name: params.key_id.to_string(), + }, + }; + let (response,): (SignWithSchnorrResponse,) = R::update_call( + runtime, + Principal::management_canister(), + "sign_with_schnorr", + (arg,), + SIGN_WITH_SCHNORR_FEE, + ) + .await + .map_err(|(rejection_code, message)| { + RpcError::ValidationError(format!( + "Failed to sign transaction, management canister returned code {rejection_code:?}: {message}") + ) + })?; + solana_signature::Signature::try_from(response.signature).map_err(|bytes| { + RpcError::ValidationError(format!( + "Expected signature to contain 64 bytes, got {} bytes", + bytes.len() + )) + }) +} + /// Runtime when interacting with a canister running on the Internet Computer. #[derive(Copy, Clone, Eq, PartialEq, Debug)] pub struct IcRuntime; diff --git a/libs/types/src/lib.rs b/libs/types/src/lib.rs index d1c6bb45..d6e4446e 100644 --- a/libs/types/src/lib.rs +++ b/libs/types/src/lib.rs @@ -25,11 +25,12 @@ use serde::Serialize; pub use solana::{ account::{AccountData, AccountEncoding, AccountInfo, ParsedAccount}, request::{ - CommitmentLevel, DataSlice, GetAccountInfoEncoding, GetAccountInfoParams, GetBalanceParams, - GetBlockCommitmentLevel, GetBlockParams, GetRecentPrioritizationFeesParams, - GetSignatureStatusesParams, GetSlotParams, GetTokenAccountBalanceParams, - GetTransactionEncoding, GetTransactionParams, SendTransactionEncoding, - SendTransactionParams, TransactionDetails, + CommitmentLevel, DataSlice, DerivationPath, Ed25519KeyId, GetAccountInfoEncoding, + GetAccountInfoParams, GetBalanceParams, GetBlockCommitmentLevel, GetBlockParams, + GetRecentPrioritizationFeesParams, GetSignatureStatusesParams, GetSlotParams, + GetTokenAccountBalanceParams, GetTransactionEncoding, GetTransactionParams, + SendTransactionEncoding, SendTransactionParams, SignTransactionRequestParams, + TransactionDetails, }, transaction::{ error::{InstructionError, TransactionError}, diff --git a/libs/types/src/solana/request/mod.rs b/libs/types/src/solana/request/mod.rs index edac1e3e..731fdf6d 100644 --- a/libs/types/src/solana/request/mod.rs +++ b/libs/types/src/solana/request/mod.rs @@ -3,8 +3,71 @@ mod tests; use crate::{solana::Pubkey, RpcError, Signature, Slot, VecWithMaxLen}; use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine}; -use candid::{CandidType, Deserialize}; +use candid::{CandidType, Deserialize, Principal}; use serde::Serialize; +use std::fmt::Display; + +/// Represents the derivation path of an Ed25519 key from one of the root keys. +/// See the [tEdDSA documentation](https://internetcomputer.org/docs/building-apps/network-features/signatures/t-schnorr#signing-messages-and-transactions) +/// for more details. +#[derive(Clone, Debug, PartialEq, Eq, Default)] +pub struct DerivationPath(Vec>); + +impl From<&[u8]> for DerivationPath { + fn from(bytes: &[u8]) -> Self { + const SCHEMA_V1: u8 = 1; + Self([vec![SCHEMA_V1], bytes.to_vec()].into_iter().collect()) + } +} + +impl From for DerivationPath { + fn from(principal: Principal) -> Self { + DerivationPath::from(principal.as_slice()) + } +} + +impl From for Vec> { + fn from(derivation_path: DerivationPath) -> Self { + derivation_path.0 + } +} + +/// The ID of one of the ICP root keys. +/// See the [tEdDSA documentation](https://internetcomputer.org/docs/building-apps/network-features/signatures/t-schnorr#signing-messages-and-transactions) +/// for more details. +#[derive(CandidType, Deserialize, Debug, Default, PartialEq, Eq, Clone, Copy)] +pub enum Ed25519KeyId { + /// Only available on the local development environment started by dfx. + #[default] + TestKeyLocalDevelopment, + /// Test key available on the ICP mainnet. + TestKey1, + /// Production key available on the ICP mainnet. + ProductionKey1, +} + +impl Display for Ed25519KeyId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str = match self { + Ed25519KeyId::TestKeyLocalDevelopment => "dfx_test_key", + Ed25519KeyId::TestKey1 => "test_key_1", + Ed25519KeyId::ProductionKey1 => "key_1", + } + .to_string(); + write!(f, "{}", str) + } +} + +/// The parameters for a request to sign the given Solana transaction with [tEdDSA](https://internetcomputer.org/docs/building-apps/network-features/signatures/t-schnorr). +pub struct SignTransactionRequestParams { + /// The transaction to sign. + pub transaction: solana_transaction::Transaction, + /// The root key from which the key used to sign the transaction is derived. + pub key_id: Ed25519KeyId, + /// The derivation path used to derive the key used to sign the transaction. + // TODO XC-317: Make this parameter optional? Is this allowed? + pub derivation_path: Option, +} /// The parameters for a Solana [`getAccountInfo`](https://solana.com/docs/rpc/http/getaccountinfo) RPC method call. #[derive(Debug, Clone, Deserialize, Serialize, CandidType)] From 814df4786dbd093905f83e61dc20bef2f4c3be2c Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Mon, 19 May 2025 15:19:06 +0200 Subject: [PATCH 02/21] XC-317: Fix doctest --- libs/client/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/client/src/lib.rs b/libs/client/src/lib.rs index e03fa98c..5670431c 100644 --- a/libs/client/src/lib.rs +++ b/libs/client/src/lib.rs @@ -949,7 +949,7 @@ impl SolRpcClient { /// /// // TODO XC-317: Use pubkey that is actually derived from the given derivation path /// let key_id = Ed25519KeyId::TestKey1; -/// let derivation_path = Some(DerivationPath::from("un4fu-tqaaa-aaaab-qadjq-cai".as_bytes())); +/// let derivation_path = None; /// // This pubkey should be derived from the root key `key_id` with `derivation_path`, see: /// // https://internetcomputer.org/docs/references/ic-interface-spec#ic-schnorr_public_key /// let payer = pubkey!("3EdRSc7CnKUGxGUSZwJ58rd7haBM8CR2Xh87KheEX7iS"); @@ -969,7 +969,7 @@ impl SolRpcClient { /// let signature = sign_transaction( /// &runtime, /// SignTransactionRequestParams { -/// transaction, +/// transaction: transaction.clone(), /// derivation_path, /// key_id, /// }, From 879db3b5daeba64bb6eddae9482cad47d000196f Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Mon, 19 May 2025 15:45:42 +0200 Subject: [PATCH 03/21] XC-317: Fix basic_solana Candid and doctest --- examples/basic_solana/basic_solana.did | 4 ++-- libs/client/src/lib.rs | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/examples/basic_solana/basic_solana.did b/examples/basic_solana/basic_solana.did index bd00af70..6d866c23 100644 --- a/examples/basic_solana/basic_solana.did +++ b/examples/basic_solana/basic_solana.did @@ -7,7 +7,7 @@ type InitArg = record { solana_commitment_level : opt CommitmentLevel; // EdDSA keys will be derived from this key. // If not specified, the value is set to `TestKeyLocalDevelopment`. - ed25519_key_name : opt Ed25519KeyName; + ed25519_key_name : opt Ed25519KeyId; // The canister will interact with this SOL RPC canister. // If not specified, the value is set to `tghme-zyaaa-aaaar-qarca-cai`. sol_rpc_canister_id : opt principal; @@ -30,7 +30,7 @@ type CommitmentLevel = variant { finalized; }; -type Ed25519KeyName = variant { +type Ed25519KeyId = variant { // For local development with `dfx`. TestKeyLocalDevelopment; // For testing with the Internet Computer's test key. diff --git a/libs/client/src/lib.rs b/libs/client/src/lib.rs index 5670431c..3e146af8 100644 --- a/libs/client/src/lib.rs +++ b/libs/client/src/lib.rs @@ -932,6 +932,7 @@ impl SolRpcClient { /// use solana_message::legacy::Message; /// use solana_program::system_instruction::transfer; /// use solana_pubkey::pubkey; +/// use solana_signature::Signature; /// use solana_transaction::Transaction; /// use sol_rpc_client::{IcRuntime, sign_transaction, SolRpcClient}; /// use sol_rpc_types::{DerivationPath, Ed25519KeyId, SignTransactionRequestParams}; @@ -943,9 +944,9 @@ impl SolRpcClient { /// use candid::Principal; /// # use ic_cdk::api::management_canister::schnorr::SignWithSchnorrResponse; /// let runtime = IcRuntime; -/// # let runtime = MockRuntime::same_response(SignWithSchnorrResponse { +/// # let runtime = MockRuntime::same_response((SignWithSchnorrResponse { /// # signature: "ityU6OGhNgvUXCL8gOy9p0LNThE8eKn4LUPNFwpeQVyXiUmNOzohl0VkcwEQnTqg".to_string().into_bytes(), -/// # }); +/// # },)); /// /// // TODO XC-317: Use pubkey that is actually derived from the given derivation path /// let key_id = Ed25519KeyId::TestKey1; @@ -977,7 +978,7 @@ impl SolRpcClient { /// /// assert_eq!( /// signature, -/// Ok(solana_signature::Signature::from_str("").unwrap()) +/// Ok(Signature::from_str("37HbmunhjSC1xxnVsaFX2xaS8gYnb5JYiLy9B51Ky9Up69aF7Qra6dHSLMCaiurRYq3Y8ZxSVUwC5sntziWuhZee").unwrap()) /// ); /// /// // The transaction is now signed and can be submitted with the `sendTransaction` RPC method. From 8d2d6d0bf859da1aa7806af3cad36a93f172ee7a Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Mon, 19 May 2025 15:48:02 +0200 Subject: [PATCH 04/21] XC-317: Remove TODO --- libs/client/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/client/src/lib.rs b/libs/client/src/lib.rs index 3e146af8..aaa22a3c 100644 --- a/libs/client/src/lib.rs +++ b/libs/client/src/lib.rs @@ -948,7 +948,6 @@ impl SolRpcClient { /// # signature: "ityU6OGhNgvUXCL8gOy9p0LNThE8eKn4LUPNFwpeQVyXiUmNOzohl0VkcwEQnTqg".to_string().into_bytes(), /// # },)); /// -/// // TODO XC-317: Use pubkey that is actually derived from the given derivation path /// let key_id = Ed25519KeyId::TestKey1; /// let derivation_path = None; /// // This pubkey should be derived from the root key `key_id` with `derivation_path`, see: From b848a0af8e8198615a240c36043d4e3775e633dc Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Mon, 19 May 2025 16:33:50 +0200 Subject: [PATCH 05/21] XC-317: Fix Candid interface --- examples/basic_solana/basic_solana.did | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/basic_solana/basic_solana.did b/examples/basic_solana/basic_solana.did index 6d866c23..9be93ce5 100644 --- a/examples/basic_solana/basic_solana.did +++ b/examples/basic_solana/basic_solana.did @@ -7,7 +7,7 @@ type InitArg = record { solana_commitment_level : opt CommitmentLevel; // EdDSA keys will be derived from this key. // If not specified, the value is set to `TestKeyLocalDevelopment`. - ed25519_key_name : opt Ed25519KeyId; + ed25519_key_id : opt Ed25519KeyId; // The canister will interact with this SOL RPC canister. // If not specified, the value is set to `tghme-zyaaa-aaaar-qarca-cai`. sol_rpc_canister_id : opt principal; From a273b6db460f1d4703c654345fe049f8eb50219c Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Mon, 19 May 2025 16:49:18 +0200 Subject: [PATCH 06/21] XC-317: Response already parsed as tuple --- libs/client/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/client/src/lib.rs b/libs/client/src/lib.rs index aaa22a3c..ca7fb6c9 100644 --- a/libs/client/src/lib.rs +++ b/libs/client/src/lib.rs @@ -997,7 +997,7 @@ pub async fn sign_transaction( name: params.key_id.to_string(), }, }; - let (response,): (SignWithSchnorrResponse,) = R::update_call( + let response: SignWithSchnorrResponse = R::update_call( runtime, Principal::management_canister(), "sign_with_schnorr", From 5dca201aeda9ea5af0379f5c51ecb388b15d8dc0 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Mon, 19 May 2025 17:04:39 +0200 Subject: [PATCH 07/21] XC-317: Unnecessary tuple in docs --- libs/client/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/client/src/lib.rs b/libs/client/src/lib.rs index ca7fb6c9..04518be3 100644 --- a/libs/client/src/lib.rs +++ b/libs/client/src/lib.rs @@ -944,9 +944,9 @@ impl SolRpcClient { /// use candid::Principal; /// # use ic_cdk::api::management_canister::schnorr::SignWithSchnorrResponse; /// let runtime = IcRuntime; -/// # let runtime = MockRuntime::same_response((SignWithSchnorrResponse { +/// # let runtime = MockRuntime::same_response(SignWithSchnorrResponse { /// # signature: "ityU6OGhNgvUXCL8gOy9p0LNThE8eKn4LUPNFwpeQVyXiUmNOzohl0VkcwEQnTqg".to_string().into_bytes(), -/// # },)); +/// # }); /// /// let key_id = Ed25519KeyId::TestKey1; /// let derivation_path = None; From 8abbf47afe6b612472fb840ac643d0e0275fd7d9 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Tue, 20 May 2025 10:23:42 +0200 Subject: [PATCH 08/21] XC-317: Add method to fetch EdDSA public key --- Cargo.lock | 1 + examples/basic_solana/src/ed25519.rs | 44 ++--- examples/basic_solana/src/solana_wallet.rs | 10 +- examples/basic_solana/src/state.rs | 2 +- libs/client/Cargo.toml | 1 + libs/client/src/lib.rs | 114 +----------- libs/client/src/threshold_signatures.rs | 202 +++++++++++++++++++++ 7 files changed, 228 insertions(+), 146 deletions(-) create mode 100644 libs/client/src/threshold_signatures.rs diff --git a/Cargo.lock b/Cargo.lock index d6bb05b7..3eec5950 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4457,6 +4457,7 @@ dependencies = [ "candid", "derive_more", "ic-cdk", + "ic-ed25519", "serde", "serde_json", "sol_rpc_types", diff --git a/examples/basic_solana/src/ed25519.rs b/examples/basic_solana/src/ed25519.rs index 7cbe65af..7f94265f 100644 --- a/examples/basic_solana/src/ed25519.rs +++ b/examples/basic_solana/src/ed25519.rs @@ -1,7 +1,5 @@ -use ic_cdk::api::management_canister::schnorr::{ - SchnorrAlgorithm, SchnorrKeyId, SchnorrPublicKeyArgument, SchnorrPublicKeyResponse, -}; use ic_ed25519::PublicKey; +use sol_rpc_client::IcRuntime; use sol_rpc_types::{DerivationPath, Ed25519KeyId}; #[derive(Clone, Debug, PartialEq, Eq)] @@ -28,34 +26,20 @@ impl Ed25519ExtendedPublicKey { } } -impl From for Ed25519ExtendedPublicKey { - fn from(value: SchnorrPublicKeyResponse) -> Self { - Ed25519ExtendedPublicKey { - public_key: PublicKey::deserialize_raw(value.public_key.as_slice()).unwrap(), - chain_code: <[u8; 32]>::try_from(value.chain_code).unwrap(), - } - } -} - pub async fn get_ed25519_public_key( - key_name: &Ed25519KeyId, + key_id: Ed25519KeyId, derivation_path: &DerivationPath, ) -> Ed25519ExtendedPublicKey { - let (response,): (SchnorrPublicKeyResponse,) = - ic_cdk::api::management_canister::schnorr::schnorr_public_key(SchnorrPublicKeyArgument { - canister_id: None, - derivation_path: derivation_path.clone().into(), - key_id: SchnorrKeyId { - algorithm: SchnorrAlgorithm::Ed25519, - name: key_name.to_string(), - }, - }) - .await - .unwrap_or_else(|(error_code, message)| { - ic_cdk::trap(&format!( - "failed to get canister's public key: {} (error code = {:?})", - message, error_code, - )) - }); - Ed25519ExtendedPublicKey::from(response) + let (pubkey, chain_code) = sol_rpc_client::threshold_signatures::get_pubkey( + &IcRuntime, + None, + Some(&derivation_path), + key_id, + ) + .await + .expect("Failed to fetch EdDSA public key"); + Ed25519ExtendedPublicKey { + public_key: PublicKey::deserialize_raw(&pubkey.to_bytes()).unwrap(), + chain_code, + } } diff --git a/examples/basic_solana/src/solana_wallet.rs b/examples/basic_solana/src/solana_wallet.rs index 5e41ef4c..c6ab0453 100644 --- a/examples/basic_solana/src/solana_wallet.rs +++ b/examples/basic_solana/src/solana_wallet.rs @@ -9,7 +9,7 @@ use crate::{ state::{lazy_call_ed25519_public_key, read_state}, }; use candid::Principal; -use sol_rpc_client::{sign_transaction, IcRuntime}; +use sol_rpc_client::{threshold_signatures::sign_transaction, IcRuntime}; use sol_rpc_types::{DerivationPath, SignTransactionRequestParams}; use solana_message::Message; use solana_pubkey::Pubkey; @@ -91,11 +91,9 @@ impl SolanaWallet { pub async fn sign_message(message: &Message, signer: &SolanaAccount) -> Signature { sign_transaction( &IcRuntime, - SignTransactionRequestParams { - transaction: Transaction::new_unsigned(message.clone()), - key_id: read_state(|s| s.ed25519_key_id()), - derivation_path: Some(signer.derivation_path.clone()), - }, + &Transaction::new_unsigned(message.clone()), + read_state(|s| s.ed25519_key_id()), + Some(&signer.derivation_path), ) .await .expect("Failed to sign transaction") diff --git a/examples/basic_solana/src/state.rs b/examples/basic_solana/src/state.rs index 69320328..f03186ce 100644 --- a/examples/basic_solana/src/state.rs +++ b/examples/basic_solana/src/state.rs @@ -72,7 +72,7 @@ pub async fn lazy_call_ed25519_public_key() -> Ed25519ExtendedPublicKey { return public_key; } let public_key = - get_ed25519_public_key(&read_state(|s| s.ed25519_key_id()), &Default::default()).await; + get_ed25519_public_key(read_state(|s| s.ed25519_key_id()), &Default::default()).await; mutate_state(|s| s.ed25519_public_key = Some(public_key.clone())); public_key } diff --git a/libs/client/Cargo.toml b/libs/client/Cargo.toml index 9a85e78e..8d0800f8 100644 --- a/libs/client/Cargo.toml +++ b/libs/client/Cargo.toml @@ -15,6 +15,7 @@ async-trait = { workspace = true } candid = { workspace = true } derive_more = { workspace = true } ic-cdk = { workspace = true } +ic-ed25519 = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } solana-account = { workspace = true } diff --git a/libs/client/src/lib.rs b/libs/client/src/lib.rs index 04518be3..ad373dfa 100644 --- a/libs/client/src/lib.rs +++ b/libs/client/src/lib.rs @@ -121,6 +121,7 @@ #[cfg(not(target_arch = "wasm32"))] pub mod fixtures; mod request; +pub mod threshold_signatures; use crate::request::{ GetAccountInfoRequest, GetBalanceRequest, GetBlockRequest, GetRecentPrioritizationFeesRequest, @@ -130,29 +131,20 @@ use crate::request::{ }; use async_trait::async_trait; use candid::{utils::ArgumentEncoder, CandidType, Principal}; -use ic_cdk::api::{ - call::RejectionCode, - management_canister::schnorr::{ - SchnorrAlgorithm, SchnorrKeyId, SignWithSchnorrArgument, SignWithSchnorrResponse, - }, -}; +use ic_cdk::api::call::RejectionCode; pub use request::{Request, RequestBuilder, SolRpcEndpoint, SolRpcRequest}; use serde::de::DeserializeOwned; use sol_rpc_types::{ CommitmentLevel, GetAccountInfoParams, GetBalanceParams, GetBlockParams, GetRecentPrioritizationFeesParams, GetSignatureStatusesParams, GetSlotParams, GetSlotRpcConfig, - GetTokenAccountBalanceParams, GetTransactionParams, Lamport, Pubkey, RpcConfig, RpcError, - RpcResult, RpcSources, SendTransactionParams, SignTransactionRequestParams, Signature, Slot, - SolanaCluster, SupportedRpcProvider, SupportedRpcProviderId, TokenAmount, TransactionDetails, - TransactionInfo, + GetTokenAccountBalanceParams, GetTransactionParams, Lamport, Pubkey, RpcConfig, RpcResult, + RpcSources, SendTransactionParams, Signature, Slot, SolanaCluster, SupportedRpcProvider, + SupportedRpcProviderId, TokenAmount, TransactionDetails, TransactionInfo, }; use solana_account_decoder_client_types::token::UiTokenAmount; use solana_transaction_status_client_types::EncodedConfirmedTransactionWithStatusMeta; use std::{fmt::Debug, sync::Arc}; -// Source: https://internetcomputer.org/docs/current/references/t-sigs-how-it-works/#fees-for-the-t-schnorr-production-key -const SIGN_WITH_SCHNORR_FEE: u128 = 26_153_846_153; - /// The principal identifying the productive Solana RPC canister under NNS control. /// /// ```rust @@ -922,102 +914,6 @@ impl SolRpcClient { } } -/// Sign an unsigned Solana transaction with threshold EdDSA, see threshold Schnorr documentation -/// [here](https://internetcomputer.org/docs/building-apps/network-features/signatures/t-schnorr). -/// -/// # Examples -/// -/// ```rust -/// use solana_hash::Hash; -/// use solana_message::legacy::Message; -/// use solana_program::system_instruction::transfer; -/// use solana_pubkey::pubkey; -/// use solana_signature::Signature; -/// use solana_transaction::Transaction; -/// use sol_rpc_client::{IcRuntime, sign_transaction, SolRpcClient}; -/// use sol_rpc_types::{DerivationPath, Ed25519KeyId, SignTransactionRequestParams}; -/// -/// # #[tokio::main] -/// # async fn main() -> Result<(), Box> { -/// # use sol_rpc_client::fixtures::MockRuntime; -/// # use std::str::FromStr; -/// use candid::Principal; -/// # use ic_cdk::api::management_canister::schnorr::SignWithSchnorrResponse; -/// let runtime = IcRuntime; -/// # let runtime = MockRuntime::same_response(SignWithSchnorrResponse { -/// # signature: "ityU6OGhNgvUXCL8gOy9p0LNThE8eKn4LUPNFwpeQVyXiUmNOzohl0VkcwEQnTqg".to_string().into_bytes(), -/// # }); -/// -/// let key_id = Ed25519KeyId::TestKey1; -/// let derivation_path = None; -/// // This pubkey should be derived from the root key `key_id` with `derivation_path`, see: -/// // https://internetcomputer.org/docs/references/ic-interface-spec#ic-schnorr_public_key -/// let payer = pubkey!("3EdRSc7CnKUGxGUSZwJ58rd7haBM8CR2Xh87KheEX7iS"); -/// -/// let recipient = pubkey!("BPebStjcgCPnWTK3FXZJ8KhqwNYLk9aubC9b4Cgqb6oE"); -/// -/// // TODO XC-317: Use client method to fetch recent blockhash -/// let recent_blockhash = Hash::new_unique(); -/// -/// let message = Message::new_with_blockhash( -/// &[transfer(&payer, &recipient, 1_000_000)], -/// Some(&payer), -/// &recent_blockhash, -/// ); -/// -/// let mut transaction = Transaction::new_unsigned(message); -/// let signature = sign_transaction( -/// &runtime, -/// SignTransactionRequestParams { -/// transaction: transaction.clone(), -/// derivation_path, -/// key_id, -/// }, -/// ).await; -/// -/// assert_eq!( -/// signature, -/// Ok(Signature::from_str("37HbmunhjSC1xxnVsaFX2xaS8gYnb5JYiLy9B51Ky9Up69aF7Qra6dHSLMCaiurRYq3Y8ZxSVUwC5sntziWuhZee").unwrap()) -/// ); -/// -/// // The transaction is now signed and can be submitted with the `sendTransaction` RPC method. -/// transaction.signatures = vec![signature.unwrap()]; -/// # Ok(()) -/// # } -/// ``` -pub async fn sign_transaction( - runtime: &R, - params: SignTransactionRequestParams, -) -> RpcResult { - let arg = SignWithSchnorrArgument { - message: params.transaction.message_data(), - derivation_path: params.derivation_path.unwrap_or_default().into(), - key_id: SchnorrKeyId { - algorithm: SchnorrAlgorithm::Ed25519, - name: params.key_id.to_string(), - }, - }; - let response: SignWithSchnorrResponse = R::update_call( - runtime, - Principal::management_canister(), - "sign_with_schnorr", - (arg,), - SIGN_WITH_SCHNORR_FEE, - ) - .await - .map_err(|(rejection_code, message)| { - RpcError::ValidationError(format!( - "Failed to sign transaction, management canister returned code {rejection_code:?}: {message}") - ) - })?; - solana_signature::Signature::try_from(response.signature).map_err(|bytes| { - RpcError::ValidationError(format!( - "Expected signature to contain 64 bytes, got {} bytes", - bytes.len() - )) - }) -} - /// Runtime when interacting with a canister running on the Internet Computer. #[derive(Copy, Clone, Eq, PartialEq, Debug)] pub struct IcRuntime; diff --git a/libs/client/src/threshold_signatures.rs b/libs/client/src/threshold_signatures.rs new file mode 100644 index 00000000..2e141bea --- /dev/null +++ b/libs/client/src/threshold_signatures.rs @@ -0,0 +1,202 @@ +//! This module provides some helper functions for the Internet Computer threshold EdDSA signatures API in the context +//! of the SOL RPC canister, e.g. signing Solana transactions and fetching and deriving EdDSA public keys. +//! See the [documentation](https://internetcomputer.org/docs/building-apps/network-features/signatures/t-schnorr) +//! for more detailed information on the full threshold Schnorr API. + +pub use crate::request::{Request, RequestBuilder, SolRpcEndpoint, SolRpcRequest}; +use crate::Runtime; +use candid::Principal; +use ic_cdk::api::management_canister::schnorr::{ + SchnorrAlgorithm, SchnorrKeyId, SignWithSchnorrArgument, SignWithSchnorrResponse, +}; +use ic_cdk::api::management_canister::schnorr::{ + SchnorrPublicKeyArgument, SchnorrPublicKeyResponse, +}; +use sol_rpc_types::{DerivationPath, Ed25519KeyId, RpcError, RpcResult}; + +// Source: https://internetcomputer.org/docs/current/references/t-sigs-how-it-works/#fees-for-the-t-schnorr-production-key +const SIGN_WITH_SCHNORR_FEE: u128 = 26_153_846_153; + +/// Sign an unsigned Solana transaction with threshold EdDSA, see threshold Schnorr documentation +/// [here](https://internetcomputer.org/docs/building-apps/network-features/signatures/t-schnorr). +/// +/// # Examples +/// +/// ```rust +/// use solana_hash::Hash; +/// use solana_message::legacy::Message; +/// use solana_program::system_instruction::transfer; +/// use solana_pubkey::pubkey; +/// use solana_signature::Signature; +/// use solana_transaction::Transaction; +/// use sol_rpc_client::{IcRuntime, SolRpcClient, threshold_signatures}; +/// use sol_rpc_types::{DerivationPath, Ed25519KeyId, SignTransactionRequestParams}; +/// +/// # #[tokio::main] +/// # async fn main() -> Result<(), Box> { +/// # use sol_rpc_client::fixtures::MockRuntime; +/// # use std::str::FromStr; +/// use candid::Principal; +/// # use ic_cdk::api::management_canister::schnorr::{SchnorrPublicKeyResponse, SignWithSchnorrResponse}; +/// let runtime = IcRuntime; +/// # let runtime = MockRuntime::same_response(SchnorrPublicKeyResponse { +/// # public_key: pubkey!("BPebStjcgCPnWTK3FXZJ8KhqwNYLk9aubC9b4Cgqb6oE").as_ref().to_vec(), +/// # chain_code: "UWbC6EgDnWEJIU4KFBqASTCYAzEiJGsR".as_bytes().to_vec(), +/// # }); +/// +/// let key_id = Ed25519KeyId::TestKey1; +/// let derivation_path = None; +/// let (payer, _) = threshold_signatures::get_pubkey( +/// &runtime, +/// None, +/// derivation_path, +/// key_id) +/// .await +/// .unwrap(); +/// +/// let recipient = pubkey!("BPebStjcgCPnWTK3FXZJ8KhqwNYLk9aubC9b4Cgqb6oE"); +/// +/// // TODO XC-317: Use client method to fetch recent blockhash +/// let recent_blockhash = Hash::new_unique(); +/// +/// let message = Message::new_with_blockhash( +/// &[transfer(&payer, &recipient, 1_000_000)], +/// Some(&payer), +/// &recent_blockhash, +/// ); +/// +/// +/// # let runtime = MockRuntime::same_response(SignWithSchnorrResponse { +/// # signature: Signature::from_str("37HbmunhjSC1xxnVsaFX2xaS8gYnb5JYiLy9B51Ky9Up69aF7Qra6dHSLMCaiurRYq3Y8ZxSVUwC5sntziWuhZee").unwrap().as_ref().to_vec(), +/// # }); +/// let mut transaction = Transaction::new_unsigned(message); +/// let signature = threshold_signatures::sign_transaction( +/// &runtime, +/// &transaction, +/// key_id, +/// derivation_path, +/// ).await; +/// +/// assert_eq!( +/// signature, +/// Ok(Signature::from_str("37HbmunhjSC1xxnVsaFX2xaS8gYnb5JYiLy9B51Ky9Up69aF7Qra6dHSLMCaiurRYq3Y8ZxSVUwC5sntziWuhZee").unwrap()) +/// ); +/// +/// // The transaction is now signed and can be submitted with the `sendTransaction` RPC method. +/// transaction.signatures = vec![signature.unwrap()]; +/// # Ok(()) +/// # } +/// ``` +pub async fn sign_transaction( + runtime: &R, + transaction: &solana_transaction::Transaction, + key_id: Ed25519KeyId, + derivation_path: Option<&DerivationPath>, +) -> RpcResult { + let arg = SignWithSchnorrArgument { + message: transaction.message_data(), + derivation_path: derivation_path.cloned().unwrap_or_default().into(), + key_id: SchnorrKeyId { + algorithm: SchnorrAlgorithm::Ed25519, + name: key_id.to_string(), + }, + }; + let response: SignWithSchnorrResponse = R::update_call( + runtime, + Principal::management_canister(), + "sign_with_schnorr", + (arg,), + SIGN_WITH_SCHNORR_FEE, + ) + .await + .map_err(|(rejection_code, message)| { + RpcError::ValidationError(format!( + "Failed to sign transaction, management canister returned code {rejection_code:?}: {message}") + ) + })?; + solana_signature::Signature::try_from(response.signature).map_err(|bytes| { + RpcError::ValidationError(format!( + "Expected signature to contain 64 bytes, got {} bytes", + bytes.len() + )) + }) +} + +/// Fetch the Ed25519 public key for the key ID, given canister ID and derivation path, see threshold Schnorr +/// documentation [here](https://internetcomputer.org/docs/building-apps/network-features/signatures/t-schnorr). +/// +/// # Examples +/// +/// ```rust +/// use candid::Principal; +/// use solana_pubkey::pubkey; +/// use sol_rpc_client::{threshold_signatures, IcRuntime}; +/// use sol_rpc_types::{DerivationPath, Ed25519KeyId}; +/// +/// # #[tokio::main] +/// # async fn main() -> Result<(), Box> { +/// # use sol_rpc_client::fixtures::MockRuntime; +/// # use ic_cdk::api::management_canister::schnorr::{SchnorrPublicKeyResponse, SignWithSchnorrResponse}; +/// let runtime = IcRuntime; +/// # let runtime = MockRuntime::same_response(SchnorrPublicKeyResponse { +/// # public_key: pubkey!("BPebStjcgCPnWTK3FXZJ8KhqwNYLk9aubC9b4Cgqb6oE").as_ref().to_vec(), +/// # chain_code: "UWbC6EgDnWEJIU4KFBqASTCYAzEiJGsR".as_bytes().to_vec(), +/// # }); +/// +/// let key_id = Ed25519KeyId::TestKey1; +/// let canister_id = Principal::from_text("un4fu-tqaaa-aaaab-qadjq-cai").unwrap(); +/// let derivation_path = DerivationPath::from("some-derivation-path".as_bytes()); +/// +/// let (pubkey, _) = threshold_signatures::get_pubkey( +/// &runtime, +/// Some(canister_id), +/// Some(&derivation_path), +/// key_id +/// ) +/// .await +/// .unwrap(); +/// +/// assert_eq!(pubkey, pubkey!("BPebStjcgCPnWTK3FXZJ8KhqwNYLk9aubC9b4Cgqb6oE") +/// ); +/// # Ok(()) +/// # } +/// ``` +pub async fn get_pubkey( + runtime: &R, + canister_id: Option, + derivation_path: Option<&DerivationPath>, + key_id: Ed25519KeyId, +) -> RpcResult<(solana_pubkey::Pubkey, [u8; 32])> { + let arg = SchnorrPublicKeyArgument { + canister_id, + derivation_path: derivation_path.cloned().unwrap_or_default().into(), + key_id: SchnorrKeyId { + algorithm: SchnorrAlgorithm::Ed25519, + name: key_id.to_string(), + }, + }; + let SchnorrPublicKeyResponse { + public_key, chain_code + } = runtime + .query_call( + Principal::management_canister(), + "schnorr_public_key", + (arg,), + ) + .await + .map_err(|(rejection_code, message)| { + RpcError::ValidationError(format!( + "Failed to fetch EdDSA public key, management canister returned code {rejection_code:?}: {message}") + ) + })?; + let pubkey = solana_pubkey::Pubkey::try_from(public_key.as_slice()).map_err(|e| { + RpcError::ValidationError(format!("Failed to parse bytes as public key: {e}")) + })?; + let chain_code = <[u8; 32]>::try_from(chain_code.as_slice()).map_err(|_| { + RpcError::ValidationError(format!( + "Expected chain code to contain 32 bytes but it contained {}", + chain_code.len() + )) + })?; + Ok((pubkey, chain_code)) +} From 81f536b8c70db23c622409294579bd0b439125fc Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Tue, 20 May 2025 10:51:37 +0200 Subject: [PATCH 09/21] XC-317: Clippy --- canister/scripts/examples.sh | 4 +- canister/src/rpc_client/tests.rs | 2 +- examples/basic_solana/src/ed25519.rs | 14 ++-- examples/basic_solana/src/lib.rs | 3 +- examples/basic_solana/src/solana_wallet.rs | 4 +- examples/basic_solana/src/state.rs | 3 +- examples/basic_solana/tests/tests.rs | 3 +- libs/client/src/lib.rs | 6 +- ...reshold_signatures.rs => threshold_sig.rs} | 21 +++--- libs/types/src/lib.rs | 12 +-- libs/types/src/solana/mod.rs | 5 +- libs/types/src/solana/request/mod.rs | 74 ++----------------- libs/types/src/solana/request/tests.rs | 4 +- libs/types/src/solana/signature/mod.rs | 53 +++++++++++++ 14 files changed, 100 insertions(+), 108 deletions(-) rename libs/client/src/{threshold_signatures.rs => threshold_sig.rs} (92%) create mode 100644 libs/types/src/solana/signature/mod.rs diff --git a/canister/scripts/examples.sh b/canister/scripts/examples.sh index d28aaf37..7176d5b8 100755 --- a/canister/scripts/examples.sh +++ b/canister/scripts/examples.sh @@ -61,8 +61,8 @@ GET_BLOCK_PARAMS="( )" CYCLES=$(dfx canister call sol_rpc getBlockCyclesCost "$GET_BLOCK_PARAMS" $FLAGS --output json | jq '.Ok' --raw-output || exit 1) GET_BLOCK_OUTPUT=$(dfx canister call sol_rpc getBlock "$GET_BLOCK_PARAMS" $FLAGS --output json --with-cycles "$CYCLES" || exit 1) -FIRST_SIGNATURE=$(jq --raw-output '.Consistent.Ok[0].signatures[0][0]' <<< "$GET_BLOCK_OUTPUT") -SECOND_SIGNATURE=$(jq --raw-output '.Consistent.Ok[0].signatures[0][1]' <<< "$GET_BLOCK_OUTPUT") +FIRST_SIGNATURE=$(jq --raw-output '.Consistent.Ok[0].signature[0][0]' <<< "$GET_BLOCK_OUTPUT") +SECOND_SIGNATURE=$(jq --raw-output '.Consistent.Ok[0].signature[0][1]' <<< "$GET_BLOCK_OUTPUT") # Fetch the statuses of the first two transactions in the received block GET_SIGNATURE_STATUSES_PARAMS="( diff --git a/canister/src/rpc_client/tests.rs b/canister/src/rpc_client/tests.rs index 83f35883..0677f70d 100644 --- a/canister/src/rpc_client/tests.rs +++ b/canister/src/rpc_client/tests.rs @@ -256,7 +256,7 @@ mod request_serialization_tests { 123, { "rewards": false, - "transactionDetails": "signatures", + "transactionDetails": "signature", "commitment": "finalized", "maxSupportedTransactionVersion": 2 }, diff --git a/examples/basic_solana/src/ed25519.rs b/examples/basic_solana/src/ed25519.rs index 7f94265f..60cfc72c 100644 --- a/examples/basic_solana/src/ed25519.rs +++ b/examples/basic_solana/src/ed25519.rs @@ -1,6 +1,6 @@ use ic_ed25519::PublicKey; use sol_rpc_client::IcRuntime; -use sol_rpc_types::{DerivationPath, Ed25519KeyId}; +use sol_rpc_types::solana::::{DerivationPath, Ed25519KeyId}; #[derive(Clone, Debug, PartialEq, Eq)] pub struct Ed25519ExtendedPublicKey { @@ -30,14 +30,10 @@ pub async fn get_ed25519_public_key( key_id: Ed25519KeyId, derivation_path: &DerivationPath, ) -> Ed25519ExtendedPublicKey { - let (pubkey, chain_code) = sol_rpc_client::threshold_signatures::get_pubkey( - &IcRuntime, - None, - Some(&derivation_path), - key_id, - ) - .await - .expect("Failed to fetch EdDSA public key"); + let (pubkey, chain_code) = + sol_rpc_client::threshold_sig::get_pubkey(&IcRuntime, None, Some(derivation_path), key_id) + .await + .expect("Failed to fetch EdDSA public key"); Ed25519ExtendedPublicKey { public_key: PublicKey::deserialize_raw(&pubkey.to_bytes()).unwrap(), chain_code, diff --git a/examples/basic_solana/src/lib.rs b/examples/basic_solana/src/lib.rs index 82bcf14b..9edfc5c3 100644 --- a/examples/basic_solana/src/lib.rs +++ b/examples/basic_solana/src/lib.rs @@ -6,9 +6,10 @@ pub mod state; use crate::state::{read_state, State}; use candid::{CandidType, Deserialize, Principal}; use sol_rpc_client::{IcRuntime, SolRpcClient}; -use sol_rpc_types::{CommitmentLevel, Ed25519KeyId, MultiRpcResult, RpcSources, SolanaCluster}; +use sol_rpc_types::{CommitmentLevel, MultiRpcResult, RpcSources, SolanaCluster}; use solana_hash::Hash; use std::str::FromStr; +use sol_rpc_types::solana::::Ed25519KeyId; // Fetch a recent blockhash using the Solana `getSlot` and `getBlock` methods. // Since the `getSlot` method might fail due to Solana's fast blocktime, and some slots do not diff --git a/examples/basic_solana/src/solana_wallet.rs b/examples/basic_solana/src/solana_wallet.rs index c6ab0453..68676c09 100644 --- a/examples/basic_solana/src/solana_wallet.rs +++ b/examples/basic_solana/src/solana_wallet.rs @@ -9,8 +9,8 @@ use crate::{ state::{lazy_call_ed25519_public_key, read_state}, }; use candid::Principal; -use sol_rpc_client::{threshold_signatures::sign_transaction, IcRuntime}; -use sol_rpc_types::{DerivationPath, SignTransactionRequestParams}; +use sol_rpc_client::{threshold_sig::sign_transaction, IcRuntime}; +use sol_rpc_types::solana::::DerivationPath; use solana_message::Message; use solana_pubkey::Pubkey; use solana_signature::Signature; diff --git a/examples/basic_solana/src/state.rs b/examples/basic_solana/src/state.rs index f03186ce..41e75d57 100644 --- a/examples/basic_solana/src/state.rs +++ b/examples/basic_solana/src/state.rs @@ -3,11 +3,12 @@ use crate::{ InitArg, SolanaNetwork, }; use candid::Principal; -use sol_rpc_types::{CommitmentLevel, Ed25519KeyId}; +use sol_rpc_types::CommitmentLevel; use std::{ cell::RefCell, ops::{Deref, DerefMut}, }; +use sol_rpc_types::solana::::Ed25519KeyId; thread_local! { pub static STATE: RefCell = RefCell::default(); diff --git a/examples/basic_solana/tests/tests.rs b/examples/basic_solana/tests/tests.rs index b1b4a73e..ca557865 100644 --- a/examples/basic_solana/tests/tests.rs +++ b/examples/basic_solana/tests/tests.rs @@ -8,7 +8,7 @@ use pocket_ic::{ }; use serde::de::DeserializeOwned; use sol_rpc_types::{ - CommitmentLevel, Ed25519KeyId, OverrideProvider, RegexSubstitution, RpcAccess, + CommitmentLevel, OverrideProvider, RegexSubstitution, RpcAccess, SupportedRpcProvider, SupportedRpcProviderId, TokenAmount, }; use solana_client::{rpc_client::RpcClient as SolanaRpcClient, rpc_config::RpcTransactionConfig}; @@ -22,6 +22,7 @@ use solana_signature::Signature; use solana_signer::Signer; use solana_transaction::Transaction; use std::{env::var, path::PathBuf, sync::Arc, thread, time::Duration}; +use sol_rpc_types::solana::::Ed25519KeyId; pub const SENDER: Principal = Principal::from_slice(&[0x9d, 0xf7, 0x42]); pub const RECEIVER: Principal = Principal::from_slice(&[0x9d, 0xf7, 0x43]); diff --git a/libs/client/src/lib.rs b/libs/client/src/lib.rs index ad373dfa..b1505c4b 100644 --- a/libs/client/src/lib.rs +++ b/libs/client/src/lib.rs @@ -121,7 +121,7 @@ #[cfg(not(target_arch = "wasm32"))] pub mod fixtures; mod request; -pub mod threshold_signatures; +pub mod threshold_sig; use crate::request::{ GetAccountInfoRequest, GetBalanceRequest, GetBlockRequest, GetRecentPrioritizationFeesRequest, @@ -602,9 +602,9 @@ impl SolRpcClient { /// /// # Errors /// - /// The number of signatures that can be passed to + /// The number of signature that can be passed to /// [`getSignatureStatuses`](https://solana.com/de/docs/rpc/http/getsignaturestatuses) - /// is limited to 256. More signatures result in an error. + /// is limited to 256. More signature result in an error. /// /// ```rust /// use std::{str::FromStr, collections::BTreeSet}; diff --git a/libs/client/src/threshold_signatures.rs b/libs/client/src/threshold_sig.rs similarity index 92% rename from libs/client/src/threshold_signatures.rs rename to libs/client/src/threshold_sig.rs index 2e141bea..da74ada3 100644 --- a/libs/client/src/threshold_signatures.rs +++ b/libs/client/src/threshold_sig.rs @@ -1,4 +1,4 @@ -//! This module provides some helper functions for the Internet Computer threshold EdDSA signatures API in the context +//! This module provides some helper functions for the Internet Computer threshold EdDSA signature API in the context //! of the SOL RPC canister, e.g. signing Solana transactions and fetching and deriving EdDSA public keys. //! See the [documentation](https://internetcomputer.org/docs/building-apps/network-features/signatures/t-schnorr) //! for more detailed information on the full threshold Schnorr API. @@ -12,7 +12,8 @@ use ic_cdk::api::management_canister::schnorr::{ use ic_cdk::api::management_canister::schnorr::{ SchnorrPublicKeyArgument, SchnorrPublicKeyResponse, }; -use sol_rpc_types::{DerivationPath, Ed25519KeyId, RpcError, RpcResult}; +use sol_rpc_types::{RpcError, RpcResult}; +use sol_rpc_types::{DerivationPath, Ed25519KeyId}; // Source: https://internetcomputer.org/docs/current/references/t-sigs-how-it-works/#fees-for-the-t-schnorr-production-key const SIGN_WITH_SCHNORR_FEE: u128 = 26_153_846_153; @@ -29,10 +30,10 @@ const SIGN_WITH_SCHNORR_FEE: u128 = 26_153_846_153; /// use solana_pubkey::pubkey; /// use solana_signature::Signature; /// use solana_transaction::Transaction; -/// use sol_rpc_client::{IcRuntime, SolRpcClient, threshold_signatures}; -/// use sol_rpc_types::{DerivationPath, Ed25519KeyId, SignTransactionRequestParams}; +/// use sol_rpc_client::{threshold_sig , IcRuntime}; +/// use sol_rpc_types::Ed25519KeyId; /// -/// # #[tokio::main] +/// #[tokio::main] /// # async fn main() -> Result<(), Box> { /// # use sol_rpc_client::fixtures::MockRuntime; /// # use std::str::FromStr; @@ -46,7 +47,7 @@ const SIGN_WITH_SCHNORR_FEE: u128 = 26_153_846_153; /// /// let key_id = Ed25519KeyId::TestKey1; /// let derivation_path = None; -/// let (payer, _) = threshold_signatures::get_pubkey( +/// let (payer, _) = threshold_sig::get_pubkey( /// &runtime, /// None, /// derivation_path, @@ -70,7 +71,7 @@ const SIGN_WITH_SCHNORR_FEE: u128 = 26_153_846_153; /// # signature: Signature::from_str("37HbmunhjSC1xxnVsaFX2xaS8gYnb5JYiLy9B51Ky9Up69aF7Qra6dHSLMCaiurRYq3Y8ZxSVUwC5sntziWuhZee").unwrap().as_ref().to_vec(), /// # }); /// let mut transaction = Transaction::new_unsigned(message); -/// let signature = threshold_signatures::sign_transaction( +/// let signature = threshold_sig::sign_transaction( /// &runtime, /// &transaction, /// key_id, @@ -130,10 +131,10 @@ pub async fn sign_transaction( /// ```rust /// use candid::Principal; /// use solana_pubkey::pubkey; -/// use sol_rpc_client::{threshold_signatures, IcRuntime}; +/// use sol_rpc_client::{threshold_sig, IcRuntime}; /// use sol_rpc_types::{DerivationPath, Ed25519KeyId}; /// -/// # #[tokio::main] +/// #[tokio::main] /// # async fn main() -> Result<(), Box> { /// # use sol_rpc_client::fixtures::MockRuntime; /// # use ic_cdk::api::management_canister::schnorr::{SchnorrPublicKeyResponse, SignWithSchnorrResponse}; @@ -147,7 +148,7 @@ pub async fn sign_transaction( /// let canister_id = Principal::from_text("un4fu-tqaaa-aaaab-qadjq-cai").unwrap(); /// let derivation_path = DerivationPath::from("some-derivation-path".as_bytes()); /// -/// let (pubkey, _) = threshold_signatures::get_pubkey( +/// let (pubkey, _) = threshold_sig::get_pubkey( /// &runtime, /// Some(canister_id), /// Some(&derivation_path), diff --git a/libs/types/src/lib.rs b/libs/types/src/lib.rs index d6e4446e..378614e2 100644 --- a/libs/types/src/lib.rs +++ b/libs/types/src/lib.rs @@ -25,13 +25,13 @@ use serde::Serialize; pub use solana::{ account::{AccountData, AccountEncoding, AccountInfo, ParsedAccount}, request::{ - CommitmentLevel, DataSlice, DerivationPath, Ed25519KeyId, GetAccountInfoEncoding, - GetAccountInfoParams, GetBalanceParams, GetBlockCommitmentLevel, GetBlockParams, - GetRecentPrioritizationFeesParams, GetSignatureStatusesParams, GetSlotParams, - GetTokenAccountBalanceParams, GetTransactionEncoding, GetTransactionParams, - SendTransactionEncoding, SendTransactionParams, SignTransactionRequestParams, - TransactionDetails, + CommitmentLevel, DataSlice, GetAccountInfoEncoding, GetAccountInfoParams, GetBalanceParams, + GetBlockCommitmentLevel, GetBlockParams, GetRecentPrioritizationFeesParams, + GetSignatureStatusesParams, GetSlotParams, GetTokenAccountBalanceParams, + GetTransactionEncoding, GetTransactionParams, SendTransactionEncoding, + SendTransactionParams, TransactionDetails, }, + signature::{DerivationPath, Ed25519KeyId}, transaction::{ error::{InstructionError, TransactionError}, instruction::{CompiledInstruction, InnerInstructions, Instruction}, diff --git a/libs/types/src/solana/mod.rs b/libs/types/src/solana/mod.rs index 081af6c3..77e2a725 100644 --- a/libs/types/src/solana/mod.rs +++ b/libs/types/src/solana/mod.rs @@ -4,6 +4,7 @@ mod tests; pub mod account; pub mod request; pub mod transaction; +pub mod signature; use crate::RpcError; use candid::CandidType; @@ -27,7 +28,7 @@ pub type MicroLamport = u64; pub type Timestamp = i64; /// The result of a Solana `getBlock` RPC method call. -// TODO XC-342: Add `transactions`, `signatures`, `rewards` and `num_reward_partitions` fields. +// TODO XC-342: Add `transactions`, `signature`, `rewards` and `num_reward_partitions` fields. #[derive(Debug, Clone, Deserialize, Serialize, CandidType, PartialEq)] pub struct ConfirmedBlock { /// The blockhash of this block's parent, as base-58 encoded string; if the parent block is not @@ -67,7 +68,7 @@ impl TryFrom for Confi } } -// TODO XC-342: Set `transactions`, `signatures`, `rewards` and `num_reward_partitions` fields. +// TODO XC-342: Set `transactions`, `signature`, `rewards` and `num_reward_partitions` fields. impl From for solana_transaction_status_client_types::UiConfirmedBlock { fn from(block: ConfirmedBlock) -> Self { Self { diff --git a/libs/types/src/solana/request/mod.rs b/libs/types/src/solana/request/mod.rs index 731fdf6d..5699ac4f 100644 --- a/libs/types/src/solana/request/mod.rs +++ b/libs/types/src/solana/request/mod.rs @@ -3,72 +3,10 @@ mod tests; use crate::{solana::Pubkey, RpcError, Signature, Slot, VecWithMaxLen}; use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine}; -use candid::{CandidType, Deserialize, Principal}; +use candid::{CandidType, Deserialize}; use serde::Serialize; use std::fmt::Display; -/// Represents the derivation path of an Ed25519 key from one of the root keys. -/// See the [tEdDSA documentation](https://internetcomputer.org/docs/building-apps/network-features/signatures/t-schnorr#signing-messages-and-transactions) -/// for more details. -#[derive(Clone, Debug, PartialEq, Eq, Default)] -pub struct DerivationPath(Vec>); - -impl From<&[u8]> for DerivationPath { - fn from(bytes: &[u8]) -> Self { - const SCHEMA_V1: u8 = 1; - Self([vec![SCHEMA_V1], bytes.to_vec()].into_iter().collect()) - } -} - -impl From for DerivationPath { - fn from(principal: Principal) -> Self { - DerivationPath::from(principal.as_slice()) - } -} - -impl From for Vec> { - fn from(derivation_path: DerivationPath) -> Self { - derivation_path.0 - } -} - -/// The ID of one of the ICP root keys. -/// See the [tEdDSA documentation](https://internetcomputer.org/docs/building-apps/network-features/signatures/t-schnorr#signing-messages-and-transactions) -/// for more details. -#[derive(CandidType, Deserialize, Debug, Default, PartialEq, Eq, Clone, Copy)] -pub enum Ed25519KeyId { - /// Only available on the local development environment started by dfx. - #[default] - TestKeyLocalDevelopment, - /// Test key available on the ICP mainnet. - TestKey1, - /// Production key available on the ICP mainnet. - ProductionKey1, -} - -impl Display for Ed25519KeyId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let str = match self { - Ed25519KeyId::TestKeyLocalDevelopment => "dfx_test_key", - Ed25519KeyId::TestKey1 => "test_key_1", - Ed25519KeyId::ProductionKey1 => "key_1", - } - .to_string(); - write!(f, "{}", str) - } -} - -/// The parameters for a request to sign the given Solana transaction with [tEdDSA](https://internetcomputer.org/docs/building-apps/network-features/signatures/t-schnorr). -pub struct SignTransactionRequestParams { - /// The transaction to sign. - pub transaction: solana_transaction::Transaction, - /// The root key from which the key used to sign the transaction is derived. - pub key_id: Ed25519KeyId, - /// The derivation path used to derive the key used to sign the transaction. - // TODO XC-317: Make this parameter optional? Is this allowed? - pub derivation_path: Option, -} - /// The parameters for a Solana [`getAccountInfo`](https://solana.com/docs/rpc/http/getaccountinfo) RPC method call. #[derive(Debug, Clone, Deserialize, Serialize, CandidType)] pub struct GetAccountInfoParams { @@ -237,10 +175,10 @@ impl From for GetBlockParams { /// are generally too large to be supported by the ICP. #[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, CandidType)] pub enum TransactionDetails { - /// Includes transaction signatures (IDs) and block metadata only. - #[serde(rename = "signatures")] + /// Includes transaction signature (IDs) and block metadata only. + #[serde(rename = "signature")] Signatures, - /// Omits all transaction data and signatures; returns only block metadata. + /// Omits all transaction data and signature; returns only block metadata. #[default] #[serde(rename = "none")] None, @@ -279,9 +217,9 @@ impl From for Vec { /// The parameters for a Solana [`getSignatureStatuses`](https://solana.com/docs/rpc/http/getsignaturestatuses) RPC method call. #[derive(Debug, Clone, Default, Deserialize, Serialize, CandidType)] pub struct GetSignatureStatusesParams { - /// An array of transaction signatures to confirm, as base-58 encoded strings (up to a maximum of 256) + /// An array of transaction signature to confirm, as base-58 encoded strings (up to a maximum of 256) pub signatures: VecWithMaxLen, - /// If set to true, a Solana node will search its ledger cache for any signatures not found in the recent status cache. + /// If set to true, a Solana node will search its ledger cache for any signature not found in the recent status cache. #[serde(rename = "searchTransactionHistory")] pub search_transaction_history: Option, } diff --git a/libs/types/src/solana/request/tests.rs b/libs/types/src/solana/request/tests.rs index 568486e6..e1975cb9 100644 --- a/libs/types/src/solana/request/tests.rs +++ b/libs/types/src/solana/request/tests.rs @@ -8,7 +8,7 @@ mod get_signature_statuses_params_tests { #[test] fn should_deserialize() { let params = json!({ - "signatures": vec!["5iBbqBJzgqafuQn93Np8ztWyXeYe2ReGPzUB1zXP2suZ8b5EaxSwe74ZUhg5pZQuDQkNGW7XApgfXX91YLYUuo5y"; 256] + "signature": vec!["5iBbqBJzgqafuQn93Np8ztWyXeYe2ReGPzUB1zXP2suZ8b5EaxSwe74ZUhg5pZQuDQkNGW7XApgfXX91YLYUuo5y"; 256] }); let result = GetSignatureStatusesParams::deserialize(¶ms); @@ -19,7 +19,7 @@ mod get_signature_statuses_params_tests { #[test] fn should_not_deserialize() { let params = json!({ - "signatures": vec!["5iBbqBJzgqafuQn93Np8ztWyXeYe2ReGPzUB1zXP2suZ8b5EaxSwe74ZUhg5pZQuDQkNGW7XApgfXX91YLYUuo5y"; 256 + 1] + "signature": vec!["5iBbqBJzgqafuQn93Np8ztWyXeYe2ReGPzUB1zXP2suZ8b5EaxSwe74ZUhg5pZQuDQkNGW7XApgfXX91YLYUuo5y"; 256 + 1] }); let result = GetSignatureStatusesParams::deserialize(¶ms); diff --git a/libs/types/src/solana/signature/mod.rs b/libs/types/src/solana/signature/mod.rs new file mode 100644 index 00000000..821cae87 --- /dev/null +++ b/libs/types/src/solana/signature/mod.rs @@ -0,0 +1,53 @@ +use std::fmt::Display; +use candid::{CandidType, Deserialize, Principal}; +use derive_more::{From, Into}; + +/// Represents the derivation path of an Ed25519 key from one of the root keys. +/// See the [tEdDSA documentation](https://internetcomputer.org/docs/building-apps/network-features/signatures/t-schnorr#signing-messages-and-transactions) +/// for more details. +#[derive(Clone, Debug, PartialEq, Eq, Default, From, Into)] +pub struct DerivationPath(Vec>); + +impl From<&[&[u8]]> for DerivationPath { + fn from(bytes: &[&[u8]]) -> Self { + Self(bytes.iter().map(|index| index.to_vec()).collect()) + } +} + +impl From<&[u8]> for DerivationPath { + fn from(bytes: &[u8]) -> Self { + Self(vec![bytes.to_vec()]) + } +} + +impl From for DerivationPath { + fn from(principal: Principal) -> Self { + DerivationPath::from(principal.as_slice()) + } +} + +/// The ID of one of the ICP root keys. +/// See the [tEdDSA documentation](https://internetcomputer.org/docs/building-apps/network-features/signatures/t-schnorr#signing-messages-and-transactions) +/// for more details. +#[derive(CandidType, Deserialize, Debug, Default, PartialEq, Eq, Clone, Copy)] +pub enum Ed25519KeyId { + /// Only available on the local development environment started by dfx. + #[default] + TestKeyLocalDevelopment, + /// Test key available on the ICP mainnet. + TestKey1, + /// Production key available on the ICP mainnet. + ProductionKey1, +} + +impl Display for Ed25519KeyId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str = match self { + Ed25519KeyId::TestKeyLocalDevelopment => "dfx_test_key", + Ed25519KeyId::TestKey1 => "test_key_1", + Ed25519KeyId::ProductionKey1 => "key_1", + } + .to_string(); + write!(f, "{}", str) + } +} \ No newline at end of file From f52b4174ae2be06f837dce729d485b896b5e94b0 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Tue, 20 May 2025 10:56:18 +0200 Subject: [PATCH 10/21] XC-317: Clippy --- canister/src/rpc_client/tests.rs | 2 +- examples/basic_solana/src/ed25519.rs | 2 +- examples/basic_solana/src/lib.rs | 3 +-- examples/basic_solana/src/solana_wallet.rs | 2 +- examples/basic_solana/src/state.rs | 3 +-- examples/basic_solana/tests/tests.rs | 3 +-- libs/client/src/threshold_sig.rs | 3 +-- libs/types/src/solana/mod.rs | 2 +- libs/types/src/solana/request/mod.rs | 1 - libs/types/src/solana/signature/mod.rs | 4 ++-- 10 files changed, 10 insertions(+), 15 deletions(-) diff --git a/canister/src/rpc_client/tests.rs b/canister/src/rpc_client/tests.rs index 0677f70d..83f35883 100644 --- a/canister/src/rpc_client/tests.rs +++ b/canister/src/rpc_client/tests.rs @@ -256,7 +256,7 @@ mod request_serialization_tests { 123, { "rewards": false, - "transactionDetails": "signature", + "transactionDetails": "signatures", "commitment": "finalized", "maxSupportedTransactionVersion": 2 }, diff --git a/examples/basic_solana/src/ed25519.rs b/examples/basic_solana/src/ed25519.rs index 60cfc72c..5236f67e 100644 --- a/examples/basic_solana/src/ed25519.rs +++ b/examples/basic_solana/src/ed25519.rs @@ -1,6 +1,6 @@ use ic_ed25519::PublicKey; use sol_rpc_client::IcRuntime; -use sol_rpc_types::solana::::{DerivationPath, Ed25519KeyId}; +use sol_rpc_types::{DerivationPath, Ed25519KeyId}; #[derive(Clone, Debug, PartialEq, Eq)] pub struct Ed25519ExtendedPublicKey { diff --git a/examples/basic_solana/src/lib.rs b/examples/basic_solana/src/lib.rs index 9edfc5c3..82bcf14b 100644 --- a/examples/basic_solana/src/lib.rs +++ b/examples/basic_solana/src/lib.rs @@ -6,10 +6,9 @@ pub mod state; use crate::state::{read_state, State}; use candid::{CandidType, Deserialize, Principal}; use sol_rpc_client::{IcRuntime, SolRpcClient}; -use sol_rpc_types::{CommitmentLevel, MultiRpcResult, RpcSources, SolanaCluster}; +use sol_rpc_types::{CommitmentLevel, Ed25519KeyId, MultiRpcResult, RpcSources, SolanaCluster}; use solana_hash::Hash; use std::str::FromStr; -use sol_rpc_types::solana::::Ed25519KeyId; // Fetch a recent blockhash using the Solana `getSlot` and `getBlock` methods. // Since the `getSlot` method might fail due to Solana's fast blocktime, and some slots do not diff --git a/examples/basic_solana/src/solana_wallet.rs b/examples/basic_solana/src/solana_wallet.rs index 68676c09..b73fb49c 100644 --- a/examples/basic_solana/src/solana_wallet.rs +++ b/examples/basic_solana/src/solana_wallet.rs @@ -10,7 +10,7 @@ use crate::{ }; use candid::Principal; use sol_rpc_client::{threshold_sig::sign_transaction, IcRuntime}; -use sol_rpc_types::solana::::DerivationPath; +use sol_rpc_types::DerivationPath; use solana_message::Message; use solana_pubkey::Pubkey; use solana_signature::Signature; diff --git a/examples/basic_solana/src/state.rs b/examples/basic_solana/src/state.rs index 41e75d57..f03186ce 100644 --- a/examples/basic_solana/src/state.rs +++ b/examples/basic_solana/src/state.rs @@ -3,12 +3,11 @@ use crate::{ InitArg, SolanaNetwork, }; use candid::Principal; -use sol_rpc_types::CommitmentLevel; +use sol_rpc_types::{CommitmentLevel, Ed25519KeyId}; use std::{ cell::RefCell, ops::{Deref, DerefMut}, }; -use sol_rpc_types::solana::::Ed25519KeyId; thread_local! { pub static STATE: RefCell = RefCell::default(); diff --git a/examples/basic_solana/tests/tests.rs b/examples/basic_solana/tests/tests.rs index ca557865..b1b4a73e 100644 --- a/examples/basic_solana/tests/tests.rs +++ b/examples/basic_solana/tests/tests.rs @@ -8,7 +8,7 @@ use pocket_ic::{ }; use serde::de::DeserializeOwned; use sol_rpc_types::{ - CommitmentLevel, OverrideProvider, RegexSubstitution, RpcAccess, + CommitmentLevel, Ed25519KeyId, OverrideProvider, RegexSubstitution, RpcAccess, SupportedRpcProvider, SupportedRpcProviderId, TokenAmount, }; use solana_client::{rpc_client::RpcClient as SolanaRpcClient, rpc_config::RpcTransactionConfig}; @@ -22,7 +22,6 @@ use solana_signature::Signature; use solana_signer::Signer; use solana_transaction::Transaction; use std::{env::var, path::PathBuf, sync::Arc, thread, time::Duration}; -use sol_rpc_types::solana::::Ed25519KeyId; pub const SENDER: Principal = Principal::from_slice(&[0x9d, 0xf7, 0x42]); pub const RECEIVER: Principal = Principal::from_slice(&[0x9d, 0xf7, 0x43]); diff --git a/libs/client/src/threshold_sig.rs b/libs/client/src/threshold_sig.rs index da74ada3..7620ad44 100644 --- a/libs/client/src/threshold_sig.rs +++ b/libs/client/src/threshold_sig.rs @@ -12,8 +12,7 @@ use ic_cdk::api::management_canister::schnorr::{ use ic_cdk::api::management_canister::schnorr::{ SchnorrPublicKeyArgument, SchnorrPublicKeyResponse, }; -use sol_rpc_types::{RpcError, RpcResult}; -use sol_rpc_types::{DerivationPath, Ed25519KeyId}; +use sol_rpc_types::{DerivationPath, Ed25519KeyId, RpcError, RpcResult}; // Source: https://internetcomputer.org/docs/current/references/t-sigs-how-it-works/#fees-for-the-t-schnorr-production-key const SIGN_WITH_SCHNORR_FEE: u128 = 26_153_846_153; diff --git a/libs/types/src/solana/mod.rs b/libs/types/src/solana/mod.rs index 77e2a725..93c2b867 100644 --- a/libs/types/src/solana/mod.rs +++ b/libs/types/src/solana/mod.rs @@ -3,8 +3,8 @@ mod tests; pub mod account; pub mod request; -pub mod transaction; pub mod signature; +pub mod transaction; use crate::RpcError; use candid::CandidType; diff --git a/libs/types/src/solana/request/mod.rs b/libs/types/src/solana/request/mod.rs index 5699ac4f..d3428e5d 100644 --- a/libs/types/src/solana/request/mod.rs +++ b/libs/types/src/solana/request/mod.rs @@ -5,7 +5,6 @@ use crate::{solana::Pubkey, RpcError, Signature, Slot, VecWithMaxLen}; use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine}; use candid::{CandidType, Deserialize}; use serde::Serialize; -use std::fmt::Display; /// The parameters for a Solana [`getAccountInfo`](https://solana.com/docs/rpc/http/getaccountinfo) RPC method call. #[derive(Debug, Clone, Deserialize, Serialize, CandidType)] diff --git a/libs/types/src/solana/signature/mod.rs b/libs/types/src/solana/signature/mod.rs index 821cae87..ba9a52c4 100644 --- a/libs/types/src/solana/signature/mod.rs +++ b/libs/types/src/solana/signature/mod.rs @@ -1,6 +1,6 @@ -use std::fmt::Display; use candid::{CandidType, Deserialize, Principal}; use derive_more::{From, Into}; +use std::fmt::Display; /// Represents the derivation path of an Ed25519 key from one of the root keys. /// See the [tEdDSA documentation](https://internetcomputer.org/docs/building-apps/network-features/signatures/t-schnorr#signing-messages-and-transactions) @@ -50,4 +50,4 @@ impl Display for Ed25519KeyId { .to_string(); write!(f, "{}", str) } -} \ No newline at end of file +} From fc895e47990bcda7f4243c7d7487e9e8c7f2372a Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Tue, 20 May 2025 10:59:48 +0200 Subject: [PATCH 11/21] XC-317: Revert aggressive refactoring --- libs/types/src/solana/mod.rs | 4 ++-- libs/types/src/solana/request/mod.rs | 10 +++++----- libs/types/src/solana/request/tests.rs | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/libs/types/src/solana/mod.rs b/libs/types/src/solana/mod.rs index 93c2b867..26fd665a 100644 --- a/libs/types/src/solana/mod.rs +++ b/libs/types/src/solana/mod.rs @@ -28,7 +28,7 @@ pub type MicroLamport = u64; pub type Timestamp = i64; /// The result of a Solana `getBlock` RPC method call. -// TODO XC-342: Add `transactions`, `signature`, `rewards` and `num_reward_partitions` fields. +// TODO XC-342: Add `transactions`, `signatures`, `rewards` and `num_reward_partitions` fields. #[derive(Debug, Clone, Deserialize, Serialize, CandidType, PartialEq)] pub struct ConfirmedBlock { /// The blockhash of this block's parent, as base-58 encoded string; if the parent block is not @@ -68,7 +68,7 @@ impl TryFrom for Confi } } -// TODO XC-342: Set `transactions`, `signature`, `rewards` and `num_reward_partitions` fields. +// TODO XC-342: Set `transactions`, `signatures`, `rewards` and `num_reward_partitions` fields. impl From for solana_transaction_status_client_types::UiConfirmedBlock { fn from(block: ConfirmedBlock) -> Self { Self { diff --git a/libs/types/src/solana/request/mod.rs b/libs/types/src/solana/request/mod.rs index d3428e5d..edac1e3e 100644 --- a/libs/types/src/solana/request/mod.rs +++ b/libs/types/src/solana/request/mod.rs @@ -174,10 +174,10 @@ impl From for GetBlockParams { /// are generally too large to be supported by the ICP. #[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, CandidType)] pub enum TransactionDetails { - /// Includes transaction signature (IDs) and block metadata only. - #[serde(rename = "signature")] + /// Includes transaction signatures (IDs) and block metadata only. + #[serde(rename = "signatures")] Signatures, - /// Omits all transaction data and signature; returns only block metadata. + /// Omits all transaction data and signatures; returns only block metadata. #[default] #[serde(rename = "none")] None, @@ -216,9 +216,9 @@ impl From for Vec { /// The parameters for a Solana [`getSignatureStatuses`](https://solana.com/docs/rpc/http/getsignaturestatuses) RPC method call. #[derive(Debug, Clone, Default, Deserialize, Serialize, CandidType)] pub struct GetSignatureStatusesParams { - /// An array of transaction signature to confirm, as base-58 encoded strings (up to a maximum of 256) + /// An array of transaction signatures to confirm, as base-58 encoded strings (up to a maximum of 256) pub signatures: VecWithMaxLen, - /// If set to true, a Solana node will search its ledger cache for any signature not found in the recent status cache. + /// If set to true, a Solana node will search its ledger cache for any signatures not found in the recent status cache. #[serde(rename = "searchTransactionHistory")] pub search_transaction_history: Option, } diff --git a/libs/types/src/solana/request/tests.rs b/libs/types/src/solana/request/tests.rs index e1975cb9..568486e6 100644 --- a/libs/types/src/solana/request/tests.rs +++ b/libs/types/src/solana/request/tests.rs @@ -8,7 +8,7 @@ mod get_signature_statuses_params_tests { #[test] fn should_deserialize() { let params = json!({ - "signature": vec!["5iBbqBJzgqafuQn93Np8ztWyXeYe2ReGPzUB1zXP2suZ8b5EaxSwe74ZUhg5pZQuDQkNGW7XApgfXX91YLYUuo5y"; 256] + "signatures": vec!["5iBbqBJzgqafuQn93Np8ztWyXeYe2ReGPzUB1zXP2suZ8b5EaxSwe74ZUhg5pZQuDQkNGW7XApgfXX91YLYUuo5y"; 256] }); let result = GetSignatureStatusesParams::deserialize(¶ms); @@ -19,7 +19,7 @@ mod get_signature_statuses_params_tests { #[test] fn should_not_deserialize() { let params = json!({ - "signature": vec!["5iBbqBJzgqafuQn93Np8ztWyXeYe2ReGPzUB1zXP2suZ8b5EaxSwe74ZUhg5pZQuDQkNGW7XApgfXX91YLYUuo5y"; 256 + 1] + "signatures": vec!["5iBbqBJzgqafuQn93Np8ztWyXeYe2ReGPzUB1zXP2suZ8b5EaxSwe74ZUhg5pZQuDQkNGW7XApgfXX91YLYUuo5y"; 256 + 1] }); let result = GetSignatureStatusesParams::deserialize(¶ms); From ad6a89238ba1c429e3a6c13bc6780cce97ddb696 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Thu, 22 May 2025 09:20:41 +0200 Subject: [PATCH 12/21] XC-317: Revert aggressive refactoring from IntelliJ --- canister/scripts/examples.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/canister/scripts/examples.sh b/canister/scripts/examples.sh index 7176d5b8..d28aaf37 100755 --- a/canister/scripts/examples.sh +++ b/canister/scripts/examples.sh @@ -61,8 +61,8 @@ GET_BLOCK_PARAMS="( )" CYCLES=$(dfx canister call sol_rpc getBlockCyclesCost "$GET_BLOCK_PARAMS" $FLAGS --output json | jq '.Ok' --raw-output || exit 1) GET_BLOCK_OUTPUT=$(dfx canister call sol_rpc getBlock "$GET_BLOCK_PARAMS" $FLAGS --output json --with-cycles "$CYCLES" || exit 1) -FIRST_SIGNATURE=$(jq --raw-output '.Consistent.Ok[0].signature[0][0]' <<< "$GET_BLOCK_OUTPUT") -SECOND_SIGNATURE=$(jq --raw-output '.Consistent.Ok[0].signature[0][1]' <<< "$GET_BLOCK_OUTPUT") +FIRST_SIGNATURE=$(jq --raw-output '.Consistent.Ok[0].signatures[0][0]' <<< "$GET_BLOCK_OUTPUT") +SECOND_SIGNATURE=$(jq --raw-output '.Consistent.Ok[0].signatures[0][1]' <<< "$GET_BLOCK_OUTPUT") # Fetch the statuses of the first two transactions in the received block GET_SIGNATURE_STATUSES_PARAMS="( From e1d5f2dcee73fe4dd58cda3e7393db2288bc7312 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Thu, 22 May 2025 09:38:20 +0200 Subject: [PATCH 13/21] XC-317: Remove Ed25519 and Derivation path from sol_rpc_types crate --- examples/basic_solana/src/ed25519.rs | 2 +- examples/basic_solana/src/lib.rs | 20 +++++++- examples/basic_solana/src/solana_wallet.rs | 4 +- examples/basic_solana/src/state.rs | 11 +++-- examples/basic_solana/tests/tests.rs | 6 +-- libs/client/src/threshold_sig.rs | 54 +++++++++++++++++++++- libs/types/src/solana/signature/mod.rs | 52 --------------------- 7 files changed, 85 insertions(+), 64 deletions(-) diff --git a/examples/basic_solana/src/ed25519.rs b/examples/basic_solana/src/ed25519.rs index 5236f67e..df0c0def 100644 --- a/examples/basic_solana/src/ed25519.rs +++ b/examples/basic_solana/src/ed25519.rs @@ -1,6 +1,6 @@ use ic_ed25519::PublicKey; +use sol_rpc_client::threshold_sig::{DerivationPath, Ed25519KeyId}; use sol_rpc_client::IcRuntime; -use sol_rpc_types::{DerivationPath, Ed25519KeyId}; #[derive(Clone, Debug, PartialEq, Eq)] pub struct Ed25519ExtendedPublicKey { diff --git a/examples/basic_solana/src/lib.rs b/examples/basic_solana/src/lib.rs index 82bcf14b..e89a730a 100644 --- a/examples/basic_solana/src/lib.rs +++ b/examples/basic_solana/src/lib.rs @@ -6,7 +6,7 @@ pub mod state; use crate::state::{read_state, State}; use candid::{CandidType, Deserialize, Principal}; use sol_rpc_client::{IcRuntime, SolRpcClient}; -use sol_rpc_types::{CommitmentLevel, Ed25519KeyId, MultiRpcResult, RpcSources, SolanaCluster}; +use sol_rpc_types::{CommitmentLevel, MultiRpcResult, RpcSources, SolanaCluster}; use solana_hash::Hash; use std::str::FromStr; @@ -90,6 +90,24 @@ impl From for SolanaCluster { } } +#[derive(CandidType, Deserialize, Debug, Default, PartialEq, Eq, Clone, Copy)] +pub enum Ed25519KeyId { + #[default] + TestKeyLocalDevelopment, + TestKey1, + ProductionKey1, +} + +impl From for sol_rpc_client::threshold_sig::Ed25519KeyId { + fn from(key_id: Ed25519KeyId) -> Self { + match key_id { + Ed25519KeyId::TestKeyLocalDevelopment => Self::TestKeyLocalDevelopment, + Ed25519KeyId::TestKey1 => Self::TestKey1, + Ed25519KeyId::ProductionKey1 => Self::ProductionKey1, + } + } +} + pub fn validate_caller_not_anonymous() -> Principal { let principal = ic_cdk::caller(); if principal == Principal::anonymous() { diff --git a/examples/basic_solana/src/solana_wallet.rs b/examples/basic_solana/src/solana_wallet.rs index b73fb49c..c5ece3e3 100644 --- a/examples/basic_solana/src/solana_wallet.rs +++ b/examples/basic_solana/src/solana_wallet.rs @@ -9,8 +9,8 @@ use crate::{ state::{lazy_call_ed25519_public_key, read_state}, }; use candid::Principal; +use sol_rpc_client::threshold_sig::DerivationPath; use sol_rpc_client::{threshold_sig::sign_transaction, IcRuntime}; -use sol_rpc_types::DerivationPath; use solana_message::Message; use solana_pubkey::Pubkey; use solana_signature::Signature; @@ -92,7 +92,7 @@ impl SolanaWallet { sign_transaction( &IcRuntime, &Transaction::new_unsigned(message.clone()), - read_state(|s| s.ed25519_key_id()), + read_state(|s| s.ed25519_key_id()).into(), Some(&signer.derivation_path), ) .await diff --git a/examples/basic_solana/src/state.rs b/examples/basic_solana/src/state.rs index f03186ce..70fb1fa0 100644 --- a/examples/basic_solana/src/state.rs +++ b/examples/basic_solana/src/state.rs @@ -1,9 +1,9 @@ use crate::{ ed25519::{get_ed25519_public_key, Ed25519ExtendedPublicKey}, - InitArg, SolanaNetwork, + Ed25519KeyId, InitArg, SolanaNetwork, }; use candid::Principal; -use sol_rpc_types::{CommitmentLevel, Ed25519KeyId}; +use sol_rpc_types::CommitmentLevel; use std::{ cell::RefCell, ops::{Deref, DerefMut}, @@ -71,8 +71,11 @@ pub async fn lazy_call_ed25519_public_key() -> Ed25519ExtendedPublicKey { if let Some(public_key) = read_state(|s| s.ed25519_public_key.clone()) { return public_key; } - let public_key = - get_ed25519_public_key(read_state(|s| s.ed25519_key_id()), &Default::default()).await; + let public_key = get_ed25519_public_key( + read_state(|s| s.ed25519_key_id()).into(), + &Default::default(), + ) + .await; mutate_state(|s| s.ed25519_public_key = Some(public_key.clone())); public_key } diff --git a/examples/basic_solana/tests/tests.rs b/examples/basic_solana/tests/tests.rs index b1b4a73e..d770f794 100644 --- a/examples/basic_solana/tests/tests.rs +++ b/examples/basic_solana/tests/tests.rs @@ -1,4 +1,4 @@ -use basic_solana::SolanaNetwork; +use basic_solana::{Ed25519KeyId, SolanaNetwork}; use candid::{ decode_args, encode_args, utils::ArgumentEncoder, CandidType, Encode, Nat, Principal, }; @@ -8,8 +8,8 @@ use pocket_ic::{ }; use serde::de::DeserializeOwned; use sol_rpc_types::{ - CommitmentLevel, Ed25519KeyId, OverrideProvider, RegexSubstitution, RpcAccess, - SupportedRpcProvider, SupportedRpcProviderId, TokenAmount, + CommitmentLevel, OverrideProvider, RegexSubstitution, RpcAccess, SupportedRpcProvider, + SupportedRpcProviderId, TokenAmount, }; use solana_client::{rpc_client::RpcClient as SolanaRpcClient, rpc_config::RpcTransactionConfig}; use solana_commitment_config::CommitmentConfig; diff --git a/libs/client/src/threshold_sig.rs b/libs/client/src/threshold_sig.rs index 7620ad44..33e5db47 100644 --- a/libs/client/src/threshold_sig.rs +++ b/libs/client/src/threshold_sig.rs @@ -6,17 +6,69 @@ pub use crate::request::{Request, RequestBuilder, SolRpcEndpoint, SolRpcRequest}; use crate::Runtime; use candid::Principal; +use derive_more::{From, Into}; use ic_cdk::api::management_canister::schnorr::{ SchnorrAlgorithm, SchnorrKeyId, SignWithSchnorrArgument, SignWithSchnorrResponse, }; use ic_cdk::api::management_canister::schnorr::{ SchnorrPublicKeyArgument, SchnorrPublicKeyResponse, }; -use sol_rpc_types::{DerivationPath, Ed25519KeyId, RpcError, RpcResult}; +use sol_rpc_types::{RpcError, RpcResult}; +use std::fmt::Display; // Source: https://internetcomputer.org/docs/current/references/t-sigs-how-it-works/#fees-for-the-t-schnorr-production-key const SIGN_WITH_SCHNORR_FEE: u128 = 26_153_846_153; +/// Represents the derivation path of an Ed25519 key from one of the root keys. +/// See the [tEdDSA documentation](https://internetcomputer.org/docs/building-apps/network-features/signatures/t-schnorr#signing-messages-and-transactions) +/// for more details. +#[derive(Clone, Debug, PartialEq, Eq, Default, From, Into)] +pub struct DerivationPath(Vec>); + +impl From<&[&[u8]]> for DerivationPath { + fn from(bytes: &[&[u8]]) -> Self { + Self(bytes.iter().map(|index| index.to_vec()).collect()) + } +} + +impl From<&[u8]> for DerivationPath { + fn from(bytes: &[u8]) -> Self { + Self(vec![bytes.to_vec()]) + } +} + +impl From for DerivationPath { + fn from(principal: Principal) -> Self { + DerivationPath::from(principal.as_slice()) + } +} + +/// The ID of one of the ICP root keys. +/// See the [tEdDSA documentation](https://internetcomputer.org/docs/building-apps/network-features/signatures/t-schnorr#signing-messages-and-transactions) +/// for more details. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] +pub enum Ed25519KeyId { + /// Only available on the local development environment started by dfx. + #[default] + TestKeyLocalDevelopment, + /// Test key available on the ICP mainnet. + TestKey1, + /// Production key available on the ICP mainnet. + ProductionKey1, +} + +impl Display for Ed25519KeyId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str = match self { + Ed25519KeyId::TestKeyLocalDevelopment => "dfx_test_key", + Ed25519KeyId::TestKey1 => "test_key_1", + Ed25519KeyId::ProductionKey1 => "key_1", + } + .to_string(); + write!(f, "{}", str) + } +} + /// Sign an unsigned Solana transaction with threshold EdDSA, see threshold Schnorr documentation /// [here](https://internetcomputer.org/docs/building-apps/network-features/signatures/t-schnorr). /// diff --git a/libs/types/src/solana/signature/mod.rs b/libs/types/src/solana/signature/mod.rs index ba9a52c4..8b137891 100644 --- a/libs/types/src/solana/signature/mod.rs +++ b/libs/types/src/solana/signature/mod.rs @@ -1,53 +1 @@ -use candid::{CandidType, Deserialize, Principal}; -use derive_more::{From, Into}; -use std::fmt::Display; -/// Represents the derivation path of an Ed25519 key from one of the root keys. -/// See the [tEdDSA documentation](https://internetcomputer.org/docs/building-apps/network-features/signatures/t-schnorr#signing-messages-and-transactions) -/// for more details. -#[derive(Clone, Debug, PartialEq, Eq, Default, From, Into)] -pub struct DerivationPath(Vec>); - -impl From<&[&[u8]]> for DerivationPath { - fn from(bytes: &[&[u8]]) -> Self { - Self(bytes.iter().map(|index| index.to_vec()).collect()) - } -} - -impl From<&[u8]> for DerivationPath { - fn from(bytes: &[u8]) -> Self { - Self(vec![bytes.to_vec()]) - } -} - -impl From for DerivationPath { - fn from(principal: Principal) -> Self { - DerivationPath::from(principal.as_slice()) - } -} - -/// The ID of one of the ICP root keys. -/// See the [tEdDSA documentation](https://internetcomputer.org/docs/building-apps/network-features/signatures/t-schnorr#signing-messages-and-transactions) -/// for more details. -#[derive(CandidType, Deserialize, Debug, Default, PartialEq, Eq, Clone, Copy)] -pub enum Ed25519KeyId { - /// Only available on the local development environment started by dfx. - #[default] - TestKeyLocalDevelopment, - /// Test key available on the ICP mainnet. - TestKey1, - /// Production key available on the ICP mainnet. - ProductionKey1, -} - -impl Display for Ed25519KeyId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let str = match self { - Ed25519KeyId::TestKeyLocalDevelopment => "dfx_test_key", - Ed25519KeyId::TestKey1 => "test_key_1", - Ed25519KeyId::ProductionKey1 => "key_1", - } - .to_string(); - write!(f, "{}", str) - } -} From d9d3403450446b090c132e74c67164048c3e24bf Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Thu, 22 May 2025 09:39:04 +0200 Subject: [PATCH 14/21] XC-317: Revert aggressive refactoring from IntelliJ --- libs/client/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/client/src/lib.rs b/libs/client/src/lib.rs index b1505c4b..dd2c632f 100644 --- a/libs/client/src/lib.rs +++ b/libs/client/src/lib.rs @@ -602,9 +602,9 @@ impl SolRpcClient { /// /// # Errors /// - /// The number of signature that can be passed to + /// The number of signatures that can be passed to /// [`getSignatureStatuses`](https://solana.com/de/docs/rpc/http/getsignaturestatuses) - /// is limited to 256. More signature result in an error. + /// is limited to 256. More signatures result in an error. /// /// ```rust /// use std::{str::FromStr, collections::BTreeSet}; From f533b56792cdacf3e7f04ec5c4ad5d04ba509a16 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Thu, 22 May 2025 10:28:12 +0200 Subject: [PATCH 15/21] XC-317: Add threshold_sig feature flag --- examples/basic_solana/Cargo.toml | 6 +++--- libs/client/Cargo.toml | 22 ++++++++++++++++------ libs/client/src/lib.rs | 1 + libs/client/src/threshold_sig.rs | 22 ++++++++++++---------- 4 files changed, 32 insertions(+), 19 deletions(-) diff --git a/examples/basic_solana/Cargo.toml b/examples/basic_solana/Cargo.toml index ede830c7..b3ff2f4d 100644 --- a/examples/basic_solana/Cargo.toml +++ b/examples/basic_solana/Cargo.toml @@ -19,7 +19,7 @@ ic-cdk = { workspace = true } ic-ed25519 = { workspace = true } num = { workspace = true } serde = { workspace = true, features = ["derive"] } -sol_rpc_client = { path = "../../libs/client" } +sol_rpc_client = { path = "../../libs/client", features = ["threshold_sig"] } sol_rpc_types = { path = "../../libs/types" } solana-account-decoder-client-types = { workspace = true } solana-hash = { workspace = true } @@ -39,5 +39,5 @@ pocket-ic = { workspace = true } solana-client = { workspace = true } solana-commitment-config = { workspace = true } solana-keypair = { workspace = true } -solana-rpc-client-nonce-utils = {workspace = true} -solana-signer = {workspace = true} +solana-rpc-client-nonce-utils = { workspace = true } +solana-signer = { workspace = true } diff --git a/libs/client/Cargo.toml b/libs/client/Cargo.toml index 8d0800f8..66835939 100644 --- a/libs/client/Cargo.toml +++ b/libs/client/Cargo.toml @@ -15,24 +15,34 @@ async-trait = { workspace = true } candid = { workspace = true } derive_more = { workspace = true } ic-cdk = { workspace = true } -ic-ed25519 = { workspace = true } +ic-ed25519 = { workspace = true, optional = true } serde = { workspace = true } serde_json = { workspace = true } solana-account = { workspace = true } solana-account-decoder-client-types = { workspace = true } -solana-hash = { workspace = true } +solana-hash = { workspace = true, optional = true } solana-instruction = { workspace = true } -solana-keypair = { workspace = true } -solana-message = { workspace = true } -solana-program = { workspace = true } +solana-keypair = { workspace = true, optional = true } +solana-message = { workspace = true, optional = true } +solana-program = { workspace = true, optional = true } solana-pubkey = { workspace = true } solana-signature = { workspace = true } -solana-transaction = { workspace = true } +solana-transaction = { workspace = true, optional = true } solana-transaction-error = { workspace = true } solana-transaction-status-client-types = { workspace = true } sol_rpc_types = { version = "0.1.0", path = "../types" } strum = { workspace = true } +[features] +threshold_sig = [ + "ic-ed25519", + "solana-hash", + "solana-keypair", + "solana-message", + "solana-program", + "solana-transaction", +] + [dev-dependencies] assert_matches = { workspace = true } tokio = { workspace = true, features = ["full"] } diff --git a/libs/client/src/lib.rs b/libs/client/src/lib.rs index dd2c632f..ee3231fa 100644 --- a/libs/client/src/lib.rs +++ b/libs/client/src/lib.rs @@ -121,6 +121,7 @@ #[cfg(not(target_arch = "wasm32"))] pub mod fixtures; mod request; +#[cfg(feature = "threshold_sig")] pub mod threshold_sig; use crate::request::{ diff --git a/libs/client/src/threshold_sig.rs b/libs/client/src/threshold_sig.rs index 33e5db47..ce54a2fa 100644 --- a/libs/client/src/threshold_sig.rs +++ b/libs/client/src/threshold_sig.rs @@ -16,8 +16,10 @@ use ic_cdk::api::management_canister::schnorr::{ use sol_rpc_types::{RpcError, RpcResult}; use std::fmt::Display; +// Source: https://internetcomputer.org/docs/current/references/t-sigs-how-it-works/#fees-for-the-t-schnorr-test-key +const SIGN_WITH_SCHNORR_TEST_FEE: u128 = 10_000_000_000; // Source: https://internetcomputer.org/docs/current/references/t-sigs-how-it-works/#fees-for-the-t-schnorr-production-key -const SIGN_WITH_SCHNORR_FEE: u128 = 26_153_846_153; +const SIGN_WITH_SCHNORR_PRODUCTION_FEE: u128 = 26_153_846_153; /// Represents the derivation path of an Ed25519 key from one of the root keys. /// See the [tEdDSA documentation](https://internetcomputer.org/docs/building-apps/network-features/signatures/t-schnorr#signing-messages-and-transactions) @@ -46,10 +48,9 @@ impl From for DerivationPath { /// The ID of one of the ICP root keys. /// See the [tEdDSA documentation](https://internetcomputer.org/docs/building-apps/network-features/signatures/t-schnorr#signing-messages-and-transactions) /// for more details. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Ed25519KeyId { - /// Only available on the local development environment started by dfx. - #[default] + /// Only available on the local development environment started by `dfx`. TestKeyLocalDevelopment, /// Test key available on the ICP mainnet. TestKey1, @@ -82,7 +83,6 @@ impl Display for Ed25519KeyId { /// use solana_signature::Signature; /// use solana_transaction::Transaction; /// use sol_rpc_client::{threshold_sig , IcRuntime}; -/// use sol_rpc_types::Ed25519KeyId; /// /// #[tokio::main] /// # async fn main() -> Result<(), Box> { @@ -96,7 +96,7 @@ impl Display for Ed25519KeyId { /// # chain_code: "UWbC6EgDnWEJIU4KFBqASTCYAzEiJGsR".as_bytes().to_vec(), /// # }); /// -/// let key_id = Ed25519KeyId::TestKey1; +/// let key_id = threshold_sig::Ed25519KeyId::TestKey1; /// let derivation_path = None; /// let (payer, _) = threshold_sig::get_pubkey( /// &runtime, @@ -158,7 +158,10 @@ pub async fn sign_transaction( Principal::management_canister(), "sign_with_schnorr", (arg,), - SIGN_WITH_SCHNORR_FEE, + match key_id { + Ed25519KeyId::TestKeyLocalDevelopment | Ed25519KeyId::TestKey1 => SIGN_WITH_SCHNORR_TEST_FEE, + Ed25519KeyId::ProductionKey1 => SIGN_WITH_SCHNORR_PRODUCTION_FEE, + }, ) .await .map_err(|(rejection_code, message)| { @@ -183,7 +186,6 @@ pub async fn sign_transaction( /// use candid::Principal; /// use solana_pubkey::pubkey; /// use sol_rpc_client::{threshold_sig, IcRuntime}; -/// use sol_rpc_types::{DerivationPath, Ed25519KeyId}; /// /// #[tokio::main] /// # async fn main() -> Result<(), Box> { @@ -195,9 +197,9 @@ pub async fn sign_transaction( /// # chain_code: "UWbC6EgDnWEJIU4KFBqASTCYAzEiJGsR".as_bytes().to_vec(), /// # }); /// -/// let key_id = Ed25519KeyId::TestKey1; +/// let key_id = threshold_sig::Ed25519KeyId::TestKey1; /// let canister_id = Principal::from_text("un4fu-tqaaa-aaaab-qadjq-cai").unwrap(); -/// let derivation_path = DerivationPath::from("some-derivation-path".as_bytes()); +/// let derivation_path = threshold_sig::DerivationPath::from("some-derivation-path".as_bytes()); /// /// let (pubkey, _) = threshold_sig::get_pubkey( /// &runtime, From 68a0a512c10b0dd2e9cb16e78ecd2de5a35f56af Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Thu, 22 May 2025 10:36:58 +0200 Subject: [PATCH 16/21] XC-317: Remove sol_rpc_types::signature module --- examples/basic_solana/src/ed25519.rs | 6 ++++-- examples/basic_solana/src/solana_wallet.rs | 6 ++++-- libs/client/src/threshold_sig.rs | 6 ++---- libs/types/src/lib.rs | 1 - libs/types/src/solana/mod.rs | 1 - libs/types/src/solana/signature/mod.rs | 1 - 6 files changed, 10 insertions(+), 11 deletions(-) delete mode 100644 libs/types/src/solana/signature/mod.rs diff --git a/examples/basic_solana/src/ed25519.rs b/examples/basic_solana/src/ed25519.rs index df0c0def..1fbf1a8f 100644 --- a/examples/basic_solana/src/ed25519.rs +++ b/examples/basic_solana/src/ed25519.rs @@ -1,6 +1,8 @@ use ic_ed25519::PublicKey; -use sol_rpc_client::threshold_sig::{DerivationPath, Ed25519KeyId}; -use sol_rpc_client::IcRuntime; +use sol_rpc_client::{ + threshold_sig::{DerivationPath, Ed25519KeyId}, + IcRuntime, +}; #[derive(Clone, Debug, PartialEq, Eq)] pub struct Ed25519ExtendedPublicKey { diff --git a/examples/basic_solana/src/solana_wallet.rs b/examples/basic_solana/src/solana_wallet.rs index c5ece3e3..71c805e9 100644 --- a/examples/basic_solana/src/solana_wallet.rs +++ b/examples/basic_solana/src/solana_wallet.rs @@ -9,8 +9,10 @@ use crate::{ state::{lazy_call_ed25519_public_key, read_state}, }; use candid::Principal; -use sol_rpc_client::threshold_sig::DerivationPath; -use sol_rpc_client::{threshold_sig::sign_transaction, IcRuntime}; +use sol_rpc_client::{ + threshold_sig::{sign_transaction, DerivationPath}, + IcRuntime, +}; use solana_message::Message; use solana_pubkey::Pubkey; use solana_signature::Signature; diff --git a/libs/client/src/threshold_sig.rs b/libs/client/src/threshold_sig.rs index ce54a2fa..966d680f 100644 --- a/libs/client/src/threshold_sig.rs +++ b/libs/client/src/threshold_sig.rs @@ -8,10 +8,8 @@ use crate::Runtime; use candid::Principal; use derive_more::{From, Into}; use ic_cdk::api::management_canister::schnorr::{ - SchnorrAlgorithm, SchnorrKeyId, SignWithSchnorrArgument, SignWithSchnorrResponse, -}; -use ic_cdk::api::management_canister::schnorr::{ - SchnorrPublicKeyArgument, SchnorrPublicKeyResponse, + SchnorrAlgorithm, SchnorrKeyId, SchnorrPublicKeyArgument, SchnorrPublicKeyResponse, + SignWithSchnorrArgument, SignWithSchnorrResponse, }; use sol_rpc_types::{RpcError, RpcResult}; use std::fmt::Display; diff --git a/libs/types/src/lib.rs b/libs/types/src/lib.rs index 378614e2..d1c6bb45 100644 --- a/libs/types/src/lib.rs +++ b/libs/types/src/lib.rs @@ -31,7 +31,6 @@ pub use solana::{ GetTransactionEncoding, GetTransactionParams, SendTransactionEncoding, SendTransactionParams, TransactionDetails, }, - signature::{DerivationPath, Ed25519KeyId}, transaction::{ error::{InstructionError, TransactionError}, instruction::{CompiledInstruction, InnerInstructions, Instruction}, diff --git a/libs/types/src/solana/mod.rs b/libs/types/src/solana/mod.rs index 26fd665a..081af6c3 100644 --- a/libs/types/src/solana/mod.rs +++ b/libs/types/src/solana/mod.rs @@ -3,7 +3,6 @@ mod tests; pub mod account; pub mod request; -pub mod signature; pub mod transaction; use crate::RpcError; diff --git a/libs/types/src/solana/signature/mod.rs b/libs/types/src/solana/signature/mod.rs deleted file mode 100644 index 8b137891..00000000 --- a/libs/types/src/solana/signature/mod.rs +++ /dev/null @@ -1 +0,0 @@ - From 1aa55521a95de616ec45f09980aa3fdae7f2610e Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Thu, 22 May 2025 14:37:07 +0200 Subject: [PATCH 17/21] XC-317: Rename feature flag and basic_solana::Ed25519KeyId --- examples/basic_solana/Cargo.toml | 2 +- examples/basic_solana/basic_solana.did | 4 ++-- examples/basic_solana/src/ed25519.rs | 20 ++++++++++--------- examples/basic_solana/src/lib.rs | 16 +++++++-------- examples/basic_solana/src/solana_wallet.rs | 4 ++-- examples/basic_solana/src/state.rs | 15 ++++++-------- examples/basic_solana/tests/tests.rs | 4 ++-- libs/client/Cargo.toml | 2 +- .../src/{threshold_sig.rs => ed25519.rs} | 16 +++++++-------- libs/client/src/lib.rs | 4 ++-- 10 files changed, 43 insertions(+), 44 deletions(-) rename libs/client/src/{threshold_sig.rs => ed25519.rs} (95%) diff --git a/examples/basic_solana/Cargo.toml b/examples/basic_solana/Cargo.toml index b3ff2f4d..f73a4f7d 100644 --- a/examples/basic_solana/Cargo.toml +++ b/examples/basic_solana/Cargo.toml @@ -19,7 +19,7 @@ ic-cdk = { workspace = true } ic-ed25519 = { workspace = true } num = { workspace = true } serde = { workspace = true, features = ["derive"] } -sol_rpc_client = { path = "../../libs/client", features = ["threshold_sig"] } +sol_rpc_client = { path = "../../libs/client", features = ["ed25519"] } sol_rpc_types = { path = "../../libs/types" } solana-account-decoder-client-types = { workspace = true } solana-hash = { workspace = true } diff --git a/examples/basic_solana/basic_solana.did b/examples/basic_solana/basic_solana.did index 9be93ce5..bd00af70 100644 --- a/examples/basic_solana/basic_solana.did +++ b/examples/basic_solana/basic_solana.did @@ -7,7 +7,7 @@ type InitArg = record { solana_commitment_level : opt CommitmentLevel; // EdDSA keys will be derived from this key. // If not specified, the value is set to `TestKeyLocalDevelopment`. - ed25519_key_id : opt Ed25519KeyId; + ed25519_key_name : opt Ed25519KeyName; // The canister will interact with this SOL RPC canister. // If not specified, the value is set to `tghme-zyaaa-aaaar-qarca-cai`. sol_rpc_canister_id : opt principal; @@ -30,7 +30,7 @@ type CommitmentLevel = variant { finalized; }; -type Ed25519KeyId = variant { +type Ed25519KeyName = variant { // For local development with `dfx`. TestKeyLocalDevelopment; // For testing with the Internet Computer's test key. diff --git a/examples/basic_solana/src/ed25519.rs b/examples/basic_solana/src/ed25519.rs index 1fbf1a8f..c3f822a9 100644 --- a/examples/basic_solana/src/ed25519.rs +++ b/examples/basic_solana/src/ed25519.rs @@ -1,8 +1,6 @@ +use crate::Ed25519KeyName; use ic_ed25519::PublicKey; -use sol_rpc_client::{ - threshold_sig::{DerivationPath, Ed25519KeyId}, - IcRuntime, -}; +use sol_rpc_client::{ed25519::DerivationPath, IcRuntime}; #[derive(Clone, Debug, PartialEq, Eq)] pub struct Ed25519ExtendedPublicKey { @@ -29,13 +27,17 @@ impl Ed25519ExtendedPublicKey { } pub async fn get_ed25519_public_key( - key_id: Ed25519KeyId, + key_name: Ed25519KeyName, derivation_path: &DerivationPath, ) -> Ed25519ExtendedPublicKey { - let (pubkey, chain_code) = - sol_rpc_client::threshold_sig::get_pubkey(&IcRuntime, None, Some(derivation_path), key_id) - .await - .expect("Failed to fetch EdDSA public key"); + let (pubkey, chain_code) = sol_rpc_client::ed25519::get_pubkey( + &IcRuntime, + None, + Some(derivation_path), + key_name.into(), + ) + .await + .expect("Failed to fetch EdDSA public key"); Ed25519ExtendedPublicKey { public_key: PublicKey::deserialize_raw(&pubkey.to_bytes()).unwrap(), chain_code, diff --git a/examples/basic_solana/src/lib.rs b/examples/basic_solana/src/lib.rs index e89a730a..22ecf514 100644 --- a/examples/basic_solana/src/lib.rs +++ b/examples/basic_solana/src/lib.rs @@ -5,7 +5,7 @@ pub mod state; use crate::state::{read_state, State}; use candid::{CandidType, Deserialize, Principal}; -use sol_rpc_client::{IcRuntime, SolRpcClient}; +use sol_rpc_client::{ed25519::Ed25519KeyId, IcRuntime, SolRpcClient}; use sol_rpc_types::{CommitmentLevel, MultiRpcResult, RpcSources, SolanaCluster}; use solana_hash::Hash; use std::str::FromStr; @@ -68,7 +68,7 @@ pub fn client() -> SolRpcClient { pub struct InitArg { pub sol_rpc_canister_id: Option, pub solana_network: Option, - pub ed25519_key_id: Option, + pub ed25519_key_name: Option, pub solana_commitment_level: Option, } @@ -91,19 +91,19 @@ impl From for SolanaCluster { } #[derive(CandidType, Deserialize, Debug, Default, PartialEq, Eq, Clone, Copy)] -pub enum Ed25519KeyId { +pub enum Ed25519KeyName { #[default] TestKeyLocalDevelopment, TestKey1, ProductionKey1, } -impl From for sol_rpc_client::threshold_sig::Ed25519KeyId { - fn from(key_id: Ed25519KeyId) -> Self { +impl From for Ed25519KeyId { + fn from(key_id: Ed25519KeyName) -> Self { match key_id { - Ed25519KeyId::TestKeyLocalDevelopment => Self::TestKeyLocalDevelopment, - Ed25519KeyId::TestKey1 => Self::TestKey1, - Ed25519KeyId::ProductionKey1 => Self::ProductionKey1, + Ed25519KeyName::TestKeyLocalDevelopment => Self::TestKeyLocalDevelopment, + Ed25519KeyName::TestKey1 => Self::TestKey1, + Ed25519KeyName::ProductionKey1 => Self::ProductionKey1, } } } diff --git a/examples/basic_solana/src/solana_wallet.rs b/examples/basic_solana/src/solana_wallet.rs index 71c805e9..5069af78 100644 --- a/examples/basic_solana/src/solana_wallet.rs +++ b/examples/basic_solana/src/solana_wallet.rs @@ -10,7 +10,7 @@ use crate::{ }; use candid::Principal; use sol_rpc_client::{ - threshold_sig::{sign_transaction, DerivationPath}, + ed25519::{sign_transaction, DerivationPath}, IcRuntime, }; use solana_message::Message; @@ -94,7 +94,7 @@ impl SolanaWallet { sign_transaction( &IcRuntime, &Transaction::new_unsigned(message.clone()), - read_state(|s| s.ed25519_key_id()).into(), + read_state(|s| s.ed25519_key_name()).into(), Some(&signer.derivation_path), ) .await diff --git a/examples/basic_solana/src/state.rs b/examples/basic_solana/src/state.rs index 70fb1fa0..1ea262d1 100644 --- a/examples/basic_solana/src/state.rs +++ b/examples/basic_solana/src/state.rs @@ -1,6 +1,6 @@ use crate::{ ed25519::{get_ed25519_public_key, Ed25519ExtendedPublicKey}, - Ed25519KeyId, InitArg, SolanaNetwork, + Ed25519KeyName, InitArg, SolanaNetwork, }; use candid::Principal; use sol_rpc_types::CommitmentLevel; @@ -34,11 +34,11 @@ pub struct State { solana_network: SolanaNetwork, solana_commitment_level: CommitmentLevel, ed25519_public_key: Option, - ed25519_key_name: Ed25519KeyId, + ed25519_key_name: Ed25519KeyName, } impl State { - pub fn ed25519_key_id(&self) -> Ed25519KeyId { + pub fn ed25519_key_name(&self) -> Ed25519KeyName { self.ed25519_key_name } @@ -62,7 +62,7 @@ impl From for State { solana_network: init_arg.solana_network.unwrap_or_default(), solana_commitment_level: init_arg.solana_commitment_level.unwrap_or_default(), ed25519_public_key: None, - ed25519_key_name: init_arg.ed25519_key_id.unwrap_or_default(), + ed25519_key_name: init_arg.ed25519_key_name.unwrap_or_default(), } } } @@ -71,11 +71,8 @@ pub async fn lazy_call_ed25519_public_key() -> Ed25519ExtendedPublicKey { if let Some(public_key) = read_state(|s| s.ed25519_public_key.clone()) { return public_key; } - let public_key = get_ed25519_public_key( - read_state(|s| s.ed25519_key_id()).into(), - &Default::default(), - ) - .await; + let public_key = + get_ed25519_public_key(read_state(|s| s.ed25519_key_name()), &Default::default()).await; mutate_state(|s| s.ed25519_public_key = Some(public_key.clone())); public_key } diff --git a/examples/basic_solana/tests/tests.rs b/examples/basic_solana/tests/tests.rs index d770f794..4b1d12c3 100644 --- a/examples/basic_solana/tests/tests.rs +++ b/examples/basic_solana/tests/tests.rs @@ -1,4 +1,4 @@ -use basic_solana::{Ed25519KeyId, SolanaNetwork}; +use basic_solana::{Ed25519KeyName, SolanaNetwork}; use candid::{ decode_args, encode_args, utils::ArgumentEncoder, CandidType, Encode, Nat, Principal, }; @@ -292,7 +292,7 @@ impl Setup { let basic_solana_install_args = basic_solana::InitArg { sol_rpc_canister_id: Some(sol_rpc_canister_id), solana_network: Some(SolanaNetwork::Devnet), - ed25519_key_id: Some(Ed25519KeyId::ProductionKey1), + ed25519_key_name: Some(Ed25519KeyName::ProductionKey1), solana_commitment_level: Some(CommitmentLevel::Confirmed), }; env.install_canister( diff --git a/libs/client/Cargo.toml b/libs/client/Cargo.toml index 66835939..b44ffe97 100644 --- a/libs/client/Cargo.toml +++ b/libs/client/Cargo.toml @@ -34,7 +34,7 @@ sol_rpc_types = { version = "0.1.0", path = "../types" } strum = { workspace = true } [features] -threshold_sig = [ +ed25519 = [ "ic-ed25519", "solana-hash", "solana-keypair", diff --git a/libs/client/src/threshold_sig.rs b/libs/client/src/ed25519.rs similarity index 95% rename from libs/client/src/threshold_sig.rs rename to libs/client/src/ed25519.rs index 966d680f..76dff0fb 100644 --- a/libs/client/src/threshold_sig.rs +++ b/libs/client/src/ed25519.rs @@ -80,7 +80,7 @@ impl Display for Ed25519KeyId { /// use solana_pubkey::pubkey; /// use solana_signature::Signature; /// use solana_transaction::Transaction; -/// use sol_rpc_client::{threshold_sig , IcRuntime}; +/// use sol_rpc_client::{ed25519 , IcRuntime}; /// /// #[tokio::main] /// # async fn main() -> Result<(), Box> { @@ -94,9 +94,9 @@ impl Display for Ed25519KeyId { /// # chain_code: "UWbC6EgDnWEJIU4KFBqASTCYAzEiJGsR".as_bytes().to_vec(), /// # }); /// -/// let key_id = threshold_sig::Ed25519KeyId::TestKey1; +/// let key_id = ed25519::Ed25519KeyId::TestKey1; /// let derivation_path = None; -/// let (payer, _) = threshold_sig::get_pubkey( +/// let (payer, _) = ed25519::get_pubkey( /// &runtime, /// None, /// derivation_path, @@ -120,7 +120,7 @@ impl Display for Ed25519KeyId { /// # signature: Signature::from_str("37HbmunhjSC1xxnVsaFX2xaS8gYnb5JYiLy9B51Ky9Up69aF7Qra6dHSLMCaiurRYq3Y8ZxSVUwC5sntziWuhZee").unwrap().as_ref().to_vec(), /// # }); /// let mut transaction = Transaction::new_unsigned(message); -/// let signature = threshold_sig::sign_transaction( +/// let signature = ed25519::sign_transaction( /// &runtime, /// &transaction, /// key_id, @@ -183,7 +183,7 @@ pub async fn sign_transaction( /// ```rust /// use candid::Principal; /// use solana_pubkey::pubkey; -/// use sol_rpc_client::{threshold_sig, IcRuntime}; +/// use sol_rpc_client::{ed25519, IcRuntime}; /// /// #[tokio::main] /// # async fn main() -> Result<(), Box> { @@ -195,11 +195,11 @@ pub async fn sign_transaction( /// # chain_code: "UWbC6EgDnWEJIU4KFBqASTCYAzEiJGsR".as_bytes().to_vec(), /// # }); /// -/// let key_id = threshold_sig::Ed25519KeyId::TestKey1; +/// let key_id = ed25519::Ed25519KeyId::TestKey1; /// let canister_id = Principal::from_text("un4fu-tqaaa-aaaab-qadjq-cai").unwrap(); -/// let derivation_path = threshold_sig::DerivationPath::from("some-derivation-path".as_bytes()); +/// let derivation_path = ed25519::DerivationPath::from("some-derivation-path".as_bytes()); /// -/// let (pubkey, _) = threshold_sig::get_pubkey( +/// let (pubkey, _) = ed25519::get_pubkey( /// &runtime, /// Some(canister_id), /// Some(&derivation_path), diff --git a/libs/client/src/lib.rs b/libs/client/src/lib.rs index ee3231fa..4df8c02c 100644 --- a/libs/client/src/lib.rs +++ b/libs/client/src/lib.rs @@ -118,11 +118,11 @@ #![forbid(unsafe_code)] #![forbid(missing_docs)] +#[cfg(feature = "ed25519")] +pub mod ed25519; #[cfg(not(target_arch = "wasm32"))] pub mod fixtures; mod request; -#[cfg(feature = "threshold_sig")] -pub mod threshold_sig; use crate::request::{ GetAccountInfoRequest, GetBalanceRequest, GetBlockRequest, GetRecentPrioritizationFeesRequest, From db463f9137e62316170938c15740353760bd53ff Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Thu, 22 May 2025 15:56:05 +0200 Subject: [PATCH 18/21] XC-317: Various renamings from review feedback --- canister/src/rpc_client/sol_rpc/mod.rs | 3 +- examples/basic_solana/basic_solana.did | 6 +- examples/basic_solana/src/lib.rs | 12 +- examples/basic_solana/src/main.rs | 12 +- examples/basic_solana/src/solana_wallet.rs | 25 ++--- examples/basic_solana/tests/tests.rs | 2 +- .../tests/solana_test_validator.rs | 2 +- libs/client/src/ed25519.rs | 104 ++++++++++-------- libs/types/src/rpc_client/mod.rs | 9 +- libs/types/src/rpc_client/tests.rs | 9 +- 10 files changed, 99 insertions(+), 85 deletions(-) diff --git a/canister/src/rpc_client/sol_rpc/mod.rs b/canister/src/rpc_client/sol_rpc/mod.rs index fa3455b8..a2b86159 100644 --- a/canister/src/rpc_client/sol_rpc/mod.rs +++ b/canister/src/rpc_client/sol_rpc/mod.rs @@ -13,8 +13,7 @@ use serde_json::{from_slice, Value}; use sol_rpc_types::{PrioritizationFee, RoundingError}; use solana_clock::Slot; use solana_transaction_status_client_types::TransactionStatus; -use std::fmt::Debug; -use std::num::NonZeroU8; +use std::{fmt::Debug, num::NonZeroU8}; /// Describes a payload transformation to execute before passing the HTTP response to consensus. /// The purpose of these transformations is to ensure that the response encoding is deterministic diff --git a/examples/basic_solana/basic_solana.did b/examples/basic_solana/basic_solana.did index bd00af70..928b4af1 100644 --- a/examples/basic_solana/basic_solana.did +++ b/examples/basic_solana/basic_solana.did @@ -32,11 +32,11 @@ type CommitmentLevel = variant { type Ed25519KeyName = variant { // For local development with `dfx`. - TestKeyLocalDevelopment; + LocalDevelopment; // For testing with the Internet Computer's test key. - TestKey1; + MainnetTestKey1; // For running the canister in a production environment using the Internet Computer's production key. - ProductionKey1; + MainnetProdKey1; }; // Atomic unit of SOL, i.e., 1 SOL = 10^9 Lamports diff --git a/examples/basic_solana/src/lib.rs b/examples/basic_solana/src/lib.rs index 22ecf514..f39b5381 100644 --- a/examples/basic_solana/src/lib.rs +++ b/examples/basic_solana/src/lib.rs @@ -93,17 +93,17 @@ impl From for SolanaCluster { #[derive(CandidType, Deserialize, Debug, Default, PartialEq, Eq, Clone, Copy)] pub enum Ed25519KeyName { #[default] - TestKeyLocalDevelopment, - TestKey1, - ProductionKey1, + LocalDevelopment, + MainnetTestKey1, + MainnetProdKey1, } impl From for Ed25519KeyId { fn from(key_id: Ed25519KeyName) -> Self { match key_id { - Ed25519KeyName::TestKeyLocalDevelopment => Self::TestKeyLocalDevelopment, - Ed25519KeyName::TestKey1 => Self::TestKey1, - Ed25519KeyName::ProductionKey1 => Self::ProductionKey1, + Ed25519KeyName::LocalDevelopment => Self::LocalDevelopment, + Ed25519KeyName::MainnetTestKey1 => Self::MainnetTestKey1, + Ed25519KeyName::MainnetProdKey1 => Self::MainnetProdKey1, } } } diff --git a/examples/basic_solana/src/main.rs b/examples/basic_solana/src/main.rs index deff0b54..a22d8ede 100644 --- a/examples/basic_solana/src/main.rs +++ b/examples/basic_solana/src/main.rs @@ -156,8 +156,8 @@ pub async fn create_nonce_account(owner: Option) -> String { ); let signatures = vec![ - SolanaWallet::sign_message(&message, &payer).await, - SolanaWallet::sign_message(&message, &nonce_account).await, + payer.sign_message(&message).await, + nonce_account.sign_message(&message).await, ]; let transaction = Transaction { @@ -213,7 +213,7 @@ pub async fn create_associated_token_account( &get_recent_blockhash(&client).await, ); - let signatures = vec![SolanaWallet::sign_message(&message, &payer).await]; + let signatures = vec![payer.sign_message(&message).await]; let transaction = Transaction { message, signatures, @@ -252,7 +252,7 @@ pub async fn send_sol(owner: Option, to: String, amount: Nat) -> Stri Some(payer.as_ref()), &get_recent_blockhash(&client).await, ); - let signatures = vec![SolanaWallet::sign_message(&message, &payer).await]; + let signatures = vec![payer.sign_message(&message).await]; let transaction = Transaction { message, signatures, @@ -291,7 +291,7 @@ pub async fn send_sol_with_durable_nonce( let blockhash = Hash::from(get_nonce(Some(nonce_account.as_ref().into())).await); let message = Message::new_with_blockhash(instructions, Some(payer.as_ref()), &blockhash); - let signatures = vec![SolanaWallet::sign_message(&message, &payer).await]; + let signatures = vec![payer.sign_message(&message).await]; let transaction = Transaction { message, signatures, @@ -333,7 +333,7 @@ pub async fn send_spl_token( Some(payer.as_ref()), &get_recent_blockhash(&client).await, ); - let signatures = vec![SolanaWallet::sign_message(&message, &payer).await]; + let signatures = vec![payer.sign_message(&message).await]; let transaction = Transaction { message, signatures, diff --git a/examples/basic_solana/src/solana_wallet.rs b/examples/basic_solana/src/solana_wallet.rs index 5069af78..62e42723 100644 --- a/examples/basic_solana/src/solana_wallet.rs +++ b/examples/basic_solana/src/solana_wallet.rs @@ -10,13 +10,12 @@ use crate::{ }; use candid::Principal; use sol_rpc_client::{ - ed25519::{sign_transaction, DerivationPath}, + ed25519::{sign_message, DerivationPath}, IcRuntime, }; use solana_message::Message; use solana_pubkey::Pubkey; use solana_signature::Signature; -use solana_transaction::Transaction; use std::fmt::Display; #[derive(Clone)] @@ -40,6 +39,17 @@ impl SolanaAccount { derivation_path, } } + + pub async fn sign_message(&self, message: &Message) -> Signature { + sign_message( + &IcRuntime, + message, + read_state(|s| s.ed25519_key_name()).into(), + Some(&self.derivation_path), + ) + .await + .expect("Failed to sign transaction") + } } impl AsRef for SolanaAccount { @@ -89,15 +99,4 @@ impl SolanaWallet { .into(), ) } - - pub async fn sign_message(message: &Message, signer: &SolanaAccount) -> Signature { - sign_transaction( - &IcRuntime, - &Transaction::new_unsigned(message.clone()), - read_state(|s| s.ed25519_key_name()).into(), - Some(&signer.derivation_path), - ) - .await - .expect("Failed to sign transaction") - } } diff --git a/examples/basic_solana/tests/tests.rs b/examples/basic_solana/tests/tests.rs index 4b1d12c3..05a6de4e 100644 --- a/examples/basic_solana/tests/tests.rs +++ b/examples/basic_solana/tests/tests.rs @@ -292,7 +292,7 @@ impl Setup { let basic_solana_install_args = basic_solana::InitArg { sol_rpc_canister_id: Some(sol_rpc_canister_id), solana_network: Some(SolanaNetwork::Devnet), - ed25519_key_name: Some(Ed25519KeyName::ProductionKey1), + ed25519_key_name: Some(Ed25519KeyName::MainnetProdKey1), solana_commitment_level: Some(CommitmentLevel::Confirmed), }; env.install_canister( diff --git a/integration_tests/tests/solana_test_validator.rs b/integration_tests/tests/solana_test_validator.rs index 13fd155a..8312cb60 100644 --- a/integration_tests/tests/solana_test_validator.rs +++ b/integration_tests/tests/solana_test_validator.rs @@ -33,10 +33,10 @@ use solana_signature::Signature; use solana_signer::Signer; use solana_transaction::Transaction; use solana_transaction_status_client_types::UiTransactionEncoding; -use std::num::NonZeroU8; use std::{ future::Future, iter::zip, + num::NonZeroU8, str::FromStr, thread, thread::sleep, diff --git a/libs/client/src/ed25519.rs b/libs/client/src/ed25519.rs index 76dff0fb..1f8d3302 100644 --- a/libs/client/src/ed25519.rs +++ b/libs/client/src/ed25519.rs @@ -12,7 +12,6 @@ use ic_cdk::api::management_canister::schnorr::{ SignWithSchnorrArgument, SignWithSchnorrResponse, }; use sol_rpc_types::{RpcError, RpcResult}; -use std::fmt::Display; // Source: https://internetcomputer.org/docs/current/references/t-sigs-how-it-works/#fees-for-the-t-schnorr-test-key const SIGN_WITH_SCHNORR_TEST_FEE: u128 = 10_000_000_000; @@ -49,44 +48,47 @@ impl From for DerivationPath { #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Ed25519KeyId { /// Only available on the local development environment started by `dfx`. - TestKeyLocalDevelopment, + LocalDevelopment, /// Test key available on the ICP mainnet. - TestKey1, + MainnetTestKey1, /// Production key available on the ICP mainnet. - ProductionKey1, + MainnetProdKey1, } -impl Display for Ed25519KeyId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let str = match self { - Ed25519KeyId::TestKeyLocalDevelopment => "dfx_test_key", - Ed25519KeyId::TestKey1 => "test_key_1", - Ed25519KeyId::ProductionKey1 => "key_1", +impl Ed25519KeyId { + /// The string representation of a [`Ed25519KeyId`] used as an argument to threshold Schnorr + /// method calls such as `schnorr_public_key` or `sign_with_schnorr`. + pub fn id(&self) -> &'static str { + match self { + Ed25519KeyId::LocalDevelopment => "dfx_test_key", + Ed25519KeyId::MainnetTestKey1 => "test_key_1", + Ed25519KeyId::MainnetProdKey1 => "key_1", } - .to_string(); - write!(f, "{}", str) } } -/// Sign an unsigned Solana transaction with threshold EdDSA, see threshold Schnorr documentation +/// Sign a Solana message with threshold EdDSA, see threshold Schnorr documentation /// [here](https://internetcomputer.org/docs/building-apps/network-features/signatures/t-schnorr). /// /// # Examples /// /// ```rust +/// use candid::Principal; /// use solana_hash::Hash; /// use solana_message::legacy::Message; /// use solana_program::system_instruction::transfer; /// use solana_pubkey::pubkey; /// use solana_signature::Signature; /// use solana_transaction::Transaction; -/// use sol_rpc_client::{ed25519 , IcRuntime}; +/// use sol_rpc_client::{ +/// ed25519::{get_pubkey, sign_message, DerivationPath, Ed25519KeyId}, +/// IcRuntime +/// }; /// -/// #[tokio::main] +/// # #[tokio::main] /// # async fn main() -> Result<(), Box> { /// # use sol_rpc_client::fixtures::MockRuntime; /// # use std::str::FromStr; -/// use candid::Principal; /// # use ic_cdk::api::management_canister::schnorr::{SchnorrPublicKeyResponse, SignWithSchnorrResponse}; /// let runtime = IcRuntime; /// # let runtime = MockRuntime::same_response(SchnorrPublicKeyResponse { @@ -94,61 +96,64 @@ impl Display for Ed25519KeyId { /// # chain_code: "UWbC6EgDnWEJIU4KFBqASTCYAzEiJGsR".as_bytes().to_vec(), /// # }); /// -/// let key_id = ed25519::Ed25519KeyId::TestKey1; -/// let derivation_path = None; -/// let (payer, _) = ed25519::get_pubkey( +/// let key_id = Ed25519KeyId::MainnetTestKey1; +/// let derivation_path = DerivationPath::from( +/// Principal::from_text("vaupb-eqaaa-aaaai-qplka-cai").unwrap() +/// ); +/// let (payer, _) = get_pubkey( /// &runtime, /// None, -/// derivation_path, -/// key_id) +/// Some(&derivation_path), +/// key_id +/// ) /// .await /// .unwrap(); /// /// let recipient = pubkey!("BPebStjcgCPnWTK3FXZJ8KhqwNYLk9aubC9b4Cgqb6oE"); /// -/// // TODO XC-317: Use client method to fetch recent blockhash -/// let recent_blockhash = Hash::new_unique(); -/// +/// # // TODO XC-317: Use client method to fetch recent blockhash +/// # let recent_blockhash = Hash::default(); /// let message = Message::new_with_blockhash( /// &[transfer(&payer, &recipient, 1_000_000)], /// Some(&payer), /// &recent_blockhash, /// ); /// -/// /// # let runtime = MockRuntime::same_response(SignWithSchnorrResponse { /// # signature: Signature::from_str("37HbmunhjSC1xxnVsaFX2xaS8gYnb5JYiLy9B51Ky9Up69aF7Qra6dHSLMCaiurRYq3Y8ZxSVUwC5sntziWuhZee").unwrap().as_ref().to_vec(), /// # }); -/// let mut transaction = Transaction::new_unsigned(message); -/// let signature = ed25519::sign_transaction( +/// let signature = sign_message( /// &runtime, -/// &transaction, +/// &message, /// key_id, -/// derivation_path, -/// ).await; +/// Some(&derivation_path), +/// ) +/// .await; /// /// assert_eq!( /// signature, /// Ok(Signature::from_str("37HbmunhjSC1xxnVsaFX2xaS8gYnb5JYiLy9B51Ky9Up69aF7Qra6dHSLMCaiurRYq3Y8ZxSVUwC5sntziWuhZee").unwrap()) /// ); /// -/// // The transaction is now signed and can be submitted with the `sendTransaction` RPC method. -/// transaction.signatures = vec![signature.unwrap()]; +/// let transaction = Transaction { +/// message, +/// signatures: vec![signature.unwrap()], +/// }; /// # Ok(()) /// # } /// ``` -pub async fn sign_transaction( +pub async fn sign_message( runtime: &R, - transaction: &solana_transaction::Transaction, + message: &solana_message::Message, key_id: Ed25519KeyId, derivation_path: Option<&DerivationPath>, ) -> RpcResult { let arg = SignWithSchnorrArgument { - message: transaction.message_data(), + message: message.serialize(), derivation_path: derivation_path.cloned().unwrap_or_default().into(), key_id: SchnorrKeyId { algorithm: SchnorrAlgorithm::Ed25519, - name: key_id.to_string(), + name: key_id.id().to_string(), }, }; let response: SignWithSchnorrResponse = R::update_call( @@ -157,8 +162,8 @@ pub async fn sign_transaction( "sign_with_schnorr", (arg,), match key_id { - Ed25519KeyId::TestKeyLocalDevelopment | Ed25519KeyId::TestKey1 => SIGN_WITH_SCHNORR_TEST_FEE, - Ed25519KeyId::ProductionKey1 => SIGN_WITH_SCHNORR_PRODUCTION_FEE, + Ed25519KeyId::LocalDevelopment | Ed25519KeyId::MainnetTestKey1 => SIGN_WITH_SCHNORR_TEST_FEE, + Ed25519KeyId::MainnetProdKey1 => SIGN_WITH_SCHNORR_PRODUCTION_FEE, }, ) .await @@ -183,7 +188,10 @@ pub async fn sign_transaction( /// ```rust /// use candid::Principal; /// use solana_pubkey::pubkey; -/// use sol_rpc_client::{ed25519, IcRuntime}; +/// use sol_rpc_client::{ +/// ed25519::{get_pubkey, DerivationPath, Ed25519KeyId}, +/// IcRuntime +/// }; /// /// #[tokio::main] /// # async fn main() -> Result<(), Box> { @@ -195,11 +203,21 @@ pub async fn sign_transaction( /// # chain_code: "UWbC6EgDnWEJIU4KFBqASTCYAzEiJGsR".as_bytes().to_vec(), /// # }); /// -/// let key_id = ed25519::Ed25519KeyId::TestKey1; +/// let key_id = Ed25519KeyId::MainnetTestKey1; /// let canister_id = Principal::from_text("un4fu-tqaaa-aaaab-qadjq-cai").unwrap(); -/// let derivation_path = ed25519::DerivationPath::from("some-derivation-path".as_bytes()); +/// let derivation_path = DerivationPath::from( +/// Principal::from_text("vaupb-eqaaa-aaaai-qplka-cai").unwrap() +/// ); +/// let (payer, _) = get_pubkey( +/// &runtime, +/// None, +/// Some(&derivation_path), +/// key_id +/// ) +/// .await +/// .unwrap(); /// -/// let (pubkey, _) = ed25519::get_pubkey( +/// let (pubkey, _) = get_pubkey( /// &runtime, /// Some(canister_id), /// Some(&derivation_path), @@ -224,7 +242,7 @@ pub async fn get_pubkey( derivation_path: derivation_path.cloned().unwrap_or_default().into(), key_id: SchnorrKeyId { algorithm: SchnorrAlgorithm::Ed25519, - name: key_id.to_string(), + name: key_id.id().to_string(), }, }; let SchnorrPublicKeyResponse { diff --git a/libs/types/src/rpc_client/mod.rs b/libs/types/src/rpc_client/mod.rs index 21ebb5e8..f2e72b77 100644 --- a/libs/types/src/rpc_client/mod.rs +++ b/libs/types/src/rpc_client/mod.rs @@ -1,15 +1,16 @@ #[cfg(test)] mod tests; -use candid::types::{Serializer, Type, TypeInner}; -use candid::CandidType; +use candid::{ + types::{Serializer, Type, TypeInner}, + CandidType, +}; use derive_more::{From, Into}; use ic_cdk::api::call::RejectionCode; pub use ic_cdk::api::management_canister::http_request::HttpHeader; use regex::Regex; use serde::{Deserialize, Serialize}; -use std::fmt::Debug; -use std::num::TryFromIntError; +use std::{fmt::Debug, num::TryFromIntError}; use strum::Display; use thiserror::Error; diff --git a/libs/types/src/rpc_client/tests.rs b/libs/types/src/rpc_client/tests.rs index 8b2afecb..4d4ab4c3 100644 --- a/libs/types/src/rpc_client/tests.rs +++ b/libs/types/src/rpc_client/tests.rs @@ -1,7 +1,6 @@ use crate::{HttpHeader, RpcEndpoint}; use candid::{CandidType, Decode, Encode}; -use proptest::prelude::TestCaseError; -use proptest::prop_assert_eq; +use proptest::{prelude::TestCaseError, prop_assert_eq}; use serde::de::DeserializeOwned; #[test] @@ -32,8 +31,7 @@ fn should_contain_host_without_sensitive_information() { } mod rounding_error_tests { - use crate::rpc_client::tests::encode_decode_roundtrip; - use crate::RoundingError; + use crate::{rpc_client::tests::encode_decode_roundtrip, RoundingError}; use proptest::proptest; #[test] @@ -74,8 +72,7 @@ mod rounding_error_tests { } mod non_zero_u8 { - use crate::rpc_client::tests::encode_decode_roundtrip; - use crate::rpc_client::NonZeroU8; + use crate::rpc_client::{tests::encode_decode_roundtrip, NonZeroU8}; use candid::{Decode, Encode}; use proptest::proptest; From e823351bc9df82cdca0c72d0a33bb93532b12f76 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Thu, 22 May 2025 15:58:14 +0200 Subject: [PATCH 19/21] XC-317: Change schnorr_public_key call to update --- libs/client/src/ed25519.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/client/src/ed25519.rs b/libs/client/src/ed25519.rs index 1f8d3302..21751fb4 100644 --- a/libs/client/src/ed25519.rs +++ b/libs/client/src/ed25519.rs @@ -248,10 +248,11 @@ pub async fn get_pubkey( let SchnorrPublicKeyResponse { public_key, chain_code } = runtime - .query_call( + .update_call( Principal::management_canister(), "schnorr_public_key", (arg,), + 0, ) .await .map_err(|(rejection_code, message)| { From cf00eb941242aef054869d041084bd7d09fb025f Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Thu, 22 May 2025 17:19:19 +0200 Subject: [PATCH 20/21] XC-317: Don't use RpcResult for Ed25519 module methods --- libs/client/src/ed25519.rs | 59 +++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/libs/client/src/ed25519.rs b/libs/client/src/ed25519.rs index 21751fb4..5babe627 100644 --- a/libs/client/src/ed25519.rs +++ b/libs/client/src/ed25519.rs @@ -7,11 +7,11 @@ pub use crate::request::{Request, RequestBuilder, SolRpcEndpoint, SolRpcRequest} use crate::Runtime; use candid::Principal; use derive_more::{From, Into}; +use ic_cdk::api::call::RejectionCode; use ic_cdk::api::management_canister::schnorr::{ SchnorrAlgorithm, SchnorrKeyId, SchnorrPublicKeyArgument, SchnorrPublicKeyResponse, SignWithSchnorrArgument, SignWithSchnorrResponse, }; -use sol_rpc_types::{RpcError, RpcResult}; // Source: https://internetcomputer.org/docs/current/references/t-sigs-how-it-works/#fees-for-the-t-schnorr-test-key const SIGN_WITH_SCHNORR_TEST_FEE: u128 = 10_000_000_000; @@ -147,7 +147,7 @@ pub async fn sign_message( message: &solana_message::Message, key_id: Ed25519KeyId, derivation_path: Option<&DerivationPath>, -) -> RpcResult { +) -> Result { let arg = SignWithSchnorrArgument { message: message.serialize(), derivation_path: derivation_path.cloned().unwrap_or_default().into(), @@ -162,22 +162,20 @@ pub async fn sign_message( "sign_with_schnorr", (arg,), match key_id { - Ed25519KeyId::LocalDevelopment | Ed25519KeyId::MainnetTestKey1 => SIGN_WITH_SCHNORR_TEST_FEE, + Ed25519KeyId::LocalDevelopment | Ed25519KeyId::MainnetTestKey1 => { + SIGN_WITH_SCHNORR_TEST_FEE + } Ed25519KeyId::MainnetProdKey1 => SIGN_WITH_SCHNORR_PRODUCTION_FEE, }, ) - .await - .map_err(|(rejection_code, message)| { - RpcError::ValidationError(format!( - "Failed to sign transaction, management canister returned code {rejection_code:?}: {message}") - ) - })?; - solana_signature::Signature::try_from(response.signature).map_err(|bytes| { - RpcError::ValidationError(format!( + .await?; + match solana_signature::Signature::try_from(response.signature) { + Ok(signature) => Ok(signature), + Err(bytes) => panic!( "Expected signature to contain 64 bytes, got {} bytes", bytes.len() - )) - }) + ), + } } /// Fetch the Ed25519 public key for the key ID, given canister ID and derivation path, see threshold Schnorr @@ -236,7 +234,7 @@ pub async fn get_pubkey( canister_id: Option, derivation_path: Option<&DerivationPath>, key_id: Ed25519KeyId, -) -> RpcResult<(solana_pubkey::Pubkey, [u8; 32])> { +) -> Result<(solana_pubkey::Pubkey, [u8; 32]), (RejectionCode, String)> { let arg = SchnorrPublicKeyArgument { canister_id, derivation_path: derivation_path.cloned().unwrap_or_default().into(), @@ -246,7 +244,8 @@ pub async fn get_pubkey( }, }; let SchnorrPublicKeyResponse { - public_key, chain_code + public_key, + chain_code, } = runtime .update_call( Principal::management_canister(), @@ -254,20 +253,20 @@ pub async fn get_pubkey( (arg,), 0, ) - .await - .map_err(|(rejection_code, message)| { - RpcError::ValidationError(format!( - "Failed to fetch EdDSA public key, management canister returned code {rejection_code:?}: {message}") - ) - })?; - let pubkey = solana_pubkey::Pubkey::try_from(public_key.as_slice()).map_err(|e| { - RpcError::ValidationError(format!("Failed to parse bytes as public key: {e}")) - })?; - let chain_code = <[u8; 32]>::try_from(chain_code.as_slice()).map_err(|_| { - RpcError::ValidationError(format!( - "Expected chain code to contain 32 bytes but it contained {}", - chain_code.len() - )) - })?; + .await?; + let pubkey = match solana_pubkey::Pubkey::try_from(public_key) { + Ok(pubkey) => pubkey, + Err(bytes) => panic!( + "Expected public key to contain 32 bytes, got {} bytes", + bytes.len() + ), + }; + let chain_code = match <[u8; 32]>::try_from(chain_code) { + Ok(pubkey) => pubkey, + Err(bytes) => panic!( + "Expected chain code key to contain 32 bytes, got {} bytes", + bytes.len() + ), + }; Ok((pubkey, chain_code)) } From 5940dec529cc264b1bb2a0af7213db5d9e8c2f81 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Thu, 22 May 2025 17:29:27 +0200 Subject: [PATCH 21/21] XC-317: Format --- libs/client/src/ed25519.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/libs/client/src/ed25519.rs b/libs/client/src/ed25519.rs index 5babe627..a0755555 100644 --- a/libs/client/src/ed25519.rs +++ b/libs/client/src/ed25519.rs @@ -7,10 +7,12 @@ pub use crate::request::{Request, RequestBuilder, SolRpcEndpoint, SolRpcRequest} use crate::Runtime; use candid::Principal; use derive_more::{From, Into}; -use ic_cdk::api::call::RejectionCode; -use ic_cdk::api::management_canister::schnorr::{ - SchnorrAlgorithm, SchnorrKeyId, SchnorrPublicKeyArgument, SchnorrPublicKeyResponse, - SignWithSchnorrArgument, SignWithSchnorrResponse, +use ic_cdk::api::{ + call::RejectionCode, + management_canister::schnorr::{ + SchnorrAlgorithm, SchnorrKeyId, SchnorrPublicKeyArgument, SchnorrPublicKeyResponse, + SignWithSchnorrArgument, SignWithSchnorrResponse, + }, }; // Source: https://internetcomputer.org/docs/current/references/t-sigs-how-it-works/#fees-for-the-t-schnorr-test-key