From b88b421672849cf431898bd331f69eb2fc4e5772 Mon Sep 17 00:00:00 2001 From: George Date: Mon, 12 Aug 2024 13:53:08 -0700 Subject: [PATCH] Support JSON output for XDR structures across all endpoints. Add support for base64-encoded XDR fields to be returned as unpacked JSON. # Details The following endpoints have a new, optional request parameter, `xdrFormat?: ""|"base64"|"json"`: * `getTransaction` * `getTransactions` * `getLedgerEntry` * `getLedgerEntries` * `getEvents` * `sendTransaction` * `simulateTransaction` When omitted, the behavior does not change and we encode fields as base64. # New Response Fields There are new field names for the JSONified versions of XDR structures. Any field with an `Xdr` suffix (e.g., `resultXdr` in `getTransaction()`) will be replaced with one that has a `Json` suffix (e.g., `resultJson`) that is a JSON object verbosely and completely describing the XDR structure. Certain XDR-encoded fields do not have an `Xdr` suffix, but those also have a `*Json` equivalent and are listed below: * _getEvents_: `topic` -> `topicJson`, `value` -> `valueJson` * _getLedgerEntries_: `key` -> `keyJson`, `xdr` -> `dataJson` * _getLedgerEntry_: `xdr` -> `entryJson` * _simulateTransaction_: `transactionData`, `events`, `results.auth`, `restorePreamble.transactionData`, `stateChanges.key|before|after` all have a `Json` suffix, and `results.xdr` is now `results.returnValueJson` Closes #124. --- .github/workflows/codeql.yml | 2 +- .github/workflows/golang.yml | 6 +- .github/workflows/soroban-rpc.yml | 6 +- Cargo.lock | 319 +++++++++++++++++- Cargo.toml | 7 + Makefile | 22 +- .../integrationtest/get_fee_stats_test.go | 6 +- .../get_ledger_entries_test.go | 8 +- .../integrationtest/get_ledger_entry_test.go | 19 +- .../integrationtest/infrastructure/client.go | 38 +-- .../simulate_transaction_test.go | 236 ++++++------- .../integrationtest/transaction_test.go | 95 +++--- .../internal/methods/get_events.go | 103 ++++-- .../internal/methods/get_events_test.go | 65 +++- .../internal/methods/get_ledger_entries.go | 84 +++-- .../internal/methods/get_ledger_entry.go | 33 +- .../internal/methods/get_transaction.go | 62 +++- .../internal/methods/get_transaction_test.go | 146 ++++++-- .../internal/methods/get_transactions.go | 71 +++- .../internal/methods/get_transactions_test.go | 42 +++ .../internal/methods/get_version_info.go | 2 +- cmd/soroban-rpc/internal/methods/json.go | 60 ++++ .../internal/methods/send_transaction.go | 102 +++++- .../internal/methods/simulate_transaction.go | 233 +++++++++++-- .../methods/simulate_transaction_test.go | 49 ++- .../internal/xdr2json/conversion.go | 86 +++++ cmd/soroban-rpc/lib/ffi/Cargo.toml | 10 + cmd/soroban-rpc/lib/ffi/src/lib.rs | 80 +++++ cmd/soroban-rpc/lib/preflight.h | 6 +- cmd/soroban-rpc/lib/preflight/Cargo.toml | 7 +- cmd/soroban-rpc/lib/preflight/src/lib.rs | 66 ++-- cmd/soroban-rpc/lib/shared.h | 4 + cmd/soroban-rpc/lib/xdr2json.h | 13 + cmd/soroban-rpc/lib/xdr2json/Cargo.toml | 19 ++ cmd/soroban-rpc/lib/xdr2json/src/lib.rs | 121 +++++++ 35 files changed, 1763 insertions(+), 465 deletions(-) create mode 100644 cmd/soroban-rpc/internal/methods/json.go create mode 100644 cmd/soroban-rpc/internal/xdr2json/conversion.go create mode 100644 cmd/soroban-rpc/lib/ffi/Cargo.toml create mode 100644 cmd/soroban-rpc/lib/ffi/src/lib.rs create mode 100644 cmd/soroban-rpc/lib/shared.h create mode 100644 cmd/soroban-rpc/lib/xdr2json.h create mode 100644 cmd/soroban-rpc/lib/xdr2json/Cargo.toml create mode 100644 cmd/soroban-rpc/lib/xdr2json/src/lib.rs diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 54dbaa1f..ea699b2b 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -32,7 +32,7 @@ jobs: go-version: 1.22 - run: rustup update - uses: stellar/actions/rust-cache@main - - run: make build-libpreflight + - run: make build-libs # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/golang.yml b/.github/workflows/golang.yml index f7696412..009decb5 100644 --- a/.github/workflows/golang.yml +++ b/.github/workflows/golang.yml @@ -30,7 +30,7 @@ jobs: - name: Build libpreflight run: | rustup update - make build-libpreflight + make build-libs - name: Run golangci-lint uses: golangci/golangci-lint-action@a4f60bb28d35aeee14e6880718e0c85ff1882e64 # version v6.0.1 @@ -38,7 +38,3 @@ jobs: version: v1.59.1 # this is the golangci-lint version github-token: ${{ secrets.GITHUB_TOKEN }} only-new-issues: true - - - - diff --git a/.github/workflows/soroban-rpc.yml b/.github/workflows/soroban-rpc.yml index c9e3dc1f..bc7ed064 100644 --- a/.github/workflows/soroban-rpc.yml +++ b/.github/workflows/soroban-rpc.yml @@ -31,7 +31,7 @@ jobs: - uses: ./.github/actions/setup-go - run: rustup update - uses: stellar/actions/rust-cache@main - - run: make build-libpreflight + - run: make build-libs - run: go test -race -timeout 25m ./cmd/soroban-rpc/... build: @@ -79,7 +79,7 @@ jobs: rustup target add ${{ matrix.rust_target }} rustup update - uses: stellar/actions/rust-cache@main - - run: make build-libpreflight + - run: make build-libs env: CARGO_BUILD_TARGET: ${{ matrix.rust_target }} @@ -168,7 +168,7 @@ jobs: - run: rustup update - uses: stellar/actions/rust-cache@main - - run: make build-libpreflight + - run: make build-libs - name: Run Soroban RPC Integration Tests run: | diff --git a/Cargo.lock b/Cargo.lock index 0b4a4cc2..d90f8a22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,21 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.79" @@ -113,12 +128,31 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets", +] + [[package]] name = "const-oid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + [[package]] name = "cpufeatures" version = "0.2.12" @@ -189,6 +223,41 @@ dependencies = [ "syn", ] +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "der" version = "0.7.8" @@ -199,6 +268,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + [[package]] name = "derive_arbitrary" version = "1.3.2" @@ -317,12 +396,25 @@ dependencies = [ "subtle", ] +[[package]] +name = "ffi" +version = "21.0.0" +dependencies = [ + "libc", +] + [[package]] name = "fiat-crypto" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1676f435fc1dadde4d03e43f5d62b259e1ce5f40bd4ffb21db2b42ebe59c1382" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "generic-array" version = "0.14.7" @@ -364,6 +456,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.3" @@ -375,6 +473,9 @@ name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] [[package]] name = "hex-literal" @@ -391,6 +492,46 @@ dependencies = [ "digest", ] +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.2.3" @@ -398,7 +539,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.3", + "serde", ] [[package]] @@ -485,6 +627,12 @@ dependencies = [ "adler", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-derive" version = "0.4.1" @@ -564,6 +712,12 @@ version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "626dec3cac7cc0e1577a2ec3fc496277ec2baa084bebad95bb6fdbfae235f84c" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -572,12 +726,14 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "preflight" -version = "21.3.0" +version = "21.4.0" dependencies = [ "anyhow", "base64 0.22.0", + "ffi", "libc", "rand", + "serde_json", "sha2", "soroban-env-host", "soroban-simulation", @@ -721,6 +877,36 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20" +dependencies = [ + "base64 0.22.0", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.2.3", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha2" version = "0.10.8" @@ -906,9 +1092,17 @@ dependencies = [ "crate-git-revision", "escape-bytes", "hex", + "serde", + "serde_with", "stellar-strkey", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.5.0" @@ -946,6 +1140,37 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "typenum" version = "1.17.0" @@ -1048,7 +1273,7 @@ version = "0.116.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a58e28b80dd8340cb07b8242ae654756161f6fc8d0038123d679b7b99964fa50" dependencies = [ - "indexmap", + "indexmap 2.2.3", "semver", ] @@ -1061,6 +1286,94 @@ dependencies = [ "indexmap-nostd", ] +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "xdr2json" +version = "21.0.0" +dependencies = [ + "anyhow", + "base64 0.22.0", + "ffi", + "libc", + "rand", + "serde_json", + "sha2", + "soroban-env-host", + "stellar-xdr", +] + [[package]] name = "zeroize" version = "1.7.0" diff --git a/Cargo.toml b/Cargo.toml index 5a1c0e24..bdd76994 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,8 @@ resolver = "2" members = [ "cmd/soroban-rpc/lib/preflight", + "cmd/soroban-rpc/lib/ffi", + "cmd/soroban-rpc/lib/xdr2json" ] [workspace.package] @@ -13,12 +15,17 @@ version = "=21.1.0" [workspace.dependencies.soroban-simulation] version = "=21.1.0" +[workspace.dependencies.stellar-xdr] +version = "=21.1.0" +features = [ "serde" ] + [workspace.dependencies] base64 = "0.22.0" sha2 = "0.10.7" libc = "0.2.147" anyhow = "1.0.75" rand = { version = "0.8.5", features = [] } +serde_json = "1.0" [profile.release-with-panic-unwind] inherits = 'release' diff --git a/Makefile b/Makefile index 0e0b713e..59e4f671 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ endif # 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 +endif REPOSITORY_BRANCH := "$(shell git rev-parse --abbrev-ref HEAD)" BUILD_TIMESTAMP ?= $(shell date '+%Y-%m-%dT%H:%M:%S') GOLDFLAGS := -X 'github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/config.Version=${REPOSITORY_VERSION}' \ @@ -40,15 +40,17 @@ CARGO_BUILD_TARGET ?= $(shell rustc -vV | sed -n 's|host: ||p') Cargo.lock: Cargo.toml cargo update --workspace -install: build-libpreflight +install: build-libs go install -ldflags="${GOLDFLAGS}" ${MACOS_MIN_VER} ./... - -build: build-libpreflight +build: build-libs go build -ldflags="${GOLDFLAGS}" ${MACOS_MIN_VER} ./... -build-libpreflight: Cargo.lock - cd cmd/soroban-rpc/lib/preflight && cargo build --target $(CARGO_BUILD_TARGET) --profile release-with-panic-unwind +build-libs: Cargo.lock + cd cmd/soroban-rpc/lib/preflight && \ + cargo build --target $(CARGO_BUILD_TARGET) --profile release-with-panic-unwind && \ + cd ../xdr2json && \ + cargo build --target $(CARGO_BUILD_TARGET) --profile release-with-panic-unwind check: rust-check go-check @@ -66,7 +68,7 @@ fmt: rust-test: cargo test -go-test: build-libpreflight +go-test: build-libs go test ./... test: go-test rust-test @@ -75,10 +77,10 @@ clean: cargo clean go clean ./... -# the build-soroban-rpc build target is an optimized build target used by +# the build-soroban-rpc build target is an optimized build target used by # https://github.com/stellar/pipelines/stellar-horizon/Jenkinsfile-soroban-rpc-package-builder # as part of the package building. -build-soroban-rpc: build-libpreflight +build-soroban-rpc: build-libs go build -ldflags="${GOLDFLAGS}" ${MACOS_MIN_VER} -o soroban-rpc -trimpath -v ./cmd/soroban-rpc go-check-branch: @@ -89,4 +91,4 @@ go-check: # PHONY lists all the targets that aren't file names, so that make would skip the timestamp based check. -.PHONY: clean fmt watch test rust-test go-test check rust-check go-check install build build-soroban-rpc build-libpreflight lint lint-changes +.PHONY: clean fmt watch test rust-test go-test check rust-check go-check install build build-soroban-rpc build-libs lint lint-changes diff --git a/cmd/soroban-rpc/internal/integrationtest/get_fee_stats_test.go b/cmd/soroban-rpc/internal/integrationtest/get_fee_stats_test.go index 7ec8e132..68fc2486 100644 --- a/cmd/soroban-rpc/internal/integrationtest/get_fee_stats_test.go +++ b/cmd/soroban-rpc/internal/integrationtest/get_fee_stats_test.go @@ -19,10 +19,10 @@ func TestGetFeeStats(t *testing.T) { sorobanTxResponse, _ := test.UploadHelloWorldContract() var sorobanTxResult xdr.TransactionResult - require.NoError(t, xdr.SafeUnmarshalBase64(sorobanTxResponse.ResultXdr, &sorobanTxResult)) + require.NoError(t, xdr.SafeUnmarshalBase64(sorobanTxResponse.ResultXDR, &sorobanTxResult)) sorobanTotalFee := sorobanTxResult.FeeCharged var sorobanTxMeta xdr.TransactionMeta - require.NoError(t, xdr.SafeUnmarshalBase64(sorobanTxResponse.ResultMetaXdr, &sorobanTxMeta)) + require.NoError(t, xdr.SafeUnmarshalBase64(sorobanTxResponse.ResultMetaXDR, &sorobanTxMeta)) sorobanFees := sorobanTxMeta.MustV3().SorobanMeta.Ext.MustV1() sorobanResourceFeeCharged := sorobanFees.TotalRefundableResourceFeeCharged + sorobanFees.TotalNonRefundableResourceFeeCharged sorobanInclusionFee := uint64(sorobanTotalFee - sorobanResourceFeeCharged) @@ -34,7 +34,7 @@ func TestGetFeeStats(t *testing.T) { &txnbuild.BumpSequence{BumpTo: seq + 100}, ) var classicTxResult xdr.TransactionResult - require.NoError(t, xdr.SafeUnmarshalBase64(classicTxResponse.ResultXdr, &classicTxResult)) + require.NoError(t, xdr.SafeUnmarshalBase64(classicTxResponse.ResultXDR, &classicTxResult)) classicFee := uint64(classicTxResult.FeeCharged) var result methods.GetFeeStatsResult diff --git a/cmd/soroban-rpc/internal/integrationtest/get_ledger_entries_test.go b/cmd/soroban-rpc/internal/integrationtest/get_ledger_entries_test.go index 65b819b2..71a25b22 100644 --- a/cmd/soroban-rpc/internal/integrationtest/get_ledger_entries_test.go +++ b/cmd/soroban-rpc/internal/integrationtest/get_ledger_entries_test.go @@ -111,9 +111,9 @@ func TestGetLedgerEntriesSucceeds(t *testing.T) { require.LessOrEqual(t, result.Entries[0].LastModifiedLedger, result.LatestLedger) require.NotNil(t, result.Entries[0].LiveUntilLedgerSeq) require.Greater(t, *result.Entries[0].LiveUntilLedgerSeq, result.LatestLedger) - require.Equal(t, contractCodeKeyB64, result.Entries[0].Key) + require.Equal(t, contractCodeKeyB64, result.Entries[0].KeyXDR) var firstEntry xdr.LedgerEntryData - require.NoError(t, xdr.SafeUnmarshalBase64(result.Entries[0].XDR, &firstEntry)) + require.NoError(t, xdr.SafeUnmarshalBase64(result.Entries[0].DataXDR, &firstEntry)) require.Equal(t, xdr.LedgerEntryTypeContractCode, firstEntry.Type) require.Equal(t, infrastructure.GetHelloWorldContract(), firstEntry.MustContractCode().Code) @@ -121,9 +121,9 @@ func TestGetLedgerEntriesSucceeds(t *testing.T) { require.LessOrEqual(t, result.Entries[1].LastModifiedLedger, result.LatestLedger) require.NotNil(t, result.Entries[1].LiveUntilLedgerSeq) require.Greater(t, *result.Entries[1].LiveUntilLedgerSeq, result.LatestLedger) - require.Equal(t, contractInstanceKeyB64, result.Entries[1].Key) + require.Equal(t, contractInstanceKeyB64, result.Entries[1].KeyXDR) var secondEntry xdr.LedgerEntryData - require.NoError(t, xdr.SafeUnmarshalBase64(result.Entries[1].XDR, &secondEntry)) + require.NoError(t, xdr.SafeUnmarshalBase64(result.Entries[1].DataXDR, &secondEntry)) require.Equal(t, xdr.LedgerEntryTypeContractData, secondEntry.Type) require.True(t, secondEntry.MustContractData().Key.Equals(xdr.ScVal{ Type: xdr.ScValTypeScvLedgerKeyContractInstance, diff --git a/cmd/soroban-rpc/internal/integrationtest/get_ledger_entry_test.go b/cmd/soroban-rpc/internal/integrationtest/get_ledger_entry_test.go index 2554776f..439a2e85 100644 --- a/cmd/soroban-rpc/internal/integrationtest/get_ledger_entry_test.go +++ b/cmd/soroban-rpc/internal/integrationtest/get_ledger_entry_test.go @@ -5,7 +5,6 @@ import ( "testing" "github.com/creachadair/jrpc2" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stellar/go/xdr" @@ -39,8 +38,8 @@ func TestGetLedgerEntryNotFound(t *testing.T) { var result methods.GetLedgerEntryResponse client := test.GetRPCLient() jsonRPCErr := client.CallResult(context.Background(), "getLedgerEntry", request, &result).(*jrpc2.Error) - assert.Contains(t, jsonRPCErr.Message, "not found") - assert.Equal(t, jrpc2.InvalidRequest, jsonRPCErr.Code) + require.Contains(t, jsonRPCErr.Message, "not found") + require.Equal(t, jrpc2.InvalidRequest, jsonRPCErr.Code) } func TestGetLedgerEntryInvalidParams(t *testing.T) { @@ -54,8 +53,8 @@ func TestGetLedgerEntryInvalidParams(t *testing.T) { var result methods.GetLedgerEntryResponse jsonRPCErr := client.CallResult(context.Background(), "getLedgerEntry", request, &result).(*jrpc2.Error) - assert.Equal(t, "cannot unmarshal key value", jsonRPCErr.Message) - assert.Equal(t, jrpc2.InvalidParams, jsonRPCErr.Code) + require.Equal(t, "cannot unmarshal key value", jsonRPCErr.Message) + require.Equal(t, jrpc2.InvalidParams, jsonRPCErr.Code) } func TestGetLedgerEntrySucceeds(t *testing.T) { @@ -76,10 +75,10 @@ func TestGetLedgerEntrySucceeds(t *testing.T) { var result methods.GetLedgerEntryResponse err = test.GetRPCLient().CallResult(context.Background(), "getLedgerEntry", request, &result) - assert.NoError(t, err) - assert.Greater(t, result.LatestLedger, uint32(0)) - assert.GreaterOrEqual(t, result.LatestLedger, result.LastModifiedLedger) + require.NoError(t, err) + require.Greater(t, result.LatestLedger, uint32(0)) + require.GreaterOrEqual(t, result.LatestLedger, result.LastModifiedLedger) var entry xdr.LedgerEntryData - assert.NoError(t, xdr.SafeUnmarshalBase64(result.XDR, &entry)) - assert.Equal(t, infrastructure.GetHelloWorldContract(), entry.MustContractCode().Code) + require.NoError(t, xdr.SafeUnmarshalBase64(result.EntryXDR, &entry)) + require.Equal(t, infrastructure.GetHelloWorldContract(), entry.MustContractCode().Code) } diff --git a/cmd/soroban-rpc/internal/integrationtest/infrastructure/client.go b/cmd/soroban-rpc/internal/integrationtest/infrastructure/client.go index 18a154cb..9b08bb52 100644 --- a/cmd/soroban-rpc/internal/integrationtest/infrastructure/client.go +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/client.go @@ -57,7 +57,7 @@ func getTransaction(t *testing.T, client *Client, hash string) methods.GetTransa for i := 0; i < 60; i++ { request := methods.GetTransactionRequest{Hash: hash} err := client.CallResult(context.Background(), "getTransaction", request, &result) - assert.NoError(t, err) + require.NoError(t, err) if result.Status == methods.TransactionStatusNotFound { time.Sleep(time.Second) @@ -72,35 +72,35 @@ func getTransaction(t *testing.T, client *Client, hash string) methods.GetTransa func SendSuccessfulTransaction(t *testing.T, client *Client, kp *keypair.Full, transaction *txnbuild.Transaction) methods.GetTransactionResponse { tx, err := transaction.Sign(StandaloneNetworkPassphrase, kp) - assert.NoError(t, err) + require.NoError(t, err) b64, err := tx.Base64() - assert.NoError(t, err) + require.NoError(t, err) request := methods.SendTransactionRequest{Transaction: b64} var result methods.SendTransactionResponse - assert.NoError(t, client.CallResult(context.Background(), "sendTransaction", request, &result)) + require.NoError(t, client.CallResult(context.Background(), "sendTransaction", request, &result)) expectedHashHex, err := tx.HashHex(StandaloneNetworkPassphrase) - assert.NoError(t, err) + require.NoError(t, err) - assert.Equal(t, expectedHashHex, result.Hash) + require.Equal(t, expectedHashHex, result.Hash) if !assert.Equal(t, stellarcore.TXStatusPending, result.Status) { var txResult xdr.TransactionResult err := xdr.SafeUnmarshalBase64(result.ErrorResultXDR, &txResult) - assert.NoError(t, err) + require.NoError(t, err) t.Logf("error: %#v\n", txResult) } - assert.NotZero(t, result.LatestLedger) - assert.NotZero(t, result.LatestLedgerCloseTime) + require.NotZero(t, result.LatestLedger) + require.NotZero(t, result.LatestLedgerCloseTime) response := getTransaction(t, client, expectedHashHex) if !assert.Equal(t, methods.TransactionStatusSuccess, response.Status) { var txResult xdr.TransactionResult - assert.NoError(t, xdr.SafeUnmarshalBase64(response.ResultXdr, &txResult)) + require.NoError(t, xdr.SafeUnmarshalBase64(response.ResultXDR, &txResult)) t.Logf("error: %#v\n", txResult) var txMeta xdr.TransactionMeta - assert.NoError(t, xdr.SafeUnmarshalBase64(response.ResultMetaXdr, &txMeta)) + require.NoError(t, xdr.SafeUnmarshalBase64(response.ResultMetaXDR, &txMeta)) if txMeta.V == 3 && txMeta.V3.SorobanMeta != nil { if len(txMeta.V3.SorobanMeta.Events) > 0 { @@ -119,11 +119,11 @@ func SendSuccessfulTransaction(t *testing.T, client *Client, kp *keypair.Full, t } } - require.NotNil(t, response.ResultXdr) - assert.Greater(t, response.Ledger, result.LatestLedger) - assert.Greater(t, response.LedgerCloseTime, result.LatestLedgerCloseTime) - assert.GreaterOrEqual(t, response.LatestLedger, response.Ledger) - assert.GreaterOrEqual(t, response.LatestLedgerCloseTime, response.LedgerCloseTime) + require.NotNil(t, response.ResultXDR) + require.Greater(t, response.Ledger, result.LatestLedger) + require.Greater(t, response.LedgerCloseTime, result.LatestLedgerCloseTime) + require.GreaterOrEqual(t, response.LatestLedger, response.Ledger) + require.GreaterOrEqual(t, response.LatestLedgerCloseTime, response.LedgerCloseTime) return response } @@ -147,7 +147,7 @@ func PreflightTransactionParamsLocally(t *testing.T, params txnbuild.Transaction t.Log(response.Error) } var transactionData xdr.SorobanTransactionData - err := xdr.SafeUnmarshalBase64(response.TransactionData, &transactionData) + err := xdr.SafeUnmarshalBase64(response.TransactionDataXDR, &transactionData) require.NoError(t, err) op := params.Operations[0] @@ -159,7 +159,7 @@ func PreflightTransactionParamsLocally(t *testing.T, params txnbuild.Transaction SorobanData: &transactionData, } var auth []xdr.SorobanAuthorizationEntry - for _, b64 := range response.Results[0].Auth { + for _, b64 := range response.Results[0].AuthXDR { var a xdr.SorobanAuthorizationEntry err := xdr.SafeUnmarshalBase64(b64, &a) require.NoError(t, err) @@ -191,6 +191,6 @@ func PreflightTransactionParamsLocally(t *testing.T, params txnbuild.Transaction func PreflightTransactionParams(t *testing.T, client *Client, params txnbuild.TransactionParams) txnbuild.TransactionParams { response := SimulateTransactionFromTxParams(t, client, params) // The preamble should be zero except for the special restore case - assert.Nil(t, response.RestorePreamble) + require.Nil(t, response.RestorePreamble) return PreflightTransactionParamsLocally(t, params, response) } diff --git a/cmd/soroban-rpc/internal/integrationtest/simulate_transaction_test.go b/cmd/soroban-rpc/internal/integrationtest/simulate_transaction_test.go index 380c9307..c64f1bea 100644 --- a/cmd/soroban-rpc/internal/integrationtest/simulate_transaction_test.go +++ b/cmd/soroban-rpc/internal/integrationtest/simulate_transaction_test.go @@ -1,3 +1,4 @@ +//nolint:lll package integrationtest import ( @@ -6,7 +7,6 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stellar/go/keypair" @@ -31,9 +31,9 @@ func TestSimulateTransactionSucceeds(t *testing.T) { contractHash := sha256.Sum256(contractBinary) contractHashBytes := xdr.ScBytes(contractHash[:]) expectedXdr := xdr.ScVal{Type: xdr.ScValTypeScvBytes, Bytes: &contractHashBytes} - assert.Greater(t, result.LatestLedger, uint32(0)) - assert.Greater(t, result.Cost.CPUInstructions, uint64(0)) - assert.Greater(t, result.Cost.MemoryBytes, uint64(0)) + require.Greater(t, result.LatestLedger, uint32(0)) + require.Greater(t, result.Cost.CPUInstructions, uint64(0)) + require.Greater(t, result.Cost.MemoryBytes, uint64(0)) expectedTransactionData := xdr.SorobanTransactionData{ Resources: xdr.SorobanResources{ @@ -52,7 +52,7 @@ func TestSimulateTransactionSucceeds(t *testing.T) { WriteBytes: 7048, }, // the resulting fee is derived from the compute factors and a default padding is applied to instructions by preflight - // for test purposes, the most deterministic way to assert the resulting fee is expected value in test scope, is to capture + // for test purposes, the most deterministic way to require the resulting fee is expected value in test scope, is to capture // the resulting fee from current preflight output and re-plug it in here, rather than try to re-implement the cost-model algo // in the test. ResourceFee: 149755, @@ -60,34 +60,34 @@ func TestSimulateTransactionSucceeds(t *testing.T) { // First, decode and compare the transaction data so we get a decent diff if it fails. var transactionData xdr.SorobanTransactionData - err := xdr.SafeUnmarshalBase64(result.TransactionData, &transactionData) - assert.NoError(t, err) - assert.Equal(t, expectedTransactionData.Resources.Footprint, transactionData.Resources.Footprint) - assert.InDelta(t, uint32(expectedTransactionData.Resources.Instructions), uint32(transactionData.Resources.Instructions), 3200000) - assert.InDelta(t, uint32(expectedTransactionData.Resources.ReadBytes), uint32(transactionData.Resources.ReadBytes), 10) - assert.InDelta(t, uint32(expectedTransactionData.Resources.WriteBytes), uint32(transactionData.Resources.WriteBytes), 300) - assert.InDelta(t, int64(expectedTransactionData.ResourceFee), int64(transactionData.ResourceFee), 40000) + err := xdr.SafeUnmarshalBase64(result.TransactionDataXDR, &transactionData) + require.NoError(t, err) + require.Equal(t, expectedTransactionData.Resources.Footprint, transactionData.Resources.Footprint) + require.InDelta(t, uint32(expectedTransactionData.Resources.Instructions), uint32(transactionData.Resources.Instructions), 3200000) + require.InDelta(t, uint32(expectedTransactionData.Resources.ReadBytes), uint32(transactionData.Resources.ReadBytes), 10) + require.InDelta(t, uint32(expectedTransactionData.Resources.WriteBytes), uint32(transactionData.Resources.WriteBytes), 300) + require.InDelta(t, int64(expectedTransactionData.ResourceFee), int64(transactionData.ResourceFee), 40000) // Then decode and check the result xdr, separately so we get a decent diff if it fails. - assert.Len(t, result.Results, 1) + require.Len(t, result.Results, 1) var resultXdr xdr.ScVal - err = xdr.SafeUnmarshalBase64(result.Results[0].XDR, &resultXdr) - assert.NoError(t, err) - assert.Equal(t, expectedXdr, resultXdr) + err = xdr.SafeUnmarshalBase64(result.Results[0].ReturnValueXDR, &resultXdr) + require.NoError(t, err) + require.Equal(t, expectedXdr, resultXdr) // Check state diff - assert.Len(t, result.StateChanges, 1) - assert.Nil(t, result.StateChanges[0].Before) - assert.NotNil(t, result.StateChanges[0].After) - assert.Equal(t, methods.LedgerEntryChangeTypeCreated, result.StateChanges[0].Type) + require.Len(t, result.StateChanges, 1) + require.Nil(t, result.StateChanges[0].BeforeXDR) + require.NotNil(t, result.StateChanges[0].AfterXDR) + require.Equal(t, methods.LedgerEntryChangeTypeCreated, result.StateChanges[0].Type) var after xdr.LedgerEntry - assert.NoError(t, xdr.SafeUnmarshalBase64(*result.StateChanges[0].After, &after)) - assert.Equal(t, xdr.LedgerEntryTypeContractCode, after.Data.Type) + require.NoError(t, xdr.SafeUnmarshalBase64(*result.StateChanges[0].AfterXDR, &after)) + require.Equal(t, xdr.LedgerEntryTypeContractCode, after.Data.Type) entryKey, err := after.LedgerKey() - assert.NoError(t, err) + require.NoError(t, err) entryKeyB64, err := xdr.MarshalBase64(entryKey) - assert.NoError(t, err) - assert.Equal(t, entryKeyB64, result.StateChanges[0].Key) + require.NoError(t, err) + require.Equal(t, entryKeyB64, result.StateChanges[0].KeyXDR) // test operation which does not have a source account params = infrastructure.CreateTransactionParams(test.MasterAccount(), @@ -98,7 +98,7 @@ func TestSimulateTransactionSucceeds(t *testing.T) { resultForRequestWithoutOpSource := infrastructure.SimulateTransactionFromTxParams(t, client, params) // Let's not compare the latest ledger since it may change result.LatestLedger = resultForRequestWithoutOpSource.LatestLedger - assert.Equal(t, result, resultForRequestWithoutOpSource) + require.Equal(t, result, resultForRequestWithoutOpSource) // test that operation source account takes precedence over tx source account params = infrastructure.CreateTransactionParams( @@ -110,10 +110,10 @@ func TestSimulateTransactionSucceeds(t *testing.T) { ) resultForRequestWithDifferentTxSource := infrastructure.SimulateTransactionFromTxParams(t, client, params) - assert.GreaterOrEqual(t, resultForRequestWithDifferentTxSource.LatestLedger, result.LatestLedger) + require.GreaterOrEqual(t, resultForRequestWithDifferentTxSource.LatestLedger, result.LatestLedger) // apart from latest ledger the response should be the same resultForRequestWithDifferentTxSource.LatestLedger = result.LatestLedger - assert.Equal(t, result, resultForRequestWithDifferentTxSource) + require.Equal(t, result, resultForRequestWithDifferentTxSource) } func TestSimulateTransactionWithAuth(t *testing.T) { @@ -130,11 +130,11 @@ func TestSimulateTransactionWithAuth(t *testing.T) { client := test.GetRPCLient() response := infrastructure.SimulateTransactionFromTxParams(t, client, deployContractParams) require.NotEmpty(t, response.Results) - require.Len(t, response.Results[0].Auth, 1) + require.Len(t, response.Results[0].AuthXDR, 1) require.Empty(t, deployContractOp.Auth) var auth xdr.SorobanAuthorizationEntry - assert.NoError(t, xdr.SafeUnmarshalBase64(response.Results[0].Auth[0], &auth)) + require.NoError(t, xdr.SafeUnmarshalBase64(response.Results[0].AuthXDR[0], &auth)) require.Equal(t, auth.Credentials.Type, xdr.SorobanCredentialsTypeSorobanCredentialsSourceAccount) deployContractOp.Auth = append(deployContractOp.Auth, auth) deployContractParams.Operations = []txnbuild.Operation{deployContractOp} @@ -142,7 +142,7 @@ func TestSimulateTransactionWithAuth(t *testing.T) { // preflight deployContractOp with auth deployContractParams = infrastructure.PreflightTransactionParams(t, client, deployContractParams) tx, err := txnbuild.NewTransaction(deployContractParams) - assert.NoError(t, err) + require.NoError(t, err) test.SendMasterTransaction(tx) } @@ -179,90 +179,90 @@ func TestSimulateInvokeContractTransactionSucceeds(t *testing.T) { ), ) tx, err := txnbuild.NewTransaction(params) - assert.NoError(t, err) + require.NoError(t, err) txB64, err := tx.Base64() - assert.NoError(t, err) + require.NoError(t, err) request := methods.SimulateTransactionRequest{Transaction: txB64} var response methods.SimulateTransactionResponse err = test.GetRPCLient().CallResult(context.Background(), "simulateTransaction", request, &response) - assert.NoError(t, err) - assert.Empty(t, response.Error) + require.NoError(t, err) + require.Empty(t, response.Error) // check the result - assert.Len(t, response.Results, 1) + require.Len(t, response.Results, 1) var obtainedResult xdr.ScVal - err = xdr.SafeUnmarshalBase64(response.Results[0].XDR, &obtainedResult) - assert.NoError(t, err) - assert.Equal(t, xdr.ScValTypeScvAddress, obtainedResult.Type) + err = xdr.SafeUnmarshalBase64(response.Results[0].ReturnValueXDR, &obtainedResult) + require.NoError(t, err) + require.Equal(t, xdr.ScValTypeScvAddress, obtainedResult.Type) require.NotNil(t, obtainedResult.Address) - assert.Equal(t, authAccountIDArg, obtainedResult.Address.MustAccountId()) + require.Equal(t, authAccountIDArg, obtainedResult.Address.MustAccountId()) // check the footprint var obtainedTransactionData xdr.SorobanTransactionData - err = xdr.SafeUnmarshalBase64(response.TransactionData, &obtainedTransactionData) + err = xdr.SafeUnmarshalBase64(response.TransactionDataXDR, &obtainedTransactionData) obtainedFootprint := obtainedTransactionData.Resources.Footprint - assert.NoError(t, err) - assert.Len(t, obtainedFootprint.ReadWrite, 1) - assert.Len(t, obtainedFootprint.ReadOnly, 3) + require.NoError(t, err) + require.Len(t, obtainedFootprint.ReadWrite, 1) + require.Len(t, obtainedFootprint.ReadOnly, 3) ro0 := obtainedFootprint.ReadOnly[0] - assert.Equal(t, xdr.LedgerEntryTypeAccount, ro0.Type) - assert.Equal(t, authAddrArg, ro0.Account.AccountId.Address()) + require.Equal(t, xdr.LedgerEntryTypeAccount, ro0.Type) + require.Equal(t, authAddrArg, ro0.Account.AccountId.Address()) ro1 := obtainedFootprint.ReadOnly[1] - assert.Equal(t, xdr.LedgerEntryTypeContractData, ro1.Type) - assert.Equal(t, xdr.ScAddressTypeScAddressTypeContract, ro1.ContractData.Contract.Type) - assert.Equal(t, xdr.Hash(contractID), *ro1.ContractData.Contract.ContractId) - assert.Equal(t, xdr.ScValTypeScvLedgerKeyContractInstance, ro1.ContractData.Key.Type) + require.Equal(t, xdr.LedgerEntryTypeContractData, ro1.Type) + require.Equal(t, xdr.ScAddressTypeScAddressTypeContract, ro1.ContractData.Contract.Type) + require.Equal(t, xdr.Hash(contractID), *ro1.ContractData.Contract.ContractId) + require.Equal(t, xdr.ScValTypeScvLedgerKeyContractInstance, ro1.ContractData.Key.Type) ro2 := obtainedFootprint.ReadOnly[2] - assert.Equal(t, xdr.LedgerEntryTypeContractCode, ro2.Type) - assert.Equal(t, contractHash, ro2.ContractCode.Hash) - assert.NoError(t, err) + require.Equal(t, xdr.LedgerEntryTypeContractCode, ro2.Type) + require.Equal(t, contractHash, ro2.ContractCode.Hash) + require.NoError(t, err) - assert.NotZero(t, obtainedTransactionData.ResourceFee) - assert.NotZero(t, obtainedTransactionData.Resources.Instructions) - assert.NotZero(t, obtainedTransactionData.Resources.ReadBytes) - assert.NotZero(t, obtainedTransactionData.Resources.WriteBytes) + require.NotZero(t, obtainedTransactionData.ResourceFee) + require.NotZero(t, obtainedTransactionData.Resources.Instructions) + require.NotZero(t, obtainedTransactionData.Resources.ReadBytes) + require.NotZero(t, obtainedTransactionData.Resources.WriteBytes) // check the auth - assert.Len(t, response.Results[0].Auth, 1) + require.Len(t, response.Results[0].AuthXDR, 1) var obtainedAuth xdr.SorobanAuthorizationEntry - err = xdr.SafeUnmarshalBase64(response.Results[0].Auth[0], &obtainedAuth) - assert.NoError(t, err) - assert.Equal(t, obtainedAuth.Credentials.Type, xdr.SorobanCredentialsTypeSorobanCredentialsAddress) - assert.Equal(t, obtainedAuth.Credentials.Address.Signature.Type, xdr.ScValTypeScvVoid) - - assert.NotZero(t, obtainedAuth.Credentials.Address.Nonce) - assert.Equal(t, xdr.ScAddressTypeScAddressTypeAccount, obtainedAuth.Credentials.Address.Address.Type) - assert.Equal(t, authAddrArg, obtainedAuth.Credentials.Address.Address.AccountId.Address()) - - assert.Equal(t, xdr.SorobanCredentialsTypeSorobanCredentialsAddress, obtainedAuth.Credentials.Type) - assert.Equal(t, xdr.ScAddressTypeScAddressTypeAccount, obtainedAuth.Credentials.Address.Address.Type) - assert.Equal(t, authAddrArg, obtainedAuth.Credentials.Address.Address.AccountId.Address()) - assert.Equal(t, xdr.SorobanAuthorizedFunctionTypeSorobanAuthorizedFunctionTypeContractFn, obtainedAuth.RootInvocation.Function.Type) - assert.Equal(t, xdr.ScSymbol("auth"), obtainedAuth.RootInvocation.Function.ContractFn.FunctionName) - assert.Len(t, obtainedAuth.RootInvocation.Function.ContractFn.Args, 2) + err = xdr.SafeUnmarshalBase64(response.Results[0].AuthXDR[0], &obtainedAuth) + require.NoError(t, err) + require.Equal(t, xdr.SorobanCredentialsTypeSorobanCredentialsAddress, obtainedAuth.Credentials.Type) + require.Equal(t, xdr.ScValTypeScvVoid, obtainedAuth.Credentials.Address.Signature.Type) + + require.NotZero(t, obtainedAuth.Credentials.Address.Nonce) + require.Equal(t, xdr.ScAddressTypeScAddressTypeAccount, obtainedAuth.Credentials.Address.Address.Type) + require.Equal(t, authAddrArg, obtainedAuth.Credentials.Address.Address.AccountId.Address()) + + require.Equal(t, xdr.SorobanCredentialsTypeSorobanCredentialsAddress, obtainedAuth.Credentials.Type) + require.Equal(t, xdr.ScAddressTypeScAddressTypeAccount, obtainedAuth.Credentials.Address.Address.Type) + require.Equal(t, authAddrArg, obtainedAuth.Credentials.Address.Address.AccountId.Address()) + require.Equal(t, xdr.SorobanAuthorizedFunctionTypeSorobanAuthorizedFunctionTypeContractFn, obtainedAuth.RootInvocation.Function.Type) + require.Equal(t, xdr.ScSymbol("auth"), obtainedAuth.RootInvocation.Function.ContractFn.FunctionName) + require.Len(t, obtainedAuth.RootInvocation.Function.ContractFn.Args, 2) world := obtainedAuth.RootInvocation.Function.ContractFn.Args[1] - assert.Equal(t, xdr.ScValTypeScvSymbol, world.Type) - assert.Equal(t, xdr.ScSymbol("world"), *world.Sym) - assert.Nil(t, obtainedAuth.RootInvocation.SubInvocations) + require.Equal(t, xdr.ScValTypeScvSymbol, world.Type) + require.Equal(t, xdr.ScSymbol("world"), *world.Sym) + require.Nil(t, obtainedAuth.RootInvocation.SubInvocations) // check the events. There will be 2 debug events and the event emitted by the "auth" function // which is the one we are going to check. - assert.Len(t, response.Events, 3) + require.Len(t, response.EventsXDR, 3) var event xdr.DiagnosticEvent - err = xdr.SafeUnmarshalBase64(response.Events[1], &event) - assert.NoError(t, err) - assert.True(t, event.InSuccessfulContractCall) - assert.NotNil(t, event.Event.ContractId) - assert.Equal(t, xdr.Hash(contractID), *event.Event.ContractId) - assert.Equal(t, xdr.ContractEventTypeContract, event.Event.Type) - assert.Equal(t, int32(0), event.Event.Body.V) - assert.Equal(t, xdr.ScValTypeScvSymbol, event.Event.Body.V0.Data.Type) - assert.Equal(t, xdr.ScSymbol("world"), *event.Event.Body.V0.Data.Sym) - assert.Len(t, event.Event.Body.V0.Topics, 1) - assert.Equal(t, xdr.ScValTypeScvString, event.Event.Body.V0.Topics[0].Type) - assert.Equal(t, xdr.ScString("auth"), *event.Event.Body.V0.Topics[0].Str) + err = xdr.SafeUnmarshalBase64(response.EventsXDR[1], &event) + require.NoError(t, err) + require.True(t, event.InSuccessfulContractCall) + require.NotNil(t, event.Event.ContractId) + require.Equal(t, xdr.Hash(contractID), *event.Event.ContractId) + require.Equal(t, xdr.ContractEventTypeContract, event.Event.Type) + require.Equal(t, int32(0), event.Event.Body.V) + require.Equal(t, xdr.ScValTypeScvSymbol, event.Event.Body.V0.Data.Type) + require.Equal(t, xdr.ScSymbol("world"), *event.Event.Body.V0.Data.Sym) + require.Len(t, event.Event.Body.V0.Topics, 1) + require.Equal(t, xdr.ScValTypeScvString, event.Event.Body.V0.Topics[0].Type) + require.Equal(t, xdr.ScString("auth"), *event.Event.Body.V0.Topics[0].Str) } func TestSimulateTransactionError(t *testing.T) { @@ -291,11 +291,11 @@ func TestSimulateTransactionError(t *testing.T) { invokeHostOp, ) result := infrastructure.SimulateTransactionFromTxParams(t, client, params) - assert.Greater(t, result.LatestLedger, uint32(0)) - assert.Contains(t, result.Error, "MissingValue") - require.GreaterOrEqual(t, len(result.Events), 1) + require.Greater(t, result.LatestLedger, uint32(0)) + require.Contains(t, result.Error, "MissingValue") + require.GreaterOrEqual(t, len(result.EventsXDR), 1) var event xdr.DiagnosticEvent - require.NoError(t, xdr.SafeUnmarshalBase64(result.Events[0], &event)) + require.NoError(t, xdr.SafeUnmarshalBase64(result.EventsXDR[0], &event)) } func TestSimulateTransactionMultipleOperations(t *testing.T) { @@ -319,7 +319,7 @@ func TestSimulateTransactionMultipleOperations(t *testing.T) { client := test.GetRPCLient() result := infrastructure.SimulateTransactionFromTxParams(t, client, params) - assert.Equal( + require.Equal( t, methods.SimulateTransactionResponse{ Error: "Transaction contains more than one operation", @@ -338,7 +338,7 @@ func TestSimulateTransactionWithoutInvokeHostFunction(t *testing.T) { client := test.GetRPCLient() result := infrastructure.SimulateTransactionFromTxParams(t, client, params) - assert.Equal( + require.Equal( t, methods.SimulateTransactionResponse{ Error: "Transaction contains unsupported operation type: OperationTypeBumpSequence", @@ -355,8 +355,8 @@ func TestSimulateTransactionUnmarshalError(t *testing.T) { request := methods.SimulateTransactionRequest{Transaction: "invalid"} var result methods.SimulateTransactionResponse err := client.CallResult(context.Background(), "simulateTransaction", request, &result) - assert.NoError(t, err) - assert.Equal( + require.NoError(t, err) + require.Equal( t, "Could not unmarshal transaction", result.Error, @@ -383,11 +383,11 @@ func TestSimulateTransactionExtendAndRestoreFootprint(t *testing.T) { var getLedgerEntryResult methods.GetLedgerEntryResponse client := test.GetRPCLient() err = client.CallResult(context.Background(), "getLedgerEntry", getLedgerEntryrequest, &getLedgerEntryResult) - assert.NoError(t, err) + require.NoError(t, err) var entry xdr.LedgerEntryData - assert.NoError(t, xdr.SafeUnmarshalBase64(getLedgerEntryResult.XDR, &entry)) - assert.Equal(t, xdr.LedgerEntryTypeContractData, entry.Type) + require.NoError(t, xdr.SafeUnmarshalBase64(getLedgerEntryResult.EntryXDR, &entry)) + require.Equal(t, xdr.LedgerEntryTypeContractData, entry.Type) require.NotNil(t, getLedgerEntryResult.LiveUntilLedgerSeq) initialLiveUntil := *getLedgerEntryResult.LiveUntilLedgerSeq @@ -409,12 +409,12 @@ func TestSimulateTransactionExtendAndRestoreFootprint(t *testing.T) { ) err = client.CallResult(context.Background(), "getLedgerEntry", getLedgerEntryrequest, &getLedgerEntryResult) - assert.NoError(t, err) - assert.NoError(t, xdr.SafeUnmarshalBase64(getLedgerEntryResult.XDR, &entry)) - assert.Equal(t, xdr.LedgerEntryTypeContractData, entry.Type) + require.NoError(t, err) + require.NoError(t, xdr.SafeUnmarshalBase64(getLedgerEntryResult.EntryXDR, &entry)) + require.Equal(t, xdr.LedgerEntryTypeContractData, entry.Type) require.NotNil(t, getLedgerEntryResult.LiveUntilLedgerSeq) newLiveUntilSeq := *getLedgerEntryResult.LiveUntilLedgerSeq - assert.Greater(t, newLiveUntilSeq, initialLiveUntil) + require.Greater(t, newLiveUntilSeq, initialLiveUntil) // Wait until it is not live anymore waitUntilLedgerEntryTTL(t, client, key) @@ -444,7 +444,7 @@ func TestSimulateTransactionExtendAndRestoreFootprint(t *testing.T) { ) simulationResult := infrastructure.SimulateTransactionFromTxParams(t, client, invokeIncPresistentEntryParams) require.NotNil(t, simulationResult.RestorePreamble) - assert.NotZero(t, simulationResult.RestorePreamble) + require.NotZero(t, simulationResult.RestorePreamble) params := infrastructure.PreflightTransactionParamsLocally( t, @@ -453,19 +453,19 @@ func TestSimulateTransactionExtendAndRestoreFootprint(t *testing.T) { &txnbuild.RestoreFootprint{}, ), methods.SimulateTransactionResponse{ - TransactionData: simulationResult.RestorePreamble.TransactionData, - MinResourceFee: simulationResult.RestorePreamble.MinResourceFee, + TransactionDataXDR: simulationResult.RestorePreamble.TransactionDataXDR, + MinResourceFee: simulationResult.RestorePreamble.MinResourceFee, }, ) tx, err := txnbuild.NewTransaction(params) - assert.NoError(t, err) + require.NoError(t, err) test.SendMasterTransaction(tx) // Finally, we should be able to send the inc host function invocation now that we // have pre-restored the entries params = infrastructure.PreflightTransactionParamsLocally(t, invokeIncPresistentEntryParams, simulationResult) tx, err = txnbuild.NewTransaction(params) - assert.NoError(t, err) + require.NoError(t, err) test.SendMasterTransaction(tx) } @@ -502,7 +502,7 @@ func waitUntilLedgerEntryTTL(t *testing.T, client *infrastructure.Client, ledger err := client.CallResult(context.Background(), "getLedgerEntries", request, &result) require.NoError(t, err) require.NotEmpty(t, result.Entries) - require.NoError(t, xdr.SafeUnmarshalBase64(result.Entries[0].XDR, &entry)) + require.NoError(t, xdr.SafeUnmarshalBase64(result.Entries[0].DataXDR, &entry)) require.NotEqual(t, xdr.LedgerEntryTypeTtl, entry.Type) liveUntilLedgerSeq := xdr.Uint32(*result.Entries[0].LiveUntilLedgerSeq) // See https://soroban.stellar.org/docs/fundamentals-and-concepts/state-expiration#expiration-ledger @@ -564,7 +564,7 @@ func TestSimulateInvokePrng_u64_in_range(t *testing.T) { // check the result require.Len(t, response.Results, 1) var obtainedResult xdr.ScVal - err = xdr.SafeUnmarshalBase64(response.Results[0].XDR, &obtainedResult) + err = xdr.SafeUnmarshalBase64(response.Results[0].ReturnValueXDR, &obtainedResult) require.NoError(t, err) require.Equal(t, xdr.ScValTypeScvU64, obtainedResult.Type) require.LessOrEqual(t, uint64(*obtainedResult.U64), uint64(high)) @@ -612,19 +612,19 @@ func TestSimulateSystemEvent(t *testing.T) { // check the result require.Len(t, response.Results, 1) var obtainedResult xdr.ScVal - err = xdr.SafeUnmarshalBase64(response.Results[0].XDR, &obtainedResult) + err = xdr.SafeUnmarshalBase64(response.Results[0].ReturnValueXDR, &obtainedResult) require.NoError(t, err) var transactionData xdr.SorobanTransactionData - err = xdr.SafeUnmarshalBase64(response.TransactionData, &transactionData) + err = xdr.SafeUnmarshalBase64(response.TransactionDataXDR, &transactionData) require.NoError(t, err) - assert.InDelta(t, 6856, uint32(transactionData.Resources.ReadBytes), 200) + require.InDelta(t, 6856, uint32(transactionData.Resources.ReadBytes), 200) // the resulting fee is derived from compute factors and a default padding is applied to instructions by preflight - // for test purposes, the most deterministic way to assert the resulting fee is expected value in test scope, is to capture + // for test purposes, the most deterministic way to require the resulting fee is expected value in test scope, is to capture // the resulting fee from current preflight output and re-plug it in here, rather than try to re-implement the cost-model algo // in the test. - assert.InDelta(t, 70668, int64(transactionData.ResourceFee), 20000) - assert.InDelta(t, 104, uint32(transactionData.Resources.WriteBytes), 15) - require.GreaterOrEqual(t, len(response.Events), 3) + require.InDelta(t, 70668, int64(transactionData.ResourceFee), 20000) + require.InDelta(t, 104, uint32(transactionData.Resources.WriteBytes), 15) + require.GreaterOrEqual(t, len(response.EventsXDR), 3) } diff --git a/cmd/soroban-rpc/internal/integrationtest/transaction_test.go b/cmd/soroban-rpc/internal/integrationtest/transaction_test.go index 37a0c9be..7dc0be0e 100644 --- a/cmd/soroban-rpc/internal/integrationtest/transaction_test.go +++ b/cmd/soroban-rpc/internal/integrationtest/transaction_test.go @@ -6,6 +6,7 @@ import ( "github.com/creachadair/jrpc2" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/stellar/go/keypair" proto "github.com/stellar/go/protocols/stellarcore" @@ -30,19 +31,19 @@ func TestSendTransactionSucceedsWithResults(t *testing.T) { // Check the result is what we expect var transactionResult xdr.TransactionResult - assert.NoError(t, xdr.SafeUnmarshalBase64(response.ResultXdr, &transactionResult)) + require.NoError(t, xdr.SafeUnmarshalBase64(response.ResultXDR, &transactionResult)) opResults, ok := transactionResult.OperationResults() - assert.True(t, ok) + require.True(t, ok) invokeHostFunctionResult, ok := opResults[0].MustTr().GetInvokeHostFunctionResult() - assert.True(t, ok) - assert.Equal(t, invokeHostFunctionResult.Code, xdr.InvokeHostFunctionResultCodeInvokeHostFunctionSuccess) + require.True(t, ok) + require.Equal(t, xdr.InvokeHostFunctionResultCodeInvokeHostFunctionSuccess, invokeHostFunctionResult.Code) contractHashBytes := xdr.ScBytes(contractHash[:]) expectedScVal := xdr.ScVal{Type: xdr.ScValTypeScvBytes, Bytes: &contractHashBytes} var transactionMeta xdr.TransactionMeta - assert.NoError(t, xdr.SafeUnmarshalBase64(response.ResultMetaXdr, &transactionMeta)) - assert.True(t, expectedScVal.Equals(transactionMeta.V3.SorobanMeta.ReturnValue)) + require.NoError(t, xdr.SafeUnmarshalBase64(response.ResultMetaXDR, &transactionMeta)) + require.True(t, expectedScVal.Equals(transactionMeta.V3.SorobanMeta.ReturnValue)) var resultXdr xdr.TransactionResult - assert.NoError(t, xdr.SafeUnmarshalBase64(response.ResultXdr, &resultXdr)) + require.NoError(t, xdr.SafeUnmarshalBase64(response.ResultXDR, &resultXdr)) expectedResult := xdr.TransactionResult{ FeeCharged: resultXdr.FeeCharged, Result: xdr.TransactionResultResult{ @@ -62,7 +63,7 @@ func TestSendTransactionSucceedsWithResults(t *testing.T) { }, } - assert.Equal(t, expectedResult, resultXdr) + require.Equal(t, expectedResult, resultXdr) } func TestSendTransactionBadSequence(t *testing.T) { @@ -74,27 +75,27 @@ func TestSendTransactionBadSequence(t *testing.T) { ) params.IncrementSequenceNum = false tx, err := txnbuild.NewTransaction(params) - assert.NoError(t, err) + require.NoError(t, err) tx, err = tx.Sign(infrastructure.StandaloneNetworkPassphrase, test.MasterKey()) - assert.NoError(t, err) + require.NoError(t, err) b64, err := tx.Base64() - assert.NoError(t, err) + require.NoError(t, err) request := methods.SendTransactionRequest{Transaction: b64} var result methods.SendTransactionResponse client := test.GetRPCLient() err = client.CallResult(context.Background(), "sendTransaction", request, &result) - assert.NoError(t, err) + require.NoError(t, err) - assert.NotZero(t, result.LatestLedger) - assert.NotZero(t, result.LatestLedgerCloseTime) + require.NotZero(t, result.LatestLedger) + require.NotZero(t, result.LatestLedgerCloseTime) expectedHashHex, err := tx.HashHex(infrastructure.StandaloneNetworkPassphrase) - assert.NoError(t, err) - assert.Equal(t, expectedHashHex, result.Hash) - assert.Equal(t, proto.TXStatusError, result.Status) + require.NoError(t, err) + require.Equal(t, expectedHashHex, result.Hash) + require.Equal(t, proto.TXStatusError, result.Status) var errorResult xdr.TransactionResult - assert.NoError(t, xdr.SafeUnmarshalBase64(result.ErrorResultXDR, &errorResult)) - assert.Equal(t, xdr.TransactionResultCodeTxBadSeq, errorResult.Result.Code) + require.NoError(t, xdr.SafeUnmarshalBase64(result.ErrorResultXDR, &errorResult)) + require.Equal(t, xdr.TransactionResultCodeTxBadSeq, errorResult.Result.Code) } func TestSendTransactionFailedInsufficientResourceFee(t *testing.T) { @@ -113,28 +114,28 @@ func TestSendTransactionFailedInsufficientResourceFee(t *testing.T) { params.Operations[0].(*txnbuild.InvokeHostFunction).Ext.SorobanData.ResourceFee /= 2 tx, err := txnbuild.NewTransaction(params) - assert.NoError(t, err) + require.NoError(t, err) - assert.NoError(t, err) + require.NoError(t, err) tx, err = tx.Sign(infrastructure.StandaloneNetworkPassphrase, test.MasterKey()) - assert.NoError(t, err) + require.NoError(t, err) b64, err := tx.Base64() - assert.NoError(t, err) + require.NoError(t, err) request := methods.SendTransactionRequest{Transaction: b64} var result methods.SendTransactionResponse err = client.CallResult(context.Background(), "sendTransaction", request, &result) - assert.NoError(t, err) + require.NoError(t, err) - assert.Equal(t, proto.TXStatusError, result.Status) + require.Equal(t, proto.TXStatusError, result.Status) var errorResult xdr.TransactionResult - assert.NoError(t, xdr.SafeUnmarshalBase64(result.ErrorResultXDR, &errorResult)) - assert.Equal(t, xdr.TransactionResultCodeTxSorobanInvalid, errorResult.Result.Code) + require.NoError(t, xdr.SafeUnmarshalBase64(result.ErrorResultXDR, &errorResult)) + require.Equal(t, xdr.TransactionResultCodeTxSorobanInvalid, errorResult.Result.Code) - assert.NotEmpty(t, result.DiagnosticEventsXDR) + require.NotEmpty(t, result.DiagnosticEventsXDR) var event xdr.DiagnosticEvent err = xdr.SafeUnmarshalBase64(result.DiagnosticEventsXDR[0], &event) - assert.NoError(t, err) + require.NoError(t, err) } func TestSendTransactionFailedInLedger(t *testing.T) { @@ -155,39 +156,39 @@ func TestSendTransactionFailedInLedger(t *testing.T) { }, ), ) - assert.NoError(t, err) + require.NoError(t, err) tx, err = tx.Sign(infrastructure.StandaloneNetworkPassphrase, kp) - assert.NoError(t, err) + require.NoError(t, err) b64, err := tx.Base64() - assert.NoError(t, err) + require.NoError(t, err) request := methods.SendTransactionRequest{Transaction: b64} var result methods.SendTransactionResponse err = client.CallResult(context.Background(), "sendTransaction", request, &result) - assert.NoError(t, err) + require.NoError(t, err) expectedHashHex, err := tx.HashHex(infrastructure.StandaloneNetworkPassphrase) - assert.NoError(t, err) + require.NoError(t, err) - assert.Equal(t, expectedHashHex, result.Hash) + require.Equal(t, expectedHashHex, result.Hash) if !assert.Equal(t, proto.TXStatusPending, result.Status) { var txResult xdr.TransactionResult err := xdr.SafeUnmarshalBase64(result.ErrorResultXDR, &txResult) - assert.NoError(t, err) + require.NoError(t, err) t.Logf("error: %#v\n", txResult) } - assert.NotZero(t, result.LatestLedger) - assert.NotZero(t, result.LatestLedgerCloseTime) + require.NotZero(t, result.LatestLedger) + require.NotZero(t, result.LatestLedgerCloseTime) response := test.GetTransaction(expectedHashHex) - assert.Equal(t, methods.TransactionStatusFailed, response.Status) + require.Equal(t, methods.TransactionStatusFailed, response.Status) var transactionResult xdr.TransactionResult - assert.NoError(t, xdr.SafeUnmarshalBase64(response.ResultXdr, &transactionResult)) - assert.Equal(t, xdr.TransactionResultCodeTxFailed, transactionResult.Result.Code) - assert.Greater(t, response.Ledger, result.LatestLedger) - assert.Greater(t, response.LedgerCloseTime, result.LatestLedgerCloseTime) - assert.GreaterOrEqual(t, response.LatestLedger, response.Ledger) - assert.GreaterOrEqual(t, response.LatestLedgerCloseTime, response.LedgerCloseTime) + require.NoError(t, xdr.SafeUnmarshalBase64(response.ResultXDR, &transactionResult)) + require.Equal(t, xdr.TransactionResultCodeTxFailed, transactionResult.Result.Code) + require.Greater(t, response.Ledger, result.LatestLedger) + require.Greater(t, response.LedgerCloseTime, result.LatestLedgerCloseTime) + require.GreaterOrEqual(t, response.LatestLedger, response.Ledger) + require.GreaterOrEqual(t, response.LatestLedgerCloseTime, response.LedgerCloseTime) } func TestSendTransactionFailedInvalidXDR(t *testing.T) { @@ -198,6 +199,6 @@ func TestSendTransactionFailedInvalidXDR(t *testing.T) { request := methods.SendTransactionRequest{Transaction: "abcdef"} var response methods.SendTransactionResponse jsonRPCErr := client.CallResult(context.Background(), "sendTransaction", request, &response).(*jrpc2.Error) - assert.Equal(t, "invalid_xdr", jsonRPCErr.Message) - assert.Equal(t, jrpc2.InvalidParams, jsonRPCErr.Code) + require.Equal(t, "invalid_xdr", jsonRPCErr.Message) + require.Equal(t, jrpc2.InvalidParams, jsonRPCErr.Code) } diff --git a/cmd/soroban-rpc/internal/methods/get_events.go b/cmd/soroban-rpc/internal/methods/get_events.go index 0ce602fc..db730f09 100644 --- a/cmd/soroban-rpc/internal/methods/get_events.go +++ b/cmd/soroban-rpc/internal/methods/get_events.go @@ -14,6 +14,7 @@ import ( "github.com/stellar/go/xdr" "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/events" + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/xdr2json" ) type eventTypeSet map[string]interface{} @@ -66,32 +67,43 @@ func (e eventTypeSet) matches(event xdr.ContractEvent) bool { } type EventInfo struct { - EventType string `json:"type"` - Ledger int32 `json:"ledger"` - LedgerClosedAt string `json:"ledgerClosedAt"` - ContractID string `json:"contractId"` - ID string `json:"id"` - PagingToken string `json:"pagingToken"` - Topic []string `json:"topic"` - Value string `json:"value"` - InSuccessfulContractCall bool `json:"inSuccessfulContractCall"` - TransactionHash string `json:"txHash"` + EventType string `json:"type"` + Ledger int32 `json:"ledger"` + LedgerClosedAt string `json:"ledgerClosedAt"` + ContractID string `json:"contractId"` + ID string `json:"id"` + PagingToken string `json:"pagingToken"` + InSuccessfulContractCall bool `json:"inSuccessfulContractCall"` + TransactionHash string `json:"txHash"` + + // TopicXDR is a base64-encoded list of ScVals + TopicXDR []string `json:"topic,omitempty"` + TopicJSON []json.RawMessage `json:"topicJson,omitempty"` + + // ValueXDR is a base64-encoded ScVal + ValueXDR string `json:"value,omitempty"` + ValueJSON json.RawMessage `json:"valueJson,omitempty"` } type GetEventsRequest struct { StartLedger uint32 `json:"startLedger,omitempty"` Filters []EventFilter `json:"filters"` Pagination *PaginationOptions `json:"pagination,omitempty"` + Format string `json:"xdrFormat,omitempty"` } func (g *GetEventsRequest) Valid(maxLimit uint) error { - // Validate start + if err := IsValidFormat(g.Format); err != nil { + return err + } + // Validate the paging limit (if it exists) if g.Pagination != nil && g.Pagination.Cursor != nil { if g.StartLedger != 0 { return errors.New("startLedger and cursor cannot both be set") } } else if g.StartLedger <= 0 { + // Validate start return errors.New("startLedger must be positive") } if g.Pagination != nil && g.Pagination.Limit > maxLimit { @@ -369,6 +381,7 @@ func (h eventsRPCHandler) getEvents(request GetEventsRequest) (GetEventsResponse entry.cursor, time.Unix(entry.ledgerCloseTimestamp, 0).UTC().Format(time.RFC3339), entry.txHash.HexString(), + request.Format, ) if err != nil { return GetEventsResponse{}, errors.Wrap(err, "could not parse event") @@ -381,7 +394,11 @@ func (h eventsRPCHandler) getEvents(request GetEventsRequest) (GetEventsResponse }, nil } -func eventInfoForEvent(event xdr.DiagnosticEvent, cursor events.Cursor, ledgerClosedAt string, txHash string) (EventInfo, error) { +func eventInfoForEvent( + event xdr.DiagnosticEvent, + cursor events.Cursor, + ledgerClosedAt, txHash, format string, +) (EventInfo, error) { v0, ok := event.Event.Body.GetV0() if !ok { return EventInfo{}, errors.New("unknown event version") @@ -392,35 +409,59 @@ func eventInfoForEvent(event xdr.DiagnosticEvent, cursor events.Cursor, ledgerCl return EventInfo{}, fmt.Errorf("unknown XDR ContractEventType type: %d", event.Event.Type) } - // base64-xdr encode the topic - topic := make([]string, 0, 4) - for _, segment := range v0.Topics { - seg, err := xdr.MarshalBase64(segment) - if err != nil { - return EventInfo{}, err - } - topic = append(topic, seg) - } - - // base64-xdr encode the data - data, err := xdr.MarshalBase64(v0.Data) - if err != nil { - return EventInfo{}, err - } - info := EventInfo{ EventType: eventType, Ledger: int32(cursor.Ledger), LedgerClosedAt: ledgerClosedAt, ID: cursor.String(), PagingToken: cursor.String(), - Topic: topic, - Value: data, InSuccessfulContractCall: event.InSuccessfulContractCall, TransactionHash: txHash, } + + switch format { + case FormatJSON: + // json encode the topic + info.TopicJSON = make([]json.RawMessage, 0, maxTopicCount) + for _, topic := range v0.Topics { + topic, err := xdr2json.ConvertInterface(topic) + if err != nil { + return EventInfo{}, err + } + info.TopicJSON = append(info.TopicJSON, topic) + } + + var convErr error + info.ValueJSON, convErr = xdr2json.ConvertInterface(v0.Data) + if convErr != nil { + return EventInfo{}, convErr + } + + default: + // base64-xdr encode the topic + topic := make([]string, 0, maxTopicCount) + for _, segment := range v0.Topics { + seg, err := xdr.MarshalBase64(segment) + if err != nil { + return EventInfo{}, err + } + topic = append(topic, seg) + } + + // base64-xdr encode the data + data, err := xdr.MarshalBase64(v0.Data) + if err != nil { + return EventInfo{}, err + } + + info.TopicXDR = topic + info.ValueXDR = data + } + if event.Event.ContractId != nil { - info.ContractID = strkey.MustEncode(strkey.VersionByteContract, (*event.Event.ContractId)[:]) + info.ContractID = strkey.MustEncode( + strkey.VersionByteContract, + (*event.Event.ContractId)[:]) } return info, nil } diff --git a/cmd/soroban-rpc/internal/methods/get_events_test.go b/cmd/soroban-rpc/internal/methods/get_events_test.go index 1960ad0d..e5b5c464 100644 --- a/cmd/soroban-rpc/internal/methods/get_events_test.go +++ b/cmd/soroban-rpc/internal/methods/get_events_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/stellar/go/keypair" "github.com/stellar/go/network" @@ -16,6 +17,7 @@ import ( "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/daemon/interfaces" "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/events" + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/xdr2json" ) func TestEventTypeSetMatches(t *testing.T) { @@ -624,8 +626,8 @@ func TestGetEvents(t *testing.T) { ContractID: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", ID: id, PagingToken: id, - Topic: []string{value}, - Value: value, + TopicXDR: []string{value}, + ValueXDR: value, InSuccessfulContractCall: true, TransactionHash: ledgerCloseMeta.TransactionHash(i).HexString(), }) @@ -725,10 +727,11 @@ func TestGetEvents(t *testing.T) { id := events.Cursor{Ledger: 1, Tx: 5, Op: 0, Event: 0}.String() assert.NoError(t, err) - value, err := xdr.MarshalBase64(xdr.ScVal{ + scVal := xdr.ScVal{ Type: xdr.ScValTypeScvU64, U64: &number, - }) + } + value, err := xdr.MarshalBase64(scVal) assert.NoError(t, err) expected := []EventInfo{ { @@ -738,13 +741,47 @@ func TestGetEvents(t *testing.T) { ContractID: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", ID: id, PagingToken: id, - Topic: []string{counterXdr, value}, - Value: value, + TopicXDR: []string{counterXdr, value}, + ValueXDR: value, InSuccessfulContractCall: true, TransactionHash: ledgerCloseMeta.TransactionHash(4).HexString(), }, } assert.Equal(t, GetEventsResponse{expected, 1}, results) + + results, err = handler.getEvents(GetEventsRequest{ + StartLedger: 1, + Format: FormatJSON, + Filters: []EventFilter{ + {Topics: []TopicFilter{ + []SegmentFilter{ + {scval: &xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &counter}}, + {scval: &xdr.ScVal{Type: xdr.ScValTypeScvU64, U64: &number}}, + }, + }}, + }, + }) + require.NoError(t, err) + + // + // Test that JSON conversion will work correctly + // + + expected[0].TopicXDR = nil + expected[0].ValueXDR = "" + + valueJs, err := xdr2json.ConvertInterface(scVal) + require.NoError(t, err) + + topicsJs := make([]json.RawMessage, 2) + for i, scv := range []xdr.ScVal{counterScVal, scVal} { + topicsJs[i], err = xdr2json.ConvertInterface(scv) + require.NoError(t, err) + } + + expected[0].ValueJSON = valueJs + expected[0].TopicJSON = topicsJs + require.Equal(t, GetEventsResponse{expected, 1}, results) }) t.Run("filtering by both contract id and topic", func(t *testing.T) { @@ -834,8 +871,8 @@ func TestGetEvents(t *testing.T) { ContractID: strkey.MustEncode(strkey.VersionByteContract, contractID[:]), ID: id, PagingToken: id, - Topic: []string{counterXdr, value}, - Value: value, + TopicXDR: []string{counterXdr, value}, + ValueXDR: value, InSuccessfulContractCall: true, TransactionHash: ledgerCloseMeta.TransactionHash(3).HexString(), }, @@ -896,8 +933,8 @@ func TestGetEvents(t *testing.T) { ContractID: strkey.MustEncode(strkey.VersionByteContract, contractID[:]), ID: id, PagingToken: id, - Topic: []string{counterXdr}, - Value: counterXdr, + TopicXDR: []string{counterXdr}, + ValueXDR: counterXdr, InSuccessfulContractCall: true, TransactionHash: ledgerCloseMeta.TransactionHash(0).HexString(), }, @@ -953,8 +990,8 @@ func TestGetEvents(t *testing.T) { ContractID: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", ID: id, PagingToken: id, - Topic: []string{value}, - Value: value, + TopicXDR: []string{value}, + ValueXDR: value, InSuccessfulContractCall: true, TransactionHash: ledgerCloseMeta.TransactionHash(i).HexString(), }) @@ -1039,8 +1076,8 @@ func TestGetEvents(t *testing.T) { ContractID: strkey.MustEncode(strkey.VersionByteContract, contractID[:]), ID: id, PagingToken: id, - Topic: []string{counterXdr}, - Value: expectedXdr, + TopicXDR: []string{counterXdr}, + ValueXDR: expectedXdr, InSuccessfulContractCall: true, TransactionHash: ledgerCloseMeta.TransactionHash(i).HexString(), }) diff --git a/cmd/soroban-rpc/internal/methods/get_ledger_entries.go b/cmd/soroban-rpc/internal/methods/get_ledger_entries.go index f7c63b91..3984cdcc 100644 --- a/cmd/soroban-rpc/internal/methods/get_ledger_entries.go +++ b/cmd/soroban-rpc/internal/methods/get_ledger_entries.go @@ -2,6 +2,7 @@ package methods import ( "context" + "encoding/json" "fmt" "github.com/creachadair/jrpc2" @@ -10,20 +11,24 @@ import ( "github.com/stellar/go/xdr" "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/db" + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/xdr2json" ) //nolint:gochecknoglobals var ErrLedgerTTLEntriesCannotBeQueriedDirectly = "ledger ttl entries cannot be queried directly" type GetLedgerEntriesRequest struct { - Keys []string `json:"keys"` + Keys []string `json:"keys"` + Format string `json:"xdrFormat,omitempty"` } type LedgerEntryResult struct { // Original request key matching this LedgerEntryResult. - Key string `json:"key"` + KeyXDR string `json:"key,omitempty"` + KeyJSON json.RawMessage `json:"keyJson,omitempty"` // Ledger entry data encoded in base 64. - XDR string `json:"xdr"` + DataXDR string `json:"xdr,omitempty"` + DataJSON json.RawMessage `json:"dataJson,omitempty"` // Last modified ledger for this entry. LastModifiedLedger uint32 `json:"lastModifiedLedgerSeq"` // The ledger sequence until the entry is live, available for entries that have associated ttl ledger entries. @@ -42,6 +47,13 @@ const getLedgerEntriesMaxKeys = 200 // NewGetLedgerEntriesHandler returns a JSON RPC handler to retrieve the specified ledger entries from Stellar Core. func NewGetLedgerEntriesHandler(logger *log.Entry, ledgerEntryReader db.LedgerEntryReader) jrpc2.Handler { return NewHandler(func(ctx context.Context, request GetLedgerEntriesRequest) (GetLedgerEntriesResponse, error) { + if err := IsValidFormat(request.Format); err != nil { + return GetLedgerEntriesResponse{}, &jrpc2.Error{ + Code: jrpc2.InvalidParams, + Message: err.Error(), + } + } + if len(request.Keys) > getLedgerEntriesMaxKeys { return GetLedgerEntriesResponse{}, &jrpc2.Error{ Code: jrpc2.InvalidParams, @@ -101,32 +113,54 @@ func NewGetLedgerEntriesHandler(logger *log.Entry, ledgerEntryReader db.LedgerEn } for _, ledgerKeyAndEntry := range ledgerKeysAndEntries { - keyXDR, err := xdr.MarshalBase64(ledgerKeyAndEntry.Key) - if err != nil { - logger.WithError(err).WithField("request", request). - Infof("could not serialize ledger key %v", ledgerKeyAndEntry.Key) - return GetLedgerEntriesResponse{}, &jrpc2.Error{ - Code: jrpc2.InternalError, - Message: fmt.Sprintf("could not serialize ledger key %v", ledgerKeyAndEntry.Key), + switch request.Format { + case FormatJSON: + keyJs, err := xdr2json.ConvertInterface(ledgerKeyAndEntry.Key) + if err != nil { + return GetLedgerEntriesResponse{}, &jrpc2.Error{ + Code: jrpc2.InternalError, + Message: err.Error(), + } + } + entryJs, err := xdr2json.ConvertInterface(ledgerKeyAndEntry.Entry.Data) + if err != nil { + return GetLedgerEntriesResponse{}, &jrpc2.Error{ + Code: jrpc2.InternalError, + Message: err.Error(), + } } - } - entryXDR, err := xdr.MarshalBase64(ledgerKeyAndEntry.Entry.Data) - if err != nil { - logger.WithError(err).WithField("request", request). - Infof("could not serialize ledger entry data for ledger entry %v", ledgerKeyAndEntry.Entry) - return GetLedgerEntriesResponse{}, &jrpc2.Error{ - Code: jrpc2.InternalError, - Message: fmt.Sprintf("could not serialize ledger entry data for ledger entry %v", ledgerKeyAndEntry.Entry), + ledgerEntryResults = append(ledgerEntryResults, LedgerEntryResult{ + KeyJSON: keyJs, + DataJSON: entryJs, + LastModifiedLedger: uint32(ledgerKeyAndEntry.Entry.LastModifiedLedgerSeq), + LiveUntilLedgerSeq: ledgerKeyAndEntry.LiveUntilLedgerSeq, + }) + + default: + keyXDR, err := xdr.MarshalBase64(ledgerKeyAndEntry.Key) + if err != nil { + return GetLedgerEntriesResponse{}, &jrpc2.Error{ + Code: jrpc2.InternalError, + Message: fmt.Sprintf("could not serialize ledger key %v", ledgerKeyAndEntry.Key), + } } - } - ledgerEntryResults = append(ledgerEntryResults, LedgerEntryResult{ - Key: keyXDR, - XDR: entryXDR, - LastModifiedLedger: uint32(ledgerKeyAndEntry.Entry.LastModifiedLedgerSeq), - LiveUntilLedgerSeq: ledgerKeyAndEntry.LiveUntilLedgerSeq, - }) + entryXDR, err := xdr.MarshalBase64(ledgerKeyAndEntry.Entry.Data) + if err != nil { + return GetLedgerEntriesResponse{}, &jrpc2.Error{ + Code: jrpc2.InternalError, + Message: fmt.Sprintf("could not serialize ledger entry data for ledger entry %v", ledgerKeyAndEntry.Entry), + } + } + + ledgerEntryResults = append(ledgerEntryResults, LedgerEntryResult{ + KeyXDR: keyXDR, + DataXDR: entryXDR, + LastModifiedLedger: uint32(ledgerKeyAndEntry.Entry.LastModifiedLedgerSeq), + LiveUntilLedgerSeq: ledgerKeyAndEntry.LiveUntilLedgerSeq, + }) + } } response := GetLedgerEntriesResponse{ diff --git a/cmd/soroban-rpc/internal/methods/get_ledger_entry.go b/cmd/soroban-rpc/internal/methods/get_ledger_entry.go index 5ffd763a..6146d48f 100644 --- a/cmd/soroban-rpc/internal/methods/get_ledger_entry.go +++ b/cmd/soroban-rpc/internal/methods/get_ledger_entry.go @@ -2,6 +2,7 @@ package methods import ( "context" + "encoding/json" "fmt" "github.com/creachadair/jrpc2" @@ -10,18 +11,22 @@ import ( "github.com/stellar/go/xdr" "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/db" + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/xdr2json" ) // Deprecated. Use GetLedgerEntriesRequest instead. // TODO(https://github.com/stellar/soroban-tools/issues/374) remove after getLedgerEntries is deployed. type GetLedgerEntryRequest struct { - Key string `json:"key"` + Key string `json:"key"` + Format string `json:"xdrFormat"` } // Deprecated. Use GetLedgerEntriesResponse instead. // TODO(https://github.com/stellar/soroban-tools/issues/374) remove after getLedgerEntries is deployed. type GetLedgerEntryResponse struct { - XDR string `json:"xdr"` + EntryXDR string `json:"xdr"` + EntryJSON json.RawMessage `json:"entryJson"` + LastModifiedLedger uint32 `json:"lastModifiedLedgerSeq"` LatestLedger uint32 `json:"latestLedger"` // The ledger sequence until the entry is live, available for entries that have associated ttl ledger entries. @@ -34,6 +39,13 @@ type GetLedgerEntryResponse struct { // TODO(https://github.com/stellar/soroban-tools/issues/374) remove after getLedgerEntries is deployed. func NewGetLedgerEntryHandler(logger *log.Entry, ledgerEntryReader db.LedgerEntryReader) jrpc2.Handler { return NewHandler(func(ctx context.Context, request GetLedgerEntryRequest) (GetLedgerEntryResponse, error) { + if err := IsValidFormat(request.Format); err != nil { + return GetLedgerEntryResponse{}, &jrpc2.Error{ + Code: jrpc2.InvalidParams, + Message: err.Error(), + } + } + var key xdr.LedgerKey if err := xdr.SafeUnmarshalBase64(request.Key, &key); err != nil { logger.WithError(err).WithField("request", request). @@ -92,13 +104,26 @@ func NewGetLedgerEntryHandler(logger *log.Entry, ledgerEntryReader db.LedgerEntr LatestLedger: latestLedger, LiveUntilLedgerSeq: liveUntilLedgerSeq, } - if response.XDR, err = xdr.MarshalBase64(ledgerEntry.Data); err != nil { + + switch request.Format { + case FormatJSON: + response.EntryJSON, err = xdr2json.ConvertInterface(ledgerEntry.Data) logger.WithError(err).WithField("request", request). - Info("could not serialize ledger entry data") + Info("could not JSONify ledger entry data") return GetLedgerEntryResponse{}, &jrpc2.Error{ Code: jrpc2.InternalError, Message: "could not serialize ledger entry data", } + + default: + if response.EntryXDR, err = xdr.MarshalBase64(ledgerEntry.Data); err != nil { + logger.WithError(err).WithField("request", request). + Info("could not serialize ledger entry data") + return GetLedgerEntryResponse{}, &jrpc2.Error{ + Code: jrpc2.InternalError, + Message: "could not serialize ledger entry data", + } + } } return response, nil diff --git a/cmd/soroban-rpc/internal/methods/get_transaction.go b/cmd/soroban-rpc/internal/methods/get_transaction.go index ac6ac8b2..556f23cd 100644 --- a/cmd/soroban-rpc/internal/methods/get_transaction.go +++ b/cmd/soroban-rpc/internal/methods/get_transaction.go @@ -4,6 +4,7 @@ import ( "context" "encoding/base64" "encoding/hex" + "encoding/json" "errors" "fmt" @@ -47,12 +48,15 @@ type GetTransactionResponse struct { ApplicationOrder int32 `json:"applicationOrder,omitempty"` // FeeBump indicates whether the transaction is a feebump transaction FeeBump bool `json:"feeBump,omitempty"` - // EnvelopeXdr is the TransactionEnvelope XDR value. - EnvelopeXdr string `json:"envelopeXdr,omitempty"` - // ResultXdr is the TransactionResult XDR value. - ResultXdr string `json:"resultXdr,omitempty"` - // ResultMetaXdr is the TransactionMeta XDR value. - ResultMetaXdr string `json:"resultMetaXdr,omitempty"` + // EnvelopeXDR is the TransactionEnvelope XDR value. + EnvelopeXDR string `json:"envelopeXdr,omitempty"` + EnvelopeJSON json.RawMessage `json:"envelopeJson,omitempty"` + // ResultXDR is the TransactionResult XDR value. + ResultXDR string `json:"resultXdr,omitempty"` + ResultJSON json.RawMessage `json:"resultJson,omitempty"` + // ResultMetaXDR is the TransactionMeta XDR value. + ResultMetaXDR string `json:"resultMetaXdr,omitempty"` + ResultMetaJSON json.RawMessage `json:"resultMetaJson,omitempty"` // Ledger is the sequence of the ledger which included the transaction. Ledger uint32 `json:"ledger,omitempty"` @@ -61,11 +65,13 @@ type GetTransactionResponse struct { // DiagnosticEventsXDR is present only if Status is equal to TransactionFailed. // DiagnosticEventsXDR is a base64-encoded slice of xdr.DiagnosticEvent - DiagnosticEventsXDR []string `json:"diagnosticEventsXdr,omitempty"` + DiagnosticEventsXDR []string `json:"diagnosticEventsXdr,omitempty"` + DiagnosticEventsJSON []json.RawMessage `json:"diagnosticEventsJson,omitempty"` } type GetTransactionRequest struct { - Hash string `json:"hash"` + Hash string `json:"hash"` + Format string `json:"xdrFormat,omitempty"` } func GetTransaction( @@ -75,6 +81,13 @@ func GetTransaction( ledgerReader db.LedgerReader, request GetTransactionRequest, ) (GetTransactionResponse, error) { + if err := IsValidFormat(request.Format); err != nil { + return GetTransactionResponse{}, &jrpc2.Error{ + Code: jrpc2.InvalidParams, + Message: err.Error(), + } + } + // parse hash if hex.DecodedLen(len(request.Hash)) != len(xdr.Hash{}) { return GetTransactionResponse{}, &jrpc2.Error{ @@ -126,10 +139,34 @@ func GetTransaction( response.Ledger = tx.Ledger.Sequence response.LedgerCloseTime = tx.Ledger.CloseTime - response.ResultXdr = base64.StdEncoding.EncodeToString(tx.Result) - response.EnvelopeXdr = base64.StdEncoding.EncodeToString(tx.Envelope) - response.ResultMetaXdr = base64.StdEncoding.EncodeToString(tx.Meta) - response.DiagnosticEventsXDR = base64EncodeSlice(tx.Events) + switch request.Format { + case FormatJSON: + result, envelope, meta, convErr := transactionToJSON(tx) + if convErr != nil { + return response, &jrpc2.Error{ + Code: jrpc2.InternalError, + Message: convErr.Error(), + } + } + diagEvents, convErr := jsonifySlice(xdr.DiagnosticEvent{}, tx.Events) + if convErr != nil { + return response, &jrpc2.Error{ + Code: jrpc2.InternalError, + Message: convErr.Error(), + } + } + + response.ResultJSON = result + response.EnvelopeJSON = envelope + response.ResultMetaJSON = meta + response.DiagnosticEventsJSON = diagEvents + + default: + response.ResultXDR = base64.StdEncoding.EncodeToString(tx.Result) + response.EnvelopeXDR = base64.StdEncoding.EncodeToString(tx.Envelope) + response.ResultMetaXDR = base64.StdEncoding.EncodeToString(tx.Meta) + response.DiagnosticEventsXDR = base64EncodeSlice(tx.Events) + } response.Status = TransactionStatusFailed if tx.Successful { @@ -139,6 +176,7 @@ func GetTransaction( } // NewGetTransactionHandler returns a get transaction json rpc handler + func NewGetTransactionHandler(logger *log.Entry, getter db.TransactionReader, ledgerReader db.LedgerReader, ) jrpc2.Handler { diff --git a/cmd/soroban-rpc/internal/methods/get_transaction_test.go b/cmd/soroban-rpc/internal/methods/get_transaction_test.go index 1bc6f2b9..1c308e58 100644 --- a/cmd/soroban-rpc/internal/methods/get_transaction_test.go +++ b/cmd/soroban-rpc/internal/methods/get_transaction_test.go @@ -3,6 +3,7 @@ package methods import ( "context" "encoding/hex" + "encoding/json" "testing" "github.com/sirupsen/logrus" @@ -13,6 +14,7 @@ import ( "github.com/stellar/go/xdr" "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/db" + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/xdr2json" ) func TestGetTransaction(t *testing.T) { @@ -24,14 +26,14 @@ func TestGetTransaction(t *testing.T) { ) log.SetLevel(logrus.DebugLevel) - _, err := GetTransaction(ctx, log, store, ledgerReader, GetTransactionRequest{"ab"}) + _, err := GetTransaction(ctx, log, store, ledgerReader, GetTransactionRequest{"ab", ""}) require.EqualError(t, err, "[-32602] unexpected hash length (2)") _, err = GetTransaction(ctx, log, store, ledgerReader, - GetTransactionRequest{"foo "}) + GetTransactionRequest{"foo ", ""}) require.EqualError(t, err, "[-32602] incorrect hash: encoding/hex: invalid byte: U+006F 'o'") hash := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - tx, err := GetTransaction(ctx, log, store, ledgerReader, GetTransactionRequest{hash}) + tx, err := GetTransaction(ctx, log, store, ledgerReader, GetTransactionRequest{hash, ""}) require.NoError(t, err) require.Equal(t, GetTransactionResponse{Status: TransactionStatusNotFound}, tx) @@ -40,7 +42,7 @@ func TestGetTransaction(t *testing.T) { xdrHash := txHash(1) hash = hex.EncodeToString(xdrHash[:]) - tx, err = GetTransaction(ctx, log, store, ledgerReader, GetTransactionRequest{hash}) + tx, err = GetTransaction(ctx, log, store, ledgerReader, GetTransactionRequest{hash, ""}) require.NoError(t, err) expectedTxResult, err := xdr.MarshalBase64(meta.V1.TxProcessing[0].Result.Result) @@ -57,9 +59,9 @@ func TestGetTransaction(t *testing.T) { OldestLedgerCloseTime: 2625, ApplicationOrder: 1, FeeBump: false, - EnvelopeXdr: expectedEnvelope, - ResultXdr: expectedTxResult, - ResultMetaXdr: expectedTxMeta, + EnvelopeXDR: expectedEnvelope, + ResultXDR: expectedTxResult, + ResultMetaXDR: expectedTxMeta, Ledger: 101, LedgerCloseTime: 2625, DiagnosticEventsXDR: []string{}, @@ -70,7 +72,7 @@ func TestGetTransaction(t *testing.T) { require.NoError(t, store.InsertTransactions(meta)) // the first transaction should still be there - tx, err = GetTransaction(ctx, log, store, ledgerReader, GetTransactionRequest{hash}) + tx, err = GetTransaction(ctx, log, store, ledgerReader, GetTransactionRequest{hash, ""}) require.NoError(t, err) require.Equal(t, GetTransactionResponse{ Status: TransactionStatusSuccess, @@ -80,9 +82,9 @@ func TestGetTransaction(t *testing.T) { OldestLedgerCloseTime: 2625, ApplicationOrder: 1, FeeBump: false, - EnvelopeXdr: expectedEnvelope, - ResultXdr: expectedTxResult, - ResultMetaXdr: expectedTxMeta, + EnvelopeXDR: expectedEnvelope, + ResultXDR: expectedTxResult, + ResultMetaXDR: expectedTxMeta, Ledger: 101, LedgerCloseTime: 2625, DiagnosticEventsXDR: []string{}, @@ -99,7 +101,7 @@ func TestGetTransaction(t *testing.T) { expectedTxMeta, err = xdr.MarshalBase64(meta.V1.TxProcessing[0].TxApplyProcessing) require.NoError(t, err) - tx, err = GetTransaction(ctx, log, store, ledgerReader, GetTransactionRequest{hash}) + tx, err = GetTransaction(ctx, log, store, ledgerReader, GetTransactionRequest{hash, ""}) require.NoError(t, err) require.Equal(t, GetTransactionResponse{ Status: TransactionStatusFailed, @@ -109,9 +111,9 @@ func TestGetTransaction(t *testing.T) { OldestLedgerCloseTime: 2625, ApplicationOrder: 1, FeeBump: false, - EnvelopeXdr: expectedEnvelope, - ResultXdr: expectedTxResult, - ResultMetaXdr: expectedTxMeta, + EnvelopeXDR: expectedEnvelope, + ResultXDR: expectedTxResult, + ResultMetaXDR: expectedTxMeta, Ledger: 102, LedgerCloseTime: 2650, DiagnosticEventsXDR: []string{}, @@ -136,7 +138,7 @@ func TestGetTransaction(t *testing.T) { expectedEventsMeta, err := xdr.MarshalBase64(diagnosticEvents[0]) require.NoError(t, err) - tx, err = GetTransaction(ctx, log, store, ledgerReader, GetTransactionRequest{hash}) + tx, err = GetTransaction(ctx, log, store, ledgerReader, GetTransactionRequest{hash, ""}) require.NoError(t, err) require.Equal(t, GetTransactionResponse{ Status: TransactionStatusSuccess, @@ -146,9 +148,9 @@ func TestGetTransaction(t *testing.T) { OldestLedgerCloseTime: 2625, ApplicationOrder: 1, FeeBump: false, - EnvelopeXdr: expectedEnvelope, - ResultXdr: expectedTxResult, - ResultMetaXdr: expectedTxMeta, + EnvelopeXDR: expectedEnvelope, + ResultXDR: expectedTxResult, + ResultMetaXDR: expectedTxMeta, Ledger: 103, LedgerCloseTime: 2675, DiagnosticEventsXDR: []string{expectedEventsMeta}, @@ -290,3 +292,109 @@ func txMetaWithEvents(acctSeq uint32, successful bool) xdr.LedgerCloseMeta { return meta } + +func TestGetTransaction_JSONFormat(t *testing.T) { + mockDBReader := db.NewMockTransactionStore(NetworkPassphrase) + mockLedgerReader := db.NewMockLedgerReader(mockDBReader) + var lookupHash string + var lookupEnv xdr.TransactionEnvelope + for i := 1; i <= 3; i++ { + meta := createTestLedger(uint32(i)) + err := mockDBReader.InsertTransactions(meta) + require.NoError(t, err) + + if lookupHash == "" { + lookupEnv = meta.TransactionEnvelopes()[0] + rawHash, hashErr := network.HashTransactionInEnvelope(lookupEnv, "passphrase") + require.NoError(t, hashErr) + lookupHash = hex.EncodeToString(rawHash[:]) + } + } + + request := GetTransactionRequest{ + Format: FormatJSON, + Hash: lookupHash, + } + + txResp, err := GetTransaction(context.TODO(), nil, mockDBReader, mockLedgerReader, request) + require.NoError(t, err) + + // Do a marshaling round-trip on a transaction so we can check that the + // fields are encoded correctly as JSON. + jsBytes, err := json.Marshal(txResp) + require.NoError(t, err) + + var tx map[string]interface{} + require.NoError(t, json.Unmarshal(jsBytes, &tx)) + + require.Nilf(t, tx["envelopeXdr"], "field: 'envelopeXdr'") + require.NotNilf(t, tx["envelopeJson"], "field: 'envelopeJson'") + require.Nilf(t, tx["resultXdr"], "field: 'resultXdr'") + require.NotNilf(t, tx["resultJson"], "field: 'resultJson'") + require.Nilf(t, tx["resultMetaXdr"], "field: 'resultMetaXdr'") + require.NotNilf(t, tx["resultMetaJson"], "field: 'resultMetaJson'") + + // Do a deep validation on the format + + envJs, err := xdr2json.ConvertInterface(lookupEnv) + require.NoError(t, err) + + var envelope map[string]interface{} + require.NoError(t, json.Unmarshal(envJs, &envelope)) + require.Equal(t, envelope, tx["envelopeJson"]) +} + +func BenchmarkJSONTransactions(b *testing.B) { + mockDBReader := db.NewMockTransactionStore(NetworkPassphrase) + mockLedgerReader := db.NewMockLedgerReader(mockDBReader) + + var lookupHash string + var lookupEnv xdr.TransactionEnvelope + for i := range 10_000 { + meta := createTestLedger(uint32(i)) + err := mockDBReader.InsertTransactions(meta) + require.NoError(b, err) + + if lookupHash == "" { + lookupEnv = meta.TransactionEnvelopes()[0] + rawHash, hashErr := network.HashTransactionInEnvelope(lookupEnv, "passphrase") + require.NoError(b, hashErr) + lookupHash = hex.EncodeToString(rawHash[:]) + } + } + + b.ResetTimer() + b.Run("JSON format", func(bb *testing.B) { + request := GetTransactionRequest{ + Format: FormatJSON, + Hash: lookupHash, + } + bb.ResetTimer() + + for range bb.N { + _, err := GetTransaction( + context.TODO(), + nil, + mockDBReader, + mockLedgerReader, + request) + require.NoError(bb, err) + } + }) + + b.ResetTimer() + b.Run("XDR format", func(bb *testing.B) { + request := GetTransactionRequest{Hash: lookupHash} + bb.ResetTimer() + + for range bb.N { + _, err := GetTransaction( + context.TODO(), + nil, + mockDBReader, + mockLedgerReader, + request) + require.NoError(bb, err) + } + }) +} diff --git a/cmd/soroban-rpc/internal/methods/get_transactions.go b/cmd/soroban-rpc/internal/methods/get_transactions.go index 13ff359f..d32fb61d 100644 --- a/cmd/soroban-rpc/internal/methods/get_transactions.go +++ b/cmd/soroban-rpc/internal/methods/get_transactions.go @@ -3,6 +3,7 @@ package methods import ( "context" "encoding/base64" + "encoding/json" "errors" "fmt" "io" @@ -30,6 +31,7 @@ type TransactionsPaginationOptions struct { type GetTransactionsRequest struct { StartLedger uint32 `json:"startLedger"` Pagination *TransactionsPaginationOptions `json:"pagination,omitempty"` + Format string `json:"xdrFormat,omitempty"` } // isValid checks the validity of the request parameters. @@ -50,7 +52,7 @@ func (req GetTransactionsRequest) isValid(maxLimit uint, ledgerRange ledgerbucke return fmt.Errorf("limit must not exceed %d", maxLimit) } - return nil + return IsValidFormat(req.Format) } type TransactionInfo struct { @@ -61,15 +63,19 @@ type TransactionInfo struct { ApplicationOrder int32 `json:"applicationOrder"` // FeeBump indicates whether the transaction is a feebump transaction FeeBump bool `json:"feeBump"` - // EnvelopeXdr is the TransactionEnvelope XDR value. - EnvelopeXdr string `json:"envelopeXdr"` - // ResultXdr is the TransactionResult XDR value. - ResultXdr string `json:"resultXdr"` - // ResultMetaXdr is the TransactionMeta XDR value. - ResultMetaXdr string `json:"resultMetaXdr"` + // EnvelopeXDR is the TransactionEnvelope XDR value. + EnvelopeXDR string `json:"envelopeXdr,omitempty"` + EnvelopeJSON json.RawMessage `json:"envelopeJson,omitempty"` + // ResultXDR is the TransactionResult XDR value. + ResultXDR string `json:"resultXdr,omitempty"` + ResultJSON json.RawMessage `json:"resultJson,omitempty"` + // ResultMetaXDR is the TransactionMeta XDR value. + ResultMetaXDR string `json:"resultMetaXdr,omitempty"` + ResultMetaJSON json.RawMessage `json:"resultMetaJson,omitempty"` // DiagnosticEventsXDR is present only if transaction was not successful. // DiagnosticEventsXDR is a base64-encoded slice of xdr.DiagnosticEvent - DiagnosticEventsXDR []string `json:"diagnosticEventsXdr,omitempty"` + DiagnosticEventsXDR []string `json:"diagnosticEventsXdr,omitempty"` + DiagnosticEventsJSON []json.RawMessage `json:"diagnosticEventsJson,omitempty"` // Ledger is the sequence of the ledger which included the transaction. Ledger uint32 `json:"ledger"` // LedgerCloseTime is the unix timestamp of when the transaction was included in the ledger. @@ -138,8 +144,10 @@ func (h transactionsRPCHandler) fetchLedgerData(ctx context.Context, ledgerSeq u // processTransactionsInLedger cycles through all the transactions in a ledger, extracts the transaction info // and builds the list of transactions. -func (h transactionsRPCHandler) processTransactionsInLedger(ledger xdr.LedgerCloseMeta, start toid.ID, +func (h transactionsRPCHandler) processTransactionsInLedger( + ledger xdr.LedgerCloseMeta, start toid.ID, txns *[]TransactionInfo, limit uint, + format string, ) (*toid.ID, bool, error) { reader, err := ingest.NewLedgerTransactionReaderFromLedgerCloseMeta(h.networkPassphrase, ledger) if err != nil { @@ -186,15 +194,42 @@ func (h transactionsRPCHandler) processTransactionsInLedger(ledger xdr.LedgerClo } txInfo := TransactionInfo{ - ApplicationOrder: tx.ApplicationOrder, - FeeBump: tx.FeeBump, - ResultXdr: base64.StdEncoding.EncodeToString(tx.Result), - ResultMetaXdr: base64.StdEncoding.EncodeToString(tx.Meta), - EnvelopeXdr: base64.StdEncoding.EncodeToString(tx.Envelope), - DiagnosticEventsXDR: base64EncodeSlice(tx.Events), - Ledger: tx.Ledger.Sequence, - LedgerCloseTime: tx.Ledger.CloseTime, + ApplicationOrder: tx.ApplicationOrder, + FeeBump: tx.FeeBump, + Ledger: tx.Ledger.Sequence, + LedgerCloseTime: tx.Ledger.CloseTime, } + + switch format { + case FormatJSON: + result, envelope, meta, convErr := transactionToJSON(tx) + if convErr != nil { + return nil, false, &jrpc2.Error{ + Code: jrpc2.InternalError, + Message: convErr.Error(), + } + } + + diagEvents, convErr := jsonifySlice(xdr.DiagnosticEvent{}, tx.Events) + if convErr != nil { + return nil, false, &jrpc2.Error{ + Code: jrpc2.InternalError, + Message: convErr.Error(), + } + } + + txInfo.ResultJSON = result + txInfo.ResultMetaJSON = envelope + txInfo.EnvelopeJSON = meta + txInfo.DiagnosticEventsJSON = diagEvents + + default: + txInfo.ResultXDR = base64.StdEncoding.EncodeToString(tx.Result) + txInfo.ResultMetaXDR = base64.StdEncoding.EncodeToString(tx.Meta) + txInfo.EnvelopeXDR = base64.StdEncoding.EncodeToString(tx.Envelope) + txInfo.DiagnosticEventsXDR = base64EncodeSlice(tx.Events) + } + txInfo.Status = TransactionStatusFailed if tx.Successful { txInfo.Status = TransactionStatusSuccess @@ -246,7 +281,7 @@ func (h transactionsRPCHandler) getTransactionsByLedgerSequence(ctx context.Cont return GetTransactionsResponse{}, err } - cursor, done, err = h.processTransactionsInLedger(ledger, start, &txns, limit) + cursor, done, err = h.processTransactionsInLedger(ledger, start, &txns, limit, request.Format) if err != nil { return GetTransactionsResponse{}, err } diff --git a/cmd/soroban-rpc/internal/methods/get_transactions_test.go b/cmd/soroban-rpc/internal/methods/get_transactions_test.go index ef695ffb..86304147 100644 --- a/cmd/soroban-rpc/internal/methods/get_transactions_test.go +++ b/cmd/soroban-rpc/internal/methods/get_transactions_test.go @@ -2,6 +2,7 @@ package methods import ( "context" + "encoding/json" "fmt" "testing" @@ -297,3 +298,44 @@ func TestGetTransactions_InvalidCursorString(t *testing.T) { expectedErr := fmt.Errorf("[%d] strconv.ParseInt: parsing \"abc\": invalid syntax", jrpc2.InvalidParams) assert.Equal(t, expectedErr.Error(), err.Error()) } + +func TestGetTransactions_JSONFormat(t *testing.T) { + mockDBReader := db.NewMockTransactionStore(NetworkPassphrase) + mockLedgerReader := db.NewMockLedgerReader(mockDBReader) + for i := 1; i <= 3; i++ { + meta := createTestLedger(uint32(i)) + err := mockDBReader.InsertTransactions(meta) + require.NoError(t, err) + } + + handler := transactionsRPCHandler{ + ledgerReader: mockLedgerReader, + maxLimit: 100, + defaultLimit: 10, + networkPassphrase: NetworkPassphrase, + } + + request := GetTransactionsRequest{ + Format: FormatJSON, + StartLedger: 1, + } + + js, err := handler.getTransactionsByLedgerSequence(context.TODO(), request) + require.NoError(t, err) + + // Do a marshaling round-trip on a transaction so we can check that the + // fields are encoded correctly as JSON. + txResp := js.Transactions[0] + jsBytes, err := json.Marshal(txResp) + require.NoError(t, err) + + var tx map[string]interface{} + require.NoError(t, json.Unmarshal(jsBytes, &tx)) + + require.Nilf(t, tx["envelopeXdr"], "field: 'envelopeXdr'") + require.NotNilf(t, tx["envelopeJson"], "field: 'envelopeJson'") + require.Nilf(t, tx["resultXdr"], "field: 'resultXdr'") + require.NotNilf(t, tx["resultJson"], "field: 'resultJson'") + require.Nilf(t, tx["resultMetaXdr"], "field: 'resultMetaXdr'") + require.NotNilf(t, tx["resultMetaJson"], "field: 'resultMetaJson'") +} diff --git a/cmd/soroban-rpc/internal/methods/get_version_info.go b/cmd/soroban-rpc/internal/methods/get_version_info.go index 1d868ef2..2af129de 100644 --- a/cmd/soroban-rpc/internal/methods/get_version_info.go +++ b/cmd/soroban-rpc/internal/methods/get_version_info.go @@ -15,7 +15,7 @@ import ( type GetVersionInfoResponse struct { Version string `json:"version"` - // TODO: to be fixed by https://github.com/stellar/soroban-rpc/pull/164 + // TODO: casing to be fixed by https://github.com/stellar/soroban-rpc/pull/164 CommitHash string `json:"commit_hash"` //nolint:tagliatelle BuildTimestamp string `json:"build_time_stamp"` //nolint:tagliatelle CaptiveCoreVersion string `json:"captive_core_version"` //nolint:tagliatelle diff --git a/cmd/soroban-rpc/internal/methods/json.go b/cmd/soroban-rpc/internal/methods/json.go new file mode 100644 index 00000000..739a673a --- /dev/null +++ b/cmd/soroban-rpc/internal/methods/json.go @@ -0,0 +1,60 @@ +package methods + +import ( + "fmt" + "strings" + + "github.com/pkg/errors" + + "github.com/stellar/go/xdr" + + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/db" + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/xdr2json" +) + +const ( + FormatBase64 = "base64" + FormatJSON = "json" +) + +var errInvalidFormat = fmt.Errorf( + "expected %s for optional 'xdrFormat'", + strings.Join([]string{FormatBase64, FormatJSON}, ", ")) + +func IsValidFormat(format string) error { + switch format { + case "": + case FormatJSON: + case FormatBase64: + default: + return errors.Wrapf(errInvalidFormat, "got '%s'", format) + } + return nil +} + +func transactionToJSON(tx db.Transaction) ( + []byte, + []byte, + []byte, + error, +) { + var err error + var result, resultMeta, envelope []byte + + result, err = xdr2json.ConvertBytes(xdr.TransactionResult{}, tx.Result) + if err != nil { + return result, envelope, resultMeta, err + } + + envelope, err = xdr2json.ConvertBytes(xdr.TransactionEnvelope{}, tx.Envelope) + if err != nil { + return result, envelope, resultMeta, err + } + + resultMeta, err = xdr2json.ConvertBytes(xdr.TransactionMeta{}, tx.Meta) + if err != nil { + return result, envelope, resultMeta, err + } + + return result, envelope, resultMeta, nil +} diff --git a/cmd/soroban-rpc/internal/methods/send_transaction.go b/cmd/soroban-rpc/internal/methods/send_transaction.go index 9cdd6a20..a73a0140 100644 --- a/cmd/soroban-rpc/internal/methods/send_transaction.go +++ b/cmd/soroban-rpc/internal/methods/send_transaction.go @@ -3,8 +3,10 @@ package methods import ( "context" "encoding/hex" + "encoding/json" "github.com/creachadair/jrpc2" + "github.com/pkg/errors" "github.com/stellar/go/network" proto "github.com/stellar/go/protocols/stellarcore" @@ -13,6 +15,7 @@ import ( "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/daemon/interfaces" "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/db" + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/xdr2json" ) // SendTransactionResponse represents the transaction submission response returned Soroban-RPC @@ -20,10 +23,14 @@ type SendTransactionResponse struct { // ErrorResultXDR is present only if Status is equal to proto.TXStatusError. // ErrorResultXDR is a TransactionResult xdr string which contains details on why // the transaction could not be accepted by stellar-core. - ErrorResultXDR string `json:"errorResultXdr,omitempty"` + ErrorResultXDR string `json:"errorResultXdr,omitempty"` + ErrorResultJSON json.RawMessage `json:"errorResultJson,omitempty"` + // DiagnosticEventsXDR is present only if Status is equal to proto.TXStatusError. // DiagnosticEventsXDR is a base64-encoded slice of xdr.DiagnosticEvent - DiagnosticEventsXDR []string `json:"diagnosticEventsXdr,omitempty"` + DiagnosticEventsXDR []string `json:"diagnosticEventsXdr,omitempty"` + DiagnosticEventsJSON []json.RawMessage `json:"diagnosticEventsJson,omitempty"` + // Status represents the status of the transaction submission returned by stellar-core. // Status can be one of: proto.TXStatusPending, proto.TXStatusDuplicate, // proto.TXStatusTryAgainLater, or proto.TXStatusError. @@ -43,6 +50,7 @@ type SendTransactionResponse struct { type SendTransactionRequest struct { // Transaction is the base64 encoded transaction envelope. Transaction string `json:"transaction"` + Format string `json:"xdrFormat,omitempty"` } // NewSendTransactionHandler returns a submit transaction json rpc handler @@ -54,6 +62,13 @@ func NewSendTransactionHandler( ) jrpc2.Handler { submitter := daemon.CoreClient() return NewHandler(func(ctx context.Context, request SendTransactionRequest) (SendTransactionResponse, error) { + if err := IsValidFormat(request.Format); err != nil { + return SendTransactionResponse{}, &jrpc2.Error{ + Code: jrpc2.InvalidParams, + Message: err.Error(), + } + } + var envelope xdr.TransactionEnvelope err := xdr.SafeUnmarshalBase64(request.Transaction, &envelope) if err != nil { @@ -104,22 +119,80 @@ func NewSendTransactionHandler( switch resp.Status { case proto.TXStatusError: - events, err := proto.DiagnosticEventsToSlice(resp.DiagnosticEvents) - if err != nil { - logger.WithField("tx", request.Transaction).Error("Cannot decode diagnostic events:", err) - return SendTransactionResponse{}, &jrpc2.Error{ - Code: jrpc2.InternalError, - Message: "could not decode diagnostic events", - } - } - return SendTransactionResponse{ - ErrorResultXDR: resp.Error, - DiagnosticEventsXDR: events, + errorResp := SendTransactionResponse{ Status: resp.Status, Hash: txHash, LatestLedger: latestLedgerInfo.Sequence, LatestLedgerCloseTime: latestLedgerInfo.CloseTime, - }, nil + } + + switch request.Format { + case FormatJSON: + errResult := xdr.TransactionResult{} + err = xdr.SafeUnmarshalBase64(resp.Error, &errResult) + if err != nil { + logger.WithField("tx", request.Transaction). + WithError(err).Error("Cannot decode error result") + + return SendTransactionResponse{}, &jrpc2.Error{ + Code: jrpc2.InternalError, + Message: errors.Wrap(err, "couldn't decode error").Error(), + } + } + + errorResp.ErrorResultJSON, err = xdr2json.ConvertInterface(errResult) + if err != nil { + logger.WithField("tx", request.Transaction). + WithError(err).Error("Cannot JSONify error result") + + return SendTransactionResponse{}, &jrpc2.Error{ + Code: jrpc2.InternalError, + Message: errors.Wrap(err, "couldn't serialize error").Error(), + } + } + + diagEvents := xdr.DiagnosticEvents{} + err = xdr.SafeUnmarshalBase64(resp.DiagnosticEvents, &diagEvents) + if err != nil { + logger.WithField("tx", request.Transaction). + WithError(err).Error("Cannot decode events") + + return SendTransactionResponse{}, &jrpc2.Error{ + Code: jrpc2.InternalError, + Message: errors.Wrap(err, "couldn't decode events").Error(), + } + } + + errorResp.DiagnosticEventsJSON = make([]json.RawMessage, len(diagEvents)) + for i, event := range diagEvents { + errorResp.DiagnosticEventsJSON[i], err = xdr2json.ConvertInterface(event) + if err != nil { + logger.WithField("tx", request.Transaction). + WithError(err).Errorf("Cannot decode event %d: %+v", i+1, event) + + return SendTransactionResponse{}, &jrpc2.Error{ + Code: jrpc2.InternalError, + Message: errors.Wrapf(err, "couldn't decode event #%d", i+1).Error(), + } + } + } + + default: + events, err := proto.DiagnosticEventsToSlice(resp.DiagnosticEvents) + if err != nil { + logger.WithField("tx", request.Transaction).Error("Cannot decode diagnostic events:", err) + return SendTransactionResponse{}, &jrpc2.Error{ + Code: jrpc2.InternalError, + Message: "could not decode diagnostic events", + } + } + + errorResp.ErrorResultXDR = resp.Error + errorResp.DiagnosticEventsXDR = events + } + + return errorResp, nil + case proto.TXStatusPending, proto.TXStatusDuplicate, proto.TXStatusTryAgainLater: return SendTransactionResponse{ Status: resp.Status, @@ -127,6 +200,7 @@ func NewSendTransactionHandler( LatestLedger: latestLedgerInfo.Sequence, LatestLedgerCloseTime: latestLedgerInfo.CloseTime, }, nil + default: logger.WithField("status", resp.Status). WithField("tx", request.Transaction).Error("Unrecognized stellar-core status response") diff --git a/cmd/soroban-rpc/internal/methods/simulate_transaction.go b/cmd/soroban-rpc/internal/methods/simulate_transaction.go index dfe5b9d2..2a26b08a 100644 --- a/cmd/soroban-rpc/internal/methods/simulate_transaction.go +++ b/cmd/soroban-rpc/internal/methods/simulate_transaction.go @@ -16,11 +16,13 @@ import ( "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/daemon/interfaces" "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/db" "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/preflight" + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/xdr2json" ) type SimulateTransactionRequest struct { Transaction string `json:"transaction"` ResourceConfig *preflight.ResourceConfig `json:"resourceConfig,omitempty"` + Format string `json:"xdrFormat,omitempty"` } type SimulateTransactionCost struct { @@ -30,13 +32,19 @@ type SimulateTransactionCost struct { // SimulateHostFunctionResult contains the simulation result of each HostFunction within the single InvokeHostFunctionOp allowed in a Transaction type SimulateHostFunctionResult struct { - Auth []string `json:"auth"` - XDR string `json:"xdr"` + AuthXDR []string `json:"auth,omitempty"` + AuthJSON []json.RawMessage `json:"authJson,omitempty"` + + ReturnValueXDR string `json:"xdr,omitempty"` + ReturnValueJSON json.RawMessage `json:"returnValueJson,omitempty"` } type RestorePreamble struct { - TransactionData string `json:"transactionData"` // SorobanTransactionData XDR in base64 - MinResourceFee int64 `json:"minResourceFee,string"` + // TransactionDataXDR is an xdr.SorobanTransactionData in base64 + TransactionDataXDR string `json:"transactionData,omitempty"` + TransactionDataJSON json.RawMessage `json:"transactionDataJson,omitempty"` + + MinResourceFee int64 `json:"minResourceFee,string"` } type LedgerEntryChangeType int @@ -85,13 +93,18 @@ func (l *LedgerEntryChangeType) UnmarshalJSON(data []byte) error { return l.Parse(s) } -func (l *LedgerEntryChange) FromXDRDiff(diff preflight.XDRDiff) error { - beforePresent := len(diff.Before) > 0 - afterPresent := len(diff.After) > 0 +func (l *LedgerEntryChange) FromXDRDiff(diff preflight.XDRDiff, format string) error { + if err := IsValidFormat(format); err != nil { + return err + } + var ( entryXDR []byte changeType LedgerEntryChangeType ) + + beforePresent := len(diff.Before) > 0 + afterPresent := len(diff.After) > 0 switch { case beforePresent: entryXDR = diff.Before @@ -100,52 +113,107 @@ func (l *LedgerEntryChange) FromXDRDiff(diff preflight.XDRDiff) error { } else { changeType = LedgerEntryChangeTypeDeleted } + case afterPresent: entryXDR = diff.After changeType = LedgerEntryChangeTypeCreated + default: return errors.New("missing before and after") } - var entry xdr.LedgerEntry + l.Type = changeType + + // We need to unmarshal the ledger entry for both b64 and json cases + // because we need the inner ledger key. + var entry xdr.LedgerEntry if err := xdr.SafeUnmarshal(entryXDR, &entry); err != nil { return err } + key, err := entry.LedgerKey() if err != nil { return err } - keyB64, err := xdr.MarshalBase64(key) + + switch format { + case FormatJSON: + return l.jsonXdrDiff(diff, key) + + default: + keyB64, err := xdr.MarshalBase64(key) + if err != nil { + return err + } + + l.KeyXDR = keyB64 + + if beforePresent { + before := base64.StdEncoding.EncodeToString(diff.Before) + l.BeforeXDR = &before + } + + if afterPresent { + after := base64.StdEncoding.EncodeToString(diff.After) + l.AfterXDR = &after + } + } + + return nil +} + +func (l *LedgerEntryChange) jsonXdrDiff(diff preflight.XDRDiff, key xdr.LedgerKey) error { + var err error + beforePresent := len(diff.Before) > 0 + afterPresent := len(diff.After) > 0 + + l.KeyJSON, err = xdr2json.ConvertInterface(key) if err != nil { return err } - l.Type = changeType - l.Key = keyB64 + if beforePresent { - before := base64.StdEncoding.EncodeToString(diff.Before) - l.Before = &before + l.BeforeJSON, err = xdr2json.ConvertBytes(xdr.LedgerEntry{}, diff.Before) + if err != nil { + return err + } } + if afterPresent { - after := base64.StdEncoding.EncodeToString(diff.After) - l.After = &after + l.BeforeJSON, err = xdr2json.ConvertBytes(xdr.LedgerEntry{}, diff.After) + if err != nil { + return err + } } + return nil } // LedgerEntryChange designates a change in a ledger entry. Before and After cannot be omitted at the same time. // If Before is omitted, it constitutes a creation, if After is omitted, it constitutes a delation. type LedgerEntryChange struct { - Type LedgerEntryChangeType `json:"type"` - Key string `json:"key"` // LedgerEntryKey in base64 - Before *string `json:"before"` // LedgerEntry XDR in base64 - After *string `json:"after"` // LedgerEntry XDR in base64 + Type LedgerEntryChangeType `json:"type"` + + KeyXDR string `json:"key,omitempty"` // LedgerEntryKey in base64 + KeyJSON json.RawMessage `json:"keyJson,omitempty"` + + BeforeXDR *string `json:"before"` // LedgerEntry XDR in base64 + BeforeJSON json.RawMessage `json:"beforeJson,omitempty"` + + AfterXDR *string `json:"after"` // LedgerEntry XDR in base64 + AfterJSON json.RawMessage `json:"afterJson,omitempty"` } type SimulateTransactionResponse struct { - Error string `json:"error,omitempty"` - TransactionData string `json:"transactionData,omitempty"` // SorobanTransactionData XDR in base64 + Error string `json:"error,omitempty"` + + TransactionDataXDR string `json:"transactionData,omitempty"` // SorobanTransactionData XDR in base64 + TransactionDataJSON json.RawMessage `json:"transactionDataJson,omitempty"` + + EventsXDR []string `json:"events,omitempty"` // DiagnosticEvent XDR in base64 + EventsJSON []json.RawMessage `json:"eventsJson,omitempty"` + MinResourceFee int64 `json:"minResourceFee,string,omitempty"` - Events []string `json:"events,omitempty"` // DiagnosticEvent XDR in base64 Results []SimulateHostFunctionResult `json:"results,omitempty"` // an array of the individual host function call results Cost SimulateTransactionCost `json:"cost,omitempty"` // the effective cpu and memory cost of the invoked transaction execution. RestorePreamble *RestorePreamble `json:"restorePreamble,omitempty"` // If present, it indicates that a prior RestoreFootprint is required @@ -160,6 +228,10 @@ type PreflightGetter interface { // NewSimulateTransactionHandler returns a json rpc handler to run preflight simulations func NewSimulateTransactionHandler(logger *log.Entry, ledgerEntryReader db.LedgerEntryReader, ledgerReader db.LedgerReader, daemon interfaces.Daemon, getter PreflightGetter) jrpc2.Handler { return NewHandler(func(ctx context.Context, request SimulateTransactionRequest) SimulateTransactionResponse { + if err := IsValidFormat(request.Format); err != nil { + return SimulateTransactionResponse{Error: err.Error()} + } + var txEnvelope xdr.TransactionEnvelope if err := xdr.SafeUnmarshalBase64(request.Transaction, &txEnvelope); err != nil { logger.WithError(err).WithField("request", request). @@ -243,30 +315,78 @@ func NewSimulateTransactionHandler(logger *log.Entry, ledgerEntryReader db.Ledge var results []SimulateHostFunctionResult if len(result.Result) != 0 { - results = append(results, SimulateHostFunctionResult{ - XDR: base64.StdEncoding.EncodeToString(result.Result), - Auth: base64EncodeSlice(result.Auth), - }) + switch request.Format { + case FormatJSON: + rvJs, err := xdr2json.ConvertBytes(xdr.ScVal{}, result.Result) + if err != nil { + return SimulateTransactionResponse{ + Error: err.Error(), + LatestLedger: latestLedger, + } + } + + auths, err := jsonifySlice(xdr.SorobanAuthorizationEntry{}, result.Auth) + if err != nil { + return SimulateTransactionResponse{ + Error: err.Error(), + LatestLedger: latestLedger, + } + } + + results = append(results, SimulateHostFunctionResult{ + ReturnValueJSON: rvJs, + AuthJSON: auths, + }) + + default: + results = append(results, SimulateHostFunctionResult{ + ReturnValueXDR: base64.StdEncoding.EncodeToString(result.Result), + AuthXDR: base64EncodeSlice(result.Auth), + }) + } } + var restorePreamble *RestorePreamble = nil if len(result.PreRestoreTransactionData) != 0 { - restorePreamble = &RestorePreamble{ - TransactionData: base64.StdEncoding.EncodeToString(result.PreRestoreTransactionData), - MinResourceFee: result.PreRestoreMinFee, + switch request.Format { + case FormatJSON: + txDataJs, err := xdr2json.ConvertBytes( + xdr.SorobanTransactionData{}, + result.PreRestoreTransactionData) + if err != nil { + return SimulateTransactionResponse{ + Error: err.Error(), + LatestLedger: latestLedger, + } + } + + restorePreamble = &RestorePreamble{ + TransactionDataJSON: txDataJs, + MinResourceFee: result.PreRestoreMinFee, + } + + default: + restorePreamble = &RestorePreamble{ + TransactionDataXDR: base64.StdEncoding.EncodeToString(result.PreRestoreTransactionData), + MinResourceFee: result.PreRestoreMinFee, + } } } stateChanges := make([]LedgerEntryChange, len(result.LedgerEntryDiff)) for i := 0; i < len(stateChanges); i++ { - stateChanges[i].FromXDRDiff(result.LedgerEntryDiff[i]) + if err := stateChanges[i].FromXDRDiff(result.LedgerEntryDiff[i], request.Format); err != nil { + return SimulateTransactionResponse{ + Error: err.Error(), + LatestLedger: latestLedger, + } + } } - return SimulateTransactionResponse{ - Error: result.Error, - Results: results, - Events: base64EncodeSlice(result.Events), - TransactionData: base64.StdEncoding.EncodeToString(result.TransactionData), - MinResourceFee: result.MinFee, + simResp := SimulateTransactionResponse{ + Error: result.Error, + Results: results, + MinResourceFee: result.MinFee, Cost: SimulateTransactionCost{ CPUInstructions: result.CPUInstructions, MemoryBytes: result.MemoryBytes, @@ -275,6 +395,33 @@ func NewSimulateTransactionHandler(logger *log.Entry, ledgerEntryReader db.Ledge RestorePreamble: restorePreamble, StateChanges: stateChanges, } + + switch request.Format { + case FormatJSON: + simResp.TransactionDataJSON, err = xdr2json.ConvertBytes( + xdr.SorobanTransactionData{}, + result.TransactionData) + if err != nil { + return SimulateTransactionResponse{ + Error: err.Error(), + LatestLedger: latestLedger, + } + } + + simResp.EventsJSON, err = jsonifySlice(xdr.DiagnosticEvent{}, result.Events) + if err != nil { + return SimulateTransactionResponse{ + Error: err.Error(), + LatestLedger: latestLedger, + } + } + + default: + simResp.EventsXDR = base64EncodeSlice(result.Events) + simResp.TransactionDataXDR = base64.StdEncoding.EncodeToString(result.TransactionData) + } + + return simResp }) } @@ -286,6 +433,20 @@ func base64EncodeSlice(in [][]byte) []string { return result } +func jsonifySlice(xdr interface{}, values [][]byte) ([]json.RawMessage, error) { + result := make([]json.RawMessage, len(values)) + var err error + + for i, value := range values { + result[i], err = xdr2json.ConvertBytes(xdr, value) + if err != nil { + return result, err + } + } + + return result, nil +} + func getBucketListSizeAndProtocolVersion( ctx context.Context, ledgerReader db.LedgerReader, diff --git a/cmd/soroban-rpc/internal/methods/simulate_transaction_test.go b/cmd/soroban-rpc/internal/methods/simulate_transaction_test.go index bde1e843..ac60b07f 100644 --- a/cmd/soroban-rpc/internal/methods/simulate_transaction_test.go +++ b/cmd/soroban-rpc/internal/methods/simulate_transaction_test.go @@ -5,12 +5,12 @@ import ( "encoding/json" "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stellar/go/xdr" "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/preflight" + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/xdr2json" ) func TestLedgerEntryChange(t *testing.T) { @@ -36,6 +36,11 @@ func TestLedgerEntryChange(t *testing.T) { require.NoError(t, err) keyB64 := base64.StdEncoding.EncodeToString(keyXDR) + keyJs, err := xdr2json.ConvertInterface(key) + require.NoError(t, err) + entryJs, err := xdr2json.ConvertInterface(entry) + require.NoError(t, err) + for _, test := range []struct { name string input preflight.XDRDiff @@ -48,10 +53,10 @@ func TestLedgerEntryChange(t *testing.T) { After: entryXDR, }, expectedOutput: LedgerEntryChange{ - Type: LedgerEntryChangeTypeCreated, - Key: keyB64, - Before: nil, - After: &entryB64, + Type: LedgerEntryChangeTypeCreated, + KeyXDR: keyB64, + BeforeXDR: nil, + AfterXDR: &entryB64, }, }, { @@ -61,10 +66,10 @@ func TestLedgerEntryChange(t *testing.T) { After: nil, }, expectedOutput: LedgerEntryChange{ - Type: LedgerEntryChangeTypeDeleted, - Key: keyB64, - Before: &entryB64, - After: nil, + Type: LedgerEntryChangeTypeDeleted, + KeyXDR: keyB64, + BeforeXDR: &entryB64, + AfterXDR: nil, }, }, { @@ -74,22 +79,34 @@ func TestLedgerEntryChange(t *testing.T) { After: entryXDR, }, expectedOutput: LedgerEntryChange{ - Type: LedgerEntryChangeTypeUpdated, - Key: keyB64, - Before: &entryB64, - After: &entryB64, + Type: LedgerEntryChangeTypeUpdated, + KeyXDR: keyB64, + BeforeXDR: &entryB64, + AfterXDR: &entryB64, }, }, } { var change LedgerEntryChange - require.NoError(t, change.FromXDRDiff(test.input), test.name) - assert.Equal(t, test.expectedOutput, change) + require.NoError(t, change.FromXDRDiff(test.input, ""), test.name) + require.Equal(t, test.expectedOutput, change) // test json roundtrip changeJSON, err := json.Marshal(change) require.NoError(t, err, test.name) var change2 LedgerEntryChange require.NoError(t, json.Unmarshal(changeJSON, &change2)) - assert.Equal(t, change, change2, test.name) + require.Equal(t, change, change2, test.name) + + // test JSON output + var changeJs LedgerEntryChange + require.NoError(t, changeJs.FromXDRDiff(test.input, FormatJSON), test.name) + + require.Equal(t, keyJs, changeJs.KeyJSON) + if changeJs.AfterJSON != nil { + require.Equal(t, entryJs, changeJs.AfterJSON) + } + if changeJs.BeforeJSON != nil { + require.Equal(t, entryJs, changeJs.BeforeJSON) + } } } diff --git a/cmd/soroban-rpc/internal/xdr2json/conversion.go b/cmd/soroban-rpc/internal/xdr2json/conversion.go new file mode 100644 index 00000000..828b3145 --- /dev/null +++ b/cmd/soroban-rpc/internal/xdr2json/conversion.go @@ -0,0 +1,86 @@ +//nolint:lll +package xdr2json + +/* +// See preflight.go for add'l explanations: +// Note: no blank lines allowed. +#include +#include "../../lib/xdr2json.h" +#cgo windows,amd64 LDFLAGS: -L${SRCDIR}/../../../../target/x86_64-pc-windows-gnu/release-with-panic-unwind/ -lxdr2json -lntdll -static -lws2_32 -lbcrypt -luserenv +#cgo darwin,amd64 LDFLAGS: -L${SRCDIR}/../../../../target/x86_64-apple-darwin/release-with-panic-unwind/ -lxdr2json -ldl -lm +#cgo darwin,arm64 LDFLAGS: -L${SRCDIR}/../../../../target/aarch64-apple-darwin/release-with-panic-unwind/ -lxdr2json -ldl -lm +#cgo linux,amd64 LDFLAGS: -L${SRCDIR}/../../../../target/x86_64-unknown-linux-gnu/release-with-panic-unwind/ -lxdr2json -ldl -lm +#cgo linux,arm64 LDFLAGS: -L${SRCDIR}/../../../../target/aarch64-unknown-linux-gnu/release-with-panic-unwind/ -lxdr2json -ldl -lm +*/ +import "C" + +import ( + "encoding" + "encoding/json" + "reflect" + "unsafe" + + "github.com/pkg/errors" +) + +// ConvertBytes takes an XDR object (`xdr`) and its serialized bytes (`field`) +// and returns the raw JSON-formatted serialization of that object. +// It can be unmarshalled to a proper JSON structure, but the raw bytes are +// returned to avoid unnecessary round-trips. If there is an +// error, it returns an empty JSON object. +// +// The `xdr` object does not need to actually be initialized/valid: +// we only use it to determine the name of the structure. We could just +// accept a string, but that would make mistakes likelier than passing the +// structure itself (by reference). +func ConvertBytes(xdr interface{}, field []byte) ([]byte, error) { + xdrTypeName := reflect.TypeOf(xdr).Name() + return convertAnyBytes(xdrTypeName, field) +} + +// ConvertInterface takes a valid XDR object (`xdr`) and returns +// the raw JSON-formatted serialization of that object. If there is an +// error, it returns an empty JSON object. +// +// Unlike `ConvertBytes`, the value here needs to be valid and +// serializable. +func ConvertInterface(xdr encoding.BinaryMarshaler) (json.RawMessage, error) { + xdrTypeName := reflect.TypeOf(xdr).Name() + data, err := xdr.MarshalBinary() + if err != nil { + return []byte("{}"), errors.Wrapf(err, "failed to serialize XDR type '%s'", xdrTypeName) + } + + return convertAnyBytes(xdrTypeName, data) +} + +func convertAnyBytes(xdrTypeName string, field []byte) (json.RawMessage, error) { + var jsonStr, errStr string + // scope just added to show matching alloc/frees + { + goRawXdr := CXDR(field) + b := C.CString(xdrTypeName) + + result := C.xdr_to_json(b, goRawXdr) + C.free(unsafe.Pointer(b)) + + jsonStr = C.GoString(result.json) + errStr = C.GoString(result.error) + + C.free_conversion_result(result) + } + + if errStr != "" { + return json.RawMessage(jsonStr), errors.New(errStr) + } + + return json.RawMessage(jsonStr), nil +} + +// CXDR is ripped directly from preflight.go to avoid a dependency. +func CXDR(xdr []byte) C.xdr_t { + return C.xdr_t{ + xdr: (*C.uchar)(C.CBytes(xdr)), + len: C.size_t(len(xdr)), + } +} diff --git a/cmd/soroban-rpc/lib/ffi/Cargo.toml b/cmd/soroban-rpc/lib/ffi/Cargo.toml new file mode 100644 index 00000000..1d83e73d --- /dev/null +++ b/cmd/soroban-rpc/lib/ffi/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "ffi" +version = "21.0.0" +publish = false + +[lib] +crate-type = ["lib"] + +[dependencies] +libc = { workspace = true } diff --git a/cmd/soroban-rpc/lib/ffi/src/lib.rs b/cmd/soroban-rpc/lib/ffi/src/lib.rs new file mode 100644 index 00000000..de26fb1f --- /dev/null +++ b/cmd/soroban-rpc/lib/ffi/src/lib.rs @@ -0,0 +1,80 @@ +extern crate libc; + +use std::ffi::{CStr, CString}; +use std::ptr::null_mut; +use std::slice; + +#[repr(C)] +#[derive(Copy, Clone)] +pub struct CXDR { + pub xdr: *mut libc::c_uchar, + pub len: libc::size_t, +} + +// It would be nicer to derive Default, but we can't. It errors with: +// The trait bound `*mut u8: std::default::Default` is not satisfied +impl Default for CXDR { + fn default() -> Self { + CXDR { + xdr: null_mut(), + len: 0, + } + } +} + +/// Converts a Rust string to a C byte array. +/// +/// The memory allocated to the C string must be freed when you're done with it +/// by calling `free_c_string`. +/// +/// # Panics +/// +/// If `str` is valid, this never panics; just be cool. +#[must_use] +pub fn string_to_c(str: String) -> *mut libc::c_char { + CString::new(str).unwrap().into_raw() +} + +/// Frees the memory previously allocated by Rust in `string_to_c`. +/// +/// # Safety +/// +/// You should take care to only free the same string once, and don't free +/// pointers to strings allocated from across the FFI boundary. +pub unsafe fn free_c_string(str: *mut libc::c_char) { + if str.is_null() { + return; + } + unsafe { + _ = CString::from_raw(str); + } +} + +/// Frees the memory allocated to a generic XDR structure. +/// +/// # Panics +/// +/// If `str` is a valid null-terminated C string, this won't panic. +/// +/// # Safety +/// +/// You should take care to only free the same struct once, and don't free +/// pointers to structs allocated from across the FFI boundary. +#[must_use] +pub unsafe fn from_c_string(str: *const libc::c_char) -> String { + let c_str = unsafe { CStr::from_ptr(str) }; + c_str.to_str().unwrap().to_string() +} + +/// Transforms an FFI-compatible raw XDR structure into a Rust-managed chunk of +/// memory. +/// +/// # Safety +/// +/// Unless the structure itself is whack (e.g., you've free'd it before or the +/// buffer is mangled), this is safe to use. +#[must_use] +pub unsafe fn from_c_xdr(xdr: CXDR) -> Vec { + let s = unsafe { slice::from_raw_parts(xdr.xdr, xdr.len) }; + s.to_vec() +} diff --git a/cmd/soroban-rpc/lib/preflight.h b/cmd/soroban-rpc/lib/preflight.h index c56c112e..4952b652 100644 --- a/cmd/soroban-rpc/lib/preflight.h +++ b/cmd/soroban-rpc/lib/preflight.h @@ -3,6 +3,7 @@ #include #include +#include "shared.h" typedef struct ledger_info_t { uint32_t protocol_version; @@ -13,11 +14,6 @@ typedef struct ledger_info_t { uint64_t bucket_list_size; } ledger_info_t; -typedef struct xdr_t { - unsigned char *xdr; - size_t len; -} xdr_t; - typedef struct xdr_vector_t { xdr_t *array; size_t len; diff --git a/cmd/soroban-rpc/lib/preflight/Cargo.toml b/cmd/soroban-rpc/lib/preflight/Cargo.toml index f3f4193f..28e05689 100644 --- a/cmd/soroban-rpc/lib/preflight/Cargo.toml +++ b/cmd/soroban-rpc/lib/preflight/Cargo.toml @@ -1,18 +1,23 @@ [package] name = "preflight" -version = "21.3.0" +version = "21.4.0" publish = false [lib] crate-type = ["staticlib"] [dependencies] +ffi = { path = "../ffi" } + base64 = { workspace = true } libc = { workspace = true } sha2 = { workspace = true } + # we need the testutils feature in order to get backtraces in the preflight library # when soroban rpc is configured to run with --preflight-enable-debug soroban-env-host = { workspace = true, features = ["recording_mode", "testutils", "unstable-next-api"]} soroban-simulation = { workspace = true } + anyhow = { workspace = true } rand = { workspace = true } +serde_json = { workspace = true } diff --git a/cmd/soroban-rpc/lib/preflight/src/lib.rs b/cmd/soroban-rpc/lib/preflight/src/lib.rs index 79fec314..e5834403 100644 --- a/cmd/soroban-rpc/lib/preflight/src/lib.rs +++ b/cmd/soroban-rpc/lib/preflight/src/lib.rs @@ -1,12 +1,19 @@ extern crate anyhow; extern crate base64; +extern crate ffi; extern crate libc; +extern crate serde_json; extern crate sha2; extern crate soroban_env_host; extern crate soroban_simulation; use anyhow::{anyhow, bail, Result}; use sha2::{Digest, Sha256}; + +// We really do need everything. +#[allow(clippy::wildcard_imports)] +use ffi::*; + use soroban_env_host::storage::EntryWithLiveUntil; use soroban_env_host::xdr::{ AccountId, ExtendFootprintTtlOp, Hash, InvokeHostFunctionOp, LedgerEntry, LedgerEntryData, @@ -20,13 +27,14 @@ use soroban_simulation::simulation::{ SimulationAdjustmentConfig, }; use soroban_simulation::{AutoRestoringSnapshotSource, NetworkConfig, SnapshotSourceWithArchive}; + use std::cell::RefCell; use std::convert::TryFrom; -use std::ffi::{CStr, CString}; +use std::ffi::CString; +use std::mem; use std::panic; use std::ptr::null_mut; use std::rc::Rc; -use std::{mem, slice}; #[repr(C)] #[derive(Copy, Clone)] @@ -40,7 +48,7 @@ pub struct CLedgerInfo { } fn fill_ledger_info(c_ledger_info: CLedgerInfo, network_config: &NetworkConfig) -> LedgerInfo { - let network_passphrase = from_c_string(c_ledger_info.network_passphrase); + let network_passphrase = unsafe { from_c_string(c_ledger_info.network_passphrase) }; let mut ledger_info = LedgerInfo { protocol_version: c_ledger_info.protocol_version, sequence_number: c_ledger_info.sequence_number, @@ -53,24 +61,6 @@ fn fill_ledger_info(c_ledger_info: CLedgerInfo, network_config: &NetworkConfig) ledger_info } -#[repr(C)] -#[derive(Copy, Clone)] -pub struct CXDR { - pub xdr: *mut libc::c_uchar, - pub len: libc::size_t, -} - -// It would be nicer to derive Default, but we can't. It errors with: -// The trait bound `*mut u8: std::default::Default` is not satisfied -impl Default for CXDR { - fn default() -> Self { - CXDR { - xdr: null_mut(), - len: 0, - } - } -} - #[repr(C)] #[derive(Copy, Clone)] pub struct CXDRVector { @@ -238,9 +228,11 @@ fn preflight_invoke_hf_op_or_maybe_panic( enable_debug: bool, ) -> Result { let invoke_hf_op = - InvokeHostFunctionOp::from_xdr(from_c_xdr(invoke_hf_op), DEFAULT_XDR_RW_LIMITS).unwrap(); + InvokeHostFunctionOp::from_xdr(unsafe { from_c_xdr(invoke_hf_op) }, DEFAULT_XDR_RW_LIMITS) + .unwrap(); let source_account = - AccountId::from_xdr(from_c_xdr(source_account), DEFAULT_XDR_RW_LIMITS).unwrap(); + AccountId::from_xdr(unsafe { from_c_xdr(source_account) }, DEFAULT_XDR_RW_LIMITS).unwrap(); + let go_storage = Rc::new(GoLedgerStorage::new(handle)); let network_config = NetworkConfig::load_from_snapshot(go_storage.as_ref(), c_ledger_info.bucket_list_size)?; @@ -312,8 +304,9 @@ fn preflight_footprint_ttl_op_or_maybe_panic( footprint: CXDR, c_ledger_info: CLedgerInfo, ) -> Result { - let op_body = OperationBody::from_xdr(from_c_xdr(op_body), DEFAULT_XDR_RW_LIMITS)?; - let footprint = LedgerFootprint::from_xdr(from_c_xdr(footprint), DEFAULT_XDR_RW_LIMITS)?; + let op_body = OperationBody::from_xdr(unsafe { from_c_xdr(op_body) }, DEFAULT_XDR_RW_LIMITS)?; + let footprint = + LedgerFootprint::from_xdr(unsafe { from_c_xdr(footprint) }, DEFAULT_XDR_RW_LIMITS)?; let go_storage = Rc::new(GoLedgerStorage::new(handle)); let network_config = NetworkConfig::load_from_snapshot(go_storage.as_ref(), c_ledger_info.bucket_list_size)?; @@ -495,21 +488,12 @@ pub unsafe extern "C" fn free_preflight_result(result: *mut CPreflightResult) { free_c_xdr_diff_array(boxed.ledger_entry_diff); } -fn free_c_string(str: *mut libc::c_char) { - if str.is_null() { - return; - } - unsafe { - _ = CString::from_raw(str); - } -} - fn free_c_xdr(xdr: CXDR) { if xdr.xdr.is_null() { return; } unsafe { - let _ = Vec::from_raw_parts(xdr.xdr, xdr.len, xdr.len); + _ = Vec::from_raw_parts(xdr.xdr, xdr.len, xdr.len); } } @@ -538,16 +522,6 @@ fn free_c_xdr_diff_array(xdr_array: CXDRDiffVector) { } } -fn from_c_string(str: *const libc::c_char) -> String { - let c_str = unsafe { CStr::from_ptr(str) }; - c_str.to_str().unwrap().to_string() -} - -fn from_c_xdr(xdr: CXDR) -> Vec { - let s = unsafe { slice::from_raw_parts(xdr.xdr, xdr.len) }; - s.to_vec() -} - // Functions imported from Golang extern "C" { // Free Strings returned from Go functions @@ -579,7 +553,7 @@ impl GoLedgerStorage { if res.xdr.is_null() { return None; } - let v = from_c_xdr(res); + let v = unsafe { from_c_xdr(res) }; unsafe { FreeGoXDR(res) }; Some(v) } diff --git a/cmd/soroban-rpc/lib/shared.h b/cmd/soroban-rpc/lib/shared.h new file mode 100644 index 00000000..63feebe9 --- /dev/null +++ b/cmd/soroban-rpc/lib/shared.h @@ -0,0 +1,4 @@ +typedef struct xdr_t { + unsigned char *xdr; + size_t len; +} xdr_t; \ No newline at end of file diff --git a/cmd/soroban-rpc/lib/xdr2json.h b/cmd/soroban-rpc/lib/xdr2json.h new file mode 100644 index 00000000..89ef9230 --- /dev/null +++ b/cmd/soroban-rpc/lib/xdr2json.h @@ -0,0 +1,13 @@ +#include "shared.h" + +typedef struct { + const char* const json; + const char* const error; +} conversion_result_t; + +conversion_result_t* xdr_to_json( + const char* const typename, + xdr_t xdr +); + +void free_conversion_result(conversion_result_t*); \ No newline at end of file diff --git a/cmd/soroban-rpc/lib/xdr2json/Cargo.toml b/cmd/soroban-rpc/lib/xdr2json/Cargo.toml new file mode 100644 index 00000000..e0119838 --- /dev/null +++ b/cmd/soroban-rpc/lib/xdr2json/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "xdr2json" +version = "21.0.0" +publish = false + +[lib] +crate-type = ["staticlib"] + +[dependencies] +ffi = { path = "../ffi" } +libc = { workspace = true } +sha2 = { workspace = true } +base64 = { workspace = true } +anyhow = { workspace = true } +serde_json = { workspace = true } +rand = { workspace = true } + +stellar-xdr = { workspace = true } +soroban-env-host = { workspace = true, features = ["unstable-next-api"]} \ No newline at end of file diff --git a/cmd/soroban-rpc/lib/xdr2json/src/lib.rs b/cmd/soroban-rpc/lib/xdr2json/src/lib.rs new file mode 100644 index 00000000..c4df4c62 --- /dev/null +++ b/cmd/soroban-rpc/lib/xdr2json/src/lib.rs @@ -0,0 +1,121 @@ +extern crate anyhow; +extern crate ffi; +extern crate soroban_env_host; +extern crate stellar_xdr; + +use std::{panic, str::FromStr}; + +use anyhow::Result; + +// We really do need everything. +#[allow(clippy::wildcard_imports)] +use ffi::*; +use soroban_env_host::{xdr, DEFAULT_XDR_RW_LIMITS}; + +#[repr(C)] +pub struct ConversionResult { + json: *mut libc::c_char, + error: *mut libc::c_char, +} + +struct RustConversionResult { + json: String, + error: String, +} + +/// Takes in a string name of an XDR type in the Stellar Protocol (i.e. from the +/// `stellar_xdr` crate) as well as a raw byte structure and returns a structure +/// containing the JSON-ified string of the given structure. +/// +/// # Errors +/// +/// On error, the struct's `error` field will be filled out with the appropriate +/// message that caused the function to panic. +/// +/// # Panics +/// +/// This should never panic due to `catch_json_to_xdr_panic` catching and +/// unwinding all panics to stringified error messages. +/// +/// # Safety +/// +/// This relies on the function parameters to be valid structures. The +/// `typename` must be a null-terminated C string. The `xdr` structure should +/// have a valid pointer to an aligned byte array and have a matching size. If +/// these aren't true there may be segfaults when trying to manage their memory. +#[no_mangle] +pub unsafe extern "C" fn xdr_to_json( + typename: *mut libc::c_char, + xdr: CXDR, +) -> *mut ConversionResult { + let result = catch_json_to_xdr_panic(Box::new(move || { + let type_str = unsafe { from_c_string(typename) }; + let the_type = xdr::TypeVariant::from_str(&type_str).unwrap(); + + let xdr_bytearray = unsafe { from_c_xdr(xdr) }; + let mut buffer = xdr::Limited::new(xdr_bytearray.as_slice(), DEFAULT_XDR_RW_LIMITS.clone()); + + let t = xdr::Type::read_xdr_to_end(the_type, &mut buffer).unwrap(); + + Ok(RustConversionResult { + json: serde_json::to_string(&t).unwrap(), + error: String::new(), + }) + })); + + // Caller is responsible for calling free_conversion_result. + Box::into_raw(Box::new(ConversionResult { + json: string_to_c(result.json), + error: string_to_c(result.error), + })) +} + +/// Frees memory allocated for the corresponding conversion result. +/// +/// # Safety +/// +/// You should *only* use this to free the return value of `xdr_to_json`. +#[no_mangle] +pub unsafe extern "C" fn free_conversion_result(ptr: *mut ConversionResult) { + if ptr.is_null() { + return; + } + + unsafe { + free_c_string((*ptr).json); + free_c_string((*ptr).error); + drop(Box::from_raw(ptr)); + } +} + +/// Runs a JSON conversion operation and unwinds panics. +/// +/// It is modeled after `catch_preflight_panic()` and will always return valid +/// JSON in the result's `json` field and an error string in `error` if a panic +/// occurs. +fn catch_json_to_xdr_panic( + op: Box Result>, +) -> RustConversionResult { + // catch panics before they reach foreign callers (which otherwise would result in + // undefined behavior) + let res: std::thread::Result> = + panic::catch_unwind(panic::AssertUnwindSafe(op)); + + match res { + Err(panic) => match panic.downcast::() { + Ok(panic_msg) => RustConversionResult { + json: "{}".to_string(), + error: format!("xdr_to_json() failed: {panic_msg}"), + }, + Err(_) => RustConversionResult { + json: "{}".to_string(), + error: "xdr_to_json() failed: unknown cause".to_string(), + }, + }, + // See https://docs.rs/anyhow/latest/anyhow/struct.Error.html#display-representations + Ok(r) => r.unwrap_or_else(|e| RustConversionResult { + json: "{}".to_string(), + error: format!("{e:?}"), + }), + } +}