Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
500f760
XC-317: Add client method to sign a transaction
lpahlavi May 19, 2025
814df47
XC-317: Fix doctest
lpahlavi May 19, 2025
879db3b
XC-317: Fix basic_solana Candid and doctest
lpahlavi May 19, 2025
8d2d6d0
XC-317: Remove TODO
lpahlavi May 19, 2025
b848a0a
XC-317: Fix Candid interface
lpahlavi May 19, 2025
a273b6d
XC-317: Response already parsed as tuple
lpahlavi May 19, 2025
5dca201
XC-317: Unnecessary tuple in docs
lpahlavi May 19, 2025
8abbf47
XC-317: Add method to fetch EdDSA public key
lpahlavi May 20, 2025
81f536b
XC-317: Clippy
lpahlavi May 20, 2025
f52b417
XC-317: Clippy
lpahlavi May 20, 2025
fc895e4
XC-317: Revert aggressive refactoring
lpahlavi May 20, 2025
ad6a892
XC-317: Revert aggressive refactoring from IntelliJ
lpahlavi May 22, 2025
e1d5f2d
XC-317: Remove Ed25519 and Derivation path from sol_rpc_types crate
lpahlavi May 22, 2025
d9d3403
XC-317: Revert aggressive refactoring from IntelliJ
lpahlavi May 22, 2025
f533b56
XC-317: Add threshold_sig feature flag
lpahlavi May 22, 2025
68a0a51
XC-317: Remove sol_rpc_types::signature module
lpahlavi May 22, 2025
1aa5552
XC-317: Rename feature flag and basic_solana::Ed25519KeyId
lpahlavi May 22, 2025
f619cf4
Merge branch 'main' into lpahlavi/XC-317-client-method-to-sign-transa…
lpahlavi May 22, 2025
db463f9
XC-317: Various renamings from review feedback
lpahlavi May 22, 2025
e823351
XC-317: Change schnorr_public_key call to update
lpahlavi May 22, 2025
cf00eb9
XC-317: Don't use RpcResult for Ed25519 module methods
lpahlavi May 22, 2025
5940dec
XC-317: Format
lpahlavi May 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion Cargo.lock

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

3 changes: 1 addition & 2 deletions canister/src/rpc_client/sol_rpc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions end_to_end_tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this change needed?

solana-pubkey = { workspace = true }
solana-signature = { workspace = true }
solana-signer = { workspace = true }
Expand Down
1 change: 1 addition & 0 deletions end_to_end_tests/tests/end_to_end.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
Expand Down
6 changes: 3 additions & 3 deletions examples/basic_solana/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ["ed25519"] }
sol_rpc_types = { path = "../../libs/types" }
solana-account-decoder-client-types = { workspace = true }
solana-hash = { workspace = true }
Expand All @@ -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 }
6 changes: 3 additions & 3 deletions examples/basic_solana/basic_solana.did
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
88 changes: 15 additions & 73 deletions examples/basic_solana/src/ed25519.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,6 @@
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<Vec<u8>>);

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<Vec<u8>> {
fn from(derivation_path: &DerivationPath) -> Self {
derivation_path.0.clone()
}
}
use sol_rpc_client::{ed25519::DerivationPath, IcRuntime};

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Ed25519ExtendedPublicKey {
Expand All @@ -28,7 +9,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(
<Vec<Vec<u8>>>::from(derivation_path)
.into_iter()
Expand All @@ -45,59 +26,20 @@ impl Ed25519ExtendedPublicKey {
}
}

impl From<SchnorrPublicKeyResponse> 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: &Ed25519KeyName,
key_name: Ed25519KeyName,
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(),
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)
}

pub async fn sign_with_ed25519(
message: Vec<u8>,
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
)
})
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,
}
}
24 changes: 11 additions & 13 deletions examples/basic_solana/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ 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::{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
Expand Down Expand Up @@ -93,20 +93,18 @@ impl From<SolanaNetwork> for SolanaCluster {
#[derive(CandidType, Deserialize, Debug, Default, PartialEq, Eq, Clone, Copy)]
pub enum Ed25519KeyName {
#[default]
TestKeyLocalDevelopment,
TestKey1,
ProductionKey1,
LocalDevelopment,
MainnetTestKey1,
MainnetProdKey1,
}

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",
impl From<Ed25519KeyName> for Ed25519KeyId {
fn from(key_id: Ed25519KeyName) -> Self {
match key_id {
Ed25519KeyName::LocalDevelopment => Self::LocalDevelopment,
Ed25519KeyName::MainnetTestKey1 => Self::MainnetTestKey1,
Ed25519KeyName::MainnetProdKey1 => Self::MainnetProdKey1,
}
.to_string();
write!(f, "{}", str)
}
}

Expand Down
13 changes: 7 additions & 6 deletions examples/basic_solana/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,10 @@ pub async fn create_nonce_account(owner: Option<Principal>) -> String {
);

let signatures = vec![
wallet.sign_with_ed25519(&message, &payer).await,
wallet.sign_with_ed25519(&message, &nonce_account).await,
payer.sign_message(&message).await,
nonce_account.sign_message(&message).await,
];

let transaction = Transaction {
message,
signatures,
Expand Down Expand Up @@ -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![payer.sign_message(&message).await];
let transaction = Transaction {
message,
signatures,
Expand Down Expand Up @@ -251,7 +252,7 @@ pub async fn send_sol(owner: Option<Principal>, 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![payer.sign_message(&message).await];
let transaction = Transaction {
message,
signatures,
Expand Down Expand Up @@ -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![payer.sign_message(&message).await];
let transaction = Transaction {
message,
signatures,
Expand Down Expand Up @@ -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![payer.sign_message(&message).await];
let transaction = Transaction {
message,
signatures,
Expand Down
26 changes: 17 additions & 9 deletions examples/basic_solana/src/solana_wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@
//! 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::{
ed25519::{sign_message, DerivationPath},
IcRuntime,
};
use solana_message::Message;
use solana_pubkey::Pubkey;
use solana_signature::Signature;
Expand All @@ -26,7 +30,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();
Expand All @@ -35,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<Pubkey> for SolanaAccount {
Expand Down Expand Up @@ -84,11 +99,4 @@ impl SolanaWallet {
.into(),
)
}

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)
}
}
2 changes: 1 addition & 1 deletion examples/basic_solana/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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_name()), &Default::default()).await;
mutate_state(|s| s.ed25519_public_key = Some(public_key.clone()));
public_key
}
2 changes: 1 addition & 1 deletion examples/basic_solana/tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion integration_tests/tests/solana_test_validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading