Skip to content

Commit

Permalink
feat: add tx sign (#1590)
Browse files Browse the repository at this point in the history
Co-authored-by: Leigh McCulloch <[email protected]>
  • Loading branch information
2 people authored and elizabethengelman committed Sep 24, 2024
1 parent d567965 commit b47be01
Show file tree
Hide file tree
Showing 12 changed files with 258 additions and 61 deletions.
19 changes: 19 additions & 0 deletions FULL_HELP_DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1291,6 +1291,7 @@ Sign, Simulate, and Send transactions

* `simulate` — Simulate a transaction envelope from stdin
* `hash` — Calculate the hash of a transaction envelope from stdin
* `sign` — Sign a transaction envelope appending the signature to the envelope



Expand Down Expand Up @@ -1326,6 +1327,24 @@ Calculate the hash of a transaction envelope from stdin



## `stellar tx sign`

Sign a transaction envelope appending the signature to the envelope

**Usage:** `stellar tx sign [OPTIONS]`

###### **Options:**

* `--sign-with-key <SIGN_WITH_KEY>` — Sign with a local key. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path
* `--hd-path <HD_PATH>` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0`
* `--rpc-url <RPC_URL>` — RPC server endpoint
* `--network-passphrase <NETWORK_PASSPHRASE>` — Network passphrase to sign the transaction sent to the rpc server
* `--network <NETWORK>` — Name of network to use from config
* `--global` — Use global config
* `--config-dir <CONFIG_DIR>` — Location of config directory, default is "."



## `stellar xdr`

Decode and encode XDR
Expand Down
4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ endif
REPOSITORY_BRANCH := "$(shell git rev-parse --abbrev-ref HEAD)"
BUILD_TIMESTAMP ?= $(shell date '+%Y-%m-%dT%H:%M:%S')

SOROBAN_PORT?=8000

# The following works around incompatibility between the rust and the go linkers -
# the rust would generate an object file with min-version of 13.0 where-as the go
# compiler would generate a binary compatible with 12.3 and up. To align these
Expand Down Expand Up @@ -53,7 +55,7 @@ test: build-test
cargo test

e2e-test:
cargo test --test it -- --ignored
cargo test --features it --test it -- integration

check:
cargo clippy --all-targets
Expand Down
32 changes: 31 additions & 1 deletion cmd/crates/soroban-test/tests/it/integration/tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use soroban_test::{AssertExt, TestEnv};
use crate::integration::util::{deploy_contract, DeployKind, HELLO_WORLD};

#[tokio::test]
async fn txn_simulate() {
async fn simulate() {
let sandbox = &TestEnv::new();
let xdr_base64_build_only = deploy_contract(sandbox, HELLO_WORLD, DeployKind::BuildOnly).await;
let xdr_base64_sim_only = deploy_contract(sandbox, HELLO_WORLD, DeployKind::SimOnly).await;
Expand Down Expand Up @@ -49,3 +49,33 @@ async fn txn_hash() {

assert_eq!(hash.trim(), expected_hash);
}

#[tokio::test]
async fn build_simulate_sign_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 tx_simulated = deploy_contract(sandbox, HELLO_WORLD, DeployKind::SimOnly).await;
dbg!("{tx_simulated}");

let tx_signed = sandbox
.new_assert_cmd("tx")
.arg("sign")
.arg("--sign-with-key=test")
.write_stdin(tx_simulated.as_bytes())
.assert()
.success()
.stdout_as_str();
dbg!("{tx_signed}");

// TODO: Replace with calling tx send when that command is added.
let tx_signed = TransactionEnvelope::from_xdr_base64(tx_signed, Limits::none()).unwrap();
let client = soroban_rpc::Client::new(&sandbox.rpc_url).unwrap();
let rpc_result = client.send_transaction_polling(&tx_signed).await.unwrap();
assert_eq!(rpc_result.status, "SUCCESS");
}
6 changes: 6 additions & 0 deletions cmd/soroban-cli/src/commands/tx/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use clap::Parser;
use super::global;

pub mod hash;
pub mod sign;
pub mod simulate;
pub mod xdr;

Expand All @@ -12,6 +13,8 @@ pub enum Cmd {
Simulate(simulate::Cmd),
/// Calculate the hash of a transaction envelope from stdin
Hash(hash::Cmd),
/// Sign a transaction envelope appending the signature to the envelope
Sign(sign::Cmd),
}

#[derive(thiserror::Error, Debug)]
Expand All @@ -22,13 +25,16 @@ pub enum Error {
/// An error during hash calculation
#[error(transparent)]
Hash(#[from] hash::Error),
#[error(transparent)]
Sign(#[from] sign::Error),
}

impl Cmd {
pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> {
match self {
Cmd::Simulate(cmd) => cmd.run(global_args).await?,
Cmd::Hash(cmd) => cmd.run(global_args)?,
Cmd::Sign(cmd) => cmd.run(global_args).await?,
};
Ok(())
}
Expand Down
45 changes: 45 additions & 0 deletions cmd/soroban-cli/src/commands/tx/sign.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use crate::{
commands::global,
config::{locator, network, sign_with},
xdr::{self, Limits, WriteXdr},
};

#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error(transparent)]
XdrArgs(#[from] super::xdr::Error),
#[error(transparent)]
Network(#[from] network::Error),
#[error(transparent)]
Locator(#[from] locator::Error),
#[error(transparent)]
SignWith(#[from] sign_with::Error),
#[error(transparent)]
Xdr(#[from] xdr::Error),
}

#[derive(Debug, clap::Parser, Clone)]
#[group(skip)]
pub struct Cmd {
#[command(flatten)]
pub sign_with: sign_with::Args,
#[command(flatten)]
pub network: network::Args,
#[command(flatten)]
pub locator: locator::Args,
}

impl Cmd {
#[allow(clippy::unused_async)]
pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> {
let tx_env = super::xdr::tx_envelope_from_stdin()?;
let tx_env_signed = self.sign_with.sign_tx_env(
tx_env,
&self.locator,
&self.network.get(&self.locator)?,
global_args.quiet,
)?;
println!("{}", tx_env_signed.to_xdr_base64(Limits::none())?);
Ok(())
}
}
8 changes: 8 additions & 0 deletions cmd/soroban-cli/src/config/locator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,14 @@ impl Args {
KeyType::Identity.read_with_global(name, &self.local_config()?)
}

pub fn key(&self, key_or_name: &str) -> Result<Secret, Error> {
if let Ok(signer) = key_or_name.parse::<Secret>() {
Ok(signer)
} else {
self.read_identity(key_or_name)
}
}

pub fn read_network(&self, name: &str) -> Result<Network, Error> {
let res = KeyType::Network.read_with_global(name, &self.local_config()?);
if let Err(Error::ConfigMissing(_, _)) = &res {
Expand Down
14 changes: 9 additions & 5 deletions cmd/soroban-cli/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ use serde::{Deserialize, Serialize};
use soroban_rpc::Client;

use crate::{
signer,
print::Print,
signer::{self, LocalKey, Signer, SignerKind},
xdr::{Transaction, TransactionEnvelope},
Pwd,
};
Expand All @@ -18,6 +19,7 @@ pub mod data;
pub mod locator;
pub mod network;
pub mod secret;
pub mod sign_with;
pub mod upgrade_check;

#[derive(thiserror::Error, Debug)]
Expand Down Expand Up @@ -65,10 +67,12 @@ impl Args {
#[allow(clippy::unused_async)]
pub async fn sign(&self, tx: Transaction) -> Result<TransactionEnvelope, Error> {
let key = self.key_pair()?;
let Network {
network_passphrase, ..
} = &self.get_network()?;
Ok(signer::sign_tx(&key, &tx, network_passphrase)?)
let network = &self.get_network()?;
let signer = Signer {
kind: SignerKind::Local(LocalKey { key }),
printer: Print::new(false),
};
Ok(signer.sign_tx(tx, network)?)
}

pub async fn sign_soroban_authorizations(
Expand Down
21 changes: 20 additions & 1 deletion cmd/soroban-cli/src/config/secret.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ use serde::{Deserialize, Serialize};
use std::{io::Write, str::FromStr};
use stellar_strkey::ed25519::{PrivateKey, PublicKey};

use crate::utils;
use crate::print::Print;
use crate::{
signer::{self, LocalKey, Signer, SignerKind},
utils,
};

#[derive(thiserror::Error, Debug)]
pub enum Error {
Expand All @@ -21,6 +25,8 @@ pub enum Error {
Ed25519(#[from] ed25519_dalek::SignatureError),
#[error("Invalid address {0}")]
InvalidAddress(String),
#[error(transparent)]
Signer(#[from] signer::Error),
}

#[derive(Debug, clap::Args, Clone)]
Expand Down Expand Up @@ -120,6 +126,19 @@ impl Secret {
)?)
}

pub fn signer(&self, index: Option<usize>, quiet: bool) -> Result<Signer, Error> {
let kind = match self {
Secret::SecretKey { .. } | Secret::SeedPhrase { .. } => {
let key = self.key_pair(index)?;
SignerKind::Local(LocalKey { key })
}
};
Ok(Signer {
kind,
printer: Print::new(quiet),
})
}

pub fn key_pair(&self, index: Option<usize>) -> Result<ed25519_dalek::SigningKey, Error> {
Ok(utils::into_signing_key(&self.private_key(index)?))
}
Expand Down
53 changes: 53 additions & 0 deletions cmd/soroban-cli/src/config/sign_with.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use crate::{signer, xdr::TransactionEnvelope};
use clap::arg;

use super::{
locator,
network::{self, Network},
secret,
};

#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error(transparent)]
Network(#[from] network::Error),
#[error(transparent)]
Signer(#[from] signer::Error),
#[error(transparent)]
Secret(#[from] secret::Error),
#[error(transparent)]
Locator(#[from] locator::Error),
#[error(transparent)]
Rpc(#[from] soroban_rpc::Error),
#[error("No sign with key provided")]
NoSignWithKey,
#[error(transparent)]
StrKey(#[from] stellar_strkey::DecodeError),
}

#[derive(Debug, clap::Args, Clone, Default)]
#[group(skip)]
pub struct Args {
/// Sign with a local key. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path.
#[arg(long, env = "STELLAR_SIGN_WITH_KEY")]
pub sign_with_key: Option<String>,

#[arg(long, requires = "sign_with_key")]
/// If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0`
pub hd_path: Option<usize>,
}

impl Args {
pub fn sign_tx_env(
&self,
tx: TransactionEnvelope,
locator: &locator::Args,
network: &Network,
quiet: bool,
) -> Result<TransactionEnvelope, Error> {
let key_or_name = self.sign_with_key.as_deref().ok_or(Error::NoSignWithKey)?;
let secret = locator.key(key_or_name)?;
let signer = secret.signer(self.hd_path, quiet)?;
Ok(signer.sign_tx_env(tx, network)?)
}
}
3 changes: 1 addition & 2 deletions cmd/soroban-cli/src/print.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ use std::{env, fmt::Display};
use soroban_env_host::xdr::{Error as XdrError, Transaction};

use crate::{
config::network::Network,
utils::{explorer_url_for_transaction, transaction_hash},
config::network::Network, utils::explorer_url_for_transaction, utils::transaction_hash,
};

const TERMS: &[&str] = &["Apple_Terminal", "vscode"];
Expand Down
Loading

0 comments on commit b47be01

Please sign in to comment.