Skip to content

Commit

Permalink
feat: add Stellar; tx sign/send
Browse files Browse the repository at this point in the history
  • Loading branch information
willemneal committed Jun 24, 2024
1 parent da5f456 commit 957d451
Show file tree
Hide file tree
Showing 11 changed files with 475 additions and 202 deletions.
38 changes: 37 additions & 1 deletion Cargo.lock

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

8 changes: 7 additions & 1 deletion cmd/crates/soroban-test/tests/it/integration/custom_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ use super::util::invoke_with_roundtrip;

fn invoke_custom(e: &TestEnv, id: &str, func: &str) -> assert_cmd::Command {
let mut s = e.new_assert_cmd("contract");
s.arg("invoke").arg("--id").arg(id).arg("--").arg(func);
s.env("RUST_LOG", "soroban_cli::log::diagnostic_event=off")
.arg("invoke")
.arg("--id")
.arg(id)
.arg("--is-view")
.arg("--")
.arg(func);
s
}

Expand Down
11 changes: 6 additions & 5 deletions cmd/crates/soroban-test/tests/it/integration/hello_world.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ async fn invoke() {
.stdout_as_str();
let dir = sandbox.dir();
let seed_phrase = std::fs::read_to_string(dir.join(".soroban/identity/test.toml")).unwrap();
let s = toml::from_str::<secret::Secret>(&seed_phrase).unwrap();
let secret::Secret::SeedPhrase { seed_phrase } = s else {
let s = toml::from_str::<secret::Signer>(&seed_phrase).unwrap();
let secret::Signer::SeedPhrase { seed_phrase } = s else {
panic!("Expected seed phrase")
};
let id = &deploy_hello(sandbox).await;
Expand Down Expand Up @@ -106,7 +106,7 @@ async fn invoke() {
config_locator
.write_identity(
"testone",
&secret::Secret::SecretKey {
&secret::Signer::SecretKey {
secret_key: secret_key_1.clone(),
},
)
Expand Down Expand Up @@ -222,6 +222,7 @@ async fn invoke_auth_with_different_test_account_fail(sandbox: &TestEnv, id: &st
"--hd-path=0",
"--id",
id,
"--fee=1000000",
"--",
"auth",
&format!("--addr={addr}"),
Expand All @@ -230,8 +231,8 @@ async fn invoke_auth_with_different_test_account_fail(sandbox: &TestEnv, id: &st
.await;
let e = res.unwrap_err();
assert!(
matches!(e, contract::invoke::Error::Config(_)),
"Expected config error got {e:?}"
matches!(e, contract::invoke::Error::Rpc(_)),
"Expected rpc error got {e:?}"
);
}

Expand Down
50 changes: 49 additions & 1 deletion cmd/crates/soroban-test/tests/it/integration/tx.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use soroban_sdk::xdr::{Limits, ReadXdr, TransactionEnvelope, WriteXdr};
use soroban_sdk::xdr::{
Limits, ReadXdr, TransactionEnvelope, TransactionV1Envelope, VecM, WriteXdr,
};
use soroban_test::{AssertExt, TestEnv};

use crate::integration::util::{deploy_contract, DeployKind, HELLO_WORLD};
Expand Down Expand Up @@ -30,3 +32,49 @@ async fn txn_simulate() {
assembled_str
);
}

#[tokio::test]
async fn txn_send() {
let sandbox = &TestEnv::new();
sandbox
.new_assert_cmd("contract")
.arg("install")
.args(["--wasm", HELLO_WORLD.path().as_os_str().to_str().unwrap()])
.assert()
.success();

let xdr_base64 = deploy_contract(sandbox, HELLO_WORLD, DeployKind::SimOnly).await;
println!("{xdr_base64}");
let tx_env = TransactionEnvelope::from_xdr_base64(&xdr_base64, Limits::none()).unwrap();
let tx_env = sign(sandbox, tx_env);

println!(
"Transaction to send:\n{}",
tx_env.to_xdr_base64(Limits::none()).unwrap()
);

let assembled_str = sandbox
.new_assert_cmd("tx")
.arg("send")
.arg("--source=test")
.write_stdin(tx_env.to_xdr_base64(Limits::none()).unwrap())
.assert()
.success()
.stdout_as_str();
println!("Transaction sent: {assembled_str}");
}

fn sign(sandbox: &TestEnv, tx_env: TransactionEnvelope) -> TransactionEnvelope {
TransactionEnvelope::from_xdr_base64(
sandbox
.new_assert_cmd("tx")
.arg("sign")
.arg("--source=test")
.write_stdin(tx_env.to_xdr_base64(Limits::none()).unwrap().as_bytes())
.assert()
.success()
.stdout_as_str(),
Limits::none(),
)
.unwrap()
}
21 changes: 8 additions & 13 deletions cmd/crates/stellar-ledger/src/hd_path.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
use crate::Error;

#[derive(Clone, Copy)]
pub struct HdPath(pub u32);

impl HdPath {
#[must_use]
pub fn depth(&self) -> u8 {
let path: slip10::BIP32Path = self.into();
path.depth()
Expand All @@ -23,7 +22,8 @@ impl From<&u32> for HdPath {
}

impl HdPath {
pub fn to_vec(&self) -> Result<Vec<u8>, Error> {
#[must_use]
pub fn to_vec(&self) -> Vec<u8> {
hd_path_to_bytes(&self.into())
}
}
Expand All @@ -35,16 +35,11 @@ impl From<&HdPath> for slip10::BIP32Path {
}
}

fn hd_path_to_bytes(hd_path: &slip10::BIP32Path) -> Result<Vec<u8>, Error> {
fn hd_path_to_bytes(hd_path: &slip10::BIP32Path) -> Vec<u8> {
let hd_path_indices = 0..hd_path.depth();
let result = hd_path_indices
// Unsafe unwrap is safe because the depth is the length of interneal vector
hd_path_indices
.into_iter()
.map(|index| {
Ok(hd_path
.index(index)
.ok_or_else(|| Error::Bip32PathError(format!("{hd_path}")))?
.to_be_bytes())
})
.collect::<Result<Vec<_>, Error>>()?;
Ok(result.into_iter().flatten().collect())
.flat_map(|index| unsafe { hd_path.index(index).unwrap_unchecked().to_be_bytes() })
.collect()
}
69 changes: 41 additions & 28 deletions cmd/crates/stellar-ledger/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use hd_path::HdPath;
use ledger_transport::{APDUCommand, Exchange};
use ledger_transport::APDUCommand;
pub use ledger_transport_hid::TransportNativeHID;
use ledger_transport_hid::{
hidapi::{HidApi, HidError},
LedgerHIDError, TransportNativeHID,
LedgerHIDError,
};

use soroban_env_host::xdr::{Hash, Transaction};
Expand All @@ -15,6 +16,8 @@ use stellar_xdr::curr::{

pub use crate::signer::Blob;
pub mod hd_path;
pub use ledger_transport::Exchange;

mod signer;

// this is from https://github.com/LedgerHQ/ledger-live/blob/36cfbf3fa3300fd99bcee2ab72e1fd8f280e6280/libs/ledgerjs/packages/hw-app-str/src/Str.ts#L181
Expand Down Expand Up @@ -80,6 +83,11 @@ pub struct LedgerSigner<T: Exchange> {
unsafe impl<T> Send for LedgerSigner<T> where T: Exchange {}
unsafe impl<T> Sync for LedgerSigner<T> where T: Exchange {}

/// Returns a new `LedgerSigner` with a native HID transport, e.i. the transport is connected to the Ledger device
///
/// # Errors
///
/// Returns an error if there is an issue with connecting with the device
pub fn native() -> Result<LedgerSigner<TransportNativeHID>, Error> {
Ok(LedgerSigner {
transport: get_transport()?,
Expand All @@ -93,11 +101,7 @@ where
pub fn new(transport: T) -> Self {
Self { transport }
}
pub fn native() -> Result<LedgerSigner<TransportNativeHID>, Error> {
Ok(LedgerSigner {
transport: get_transport()?,
})
}

/// Get the device app's configuration
/// # Errors
/// Returns an error if there is an issue with connecting with the device or getting the config from the device
Expand Down Expand Up @@ -141,7 +145,7 @@ where
};
let mut signature_payload_as_bytes = signature_payload.to_xdr(Limits::none())?;

let mut hd_path_to_bytes = hd_path.into().to_vec()?;
let mut hd_path_to_bytes = hd_path.into().to_vec();

let capacity = 1 + hd_path_to_bytes.len() + signature_payload_as_bytes.len();
let mut data: Vec<u8> = Vec::with_capacity(capacity);
Expand Down Expand Up @@ -182,7 +186,9 @@ where
}

/// The `display_and_confirm` bool determines if the Ledger will display the public key on its screen and requires user approval to share
async fn get_public_key_with_display_flag(
/// # Errors
/// Returns an error if there is an issue with connecting with the device or getting the public key from the device
pub async fn get_public_key_with_display_flag(
&self,
hd_path: impl Into<HdPath>,
display_and_confirm: bool,
Expand All @@ -191,7 +197,7 @@ where
// the first element of the data should be the number of elements in the path
let hd_path = hd_path.into();
let hd_path_elements_count = hd_path.depth();
let mut hd_path_to_bytes = hd_path.to_vec()?;
let mut hd_path_to_bytes = hd_path.to_vec();
hd_path_to_bytes.insert(0, hd_path_elements_count);

let p2 = if display_and_confirm {
Expand Down Expand Up @@ -241,6 +247,30 @@ where
)),
}
}

/// Sign a blob of data with the account on the Ledger device
/// # Errors
/// Returns an error if there is an issue with connecting with the device or signing the given tx on the device
pub async fn sign_data(&self, index: &HdPath, blob: &[u8]) -> Result<Vec<u8>, Error> {
let mut hd_path_to_bytes = index.to_vec();

let capacity = 1 + hd_path_to_bytes.len() + blob.len();
let mut data: Vec<u8> = Vec::with_capacity(capacity);

data.insert(0, HD_PATH_ELEMENTS_COUNT);
data.append(&mut hd_path_to_bytes);
data.extend_from_slice(blob);

let command = APDUCommand {
cla: CLA,
ins: SIGN_TX_HASH,
p1: P1_SIGN_TX_HASH,
p2: P2_SIGN_TX_HASH,
data,
};

self.send_command_to_ledger(command).await
}
}

#[async_trait::async_trait]
Expand All @@ -265,24 +295,7 @@ where
/// # Errors
/// Returns an error if there is an issue with connecting with the device or signing the given tx on the device. Or, if the device has not enabled hash signing
async fn sign_blob(&self, index: &Self::Key, blob: &[u8]) -> Result<Vec<u8>, Error> {
let mut hd_path_to_bytes = index.to_vec()?;

let capacity = 1 + hd_path_to_bytes.len() + blob.len();
let mut data: Vec<u8> = Vec::with_capacity(capacity);

data.insert(0, HD_PATH_ELEMENTS_COUNT);
data.append(&mut hd_path_to_bytes);
data.extend_from_slice(blob);

let command = APDUCommand {
cla: CLA,
ins: SIGN_TX_HASH,
p1: P1_SIGN_TX_HASH,
p2: P2_SIGN_TX_HASH,
data,
};

self.send_command_to_ledger(command).await
self.sign_data(index, blob).await
}
}

Expand Down
1 change: 1 addition & 0 deletions cmd/soroban-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ rust-embed = { version = "8.2.0", features = ["debug-embed"] }
bollard = { workspace=true }
futures-util = "0.3.30"
home = "0.5.9"
termion = "4.0.0"
# For hyper-tls
[target.'cfg(unix)'.dependencies]
openssl = { version = "=0.10.55", features = ["vendored"] }
Expand Down
Loading

0 comments on commit 957d451

Please sign in to comment.