diff --git a/.github/workflows/binaries.yml b/.github/workflows/binaries.yml index 48e2e5657..5bbc20396 100644 --- a/.github/workflows/binaries.yml +++ b/.github/workflows/binaries.yml @@ -46,7 +46,14 @@ jobs: - run: rustup target add ${{ matrix.sys.target }} - if: matrix.sys.target == 'aarch64-unknown-linux-gnu' +<<<<<<< HEAD + run: sudo apt-get update && sudo apt-get -y install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu libudev-dev libssl-dev + + - if: matrix.sys.target == 'x86_64-unknown-linux-gnu' + run: sudo apt-get update && sudo apt-get -y install libudev-dev libssl-dev +======= run: sudo apt-get update && sudo apt-get -y install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu libudev-dev libdbus-1-dev +>>>>>>> feat/os_keychain - name: Setup vars run: | diff --git a/.github/workflows/bindings-ts.yml b/.github/workflows/bindings-ts.yml index 7f87baf08..0bb18aaa3 100644 --- a/.github/workflows/bindings-ts.yml +++ b/.github/workflows/bindings-ts.yml @@ -1,9 +1,9 @@ name: bindings typescript on: - push: - branches: [main, release/**] - pull_request: + push: + branches: [main, release/**] + pull_request: jobs: test: @@ -38,9 +38,16 @@ jobs: target/ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - run: rustup update +<<<<<<< HEAD + - name: install libudev-dev + run: | + sudo apt install -y libudev-dev +======= - run: sudo apt install -y libdbus-1-dev +>>>>>>> feat/os_keychain - run: cargo build - run: rustup target add wasm32-unknown-unknown - run: make build-test-wasms - run: npm ci && npm run test working-directory: cmd/crates/soroban-spec-typescript/ts-tests + diff --git a/.github/workflows/ledger-emulator.yml b/.github/workflows/ledger-emulator.yml index 7a3068d2e..907a55e7f 100644 --- a/.github/workflows/ledger-emulator.yml +++ b/.github/workflows/ledger-emulator.yml @@ -1,28 +1,31 @@ name: Ledger Emulator Tests on: - push: - branches: [main, release/**] - pull_request: + push: + branches: [main, release/**] + pull_request: concurrency: - group: ${{ github.workflow }}-${{ github.ref_protected == 'true' && github.sha || github.ref }} - cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.ref_protected == 'true' && github.sha || github.ref }} + cancel-in-progress: true defaults: - run: - shell: bash + run: + shell: bash jobs: - emulator-tests: - runs-on: ubuntu-latest - env: - CI_TESTS: true - steps: - - uses: actions/checkout@v4 - - uses: stellar/actions/rust-cache@main - - name: install libudev-dev - run: | - sudo apt install -y libudev-dev - - run: | - cargo test --manifest-path cmd/crates/stellar-ledger/Cargo.toml --features "emulator-tests" -- --nocapture \ No newline at end of file + emulator-tests: + runs-on: ubuntu-latest + env: + CI_TESTS: true + steps: + - uses: actions/checkout@v4 + - uses: stellar/actions/rust-cache@main + - name: install libudev-dev + run: | + sudo apt install -y libudev-dev + - run: | + cargo test --manifest-path cmd/crates/stellar-ledger/Cargo.toml --features "emulator-tests" -- --nocapture + - run: cargo build --features emulator-tests + - run: | + cargo test --features emulator-tests --package soroban-test --test it -- emulator diff --git a/.github/workflows/rpc-tests.yml b/.github/workflows/rpc-tests.yml index 5392d9830..c52475d1c 100644 --- a/.github/workflows/rpc-tests.yml +++ b/.github/workflows/rpc-tests.yml @@ -1,23 +1,55 @@ name: RPC Tests on: - push: - branches: [main, release/**] - pull_request: + push: + branches: [main, release/**] + pull_request: concurrency: - group: ${{ github.workflow }}-${{ github.ref_protected == 'true' && github.sha || github.ref }} - cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.ref_protected == 'true' && github.sha || github.ref }} + cancel-in-progress: true jobs: - test: - name: test RPC - runs-on: ubuntu-22.04 - services: - rpc: - image: stellar/quickstart:testing - ports: - - 8000:8000 + test: + name: test RPC + runs-on: ubuntu-22.04 env: +<<<<<<< HEAD + CI_TESTS: true + services: + rpc: + image: stellar/quickstart:testing + ports: + - 8000:8000 + env: + ENABLE_LOGS: true + ENABLE_SOROBAN_DIAGNOSTIC_EVENTS: true + NETWORK: local + PROTOCOL_VERSION: 22 + options: >- + --health-cmd "curl --no-progress-meter --fail-with-body -X POST \"http://localhost:8000/soroban/rpc\" -H 'Content-Type: application/json' -d '{\"jsonrpc\":\"2.0\",\"id\":8675309,\"method\":\"getNetwork\"}' && curl --no-progress-meter \"http://localhost:8000/friendbot\" | grep '\"invalid_field\": \"addr\"'" + --health-interval 10s + --health-timeout 5s + --health-retries 50 + steps: + - uses: actions/checkout@v4 + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + - run: rustup update + - run: rustup target add wasm32-unknown-unknown + - run: make build-test-wasms + - name: install libudev-dev + run: | + sudo apt install -y libudev-dev + - run: cargo build + - run: SOROBAN_PORT=8000 cargo test --features it --package soroban-test --test it -- integration +======= ENABLE_LOGS: true ENABLE_SOROBAN_DIAGNOSTIC_EVENTS: true NETWORK: local @@ -44,3 +76,4 @@ jobs: - run: rustup target add wasm32-unknown-unknown - run: make build-test-wasms - run: SOROBAN_PORT=8000 cargo test --features it --package soroban-test --test it -- integration +>>>>>>> feat/os_keychain diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 22a27a939..8887d8380 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -49,7 +49,11 @@ jobs: - uses: actions/checkout@v4 - uses: stellar/actions/rust-cache@main - run: rustup update +<<<<<<< HEAD + - run: sudo apt install -y libudev-dev +======= - run: sudo apt install -y libdbus-1-dev +>>>>>>> feat/os_keychain - run: make generate-full-help-doc - run: git add -N . && git diff HEAD --exit-code diff --git a/.gitignore b/.gitignore index b7150ae5d..578b28adf 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ test_snapshots .idea local.sh .stellar +.zed diff --git a/Cargo.lock b/Cargo.lock index ca8bc5ee1..1c6cd83e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4158,9 +4158,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.9.0" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" +checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" dependencies = [ "base64 0.22.1", "chrono", @@ -4176,9 +4176,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.9.0" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350" +checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" dependencies = [ "darling", "proc-macro2", @@ -4428,6 +4428,7 @@ dependencies = [ "serde", "serde-aux", "serde_json", + "serde_with", "sha2 0.10.8", "shell-escape", "shlex", @@ -4438,6 +4439,7 @@ dependencies = [ "soroban-spec-rust", "soroban-spec-tools", "soroban-spec-typescript", + "stellar-ledger", "stellar-rpc-client", "stellar-strkey 0.0.11", "stellar-xdr", @@ -4705,8 +4707,11 @@ dependencies = [ "soroban-ledger-snapshot", "soroban-spec", "soroban-spec-tools", + "stellar-ledger", "stellar-rpc-client", "stellar-strkey 0.0.11", + "test-case", + "testcontainers", "thiserror", "tokio", "toml", @@ -4779,7 +4784,6 @@ dependencies = [ "byteorder 1.5.0", "ed25519-dalek", "env_logger", - "futures", "hex", "home", "httpmock", @@ -4790,15 +4794,12 @@ dependencies = [ "phf", "pretty_assertions", "reqwest", - "sep5", "serde", "serde_derive", "serde_json", "serial_test", "sha2 0.10.8", "slipped10", - "soroban-spec", - "stellar-rpc-client", "stellar-strkey 0.0.11", "stellar-xdr", "test-case", @@ -5807,9 +5808,9 @@ dependencies = [ [[package]] name = "wasm-streams" -version = "0.4.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e072d4e72f700fb3443d8fe94a39315df013eef1104903cdb0a2abd322bbecd" +checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" dependencies = [ "futures-util", "js-sys", diff --git a/Cargo.toml b/Cargo.toml index 12c5490d6..97d0d9b9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,6 +65,10 @@ version = "=22.0.4" package = "stellar-rpc-client" version = "=22.0.0" +[workspace.dependencies.stellar-ledger] +version = "=22.2.0" +path = "cmd/crates/stellar-ledger" + # Dependencies from elsewhere shared by crates: [workspace.dependencies] stellar-strkey = "0.0.11" @@ -104,7 +108,9 @@ walkdir = "2.5.0" toml_edit = "0.22.20" toml = "0.8.19" reqwest = "0.12.7" +# testing predicates = "3.1.2" +testcontainers = { version = "0.20.1" } httpmock = "0.7.0" [profile.test-wasms] diff --git a/FULL_HELP_DOCS.md b/FULL_HELP_DOCS.md index 9347f4a68..bcb8cc4cb 100644 --- a/FULL_HELP_DOCS.md +++ b/FULL_HELP_DOCS.md @@ -965,6 +965,7 @@ Add a new identity (keypair, ledger, OS specific secure store) * `--seed-phrase` — (deprecated) Enter key using 12-24 word seed phrase * `--global` — Use global config * `--config-dir ` — Location of config directory, default is "." +* `--public-key ` — Add a public key, ed25519, or muxed account, e.g. G1.., M2.. @@ -1770,7 +1771,7 @@ https://developers.stellar.org/docs/learn/glossary#flags * `--hd-path ` — If using a seed phrase, which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` * `--global` — Use global config * `--config-dir ` — Location of config directory, default is "." -* `--trustor ` — Account to set trustline flags for +* `--trustor ` — Account to set trustline flags for, e.g. `GBX...`, or alias, or muxed account, `M123...`` * `--asset ` — Asset to set trustline flags for * `--set-authorize` — Signifies complete authorization allowing an account to transact freely with the asset to make and receive payments and place orders * `--set-authorize-to-maintain-liabilities` — Denotes limited authorization that allows an account to maintain current orders but not to otherwise transact with the asset @@ -1830,13 +1831,26 @@ https://developers.stellar.org/docs/learn/glossary#flags Transfers the XLM balance of an account to another account and removes the source account from the ledger -**Usage:** `stellar tx operation add account-merge [OPTIONS] --account ` +**Usage:** `stellar tx operation add account-merge [OPTIONS] --source-account --account ` ###### **Options:** +* `--operation-source-account ` — Source account used for the operation +* `--fee ` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm + + Default value: `100` +* `--cost` — Output the cost execution to stderr +* `--instructions ` — Number of instructions to simulate +* `--build-only` — Build the transaction and only write the base64 xdr to stdout +* `--sim-only` — (Deprecated) simulate the transaction and only write the base64 xdr to stdout +* `--rpc-url ` — RPC server endpoint +* `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider +* `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server +* `--network ` — Name of network to use from config +* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail +* `--hd-path ` — If using a seed phrase, which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` * `--global` — Use global config * `--config-dir ` — Location of config directory, default is "." -* `--operation-source-account ` — Source account used for the operation * `--account ` — Muxed Account to merge with, e.g. `GBX...`, 'MBX...' @@ -1845,13 +1859,26 @@ Transfers the XLM balance of an account to another account and removes the sourc Bumps forward the sequence number of the source account to the given sequence number, invalidating any transaction with a smaller sequence number -**Usage:** `stellar tx operation add bump-sequence [OPTIONS] --bump-to ` +**Usage:** `stellar tx operation add bump-sequence [OPTIONS] --source-account --bump-to ` ###### **Options:** +* `--operation-source-account ` — Source account used for the operation +* `--fee ` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm + + Default value: `100` +* `--cost` — Output the cost execution to stderr +* `--instructions ` — Number of instructions to simulate +* `--build-only` — Build the transaction and only write the base64 xdr to stdout +* `--sim-only` — (Deprecated) simulate the transaction and only write the base64 xdr to stdout +* `--rpc-url ` — RPC server endpoint +* `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider +* `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server +* `--network ` — Name of network to use from config +* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail +* `--hd-path ` — If using a seed phrase, which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` * `--global` — Use global config * `--config-dir ` — Location of config directory, default is "." -* `--operation-source-account ` — Source account used for the operation * `--bump-to ` — Sequence number to bump to @@ -1862,13 +1889,26 @@ Creates, updates, or deletes a trustline Learn more about trustlines https://developers.stellar.org/docs/learn/fundamentals/stellar-data-structures/accounts#trustlines -**Usage:** `stellar tx operation add change-trust [OPTIONS] --line ` +**Usage:** `stellar tx operation add change-trust [OPTIONS] --source-account --line ` ###### **Options:** +* `--operation-source-account ` — Source account used for the operation +* `--fee ` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm + + Default value: `100` +* `--cost` — Output the cost execution to stderr +* `--instructions ` — Number of instructions to simulate +* `--build-only` — Build the transaction and only write the base64 xdr to stdout +* `--sim-only` — (Deprecated) simulate the transaction and only write the base64 xdr to stdout +* `--rpc-url ` — RPC server endpoint +* `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider +* `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server +* `--network ` — Name of network to use from config +* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail +* `--hd-path ` — If using a seed phrase, which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` * `--global` — Use global config * `--config-dir ` — Location of config directory, default is "." -* `--operation-source-account ` — Source account used for the operation * `--line ` * `--limit ` — Limit for the trust line, 0 to remove the trust line @@ -1880,13 +1920,26 @@ https://developers.stellar.org/docs/learn/fundamentals/stellar-data-structures/a Creates and funds a new account with the specified starting balance -**Usage:** `stellar tx operation add create-account [OPTIONS] --destination ` +**Usage:** `stellar tx operation add create-account [OPTIONS] --source-account --destination ` ###### **Options:** +* `--operation-source-account ` — Source account used for the operation +* `--fee ` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm + + Default value: `100` +* `--cost` — Output the cost execution to stderr +* `--instructions ` — Number of instructions to simulate +* `--build-only` — Build the transaction and only write the base64 xdr to stdout +* `--sim-only` — (Deprecated) simulate the transaction and only write the base64 xdr to stdout +* `--rpc-url ` — RPC server endpoint +* `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider +* `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server +* `--network ` — Name of network to use from config +* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail +* `--hd-path ` — If using a seed phrase, which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` * `--global` — Use global config * `--config-dir ` — Location of config directory, default is "." -* `--operation-source-account ` — Source account used for the operation * `--destination ` — Account Id to create, e.g. `GBX...` * `--starting-balance ` — Initial balance in stroops of the account, default 1 XLM @@ -1900,13 +1953,26 @@ Sets, modifies, or deletes a data entry (name/value pair) that is attached to an Learn more about entries and subentries: https://developers.stellar.org/docs/learn/fundamentals/stellar-data-structures/accounts#subentries -**Usage:** `stellar tx operation add manage-data [OPTIONS] --data-name ` +**Usage:** `stellar tx operation add manage-data [OPTIONS] --source-account --data-name ` ###### **Options:** +* `--operation-source-account ` — Source account used for the operation +* `--fee ` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm + + Default value: `100` +* `--cost` — Output the cost execution to stderr +* `--instructions ` — Number of instructions to simulate +* `--build-only` — Build the transaction and only write the base64 xdr to stdout +* `--sim-only` — (Deprecated) simulate the transaction and only write the base64 xdr to stdout +* `--rpc-url ` — RPC server endpoint +* `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider +* `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server +* `--network ` — Name of network to use from config +* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail +* `--hd-path ` — If using a seed phrase, which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` * `--global` — Use global config * `--config-dir ` — Location of config directory, default is "." -* `--operation-source-account ` — Source account used for the operation * `--data-name ` — String up to 64 bytes long. If this is a new Name it will add the given name/value pair to the account. If this Name is already present then the associated value will be modified * `--data-value ` — Up to 64 bytes long hex string If not present then the existing Name will be deleted. If present then this value will be set in the `DataEntry` @@ -1916,13 +1982,26 @@ https://developers.stellar.org/docs/learn/fundamentals/stellar-data-structures/a Sends an amount in a specific asset to a destination account -**Usage:** `stellar tx operation add payment [OPTIONS] --destination --amount ` +**Usage:** `stellar tx operation add payment [OPTIONS] --source-account --destination --amount ` ###### **Options:** +* `--operation-source-account ` — Source account used for the operation +* `--fee ` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm + + Default value: `100` +* `--cost` — Output the cost execution to stderr +* `--instructions ` — Number of instructions to simulate +* `--build-only` — Build the transaction and only write the base64 xdr to stdout +* `--sim-only` — (Deprecated) simulate the transaction and only write the base64 xdr to stdout +* `--rpc-url ` — RPC server endpoint +* `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider +* `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server +* `--network ` — Name of network to use from config +* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail +* `--hd-path ` — If using a seed phrase, which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` * `--global` — Use global config * `--config-dir ` — Location of config directory, default is "." -* `--operation-source-account ` — Source account used for the operation * `--destination ` — Account to send to, e.g. `GBX...` * `--asset ` — Asset to send, default native, e.i. XLM @@ -1941,13 +2020,26 @@ https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md Learn more about signers operations and key weight: https://developers.stellar.org/docs/learn/encyclopedia/security/signatures-multisig#multisig -**Usage:** `stellar tx operation add set-options [OPTIONS]` +**Usage:** `stellar tx operation add set-options [OPTIONS] --source-account ` ###### **Options:** +* `--operation-source-account ` — Source account used for the operation +* `--fee ` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm + + Default value: `100` +* `--cost` — Output the cost execution to stderr +* `--instructions ` — Number of instructions to simulate +* `--build-only` — Build the transaction and only write the base64 xdr to stdout +* `--sim-only` — (Deprecated) simulate the transaction and only write the base64 xdr to stdout +* `--rpc-url ` — RPC server endpoint +* `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider +* `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server +* `--network ` — Name of network to use from config +* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail +* `--hd-path ` — If using a seed phrase, which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` * `--global` — Use global config * `--config-dir ` — Location of config directory, default is "." -* `--operation-source-account ` — Source account used for the operation * `--inflation-dest ` — Account of the inflation destination * `--master-weight ` — A number from 0-255 (inclusive) representing the weight of the master key. If the weight of the master key is updated to 0, it is effectively disabled * `--low-threshold ` — A number from 0-255 (inclusive) representing the threshold this account sets on all operations it performs that have a low threshold. https://developers.stellar.org/docs/learn/encyclopedia/security/signatures-multisig#multisig @@ -1975,14 +2067,27 @@ If you are modifying a trustline to a pool share, however, this is composed of t Learn more about flags: https://developers.stellar.org/docs/learn/glossary#flags -**Usage:** `stellar tx operation add set-trustline-flags [OPTIONS] --trustor --asset ` +**Usage:** `stellar tx operation add set-trustline-flags [OPTIONS] --source-account --trustor --asset ` ###### **Options:** +* `--operation-source-account ` — Source account used for the operation +* `--fee ` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm + + Default value: `100` +* `--cost` — Output the cost execution to stderr +* `--instructions ` — Number of instructions to simulate +* `--build-only` — Build the transaction and only write the base64 xdr to stdout +* `--sim-only` — (Deprecated) simulate the transaction and only write the base64 xdr to stdout +* `--rpc-url ` — RPC server endpoint +* `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider +* `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server +* `--network ` — Name of network to use from config +* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail +* `--hd-path ` — If using a seed phrase, which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` * `--global` — Use global config * `--config-dir ` — Location of config directory, default is "." -* `--operation-source-account ` — Source account used for the operation -* `--trustor ` — Account to set trustline flags for +* `--trustor ` — Account to set trustline flags for, e.g. `GBX...`, or alias, or muxed account, `M123...`` * `--asset ` — Asset to set trustline flags for * `--set-authorize` — Signifies complete authorization allowing an account to transact freely with the asset to make and receive payments and place orders * `--set-authorize-to-maintain-liabilities` — Denotes limited authorization that allows an account to maintain current orders but not to otherwise transact with the asset @@ -2021,6 +2126,7 @@ Sign a transaction envelope appending the signature to the envelope * `--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 ` — 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` * `--sign-with-lab` — Sign with https://lab.stellar.org +* `--sign-with-ledger` — Sign with a ledger wallet * `--rpc-url ` — RPC server endpoint * `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider * `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server diff --git a/cmd/crates/soroban-test/Cargo.toml b/cmd/crates/soroban-test/Cargo.toml index ddb10d5f7..f5bb6bcfd 100644 --- a/cmd/crates/soroban-test/Cargo.toml +++ b/cmd/crates/soroban-test/Cargo.toml @@ -24,6 +24,7 @@ stellar-strkey = { workspace = true } sep5 = { workspace = true } soroban-cli = { workspace = true } soroban-rpc = { workspace = true } +stellar-ledger = { workspace = true } thiserror = "1.0.31" sha2 = "0.10.6" @@ -32,6 +33,7 @@ assert_fs = "1.0.7" predicates = { workspace = true } fs_extra = "1.3.0" toml = { workspace = true } +testcontainers = { workspace = true } [dev-dependencies] serde_json = "1.0.93" @@ -41,7 +43,9 @@ walkdir = "2.4.0" ulid.workspace = true ed25519-dalek = { workspace = true } hex = { workspace = true } +test-case = "3.3.1" httpmock = { workspace = true } [features] it = [] +emulator-tests = ["stellar-ledger/emulator-tests"] diff --git a/cmd/crates/soroban-test/src/lib.rs b/cmd/crates/soroban-test/src/lib.rs index 4f36a0b33..fdaa5052c 100644 --- a/cmd/crates/soroban-test/src/lib.rs +++ b/cmd/crates/soroban-test/src/lib.rs @@ -36,6 +36,7 @@ use soroban_cli::{ }; mod wasm; + pub use wasm::Wasm; pub const TEST_ACCOUNT: &str = "test"; @@ -299,9 +300,10 @@ impl TestEnv { } /// Returns the public key corresponding to the test keys's `hd_path` - pub fn test_address(&self, hd_path: usize) -> String { + pub async fn test_address(&self, hd_path: usize) -> String { self.cmd::(&format!("--hd-path={hd_path}")) .public_key() + .await .unwrap() .to_string() } @@ -330,6 +332,21 @@ impl TestEnv { pub fn client(&self) -> soroban_rpc::Client { self.network.rpc_client().unwrap() } + + #[cfg(feature = "emulator-tests")] + pub async fn speculos_container( + ledger_device_model: &str, + ) -> testcontainers::ContainerAsync + { + use stellar_ledger::emulator_test_support::{ + enable_hash_signing, get_container, wait_for_emulator_start_text, + }; + let container = get_container(ledger_device_model).await; + let ui_host_port: u16 = container.get_host_port_ipv4(5000).await.unwrap(); + wait_for_emulator_start_text(ui_host_port).await; + enable_hash_signing(ui_host_port).await; + container + } } pub fn temp_ledger_file() -> OsString { diff --git a/cmd/crates/soroban-test/tests/it/emulator.rs b/cmd/crates/soroban-test/tests/it/emulator.rs new file mode 100644 index 000000000..b959c541c --- /dev/null +++ b/cmd/crates/soroban-test/tests/it/emulator.rs @@ -0,0 +1,90 @@ +use stellar_ledger::Blob; + +use soroban_test::{AssertExt, TestEnv}; +use std::sync::Arc; + +use stellar_ledger::emulator_test_support::*; + +use soroban_cli::{ + tx::builder::TxExt, + xdr::{self, Limits, OperationBody, ReadXdr, TransactionEnvelope, WriteXdr}, +}; + +use test_case::test_case; + +#[test_case("nanos", 0; "when the device is NanoS")] +#[test_case("nanox", 1; "when the device is NanoX")] +#[test_case("nanosp", 2; "when the device is NanoS Plus")] +#[tokio::test] +async fn test_signer(ledger_device_model: &str, hd_path: u32) { + let sandbox = Arc::new(TestEnv::new()); + let container = TestEnv::speculos_container(ledger_device_model).await; + let host_port = container.get_host_port_ipv4(9998).await.unwrap(); + let ui_host_port = container.get_host_port_ipv4(5000).await.unwrap(); + + let ledger = ledger(host_port).await; + + let key = ledger.get_public_key(&hd_path.into()).await.unwrap(); + + let verifying_key = ed25519_dalek::VerifyingKey::from_bytes(&key.0).unwrap(); + let body: OperationBody = + (&soroban_cli::commands::tx::new::bump_sequence::Args { bump_to: 100 }).into(); + let operation = xdr::Operation { + body, + source_account: None, + }; + let source_account = xdr::MuxedAccount::Ed25519(key.0.into()); + let tx_env: TransactionEnvelope = + xdr::Transaction::new_tx(source_account, 100, 100, operation).into(); + let tx_env = tx_env.to_xdr_base64(Limits::none()).unwrap(); + + let hash: xdr::Hash = sandbox + .new_assert_cmd("tx") + .arg("hash") + .write_stdin(tx_env.as_bytes()) + .assert() + .success() + .stdout_as_str() + .parse() + .unwrap(); + + let sign = tokio::task::spawn_blocking({ + let sandbox = Arc::clone(&sandbox); + + move || { + sandbox + .new_assert_cmd("tx") + .arg("sign") + .arg("--sign-with-ledger") + .arg("--hd-path") + .arg(hd_path.to_string()) + .write_stdin(tx_env.as_bytes()) + .env("SPECULOS_PORT", host_port.to_string()) + .env("RUST_LOGS", "trace") + .assert() + .success() + .stdout_as_str() + } + }); + + let approve = tokio::task::spawn(approve_tx_hash_signature( + ui_host_port, + ledger_device_model.to_string(), + )); + + let response = sign.await.unwrap(); + approve.await.unwrap(); + let txn_env = + xdr::TransactionEnvelope::from_xdr_base64(&response, xdr::Limits::none()).unwrap(); + let xdr::TransactionEnvelope::Tx(tx_env) = txn_env else { + panic!("expected Tx") + }; + let signatures = tx_env.signatures.to_vec(); + let signature = signatures[0].signature.to_vec(); + verifying_key + .verify_strict( + &hash.0, + &ed25519_dalek::Signature::from_slice(&signature).unwrap(), + ) + .unwrap(); +} diff --git a/cmd/crates/soroban-test/tests/it/integration/tx.rs b/cmd/crates/soroban-test/tests/it/integration/tx.rs index 3fa85bc09..e4cb17779 100644 --- a/cmd/crates/soroban-test/tests/it/integration/tx.rs +++ b/cmd/crates/soroban-test/tests/it/integration/tx.rs @@ -2,20 +2,37 @@ use soroban_cli::assembled::simulate_and_assemble_transaction; use soroban_cli::xdr::{Limits, ReadXdr, TransactionEnvelope, WriteXdr}; use soroban_test::{AssertExt, TestEnv}; -use crate::integration::util::{deploy_contract, DeployKind, HELLO_WORLD}; +use crate::integration::util::{deploy_contract, DeployKind, DeployOptions, HELLO_WORLD}; pub mod operations; #[tokio::test] async fn simulate() { let sandbox = &TestEnv::new(); - let xdr_base64_build_only = - deploy_contract(sandbox, HELLO_WORLD, DeployKind::BuildOnly, None).await; - let xdr_base64_sim_only = - deploy_contract(sandbox, HELLO_WORLD, DeployKind::SimOnly, None).await; + let salt = Some(String::from("A")); + let xdr_base64_build_only = deploy_contract( + sandbox, + HELLO_WORLD, + DeployOptions { + kind: DeployKind::BuildOnly, + salt: salt.clone(), + ..Default::default() + }, + ) + .await; + let xdr_base64_sim_only = deploy_contract( + sandbox, + HELLO_WORLD, + DeployOptions { + kind: DeployKind::SimOnly, + salt: salt.clone(), + ..Default::default() + }, + ) + .await; let tx_env = TransactionEnvelope::from_xdr_base64(&xdr_base64_build_only, Limits::none()).unwrap(); - let tx = soroban_cli::commands::tx::xdr::unwrap_envelope_v1(tx_env).unwrap(); + let tx = soroban_cli::commands::tx::xdr::unwrap_envelope_v1(tx_env.clone()).unwrap(); let assembled_str = sandbox .new_assert_cmd("tx") .arg("simulate") @@ -23,6 +40,11 @@ async fn simulate() { .assert() .success() .stdout_as_str(); + let tx_env_from_cli_tx = + TransactionEnvelope::from_xdr_base64(&assembled_str, Limits::none()).unwrap(); + let tx_env_sim_only = + TransactionEnvelope::from_xdr_base64(&xdr_base64_sim_only, Limits::none()).unwrap(); + assert_eq!(tx_env_from_cli_tx, tx_env_sim_only); assert_eq!(xdr_base64_sim_only, assembled_str); let assembled = simulate_and_assemble_transaction(&sandbox.client(), &tx) .await @@ -56,20 +78,37 @@ async fn txn_hash() { #[tokio::test] async fn build_simulate_sign_send() { let sandbox = &TestEnv::new(); + build_sim_sign_send(sandbox, "test", "--sign-with-key=test").await; +} + +pub(crate) async fn build_sim_sign_send(sandbox: &TestEnv, account: &str, sign_with: &str) { sandbox .new_assert_cmd("contract") .arg("install") - .args(["--wasm", HELLO_WORLD.path().as_os_str().to_str().unwrap()]) + .args([ + "--wasm", + HELLO_WORLD.path().as_os_str().to_str().unwrap(), + "--source", + account, + ]) .assert() .success(); - let tx_simulated = deploy_contract(sandbox, HELLO_WORLD, DeployKind::SimOnly, None).await; + let tx_simulated = deploy_contract( + sandbox, + HELLO_WORLD, + DeployOptions { + kind: DeployKind::SimOnly, + ..Default::default() + }, + ) + .await; dbg!("{tx_simulated}"); let tx_signed = sandbox .new_assert_cmd("tx") .arg("sign") - .arg("--sign-with-key=test") + .arg(sign_with) .write_stdin(tx_simulated.as_bytes()) .assert() .success() diff --git a/cmd/crates/soroban-test/tests/it/integration/tx/operations.rs b/cmd/crates/soroban-test/tests/it/integration/tx/operations.rs index 8c894e5dc..c250f1221 100644 --- a/cmd/crates/soroban-test/tests/it/integration/tx/operations.rs +++ b/cmd/crates/soroban-test/tests/it/integration/tx/operations.rs @@ -8,7 +8,7 @@ use soroban_test::{AssertExt, TestEnv}; use crate::integration::{ hello_world::invoke_hello_world, - util::{deploy_contract, DeployKind, HELLO_WORLD}, + util::{deploy_contract, DeployOptions, HELLO_WORLD}, }; pub fn test_address(sandbox: &TestEnv) -> String { @@ -84,11 +84,92 @@ async fn create_account() { .success(); let test_account_after = client.get_account(&test).await.unwrap(); assert!(test_account_after.balance < test_account.balance); - let id = deploy_contract(sandbox, HELLO_WORLD, DeployKind::Normal, Some("new")).await; + let id = deploy_contract( + sandbox, + HELLO_WORLD, + DeployOptions { + deployer: Some("new".to_string()), + ..Default::default() + }, + ) + .await; println!("{id}"); invoke_hello_world(sandbox, &id); } +#[tokio::test] +async fn create_account_with_alias() { + let sandbox = &TestEnv::new(); + sandbox + .new_assert_cmd("keys") + .args(["generate", "--no-fund", "new"]) + .assert() + .success(); + let test = test_address(sandbox); + let client = sandbox.client(); + let test_account = client.get_account(&test).await.unwrap(); + println!("test account has a balance of {}", test_account.balance); + let starting_balance = ONE_XLM * 100; + sandbox + .new_assert_cmd("tx") + .args([ + "new", + "create-account", + "--destination", + "new", + "--starting-balance", + starting_balance.to_string().as_str(), + ]) + .assert() + .success(); + let test_account_after = client.get_account(&test).await.unwrap(); + assert!(test_account_after.balance < test_account.balance); + let id = deploy_contract( + sandbox, + HELLO_WORLD, + DeployOptions { + deployer: Some("new".to_string()), + ..Default::default() + }, + ) + .await; + println!("{id}"); + invoke_hello_world(sandbox, &id); +} + +#[tokio::test] +async fn payment_with_alias() { + let sandbox = &TestEnv::new(); + let client = sandbox.client(); + let (test, test1) = setup_accounts(sandbox); + let test_account = client.get_account(&test).await.unwrap(); + println!("test account has a balance of {}", test_account.balance); + + let before = client.get_account(&test).await.unwrap(); + let test1_account_entry_before = client.get_account(&test1).await.unwrap(); + + sandbox + .new_assert_cmd("tx") + .args([ + "new", + "payment", + "--destination", + "test1", + "--amount", + ONE_XLM.to_string().as_str(), + ]) + .assert() + .success(); + let test1_account_entry = client.get_account(&test1).await.unwrap(); + assert_eq!( + ONE_XLM, + test1_account_entry.balance - test1_account_entry_before.balance, + "Should have One XLM more" + ); + let after = client.get_account(&test).await.unwrap(); + assert_eq!(before.balance - 10_000_100, after.balance); +} + #[tokio::test] async fn payment() { let sandbox = &TestEnv::new(); @@ -172,6 +253,33 @@ async fn account_merge() { assert_eq!(before.balance + before1.balance - fee, after.balance); } +#[tokio::test] +async fn account_merge_with_alias() { + let sandbox = &TestEnv::new(); + let client = sandbox.client(); + let (test, test1) = setup_accounts(sandbox); + let before = client.get_account(&test).await.unwrap(); + let before1 = client.get_account(&test1).await.unwrap(); + let fee = 100; + sandbox + .new_assert_cmd("tx") + .args([ + "new", + "account-merge", + "--source", + "test1", + "--account", + "test", + "--fee", + fee.to_string().as_str(), + ]) + .assert() + .success(); + let after = client.get_account(&test).await.unwrap(); + assert!(client.get_account(&test1).await.is_err()); + assert_eq!(before.balance + before1.balance - fee, after.balance); +} + #[tokio::test] async fn set_trustline_flags() { let sandbox = &TestEnv::new(); diff --git a/cmd/crates/soroban-test/tests/it/integration/util.rs b/cmd/crates/soroban-test/tests/it/integration/util.rs index fc7f824b6..554479184 100644 --- a/cmd/crates/soroban-test/tests/it/integration/util.rs +++ b/cmd/crates/soroban-test/tests/it/integration/util.rs @@ -27,10 +27,10 @@ where assert_eq!(res, data); } -pub const TEST_SALT: &str = "f55ff16f66f43360266b95db6f8fec01d76031054306ae4a4b380598f6cfd114"; - +#[derive(Default)] pub enum DeployKind { BuildOnly, + #[default] Normal, SimOnly, } @@ -46,45 +46,55 @@ impl Display for DeployKind { } pub async fn deploy_hello(sandbox: &TestEnv) -> String { - deploy_contract(sandbox, HELLO_WORLD, DeployKind::Normal, None).await + deploy_contract(sandbox, HELLO_WORLD, DeployOptions::default()).await } pub async fn deploy_custom(sandbox: &TestEnv) -> String { - deploy_contract(sandbox, CUSTOM_TYPES, DeployKind::Normal, None).await + deploy_contract(sandbox, CUSTOM_TYPES, DeployOptions::default()).await } pub async fn deploy_swap(sandbox: &TestEnv) -> String { - deploy_contract(sandbox, SWAP, DeployKind::Normal, None).await + deploy_contract(sandbox, SWAP, DeployOptions::default()).await } pub async fn deploy_custom_account(sandbox: &TestEnv) -> String { - deploy_contract(sandbox, CUSTOM_ACCOUNT, DeployKind::Normal, None).await + deploy_contract(sandbox, CUSTOM_ACCOUNT, DeployOptions::default()).await +} + +#[derive(Default)] +pub struct DeployOptions { + pub kind: DeployKind, + pub deployer: Option, + pub salt: Option, } pub async fn deploy_contract( sandbox: &TestEnv, wasm: &Wasm<'static>, - deploy: DeployKind, - deployer: Option<&str>, + DeployOptions { + kind, + deployer, + salt, + }: DeployOptions, ) -> String { - let cmd = sandbox.cmd_with_config::<_, commands::contract::deploy::wasm::Cmd>( + let mut cmd = sandbox.cmd_with_config::<_, commands::contract::deploy::wasm::Cmd>( &[ "--fee", "1000000", "--wasm", &wasm.path().to_string_lossy(), - "--salt", - TEST_SALT, "--ignore-checks", - &deploy.to_string(), + &kind.to_string(), ], None, ); + cmd.salt = salt; + let res = sandbox - .run_cmd_with(cmd, deployer.unwrap_or("test")) + .run_cmd_with(cmd, deployer.as_deref().unwrap_or("test")) .await .unwrap(); - match deploy { + match kind { DeployKind::BuildOnly | DeployKind::SimOnly => match res.to_envelope() { commands::txn_result::TxnEnvelopeResult::TxnEnvelope(e) => { return e.to_xdr_base64(Limits::none()).unwrap() diff --git a/cmd/crates/soroban-test/tests/it/main.rs b/cmd/crates/soroban-test/tests/it/main.rs index 4dc54a194..01f695789 100644 --- a/cmd/crates/soroban-test/tests/it/main.rs +++ b/cmd/crates/soroban-test/tests/it/main.rs @@ -1,9 +1,11 @@ mod arg_parsing; mod build; mod config; +#[cfg(feature = "emulator-tests")] +mod emulator; mod help; mod init; -// #[cfg(feature = "it")] +#[cfg(feature = "it")] mod integration; mod plugin; mod rpc_provider; diff --git a/cmd/crates/stellar-ledger/Cargo.toml b/cmd/crates/stellar-ledger/Cargo.toml index f06d17a2b..190b0373c 100644 --- a/cmd/crates/stellar-ledger/Cargo.toml +++ b/cmd/crates/stellar-ledger/Cargo.toml @@ -16,7 +16,6 @@ rust-version.workspace = true publish = false [dependencies] -soroban-spec = { workspace = true } thiserror = "1.0.32" serde = "1.0.82" serde_derive = "1.0.82" @@ -26,7 +25,6 @@ ed25519-dalek = { workspace = true } stellar-strkey = { workspace = true } ledger-transport-hid = "0.10.0" ledger-transport = "0.10.0" -sep5.workspace = true slip10 = { package = "slipped10", version = "0.4.6" } tracing = { workspace = true } hex.workspace = true @@ -35,10 +33,9 @@ bollard = { workspace = true } home = "0.5.9" tokio = { version = "1", features = ["full"] } reqwest = { workspace = true, features = ["json"] } -soroban-rpc.workspace = true -phf = { version = "0.11.2", features = ["macros"] } -futures = "0.3.30" +phf = { version = "0.11.2", features = ["macros"], optional = true } async-trait = { workspace = true } +testcontainers = { workspace = true, optional = true } [dependencies.stellar-xdr] workspace = true @@ -46,15 +43,16 @@ features = ["curr", "std", "serde"] [dev-dependencies] env_logger = "0.11.3" -futures = "0.3.30" log = "0.4.21" once_cell = "1.19.0" pretty_assertions = "1.2.1" serial_test = "3.0.0" httpmock = { workspace = true } test-case = "3.3.1" -testcontainers = "0.20.1" + [features] -emulator-tests = [] +default = ["http-transport"] +emulator-tests = ["dep:testcontainers", "http-transport", "dep:phf"] +http-transport = [] diff --git a/cmd/crates/stellar-ledger/src/emulator_test_support.rs b/cmd/crates/stellar-ledger/src/emulator_test_support.rs new file mode 100644 index 000000000..e69ec128f --- /dev/null +++ b/cmd/crates/stellar-ledger/src/emulator_test_support.rs @@ -0,0 +1,7 @@ +pub mod http_transport; +#[cfg(feature = "emulator-tests")] +pub mod speculos; +#[cfg(feature = "emulator-tests")] +pub mod util; +#[cfg(feature = "emulator-tests")] +pub use util::*; diff --git a/cmd/crates/stellar-ledger/tests/utils/emulator_http_transport.rs b/cmd/crates/stellar-ledger/src/emulator_test_support/http_transport.rs similarity index 95% rename from cmd/crates/stellar-ledger/tests/utils/emulator_http_transport.rs rename to cmd/crates/stellar-ledger/src/emulator_test_support/http_transport.rs index c90c28d92..48ab4423d 100644 --- a/cmd/crates/stellar-ledger/tests/utils/emulator_http_transport.rs +++ b/cmd/crates/stellar-ledger/src/emulator_test_support/http_transport.rs @@ -23,7 +23,7 @@ pub enum LedgerZemuError { InnerError, } -pub struct EmulatorHttpTransport { +pub struct Emulator { url: String, } @@ -39,8 +39,9 @@ struct ZemuResponse { error: Option, } -impl EmulatorHttpTransport { +impl Emulator { #[allow(dead_code)] //this is being used in tests only + #[must_use] pub fn new(host: &str, port: u16) -> Self { Self { url: format!("http://{host}:{port}"), @@ -49,7 +50,7 @@ impl EmulatorHttpTransport { } #[async_trait] -impl Exchange for EmulatorHttpTransport { +impl Exchange for Emulator { type Error = LedgerZemuError; type AnswerType = Vec; @@ -72,7 +73,7 @@ impl Exchange for EmulatorHttpTransport { let resp: Response = HttpClient::new() .post(&self.url) .headers(headers) - .timeout(Duration::from_secs(25)) + .timeout(Duration::from_secs(60)) .json(&request) .send() .await diff --git a/cmd/crates/stellar-ledger/tests/utils/speculos.rs b/cmd/crates/stellar-ledger/src/emulator_test_support/speculos.rs similarity index 59% rename from cmd/crates/stellar-ledger/tests/utils/speculos.rs rename to cmd/crates/stellar-ledger/src/emulator_test_support/speculos.rs index 57439b0af..f8b95c13a 100644 --- a/cmd/crates/stellar-ledger/tests/utils/speculos.rs +++ b/cmd/crates/stellar-ledger/src/emulator_test_support/speculos.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, collections::HashMap, path::PathBuf}; +use std::{borrow::Cow, collections::HashMap, path::PathBuf, str::FromStr}; use testcontainers::{ core::{Mount, WaitFor}, Image, @@ -55,14 +55,54 @@ impl Speculos { } fn get_cmd(ledger_device_model: String) -> String { - let device_model = ledger_device_model.clone(); - let container_elf_path = match device_model.as_str() { - "nanos" => format!("{DEFAULT_APP_PATH}/stellarNanoSApp.elf"), - "nanosp" => format!("{DEFAULT_APP_PATH}/stellarNanoSPApp.elf"), - "nanox" => format!("{DEFAULT_APP_PATH}/stellarNanoXApp.elf"), - _ => panic!("Unsupported device model"), - }; - format!("/home/zondax/speculos/speculos.py --log-level speculos:DEBUG --color JADE_GREEN --display headless -s {TEST_SEED_PHRASE} -m {device_model} {container_elf_path}") + let device_model: DeviceModel = ledger_device_model.parse().unwrap(); + let container_elf_path = format!("{DEFAULT_APP_PATH}/{}", device_model.as_file()); + format!( + "/home/zondax/speculos/speculos.py --log-level speculos:DEBUG --color JADE_GREEN \ + --display headless \ + -s {TEST_SEED_PHRASE} \ + -m {device_model} {container_elf_path}" + ) + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum DeviceModel { + NanoS, + NanoSP, + NanoX, +} + +impl FromStr for DeviceModel { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "nanos" => Ok(DeviceModel::NanoS), + "nanosp" => Ok(DeviceModel::NanoSP), + "nanox" => Ok(DeviceModel::NanoX), + _ => Err(format!("Unsupported device model: {}", s)), + } + } +} + +impl std::fmt::Display for DeviceModel { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + DeviceModel::NanoS => write!(f, "nanos"), + DeviceModel::NanoSP => write!(f, "nanosp"), + DeviceModel::NanoX => write!(f, "nanox"), + } + } +} + +impl DeviceModel { + pub fn as_file(&self) -> &str { + match self { + DeviceModel::NanoS => "stellarNanoSApp.elf", + DeviceModel::NanoSP => "stellarNanoSPApp.elf", + DeviceModel::NanoX => "stellarNanoXApp.elf", + } } } @@ -92,4 +132,4 @@ impl Image for Speculos { fn cmd(&self) -> impl IntoIterator>> { vec![self.cmd.clone()].into_iter() } -} \ No newline at end of file +} diff --git a/cmd/crates/stellar-ledger/src/emulator_test_support/util.rs b/cmd/crates/stellar-ledger/src/emulator_test_support/util.rs new file mode 100644 index 000000000..143cfca2b --- /dev/null +++ b/cmd/crates/stellar-ledger/src/emulator_test_support/util.rs @@ -0,0 +1,199 @@ +use serde::Deserialize; +use std::ops::Range; +use std::sync::LazyLock; +use std::sync::Mutex; + +use crate::{Error, LedgerSigner}; +use std::net::TcpListener; + +use super::{http_transport::Emulator, speculos::Speculos}; + +use std::{collections::HashMap, time::Duration}; + +use stellar_xdr::curr::Hash; + +use testcontainers::{core::ContainerPort, runners::AsyncRunner, ContainerAsync, ImageExt}; +use tokio::time::sleep; + +static PORT_RANGE: LazyLock>> = LazyLock::new(|| Mutex::new(40000..50000)); + +pub const TEST_NETWORK_PASSPHRASE: &[u8] = b"Test SDF Network ; September 2015"; +pub fn test_network_hash() -> Hash { + use sha2::Digest; + Hash(sha2::Sha256::digest(TEST_NETWORK_PASSPHRASE).into()) +} + +pub async fn ledger(host_port: u16) -> LedgerSigner { + LedgerSigner::new(get_http_transport("127.0.0.1", host_port).await.unwrap()) +} + +pub async fn click(ui_host_port: u16, url: &str) { + let previous_events = get_emulator_events(ui_host_port).await; + + let client = reqwest::Client::new(); + let mut payload = HashMap::new(); + payload.insert("action", "press-and-release"); + + let mut screen_has_changed = false; + + client + .post(format!("http://localhost:{ui_host_port}/{url}")) + .json(&payload) + .send() + .await + .unwrap(); + + while !screen_has_changed { + let current_events = get_emulator_events(ui_host_port).await; + + if !(previous_events == current_events) { + screen_has_changed = true + } + } + + sleep(Duration::from_secs(1)).await; +} + +pub async fn enable_hash_signing(ui_host_port: u16) { + click(ui_host_port, "button/right").await; + + click(ui_host_port, "button/both").await; + + click(ui_host_port, "button/both").await; + + click(ui_host_port, "button/right").await; + + click(ui_host_port, "button/right").await; + + click(ui_host_port, "button/both").await; +} + +#[derive(Debug, Deserialize, PartialEq)] +pub struct EmulatorEvent { + text: String, + x: u16, + y: u16, + w: u16, + h: u16, +} + +#[derive(Debug, Deserialize)] +struct EventsResponse { + events: Vec, +} + +pub async fn get_container(ledger_device_model: &str) -> ContainerAsync { + let (tcp_port_1, tcp_port_2) = get_available_ports(2); + Speculos::new(ledger_device_model.to_string()) + .with_mapped_port(tcp_port_1, ContainerPort::Tcp(9998)) + .with_mapped_port(tcp_port_2, ContainerPort::Tcp(5000)) + .start() + .await + .unwrap() +} + +pub fn get_available_ports(n: usize) -> (u16, u16) { + let mut range = PORT_RANGE.lock().unwrap(); + let mut ports = Vec::with_capacity(n); + while ports.len() < n { + if let Some(port) = range.next() { + if let Ok(listener) = TcpListener::bind(("0.0.0.0", port)) { + ports.push(port); + drop(listener); + } + } else { + panic!("No more available ports"); + } + } + + (ports[0], ports[1]) +} + +pub async fn get_http_transport(host: &str, port: u16) -> Result { + let max_retries = 5; + let mut retries = 0; + let mut wait_time = Duration::from_secs(1); + // ping the emulator port to make sure it's up and running + // retry with exponential backoff + loop { + match reqwest::get(format!("http://{host}:{port}")).await { + Ok(_) => return Ok(Emulator::new(host, port)), + Err(e) => { + retries += 1; + if retries >= max_retries { + println!("get_http_transport: Exceeded max retries for connecting to emulated device"); + + return Err(Error::APDUExchangeError(format!( + "Failed to connect to emulator: {e}" + ))); + } + sleep(wait_time).await; + wait_time *= 2; + } + } + } +} + +pub async fn wait_for_emulator_start_text(ui_host_port: u16) { + let mut ready = false; + while !ready { + let events = get_emulator_events_with_retries(ui_host_port, 5).await; + + if events.iter().any(|event| event.text == "is ready") { + ready = true; + } + } +} + +pub async fn get_emulator_events(ui_host_port: u16) -> Vec { + // Allowing for less retries here because presumably the emulator should be up and running since we waited + // for the "is ready" text via wait_for_emulator_start_text + get_emulator_events_with_retries(ui_host_port, 1).await +} + +pub async fn get_emulator_events_with_retries( + ui_host_port: u16, + max_retries: u16, +) -> Vec { + let client = reqwest::Client::new(); + let mut retries = 0; + let mut wait_time = Duration::from_secs(1); + loop { + match client + .get(format!("http://localhost:{ui_host_port}/events")) + .send() + .await + { + Ok(req) => { + let resp = req.json::().await.unwrap(); + return resp.events; + } + Err(e) => { + retries += 1; + if retries >= max_retries { + println!("get_emulator_events_with_retries: Exceeded max retries"); + panic!("get_emulator_events_with_retries: Failed to get emulator events: {e}"); + } + sleep(wait_time).await; + wait_time *= 2; + } + } + } +} + +pub async fn approve_tx_hash_signature(ui_host_port: u16, device_model: String) { + let number_of_right_clicks = if device_model == "nanos" { 10 } else { 6 }; + for _ in 0..number_of_right_clicks { + click(ui_host_port, "button/right").await; + } + + click(ui_host_port, "button/both").await; +} + +pub async fn approve_tx_signature(ui_host_port: u16, device_model: String) { + let number_of_right_clicks = if device_model == "nanos" { 17 } else { 11 }; + for _ in 0..number_of_right_clicks { + click(ui_host_port, "button/right").await; + } + click(ui_host_port, "button/both").await; +} diff --git a/cmd/crates/stellar-ledger/src/hd_path.rs b/cmd/crates/stellar-ledger/src/hd_path.rs index 07ed133f1..79fca40a2 100644 --- a/cmd/crates/stellar-ledger/src/hd_path.rs +++ b/cmd/crates/stellar-ledger/src/hd_path.rs @@ -4,6 +4,7 @@ use crate::Error; pub struct HdPath(pub u32); impl HdPath { + #[must_use] pub fn depth(&self) -> u8 { let path: slip10::BIP32Path = self.into(); path.depth() @@ -23,6 +24,9 @@ impl From<&u32> for HdPath { } impl HdPath { + /// # Errors + /// + /// Could fail to convert the path to bytes pub fn to_vec(&self) -> Result, Error> { hd_path_to_bytes(&self.into()) } diff --git a/cmd/crates/stellar-ledger/src/lib.rs b/cmd/crates/stellar-ledger/src/lib.rs index e90c31bb9..1cfc5d133 100644 --- a/cmd/crates/stellar-ledger/src/lib.rs +++ b/cmd/crates/stellar-ledger/src/lib.rs @@ -1,10 +1,14 @@ use hd_path::HdPath; -use ledger_transport::{APDUCommand, Exchange}; +use ledger_transport::APDUCommand; +pub use ledger_transport::Exchange; + use ledger_transport_hid::{ hidapi::{HidApi, HidError}, - LedgerHIDError, TransportNativeHID, + LedgerHIDError, }; +pub use ledger_transport_hid::TransportNativeHID; + use std::vec; use stellar_strkey::DecodeError; use stellar_xdr::curr::{ @@ -16,6 +20,8 @@ pub use crate::signer::Blob; pub mod hd_path; mod signer; +pub mod emulator_test_support; + // this is from https://github.com/LedgerHQ/ledger-live/blob/36cfbf3fa3300fd99bcee2ab72e1fd8f280e6280/libs/ledgerjs/packages/hw-app-str/src/Str.ts#L181 const APDU_MAX_SIZE: u8 = 150; const HD_PATH_ELEMENTS_COUNT: u8 = 3; @@ -79,6 +85,8 @@ pub struct LedgerSigner { unsafe impl Send for LedgerSigner where T: Exchange {} unsafe impl Sync for LedgerSigner where T: Exchange {} +/// # Errors +/// Could fail to make the connection to the Ledger device pub fn native() -> Result, Error> { Ok(LedgerSigner { transport: get_transport()?, @@ -92,6 +100,9 @@ where pub fn new(transport: T) -> Self { Self { transport } } + + /// # Errors + /// Returns an error if there is an issue with connecting with the device pub fn native() -> Result, Error> { Ok(LedgerSigner { transport: get_transport()?, @@ -298,18 +309,13 @@ pub fn test_network_hash() -> Hash { Hash(sha2::Sha256::digest(TEST_NETWORK_PASSPHRASE).into()) } -#[cfg(test)] +#[cfg(all(test, feature = "http-transport"))] mod test { - mod test_helpers { - pub mod test { - include!("../tests/utils/mod.rs"); - } - } use httpmock::prelude::*; use serde_json::json; + use super::emulator_test_support::http_transport::Emulator; use crate::Blob; - use test_helpers::test::emulator_http_transport::EmulatorHttpTransport; use std::vec; @@ -321,8 +327,8 @@ mod test { Memo, MuxedAccount, PaymentOp, Preconditions, SequenceNumber, TransactionExt, }; - fn ledger(server: &MockServer) -> LedgerSigner { - let transport = EmulatorHttpTransport::new(&server.host(), server.port()); + fn ledger(server: &MockServer) -> LedgerSigner { + let transport = Emulator::new(&server.host(), server.port()); LedgerSigner::new(transport) } diff --git a/cmd/crates/stellar-ledger/tests/test/emulator_tests.rs b/cmd/crates/stellar-ledger/tests/test/emulator_tests.rs index 1b0c09bab..880ac95ff 100644 --- a/cmd/crates/stellar-ledger/tests/test/emulator_tests.rs +++ b/cmd/crates/stellar-ledger/tests/test/emulator_tests.rs @@ -1,52 +1,23 @@ -use ledger_transport::Exchange; -use once_cell::sync::Lazy; -use serde::Deserialize; -use std::ops::Range; -use std::sync::Mutex; -use std::vec; - -use std::net::TcpListener; use stellar_ledger::hd_path::HdPath; -use stellar_ledger::{Blob, Error, LedgerSigner}; +use stellar_ledger::{Blob, Error}; use std::sync::Arc; -use std::{collections::HashMap, time::Duration}; use stellar_xdr::curr::{ - self as xdr, Hash, Memo, MuxedAccount, Operation, OperationBody, PaymentOp, Preconditions, + self as xdr, Memo, MuxedAccount, Operation, OperationBody, PaymentOp, Preconditions, SequenceNumber, Transaction, TransactionExt, Uint256, }; -use testcontainers::{core::ContainerPort, runners::AsyncRunner, ContainerAsync, ImageExt}; -use tokio::time::sleep; - -static PORT_RANGE: Lazy>> = Lazy::new(|| Mutex::new(40000..50000)); - -pub const TEST_NETWORK_PASSPHRASE: &[u8] = b"Test SDF Network ; September 2015"; -pub fn test_network_hash() -> Hash { - use sha2::Digest; - Hash(sha2::Sha256::digest(TEST_NETWORK_PASSPHRASE).into()) -} - -async fn ledger(host_port: u16) -> LedgerSigner { - LedgerSigner::new(get_http_transport("127.0.0.1", host_port).await.unwrap()) -} - -mod test_helpers { - pub mod test { - include!("../utils/mod.rs"); - } -} +use stellar_ledger::emulator_test_support::*; use test_case::test_case; -use test_helpers::test::{emulator_http_transport::EmulatorHttpTransport, speculos::Speculos}; -#[test_case("nanos".to_string() ; "when the device is NanoS")] -#[test_case("nanox".to_string() ; "when the device is NanoX")] -#[test_case("nanosp".to_string() ; "when the device is NanoS Plus")] +#[test_case("nanos"; "when the device is NanoS")] +#[test_case("nanox"; "when the device is NanoX")] +#[test_case("nanosp"; "when the device is NanoS Plus")] #[tokio::test] -async fn test_get_public_key(ledger_device_model: String) { - let container = get_container(ledger_device_model.clone()).await; +async fn test_get_public_key(ledger_device_model: &str) { + let container = get_container(ledger_device_model).await; let host_port = container.get_host_port_ipv4(9998).await.unwrap(); let ui_host_port: u16 = container.get_host_port_ipv4(5000).await.unwrap(); wait_for_emulator_start_text(ui_host_port).await; @@ -68,12 +39,12 @@ async fn test_get_public_key(ledger_device_model: String) { } } -#[test_case("nanos".to_string() ; "when the device is NanoS")] -#[test_case("nanox".to_string() ; "when the device is NanoX")] -#[test_case("nanosp".to_string() ; "when the device is NanoS Plus")] +#[test_case("nanos"; "when the device is NanoS")] +#[test_case("nanox"; "when the device is NanoX")] +#[test_case("nanosp"; "when the device is NanoS Plus")] #[tokio::test] -async fn test_get_app_configuration(ledger_device_model: String) { - let container = get_container(ledger_device_model.clone()).await; +async fn test_get_app_configuration(ledger_device_model: &str) { + let container = get_container(ledger_device_model).await; let host_port = container.get_host_port_ipv4(9998).await.unwrap(); let ui_host_port: u16 = container.get_host_port_ipv4(5000).await.unwrap(); wait_for_emulator_start_text(ui_host_port).await; @@ -91,12 +62,12 @@ async fn test_get_app_configuration(ledger_device_model: String) { }; } -#[test_case("nanos".to_string() ; "when the device is NanoS")] -#[test_case("nanox".to_string() ; "when the device is NanoX")] -#[test_case("nanosp".to_string() ; "when the device is NanoS Plus")] +#[test_case("nanos"; "when the device is NanoS")] +#[test_case("nanox"; "when the device is NanoX")] +#[test_case("nanosp"; "when the device is NanoS Plus")] #[tokio::test] -async fn test_sign_tx(ledger_device_model: String) { - let container = get_container(ledger_device_model.clone()).await; +async fn test_sign_tx(ledger_device_model: &str) { + let container = get_container(ledger_device_model).await; let host_port = container.get_host_port_ipv4(9998).await.unwrap(); let ui_host_port: u16 = container.get_host_port_ipv4(5000).await.unwrap(); wait_for_emulator_start_text(ui_host_port).await; @@ -141,7 +112,7 @@ async fn test_sign_tx(ledger_device_model: String) { fee: 100, seq_num: SequenceNumber(1), cond: Preconditions::None, - memo: Memo::Text("Stellar".as_bytes().try_into().unwrap()), + memo: Memo::Text("Stellar".try_into().unwrap()), ext: TransactionExt::V0, operations: [Operation { source_account: Some(MuxedAccount::Ed25519(Uint256(source_account_bytes))), @@ -159,7 +130,10 @@ async fn test_sign_tx(ledger_device_model: String) { let ledger = Arc::clone(&ledger); async move { ledger.sign_transaction(path, tx, test_network_hash()).await } }); - let approve = tokio::task::spawn(approve_tx_signature(ui_host_port, ledger_device_model)); + let approve = tokio::task::spawn(approve_tx_signature( + ui_host_port, + ledger_device_model.to_string(), + )); let result = sign.await.unwrap(); let _ = approve.await.unwrap(); @@ -175,12 +149,12 @@ async fn test_sign_tx(ledger_device_model: String) { }; } -#[test_case("nanos".to_string() ; "when the device is NanoS")] -#[test_case("nanox".to_string() ; "when the device is NanoX")] -#[test_case("nanosp".to_string() ; "when the device is NanoS Plus")] +#[test_case("nanos"; "when the device is NanoS")] +#[test_case("nanox"; "when the device is NanoX")] +#[test_case("nanosp"; "when the device is NanoS Plus")] #[tokio::test] -async fn test_sign_tx_hash_when_hash_signing_is_not_enabled(ledger_device_model: String) { - let container = get_container(ledger_device_model.clone()).await; +async fn test_sign_tx_hash_when_hash_signing_is_not_enabled(ledger_device_model: &str) { + let container = get_container(ledger_device_model).await; let host_port = container.get_host_port_ipv4(9998).await.unwrap(); let ui_host_port: u16 = container.get_host_port_ipv4(5000).await.unwrap(); wait_for_emulator_start_text(ui_host_port).await; @@ -199,12 +173,12 @@ async fn test_sign_tx_hash_when_hash_signing_is_not_enabled(ledger_device_model: } } -#[test_case("nanos".to_string() ; "when the device is NanoS")] -#[test_case("nanox".to_string() ; "when the device is NanoX")] -#[test_case("nanosp".to_string() ; "when the device is NanoS Plus")] +#[test_case("nanos"; "when the device is NanoS")] +#[test_case("nanox"; "when the device is NanoX")] +#[test_case("nanosp"; "when the device is NanoS Plus")] #[tokio::test] -async fn test_sign_tx_hash_when_hash_signing_is_enabled(ledger_device_model: String) { - let container = get_container(ledger_device_model.clone()).await; +async fn test_sign_tx_hash_when_hash_signing_is_enabled(ledger_device_model: &str) { + let container = get_container(ledger_device_model).await; let host_port = container.get_host_port_ipv4(9998).await.unwrap(); let ui_host_port: u16 = container.get_host_port_ipv4(5000).await.unwrap(); @@ -230,7 +204,10 @@ async fn test_sign_tx_hash_when_hash_signing_is_enabled(ledger_device_model: Str let ledger = Arc::clone(&ledger); async move { ledger.sign_transaction_hash(path, &test_hash).await } }); - let approve = tokio::task::spawn(approve_tx_hash_signature(ui_host_port, ledger_device_model)); + let approve = tokio::task::spawn(approve_tx_hash_signature( + ui_host_port, + ledger_device_model.to_string(), + )); let response = sign.await.unwrap(); let _ = approve.await.unwrap(); @@ -244,174 +221,3 @@ async fn test_sign_tx_hash_when_hash_signing_is_enabled(ledger_device_model: Str } } } - -async fn click(ui_host_port: u16, url: &str) { - let previous_events = get_emulator_events(ui_host_port).await; - - let client = reqwest::Client::new(); - let mut payload = HashMap::new(); - payload.insert("action", "press-and-release"); - - let mut screen_has_changed = false; - - client - .post(format!("http://localhost:{ui_host_port}/{url}")) - .json(&payload) - .send() - .await - .unwrap(); - - while !screen_has_changed { - let current_events = get_emulator_events(ui_host_port).await; - - if !(previous_events == current_events) { - screen_has_changed = true - } - } - - sleep(Duration::from_secs(1)).await; -} - -async fn enable_hash_signing(ui_host_port: u16) { - click(ui_host_port, "button/right").await; - - click(ui_host_port, "button/both").await; - - click(ui_host_port, "button/both").await; - - click(ui_host_port, "button/right").await; - - click(ui_host_port, "button/right").await; - - click(ui_host_port, "button/both").await; -} - -#[derive(Debug, Deserialize, PartialEq)] -struct EmulatorEvent { - text: String, - x: u16, - y: u16, - w: u16, - h: u16, -} - -#[derive(Debug, Deserialize)] -struct EventsResponse { - events: Vec, -} - -async fn get_container(ledger_device_model: String) -> ContainerAsync { - let (tcp_port_1, tcp_port_2) = get_available_ports(2); - Speculos::new(ledger_device_model) - .with_mapped_port(tcp_port_1, ContainerPort::Tcp(9998)) - .with_mapped_port(tcp_port_2, ContainerPort::Tcp(5000)) - .start() - .await - .unwrap() -} - -fn get_available_ports(n: usize) -> (u16, u16) { - let mut range = PORT_RANGE.lock().unwrap(); - let mut ports = Vec::with_capacity(n); - while ports.len() < n { - if let Some(port) = range.next() { - if let Ok(listener) = TcpListener::bind(("0.0.0.0", port)) { - ports.push(port); - drop(listener); - } - } else { - panic!("No more available ports"); - } - } - - (ports[0], ports[1]) -} - -async fn get_http_transport(host: &str, port: u16) -> Result { - let max_retries = 5; - let mut retries = 0; - let mut wait_time = Duration::from_secs(1); - // ping the emulator port to make sure it's up and running - // retry with exponential backoff - loop { - match reqwest::get(format!("http://{host}:{port}")).await { - Ok(_) => return Ok(EmulatorHttpTransport::new(host, port)), - Err(e) => { - retries += 1; - if retries >= max_retries { - println!("get_http_transport: Exceeded max retries for connecting to emulated device"); - - return Err(Error::APDUExchangeError(format!( - "Failed to connect to emulator: {e}" - ))); - } - sleep(wait_time).await; - wait_time *= 2; - } - } - } -} - -async fn wait_for_emulator_start_text(ui_host_port: u16) { - let mut ready = false; - while !ready { - let events = get_emulator_events_with_retries(ui_host_port, 5).await; - - if events.iter().any(|event| event.text == "is ready") { - ready = true; - } - } -} - -async fn get_emulator_events(ui_host_port: u16) -> Vec { - // Allowing for less retries here because presumably the emulator should be up and running since we waited - // for the "is ready" text via wait_for_emulator_start_text - get_emulator_events_with_retries(ui_host_port, 1).await -} - -async fn get_emulator_events_with_retries( - ui_host_port: u16, - max_retries: u16, -) -> Vec { - let client = reqwest::Client::new(); - let mut retries = 0; - let mut wait_time = Duration::from_secs(1); - loop { - match client - .get(format!("http://localhost:{ui_host_port}/events")) - .send() - .await - { - Ok(req) => { - let resp = req.json::().await.unwrap(); - return resp.events; - } - Err(e) => { - retries += 1; - if retries >= max_retries { - println!("get_emulator_events_with_retries: Exceeded max retries"); - panic!("get_emulator_events_with_retries: Failed to get emulator events: {e}"); - } - sleep(wait_time).await; - wait_time *= 2; - } - } - } -} - -async fn approve_tx_hash_signature(ui_host_port: u16, device_model: String) { - let number_of_right_clicks = if device_model == "nanos" { 10 } else { 6 }; - for _ in 0..number_of_right_clicks { - click(ui_host_port, "button/right").await; - } - - click(ui_host_port, "button/both").await; -} - -async fn approve_tx_signature(ui_host_port: u16, device_model: String) { - let number_of_right_clicks = if device_model == "nanos" { 17 } else { 11 }; - for _ in 0..number_of_right_clicks { - click(ui_host_port, "button/right").await; - } - click(ui_host_port, "button/both").await; -} diff --git a/cmd/crates/stellar-ledger/tests/utils/mod.rs b/cmd/crates/stellar-ledger/tests/utils/mod.rs deleted file mode 100644 index 5b63732bc..000000000 --- a/cmd/crates/stellar-ledger/tests/utils/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub(crate) mod emulator_http_transport; -pub(crate) mod speculos; diff --git a/cmd/soroban-cli/Cargo.toml b/cmd/soroban-cli/Cargo.toml index 3d366464b..a3918180f 100644 --- a/cmd/soroban-cli/Cargo.toml +++ b/cmd/soroban-cli/Cargo.toml @@ -37,6 +37,7 @@ doctest = false [features] default = [] opt = ["dep:wasm-opt"] +emulator-tests = ["stellar-ledger/emulator-tests"] [dependencies] stellar-xdr = { workspace = true, features = ["cli"] } @@ -49,6 +50,8 @@ soroban-ledger-snapshot = { workspace = true } stellar-strkey = { workspace = true } soroban-sdk = { workspace = true } soroban-rpc = { workspace = true } +stellar-ledger = { workspace = true } + clap = { workspace = true, features = [ "derive", "env", @@ -128,6 +131,7 @@ wasm-gen = "0.1.4" zeroize = "1.8.1" keyring = { version = "3", features = ["apple-native", "windows-native", "sync-secret-service"] } whoami = "1.5.2" +serde_with = "3.11.0" [build-dependencies] diff --git a/cmd/soroban-cli/src/commands/contract/arg_parsing.rs b/cmd/soroban-cli/src/commands/contract/arg_parsing.rs index bb2d2aa76..15c3cd8fe 100644 --- a/cmd/soroban-cli/src/commands/contract/arg_parsing.rs +++ b/cmd/soroban-cli/src/commands/contract/arg_parsing.rs @@ -13,10 +13,12 @@ use crate::xdr::{ }; use crate::commands::txn_result::TxnResult; + use crate::config::{ self, sc_address::{self, UnresolvedScAddress}, }; + use soroban_spec_tools::Spec; #[derive(thiserror::Error, Debug)] @@ -269,5 +271,11 @@ fn resolve_address(addr_or_alias: &str, config: &config::Args) -> Result Option { - config.locator.key(addr_or_alias).ok()?.key_pair(None).ok() + config + .locator + .read_key(addr_or_alias) + .ok()? + .private_key(None) + .ok() + .map(|pk| SigningKey::from_bytes(&pk.0)) } diff --git a/cmd/soroban-cli/src/commands/contract/deploy/asset.rs b/cmd/soroban-cli/src/commands/contract/deploy/asset.rs index 04a0380ed..6dac0bb7f 100644 --- a/cmd/soroban-cli/src/commands/contract/deploy/asset.rs +++ b/cmd/soroban-cli/src/commands/contract/deploy/asset.rs @@ -92,7 +92,9 @@ impl NetworkRunnable for Cmd { client .verify_network_passphrase(Some(&network.network_passphrase)) .await?; - let source_account = config.source_account()?; + + let source_account = config.source_account().await?; + // Get the account sequence number // TODO: use symbols for the method names (both here and in serve) let account_details = client diff --git a/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs b/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs index 1d85832b0..5e7c4c1fe 100644 --- a/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs +++ b/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs @@ -224,8 +224,7 @@ impl NetworkRunnable for Cmd { client .verify_network_passphrase(Some(&network.network_passphrase)) .await?; - - let MuxedAccount::Ed25519(bytes) = config.source_account()? else { + let MuxedAccount::Ed25519(bytes) = config.source_account().await? else { return Err(Error::OnlyEd25519AccountsAllowed); }; let source_account = AccountId(PublicKey::PublicKeyTypeEd25519(bytes)); diff --git a/cmd/soroban-cli/src/commands/contract/extend.rs b/cmd/soroban-cli/src/commands/contract/extend.rs index e56cbe166..bcfd0ea11 100644 --- a/cmd/soroban-cli/src/commands/contract/extend.rs +++ b/cmd/soroban-cli/src/commands/contract/extend.rs @@ -131,7 +131,7 @@ impl NetworkRunnable for Cmd { tracing::trace!(?network); let keys = self.key.parse_keys(&config.locator, &network)?; let client = network.rpc_client()?; - let source_account = config.source_account()?; + let source_account = config.source_account().await?; let extend_to = self.ledgers_to_extend(); // Get the account sequence number diff --git a/cmd/soroban-cli/src/commands/contract/id.rs b/cmd/soroban-cli/src/commands/contract/id.rs index bb8744d51..f07fa8df6 100644 --- a/cmd/soroban-cli/src/commands/contract/id.rs +++ b/cmd/soroban-cli/src/commands/contract/id.rs @@ -18,10 +18,10 @@ pub enum Error { } impl Cmd { - pub fn run(&self) -> Result<(), Error> { + pub async fn run(&self) -> Result<(), Error> { match &self { Cmd::Asset(asset) => asset.run()?, - Cmd::Wasm(wasm) => wasm.run()?, + Cmd::Wasm(wasm) => wasm.run().await?, } Ok(()) } diff --git a/cmd/soroban-cli/src/commands/contract/id/wasm.rs b/cmd/soroban-cli/src/commands/contract/id/wasm.rs index 349dd167b..a469f10c9 100644 --- a/cmd/soroban-cli/src/commands/contract/id/wasm.rs +++ b/cmd/soroban-cli/src/commands/contract/id/wasm.rs @@ -29,12 +29,12 @@ pub enum Error { OnlyEd25519AccountsAllowed, } impl Cmd { - pub fn run(&self) -> Result<(), Error> { + pub async fn run(&self) -> Result<(), Error> { let salt: [u8; 32] = soroban_spec_tools::utils::padded_hex_from_str(&self.salt, 32) .map_err(|_| Error::CannotParseSalt(self.salt.clone()))? .try_into() .map_err(|_| Error::CannotParseSalt(self.salt.clone()))?; - let source_account = match self.config.source_account()? { + let source_account = match self.config.source_account().await? { xdr::MuxedAccount::Ed25519(uint256) => stellar_strkey::ed25519::PublicKey(uint256.0), xdr::MuxedAccount::MuxedEd25519(_) => return Err(Error::OnlyEd25519AccountsAllowed), }; diff --git a/cmd/soroban-cli/src/commands/contract/install.rs b/cmd/soroban-cli/src/commands/contract/install.rs index 0a9ec856d..abba2fb1e 100644 --- a/cmd/soroban-cli/src/commands/contract/install.rs +++ b/cmd/soroban-cli/src/commands/contract/install.rs @@ -135,7 +135,7 @@ impl NetworkRunnable for Cmd { } // Get the account sequence number - let source_account = config.source_account()?; + let source_account = config.source_account().await?; let account_details = client .get_account(&source_account.clone().to_string()) diff --git a/cmd/soroban-cli/src/commands/contract/invoke.rs b/cmd/soroban-cli/src/commands/contract/invoke.rs index bd069698d..53b8bca5d 100644 --- a/cmd/soroban-cli/src/commands/contract/invoke.rs +++ b/cmd/soroban-cli/src/commands/contract/invoke.rs @@ -248,7 +248,7 @@ impl NetworkRunnable for Cmd { .await?; client - .get_account(&config.source_account()?.to_string()) + .get_account(&config.source_account().await?.to_string()) .await? } else { default_account_entry() diff --git a/cmd/soroban-cli/src/commands/contract/mod.rs b/cmd/soroban-cli/src/commands/contract/mod.rs index 42792a70d..59f24e7d5 100644 --- a/cmd/soroban-cli/src/commands/contract/mod.rs +++ b/cmd/soroban-cli/src/commands/contract/mod.rs @@ -150,7 +150,7 @@ impl Cmd { Cmd::Extend(extend) => extend.run().await?, Cmd::Alias(alias) => alias.run(global_args)?, Cmd::Deploy(deploy) => deploy.run(global_args).await?, - Cmd::Id(id) => id.run()?, + Cmd::Id(id) => id.run().await?, Cmd::Info(info) => info.run(global_args).await?, Cmd::Init(init) => init.run(global_args)?, Cmd::Inspect(inspect) => inspect.run(global_args)?, diff --git a/cmd/soroban-cli/src/commands/contract/restore.rs b/cmd/soroban-cli/src/commands/contract/restore.rs index cb35e6304..e79fffdd7 100644 --- a/cmd/soroban-cli/src/commands/contract/restore.rs +++ b/cmd/soroban-cli/src/commands/contract/restore.rs @@ -133,7 +133,7 @@ impl NetworkRunnable for Cmd { tracing::trace!(?network); let entry_keys = self.key.parse_keys(&config.locator, &network)?; let client = network.rpc_client()?; - let source_account = config.source_account()?; + let source_account = config.source_account().await?; // Get the account sequence number let account_details = client diff --git a/cmd/soroban-cli/src/commands/keys/add.rs b/cmd/soroban-cli/src/commands/keys/add.rs index 4c5ddbd9b..265b090f6 100644 --- a/cmd/soroban-cli/src/commands/keys/add.rs +++ b/cmd/soroban-cli/src/commands/keys/add.rs @@ -2,7 +2,7 @@ use clap::command; use crate::{ commands::global, - config::{address::KeyName, locator, secret}, + config::{address::KeyName, key, locator, secret}, print::Print, }; @@ -10,7 +10,8 @@ use crate::{ pub enum Error { #[error(transparent)] Secret(#[from] secret::Error), - + #[error(transparent)] + Key(#[from] key::Error), #[error(transparent)] Config(#[from] locator::Error), } @@ -26,13 +27,21 @@ pub struct Cmd { #[command(flatten)] pub config_locator: locator::Args, + + /// Add a public key, ed25519, or muxed account, e.g. G1.., M2.. + #[arg(long, conflicts_with = "seed_phrase", conflicts_with = "secret_key")] + pub public_key: Option, } impl Cmd { pub fn run(&self, global_args: &global::Args) -> Result<(), Error> { + let key = if let Some(key) = self.public_key.as_ref() { + key.parse()? + } else { + self.secrets.read_secret()?.into() + }; let print = Print::new(global_args.quiet); - let secret = self.secrets.read_secret()?; - let path = self.config_locator.write_identity(&self.name, &secret)?; + let path = self.config_locator.write_key(&self.name, &key)?; print.checkln(format!("Key saved with alias {:?} in {path:?}", self.name)); Ok(()) } diff --git a/cmd/soroban-cli/src/commands/keys/address.rs b/cmd/soroban-cli/src/commands/keys/address.rs index 51ce90ed2..e67934eb7 100644 --- a/cmd/soroban-cli/src/commands/keys/address.rs +++ b/cmd/soroban-cli/src/commands/keys/address.rs @@ -26,15 +26,16 @@ pub struct Cmd { } impl Cmd { - pub fn run(&self) -> Result<(), Error> { - println!("{}", self.public_key()?); + pub async fn run(&self) -> Result<(), Error> { + println!("{}", self.public_key().await?); Ok(()) } - pub fn public_key(&self) -> Result { + pub async fn public_key(&self) -> Result { let muxed = self .name - .resolve_muxed_account(&self.locator, self.hd_path)?; + .resolve_muxed_account(&self.locator, self.hd_path) + .await?; let bytes = match muxed { soroban_sdk::xdr::MuxedAccount::Ed25519(uint256) => uint256.0, soroban_sdk::xdr::MuxedAccount::MuxedEd25519(muxed_account) => muxed_account.ed25519.0, diff --git a/cmd/soroban-cli/src/commands/keys/fund.rs b/cmd/soroban-cli/src/commands/keys/fund.rs index 2419c4be2..b85eaaa2e 100644 --- a/cmd/soroban-cli/src/commands/keys/fund.rs +++ b/cmd/soroban-cli/src/commands/keys/fund.rs @@ -25,7 +25,7 @@ pub struct Cmd { impl Cmd { pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> { let print = Print::new(global_args.quiet); - let addr = self.address.public_key()?; + let addr = self.address.public_key().await?; let network = self.network.get(&self.address.locator)?; network.fund_address(&addr).await?; print.checkln(format!( diff --git a/cmd/soroban-cli/src/commands/keys/generate.rs b/cmd/soroban-cli/src/commands/keys/generate.rs index 8ec0158bb..7b9376dcd 100644 --- a/cmd/soroban-cli/src/commands/keys/generate.rs +++ b/cmd/soroban-cli/src/commands/keys/generate.rs @@ -5,6 +5,7 @@ use super::super::config::{ locator, network, secret::{self, Secret}, }; + use crate::{ commands::global, config::address::KeyName, @@ -178,7 +179,7 @@ impl Cmd { #[cfg(test)] mod tests { - use crate::config::{address::KeyName, secret::Secret}; + use crate::config::{address::KeyName, key::Key, secret::Secret}; use keyring::{mock, set_default_credential_builder}; fn set_up_test() -> (super::locator::Args, super::Cmd) { @@ -220,7 +221,7 @@ mod tests { let result = cmd.run(&global_args).await; assert!(result.is_ok()); let identity = test_locator.read_identity("test_name").unwrap(); - assert!(matches!(identity, Secret::SeedPhrase { .. })); + assert!(matches!(identity, Key::Secret(Secret::SeedPhrase { .. }))); } #[tokio::test] @@ -232,7 +233,7 @@ mod tests { let result = cmd.run(&global_args).await; assert!(result.is_ok()); let identity = test_locator.read_identity("test_name").unwrap(); - assert!(matches!(identity, Secret::SecretKey { .. })); + assert!(matches!(identity, Key::Secret(Secret::SecretKey { .. }))); } #[tokio::test] @@ -245,6 +246,6 @@ mod tests { let result = cmd.run(&global_args).await; assert!(result.is_ok()); let identity = test_locator.read_identity("test_name").unwrap(); - assert!(matches!(identity, Secret::SecureStore { .. })); + assert!(matches!(identity, Key::Secret(Secret::SecureStore { .. }))); } } diff --git a/cmd/soroban-cli/src/commands/keys/mod.rs b/cmd/soroban-cli/src/commands/keys/mod.rs index 3e36df085..885c51a65 100644 --- a/cmd/soroban-cli/src/commands/keys/mod.rs +++ b/cmd/soroban-cli/src/commands/keys/mod.rs @@ -71,7 +71,7 @@ impl Cmd { pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> { match self { Cmd::Add(cmd) => cmd.run(global_args)?, - Cmd::Address(cmd) => cmd.run()?, + Cmd::Address(cmd) => cmd.run().await?, Cmd::Fund(cmd) => cmd.run(global_args).await?, Cmd::Generate(cmd) => cmd.run(global_args).await?, Cmd::Ls(cmd) => cmd.run()?, diff --git a/cmd/soroban-cli/src/commands/keys/secret.rs b/cmd/soroban-cli/src/commands/keys/secret.rs index d28445247..38ac83116 100644 --- a/cmd/soroban-cli/src/commands/keys/secret.rs +++ b/cmd/soroban-cli/src/commands/keys/secret.rs @@ -1,6 +1,6 @@ use clap::arg; -use crate::config::{locator, secret}; +use crate::config::{key, locator}; #[derive(thiserror::Error, Debug)] pub enum Error { @@ -8,10 +8,7 @@ pub enum Error { Config(#[from] locator::Error), #[error(transparent)] - Secret(#[from] secret::Error), - - #[error(transparent)] - StrKey(#[from] stellar_strkey::DecodeError), + Key(#[from] key::Error), } #[derive(Debug, clap::Parser, Clone)] diff --git a/cmd/soroban-cli/src/commands/snapshot/create.rs b/cmd/soroban-cli/src/commands/snapshot/create.rs index 9ad39953f..476f525b5 100644 --- a/cmd/soroban-cli/src/commands/snapshot/create.rs +++ b/cmd/soroban-cli/src/commands/snapshot/create.rs @@ -220,7 +220,7 @@ impl Cmd { .address .iter() .cloned() - .filter_map(|a| self.resolve_address(&a, network_passphrase)) + .filter_map(|a| self.resolve_address_sync(&a, network_passphrase)) .partition_map(|a| a); let mut current = SearchInputs { @@ -400,23 +400,42 @@ impl Cmd { .ok_or(Error::ArchiveUrlNotConfigured) } - fn resolve_address( + #[allow(dead_code)] + async fn resolve_address( &self, address: &str, network_passphrase: &str, ) -> Option> { - self.resolve_contract(address, network_passphrase) - .map(Either::Right) - .or_else(|| self.resolve_account(address).map(Either::Left)) + if let Some(contract) = self.resolve_contract(address, network_passphrase) { + Some(Either::Right(contract)) + } else { + self.resolve_account(address).await.map(Either::Left) + } + } + + fn resolve_address_sync( + &self, + address: &str, + network_passphrase: &str, + ) -> Option> { + if let Some(contract) = self.resolve_contract(address, network_passphrase) { + Some(Either::Right(contract)) + } else { + self.resolve_account_sync(address).map(Either::Left) + } } // Resolve an account address to an account id. The address can be a // G-address or a key name (as in `stellar keys address NAME`). - fn resolve_account(&self, address: &str) -> Option { - let address: UnresolvedMuxedAccount = address.parse().ok()?; + async fn resolve_account(&self, address: &str) -> Option { + let address: UnresolvedMuxedAccount = address.parse().ok()?; Some(AccountId(xdr::PublicKey::PublicKeyTypeEd25519( - match address.resolve_muxed_account(&self.locator, None).ok()? { + match address + .resolve_muxed_account(&self.locator, None) + .await + .ok()? + { xdr::MuxedAccount::Ed25519(uint256) => uint256, xdr::MuxedAccount::MuxedEd25519(xdr::MuxedAccountMed25519 { ed25519, .. }) => { ed25519 @@ -424,6 +443,16 @@ impl Cmd { }, ))) } + + // Resolve an account address to an account id. The address can be a + // G-address or a key name (as in `stellar keys address NAME`). + fn resolve_account_sync(&self, address: &str) -> Option { + let address: UnresolvedMuxedAccount = address.parse().ok()?; + let muxed_account = address + .resolve_muxed_account_sync(&self.locator, None) + .ok()?; + Some(muxed_account.account_id()) + } // Resolve a contract address to a contract id. The contract can be a // C-address or a contract alias. fn resolve_contract(&self, address: &str, network_passphrase: &str) -> Option { diff --git a/cmd/soroban-cli/src/commands/tx/args.rs b/cmd/soroban-cli/src/commands/tx/args.rs index 1da1f230a..74de9450f 100644 --- a/cmd/soroban-cli/src/commands/tx/args.rs +++ b/cmd/soroban-cli/src/commands/tx/args.rs @@ -1,6 +1,10 @@ use crate::{ commands::{global, txn_result::TxnEnvelopeResult}, - config::{self, data, network, secret}, + config::{ + self, + address::{self, UnresolvedMuxedAccount}, + data, network, secret, + }, fee, rpc::{self, Client, GetTransactionResponse}, tx::builder::{self, TxExt}, @@ -32,11 +36,15 @@ pub enum Error { Data(#[from] data::Error), #[error(transparent)] Xdr(#[from] xdr::Error), + #[error(transparent)] + Address(#[from] address::Error), + #[error(transparent)] + TxXdr(#[from] super::xdr::Error), } impl Args { pub async fn tx(&self, body: impl Into) -> Result { - let source_account = self.source_account()?; + let source_account = self.source_account().await?; let seq_num = self .config .next_sequence_number(source_account.clone().account_id()) @@ -64,7 +72,7 @@ impl Args { op: impl Into, global_args: &global::Args, ) -> Result, Error> { - let tx = self.tx(op.into()).await?; + let tx = self.tx(op).await?; self.handle_tx(tx, global_args).await } pub async fn handle_and_print( @@ -101,7 +109,44 @@ impl Args { Ok(TxnEnvelopeResult::Res(txn_resp)) } - pub fn source_account(&self) -> Result { - Ok(self.config.source_account()?) + pub async fn source_account(&self) -> Result { + Ok(self.config.source_account().await?) + } + + pub fn resolve_muxed_address( + &self, + address: &UnresolvedMuxedAccount, + ) -> Result { + Ok(address.resolve_muxed_account_sync(&self.config.locator, self.config.hd_path)?) + } + + pub fn resolve_account_id( + &self, + address: &UnresolvedMuxedAccount, + ) -> Result { + Ok(address + .resolve_muxed_account_sync(&self.config.locator, self.config.hd_path)? + .account_id()) + } + + pub async fn add_op( + &self, + op_body: impl Into, + tx_env: xdr::TransactionEnvelope, + op_source: Option<&address::UnresolvedMuxedAccount>, + ) -> Result { + let mut source_account = None; + if let Some(account) = op_source { + source_account = Some( + account + .resolve_muxed_account(&self.config.locator, self.config.hd_path) + .await?, + ); + } + let op = xdr::Operation { + source_account, + body: op_body.into(), + }; + Ok(super::xdr::add_op(tx_env, op)?) } } diff --git a/cmd/soroban-cli/src/commands/tx/mod.rs b/cmd/soroban-cli/src/commands/tx/mod.rs index d9fd79faf..7ece69857 100644 --- a/cmd/soroban-cli/src/commands/tx/mod.rs +++ b/cmd/soroban-cli/src/commands/tx/mod.rs @@ -43,6 +43,8 @@ pub enum Error { #[error(transparent)] Sign(#[from] sign::Error), #[error(transparent)] + Args(#[from] args::Error), + #[error(transparent)] Simulate(#[from] simulate::Error), } @@ -51,7 +53,7 @@ impl Cmd { match self { Cmd::Hash(cmd) => cmd.run(global_args)?, Cmd::New(cmd) => cmd.run(global_args).await?, - Cmd::Operation(cmd) => cmd.run(global_args)?, + Cmd::Operation(cmd) => cmd.run(global_args).await?, Cmd::Send(cmd) => cmd.run(global_args).await?, Cmd::Sign(cmd) => cmd.run(global_args).await?, Cmd::Simulate(cmd) => cmd.run(global_args).await?, diff --git a/cmd/soroban-cli/src/commands/tx/new/account_merge.rs b/cmd/soroban-cli/src/commands/tx/new/account_merge.rs index 0d07fce91..a5cf25ce6 100644 --- a/cmd/soroban-cli/src/commands/tx/new/account_merge.rs +++ b/cmd/soroban-cli/src/commands/tx/new/account_merge.rs @@ -1,6 +1,6 @@ use clap::{command, Parser}; -use crate::{commands::tx, xdr}; +use crate::{commands::tx, config::address, xdr}; #[derive(Parser, Debug, Clone)] #[group(skip)] @@ -15,11 +15,14 @@ pub struct Cmd { pub struct Args { /// Muxed Account to merge with, e.g. `GBX...`, 'MBX...' #[arg(long)] - pub account: xdr::MuxedAccount, + pub account: address::UnresolvedMuxedAccount, } -impl From<&Args> for xdr::OperationBody { - fn from(cmd: &Args) -> Self { - xdr::OperationBody::AccountMerge(cmd.account.clone()) +impl TryFrom<&Cmd> for xdr::OperationBody { + type Error = tx::args::Error; + fn try_from(cmd: &Cmd) -> Result { + Ok(xdr::OperationBody::AccountMerge( + cmd.tx.resolve_muxed_address(&cmd.op.account)?, + )) } } diff --git a/cmd/soroban-cli/src/commands/tx/new/bump_sequence.rs b/cmd/soroban-cli/src/commands/tx/new/bump_sequence.rs index ff04e96a0..96062bba2 100644 --- a/cmd/soroban-cli/src/commands/tx/new/bump_sequence.rs +++ b/cmd/soroban-cli/src/commands/tx/new/bump_sequence.rs @@ -18,10 +18,10 @@ pub struct Args { pub bump_to: i64, } -impl From<&Args> for xdr::OperationBody { - fn from(cmd: &Args) -> Self { +impl From<&Cmd> for xdr::OperationBody { + fn from(cmd: &Cmd) -> Self { xdr::OperationBody::BumpSequence(xdr::BumpSequenceOp { - bump_to: cmd.bump_to.into(), + bump_to: cmd.op.bump_to.into(), }) } } diff --git a/cmd/soroban-cli/src/commands/tx/new/change_trust.rs b/cmd/soroban-cli/src/commands/tx/new/change_trust.rs index 2013db75b..04f17e87e 100644 --- a/cmd/soroban-cli/src/commands/tx/new/change_trust.rs +++ b/cmd/soroban-cli/src/commands/tx/new/change_trust.rs @@ -20,16 +20,16 @@ pub struct Args { pub limit: i64, } -impl From<&Args> for xdr::OperationBody { - fn from(cmd: &Args) -> Self { - let line = match cmd.line.0.clone() { +impl From<&Cmd> for xdr::OperationBody { + fn from(cmd: &Cmd) -> Self { + let line = match cmd.op.line.0.clone() { xdr::Asset::CreditAlphanum4(asset) => xdr::ChangeTrustAsset::CreditAlphanum4(asset), xdr::Asset::CreditAlphanum12(asset) => xdr::ChangeTrustAsset::CreditAlphanum12(asset), xdr::Asset::Native => xdr::ChangeTrustAsset::Native, }; xdr::OperationBody::ChangeTrust(xdr::ChangeTrustOp { line, - limit: cmd.limit, + limit: cmd.op.limit, }) } } diff --git a/cmd/soroban-cli/src/commands/tx/new/create_account.rs b/cmd/soroban-cli/src/commands/tx/new/create_account.rs index acdfd6e2d..b892646b5 100644 --- a/cmd/soroban-cli/src/commands/tx/new/create_account.rs +++ b/cmd/soroban-cli/src/commands/tx/new/create_account.rs @@ -1,6 +1,6 @@ use clap::{command, Parser}; -use crate::{commands::tx, tx::builder, xdr}; +use crate::{commands::tx, config::address, tx::builder, xdr}; #[derive(Parser, Debug, Clone)] #[group(skip)] @@ -14,18 +14,19 @@ pub struct Cmd { #[derive(Debug, clap::Args, Clone)] pub struct Args { /// Account Id to create, e.g. `GBX...` - #[arg(long, alias = "dest")] - pub destination: xdr::AccountId, + #[arg(long)] + pub destination: address::UnresolvedMuxedAccount, /// Initial balance in stroops of the account, default 1 XLM #[arg(long, default_value = "10_000_000")] pub starting_balance: builder::Amount, } -impl From<&Args> for xdr::OperationBody { - fn from(cmd: &Args) -> Self { - xdr::OperationBody::CreateAccount(xdr::CreateAccountOp { - destination: cmd.destination.clone(), - starting_balance: cmd.starting_balance.into(), - }) +impl TryFrom<&Cmd> for xdr::OperationBody { + type Error = tx::args::Error; + fn try_from(cmd: &Cmd) -> Result { + Ok(xdr::OperationBody::CreateAccount(xdr::CreateAccountOp { + destination: cmd.tx.resolve_account_id(&cmd.op.destination)?, + starting_balance: cmd.op.starting_balance.into(), + })) } } diff --git a/cmd/soroban-cli/src/commands/tx/new/manage_data.rs b/cmd/soroban-cli/src/commands/tx/new/manage_data.rs index 30e9a36fd..672d8f66e 100644 --- a/cmd/soroban-cli/src/commands/tx/new/manage_data.rs +++ b/cmd/soroban-cli/src/commands/tx/new/manage_data.rs @@ -25,10 +25,10 @@ pub struct Args { pub data_value: Option>, } -impl From<&Args> for xdr::OperationBody { - fn from(cmd: &Args) -> Self { - let data_value = cmd.data_value.clone().map(Into::into); - let data_name = cmd.data_name.clone().into(); +impl From<&Cmd> for xdr::OperationBody { + fn from(cmd: &Cmd) -> Self { + let data_value = cmd.op.data_value.clone().map(Into::into); + let data_name = cmd.op.data_name.clone().into(); xdr::OperationBody::ManageData(xdr::ManageDataOp { data_name, data_value, diff --git a/cmd/soroban-cli/src/commands/tx/new/mod.rs b/cmd/soroban-cli/src/commands/tx/new/mod.rs index 24c25995f..c00d01220 100644 --- a/cmd/soroban-cli/src/commands/tx/new/mod.rs +++ b/cmd/soroban-cli/src/commands/tx/new/mod.rs @@ -1,4 +1,5 @@ use clap::Parser; +use soroban_sdk::xdr::OperationBody; use super::global; @@ -38,17 +39,34 @@ pub enum Error { Tx(#[from] super::args::Error), } +impl TryFrom<&Cmd> for OperationBody { + type Error = super::args::Error; + fn try_from(cmd: &Cmd) -> Result { + Ok(match cmd { + Cmd::AccountMerge(cmd) => cmd.try_into()?, + Cmd::BumpSequence(cmd) => cmd.into(), + Cmd::ChangeTrust(cmd) => cmd.into(), + Cmd::CreateAccount(cmd) => cmd.try_into()?, + Cmd::ManageData(cmd) => cmd.into(), + Cmd::Payment(cmd) => cmd.try_into()?, + Cmd::SetOptions(cmd) => cmd.try_into()?, + Cmd::SetTrustlineFlags(cmd) => cmd.try_into()?, + }) + } +} + impl Cmd { pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> { + let op = OperationBody::try_from(self)?; match self { - Cmd::AccountMerge(cmd) => cmd.tx.handle_and_print(&cmd.op, global_args).await, - Cmd::BumpSequence(cmd) => cmd.tx.handle_and_print(&cmd.op, global_args).await, - Cmd::ChangeTrust(cmd) => cmd.tx.handle_and_print(&cmd.op, global_args).await, - Cmd::CreateAccount(cmd) => cmd.tx.handle_and_print(&cmd.op, global_args).await, - Cmd::ManageData(cmd) => cmd.tx.handle_and_print(&cmd.op, global_args).await, - Cmd::Payment(cmd) => cmd.tx.handle_and_print(&cmd.op, global_args).await, - Cmd::SetOptions(cmd) => cmd.tx.handle_and_print(&cmd.op, global_args).await, - Cmd::SetTrustlineFlags(cmd) => cmd.tx.handle_and_print(&cmd.op, global_args).await, + Cmd::AccountMerge(cmd) => cmd.tx.handle_and_print(op, global_args).await, + Cmd::BumpSequence(cmd) => cmd.tx.handle_and_print(op, global_args).await, + Cmd::ChangeTrust(cmd) => cmd.tx.handle_and_print(op, global_args).await, + Cmd::CreateAccount(cmd) => cmd.tx.handle_and_print(op, global_args).await, + Cmd::ManageData(cmd) => cmd.tx.handle_and_print(op, global_args).await, + Cmd::Payment(cmd) => cmd.tx.handle_and_print(op, global_args).await, + Cmd::SetOptions(cmd) => cmd.tx.handle_and_print(op, global_args).await, + Cmd::SetTrustlineFlags(cmd) => cmd.tx.handle_and_print(op, global_args).await, }?; Ok(()) } diff --git a/cmd/soroban-cli/src/commands/tx/new/payment.rs b/cmd/soroban-cli/src/commands/tx/new/payment.rs index 683b2731c..1e58110b0 100644 --- a/cmd/soroban-cli/src/commands/tx/new/payment.rs +++ b/cmd/soroban-cli/src/commands/tx/new/payment.rs @@ -1,6 +1,6 @@ use clap::{command, Parser}; -use crate::{commands::tx, tx::builder, xdr}; +use crate::{commands::tx, config::address, tx::builder, xdr}; #[derive(Parser, Debug, Clone)] #[group(skip)] @@ -14,8 +14,8 @@ pub struct Cmd { #[derive(Debug, clap::Args, Clone)] pub struct Args { /// Account to send to, e.g. `GBX...` - #[arg(long, visible_alias = "dest")] - pub destination: xdr::MuxedAccount, + #[arg(long)] + pub destination: address::UnresolvedMuxedAccount, /// Asset to send, default native, e.i. XLM #[arg(long, default_value = "native")] pub asset: builder::Asset, @@ -24,12 +24,13 @@ pub struct Args { pub amount: builder::Amount, } -impl From<&Args> for xdr::OperationBody { - fn from(cmd: &Args) -> Self { - xdr::OperationBody::Payment(xdr::PaymentOp { - destination: cmd.destination.clone(), - asset: cmd.asset.clone().into(), - amount: cmd.amount.into(), - }) +impl TryFrom<&Cmd> for xdr::OperationBody { + type Error = tx::args::Error; + fn try_from(cmd: &Cmd) -> Result { + Ok(xdr::OperationBody::Payment(xdr::PaymentOp { + destination: cmd.tx.resolve_muxed_address(&cmd.op.destination)?, + asset: cmd.op.asset.clone().into(), + amount: cmd.op.amount.into(), + })) } } diff --git a/cmd/soroban-cli/src/commands/tx/new/set_options.rs b/cmd/soroban-cli/src/commands/tx/new/set_options.rs index 77c7c0895..87e0d2423 100644 --- a/cmd/soroban-cli/src/commands/tx/new/set_options.rs +++ b/cmd/soroban-cli/src/commands/tx/new/set_options.rs @@ -1,6 +1,6 @@ use clap::{command, Parser}; -use crate::{commands::tx, xdr}; +use crate::{commands::tx, config::address, xdr}; #[derive(Parser, Debug, Clone)] #[group(skip)] @@ -16,7 +16,7 @@ pub struct Cmd { pub struct Args { #[arg(long)] /// Account of the inflation destination. - pub inflation_dest: Option, + pub inflation_dest: Option, #[arg(long)] /// A number from 0-255 (inclusive) representing the weight of the master key. If the weight of the master key is updated to 0, it is effectively disabled. pub master_weight: Option, @@ -67,12 +67,15 @@ pub struct Args { pub clear_clawback_enabled: bool, } -impl From<&Args> for xdr::OperationBody { - fn from(cmd: &Args) -> Self { +impl TryFrom<&Cmd> for xdr::OperationBody { + type Error = tx::args::Error; + fn try_from(cmd: &Cmd) -> Result { + let tx = &cmd.tx; let mut set_flags = None; let mut set_flag = |flag: xdr::AccountFlags| { *set_flags.get_or_insert(0) |= flag as u32; }; + let cmd = &cmd.op; if cmd.set_required { set_flag(xdr::AccountFlags::RequiredFlag); @@ -114,8 +117,13 @@ impl From<&Args> for xdr::OperationBody { } else { None }; - xdr::OperationBody::SetOptions(xdr::SetOptionsOp { - inflation_dest: cmd.inflation_dest.clone().map(Into::into), + let inflation_dest: Option = cmd + .inflation_dest + .as_ref() + .map(|dest| tx.resolve_account_id(dest)) + .transpose()?; + Ok(xdr::OperationBody::SetOptions(xdr::SetOptionsOp { + inflation_dest, clear_flags, set_flags, master_weight: cmd.master_weight.map(Into::into), @@ -124,6 +132,6 @@ impl From<&Args> for xdr::OperationBody { high_threshold: cmd.high_threshold.map(Into::into), home_domain: cmd.home_domain.clone().map(Into::into), signer, - }) + })) } } diff --git a/cmd/soroban-cli/src/commands/tx/new/set_trustline_flags.rs b/cmd/soroban-cli/src/commands/tx/new/set_trustline_flags.rs index 482dd3a90..ac2830222 100644 --- a/cmd/soroban-cli/src/commands/tx/new/set_trustline_flags.rs +++ b/cmd/soroban-cli/src/commands/tx/new/set_trustline_flags.rs @@ -1,6 +1,6 @@ use clap::{command, Parser}; -use crate::{commands::tx, tx::builder, xdr}; +use crate::{commands::tx, config::address, tx::builder, xdr}; #[derive(Parser, Debug, Clone)] #[group(skip)] @@ -14,9 +14,9 @@ pub struct Cmd { #[derive(Debug, clap::Args, Clone)] #[allow(clippy::struct_excessive_bools, clippy::doc_markdown)] pub struct Args { - /// Account to set trustline flags for + /// Account to set trustline flags for, e.g. `GBX...`, or alias, or muxed account, `M123...`` #[arg(long)] - pub trustor: xdr::AccountId, + pub trustor: address::UnresolvedMuxedAccount, /// Asset to set trustline flags for #[arg(long)] pub asset: builder::Asset, @@ -38,38 +38,41 @@ pub struct Args { pub clear_trustline_clawback_enabled: bool, } -impl From<&Args> for xdr::OperationBody { - fn from(cmd: &Args) -> Self { +impl TryFrom<&Cmd> for xdr::OperationBody { + type Error = tx::args::Error; + fn try_from(cmd: &Cmd) -> Result { let mut set_flags = 0; let mut set_flag = |flag: xdr::TrustLineFlags| set_flags |= flag as u32; - if cmd.set_authorize { + if cmd.op.set_authorize { set_flag(xdr::TrustLineFlags::AuthorizedFlag); }; - if cmd.set_authorize_to_maintain_liabilities { + if cmd.op.set_authorize_to_maintain_liabilities { set_flag(xdr::TrustLineFlags::AuthorizedToMaintainLiabilitiesFlag); }; - if cmd.set_trustline_clawback_enabled { + if cmd.op.set_trustline_clawback_enabled { set_flag(xdr::TrustLineFlags::TrustlineClawbackEnabledFlag); }; let mut clear_flags = 0; let mut clear_flag = |flag: xdr::TrustLineFlags| clear_flags |= flag as u32; - if cmd.clear_authorize { + if cmd.op.clear_authorize { clear_flag(xdr::TrustLineFlags::AuthorizedFlag); }; - if cmd.clear_authorize_to_maintain_liabilities { + if cmd.op.clear_authorize_to_maintain_liabilities { clear_flag(xdr::TrustLineFlags::AuthorizedToMaintainLiabilitiesFlag); }; - if cmd.clear_trustline_clawback_enabled { + if cmd.op.clear_trustline_clawback_enabled { clear_flag(xdr::TrustLineFlags::TrustlineClawbackEnabledFlag); }; - xdr::OperationBody::SetTrustLineFlags(xdr::SetTrustLineFlagsOp { - trustor: cmd.trustor.clone(), - asset: cmd.asset.clone().into(), - clear_flags, - set_flags, - }) + Ok(xdr::OperationBody::SetTrustLineFlags( + xdr::SetTrustLineFlagsOp { + trustor: cmd.tx.resolve_account_id(&cmd.op.trustor)?, + asset: cmd.op.asset.clone().into(), + clear_flags, + set_flags, + }, + )) } } diff --git a/cmd/soroban-cli/src/commands/tx/op/add/account_merge.rs b/cmd/soroban-cli/src/commands/tx/op/add/account_merge.rs index bd643c199..cf274fd67 100644 --- a/cmd/soroban-cli/src/commands/tx/op/add/account_merge.rs +++ b/cmd/soroban-cli/src/commands/tx/op/add/account_merge.rs @@ -1,14 +1,8 @@ -use clap::{command, Parser}; - -use std::fmt::Debug; - -use super::new; - -#[derive(Parser, Debug, Clone)] +#[derive(clap::Parser, Debug, Clone)] #[group(skip)] pub struct Cmd { #[command(flatten)] pub args: super::args::Args, #[command(flatten)] - pub op: new::account_merge::Args, + pub op: super::new::account_merge::Cmd, } diff --git a/cmd/soroban-cli/src/commands/tx/op/add/args.rs b/cmd/soroban-cli/src/commands/tx/op/add/args.rs index 51ee4d476..8ee211302 100644 --- a/cmd/soroban-cli/src/commands/tx/op/add/args.rs +++ b/cmd/soroban-cli/src/commands/tx/op/add/args.rs @@ -1,22 +1,8 @@ -use super::xdr::add_op; -use crate::{ - config::{address, locator}, - xdr, -}; - -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error(transparent)] - Address(#[from] address::Error), - #[error(transparent)] - TxXdr(#[from] super::xdr::Error), -} +use crate::config::address; #[derive(Debug, clap::Args, Clone)] #[group(skip)] pub struct Args { - #[clap(flatten)] - pub locator: locator::Args, /// Source account used for the operation #[arg( long, @@ -27,20 +13,7 @@ pub struct Args { } impl Args { - pub fn add_op( - &self, - op_body: impl Into, - tx_env: xdr::TransactionEnvelope, - ) -> Result { - let source_account = self - .operation_source_account - .as_ref() - .map(|a| a.resolve_muxed_account(&self.locator, None)) - .transpose()?; - let op = xdr::Operation { - source_account, - body: op_body.into(), - }; - Ok(add_op(tx_env, op)?) + pub fn source(&self) -> Option<&address::UnresolvedMuxedAccount> { + self.operation_source_account.as_ref() } } diff --git a/cmd/soroban-cli/src/commands/tx/op/add/bump_sequence.rs b/cmd/soroban-cli/src/commands/tx/op/add/bump_sequence.rs index 907d8d2d6..640b748b3 100644 --- a/cmd/soroban-cli/src/commands/tx/op/add/bump_sequence.rs +++ b/cmd/soroban-cli/src/commands/tx/op/add/bump_sequence.rs @@ -1,14 +1,8 @@ -use clap::{command, Parser}; - -use std::fmt::Debug; - -use super::new; - -#[derive(Parser, Debug, Clone)] +#[derive(clap::Parser, Debug, Clone)] #[group(skip)] pub struct Cmd { #[command(flatten)] pub args: super::args::Args, #[command(flatten)] - pub op: new::bump_sequence::Args, + pub op: super::new::bump_sequence::Cmd, } diff --git a/cmd/soroban-cli/src/commands/tx/op/add/change_trust.rs b/cmd/soroban-cli/src/commands/tx/op/add/change_trust.rs index af9afae1b..5647e4cec 100644 --- a/cmd/soroban-cli/src/commands/tx/op/add/change_trust.rs +++ b/cmd/soroban-cli/src/commands/tx/op/add/change_trust.rs @@ -1,14 +1,8 @@ -use clap::{command, Parser}; - -use std::fmt::Debug; - -use super::new; - -#[derive(Parser, Debug, Clone)] +#[derive(clap::Parser, Debug, Clone)] #[group(skip)] pub struct Cmd { #[command(flatten)] pub args: super::args::Args, #[command(flatten)] - pub op: new::change_trust::Args, + pub op: super::new::change_trust::Cmd, } diff --git a/cmd/soroban-cli/src/commands/tx/op/add/create_account.rs b/cmd/soroban-cli/src/commands/tx/op/add/create_account.rs index e30ff20a1..1ca978e0f 100644 --- a/cmd/soroban-cli/src/commands/tx/op/add/create_account.rs +++ b/cmd/soroban-cli/src/commands/tx/op/add/create_account.rs @@ -1,14 +1,8 @@ -use clap::{command, Parser}; - -use std::fmt::Debug; - -use super::new; - -#[derive(Parser, Debug, Clone)] +#[derive(clap::Parser, Debug, Clone)] #[group(skip)] pub struct Cmd { #[command(flatten)] pub args: super::args::Args, #[command(flatten)] - pub op: new::create_account::Args, + pub op: super::new::create_account::Cmd, } diff --git a/cmd/soroban-cli/src/commands/tx/op/add/manage_data.rs b/cmd/soroban-cli/src/commands/tx/op/add/manage_data.rs index 962233a84..5758478c3 100644 --- a/cmd/soroban-cli/src/commands/tx/op/add/manage_data.rs +++ b/cmd/soroban-cli/src/commands/tx/op/add/manage_data.rs @@ -1,14 +1,8 @@ -use clap::{command, Parser}; - -use std::fmt::Debug; - -use super::new; - -#[derive(Parser, Debug, Clone)] +#[derive(clap::Parser, Debug, Clone)] #[group(skip)] pub struct Cmd { #[command(flatten)] pub args: super::args::Args, #[command(flatten)] - pub op: new::manage_data::Args, + pub op: super::new::manage_data::Cmd, } diff --git a/cmd/soroban-cli/src/commands/tx/op/add/mod.rs b/cmd/soroban-cli/src/commands/tx/op/add/mod.rs index b94fc74ce..2025131af 100644 --- a/cmd/soroban-cli/src/commands/tx/op/add/mod.rs +++ b/cmd/soroban-cli/src/commands/tx/op/add/mod.rs @@ -1,9 +1,7 @@ -use clap::Parser; - use super::super::{global, help, xdr::tx_envelope_from_stdin}; -use crate::xdr::WriteXdr; +use crate::xdr::{OperationBody, WriteXdr}; -pub(crate) use super::super::{new, xdr}; +pub(crate) use super::super::new; mod account_merge; mod args; @@ -15,7 +13,7 @@ mod payment; mod set_options; mod set_trustline_flags; -#[derive(Debug, Parser)] +#[derive(Debug, clap::Parser)] #[allow(clippy::doc_markdown)] pub enum Cmd { #[command(about = help::ACCOUNT_MERGE)] @@ -38,27 +36,47 @@ pub enum Cmd { #[derive(thiserror::Error, Debug)] pub enum Error { - #[error(transparent)] - Args(#[from] args::Error), #[error(transparent)] TxXdr(#[from] super::super::xdr::Error), #[error(transparent)] Xdr(#[from] crate::xdr::Error), + #[error(transparent)] + New(#[from] super::super::new::Error), + #[error(transparent)] + Tx(#[from] super::super::args::Error), +} + +impl TryFrom<&Cmd> for OperationBody { + type Error = super::super::new::Error; + fn try_from(cmd: &Cmd) -> Result { + Ok(match &cmd { + Cmd::AccountMerge(account_merge::Cmd { op, .. }) => op.try_into()?, + Cmd::BumpSequence(bump_sequence::Cmd { op, .. }) => op.into(), + Cmd::ChangeTrust(change_trust::Cmd { op, .. }) => op.into(), + Cmd::CreateAccount(create_account::Cmd { op, .. }) => op.try_into()?, + Cmd::ManageData(manage_data::Cmd { op, .. }) => op.into(), + Cmd::Payment(payment::Cmd { op, .. }) => op.try_into()?, + Cmd::SetOptions(set_options::Cmd { op, .. }) => op.try_into()?, + Cmd::SetTrustlineFlags(set_trustline_flags::Cmd { op, .. }) => op.try_into()?, + }) + } } impl Cmd { - pub fn run(&self, _: &global::Args) -> Result<(), Error> { + pub async fn run(&self, _: &global::Args) -> Result<(), Error> { let tx_env = tx_envelope_from_stdin()?; + let op = OperationBody::try_from(self)?; let res = match self { - Cmd::AccountMerge(cmd) => cmd.args.add_op(&cmd.op, tx_env), - Cmd::BumpSequence(cmd) => cmd.args.add_op(&cmd.op, tx_env), - Cmd::ChangeTrust(cmd) => cmd.args.add_op(&cmd.op, tx_env), - Cmd::CreateAccount(cmd) => cmd.args.add_op(&cmd.op, tx_env), - Cmd::ManageData(cmd) => cmd.args.add_op(&cmd.op, tx_env), - Cmd::Payment(cmd) => cmd.args.add_op(&cmd.op, tx_env), - Cmd::SetOptions(cmd) => cmd.args.add_op(&cmd.op, tx_env), - Cmd::SetTrustlineFlags(cmd) => cmd.args.add_op(&cmd.op, tx_env), - }?; + Cmd::AccountMerge(cmd) => cmd.op.tx.add_op(op, tx_env, cmd.args.source()), + Cmd::BumpSequence(cmd) => cmd.op.tx.add_op(op, tx_env, cmd.args.source()), + Cmd::ChangeTrust(cmd) => cmd.op.tx.add_op(op, tx_env, cmd.args.source()), + Cmd::CreateAccount(cmd) => cmd.op.tx.add_op(op, tx_env, cmd.args.source()), + Cmd::ManageData(cmd) => cmd.op.tx.add_op(op, tx_env, cmd.args.source()), + Cmd::Payment(cmd) => cmd.op.tx.add_op(op, tx_env, cmd.args.source()), + Cmd::SetOptions(cmd) => cmd.op.tx.add_op(op, tx_env, cmd.args.source()), + Cmd::SetTrustlineFlags(cmd) => cmd.op.tx.add_op(op, tx_env, cmd.args.source()), + } + .await?; println!("{}", res.to_xdr_base64(crate::xdr::Limits::none())?); Ok(()) } diff --git a/cmd/soroban-cli/src/commands/tx/op/add/payment.rs b/cmd/soroban-cli/src/commands/tx/op/add/payment.rs index d8146c91a..f5f9c5fcc 100644 --- a/cmd/soroban-cli/src/commands/tx/op/add/payment.rs +++ b/cmd/soroban-cli/src/commands/tx/op/add/payment.rs @@ -1,14 +1,8 @@ -use clap::{command, Parser}; - -use std::fmt::Debug; - -use super::new; - -#[derive(Parser, Debug, Clone)] +#[derive(clap::Parser, Debug, Clone)] #[group(skip)] pub struct Cmd { #[command(flatten)] pub args: super::args::Args, #[command(flatten)] - pub op: new::payment::Args, + pub op: super::new::payment::Cmd, } diff --git a/cmd/soroban-cli/src/commands/tx/op/add/set_options.rs b/cmd/soroban-cli/src/commands/tx/op/add/set_options.rs index 75b43124a..88323f57c 100644 --- a/cmd/soroban-cli/src/commands/tx/op/add/set_options.rs +++ b/cmd/soroban-cli/src/commands/tx/op/add/set_options.rs @@ -1,14 +1,8 @@ -use clap::{command, Parser}; - -use std::fmt::Debug; - -use super::new; - -#[derive(Parser, Debug, Clone)] +#[derive(clap::Parser, Debug, Clone)] #[group(skip)] pub struct Cmd { #[command(flatten)] pub args: super::args::Args, #[command(flatten)] - pub op: new::set_options::Args, + pub op: super::new::set_options::Cmd, } diff --git a/cmd/soroban-cli/src/commands/tx/op/add/set_trustline_flags.rs b/cmd/soroban-cli/src/commands/tx/op/add/set_trustline_flags.rs index 8ffee7a7b..09ee11607 100644 --- a/cmd/soroban-cli/src/commands/tx/op/add/set_trustline_flags.rs +++ b/cmd/soroban-cli/src/commands/tx/op/add/set_trustline_flags.rs @@ -1,14 +1,8 @@ -use clap::{command, Parser}; - -use std::fmt::Debug; - -use super::new; - -#[derive(Parser, Debug, Clone)] +#[derive(clap::Parser, Debug, Clone)] #[group(skip)] pub struct Cmd { #[command(flatten)] pub args: super::args::Args, #[command(flatten)] - pub op: new::set_trustline_flags::Args, + pub op: super::new::set_trustline_flags::Cmd, } diff --git a/cmd/soroban-cli/src/commands/tx/op/mod.rs b/cmd/soroban-cli/src/commands/tx/op/mod.rs index 9c38ecfd3..e5b064a79 100644 --- a/cmd/soroban-cli/src/commands/tx/op/mod.rs +++ b/cmd/soroban-cli/src/commands/tx/op/mod.rs @@ -16,9 +16,9 @@ pub enum Error { } impl Cmd { - pub fn run(&self, global_args: &global::Args) -> Result<(), Error> { + pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> { match self { - Cmd::Add(cmd) => cmd.run(global_args)?, + Cmd::Add(cmd) => cmd.run(global_args).await?, }; Ok(()) } diff --git a/cmd/soroban-cli/src/commands/tx/sign.rs b/cmd/soroban-cli/src/commands/tx/sign.rs index c696f8586..750fd7d29 100644 --- a/cmd/soroban-cli/src/commands/tx/sign.rs +++ b/cmd/soroban-cli/src/commands/tx/sign.rs @@ -33,12 +33,15 @@ 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, - )?; + let tx_env_signed = self + .sign_with + .sign_tx_env( + &tx_env, + &self.locator, + &self.network.get(&self.locator)?, + global_args.quiet, + ) + .await?; println!("{}", tx_env_signed.to_xdr_base64(Limits::none())?); Ok(()) } diff --git a/cmd/soroban-cli/src/config/address.rs b/cmd/soroban-cli/src/config/address.rs index f86f88ecb..73c5cf190 100644 --- a/cmd/soroban-cli/src/config/address.rs +++ b/cmd/soroban-cli/src/config/address.rs @@ -3,15 +3,19 @@ use std::{ str::FromStr, }; -use crate::xdr; +use crate::{ + signer::{self, ledger}, + xdr, +}; -use super::{locator, secret}; +use super::{key, locator, secret}; /// Address can be either a public key or eventually an alias of a address. #[derive(Clone, Debug)] pub enum UnresolvedMuxedAccount { Resolved(xdr::MuxedAccount), AliasOrSecret(String), + Ledger(u32), } impl Default for UnresolvedMuxedAccount { @@ -26,20 +30,35 @@ pub enum Error { Locator(#[from] locator::Error), #[error(transparent)] Secret(#[from] secret::Error), + #[error(transparent)] + Signer(#[from] signer::Error), + #[error(transparent)] + Key(#[from] key::Error), #[error("Address cannot be used to sign {0}")] CannotSign(xdr::MuxedAccount), + #[error("Ledger cannot reveal private keys")] + LedgerPrivateKeyRevealNotSupported, + #[error("Invalid key name: {0}\n `ledger` is not allowed")] + LedgerIsInvalidKeyName(String), #[error("Invalid key name: {0}\n only alphanumeric characters, underscores (_), and hyphens (-) are allowed.")] InvalidKeyNameCharacters(String), #[error("Invalid key name: {0}\n keys cannot exceed 250 characters")] InvalidKeyNameLength(String), #[error("Invalid key name: {0}\n keys cannot be the word \"ledger\"")] InvalidKeyName(String), + #[error("Ledger not supported in this context")] + LedgerNotSupported, } impl FromStr for UnresolvedMuxedAccount { type Err = Error; fn from_str(value: &str) -> Result { + if value.starts_with("ledger") { + if let Some(ledger) = parse_ledger(value) { + return Ok(UnresolvedMuxedAccount::Ledger(ledger)); + } + } Ok(xdr::MuxedAccount::from_str(value).map_or_else( |_| UnresolvedMuxedAccount::AliasOrSecret(value.to_string()), UnresolvedMuxedAccount::Resolved, @@ -47,30 +66,45 @@ impl FromStr for UnresolvedMuxedAccount { } } +fn parse_ledger(value: &str) -> Option { + let vals: Vec<_> = value.split(':').collect(); + if vals.len() > 2 { + return None; + } + if vals.len() == 1 { + return Some(0); + } + vals[1].parse().ok() +} + impl UnresolvedMuxedAccount { - pub fn resolve_muxed_account( + pub async fn resolve_muxed_account( &self, locator: &locator::Args, hd_path: Option, ) -> Result { match self { - UnresolvedMuxedAccount::Resolved(muxed_account) => Ok(muxed_account.clone()), - UnresolvedMuxedAccount::AliasOrSecret(alias_or_secret) => { - Self::resolve_muxed_account_with_alias(alias_or_secret, locator, hd_path) + UnresolvedMuxedAccount::Ledger(hd_path) => Ok(xdr::MuxedAccount::Ed25519( + ledger(*hd_path).await?.public_key().await?.0.into(), + )), + UnresolvedMuxedAccount::Resolved(_) | UnresolvedMuxedAccount::AliasOrSecret(_) => { + self.resolve_muxed_account_sync(locator, hd_path) } } } - pub fn resolve_muxed_account_with_alias( - alias: &str, + pub fn resolve_muxed_account_sync( + &self, locator: &locator::Args, hd_path: Option, ) -> Result { - alias.parse().or_else(|_| { - Ok(xdr::MuxedAccount::Ed25519( - locator.read_identity(alias)?.public_key(hd_path)?.0.into(), - )) - }) + match self { + UnresolvedMuxedAccount::Resolved(muxed_account) => Ok(muxed_account.clone()), + UnresolvedMuxedAccount::AliasOrSecret(alias_or_secret) => { + Ok(locator.read_key(alias_or_secret)?.muxed_account(hd_path)?) + } + UnresolvedMuxedAccount::Ledger(_) => Err(Error::LedgerNotSupported), + } } pub fn resolve_secret(&self, locator: &locator::Args) -> Result { @@ -79,8 +113,9 @@ impl UnresolvedMuxedAccount { Err(Error::CannotSign(muxed_account.clone())) } UnresolvedMuxedAccount::AliasOrSecret(alias_or_secret) => { - Ok(locator.key(alias_or_secret)?) + Ok(locator.read_key(alias_or_secret)?.try_into()?) } + UnresolvedMuxedAccount::Ledger(_) => Err(Error::LedgerPrivateKeyRevealNotSupported), } } } diff --git a/cmd/soroban-cli/src/config/key.rs b/cmd/soroban-cli/src/config/key.rs new file mode 100644 index 000000000..9d21ad061 --- /dev/null +++ b/cmd/soroban-cli/src/config/key.rs @@ -0,0 +1,183 @@ +use std::{fmt::Display, str::FromStr}; + +use serde::{Deserialize, Serialize}; + +use super::secret::{self, Secret}; +use crate::xdr; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("failed to extract secret from public key ")] + SecretPublicKey, + #[error(transparent)] + Secret(#[from] secret::Error), + #[error(transparent)] + StrKey(#[from] stellar_strkey::DecodeError), + #[error("failed to parse key")] + Parse, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +pub enum Key { + #[serde(rename = "public_key")] + PublicKey(Public), + #[serde(rename = "muxed_account")] + MuxedAccount(MuxedAccount), + #[serde(untagged)] + Secret(Secret), +} + +impl Key { + pub fn muxed_account(&self, hd_path: Option) -> Result { + let bytes = match self { + Key::Secret(secret) => secret.public_key(hd_path)?.0, + Key::PublicKey(Public(key)) => key.0, + Key::MuxedAccount(MuxedAccount(stellar_strkey::ed25519::MuxedAccount { + ed25519, + id, + })) => { + return Ok(xdr::MuxedAccount::MuxedEd25519(xdr::MuxedAccountMed25519 { + ed25519: xdr::Uint256(*ed25519), + id: *id, + })) + } + }; + Ok(xdr::MuxedAccount::Ed25519(xdr::Uint256(bytes))) + } + + pub fn private_key( + &self, + hd_path: Option, + ) -> Result { + match self { + Key::Secret(secret) => Ok(secret.private_key(hd_path)?), + _ => Err(Error::SecretPublicKey), + } + } +} + +impl FromStr for Key { + type Err = Error; + + fn from_str(s: &str) -> Result { + if let Ok(secret) = s.parse() { + return Ok(Key::Secret(secret)); + } + if let Ok(public_key) = s.parse() { + return Ok(Key::PublicKey(public_key)); + } + if let Ok(muxed_account) = s.parse() { + return Ok(Key::MuxedAccount(muxed_account)); + } + Err(Error::Parse) + } +} + +impl From for Key { + fn from(value: stellar_strkey::ed25519::PublicKey) -> Self { + Key::PublicKey(Public(value)) + } +} + +impl From<&stellar_strkey::ed25519::PublicKey> for Key { + fn from(stellar_strkey::ed25519::PublicKey(key): &stellar_strkey::ed25519::PublicKey) -> Self { + stellar_strkey::ed25519::PublicKey(*key).into() + } +} + +#[derive(Debug, PartialEq, Eq, serde_with::SerializeDisplay, serde_with::DeserializeFromStr)] +pub struct Public(pub stellar_strkey::ed25519::PublicKey); + +impl FromStr for Public { + type Err = stellar_strkey::DecodeError; + + fn from_str(s: &str) -> Result { + Ok(Public(stellar_strkey::ed25519::PublicKey::from_str(s)?)) + } +} + +impl Display for Public { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From<&Public> for stellar_strkey::ed25519::MuxedAccount { + fn from(Public(stellar_strkey::ed25519::PublicKey(key)): &Public) -> Self { + stellar_strkey::ed25519::MuxedAccount { + id: 0, + ed25519: *key, + } + } +} + +#[derive(Debug, PartialEq, Eq, serde_with::SerializeDisplay, serde_with::DeserializeFromStr)] +pub struct MuxedAccount(pub stellar_strkey::ed25519::MuxedAccount); + +impl FromStr for MuxedAccount { + type Err = stellar_strkey::DecodeError; + + fn from_str(s: &str) -> Result { + Ok(MuxedAccount( + stellar_strkey::ed25519::MuxedAccount::from_str(s)?, + )) + } +} + +impl Display for MuxedAccount { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl TryFrom for Secret { + type Error = Error; + + fn try_from(key: Key) -> Result { + match key { + Key::Secret(secret) => Ok(secret), + _ => Err(Error::SecretPublicKey), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + fn round_trip(key: &Key) { + let serialized = toml::to_string(&key).unwrap(); + println!("{serialized}"); + let deserialized: Key = toml::from_str(&serialized).unwrap(); + assert_eq!(key, &deserialized); + } + + #[test] + fn public_key() { + let key = Key::PublicKey(Public(stellar_strkey::ed25519::PublicKey([0; 32]))); + round_trip(&key); + } + #[test] + fn muxed_key() { + let key: stellar_strkey::ed25519::MuxedAccount = + "MA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAAAAAAAAAPCICBKU" + .parse() + .unwrap(); + let key = Key::MuxedAccount(MuxedAccount(key)); + round_trip(&key); + } + #[test] + fn secret_key() { + let secret_key = stellar_strkey::ed25519::PrivateKey([0; 32]).to_string(); + let secret = Secret::SecretKey { secret_key }; + let key = Key::Secret(secret); + round_trip(&key); + } + #[test] + fn secret_seed_phrase() { + let seed_phrase = "singer swing mango apple singer swing mango apple singer swing mango apple singer swing mango apple".to_string(); + let secret = Secret::SeedPhrase { seed_phrase }; + let key = Key::Secret(secret); + round_trip(&key); + } +} diff --git a/cmd/soroban-cli/src/config/locator.rs b/cmd/soroban-cli/src/config/locator.rs index 1e26bbf7f..ecc31247e 100644 --- a/cmd/soroban-cli/src/config/locator.rs +++ b/cmd/soroban-cli/src/config/locator.rs @@ -12,10 +12,11 @@ use std::{ }; use stellar_strkey::{Contract, DecodeError}; -use crate::{commands::HEADING_GLOBAL, utils::find_config_dir, Pwd}; +use crate::{commands::HEADING_GLOBAL, utils::find_config_dir, xdr, Pwd}; use super::{ alias, + key::{self, Key}, network::{self, Network}, secret::Secret, Config, @@ -87,6 +88,10 @@ pub enum Error { ContractAliasCannotOverlapWithKey(String), #[error("Key cannot {0} cannot overlap with contract alias")] KeyCannotOverlapWithContractAlias(String), + #[error("Only private keys and seed phrases are supported for getting private keys {0}")] + SecretKeyOnly(String), + #[error(transparent)] + Key(#[from] key::Error), } #[derive(Debug, clap::Args, Default, Clone)] @@ -173,6 +178,18 @@ impl Args { KeyType::Identity.write(name, secret, &self.config_dir()?) } + pub fn write_public_key( + &self, + name: &str, + public_key: &stellar_strkey::ed25519::PublicKey, + ) -> Result { + self.write_key(name, &public_key.into()) + } + + pub fn write_key(&self, name: &str, key: &Key) -> Result { + KeyType::Identity.write(name, key, &self.config_dir()?) + } + pub fn write_network(&self, name: &str, network: &Network) -> Result { KeyType::Network.write(name, network, &self.config_dir()?) } @@ -235,20 +252,31 @@ impl Args { Ok(saved_networks.chain(default_networks).collect()) } - pub fn read_identity(&self, name: &str) -> Result { - Ok(KeyType::Identity - .read_with_global(name, &self.local_config()?) - .or_else(|_| name.parse())?) + pub fn read_identity(&self, name: &str) -> Result { + KeyType::Identity.read_with_global(name, &self.local_config()?) } - pub fn key(&self, key_or_name: &str) -> Result { - if let Ok(signer) = key_or_name.parse::() { - Ok(signer) - } else { - self.read_identity(key_or_name) + pub fn read_key(&self, key_or_name: &str) -> Result { + key_or_name + .parse() + .or_else(|_| self.read_identity(key_or_name)) + } + + pub fn get_secret_key(&self, key_or_name: &str) -> Result { + match self.read_key(key_or_name)? { + Key::Secret(s) => Ok(s), + _ => Err(Error::SecretKeyOnly(key_or_name.to_string())), } } + pub fn get_public_key( + &self, + key_or_name: &str, + hd_path: Option, + ) -> Result { + Ok(self.read_key(key_or_name)?.muxed_account(hd_path)?) + } + pub fn read_network(&self, name: &str) -> Result { let res = KeyType::Network.read_with_global(name, &self.local_config()?); if let Err(Error::ConfigMissing(_, _)) = &res { diff --git a/cmd/soroban-cli/src/config/mod.rs b/cmd/soroban-cli/src/config/mod.rs index 1188d3bfa..b373536b0 100644 --- a/cmd/soroban-cli/src/config/mod.rs +++ b/cmd/soroban-cli/src/config/mod.rs @@ -16,6 +16,7 @@ use network::Network; pub mod address; pub mod alias; pub mod data; +pub mod key; pub mod locator; pub mod network; pub mod sc_address; @@ -70,10 +71,11 @@ pub struct Args { impl Args { // TODO: Replace PublicKey with MuxedAccount once https://github.com/stellar/rs-stellar-xdr/pull/396 is merged. - pub fn source_account(&self) -> Result { + pub async fn source_account(&self) -> Result { Ok(self .source_account - .resolve_muxed_account(&self.locator, self.hd_path)?) + .resolve_muxed_account(&self.locator, self.hd_path) + .await?) } pub fn key_pair(&self) -> Result { @@ -93,7 +95,7 @@ impl Args { kind: SignerKind::Local(LocalKey { key }), print: Print::new(false), }; - Ok(signer.sign_tx(tx, network)?) + Ok(signer.sign_tx(tx, network).await?) } pub async fn sign_soroban_authorizations( diff --git a/cmd/soroban-cli/src/config/sc_address.rs b/cmd/soroban-cli/src/config/sc_address.rs index fc9c168f2..b580504d5 100644 --- a/cmd/soroban-cli/src/config/sc_address.rs +++ b/cmd/soroban-cli/src/config/sc_address.rs @@ -2,7 +2,7 @@ use std::str::FromStr; use crate::xdr; -use super::{address, locator, UnresolvedContract}; +use super::{key, locator, UnresolvedContract}; /// `ScAddress` can be either a resolved `xdr::ScAddress` or an alias of a `Contract` or `MuxedAccount`. #[allow(clippy::module_name_repetitions)] @@ -23,7 +23,7 @@ pub enum Error { #[error(transparent)] Locator(#[from] locator::Error), #[error(transparent)] - Address(#[from] address::Error), + Key(#[from] key::Error), #[error("Account alias not Found{0}")] AccountAliasNotFound(String), } @@ -50,9 +50,8 @@ impl UnresolvedScAddress { UnresolvedScAddress::Alias(alias) => alias, }; let contract = UnresolvedContract::resolve_alias(&alias, locator, network_passphrase); - let muxed_account = - super::UnresolvedMuxedAccount::resolve_muxed_account_with_alias(&alias, locator, None); - match (contract, muxed_account) { + let key = locator.read_key(&alias); + match (contract, key) { (Ok(contract), Ok(_)) => { eprintln!( "Warning: ScAddress alias {alias} is ambiguous, assuming it is a contract" @@ -60,7 +59,9 @@ impl UnresolvedScAddress { Ok(xdr::ScAddress::Contract(xdr::Hash(contract.0))) } (Ok(contract), _) => Ok(xdr::ScAddress::Contract(xdr::Hash(contract.0))), - (_, Ok(muxed_account)) => Ok(xdr::ScAddress::Account(muxed_account.account_id())), + (_, Ok(key)) => Ok(xdr::ScAddress::Account( + key.muxed_account(None)?.account_id(), + )), _ => Err(Error::AccountAliasNotFound(alias)), } } diff --git a/cmd/soroban-cli/src/config/secret.rs b/cmd/soroban-cli/src/config/secret.rs index f32b291e8..a3f8620ea 100644 --- a/cmd/soroban-cli/src/config/secret.rs +++ b/cmd/soroban-cli/src/config/secret.rs @@ -7,10 +7,12 @@ use stellar_strkey::ed25519::{PrivateKey, PublicKey}; use crate::{ print::Print, - signer::{self, keyring, LocalKey, SecureStoreEntry, Signer, SignerKind}, + signer::{self, keyring, ledger, LocalKey, SecureStoreEntry, Signer, SignerKind}, utils, }; +use super::key::Key; + #[derive(thiserror::Error, Debug)] pub enum Error { // #[error("seed_phrase must be 12 words long, found {len}")] @@ -27,6 +29,10 @@ pub enum Error { InvalidSecretOrSeedPhrase, #[error(transparent)] Signer(#[from] signer::Error), + + #[error("Ledger does not reveal secret key")] + LedgerDoesNotRevealSecretKey, + #[error(transparent)] Keyring(#[from] keyring::Error), #[error("Secure Store does not reveal secret key")] @@ -58,11 +64,12 @@ impl Args { } } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(untagged)] pub enum Secret { SecretKey { secret_key: String }, SeedPhrase { seed_phrase: String }, + Ledger, SecureStore { entry_name: String }, } @@ -78,6 +85,8 @@ impl FromStr for Secret { Ok(Secret::SeedPhrase { seed_phrase: s.to_string(), }) + } else if s == "ledger" { + Ok(Secret::Ledger) } else if s.starts_with(keyring::SECURE_STORE_ENTRY_PREFIX) { Ok(Secret::SecureStore { entry_name: s.to_string(), @@ -96,6 +105,12 @@ impl From for Secret { } } +impl From for Key { + fn from(value: Secret) -> Self { + Key::Secret(value) + } +} + impl From for Secret { fn from(value: SeedPhrase) -> Self { Secret::SeedPhrase { @@ -114,6 +129,7 @@ impl Secret { .private() .0, )?, + Secret::Ledger => panic!("Ledger does not reveal secret key"), Secret::SecureStore { .. } => { return Err(Error::SecureStoreDoesNotRevealSecretKey); } @@ -132,12 +148,19 @@ impl Secret { } } - pub fn signer(&self, hd_path: Option, print: Print) -> Result { + pub async fn signer(&self, hd_path: Option, print: Print) -> Result { let kind = match self { Secret::SecretKey { .. } | Secret::SeedPhrase { .. } => { let key = self.key_pair(hd_path)?; SignerKind::Local(LocalKey { key }) } + Secret::Ledger => { + let hd_path: u32 = hd_path + .unwrap_or_default() + .try_into() + .expect("uszie bigger than u32"); + SignerKind::Ledger(ledger(hd_path).await?) + } Secret::SecureStore { entry_name } => SignerKind::SecureStore(SecureStoreEntry { name: entry_name.to_string(), hd_path, diff --git a/cmd/soroban-cli/src/config/sign_with.rs b/cmd/soroban-cli/src/config/sign_with.rs index 475013bc8..a7c811adf 100644 --- a/cmd/soroban-cli/src/config/sign_with.rs +++ b/cmd/soroban-cli/src/config/sign_with.rs @@ -1,6 +1,6 @@ use crate::{ print::Print, - signer::{self, Signer, SignerKind}, + signer::{self, ledger, Signer, SignerKind}, xdr::{self, TransactionEnvelope}, }; use clap::arg; @@ -38,7 +38,7 @@ pub struct Args { #[arg(long, env = "STELLAR_SIGN_WITH_KEY")] pub sign_with_key: Option, - #[arg(long, requires = "sign_with_key")] + #[arg(long, conflicts_with = "sign_with_lab")] /// 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, @@ -46,10 +46,19 @@ pub struct Args { /// Sign with https://lab.stellar.org #[arg(long, conflicts_with = "sign_with_key", env = "STELLAR_SIGN_WITH_LAB")] pub sign_with_lab: bool, + + /// Sign with a ledger wallet + #[arg( + long, + conflicts_with = "sign_with_key", + conflicts_with = "sign_with_lab", + env = "STELLAR_SIGN_WITH_LEDGER" + )] + pub sign_with_ledger: bool, } impl Args { - pub fn sign_tx_env( + pub async fn sign_tx_env( &self, tx: &TransactionEnvelope, locator: &locator::Args, @@ -62,11 +71,23 @@ impl Args { kind: SignerKind::Lab, print, } + } else if self.sign_with_ledger { + let ledger = ledger( + self.hd_path + .unwrap_or_default() + .try_into() + .unwrap_or_default(), + ) + .await?; + Signer { + kind: SignerKind::Ledger(ledger), + print, + } } else { let key_or_name = self.sign_with_key.as_deref().ok_or(Error::NoSignWithKey)?; - let secret = locator.key(key_or_name)?; - secret.signer(self.hd_path, print)? + let secret = locator.get_secret_key(key_or_name)?; + secret.signer(self.hd_path, print).await? }; - Ok(signer.sign_tx_env(tx, network)?) + Ok(signer.sign_tx_env(tx, network).await?) } } diff --git a/cmd/soroban-cli/src/signer.rs b/cmd/soroban-cli/src/signer.rs index ebd650d40..03fb46761 100644 --- a/cmd/soroban-cli/src/signer.rs +++ b/cmd/soroban-cli/src/signer.rs @@ -9,6 +9,7 @@ use crate::xdr::{ SorobanAuthorizedFunction, SorobanCredentials, Transaction, TransactionEnvelope, TransactionV1Envelope, Uint256, VecM, WriteXdr, }; +use stellar_ledger::{Blob as _, Exchange, LedgerSigner}; use crate::{config::network::Network, print::Print, utils::transaction_hash}; @@ -27,6 +28,8 @@ pub enum Error { #[error("User cancelled signing, perhaps need to add -y")] UserCancelledSigning, #[error(transparent)] + Ledger(#[from] stellar_ledger::Error), + #[error(transparent)] Xdr(#[from] xdr::Error), #[error("Only Transaction envelope V1 type is supported")] UnsupportedTransactionEnvelopeType, @@ -211,12 +214,16 @@ pub struct Signer { #[allow(clippy::module_name_repetitions, clippy::large_enum_variant)] pub enum SignerKind { Local(LocalKey), + #[cfg(not(feature = "emulator-tests"))] + Ledger(Ledger), + #[cfg(feature = "emulator-tests")] + Ledger(Ledger), Lab, SecureStore(SecureStoreEntry), } impl Signer { - pub fn sign_tx( + pub async fn sign_tx( &self, tx: Transaction, network: &Network, @@ -225,10 +232,10 @@ impl Signer { tx, signatures: VecM::default(), }); - self.sign_tx_env(&tx_env, network) + self.sign_tx_env(&tx_env, network).await } - pub fn sign_tx_env( + pub async fn sign_tx_env( &self, tx_env: &TransactionEnvelope, network: &Network, @@ -241,6 +248,7 @@ impl Signer { let decorated_signature = match &self.kind { SignerKind::Local(key) => key.sign_tx_hash(tx_hash)?, SignerKind::Lab => Lab::sign_tx_env(tx_env, network, &self.print)?, + SignerKind::Ledger(ledger) => ledger.sign_transaction_hash(&tx_hash).await?, SignerKind::SecureStore(entry) => entry.sign_tx_hash(tx_hash)?, }; let mut sigs = signatures.clone().into_vec(); @@ -259,6 +267,76 @@ pub struct LocalKey { pub key: ed25519_dalek::SigningKey, } +#[allow(dead_code)] +pub struct Ledger { + pub(crate) index: u32, + pub(crate) signer: LedgerSigner, +} + +impl Ledger { + pub async fn sign_transaction_hash( + &self, + tx_hash: &[u8; 32], + ) -> Result { + let key = self.public_key().await?; + let hint = SignatureHint(key.0[28..].try_into()?); + let signature = Signature( + self.signer + .sign_transaction_hash(self.index, tx_hash) + .await? + .try_into()?, + ); + Ok(DecoratedSignature { hint, signature }) + } + + pub async fn sign_transaction( + &self, + tx: Transaction, + network_passphrase: &str, + ) -> Result { + let network_id = Hash(Sha256::digest(network_passphrase).into()); + let signature = self + .signer + .sign_transaction(self.index, tx, network_id) + .await?; + let key = self.public_key().await?; + let hint = SignatureHint(key.0[28..].try_into()?); + let signature = Signature(signature.try_into()?); + Ok(DecoratedSignature { hint, signature }) + } + + pub async fn public_key(&self) -> Result { + Ok(self.signer.get_public_key(&self.index.into()).await?) + } +} + +#[cfg(not(feature = "emulator-tests"))] +pub async fn ledger(hd_path: u32) -> Result, Error> { + let signer = stellar_ledger::native()?; + Ok(Ledger { + index: hd_path, + signer, + }) +} + +#[cfg(feature = "emulator-tests")] +pub async fn ledger( + hd_path: u32, +) -> Result, Error> { + use stellar_ledger::emulator_test_support::ledger as emulator_ledger; + // port from SPECULOS_PORT ENV var + let host_port: u16 = std::env::var("SPECULOS_PORT") + .expect("SPECULOS_PORT env var not set") + .parse() + .expect("port must be a number"); + let signer = emulator_ledger(host_port).await; + + Ok(Ledger { + index: hd_path, + signer, + }) +} + impl LocalKey { pub fn sign_tx_hash(&self, tx_hash: [u8; 32]) -> Result { let hint = SignatureHint(self.key.verifying_key().to_bytes()[28..].try_into()?); diff --git a/cmd/stellar-cli/Cargo.toml b/cmd/stellar-cli/Cargo.toml index e1f1dce6b..4a2808f6f 100644 --- a/cmd/stellar-cli/Cargo.toml +++ b/cmd/stellar-cli/Cargo.toml @@ -27,6 +27,7 @@ bin-dir = "{ bin }{ binary-ext }" [features] default = [] opt = ["soroban-cli/opt"] +emulator-tests = ["soroban-cli/emulator-tests"] [dependencies] soroban-cli = { workspace = true }