From fb6527f17492668fa4b4f6e814f4f13ef1ac7f1a Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Fri, 14 Jun 2024 13:46:34 -0400 Subject: [PATCH] feat: add Stellar signer trait and update to newest soroban-rpc (#1362) * feat: copy over original signing code from RPC --- Cargo.lock | 127 ++++++---- Cargo.toml | 18 +- FULL_HELP_DOCS.md | 42 +++- .../tests/it/integration/hello_world.rs | 4 +- cmd/soroban-cli/src/commands/config/mod.rs | 44 +++- .../src/commands/contract/deploy/asset.rs | 8 +- .../src/commands/contract/deploy/wasm.rs | 8 +- .../src/commands/contract/extend.rs | 7 +- .../src/commands/contract/install.rs | 8 +- .../src/commands/contract/invoke.rs | 100 ++++---- .../src/commands/contract/restore.rs | 2 +- cmd/soroban-cli/src/fee.rs | 4 +- cmd/soroban-cli/src/get_spec.rs | 12 +- cmd/soroban-cli/src/lib.rs | 1 + cmd/soroban-cli/src/signer.rs | 217 ++++++++++++++++++ 15 files changed, 462 insertions(+), 140 deletions(-) create mode 100644 cmd/soroban-cli/src/signer.rs diff --git a/Cargo.lock b/Cargo.lock index a5ff33c99..a1e6bee36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1198,6 +1198,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + [[package]] name = "ecdsa" version = "0.16.7" @@ -4227,6 +4233,30 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "schemars" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.39", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -4307,7 +4337,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ba64dc259185fdb8dfea7ab0418292743251efba80c9b20e88afc83c17b2593" dependencies = [ "slip10", - "stellar-strkey 0.0.8", + "stellar-strkey", "thiserror", "tiny-bip39", ] @@ -4343,6 +4373,17 @@ dependencies = [ "syn 2.0.39", ] +[[package]] +name = "serde_derive_internals" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "serde_json" version = "1.0.108" @@ -4619,9 +4660,9 @@ dependencies = [ [[package]] name = "soroban-builtin-sdk-macros" -version = "21.0.1" +version = "21.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1593481b51c9a2a98b0ff60ba2256796d4dfa8005f7ce237257acee175cf48b" +checksum = "084aab008009e712c445a9d7eab837a86559a6c2341f30bc4f33e9b258947688" dependencies = [ "itertools 0.11.0", "proc-macro2", @@ -4684,7 +4725,7 @@ dependencies = [ "soroban-spec-tools", "soroban-spec-typescript", "stellar-rpc-client", - "stellar-strkey 0.0.8", + "stellar-strkey", "stellar-xdr", "strsim 0.10.0", "strum 0.17.1", @@ -4708,9 +4749,9 @@ dependencies = [ [[package]] name = "soroban-env-common" -version = "21.0.1" +version = "21.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ff8c73721c0464eb8f0de7893637bd16512d3fe45143b4197b75e298988d67d" +checksum = "c16ee889fe99d6828bf3ffac00c84382793c31d62682401dbfa3e1b512f0c542" dependencies = [ "arbitrary", "crate-git-revision 0.0.6", @@ -4727,9 +4768,9 @@ dependencies = [ [[package]] name = "soroban-env-guest" -version = "21.0.1" +version = "21.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f39b60d7a8467e52ffb7863efba4b3ea3947aa028af8d88f4f5a76bb2909d8" +checksum = "95e591a15e488e66d3edd4be044fc4ffea438945f022c27129e933275744666f" dependencies = [ "soroban-env-common", "static_assertions", @@ -4737,9 +4778,9 @@ dependencies = [ [[package]] name = "soroban-env-host" -version = "21.0.1" +version = "21.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52bd39ea66b834e6e89981affd8b8ba58e450d3964d2a589e1a6a716bca12d77" +checksum = "4b73f48ae8081ecfb6c0d56c67f139c52cb6d5b6800d5e1adb9eef8e14a155b9" dependencies = [ "backtrace", "curve25519-dalek 4.1.2", @@ -4764,15 +4805,15 @@ dependencies = [ "soroban-env-common", "soroban-wasmi", "static_assertions", - "stellar-strkey 0.0.8", + "stellar-strkey", "wasmparser 0.116.1", ] [[package]] name = "soroban-env-macros" -version = "21.0.1" +version = "21.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fa76ebee7f29000d92792deccee1f670f4048422ad1a35e686a4654fb65f285" +checksum = "06d0581e3aba14892ee0ce63788f3d3e5d9eb1ab18906a9b7c66d77dae9e9fea" dependencies = [ "itertools 0.11.0", "proc-macro2", @@ -4789,9 +4830,9 @@ version = "21.0.0-rc.1" [[package]] name = "soroban-ledger-snapshot" -version = "21.0.1-preview.1" +version = "21.1.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b71878a8a3db38d5da6fa42d48d055b7937ef9ae608ffcb23f9589aa7989f10" +checksum = "07d02fc89d3a0e49776cdc35609719ddc5635ff61751b37d5577a06cc4b592ea" dependencies = [ "serde", "serde_json", @@ -4803,9 +4844,9 @@ dependencies = [ [[package]] name = "soroban-sdk" -version = "21.0.1-preview.1" +version = "21.1.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85dc6c199238c4150027034e3cfc383d9c64274292c8dc19bae598e843431356" +checksum = "1460cfdb31c0a1a7b93a35573fb9f794244a196224f8bf94cd235e4d29d8563d" dependencies = [ "arbitrary", "bytes-lit", @@ -4818,14 +4859,14 @@ dependencies = [ "soroban-env-host", "soroban-ledger-snapshot", "soroban-sdk-macros", - "stellar-strkey 0.0.8", + "stellar-strkey", ] [[package]] name = "soroban-sdk-macros" -version = "21.0.1-preview.1" +version = "21.1.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17c503bf0d43499884aa22877e7f293b19882c2088dcd3f00b01eb21b4c65001" +checksum = "a164fe430678986174b9872c6f82c23d73e74d3c6820800979af19c455c0bb70" dependencies = [ "crate-git-revision 0.0.6", "darling", @@ -4843,9 +4884,9 @@ dependencies = [ [[package]] name = "soroban-spec" -version = "21.0.1-preview.1" +version = "21.1.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4138300450ad75817954070b1cf32422b236410f937205e9f47e44114fd82fe9" +checksum = "6eeac587846418ecb0c73e9b05a9719559bccfc05da941d328eb3f8f8664c11a" dependencies = [ "base64 0.13.1", "stellar-xdr", @@ -4869,9 +4910,9 @@ dependencies = [ [[package]] name = "soroban-spec-rust" -version = "21.0.1-preview.1" +version = "21.1.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b230255799160fbcf36986f96f506cca671d96541f015546e9f74e99efdedb6" +checksum = "57c179749d655418a01d6d46d5dd08c024ed2e6ee68689430144195ba2f42ca8" dependencies = [ "prettyplease", "proc-macro2", @@ -4894,7 +4935,7 @@ dependencies = [ "serde_json", "soroban-env-host", "soroban-spec", - "stellar-strkey 0.0.8", + "stellar-strkey", "stellar-xdr", "thiserror", "tokio", @@ -4941,7 +4982,7 @@ dependencies = [ "soroban-spec", "soroban-spec-tools", "stellar-rpc-client", - "stellar-strkey 0.0.8", + "stellar-strkey", "thiserror", "tokio", "toml 0.8.13", @@ -4952,9 +4993,9 @@ dependencies = [ [[package]] name = "soroban-token-sdk" -version = "21.0.1-preview.1" +version = "21.1.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9ac1fe2457ed64e3ba18482b8375264c17340644ea485d0debce129e5cfb50f" +checksum = "61ce5410a2c201229755713f35f4bba041a201058d1084f0bcdfbca87bad1a9b" dependencies = [ "soroban-sdk", ] @@ -5031,7 +5072,7 @@ dependencies = [ "soroban-env-host", "soroban-spec", "stellar-rpc-client", - "stellar-strkey 0.0.8", + "stellar-strkey", "stellar-xdr", "test-case", "testcontainers", @@ -5042,12 +5083,10 @@ dependencies = [ [[package]] name = "stellar-rpc-client" -version = "21.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fefdc91d3cda13e6e324f0c03e42126582b7ff7149da7a0ec792820bb11fb75f" +version = "21.3.0" +source = "git+https://github.com/ahalabs/soroban-rpc?rev=f61d65dff57a7ffd5b37278490204a0e1bbf9b65#f61d65dff57a7ffd5b37278490204a0e1bbf9b65" dependencies = [ "clap", - "ed25519-dalek 2.0.0", "hex", "http 1.1.0", "itertools 0.10.5", @@ -5057,10 +5096,7 @@ dependencies = [ "serde-aux", "serde_json", "sha2 0.10.8", - "soroban-env-host", - "soroban-sdk", - "soroban-spec", - "stellar-strkey 0.0.7", + "stellar-strkey", "stellar-xdr", "termcolor", "termcolor_output", @@ -5069,16 +5105,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "stellar-strkey" -version = "0.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0689070126ca7f2effc2c5726584446db52190f0cef043c02eb4040a711c11" -dependencies = [ - "base32", - "thiserror", -] - [[package]] name = "stellar-strkey" version = "0.0.8" @@ -5092,9 +5118,9 @@ dependencies = [ [[package]] name = "stellar-xdr" -version = "21.0.1" +version = "21.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88e716110d5e050e528820217f84d4667e166ced841ba48d074152d4ad4ab884" +checksum = "ec43c9c5ae7ec7b6ac9e263b6d5b9e3781aa05ba3a1c05f6e70701c5c6600665" dependencies = [ "arbitrary", "base64 0.13.1", @@ -5102,10 +5128,11 @@ dependencies = [ "crate-git-revision 0.0.6", "escape-bytes", "hex", + "schemars", "serde", "serde_json", "serde_with", - "stellar-strkey 0.0.8", + "stellar-strkey", "thiserror", ] diff --git a/Cargo.toml b/Cargo.toml index 09b7daad2..3b0ae8d00 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,16 +15,16 @@ version = "21.0.0-rc.1" rust-version = "1.74.0" [workspace.dependencies.soroban-env-host] -version = "=21.0.1" +version = "=21.1.0" [workspace.dependencies.soroban-simulation] -version = "=21.0.1-preview.1" +version = "21.1.0" [workspace.dependencies.soroban-spec] -version = "=21.0.1-preview.1" +version = "=21.1.0-rc.1" [workspace.dependencies.soroban-spec-rust] -version = "=21.0.1-preview.1" +version = "=21.1.0-rc.1" [workspace.dependencies.soroban-spec-json] version = "=21.0.0-rc.1" @@ -39,13 +39,13 @@ version = "21.0.0-rc.1" path = "./cmd/crates/soroban-spec-tools" [workspace.dependencies.soroban-sdk] -version = "=21.0.1-preview.1" +version = "=21.1.0-rc.1" [workspace.dependencies.soroban-token-sdk] -version = "=21.0.1-preview.1" +version = "=21.1.0-rc.1" [workspace.dependencies.soroban-ledger-snapshot] -version = "=21.0.1-preview.1" +version = "=21.1.0-rc.1" [workspace.dependencies.soroban-cli] version = "=21.0.0-rc.1" @@ -53,10 +53,10 @@ path = "cmd/soroban-cli" [workspace.dependencies.soroban-rpc] package = "stellar-rpc-client" -version = "=21.0.1" +version = "21.3.1" [workspace.dependencies.stellar-xdr] -version = "=21.0.1" +version = "21.1.0" default-features = true [workspace.dependencies] diff --git a/FULL_HELP_DOCS.md b/FULL_HELP_DOCS.md index ed566d5ff..72cc3725b 100644 --- a/FULL_HELP_DOCS.md +++ b/FULL_HELP_DOCS.md @@ -40,6 +40,7 @@ This document contains the help content for the `stellar` command-line program. * [`stellar xdr`↴](#stellar-xdr) * [`stellar xdr types`↴](#stellar-xdr-types) * [`stellar xdr types list`↴](#stellar-xdr-types-list) +* [`stellar xdr types schema`↴](#stellar-xdr-types-schema) * [`stellar xdr guess`↴](#stellar-xdr-guess) * [`stellar xdr decode`↴](#stellar-xdr-decode) * [`stellar xdr encode`↴](#stellar-xdr-encode) @@ -243,11 +244,11 @@ Deploy builtin Soroban Asset Contract Possible values: `true`, `false` * `--instructions ` — Number of instructions to simulate -* `--build-only` — Build the transaction only write the base64 xdr to stdout +* `--build-only` — Build the transaction and only write the base64 xdr to stdout Possible values: `true`, `false` -* `--sim-only` — Simulation the transaction only write the base64 xdr to stdout +* `--sim-only` — Simulate the transaction and only write the base64 xdr to stdout Possible values: `true`, `false` @@ -402,11 +403,11 @@ If no keys are specified the contract itself is extended. Possible values: `true`, `false` * `--instructions ` — Number of instructions to simulate -* `--build-only` — Build the transaction only write the base64 xdr to stdout +* `--build-only` — Build the transaction and only write the base64 xdr to stdout Possible values: `true`, `false` -* `--sim-only` — Simulation the transaction only write the base64 xdr to stdout +* `--sim-only` — Simulate the transaction and only write the base64 xdr to stdout Possible values: `true`, `false` @@ -442,11 +443,11 @@ Deploy a wasm contract Possible values: `true`, `false` * `--instructions ` — Number of instructions to simulate -* `--build-only` — Build the transaction only write the base64 xdr to stdout +* `--build-only` — Build the transaction and only write the base64 xdr to stdout Possible values: `true`, `false` -* `--sim-only` — Simulation the transaction only write the base64 xdr to stdout +* `--sim-only` — Simulate the transaction and only write the base64 xdr to stdout Possible values: `true`, `false` @@ -615,11 +616,11 @@ Install a WASM file to the ledger without creating a contract instance Possible values: `true`, `false` * `--instructions ` — Number of instructions to simulate -* `--build-only` — Build the transaction only write the base64 xdr to stdout +* `--build-only` — Build the transaction and only write the base64 xdr to stdout Possible values: `true`, `false` -* `--sim-only` — Simulation the transaction only write the base64 xdr to stdout +* `--sim-only` — Simulate the transaction and only write the base64 xdr to stdout Possible values: `true`, `false` @@ -672,11 +673,11 @@ soroban contract invoke ... -- --help Possible values: `true`, `false` * `--instructions ` — Number of instructions to simulate -* `--build-only` — Build the transaction only write the base64 xdr to stdout +* `--build-only` — Build the transaction and only write the base64 xdr to stdout Possible values: `true`, `false` -* `--sim-only` — Simulation the transaction only write the base64 xdr to stdout +* `--sim-only` — Simulate the transaction and only write the base64 xdr to stdout Possible values: `true`, `false` @@ -792,11 +793,11 @@ If no keys are specificed the contract itself is restored. Possible values: `true`, `false` * `--instructions ` — Number of instructions to simulate -* `--build-only` — Build the transaction only write the base64 xdr to stdout +* `--build-only` — Build the transaction and only write the base64 xdr to stdout Possible values: `true`, `false` -* `--sim-only` — Simulation the transaction only write the base64 xdr to stdout +* `--sim-only` — Simulate the transaction and only write the base64 xdr to stdout Possible values: `true`, `false` @@ -1070,6 +1071,7 @@ View information about types ###### **Subcommands:** * `list` — +* `schema` — @@ -1088,6 +1090,22 @@ View information about types +## `stellar xdr types schema` + +**Usage:** `stellar xdr types schema [OPTIONS] --type ` + +###### **Options:** + +* `--type ` — XDR type to decode +* `--output ` + + Default value: `json-schema-draft201909` + + Possible values: `json-schema-draft7`, `json-schema-draft201909` + + + + ## `stellar xdr guess` Guess the XDR type diff --git a/cmd/crates/soroban-test/tests/it/integration/hello_world.rs b/cmd/crates/soroban-test/tests/it/integration/hello_world.rs index fae0a8463..6eb3bfb0f 100644 --- a/cmd/crates/soroban-test/tests/it/integration/hello_world.rs +++ b/cmd/crates/soroban-test/tests/it/integration/hello_world.rs @@ -228,8 +228,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::Rpc(_)), - "Expected rpc error got {e:?}" + matches!(e, contract::invoke::Error::Config(_)), + "Expected config error got {e:?}" ); } diff --git a/cmd/soroban-cli/src/commands/config/mod.rs b/cmd/soroban-cli/src/commands/config/mod.rs index e80aea73c..f0c388ecc 100644 --- a/cmd/soroban-cli/src/commands/config/mod.rs +++ b/cmd/soroban-cli/src/commands/config/mod.rs @@ -3,7 +3,13 @@ use std::path::PathBuf; use clap::{arg, command}; use serde::{Deserialize, Serialize}; -use crate::Pwd; +use soroban_rpc::Client; + +use crate::{ + signer, + xdr::{Transaction, TransactionEnvelope}, + Pwd, +}; use self::{network::Network, secret::Secret}; @@ -23,6 +29,10 @@ pub enum Error { Secret(#[from] secret::Error), #[error(transparent)] Config(#[from] locator::Error), + #[error(transparent)] + Rpc(#[from] soroban_rpc::Error), + #[error(transparent)] + Signer(#[from] signer::Error), } #[derive(Debug, clap::Args, Clone, Default)] @@ -49,6 +59,38 @@ impl Args { Ok(key.key_pair(self.hd_path)?) } + pub async fn sign_with_local_key(&self, tx: Transaction) -> Result { + self.sign(tx).await + } + + #[allow(clippy::unused_async)] + pub async fn sign(&self, tx: Transaction) -> Result { + let key = self.key_pair()?; + let Network { + network_passphrase, .. + } = &self.get_network()?; + Ok(signer::sign_tx(&key, &tx, network_passphrase)?) + } + + pub async fn sign_soroban_authorizations( + &self, + tx: &Transaction, + signers: &[ed25519_dalek::SigningKey], + ) -> Result, Error> { + let network = self.get_network()?; + let source_key = self.key_pair()?; + let client = Client::new(&network.rpc_url)?; + let latest_ledger = client.get_latest_ledger().await?.sequence; + let seq_num = latest_ledger + 60; // ~ 5 min + Ok(signer::sign_soroban_authorizations( + tx, + &source_key, + signers, + seq_num, + &network.network_passphrase, + )?) + } + pub fn account(&self, account_str: &str) -> Result { if let Ok(secret) = self.locator.read_identity(account_str) { Ok(secret) diff --git a/cmd/soroban-cli/src/commands/contract/deploy/asset.rs b/cmd/soroban-cli/src/commands/contract/deploy/asset.rs index 2f78a0dd6..e912d2b98 100644 --- a/cmd/soroban-cli/src/commands/contract/deploy/asset.rs +++ b/cmd/soroban-cli/src/commands/contract/deploy/asset.rs @@ -118,13 +118,13 @@ impl NetworkRunnable for Cmd { if self.fee.build_only { return Ok(TxnResult::Txn(tx)); } - let txn = client.create_assembled_transaction(&tx).await?; - let txn = self.fee.apply_to_assembled_txn(txn); + let txn = client.simulate_and_assemble_transaction(&tx).await?; + let txn = self.fee.apply_to_assembled_txn(txn).transaction().clone(); if self.fee.sim_only { - return Ok(TxnResult::Txn(txn.transaction().clone())); + return Ok(TxnResult::Txn(txn)); } let get_txn_resp = client - .send_assembled_transaction(txn, &key, &[], network_passphrase, None, None) + .send_transaction_polling(&self.config.sign_with_local_key(txn).await?) .await? .try_into()?; if args.map_or(true, |a| !a.no_cache) { diff --git a/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs b/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs index ff56c0e50..571e95bd4 100644 --- a/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs +++ b/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs @@ -260,13 +260,13 @@ impl NetworkRunnable for Cmd { return Ok(TxnResult::Txn(txn)); } - let txn = client.create_assembled_transaction(&txn).await?; - let txn = self.fee.apply_to_assembled_txn(txn); + let txn = client.simulate_and_assemble_transaction(&txn).await?; + let txn = self.fee.apply_to_assembled_txn(txn).transaction().clone(); if self.fee.sim_only { - return Ok(TxnResult::Txn(txn.transaction().clone())); + return Ok(TxnResult::Txn(txn)); } let get_txn_resp = client - .send_assembled_transaction(txn, &key, &[], &network.network_passphrase, None, None) + .send_transaction_polling(&config.sign_with_local_key(txn).await?) .await? .try_into()?; if global_args.map_or(true, |a| !a.no_cache) { diff --git a/cmd/soroban-cli/src/commands/contract/extend.rs b/cmd/soroban-cli/src/commands/contract/extend.rs index 544fdde3d..a6a0929eb 100644 --- a/cmd/soroban-cli/src/commands/contract/extend.rs +++ b/cmd/soroban-cli/src/commands/contract/extend.rs @@ -171,8 +171,13 @@ impl NetworkRunnable for Cmd { if self.fee.build_only { return Ok(TxnResult::Txn(tx)); } + let tx = client + .simulate_and_assemble_transaction(&tx) + .await? + .transaction() + .clone(); let res = client - .prepare_and_send_transaction(&tx, &key, &[], &network.network_passphrase, None, None) + .send_transaction_polling(&config.sign_with_local_key(tx).await?) .await?; if args.map_or(true, |a| !a.no_cache) { data::write(res.clone().try_into()?, &network.rpc_uri()?)?; diff --git a/cmd/soroban-cli/src/commands/contract/install.rs b/cmd/soroban-cli/src/commands/contract/install.rs index 39b039792..d7ebef16d 100644 --- a/cmd/soroban-cli/src/commands/contract/install.rs +++ b/cmd/soroban-cli/src/commands/contract/install.rs @@ -164,14 +164,14 @@ impl NetworkRunnable for Cmd { } } let txn = client - .create_assembled_transaction(&tx_without_preflight) + .simulate_and_assemble_transaction(&tx_without_preflight) .await?; - let txn = self.fee.apply_to_assembled_txn(txn); + let txn = self.fee.apply_to_assembled_txn(txn).transaction().clone(); if self.fee.sim_only { - return Ok(TxnResult::Txn(txn.transaction().clone())); + return Ok(TxnResult::Txn(txn)); } let txn_resp = client - .send_assembled_transaction(txn, &key, &[], &network.network_passphrase, None, None) + .send_transaction_polling(&self.config.sign_with_local_key(txn).await?) .await?; if args.map_or(true, |a| !a.no_cache) { data::write(txn_resp.clone().try_into().unwrap(), &network.rpc_uri()?)?; diff --git a/cmd/soroban-cli/src/commands/contract/invoke.rs b/cmd/soroban-cli/src/commands/contract/invoke.rs index 712c73c51..52df9fed9 100644 --- a/cmd/soroban-cli/src/commands/contract/invoke.rs +++ b/cmd/soroban-cli/src/commands/contract/invoke.rs @@ -12,18 +12,15 @@ use heck::ToKebabCase; use soroban_env_host::{ xdr::{ - self, Hash, HostFunction, InvokeContractArgs, InvokeHostFunctionOp, LedgerEntryData, - LedgerFootprint, Limits, Memo, MuxedAccount, Operation, OperationBody, Preconditions, - PublicKey, ScAddress, ScSpecEntry, ScSpecFunctionV0, ScSpecTypeDef, ScVal, ScVec, - SequenceNumber, SorobanAuthorizationEntry, SorobanResources, String32, StringM, - Transaction, TransactionExt, Uint256, VecM, WriteXdr, + self, AccountEntry, AccountEntryExt, AccountId, Hash, HostFunction, InvokeContractArgs, + InvokeHostFunctionOp, LedgerEntryData, Limits, Memo, MuxedAccount, Operation, + OperationBody, Preconditions, PublicKey, ScAddress, ScSpecEntry, ScSpecFunctionV0, + ScSpecTypeDef, ScVal, ScVec, SequenceNumber, String32, StringM, Thresholds, Transaction, + TransactionExt, Uint256, VecM, WriteXdr, }, HostError, }; -use soroban_env_host::xdr::{ - AccountEntry, AccountEntryExt, AccountId, DiagnosticEvent, Thresholds, -}; use soroban_spec::read::FromWasmError; use stellar_strkey::DecodeError; @@ -356,15 +353,14 @@ impl NetworkRunnable for Cmd { global_args: Option<&global::Args>, config: Option<&config::Args>, ) -> Result, Error> { - let unwrap_config = config.unwrap_or(&self.config); - let network = unwrap_config.get_network()?; + let config = config.unwrap_or(&self.config); + let network = config.get_network()?; tracing::trace!(?network); let contract_id = self.contract_id()?; let spec_entries = self.spec_entries()?; if let Some(spec_entries) = &spec_entries { // For testing wasm arg parsing - let _ = - self.build_host_function_parameters(contract_id, spec_entries, unwrap_config)?; + let _ = self.build_host_function_parameters(contract_id, spec_entries, config)?; } let client = rpc::Client::new(&network.rpc_url)?; let account_details = if self.is_view { @@ -373,7 +369,7 @@ impl NetworkRunnable for Cmd { client .verify_network_passphrase(Some(&network.network_passphrase)) .await?; - let key = unwrap_config.key_pair()?; + let key = config.key_pair()?; // Get the account sequence number let public_strkey = @@ -385,17 +381,17 @@ impl NetworkRunnable for Cmd { let spec_entries = get_remote_contract_spec( &contract_id, - &unwrap_config.locator, - &unwrap_config.network, + &config.locator, + &config.network, global_args, - config, + Some(config), ) .await .map_err(Error::from)?; // Get the ledger footprint let (function, spec, host_function_params, signers) = - self.build_host_function_parameters(contract_id, &spec_entries, unwrap_config)?; + self.build_host_function_parameters(contract_id, &spec_entries, config)?; let tx = build_invoke_contract_tx( host_function_params.clone(), sequence + 1, @@ -405,7 +401,7 @@ impl NetworkRunnable for Cmd { if self.fee.build_only { return Ok(TxnResult::Txn(tx)); } - let txn = client.create_assembled_transaction(&tx).await?; + let txn = client.simulate_and_assemble_transaction(&tx).await?; let txn = self.fee.apply_to_assembled_txn(txn); if self.fee.sim_only { return Ok(TxnResult::Txn(txn.transaction().clone())); @@ -415,23 +411,21 @@ impl NetworkRunnable for Cmd { data::write(sim_res.clone().into(), &network.rpc_uri()?)?; } let (return_value, events) = if self.is_view() { + // log_auth_cost_and_footprint(Some(&sim_res.transaction_data()?.resources)); (sim_res.results()?[0].xdr.clone(), sim_res.events()?) } else { - let global::Args { - verbose, - very_verbose, - no_cache, - .. - } = global_args.cloned().unwrap_or_default(); + let global::Args { no_cache, .. } = global_args.cloned().unwrap_or_default(); + // Need to sign all auth entries + let mut txn = txn.transaction().clone(); + // let auth = auth_entries(&txn); + // crate::log::auth(&[auth]); + + if let Some(tx) = config.sign_soroban_authorizations(&txn, &signers).await? { + txn = tx; + } + // log_auth_cost_and_footprint(resources(&txn)); let res = client - .send_assembled_transaction( - txn, - &unwrap_config.key_pair()?, - &signers, - &network.network_passphrase, - Some(log_events), - (verbose || very_verbose || self.fee.cost).then_some(log_resources), - ) + .send_transaction_polling(&config.sign_with_local_key(txn).await?) .await?; if !no_cache { data::write(res.clone().try_into()?, &network.rpc_uri()?)?; @@ -446,6 +440,34 @@ impl NetworkRunnable for Cmd { const DEFAULT_ACCOUNT_ID: AccountId = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))); +// fn log_auth_cost_and_footprint(resources: Option<&SorobanResources>) { +// if let Some(resources) = resources { +// crate::log::footprint(&resources.footprint); +// crate::log::cost(resources); +// } +// } + +// fn resources(tx: &Transaction) -> Option<&SorobanResources> { +// let TransactionExt::V1(SorobanTransactionData { resources, .. }) = &tx.ext else { +// return None; +// }; +// Some(resources) +// } + +// fn auth_entries(tx: &Transaction) -> VecM { +// tx.operations +// .first() +// .and_then(|op| match op.body { +// OperationBody::InvokeHostFunction(ref body) => (matches!( +// body.auth.first().map(|x| &x.root_invocation.function), +// Some(&SorobanAuthorizedFunction::ContractFn(_)) +// )) +// .then_some(body.auth.clone()), +// _ => None, +// }) +// .unwrap_or_default() +// } + fn default_account_entry() -> AccountEntry { AccountEntry { account_id: DEFAULT_ACCOUNT_ID, @@ -461,20 +483,6 @@ fn default_account_entry() -> AccountEntry { } } -fn log_events( - footprint: &LedgerFootprint, - auth: &[VecM], - events: &[DiagnosticEvent], -) { - crate::log::auth(auth); - crate::log::diagnostic_events(events, tracing::Level::TRACE); - crate::log::footprint(footprint); -} - -fn log_resources(resources: &SorobanResources) { - crate::log::cost(resources); -} - pub fn output_to_string( spec: &Spec, res: &ScVal, diff --git a/cmd/soroban-cli/src/commands/contract/restore.rs b/cmd/soroban-cli/src/commands/contract/restore.rs index ad824d34e..e38a45b31 100644 --- a/cmd/soroban-cli/src/commands/contract/restore.rs +++ b/cmd/soroban-cli/src/commands/contract/restore.rs @@ -174,7 +174,7 @@ impl NetworkRunnable for Cmd { return Ok(TxnResult::Txn(tx)); } let res = client - .prepare_and_send_transaction(&tx, &key, &[], &network.network_passphrase, None, None) + .send_transaction_polling(&config.sign_with_local_key(tx).await?) .await?; if args.map_or(true, |a| !a.no_cache) { data::write(res.clone().try_into()?, &network.rpc_uri()?)?; diff --git a/cmd/soroban-cli/src/fee.rs b/cmd/soroban-cli/src/fee.rs index ab057a587..698d66007 100644 --- a/cmd/soroban-cli/src/fee.rs +++ b/cmd/soroban-cli/src/fee.rs @@ -17,10 +17,10 @@ pub struct Args { /// Number of instructions to simulate #[arg(long, help_heading = HEADING_RPC)] pub instructions: Option, - /// Build the transaction only write the base64 xdr to stdout + /// Build the transaction and only write the base64 xdr to stdout #[arg(long, help_heading = HEADING_RPC)] pub build_only: bool, - /// Simulation the transaction only write the base64 xdr to stdout + /// Simulate the transaction and only write the base64 xdr to stdout #[arg(long, help_heading = HEADING_RPC, conflicts_with = "build_only")] pub sim_only: bool, } diff --git a/cmd/soroban-cli/src/get_spec.rs b/cmd/soroban-cli/src/get_spec.rs index e0d5edb9a..e21722794 100644 --- a/cmd/soroban-cli/src/get_spec.rs +++ b/cmd/soroban-cli/src/get_spec.rs @@ -28,6 +28,8 @@ pub enum Error { Network(#[from] network::Error), #[error(transparent)] Config(#[from] config::Error), + #[error(transparent)] + ContractSpec(#[from] contract_spec::Error), } /// @@ -60,13 +62,15 @@ pub async fn get_remote_contract_spec( // Get the contract spec entries based on the executable type Ok(match executable { ContractExecutable::Wasm(hash) => { - let hash = hash.to_string(); - if let Ok(entries) = data::read_spec(&hash) { + let hash_str = hash.to_string(); + if let Ok(entries) = data::read_spec(&hash_str) { entries } else { - let res = client.get_remote_contract_spec(contract_id).await?; + let raw_wasm = client.get_remote_wasm_from_hash(hash).await?; + let res = contract_spec::Spec::new(&raw_wasm)?; + let res = res.spec; if global_args.map_or(true, |a| !a.no_cache) { - data::write_spec(&hash, &res)?; + data::write_spec(&hash_str, &res)?; } res } diff --git a/cmd/soroban-cli/src/lib.rs b/cmd/soroban-cli/src/lib.rs index 70c7d7d5f..f71ce55e2 100644 --- a/cmd/soroban-cli/src/lib.rs +++ b/cmd/soroban-cli/src/lib.rs @@ -16,6 +16,7 @@ pub mod fee; pub mod get_spec; pub mod key; pub mod log; +pub mod signer; pub mod toid; pub mod utils; pub mod wasm; diff --git a/cmd/soroban-cli/src/signer.rs b/cmd/soroban-cli/src/signer.rs new file mode 100644 index 000000000..580a61a5e --- /dev/null +++ b/cmd/soroban-cli/src/signer.rs @@ -0,0 +1,217 @@ +use ed25519_dalek::ed25519::signature::Signer; +use sha2::{Digest, Sha256}; + +use soroban_env_host::xdr::{ + self, AccountId, DecoratedSignature, Hash, HashIdPreimage, HashIdPreimageSorobanAuthorization, + InvokeHostFunctionOp, Limits, Operation, OperationBody, PublicKey, ScAddress, ScMap, ScSymbol, + ScVal, Signature, SignatureHint, SorobanAddressCredentials, SorobanAuthorizationEntry, + SorobanAuthorizedFunction, SorobanCredentials, Transaction, TransactionEnvelope, + TransactionSignaturePayload, TransactionSignaturePayloadTaggedTransaction, + TransactionV1Envelope, Uint256, WriteXdr, +}; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("Contract addresses are not supported to sign auth entries {address}")] + ContractAddressAreNotSupported { address: String }, + #[error(transparent)] + Ed25519(#[from] ed25519_dalek::SignatureError), + #[error("Missing signing key for account {address}")] + MissingSignerForAddress { address: String }, + #[error(transparent)] + TryFromSlice(#[from] std::array::TryFromSliceError), + #[error("User cancelled signing, perhaps need to add -y")] + UserCancelledSigning, + #[error(transparent)] + Xdr(#[from] xdr::Error), +} + +fn requires_auth(txn: &Transaction) -> Option { + let [op @ Operation { + body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp { auth, .. }), + .. + }] = txn.operations.as_slice() + else { + return None; + }; + matches!( + auth.first().map(|x| &x.root_invocation.function), + Some(&SorobanAuthorizedFunction::ContractFn(_)) + ) + .then(move || op.clone()) +} + +// Use the given source_key and signers, to sign all SorobanAuthorizationEntry's in the given +// transaction. If unable to sign, return an error. +pub fn sign_soroban_authorizations( + raw: &Transaction, + source_key: &ed25519_dalek::SigningKey, + signers: &[ed25519_dalek::SigningKey], + signature_expiration_ledger: u32, + network_passphrase: &str, +) -> Result, Error> { + let mut tx = raw.clone(); + let Some(mut op) = requires_auth(&tx) else { + return Ok(None); + }; + + let Operation { + body: OperationBody::InvokeHostFunction(ref mut body), + .. + } = op + else { + return Ok(None); + }; + + let network_id = Hash(Sha256::digest(network_passphrase.as_bytes()).into()); + + let verification_key = source_key.verifying_key(); + let source_address = verification_key.as_bytes(); + + let signed_auths = body + .auth + .as_slice() + .iter() + .map(|raw_auth| { + let mut auth = raw_auth.clone(); + let SorobanAuthorizationEntry { + credentials: SorobanCredentials::Address(ref mut credentials), + .. + } = auth + else { + // Doesn't need special signing + return Ok(auth); + }; + let SorobanAddressCredentials { ref address, .. } = credentials; + + // See if we have a signer for this authorizationEntry + // If not, then we Error + let needle = match address { + ScAddress::Account(AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(ref a)))) => a, + ScAddress::Contract(Hash(c)) => { + // This address is for a contract. This means we're using a custom + // smart-contract account. Currently the CLI doesn't support that yet. + return Err(Error::MissingSignerForAddress { + address: stellar_strkey::Strkey::Contract(stellar_strkey::Contract(*c)) + .to_string(), + }); + } + }; + let signer = if let Some(s) = signers + .iter() + .find(|s| needle == s.verifying_key().as_bytes()) + { + s + } else if needle == source_address { + // This is the source address, so we can sign it + source_key + } else { + // We don't have a signer for this address + return Err(Error::MissingSignerForAddress { + address: stellar_strkey::Strkey::PublicKeyEd25519( + stellar_strkey::ed25519::PublicKey(*needle), + ) + .to_string(), + }); + }; + + sign_soroban_authorization_entry( + raw_auth, + signer, + signature_expiration_ledger, + &network_id, + ) + }) + .collect::, Error>>()?; + + body.auth = signed_auths.try_into()?; + tx.operations = vec![op].try_into()?; + Ok(Some(tx)) +} + +fn sign_soroban_authorization_entry( + raw: &SorobanAuthorizationEntry, + signer: &ed25519_dalek::SigningKey, + signature_expiration_ledger: u32, + network_id: &Hash, +) -> Result { + let mut auth = raw.clone(); + let SorobanAuthorizationEntry { + credentials: SorobanCredentials::Address(ref mut credentials), + .. + } = auth + else { + // Doesn't need special signing + return Ok(auth); + }; + let SorobanAddressCredentials { nonce, .. } = credentials; + + let preimage = HashIdPreimage::SorobanAuthorization(HashIdPreimageSorobanAuthorization { + network_id: network_id.clone(), + invocation: auth.root_invocation.clone(), + nonce: *nonce, + signature_expiration_ledger, + }) + .to_xdr(Limits::none())?; + + let payload = Sha256::digest(preimage); + let signature = signer.sign(&payload); + + let map = ScMap::sorted_from(vec![ + ( + ScVal::Symbol(ScSymbol("public_key".try_into()?)), + ScVal::Bytes( + signer + .verifying_key() + .to_bytes() + .to_vec() + .try_into() + .map_err(Error::Xdr)?, + ), + ), + ( + ScVal::Symbol(ScSymbol("signature".try_into()?)), + ScVal::Bytes( + signature + .to_bytes() + .to_vec() + .try_into() + .map_err(Error::Xdr)?, + ), + ), + ]) + .map_err(Error::Xdr)?; + credentials.signature = ScVal::Vec(Some( + vec![ScVal::Map(Some(map))].try_into().map_err(Error::Xdr)?, + )); + credentials.signature_expiration_ledger = signature_expiration_ledger; + auth.credentials = SorobanCredentials::Address(credentials.clone()); + Ok(auth) +} + +pub fn sign_tx( + key: &ed25519_dalek::SigningKey, + tx: &Transaction, + network_passphrase: &str, +) -> Result { + let tx_hash = hash(tx, network_passphrase)?; + let tx_signature = key.sign(&tx_hash); + + let decorated_signature = DecoratedSignature { + hint: SignatureHint(key.verifying_key().to_bytes()[28..].try_into()?), + signature: Signature(tx_signature.to_bytes().try_into()?), + }; + + Ok(TransactionEnvelope::Tx(TransactionV1Envelope { + tx: tx.clone(), + signatures: [decorated_signature].try_into()?, + })) +} + +pub fn hash(tx: &Transaction, network_passphrase: &str) -> Result<[u8; 32], xdr::Error> { + let signature_payload = TransactionSignaturePayload { + network_id: Hash(Sha256::digest(network_passphrase).into()), + tagged_transaction: TransactionSignaturePayloadTaggedTransaction::Tx(tx.clone()), + }; + Ok(Sha256::digest(signature_payload.to_xdr(Limits::none())?).into()) +}