diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 51c36de7f..2144052f2 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -51,11 +51,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v7.0.1 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -66,7 +66,7 @@ jobs: # queries: security-extended,security-and-quality - name: Set up JDK - uses: actions/setup-java@v2 + uses: actions/setup-java@v6.1.0 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRIBUTION }} @@ -85,6 +85,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@v4 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/sbom.yml b/.github/workflows/sbom.yml index c3a0aa10f..edb10fd33 100644 --- a/.github/workflows/sbom.yml +++ b/.github/workflows/sbom.yml @@ -17,6 +17,6 @@ jobs: id-token: write steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7.0.1 - name: Fetch and submit SBOM uses: SCF-Public-Goods-Maintenance/pg-atlas-sbom-action@main \ No newline at end of file diff --git a/.github/workflows/test-deploy.yml b/.github/workflows/test-deploy.yml index 9e1bf1703..379f125e0 100644 --- a/.github/workflows/test-deploy.yml +++ b/.github/workflows/test-deploy.yml @@ -27,25 +27,25 @@ jobs: env: ENABLE_LOGS: true NETWORK: local - ENABLE_SOROBAN_RPC: true - PROTOCOL_VERSION: 28 + ENABLE: core,rpc + PROTOCOL_VERSION: 30 options: >- - --health-cmd "curl --no-progress-meter --fail-with-body -X POST \"http://localhost:8000/rpc\" -H 'Content-Type: application/json' -d '{\"jsonrpc\":\"2.0\",\"id\":8675309,\"method\":\"getNetwork\"}' && curl --no-progress-meter \"http://localhost:8000/friendbot\" | grep '\"invalid_field\": \"addr\"'" + --health-cmd "curl --no-progress-meter -X POST \"http://localhost:8000/rpc\" -H 'Content-Type: application/json' -d '{\"jsonrpc\":\"2.0\",\"id\":8675309,\"method\":\"getHealth\"}' | grep '\"status\":\"healthy\"' && curl --no-progress-meter \"http://localhost:8000/friendbot\" | grep '\"invalid_field\": \"addr\"'" --health-interval 10s --health-timeout 5s --health-retries 50 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7.0.1 - name: Set up JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@v6.1.0 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRIBUTION }} - name: Test run: ./gradlew check - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v7.1.1 with: token: ${{ secrets.CODECOV_TOKEN }} @@ -54,16 +54,16 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7.0.1 - name: Set up JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@v6.1.0 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRIBUTION }} - name: Build run: ./gradlew build -x test -x jacocoTestReport - name: Persist JAR Artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7.0.2 with: name: libs path: build/libs/*.jar @@ -73,16 +73,16 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7.0.1 - name: Set up JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@v6.1.0 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRIBUTION }} - name: Generate Documentation run: ./gradlew javadoc - name: Persist Documentation - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7.0.2 with: name: javadoc path: javadoc @@ -101,9 +101,9 @@ jobs: ] steps: - name: checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7.0.1 - name: Download JAR Artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8.0.2 with: name: libs path: android_test/app/libs/ @@ -112,12 +112,12 @@ jobs: echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm - - uses: actions/setup-java@v4 + - uses: actions/setup-java@v6.1.0 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRIBUTION }} - name: run tests - uses: reactivecircus/android-emulator-runner@v2 + uses: reactivecircus/android-emulator-runner@v2.38.0 with: working-directory: ./android_test api-level: ${{ matrix.emulator.api-level }} @@ -134,27 +134,27 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7.0.1 - name: Download JAR Artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8.0.2 with: name: libs path: libs - name: Download Java Documentation - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8.0.2 with: name: javadoc path: javadoc - name: Archive Documentation run: tar -czf stellar-sdk-javadoc.tar.gz javadoc - name: Upload artifacts to GitHub Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3.0.3 with: files: | libs/*.jar stellar-sdk-javadoc.tar.gz - name: Upload Documentation to GitHub Pages - uses: JamesIves/github-pages-deploy-action@v4 + uses: JamesIves/github-pages-deploy-action@v4.9.0 with: folder: javadoc @@ -170,11 +170,11 @@ jobs: SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7.0.1 - name: Set up JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@v6.1.0 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRIBUTION }} - name: Publish to Maven Central - run: ./gradlew publishAllPublicationsToCentralPortal + run: ./gradlew publishAggregationToCentralPortal diff --git a/.github/workflows/xdr-generator-snapshot-test.yml b/.github/workflows/xdr-generator-snapshot-test.yml index 918f8f466..eeb6965c8 100644 --- a/.github/workflows/xdr-generator-snapshot-test.yml +++ b/.github/workflows/xdr-generator-snapshot-test.yml @@ -17,6 +17,6 @@ jobs: snapshot-test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7.0.1 - name: Run XDR generator snapshot tests run: make xdr-generator-test diff --git a/CHANGELOG.md b/CHANGELOG.md index 3af1dcd22..d4495c403 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,18 @@ - `submitTimeout` bounds the polling loop only. Each individual request is still bounded by the `SorobanServer` HTTP timeouts. - This applies to `submit`, `signAndSubmit`, `restoreFootprint`, and therefore to `ContractClient.invoke` when it submits. - fix: always encode text as UTF-8 in `Scv.toString(String)`, `Sep10Challenge` (`web_auth_domain` / `client_domain`), `Util.paddedByteArray(String, int)`, and the `SorobanServer` request body. They used the JVM default charset, so non-ASCII text produced different bytes on JVMs where that is not UTF-8. +- chore: upgrade generated XDR definitions to Protocol 30. +- fix: `StrKey.decodeClaimableBalance` and `StrKey.isValidClaimableBalance` now reject a claimable balance strkey (`B...`) whose type byte is not 0, as SEP-23 requires; `CLAIMABLE_BALANCE_ID_TYPE_V0` is the only type. Such a key used to be accepted, so `new Address(...)` took it and the error only surfaced in `Address.toSCAddress`. `new Address(...)` now throws `IllegalArgumentException` up front. +- refactor: deprecate `GetEventsResponse.EventInfo.inSuccessfulContractCall`. Stellar RPC no longer returns it from `getEvents` ([stellar-rpc#1050](https://github.com/stellar/stellar-rpc/pull/1050)), so it is `null` with newer servers. It will be removed in the next major release. +- feat: add CAP-84 muxed contract address (`W...`) support. + - New `StrKey.encodeMuxedContract`, `StrKey.decodeMuxedContract`, and `StrKey.isValidMuxedContract`. The raw form is the 32-byte contract ID followed by the 8-byte big-endian ID, as specified by SEP-23. + - `Address` accepts `W...` addresses and converts `SC_ADDRESS_TYPE_MUXED_CONTRACT` to and from `SCAddress` and `SCVal`, with the new `Address.AddressType.MUXED_CONTRACT` and `Address.fromMuxedContract(byte[])`. Contract return values and events that hold a muxed contract address no longer fail to decode. + - Map keys holding muxed contract addresses are now sorted correctly when building an `SCV_MAP`. + - A muxed contract address is only valid where a muxed account (`M...`) is, such as the `to` parameter of the SAC `transfer`. The SDK now applies the Soroban host's rules up front instead of building transactions that fail at simulation or submission: + - Authorization addresses must be an account (`G...`) or a contract (`C...`), as the host requires. `Auth.authorizeInvocation`, the `forAddress` of `Auth.authorizeEntry`, `Auth.DelegateSignature`, and the credentials of an entry passed to `Auth.authorizeEntry`, `Auth.buildAuthorizationEntryPreimage`, or `Auth.buildWithDelegatesEntry` reject any other address type, including muxed accounts (`M...`), which were accepted before. + - `SorobanServer.getContractData` requires a contract address (`C...`) and rejects a `key` that contains a muxed address (`M...` or `W...`) anywhere, since the host never stores such keys. + - `TransactionBuilder.buildRestoreAssetBalanceEntryTransaction` requires `balanceOwner` to be a contract address (`C...`). Only contracts hold SAC balances in contract data; an account's balance lives in its trustline. +- feat: add `contractMuxed` and `contractMuxedId` to `ContractCreditedEffectResponse`. Horizon sets them when a Stellar Asset Contract `transfer` or `mint` goes to a muxed contract address (`W...`); `contract` is then its base contract address (`C...`). ([stellar-horizon#237](https://github.com/stellar/stellar-horizon/pull/237)) ## 5.0.0 diff --git a/Makefile b/Makefile index b2d4fb201..bb392550c 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ xdr/Stellar-transaction.x \ xdr/Stellar-types.x # stellar-xdr commit to use, see https://github.com/stellar/stellar-xdr -XDR_COMMIT=9c9c145953e80990d6ff1ae3a6a973a0ce6d0694 +XDR_COMMIT=4f524bbac80c781c06e4fb5fc93a2d0d331d6705 .PHONY: xdr xdr-clean xdr-update xdr-generator-test xdr-generator-update-snapshots diff --git a/android_test/app/build.gradle.kts b/android_test/app/build.gradle.kts index 42aeda92d..d7a3cd7d8 100644 --- a/android_test/app/build.gradle.kts +++ b/android_test/app/build.gradle.kts @@ -69,12 +69,12 @@ dependencies { // Since we are adding local jar(libs/stellar-sdk.jar) as dependency, // gradle cannot automatically download the required third-party dependencies. implementation(files("libs/stellar-sdk-5.0.0.jar")) - implementation("com.squareup.okhttp3:okhttp:4.11.0") - implementation("com.squareup.okhttp3:okhttp-sse:4.11.0") + implementation("com.squareup.okhttp3:okhttp:4.12.1") + implementation("com.squareup.okhttp3:okhttp-sse:4.12.1") implementation("com.moandjiezana.toml:toml4j:0.7.2") - implementation("com.google.code.gson:gson:2.11.0") - implementation("org.bouncycastle:bcprov-jdk18on:1.78.1") - implementation("commons-codec:commons-codec:1.17.1") + implementation("com.google.code.gson:gson:2.15.0") + implementation("org.bouncycastle:bcprov-jdk18on:1.86") + implementation("commons-codec:commons-codec:1.23.0") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.uiautomator:uiautomator:2.3.0-alpha03") androidTestImplementation("androidx.test.ext:junit:1.1.5") diff --git a/build.gradle.kts b/build.gradle.kts index fac45d80a..0dc9916c8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,11 +4,10 @@ plugins { id("signing") id("maven-publish") id("project-report") - id("com.diffplug.spotless") version "8.7.0" - id("com.github.ben-manes.versions") version "0.54.0" - id("io.freefair.lombok") version "9.5.0" - id("com.gradleup.nmcp").version("1.6.0") - kotlin("jvm") version "2.4.0" + id("com.diffplug.spotless") version "8.10.4" + id("io.github.ben-manes.versions") version "0.66.0" + id("io.freefair.lombok") version "9.8.2" + kotlin("jvm") version "2.4.21" } group = "network.lightsail" @@ -38,30 +37,30 @@ spotless { } kotlin { target("src/test/kotlin/**/*.kt") - ktfmt("0.56").googleStyle() + ktfmt("0.64").googleStyle() } } dependencies { - val okhttpVersion = "4.12.0" + val okhttpVersion = "4.12.1" implementation("com.squareup.okhttp3:okhttp:${okhttpVersion}") implementation("com.squareup.okhttp3:okhttp-sse:${okhttpVersion}") implementation("com.moandjiezana.toml:toml4j:0.7.2") - implementation("com.google.code.gson:gson:2.14.0") - implementation("org.bouncycastle:bcprov-jdk18on:1.84") - implementation("commons-codec:commons-codec:1.22.0") + implementation("com.google.code.gson:gson:2.15.0") + implementation("org.bouncycastle:bcprov-jdk18on:1.86") + implementation("commons-codec:commons-codec:1.23.0") testImplementation(kotlin("stdlib")) - testImplementation("org.mockito:mockito-core:5.23.0") + testImplementation("org.mockito:mockito-core:5.24.0") testImplementation("com.squareup.okhttp3:mockwebserver:${okhttpVersion}") testImplementation("junit:junit:4.13.2") - testImplementation("org.bouncycastle:bcpkix-jdk18on:1.84") // mock https - testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.11.3") + testImplementation("org.bouncycastle:bcpkix-jdk18on:1.86") // mock https + testRuntimeOnly("org.junit.vintage:junit-vintage-engine:6.1.3") testRuntimeOnly("org.junit.platform:junit-platform-launcher") - testImplementation("io.kotest:kotest-runner-junit5:6.1.11") - testImplementation("io.kotest:kotest-assertions-core:6.1.11") + testImplementation("io.kotest:kotest-runner-junit5:6.2.6") + testImplementation("io.kotest:kotest-assertions-core:6.2.6") } tasks { @@ -79,12 +78,12 @@ tasks { dependsOn(testClasses) } - val sourcesJar by registering(Jar::class) { + val sourcesJar = register("sourcesJar") { archiveClassifier = "sources" from(sourceSets.main.get().allSource) } - val uberJar by registering(Jar::class) { + val uberJar = register("uberJar") { // https://docs.gradle.org/current/userguide/working_with_files.html#sec:creating_uber_jar_exampl archiveClassifier = "uber" duplicatesStrategy = DuplicatesStrategy.EXCLUDE @@ -108,7 +107,7 @@ tasks { } } - val javadocJar by registering(Jar::class) { + val javadocJar = register("javadocJar") { archiveClassifier = "javadoc" dependsOn(javadoc) from(javadoc.get().destinationDir) // It needs to be placed after the javadoc task, otherwise it cannot read the path we set. @@ -196,7 +195,7 @@ publishing { signing { - val publishCommand = "publishAllPublicationsToCentralPortal" + val publishCommand = "publishAggregationToCentralPortal" isRequired = gradle.startParameter.taskNames.contains(publishCommand) println("Need to sign? $isRequired") // https://docs.gradle.org/current/userguide/signing_plugin.html#using_in_memory_ascii_armored_openpgp_subkeys @@ -211,14 +210,3 @@ signing { useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword) sign(publishing.publications["mavenJava"]) } - -nmcp { - publishAllPublicationsToCentralPortal { - username = System.getenv("SONATYPE_USERNAME") - password = System.getenv("SONATYPE_PASSWORD") - // publish manually from the portal - publishingType = "USER_MANAGED" - // or if you want to publish automatically - // publishingType = "AUTOMATIC" - } -} diff --git a/examples/build.gradle.kts b/examples/build.gradle.kts index 71f29f116..6adefd484 100644 --- a/examples/build.gradle.kts +++ b/examples/build.gradle.kts @@ -1,6 +1,6 @@ plugins { id("java") - id("com.diffplug.spotless") version "6.24.0" + id("com.diffplug.spotless") version "8.10.4" } @@ -23,7 +23,7 @@ spotless { dependencies { // Use https://central.sonatype.com/artifact/network.lightsail/stellar-sdk in prod. implementation("network.lightsail:stellar-sdk:5.0.0") - testImplementation(platform("org.junit:junit-bom:5.10.0")) + testImplementation(platform("org.junit:junit-bom:6.1.3")) testImplementation("org.junit.jupiter:junit-jupiter") } diff --git a/examples/gradle/wrapper/gradle-wrapper.jar b/examples/gradle/wrapper/gradle-wrapper.jar index 249e5832f..9fa7f2882 100644 Binary files a/examples/gradle/wrapper/gradle-wrapper.jar and b/examples/gradle/wrapper/gradle-wrapper.jar differ diff --git a/examples/gradle/wrapper/gradle-wrapper.properties b/examples/gradle/wrapper/gradle-wrapper.properties index b5c052432..91c4fc64a 100644 --- a/examples/gradle/wrapper/gradle-wrapper.properties +++ b/examples/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,9 @@ -#Wed Jul 03 20:45:55 CST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.2-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/examples/gradlew b/examples/gradlew index 1b6c78733..249efbb03 100755 --- a/examples/gradlew +++ b/examples/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,10 +15,12 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # -# Gradle start up script for POSIX generated by Gradle. +# gradlew start up script for POSIX generated by Gradle. # # Important for running: # @@ -27,7 +29,7 @@ # bash, then to run this script, type that shell name before the whole # command line, like: # -# ksh Gradle +# ksh gradlew # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,13 +82,11 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -114,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -133,22 +132,29 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -165,7 +171,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -193,18 +198,27 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/examples/gradlew.bat b/examples/gradlew.bat index 107acd32c..3185a43f7 100644 --- a/examples/gradlew.bat +++ b/examples/gradlew.bat @@ -13,19 +13,49 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem -@rem Gradle startup script for Windows +@rem gradlew startup script for Windows @rem @rem ########################################################################## -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +@rem Catch executions from older scripts and ensure they exit cleanly. +@rem This can be removed once we can be reasonably confident that few people +@rem will be migrating directly to this new wrapper. +goto afterSafetyNet +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +goto exitWithErrorLevel +:afterSafetyNet set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,15 +70,16 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. -goto fail +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -56,34 +87,26 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. -goto fail +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 -:mainEnd -if "%OS%"=="Windows_NT" endlocal +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel -:omega +@rem This label must not be changed. We rely on old scripts being able to jump to this point. +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/examples/src/main/java/network/lightsail/CheckStellarAssetContract.java b/examples/src/main/java/network/lightsail/CheckStellarAssetContract.java index 918cb19c2..9265c07e4 100644 --- a/examples/src/main/java/network/lightsail/CheckStellarAssetContract.java +++ b/examples/src/main/java/network/lightsail/CheckStellarAssetContract.java @@ -6,7 +6,6 @@ import org.stellar.sdk.responses.sorobanrpc.GetLedgerEntriesResponse; import org.stellar.sdk.scval.Scv; import org.stellar.sdk.xdr.ContractDataDurability; -import org.stellar.sdk.xdr.LedgerEntry.LedgerEntryData; import org.stellar.sdk.xdr.LedgerEntryType; import org.stellar.sdk.xdr.LedgerKey; import org.stellar.sdk.xdr.LedgerKey.LedgerKeyContractData; diff --git a/examples/src/main/java/network/lightsail/SorobanCreateContract.java b/examples/src/main/java/network/lightsail/SorobanCreateContract.java index d7be45064..155515b0c 100644 --- a/examples/src/main/java/network/lightsail/SorobanCreateContract.java +++ b/examples/src/main/java/network/lightsail/SorobanCreateContract.java @@ -56,15 +56,8 @@ public static void main(String[] args) throws IOException { System.out.println("Transaction succeeded: " + getTransactionResponse); // parse the function return value TransactionMeta transactionMeta = getTransactionResponse.parseResultMetaXdr(); - byte[] hash = - transactionMeta - .getV3() - .getSorobanMeta() - .getReturnValue() - .getAddress() - .getContractId() - .getHash(); - String contractId = StrKey.encodeContract(hash); + String contractId = + Address.fromSCVal(transactionMeta.getV4().getSorobanMeta().getReturnValue()).toString(); System.out.println("Contract ID: " + contractId); } else { System.out.println("Transaction failed: " + getTransactionResponse); diff --git a/examples/src/main/java/network/lightsail/SorobanInvokeContractFunction.java b/examples/src/main/java/network/lightsail/SorobanInvokeContractFunction.java index 04d4c4753..7f13ab089 100644 --- a/examples/src/main/java/network/lightsail/SorobanInvokeContractFunction.java +++ b/examples/src/main/java/network/lightsail/SorobanInvokeContractFunction.java @@ -30,7 +30,7 @@ public static void main(String[] args) throws IOException { // The invoke host function operation // https://github.com/stellar/soroban-examples/tree/main/hello_world - String contractId = "CDZJVZWCY4NFGHCCZMX6QW5AK3ET5L3UUAYBVNDYOXDLQXW7PHXGYOBJ"; + String contractId = "CDSMG6PNJYIPIWFIDQVLCOXMQPZU73FSBCTMBDGBGFLF7WPLJMASODYX"; String functionName = "hello"; List parameters = List.of(Scv.toString("World!")); // Scv is a helper class to create SCVal objects @@ -98,7 +98,7 @@ public static void main(String[] args) throws IOException { System.out.println("Transaction succeeded: " + getTransactionResponse); // parse the function return value TransactionMeta transactionMeta = getTransactionResponse.parseResultMetaXdr(); - SCVal rawReturnValue = transactionMeta.getV3().getSorobanMeta().getReturnValue(); + SCVal rawReturnValue = transactionMeta.getV4().getSorobanMeta().getReturnValue(); System.out.println("Raw return value: " + rawReturnValue); List vec = Scv.fromVec(rawReturnValue).stream().toList(); String returnValue0 = new String(Scv.fromString(vec.get(0)), StandardCharsets.UTF_8); diff --git a/examples/src/main/java/network/lightsail/SorobanUploadWasm.java b/examples/src/main/java/network/lightsail/SorobanUploadWasm.java index 9008da0e3..8eaf38f16 100644 --- a/examples/src/main/java/network/lightsail/SorobanUploadWasm.java +++ b/examples/src/main/java/network/lightsail/SorobanUploadWasm.java @@ -61,7 +61,7 @@ public static void main(String[] args) throws IOException { // parse the function return value TransactionMeta transactionMeta = getTransactionResponse.parseResultMetaXdr(); byte[] wasmId = - transactionMeta.getV3().getSorobanMeta().getReturnValue().getBytes().getSCBytes(); + transactionMeta.getV4().getSorobanMeta().getReturnValue().getBytes().getSCBytes(); System.out.println("Wasm ID: " + Util.bytesToHex(wasmId).toLowerCase()); } else { System.out.println("Transaction failed: " + getTransactionResponse); diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d997cfc60..9fa7f2882 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a351597e6..91c4fc64a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,9 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.2-bin.zip networkTimeout=10000 +retries=0 +retryBackOffMs=500 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 739907dfd..249efbb03 100755 --- a/gradlew +++ b/gradlew @@ -20,7 +20,7 @@ ############################################################################## # -# Gradle start up script for POSIX generated by Gradle. +# gradlew start up script for POSIX generated by Gradle. # # Important for running: # @@ -29,7 +29,7 @@ # bash, then to run this script, type that shell name before the whole # command line, like: # -# ksh Gradle +# ksh gradlew # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: @@ -57,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. diff --git a/gradlew.bat b/gradlew.bat index e509b2dd8..03754c4c3 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -19,12 +19,39 @@ @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem -@rem Gradle startup script for Windows +@rem gradlew startup script for Windows @rem @rem ########################################################################## -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +@rem Catch executions from older scripts and ensure they exit cleanly. +@rem This can be removed once we can be reasonably confident that few people +@rem will be migrating directly to this new wrapper. +goto afterSafetyNet +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +goto exitWithErrorLevel +:afterSafetyNet set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @@ -45,13 +72,14 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. -goto fail +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -59,35 +87,26 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. -goto fail +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel :execute @rem Setup the command line -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem This label must not be changed. We rely on old scripts being able to jump to this point. +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/settings.gradle.kts b/settings.gradle.kts index b06a2ed68..b0ba7064b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,2 +1,16 @@ +plugins { + id("com.gradleup.nmcp.settings").version("1.6.2") +} + rootProject.name = "stellar-sdk" +nmcpSettings { + centralPortal { + username = System.getenv("SONATYPE_USERNAME") + password = System.getenv("SONATYPE_PASSWORD") + // publish manually from the portal + publishingType = "USER_MANAGED" + // or if you want to publish automatically + // publishingType = "AUTOMATIC" + } +} diff --git a/src/main/java/org/stellar/sdk/Address.java b/src/main/java/org/stellar/sdk/Address.java index 41a12e7f8..aaf42743f 100644 --- a/src/main/java/org/stellar/sdk/Address.java +++ b/src/main/java/org/stellar/sdk/Address.java @@ -1,12 +1,15 @@ package org.stellar.sdk; import java.io.IOException; +import java.math.BigInteger; +import java.nio.ByteBuffer; import java.util.Arrays; import lombok.EqualsAndHashCode; import org.stellar.sdk.xdr.ClaimableBalanceID; import org.stellar.sdk.xdr.ClaimableBalanceIDType; import org.stellar.sdk.xdr.ContractID; import org.stellar.sdk.xdr.Hash; +import org.stellar.sdk.xdr.MuxedContract; import org.stellar.sdk.xdr.MuxedEd25519Account; import org.stellar.sdk.xdr.PoolID; import org.stellar.sdk.xdr.SCAddress; @@ -18,7 +21,7 @@ /** * Represents a single address in the Stellar network. An address can represent an account, - * contract, muxed account, claimable balance, or liquidity pool. + * contract, muxed account, claimable balance, liquidity pool, or muxed contract. */ @EqualsAndHashCode public class Address { @@ -29,7 +32,8 @@ public class Address { /** * Creates a new {@link Address} from a Stellar public key (G...), contract ID (C...), med25519 - * public key (M...), liquidity pool ID (L...), or claimable balance ID (B...). + * public key (M...), liquidity pool ID (L...), claimable balance ID (B...), or muxed contract + * address (W...). * * @param address the StrKey encoded format of Stellar address. */ @@ -49,6 +53,9 @@ public Address(String address) { } else if (StrKey.isValidLiquidityPool(address)) { this.type = AddressType.LIQUIDITY_POOL; this.key = StrKey.decodeLiquidityPool(address); + } else if (StrKey.isValidMuxedContract(address)) { + this.type = AddressType.MUXED_CONTRACT; + this.key = StrKey.decodeMuxedContract(address); } else { throw new IllegalArgumentException("Unsupported address type"); } @@ -104,6 +111,17 @@ public static Address fromLiquidityPool(byte[] liquidityPoolId) { return new Address(StrKey.encodeLiquidityPool(liquidityPoolId)); } + /** + * Creates a new {@link Address} from a Stellar muxed contract address. + * + * @param muxedContract the byte array of the Stellar muxed contract address (W...), the 32-byte + * contract ID followed by the 8-byte big-endian ID. + * @return a new {@link Address} object from the given Stellar muxed contract address. + */ + public static Address fromMuxedContract(byte[] muxedContract) { + return new Address(StrKey.encodeMuxedContract(muxedContract)); + } + /** * Creates a new {@link Address} from a {@link SCAddress} XDR object. * @@ -135,6 +153,14 @@ public static Address fromSCAddress(SCAddress scAddress) { return fromClaimableBalance(withZeroPrefix); case SC_ADDRESS_TYPE_LIQUIDITY_POOL: return fromLiquidityPool(scAddress.getLiquidityPoolId().getPoolID().getHash()); + case SC_ADDRESS_TYPE_MUXED_CONTRACT: + MuxedContract muxedContract = scAddress.getMuxedContract(); + return fromMuxedContract( + ByteBuffer.allocate(40) + .put(muxedContract.getContractId().getContractID().getHash()) + // See StrKey#toRawMuxedAccountStrKey for why longValue() is safe for a uint64. + .putLong(muxedContract.getId().getUint64().getNumber().longValue()) + .array()); default: throw new IllegalArgumentException("Unsupported address type"); } @@ -206,6 +232,17 @@ public SCAddress toSCAddress() { throw new IllegalArgumentException("Invalid liquidity pool ID", e); } break; + case MUXED_CONTRACT: + scAddress.setDiscriminant(org.stellar.sdk.xdr.SCAddressType.SC_ADDRESS_TYPE_MUXED_CONTRACT); + scAddress.setMuxedContract( + MuxedContract.builder() + .id( + new Uint64( + new XdrUnsignedHyperInteger( + new BigInteger(1, Arrays.copyOfRange(this.key, 32, 40))))) + .contractId(new ContractID(new Hash(Arrays.copyOfRange(this.key, 0, 32)))) + .build()); + break; default: throw new IllegalArgumentException("Unsupported address type"); } @@ -225,9 +262,20 @@ public SCVal toSCVal() { } /** - * Returns the byte array of the Stellar public key or contract ID. + * Returns the raw bytes of this address, as encoded in its StrKey: + * + *
    + *
  • {@code ACCOUNT}: the 32-byte ed25519 public key + *
  • {@code CONTRACT}: the 32-byte contract ID + *
  • {@code MUXED_ACCOUNT}: the 32-byte ed25519 public key followed by the 8-byte big-endian + * ID + *
  • {@code CLAIMABLE_BALANCE}: the 1-byte claimable balance type (0) followed by the 32-byte + * hash + *
  • {@code LIQUIDITY_POOL}: the 32-byte liquidity pool ID + *
  • {@code MUXED_CONTRACT}: the 32-byte contract ID followed by the 8-byte big-endian ID + *
* - * @return the byte array of the Stellar public key or contract ID. + * @return the raw bytes of this address */ public byte[] getBytes() { return key; @@ -260,6 +308,8 @@ public String getEncodedAddress() { return StrKey.encodeClaimableBalance(this.key); case LIQUIDITY_POOL: return StrKey.encodeLiquidityPool(this.key); + case MUXED_CONTRACT: + return StrKey.encodeMuxedContract(this.key); default: throw new IllegalArgumentException("Unsupported address type"); } @@ -276,6 +326,7 @@ public enum AddressType { CONTRACT, MUXED_ACCOUNT, CLAIMABLE_BALANCE, - LIQUIDITY_POOL + LIQUIDITY_POOL, + MUXED_CONTRACT } } diff --git a/src/main/java/org/stellar/sdk/Auth.java b/src/main/java/org/stellar/sdk/Auth.java index 994745d98..ae122463d 100644 --- a/src/main/java/org/stellar/sdk/Auth.java +++ b/src/main/java/org/stellar/sdk/Auth.java @@ -18,6 +18,7 @@ import org.stellar.sdk.xdr.HashIDPreimage; import org.stellar.sdk.xdr.Int64; import org.stellar.sdk.xdr.SCAddress; +import org.stellar.sdk.xdr.SCAddressType; import org.stellar.sdk.xdr.SCVal; import org.stellar.sdk.xdr.SorobanAddressCredentials; import org.stellar.sdk.xdr.SorobanAddressCredentialsWithDelegates; @@ -352,7 +353,7 @@ public static SorobanAuthorizationEntry authorizeEntry( if (forAddress == null) { addressCredentials.setSignature(signatureScVal); } else { - SCAddress forScAddress = new Address(forAddress).toSCAddress(); + SCAddress forScAddress = toAuthScAddress(forAddress); int filled = fillMatchingSignatureNodes(clone.getCredentials(), forScAddress, signatureScVal); if (filled == 0) { throw new IllegalArgumentException( @@ -537,7 +538,7 @@ public static SorobanAuthorizationEntry authorizeInvocation( long nonce = new SecureRandom().nextLong(); SorobanAddressCredentials addressCredentials = SorobanAddressCredentials.builder() - .address(new Address(address).toSCAddress()) + .address(toAuthScAddress(address)) .nonce(new Int64(nonce)) .signatureExpirationLedger(new Uint32(new XdrUnsignedInteger(validUntilLedgerSeq))) .signature(Scv.toVoid()) @@ -680,7 +681,8 @@ public static SCVal defaultAccountSignatureScVal(String accountId, byte[] signat * @param network the network whose id is mixed into the payload * @return the preimage to hash and sign * @throws IllegalArgumentException if {@code entry} carries source-account or otherwise - * non-address credentials + * non-address credentials, or if its credential address is not an account ({@code G...}) or + * contract ({@code C...}) */ public static HashIDPreimage buildAuthorizationEntryPreimage( SorobanAuthorizationEntry entry, long validUntilLedgerSeq, Network network) { @@ -691,6 +693,7 @@ public static HashIDPreimage buildAuthorizationEntryPreimage( "cannot build a signature payload for credentials type: " + credentials.getDiscriminant()); } + ensureAuthorizationScAddress(addressCredentials.getAddress()); Uint32 signatureExpirationLedger = new Uint32(new XdrUnsignedInteger(validUntilLedgerSeq)); switch (credentials.getDiscriminant()) { @@ -760,7 +763,9 @@ public static HashIDPreimage buildAuthorizationEntryPreimage( * placeholder, which is valid for accounts that authorize purely via delegated signers * @return a new {@code SOROBAN_CREDENTIALS_ADDRESS_WITH_DELEGATES} authorization entry * @throws IllegalArgumentException if {@code entry} does not carry {@code ADDRESS} / {@code - * ADDRESS_V2} credentials, or if any delegates list contains a duplicate address + * ADDRESS_V2} credentials, if any delegates list contains a duplicate address, or if the + * entry's address or any delegate address is not an account ({@code G...}) or contract + * ({@code C...}) */ public static SorobanAuthorizationEntry buildWithDelegatesEntry( SorobanAuthorizationEntry entry, @@ -775,6 +780,7 @@ public static SorobanAuthorizationEntry buildWithDelegatesEntry( + credentials.getDiscriminant()); } SorobanAddressCredentials addressCredentials = getAddressCredentials(credentials); + ensureAuthorizationScAddress(addressCredentials.getAddress()); return SorobanAuthorizationEntry.builder() .credentials( @@ -815,7 +821,7 @@ private static SorobanDelegateSignature[] buildDelegateNodes( for (DelegateSignature delegate : delegates) { SorobanDelegateSignature node = SorobanDelegateSignature.builder() - .address(new Address(delegate.getAddress()).toSCAddress()) + .address(toAuthScAddress(delegate.getAddress())) .signature(delegate.getSignature() != null ? delegate.getSignature() : Scv.toVoid()) .nestedDelegates(buildDelegateNodes(delegate.getNestedDelegates())) .build(); @@ -878,6 +884,31 @@ private static int fillMatchingDelegateNodes( return filled; } + private static final String AUTHORIZATION_ADDRESS_ERROR = + "Authorization address must be a classic account (G...) or contract (C...) address"; + + /** + * Converts an address that takes part in authorization to its {@link SCAddress}. Only accounts + * ({@code G...}) and contracts ({@code C...}) can authorize; the host rejects any other address + * type in credentials and delegate signers. + */ + private static SCAddress toAuthScAddress(String address) { + Address parsed = new Address(address); + if (parsed.getAddressType() != Address.AddressType.ACCOUNT + && parsed.getAddressType() != Address.AddressType.CONTRACT) { + throw new IllegalArgumentException(AUTHORIZATION_ADDRESS_ERROR + ", got " + address); + } + return parsed.toSCAddress(); + } + + private static void ensureAuthorizationScAddress(SCAddress address) { + if (address.getDiscriminant() != SCAddressType.SC_ADDRESS_TYPE_ACCOUNT + && address.getDiscriminant() != SCAddressType.SC_ADDRESS_TYPE_CONTRACT) { + throw new IllegalArgumentException( + AUTHORIZATION_ADDRESS_ERROR + ", got " + address.getDiscriminant()); + } + } + private static byte[] scAddressXdrBytes(SCAddress address) { try { return address.toXdrByteArray(); diff --git a/src/main/java/org/stellar/sdk/SorobanServer.java b/src/main/java/org/stellar/sdk/SorobanServer.java index 8c43e1c32..ef6c85877 100644 --- a/src/main/java/org/stellar/sdk/SorobanServer.java +++ b/src/main/java/org/stellar/sdk/SorobanServer.java @@ -75,6 +75,7 @@ import org.stellar.sdk.xdr.SCAddress; import org.stellar.sdk.xdr.SCAddressType; import org.stellar.sdk.xdr.SCContractInstance; +import org.stellar.sdk.xdr.SCMapEntry; import org.stellar.sdk.xdr.SCVal; import org.stellar.sdk.xdr.SCValType; import org.stellar.sdk.xdr.SorobanAuthorizationEntry; @@ -208,6 +209,8 @@ public GetFeeStatsResponse getFeeStats() { * {@link Durability#TEMPORARY} or {@link Durability#PERSISTENT}. * @return A {@link GetLedgerEntriesResponse.LedgerEntryResult} object containing the ledger entry * result. + * @throws IllegalArgumentException If {@code contractId} is not a contract address ({@code + * C...}), or {@code key} contains a muxed address ({@code M...} or {@code W...}). * @throws org.stellar.sdk.exception.NetworkException All the exceptions below are subclasses of * NetworkException * @throws SorobanRpcException If the Stellar RPC instance returns an error response. @@ -217,6 +220,14 @@ public GetFeeStatsResponse getFeeStats() { */ public Optional getContractData( String contractId, SCVal key, Durability durability) { + if (!StrKey.isValidContract(contractId)) { + throw new IllegalArgumentException("Invalid contract ID: " + contractId); + } + if (containsMuxedAddress(key)) { + // The host rejects muxed addresses anywhere in a storage key, so no such entry can exist. + throw new IllegalArgumentException( + "Contract data keys cannot contain muxed addresses (M... or W...)"); + } ContractDataDurability contractDataDurability; switch (durability) { @@ -1226,6 +1237,33 @@ private R sendRequest( } } + private static boolean containsMuxedAddress(SCVal val) { + switch (val.getDiscriminant()) { + case SCV_ADDRESS: + SCAddressType type = val.getAddress().getDiscriminant(); + return type == SCAddressType.SC_ADDRESS_TYPE_MUXED_ACCOUNT + || type == SCAddressType.SC_ADDRESS_TYPE_MUXED_CONTRACT; + case SCV_VEC: + if (val.getVec() != null) { + for (SCVal element : val.getVec().getSCVec()) { + if (containsMuxedAddress(element)) return true; + } + } + return false; + case SCV_MAP: + if (val.getMap() != null) { + for (SCMapEntry entry : val.getMap().getSCMap()) { + if (containsMuxedAddress(entry.getKey()) || containsMuxedAddress(entry.getVal())) { + return true; + } + } + } + return false; + default: + return false; + } + } + @Override public void close() throws IOException { this.httpClient.connectionPool().evictAll(); diff --git a/src/main/java/org/stellar/sdk/StrKey.java b/src/main/java/org/stellar/sdk/StrKey.java index b6ed2a625..e1916e075 100644 --- a/src/main/java/org/stellar/sdk/StrKey.java +++ b/src/main/java/org/stellar/sdk/StrKey.java @@ -344,6 +344,43 @@ public static boolean isValidMed25519PublicKey(String med25519PublicKey) { } } + /** + * Encodes raw data to strkey Stellar muxed contract address (W...) + * + * @param data data to encode, the 32-byte contract ID followed by the 8-byte big-endian ID + * @return "W..." representation of the muxed contract address + */ + public static String encodeMuxedContract(byte[] data) { + char[] encoded = encodeCheck(VersionByte.MUXED_CONTRACT, data); + return String.valueOf(encoded); + } + + /** + * Decodes strkey Stellar muxed contract address (W...) to raw bytes + * + * @param data data to decode + * @return raw bytes, the 32-byte contract ID followed by the 8-byte big-endian ID + */ + public static byte[] decodeMuxedContract(String data) { + return decodeCheck(VersionByte.MUXED_CONTRACT, data.toCharArray()); + } + + /** + * Checks validity of muxed contract address (W...). + * + * @param muxedContract the muxed contract address to check + * @return true if the given muxed contract address is a valid muxed contract address, false + * otherwise + */ + public static boolean isValidMuxedContract(String muxedContract) { + try { + decodeMuxedContract(muxedContract); + return true; + } catch (Exception e) { + return false; + } + } + static VersionByte decodeVersionByte(String data) { byte[] dataBytes = data.getBytes(StandardCharsets.UTF_8); byte[] decoded = base32decode(dataBytes); @@ -440,6 +477,7 @@ static byte[] decodeCheck(VersionByte versionByte, char[] encoded) { } break; case MED25519_PUBLIC_KEY: + case MUXED_CONTRACT: if (data.length != 32 + 8) { throw new IllegalArgumentException( "Invalid data length, expected 40 bytes, got " + data.length); @@ -454,6 +492,13 @@ static byte[] decodeCheck(VersionByte versionByte, char[] encoded) { throw new IllegalArgumentException( "Invalid data length, expected 33 bytes, got " + data.length); } + // SEP-23: the first byte is the claimable balance type, and CLAIMABLE_BALANCE_ID_TYPE_V0 + // is the only one. + if (data[0] != 0) { + throw new IllegalArgumentException( + "Invalid claimable balance type, expected 0 (CLAIMABLE_BALANCE_ID_TYPE_V0), got " + + data[0]); + } break; default: if (data.length != 32) { @@ -668,7 +713,8 @@ enum VersionByte { SIGNED_PAYLOAD((byte) (15 << 3)), // P CONTRACT((byte) (2 << 3)), // C LIQUIDITY_POOL((byte) (11 << 3)), // L - CLAIMABLE_BALANCE((byte) (1 << 3)); // B + CLAIMABLE_BALANCE((byte) (1 << 3)), // B + MUXED_CONTRACT((byte) (22 << 3)); // W private final byte value; diff --git a/src/main/java/org/stellar/sdk/TransactionBuilder.java b/src/main/java/org/stellar/sdk/TransactionBuilder.java index 4199bd3b7..658717639 100644 --- a/src/main/java/org/stellar/sdk/TransactionBuilder.java +++ b/src/main/java/org/stellar/sdk/TransactionBuilder.java @@ -446,9 +446,9 @@ public Transaction buildPaymentToContractTransaction( * SorobanDataBuilder.Resources(0L, 500L, 500L)} and {@code resourceFee} set to {@code * 4_000_000L}. * - * @param balanceOwner The owner of the asset, it should be the same as the `destination` address - * in the {@link #buildPaymentToContractTransaction(String, Asset, BigDecimal, - * SorobanDataBuilder.Resources, Long, String)} method. + * @param balanceOwner The contract address ({@code C...}) that owns the balance, it should be the + * same as the `destination` address in the {@link #buildPaymentToContractTransaction(String, + * Asset, BigDecimal, SorobanDataBuilder.Resources, Long, String)} method. * @param asset The asset * @param source The source account for the transaction. * @return The transaction. @@ -470,9 +470,9 @@ public Transaction buildRestoreAssetBalanceEntryTransaction( *

Under Protocol 22, if the entry really needs to be restored, then this method will consume * about 0.25 XLM in resource fees. * - * @param balanceOwner The owner of the asset, it should be the same as the `destination` address - * in the {@link #buildPaymentToContractTransaction(String, Asset, BigDecimal, - * SorobanDataBuilder.Resources, Long, String)} method. + * @param balanceOwner The contract address ({@code C...}) that owns the balance, it should be the + * same as the `destination` address in the {@link #buildPaymentToContractTransaction(String, + * Asset, BigDecimal, SorobanDataBuilder.Resources, Long, String)} method. * @param asset The asset * @param resources The resources required for the transaction. * @param resourceFee The maximum fee (in stroops) that can be paid for the transaction. @@ -485,6 +485,11 @@ public Transaction buildRestoreAssetBalanceEntryTransaction( SorobanDataBuilder.Resources resources, Long resourceFee, @Nullable String source) { + // Only contracts hold SAC balances in contract data; an account's balance is its trustline. + if (!StrKey.isValidContract(balanceOwner)) { + throw new IllegalArgumentException( + "balanceOwner is not a valid contract address: " + balanceOwner); + } String contractId = asset.getContractId(this.network); LedgerKey balanceLedgerKey = LedgerKey.builder() diff --git a/src/main/java/org/stellar/sdk/contract/ContractSpec.java b/src/main/java/org/stellar/sdk/contract/ContractSpec.java index 1d30c5033..e3d7f7baf 100644 --- a/src/main/java/org/stellar/sdk/contract/ContractSpec.java +++ b/src/main/java/org/stellar/sdk/contract/ContractSpec.java @@ -183,7 +183,7 @@ public Optional getEvent(String name) { return Optional.empty(); } for (SCSpecEventV0 event : getEvents()) { - if (name.equals(decodeSymbol(event.getName()))) { + if (name.equals(decodeString(event.getName()))) { return Optional.of(event); } } diff --git a/src/main/java/org/stellar/sdk/responses/effects/ContractCreditedEffectResponse.java b/src/main/java/org/stellar/sdk/responses/effects/ContractCreditedEffectResponse.java index d4fa948a5..df7b4d349 100644 --- a/src/main/java/org/stellar/sdk/responses/effects/ContractCreditedEffectResponse.java +++ b/src/main/java/org/stellar/sdk/responses/effects/ContractCreditedEffectResponse.java @@ -3,6 +3,7 @@ import static org.stellar.sdk.Asset.create; import com.google.gson.annotations.SerializedName; +import java.math.BigInteger; import lombok.EqualsAndHashCode; import lombok.Value; import org.stellar.sdk.Asset; @@ -30,6 +31,17 @@ public class ContractCreditedEffectResponse extends EffectResponse { String contract; + /** + * The muxed contract address ({@code W...}) the asset was credited to, or {@code null} if the + * destination was not muxed. {@link #getContract()} is then its base contract address (CAP-84). + */ + @SerializedName("contract_muxed") + String contractMuxed; + + /** The ID of {@link #getContractMuxed()}, or {@code null} if the destination was not muxed. */ + @SerializedName("contract_muxed_id") + BigInteger contractMuxedId; + String amount; public Asset getAsset() { diff --git a/src/main/java/org/stellar/sdk/responses/sorobanrpc/GetEventsResponse.java b/src/main/java/org/stellar/sdk/responses/sorobanrpc/GetEventsResponse.java index 3ad85538c..2504c9879 100644 --- a/src/main/java/org/stellar/sdk/responses/sorobanrpc/GetEventsResponse.java +++ b/src/main/java/org/stellar/sdk/responses/sorobanrpc/GetEventsResponse.java @@ -51,8 +51,14 @@ public static class EventInfo { /** The field can be parsed as {@link org.stellar.sdk.xdr.SCVal} object. */ String value; - /** The field will be removed in the next version. */ - Boolean inSuccessfulContractCall; + /** + * Whether the event was emitted during a successful contract call. + * + * @deprecated Stellar RPC no longer returns this field from {@code getEvents} (stellar-rpc#1050), so it is + * {@code null} with newer servers. It will be removed in the next major release. + */ + @Deprecated Boolean inSuccessfulContractCall; /** * Parses the {@code topic} field from a list of strings to a list of {@link diff --git a/src/main/java/org/stellar/sdk/scval/ScvComparator.java b/src/main/java/org/stellar/sdk/scval/ScvComparator.java index a0b7de5b2..63ce19a2d 100644 --- a/src/main/java/org/stellar/sdk/scval/ScvComparator.java +++ b/src/main/java/org/stellar/sdk/scval/ScvComparator.java @@ -262,6 +262,19 @@ static int compareScAddress(SCAddress a, SCAddress b) { return Util.compareBytesUnsigned( a.getLiquidityPoolId().getPoolID().getHash(), b.getLiquidityPoolId().getPoolID().getHash()); + case SC_ADDRESS_TYPE_MUXED_CONTRACT: + { + cmp = + a.getMuxedContract() + .getId() + .getUint64() + .getNumber() + .compareTo(b.getMuxedContract().getId().getUint64().getNumber()); + if (cmp != 0) return cmp; + return Util.compareBytesUnsigned( + a.getMuxedContract().getContractId().getContractID().getHash(), + b.getMuxedContract().getContractId().getContractID().getHash()); + } default: throw new IllegalArgumentException("Unsupported SCAddress type: " + a.getDiscriminant()); } diff --git a/src/main/java/org/stellar/sdk/xdr/Constants.java b/src/main/java/org/stellar/sdk/xdr/Constants.java index e0f8f7110..22d72cf6c 100644 --- a/src/main/java/org/stellar/sdk/xdr/Constants.java +++ b/src/main/java/org/stellar/sdk/xdr/Constants.java @@ -21,6 +21,7 @@ private Constants() {} public static final int MAX_SIGNERS = 20; public static final int SCSYMBOL_LIMIT = 32; public static final int SC_SPEC_DOC_LIMIT = 1024; + public static final int SC_SPEC_TYPE_NAME_LIMIT = 1024; public static final int TX_ADVERT_VECTOR_MAX_SIZE = 1000; public static final int TX_DEMAND_VECTOR_MAX_SIZE = 1000; } diff --git a/src/main/java/org/stellar/sdk/xdr/ContractCostType.java b/src/main/java/org/stellar/sdk/xdr/ContractCostType.java index 0a6b8816a..69d7dab4d 100644 --- a/src/main/java/org/stellar/sdk/xdr/ContractCostType.java +++ b/src/main/java/org/stellar/sdk/xdr/ContractCostType.java @@ -194,7 +194,25 @@ * // Cost of performing BN254 scalar element inversion * Bn254FrInv = 84, * // Cost of performing BN254 G1 multi-scalar multiplication (MSM) - * Bn254G1Msm = 85 + * Bn254G1Msm = 85, + * // Cost of decoding and expanding an ML-DSA-44 verifying key + * MlDsa44DecodeVerifyingKey = 86, + * // Cost of decoding and expanding an ML-DSA-65 verifying key + * MlDsa65DecodeVerifyingKey = 87, + * // Cost of decoding and expanding an ML-DSA-87 verifying key + * MlDsa87DecodeVerifyingKey = 88, + * // Cost of decoding an ML-DSA-44 signature + * MlDsa44DecodeSignature = 89, + * // Cost of decoding an ML-DSA-65 signature + * MlDsa65DecodeSignature = 90, + * // Cost of decoding an ML-DSA-87 signature + * MlDsa87DecodeSignature = 91, + * // Cost of verifying an ML-DSA-44 signature, linear in message + context length + * VerifyMlDsa44Sig = 92, + * // Cost of verifying an ML-DSA-65 signature, linear in message + context length + * VerifyMlDsa65Sig = 93, + * // Cost of verifying an ML-DSA-87 signature, linear in message + context length + * VerifyMlDsa87Sig = 94 * }; * */ @@ -284,7 +302,16 @@ public enum ContractCostType implements XdrElement { Bn254FrMul(82), Bn254FrPow(83), Bn254FrInv(84), - Bn254G1Msm(85); + Bn254G1Msm(85), + MlDsa44DecodeVerifyingKey(86), + MlDsa65DecodeVerifyingKey(87), + MlDsa87DecodeVerifyingKey(88), + MlDsa44DecodeSignature(89), + MlDsa65DecodeSignature(90), + MlDsa87DecodeSignature(91), + VerifyMlDsa44Sig(92), + VerifyMlDsa65Sig(93), + VerifyMlDsa87Sig(94); private final int value; @@ -473,6 +500,24 @@ public static ContractCostType decode(XdrDataInputStream stream, int maxDepth) return Bn254FrInv; case 85: return Bn254G1Msm; + case 86: + return MlDsa44DecodeVerifyingKey; + case 87: + return MlDsa65DecodeVerifyingKey; + case 88: + return MlDsa87DecodeVerifyingKey; + case 89: + return MlDsa44DecodeSignature; + case 90: + return MlDsa65DecodeSignature; + case 91: + return MlDsa87DecodeSignature; + case 92: + return VerifyMlDsa44Sig; + case 93: + return VerifyMlDsa65Sig; + case 94: + return VerifyMlDsa87Sig; default: throw new IllegalArgumentException("Unknown enum value: " + value); } @@ -681,6 +726,24 @@ Object toJsonObject() { return "bn254frinv"; case Bn254G1Msm: return "bn254g1msm"; + case MlDsa44DecodeVerifyingKey: + return "mldsa44decodeverifyingkey"; + case MlDsa65DecodeVerifyingKey: + return "mldsa65decodeverifyingkey"; + case MlDsa87DecodeVerifyingKey: + return "mldsa87decodeverifyingkey"; + case MlDsa44DecodeSignature: + return "mldsa44decodesignature"; + case MlDsa65DecodeSignature: + return "mldsa65decodesignature"; + case MlDsa87DecodeSignature: + return "mldsa87decodesignature"; + case VerifyMlDsa44Sig: + return "verifymldsa44sig"; + case VerifyMlDsa65Sig: + return "verifymldsa65sig"; + case VerifyMlDsa87Sig: + return "verifymldsa87sig"; default: throw new IllegalArgumentException("Unknown enum value: " + this.value); } @@ -861,6 +924,24 @@ static ContractCostType fromJsonObject(Object json) { return Bn254FrInv; case "bn254g1msm": return Bn254G1Msm; + case "mldsa44decodeverifyingkey": + return MlDsa44DecodeVerifyingKey; + case "mldsa65decodeverifyingkey": + return MlDsa65DecodeVerifyingKey; + case "mldsa87decodeverifyingkey": + return MlDsa87DecodeVerifyingKey; + case "mldsa44decodesignature": + return MlDsa44DecodeSignature; + case "mldsa65decodesignature": + return MlDsa65DecodeSignature; + case "mldsa87decodesignature": + return MlDsa87DecodeSignature; + case "verifymldsa44sig": + return VerifyMlDsa44Sig; + case "verifymldsa65sig": + return VerifyMlDsa65Sig; + case "verifymldsa87sig": + return VerifyMlDsa87Sig; default: throw new IllegalArgumentException("Unknown JSON value: " + value); } diff --git a/src/main/java/org/stellar/sdk/xdr/MuxedContract.java b/src/main/java/org/stellar/sdk/xdr/MuxedContract.java new file mode 100644 index 000000000..659ee6252 --- /dev/null +++ b/src/main/java/org/stellar/sdk/xdr/MuxedContract.java @@ -0,0 +1,103 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.LinkedHashMap; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.stellar.sdk.Base64Factory; + +/** + * MuxedContract's original definition in the XDR file is: + * + *

+ * struct MuxedContract
+ * {
+ *     uint64 id;
+ *     ContractID contractId;
+ * };
+ * 
+ */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder(toBuilder = true) +public class MuxedContract implements XdrElement { + /** + * Value of the {@code id} field. + * + * @param id the {@code id} field value + * @return the {@code id} field value + */ + private Uint64 id; + + /** + * Value of the {@code contractId} field. + * + * @param contractId the {@code contractId} field value + * @return the {@code contractId} field value + */ + private ContractID contractId; + + public void encode(XdrDataOutputStream stream) throws IOException { + id.encode(stream); + contractId.encode(stream); + } + + public static MuxedContract decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + MuxedContract decodedMuxedContract = new MuxedContract(); + decodedMuxedContract.id = Uint64.decode(stream, maxDepth); + decodedMuxedContract.contractId = ContractID.decode(stream, maxDepth); + return decodedMuxedContract; + } + + public static MuxedContract decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + public static MuxedContract fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static MuxedContract fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } + + @Override + public String toJson() { + return XdrElement.gson.toJson(toJsonObject()); + } + + public static MuxedContract fromJson(String json) { + return fromJsonObject(XdrElement.gson.fromJson(json, Object.class)); + } + + Object toJsonObject() { + LinkedHashMap jsonMap = new LinkedHashMap<>(); + jsonMap.put("id", id.toJsonObject()); + jsonMap.put("contract_id", contractId.toJsonObject()); + return jsonMap; + } + + @SuppressWarnings("unchecked") + static MuxedContract fromJsonObject(Object json) { + java.util.Map jsonMap = (java.util.Map) json; + MuxedContract instance = new MuxedContract(); + instance.id = Uint64.fromJsonObject(jsonMap.get("id")); + instance.contractId = ContractID.fromJsonObject(jsonMap.get("contract_id")); + return instance; + } +} diff --git a/src/main/java/org/stellar/sdk/xdr/SCAddress.java b/src/main/java/org/stellar/sdk/xdr/SCAddress.java index 76b0f874f..8a4ab6428 100644 --- a/src/main/java/org/stellar/sdk/xdr/SCAddress.java +++ b/src/main/java/org/stellar/sdk/xdr/SCAddress.java @@ -29,6 +29,8 @@ * ClaimableBalanceID claimableBalanceId; * case SC_ADDRESS_TYPE_LIQUIDITY_POOL: * PoolID liquidityPoolId; + * case SC_ADDRESS_TYPE_MUXED_CONTRACT: + * MuxedContract muxedContract; * }; * */ @@ -85,6 +87,14 @@ public class SCAddress implements XdrElement { */ private PoolID liquidityPoolId; + /** + * Value of the {@code muxedContract} field. + * + * @param muxedContract the {@code muxedContract} field value + * @return the {@code muxedContract} field value + */ + private MuxedContract muxedContract; + public void encode(XdrDataOutputStream stream) throws IOException { stream.writeInt(discriminant.getValue()); switch (discriminant) { @@ -103,6 +113,9 @@ public void encode(XdrDataOutputStream stream) throws IOException { case SC_ADDRESS_TYPE_LIQUIDITY_POOL: liquidityPoolId.encode(stream); break; + case SC_ADDRESS_TYPE_MUXED_CONTRACT: + muxedContract.encode(stream); + break; } } @@ -130,6 +143,9 @@ public static SCAddress decode(XdrDataInputStream stream, int maxDepth) throws I case SC_ADDRESS_TYPE_LIQUIDITY_POOL: decodedSCAddress.liquidityPoolId = PoolID.decode(stream, maxDepth); break; + case SC_ADDRESS_TYPE_MUXED_CONTRACT: + decodedSCAddress.muxedContract = MuxedContract.decode(stream, maxDepth); + break; default: throw new IOException("Unknown discriminant value: " + discriminant); } diff --git a/src/main/java/org/stellar/sdk/xdr/SCAddressType.java b/src/main/java/org/stellar/sdk/xdr/SCAddressType.java index ab54450d7..872871e4f 100644 --- a/src/main/java/org/stellar/sdk/xdr/SCAddressType.java +++ b/src/main/java/org/stellar/sdk/xdr/SCAddressType.java @@ -17,7 +17,8 @@ * SC_ADDRESS_TYPE_CONTRACT = 1, * SC_ADDRESS_TYPE_MUXED_ACCOUNT = 2, * SC_ADDRESS_TYPE_CLAIMABLE_BALANCE = 3, - * SC_ADDRESS_TYPE_LIQUIDITY_POOL = 4 + * SC_ADDRESS_TYPE_LIQUIDITY_POOL = 4, + * SC_ADDRESS_TYPE_MUXED_CONTRACT = 5 * }; * */ @@ -26,7 +27,8 @@ public enum SCAddressType implements XdrElement { SC_ADDRESS_TYPE_CONTRACT(1), SC_ADDRESS_TYPE_MUXED_ACCOUNT(2), SC_ADDRESS_TYPE_CLAIMABLE_BALANCE(3), - SC_ADDRESS_TYPE_LIQUIDITY_POOL(4); + SC_ADDRESS_TYPE_LIQUIDITY_POOL(4), + SC_ADDRESS_TYPE_MUXED_CONTRACT(5); private final int value; @@ -52,6 +54,8 @@ public static SCAddressType decode(XdrDataInputStream stream, int maxDepth) thro return SC_ADDRESS_TYPE_CLAIMABLE_BALANCE; case 4: return SC_ADDRESS_TYPE_LIQUIDITY_POOL; + case 5: + return SC_ADDRESS_TYPE_MUXED_CONTRACT; default: throw new IllegalArgumentException("Unknown enum value: " + value); } @@ -98,6 +102,8 @@ Object toJsonObject() { return "claimable_balance"; case SC_ADDRESS_TYPE_LIQUIDITY_POOL: return "liquidity_pool"; + case SC_ADDRESS_TYPE_MUXED_CONTRACT: + return "muxed_contract"; default: throw new IllegalArgumentException("Unknown enum value: " + this.value); } @@ -116,6 +122,8 @@ static SCAddressType fromJsonObject(Object json) { return SC_ADDRESS_TYPE_CLAIMABLE_BALANCE; case "liquidity_pool": return SC_ADDRESS_TYPE_LIQUIDITY_POOL; + case "muxed_contract": + return SC_ADDRESS_TYPE_MUXED_CONTRACT; default: throw new IllegalArgumentException("Unknown JSON value: " + value); } diff --git a/src/main/java/org/stellar/sdk/xdr/SCSpecEventV0.java b/src/main/java/org/stellar/sdk/xdr/SCSpecEventV0.java index 96e7a9162..f9e3ad93d 100644 --- a/src/main/java/org/stellar/sdk/xdr/SCSpecEventV0.java +++ b/src/main/java/org/stellar/sdk/xdr/SCSpecEventV0.java @@ -21,7 +21,7 @@ * { * string doc<SC_SPEC_DOC_LIMIT>; * string lib<80>; - * SCSymbol name; + * string name<SC_SPEC_TYPE_NAME_LIMIT>; * SCSymbol prefixTopics<2>; * SCSpecEventParamV0 params<>; * SCSpecEventDataFormat dataFormat; @@ -55,7 +55,7 @@ public class SCSpecEventV0 implements XdrElement { * @param name the {@code name} field value * @return the {@code name} field value */ - private SCSymbol name; + private XdrString name; /** * Value of the {@code prefixTopics} field. @@ -92,6 +92,10 @@ public void encode(XdrDataOutputStream stream) throws IOException { throw new IOException("lib size " + libSize + " exceeds max size 80"); } lib.encode(stream); + int nameSize = name.getBytes().length; + if (nameSize > 1024) { + throw new IOException("name size " + nameSize + " exceeds max size 1024"); + } name.encode(stream); int prefixTopicsSize = getPrefixTopics().length; if (prefixTopicsSize > 2) { @@ -117,7 +121,8 @@ public static SCSpecEventV0 decode(XdrDataInputStream stream, int maxDepth) thro SCSpecEventV0 decodedSCSpecEventV0 = new SCSpecEventV0(); decodedSCSpecEventV0.doc = XdrString.decode(stream, maxDepth, Constants.SC_SPEC_DOC_LIMIT); decodedSCSpecEventV0.lib = XdrString.decode(stream, maxDepth, 80); - decodedSCSpecEventV0.name = SCSymbol.decode(stream, maxDepth); + decodedSCSpecEventV0.name = + XdrString.decode(stream, maxDepth, Constants.SC_SPEC_TYPE_NAME_LIMIT); int prefixTopicsSize = stream.readInt(); if (prefixTopicsSize < 0) { throw new IOException("prefixTopics size " + prefixTopicsSize + " is negative"); @@ -201,7 +206,7 @@ static SCSpecEventV0 fromJsonObject(Object json) { SCSpecEventV0 instance = new SCSpecEventV0(); instance.doc = XdrString.fromJsonObject(jsonMap.get("doc")); instance.lib = XdrString.fromJsonObject(jsonMap.get("lib")); - instance.name = SCSymbol.fromJsonObject(jsonMap.get("name")); + instance.name = XdrString.fromJsonObject(jsonMap.get("name")); instance.prefixTopics = XdrElement.jsonArrayToArray( (List) jsonMap.get("prefix_topics"), diff --git a/src/main/java/org/stellar/sdk/xdr/SCSpecTypeUDT.java b/src/main/java/org/stellar/sdk/xdr/SCSpecTypeUDT.java index e0eedac3c..8f97f6b84 100644 --- a/src/main/java/org/stellar/sdk/xdr/SCSpecTypeUDT.java +++ b/src/main/java/org/stellar/sdk/xdr/SCSpecTypeUDT.java @@ -18,7 +18,7 @@ *
  * struct SCSpecTypeUDT
  * {
- *     string name<60>;
+ *     string name<SC_SPEC_TYPE_NAME_LIMIT>;
  * };
  * 
*/ @@ -37,8 +37,8 @@ public class SCSpecTypeUDT implements XdrElement { public void encode(XdrDataOutputStream stream) throws IOException { int nameSize = name.getBytes().length; - if (nameSize > 60) { - throw new IOException("name size " + nameSize + " exceeds max size 60"); + if (nameSize > 1024) { + throw new IOException("name size " + nameSize + " exceeds max size 1024"); } name.encode(stream); } @@ -49,7 +49,8 @@ public static SCSpecTypeUDT decode(XdrDataInputStream stream, int maxDepth) thro } maxDepth -= 1; SCSpecTypeUDT decodedSCSpecTypeUDT = new SCSpecTypeUDT(); - decodedSCSpecTypeUDT.name = XdrString.decode(stream, maxDepth, 60); + decodedSCSpecTypeUDT.name = + XdrString.decode(stream, maxDepth, Constants.SC_SPEC_TYPE_NAME_LIMIT); return decodedSCSpecTypeUDT; } diff --git a/src/main/java/org/stellar/sdk/xdr/SCSpecUDTEnumV0.java b/src/main/java/org/stellar/sdk/xdr/SCSpecUDTEnumV0.java index 0b870bb4c..085f3b248 100644 --- a/src/main/java/org/stellar/sdk/xdr/SCSpecUDTEnumV0.java +++ b/src/main/java/org/stellar/sdk/xdr/SCSpecUDTEnumV0.java @@ -21,7 +21,7 @@ * { * string doc<SC_SPEC_DOC_LIMIT>; * string lib<80>; - * string name<60>; + * string name<SC_SPEC_TYPE_NAME_LIMIT>; * SCSpecUDTEnumCaseV0 cases<>; * }; * @@ -75,8 +75,8 @@ public void encode(XdrDataOutputStream stream) throws IOException { } lib.encode(stream); int nameSize = name.getBytes().length; - if (nameSize > 60) { - throw new IOException("name size " + nameSize + " exceeds max size 60"); + if (nameSize > 1024) { + throw new IOException("name size " + nameSize + " exceeds max size 1024"); } name.encode(stream); int casesSize = getCases().length; @@ -94,7 +94,8 @@ public static SCSpecUDTEnumV0 decode(XdrDataInputStream stream, int maxDepth) th SCSpecUDTEnumV0 decodedSCSpecUDTEnumV0 = new SCSpecUDTEnumV0(); decodedSCSpecUDTEnumV0.doc = XdrString.decode(stream, maxDepth, Constants.SC_SPEC_DOC_LIMIT); decodedSCSpecUDTEnumV0.lib = XdrString.decode(stream, maxDepth, 80); - decodedSCSpecUDTEnumV0.name = XdrString.decode(stream, maxDepth, 60); + decodedSCSpecUDTEnumV0.name = + XdrString.decode(stream, maxDepth, Constants.SC_SPEC_TYPE_NAME_LIMIT); int casesSize = stream.readInt(); if (casesSize < 0) { throw new IOException("cases size " + casesSize + " is negative"); diff --git a/src/main/java/org/stellar/sdk/xdr/SCSpecUDTErrorEnumV0.java b/src/main/java/org/stellar/sdk/xdr/SCSpecUDTErrorEnumV0.java index e962762c5..87e9c4b45 100644 --- a/src/main/java/org/stellar/sdk/xdr/SCSpecUDTErrorEnumV0.java +++ b/src/main/java/org/stellar/sdk/xdr/SCSpecUDTErrorEnumV0.java @@ -21,7 +21,7 @@ * { * string doc<SC_SPEC_DOC_LIMIT>; * string lib<80>; - * string name<60>; + * string name<SC_SPEC_TYPE_NAME_LIMIT>; * SCSpecUDTErrorEnumCaseV0 cases<>; * }; * @@ -75,8 +75,8 @@ public void encode(XdrDataOutputStream stream) throws IOException { } lib.encode(stream); int nameSize = name.getBytes().length; - if (nameSize > 60) { - throw new IOException("name size " + nameSize + " exceeds max size 60"); + if (nameSize > 1024) { + throw new IOException("name size " + nameSize + " exceeds max size 1024"); } name.encode(stream); int casesSize = getCases().length; @@ -96,7 +96,8 @@ public static SCSpecUDTErrorEnumV0 decode(XdrDataInputStream stream, int maxDept decodedSCSpecUDTErrorEnumV0.doc = XdrString.decode(stream, maxDepth, Constants.SC_SPEC_DOC_LIMIT); decodedSCSpecUDTErrorEnumV0.lib = XdrString.decode(stream, maxDepth, 80); - decodedSCSpecUDTErrorEnumV0.name = XdrString.decode(stream, maxDepth, 60); + decodedSCSpecUDTErrorEnumV0.name = + XdrString.decode(stream, maxDepth, Constants.SC_SPEC_TYPE_NAME_LIMIT); int casesSize = stream.readInt(); if (casesSize < 0) { throw new IOException("cases size " + casesSize + " is negative"); diff --git a/src/main/java/org/stellar/sdk/xdr/SCSpecUDTStructV0.java b/src/main/java/org/stellar/sdk/xdr/SCSpecUDTStructV0.java index d17f8b562..8193b37ce 100644 --- a/src/main/java/org/stellar/sdk/xdr/SCSpecUDTStructV0.java +++ b/src/main/java/org/stellar/sdk/xdr/SCSpecUDTStructV0.java @@ -21,7 +21,7 @@ * { * string doc<SC_SPEC_DOC_LIMIT>; * string lib<80>; - * string name<60>; + * string name<SC_SPEC_TYPE_NAME_LIMIT>; * SCSpecUDTStructFieldV0 fields<>; * }; * @@ -75,8 +75,8 @@ public void encode(XdrDataOutputStream stream) throws IOException { } lib.encode(stream); int nameSize = name.getBytes().length; - if (nameSize > 60) { - throw new IOException("name size " + nameSize + " exceeds max size 60"); + if (nameSize > 1024) { + throw new IOException("name size " + nameSize + " exceeds max size 1024"); } name.encode(stream); int fieldsSize = getFields().length; @@ -95,7 +95,8 @@ public static SCSpecUDTStructV0 decode(XdrDataInputStream stream, int maxDepth) SCSpecUDTStructV0 decodedSCSpecUDTStructV0 = new SCSpecUDTStructV0(); decodedSCSpecUDTStructV0.doc = XdrString.decode(stream, maxDepth, Constants.SC_SPEC_DOC_LIMIT); decodedSCSpecUDTStructV0.lib = XdrString.decode(stream, maxDepth, 80); - decodedSCSpecUDTStructV0.name = XdrString.decode(stream, maxDepth, 60); + decodedSCSpecUDTStructV0.name = + XdrString.decode(stream, maxDepth, Constants.SC_SPEC_TYPE_NAME_LIMIT); int fieldsSize = stream.readInt(); if (fieldsSize < 0) { throw new IOException("fields size " + fieldsSize + " is negative"); diff --git a/src/main/java/org/stellar/sdk/xdr/SCSpecUDTUnionV0.java b/src/main/java/org/stellar/sdk/xdr/SCSpecUDTUnionV0.java index a39b89676..676dc2317 100644 --- a/src/main/java/org/stellar/sdk/xdr/SCSpecUDTUnionV0.java +++ b/src/main/java/org/stellar/sdk/xdr/SCSpecUDTUnionV0.java @@ -21,7 +21,7 @@ * { * string doc<SC_SPEC_DOC_LIMIT>; * string lib<80>; - * string name<60>; + * string name<SC_SPEC_TYPE_NAME_LIMIT>; * SCSpecUDTUnionCaseV0 cases<>; * }; * @@ -75,8 +75,8 @@ public void encode(XdrDataOutputStream stream) throws IOException { } lib.encode(stream); int nameSize = name.getBytes().length; - if (nameSize > 60) { - throw new IOException("name size " + nameSize + " exceeds max size 60"); + if (nameSize > 1024) { + throw new IOException("name size " + nameSize + " exceeds max size 1024"); } name.encode(stream); int casesSize = getCases().length; @@ -95,7 +95,8 @@ public static SCSpecUDTUnionV0 decode(XdrDataInputStream stream, int maxDepth) SCSpecUDTUnionV0 decodedSCSpecUDTUnionV0 = new SCSpecUDTUnionV0(); decodedSCSpecUDTUnionV0.doc = XdrString.decode(stream, maxDepth, Constants.SC_SPEC_DOC_LIMIT); decodedSCSpecUDTUnionV0.lib = XdrString.decode(stream, maxDepth, 80); - decodedSCSpecUDTUnionV0.name = XdrString.decode(stream, maxDepth, 60); + decodedSCSpecUDTUnionV0.name = + XdrString.decode(stream, maxDepth, Constants.SC_SPEC_TYPE_NAME_LIMIT); int casesSize = stream.readInt(); if (casesSize < 0) { throw new IOException("cases size " + casesSize + " is negative"); diff --git a/src/main/java/org/stellar/sdk/xdr/StellarValue.java b/src/main/java/org/stellar/sdk/xdr/StellarValue.java index 3b2d3fe8f..d185568d8 100644 --- a/src/main/java/org/stellar/sdk/xdr/StellarValue.java +++ b/src/main/java/org/stellar/sdk/xdr/StellarValue.java @@ -44,6 +44,21 @@ * uint32 previousLedgerVersion; * LedgerCloseValueSignature lcValueSignature; * } proposedValue; + * case STELLAR_VALUE_SIGNED_MS: + * struct + * { + * TimePointMs closeTimeMs; // closeTime == closeTimeMs / 1000 + * LedgerCloseValueSignature lcValueSignature; + * } signedMsValue; + * case STELLAR_VALUE_EMPTY_TX_SET_MS: + * struct + * { + * TimePointMs closeTimeMs; // closeTime == closeTimeMs / 1000 + * Hash txSetHash; + * Hash previousLedgerHash; + * uint32 previousLedgerVersion; + * LedgerCloseValueSignature lcValueSignature; + * } proposedMsValue; * } * ext; * }; @@ -198,6 +213,21 @@ static StellarValue fromJsonObject(Object json) { * uint32 previousLedgerVersion; * LedgerCloseValueSignature lcValueSignature; * } proposedValue; + * case STELLAR_VALUE_SIGNED_MS: + * struct + * { + * TimePointMs closeTimeMs; // closeTime == closeTimeMs / 1000 + * LedgerCloseValueSignature lcValueSignature; + * } signedMsValue; + * case STELLAR_VALUE_EMPTY_TX_SET_MS: + * struct + * { + * TimePointMs closeTimeMs; // closeTime == closeTimeMs / 1000 + * Hash txSetHash; + * Hash previousLedgerHash; + * uint32 previousLedgerVersion; + * LedgerCloseValueSignature lcValueSignature; + * } proposedMsValue; * } * */ @@ -230,6 +260,22 @@ public static class StellarValueExt implements XdrElement { */ private StellarValueProposedValue proposedValue; + /** + * Value of the {@code signedMsValue} field. + * + * @param signedMsValue the {@code signedMsValue} field value + * @return the {@code signedMsValue} field value + */ + private StellarValueSignedMsValue signedMsValue; + + /** + * Value of the {@code proposedMsValue} field. + * + * @param proposedMsValue the {@code proposedMsValue} field value + * @return the {@code proposedMsValue} field value + */ + private StellarValueProposedMsValue proposedMsValue; + public void encode(XdrDataOutputStream stream) throws IOException { stream.writeInt(discriminant.getValue()); switch (discriminant) { @@ -241,6 +287,12 @@ public void encode(XdrDataOutputStream stream) throws IOException { case STELLAR_VALUE_EMPTY_TX_SET: proposedValue.encode(stream); break; + case STELLAR_VALUE_SIGNED_MS: + signedMsValue.encode(stream); + break; + case STELLAR_VALUE_EMPTY_TX_SET_MS: + proposedMsValue.encode(stream); + break; } } @@ -263,6 +315,13 @@ public static StellarValueExt decode(XdrDataInputStream stream, int maxDepth) case STELLAR_VALUE_EMPTY_TX_SET: decodedStellarValueExt.proposedValue = StellarValueProposedValue.decode(stream, maxDepth); break; + case STELLAR_VALUE_SIGNED_MS: + decodedStellarValueExt.signedMsValue = StellarValueSignedMsValue.decode(stream, maxDepth); + break; + case STELLAR_VALUE_EMPTY_TX_SET_MS: + decodedStellarValueExt.proposedMsValue = + StellarValueProposedMsValue.decode(stream, maxDepth); + break; default: throw new IOException("Unknown discriminant value: " + discriminant); } @@ -308,6 +367,16 @@ Object toJsonObject() { jsonMap.put("empty_tx_set", proposedValue.toJsonObject()); return jsonMap; } + if (discriminant == StellarValueType.STELLAR_VALUE_SIGNED_MS) { + LinkedHashMap jsonMap = new LinkedHashMap<>(); + jsonMap.put("signed_ms", signedMsValue.toJsonObject()); + return jsonMap; + } + if (discriminant == StellarValueType.STELLAR_VALUE_EMPTY_TX_SET_MS) { + LinkedHashMap jsonMap = new LinkedHashMap<>(); + jsonMap.put("empty_tx_set_ms", proposedMsValue.toJsonObject()); + return jsonMap; + } throw new IllegalArgumentException("Unknown discriminant: " + discriminant); } @@ -347,6 +416,19 @@ static StellarValueExt fromJsonObject(Object json) { StellarValueProposedValue.fromJsonObject(jsonMap.get("empty_tx_set")); return instance; } + if (key.equals("signed_ms")) { + StellarValueExt instance = new StellarValueExt(); + instance.discriminant = discriminant; + instance.signedMsValue = StellarValueSignedMsValue.fromJsonObject(jsonMap.get("signed_ms")); + return instance; + } + if (key.equals("empty_tx_set_ms")) { + StellarValueExt instance = new StellarValueExt(); + instance.discriminant = discriminant; + instance.proposedMsValue = + StellarValueProposedMsValue.fromJsonObject(jsonMap.get("empty_tx_set_ms")); + return instance; + } throw new IllegalArgumentException("Unknown key '" + key + "' for StellarValueExt"); } @@ -470,5 +552,234 @@ static StellarValueProposedValue fromJsonObject(Object json) { return instance; } } + + /** + * StellarValueSignedMsValue's original definition in the XDR file is: + * + *
+     * struct
+     *         {
+     *             TimePointMs closeTimeMs; // closeTime == closeTimeMs / 1000
+     *             LedgerCloseValueSignature lcValueSignature;
+     *         }
+     * 
+ */ + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder(toBuilder = true) + public static class StellarValueSignedMsValue implements XdrElement { + /** + * Value of the {@code closeTimeMs} field. + * + * @param closeTimeMs the {@code closeTimeMs} field value + * @return the {@code closeTimeMs} field value + */ + private TimePointMs closeTimeMs; + + /** + * Value of the {@code lcValueSignature} field. + * + * @param lcValueSignature the {@code lcValueSignature} field value + * @return the {@code lcValueSignature} field value + */ + private LedgerCloseValueSignature lcValueSignature; + + public void encode(XdrDataOutputStream stream) throws IOException { + closeTimeMs.encode(stream); + lcValueSignature.encode(stream); + } + + public static StellarValueSignedMsValue decode(XdrDataInputStream stream, int maxDepth) + throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + StellarValueSignedMsValue decodedStellarValueSignedMsValue = + new StellarValueSignedMsValue(); + decodedStellarValueSignedMsValue.closeTimeMs = TimePointMs.decode(stream, maxDepth); + decodedStellarValueSignedMsValue.lcValueSignature = + LedgerCloseValueSignature.decode(stream, maxDepth); + return decodedStellarValueSignedMsValue; + } + + public static StellarValueSignedMsValue decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + public static StellarValueSignedMsValue fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static StellarValueSignedMsValue fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } + + @Override + public String toJson() { + return XdrElement.gson.toJson(toJsonObject()); + } + + public static StellarValueSignedMsValue fromJson(String json) { + return fromJsonObject(XdrElement.gson.fromJson(json, Object.class)); + } + + Object toJsonObject() { + LinkedHashMap jsonMap = new LinkedHashMap<>(); + jsonMap.put("close_time_ms", closeTimeMs.toJsonObject()); + jsonMap.put("lc_value_signature", lcValueSignature.toJsonObject()); + return jsonMap; + } + + @SuppressWarnings("unchecked") + static StellarValueSignedMsValue fromJsonObject(Object json) { + java.util.Map jsonMap = (java.util.Map) json; + StellarValueSignedMsValue instance = new StellarValueSignedMsValue(); + instance.closeTimeMs = TimePointMs.fromJsonObject(jsonMap.get("close_time_ms")); + instance.lcValueSignature = + LedgerCloseValueSignature.fromJsonObject(jsonMap.get("lc_value_signature")); + return instance; + } + } + + /** + * StellarValueProposedMsValue's original definition in the XDR file is: + * + *
+     * struct
+     *         {
+     *             TimePointMs closeTimeMs; // closeTime == closeTimeMs / 1000
+     *             Hash txSetHash;
+     *             Hash previousLedgerHash;
+     *             uint32 previousLedgerVersion;
+     *             LedgerCloseValueSignature lcValueSignature;
+     *         }
+     * 
+ */ + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder(toBuilder = true) + public static class StellarValueProposedMsValue implements XdrElement { + /** + * Value of the {@code closeTimeMs} field. + * + * @param closeTimeMs the {@code closeTimeMs} field value + * @return the {@code closeTimeMs} field value + */ + private TimePointMs closeTimeMs; + + /** + * Value of the {@code txSetHash} field. + * + * @param txSetHash the {@code txSetHash} field value + * @return the {@code txSetHash} field value + */ + private Hash txSetHash; + + /** + * Value of the {@code previousLedgerHash} field. + * + * @param previousLedgerHash the {@code previousLedgerHash} field value + * @return the {@code previousLedgerHash} field value + */ + private Hash previousLedgerHash; + + /** + * Value of the {@code previousLedgerVersion} field. + * + * @param previousLedgerVersion the {@code previousLedgerVersion} field value + * @return the {@code previousLedgerVersion} field value + */ + private Uint32 previousLedgerVersion; + + /** + * Value of the {@code lcValueSignature} field. + * + * @param lcValueSignature the {@code lcValueSignature} field value + * @return the {@code lcValueSignature} field value + */ + private LedgerCloseValueSignature lcValueSignature; + + public void encode(XdrDataOutputStream stream) throws IOException { + closeTimeMs.encode(stream); + txSetHash.encode(stream); + previousLedgerHash.encode(stream); + previousLedgerVersion.encode(stream); + lcValueSignature.encode(stream); + } + + public static StellarValueProposedMsValue decode(XdrDataInputStream stream, int maxDepth) + throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + StellarValueProposedMsValue decodedStellarValueProposedMsValue = + new StellarValueProposedMsValue(); + decodedStellarValueProposedMsValue.closeTimeMs = TimePointMs.decode(stream, maxDepth); + decodedStellarValueProposedMsValue.txSetHash = Hash.decode(stream, maxDepth); + decodedStellarValueProposedMsValue.previousLedgerHash = Hash.decode(stream, maxDepth); + decodedStellarValueProposedMsValue.previousLedgerVersion = Uint32.decode(stream, maxDepth); + decodedStellarValueProposedMsValue.lcValueSignature = + LedgerCloseValueSignature.decode(stream, maxDepth); + return decodedStellarValueProposedMsValue; + } + + public static StellarValueProposedMsValue decode(XdrDataInputStream stream) + throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + public static StellarValueProposedMsValue fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static StellarValueProposedMsValue fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } + + @Override + public String toJson() { + return XdrElement.gson.toJson(toJsonObject()); + } + + public static StellarValueProposedMsValue fromJson(String json) { + return fromJsonObject(XdrElement.gson.fromJson(json, Object.class)); + } + + Object toJsonObject() { + LinkedHashMap jsonMap = new LinkedHashMap<>(); + jsonMap.put("close_time_ms", closeTimeMs.toJsonObject()); + jsonMap.put("tx_set_hash", txSetHash.toJsonObject()); + jsonMap.put("previous_ledger_hash", previousLedgerHash.toJsonObject()); + jsonMap.put("previous_ledger_version", previousLedgerVersion.toJsonObject()); + jsonMap.put("lc_value_signature", lcValueSignature.toJsonObject()); + return jsonMap; + } + + @SuppressWarnings("unchecked") + static StellarValueProposedMsValue fromJsonObject(Object json) { + java.util.Map jsonMap = (java.util.Map) json; + StellarValueProposedMsValue instance = new StellarValueProposedMsValue(); + instance.closeTimeMs = TimePointMs.fromJsonObject(jsonMap.get("close_time_ms")); + instance.txSetHash = Hash.fromJsonObject(jsonMap.get("tx_set_hash")); + instance.previousLedgerHash = Hash.fromJsonObject(jsonMap.get("previous_ledger_hash")); + instance.previousLedgerVersion = + Uint32.fromJsonObject(jsonMap.get("previous_ledger_version")); + instance.lcValueSignature = + LedgerCloseValueSignature.fromJsonObject(jsonMap.get("lc_value_signature")); + return instance; + } + } } } diff --git a/src/main/java/org/stellar/sdk/xdr/StellarValueType.java b/src/main/java/org/stellar/sdk/xdr/StellarValueType.java index 04007dd20..eee8a4b1f 100644 --- a/src/main/java/org/stellar/sdk/xdr/StellarValueType.java +++ b/src/main/java/org/stellar/sdk/xdr/StellarValueType.java @@ -15,14 +15,18 @@ * { * STELLAR_VALUE_BASIC = 0, * STELLAR_VALUE_SIGNED = 1, - * STELLAR_VALUE_EMPTY_TX_SET = 2 + * STELLAR_VALUE_EMPTY_TX_SET = 2, + * STELLAR_VALUE_SIGNED_MS = 3, + * STELLAR_VALUE_EMPTY_TX_SET_MS = 4 * }; * */ public enum StellarValueType implements XdrElement { STELLAR_VALUE_BASIC(0), STELLAR_VALUE_SIGNED(1), - STELLAR_VALUE_EMPTY_TX_SET(2); + STELLAR_VALUE_EMPTY_TX_SET(2), + STELLAR_VALUE_SIGNED_MS(3), + STELLAR_VALUE_EMPTY_TX_SET_MS(4); private final int value; @@ -45,6 +49,10 @@ public static StellarValueType decode(XdrDataInputStream stream, int maxDepth) return STELLAR_VALUE_SIGNED; case 2: return STELLAR_VALUE_EMPTY_TX_SET; + case 3: + return STELLAR_VALUE_SIGNED_MS; + case 4: + return STELLAR_VALUE_EMPTY_TX_SET_MS; default: throw new IllegalArgumentException("Unknown enum value: " + value); } @@ -87,6 +95,10 @@ Object toJsonObject() { return "signed"; case STELLAR_VALUE_EMPTY_TX_SET: return "empty_tx_set"; + case STELLAR_VALUE_SIGNED_MS: + return "signed_ms"; + case STELLAR_VALUE_EMPTY_TX_SET_MS: + return "empty_tx_set_ms"; default: throw new IllegalArgumentException("Unknown enum value: " + this.value); } @@ -101,6 +113,10 @@ static StellarValueType fromJsonObject(Object json) { return STELLAR_VALUE_SIGNED; case "empty_tx_set": return STELLAR_VALUE_EMPTY_TX_SET; + case "signed_ms": + return STELLAR_VALUE_SIGNED_MS; + case "empty_tx_set_ms": + return STELLAR_VALUE_EMPTY_TX_SET_MS; default: throw new IllegalArgumentException("Unknown JSON value: " + value); } diff --git a/src/main/java/org/stellar/sdk/xdr/TimePointMs.java b/src/main/java/org/stellar/sdk/xdr/TimePointMs.java new file mode 100644 index 000000000..f97f4c4e0 --- /dev/null +++ b/src/main/java/org/stellar/sdk/xdr/TimePointMs.java @@ -0,0 +1,80 @@ +// Automatically generated by xdrgen +// DO NOT EDIT or your changes may be overwritten + +package org.stellar.sdk.xdr; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.stellar.sdk.Base64Factory; + +/** + * TimePointMs's original definition in the XDR file is: + * + *
+ * typedef uint64 TimePointMs;
+ * 
+ */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class TimePointMs implements XdrElement { + /** + * Value of the {@code TimePointMs} field. + * + * @param TimePointMs the {@code TimePointMs} field value + * @return the {@code TimePointMs} field value + */ + private Uint64 TimePointMs; + + public void encode(XdrDataOutputStream stream) throws IOException { + TimePointMs.encode(stream); + } + + public static TimePointMs decode(XdrDataInputStream stream, int maxDepth) throws IOException { + if (maxDepth <= 0) { + throw new IOException("Maximum decoding depth reached"); + } + maxDepth -= 1; + TimePointMs decodedTimePointMs = new TimePointMs(); + decodedTimePointMs.TimePointMs = Uint64.decode(stream, maxDepth); + return decodedTimePointMs; + } + + public static TimePointMs decode(XdrDataInputStream stream) throws IOException { + return decode(stream, XdrDataInputStream.DEFAULT_MAX_DEPTH); + } + + public static TimePointMs fromXdrBase64(String xdr) throws IOException { + byte[] bytes = Base64Factory.getInstance().decode(xdr); + return fromXdrByteArray(bytes); + } + + public static TimePointMs fromXdrByteArray(byte[] xdr) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xdr); + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(byteArrayInputStream); + xdrDataInputStream.setMaxInputLen(xdr.length); + return decode(xdrDataInputStream); + } + + @Override + public String toJson() { + return XdrElement.gson.toJson(toJsonObject()); + } + + public static TimePointMs fromJson(String json) { + return fromJsonObject(XdrElement.gson.fromJson(json, Object.class)); + } + + Object toJsonObject() { + return TimePointMs.toJsonObject(); + } + + static TimePointMs fromJsonObject(Object json) { + TimePointMs instance = new TimePointMs(); + instance.TimePointMs = Uint64.fromJsonObject(json); + return instance; + } +} diff --git a/src/test/java/org/stellar/sdk/AuthTest.java b/src/test/java/org/stellar/sdk/AuthTest.java index 50831fe48..477f0161f 100644 --- a/src/test/java/org/stellar/sdk/AuthTest.java +++ b/src/test/java/org/stellar/sdk/AuthTest.java @@ -1236,6 +1236,119 @@ public void testBuildWithDelegatesEntrySortsDelegatesByAddress() throws IOExcept assertEquals(new Address(gHigh).toSCAddress(), nested[1].getAddress()); } + private static final String MUXED_CONTRACT_ADDRESS = + "WA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAAAAAAAAAWWC"; + + @Test + public void testAuthorizeInvocationRejectsMuxedContractAddress() { + KeyPair signer = + KeyPair.fromSecretSeed("SAEZSI6DY7AXJFIYA4PM6SIBNEYYXIEM2MSOTHFGKHDW32MBQ7KVO6EN"); + Auth.Signer entrySigner = + preimage -> + Auth.defaultAccountSignatureScVal( + signer.getAccountId(), signer.sign(Auth.authorizationPayloadHash(preimage))); + try { + Auth.authorizeInvocation( + entrySigner, MUXED_CONTRACT_ADDRESS, 654656L, buildInvocation(), Network.TESTNET); + fail(); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("Authorization address must be a classic account")); + } + } + + @Test + public void testAuthorizeInvocationRejectsMuxedAccountAddress() { + KeyPair signer = + KeyPair.fromSecretSeed("SAEZSI6DY7AXJFIYA4PM6SIBNEYYXIEM2MSOTHFGKHDW32MBQ7KVO6EN"); + Auth.Signer entrySigner = + preimage -> + Auth.defaultAccountSignatureScVal( + signer.getAccountId(), signer.sign(Auth.authorizationPayloadHash(preimage))); + String muxedAccount = + new MuxedAccount(signer.getAccountId(), java.math.BigInteger.valueOf(42)).getAddress(); + try { + Auth.authorizeInvocation( + entrySigner, muxedAccount, 654656L, buildInvocation(), Network.TESTNET); + fail(); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("Authorization address must be a classic account")); + } + } + + @Test + public void testSignAuthorizeEntryWithMuxedContractCredentialsThrows() { + KeyPair signer = + KeyPair.fromSecretSeed("SAEZSI6DY7AXJFIYA4PM6SIBNEYYXIEM2MSOTHFGKHDW32MBQ7KVO6EN"); + SorobanAuthorizationEntry entry = + SorobanAuthorizationEntry.builder() + .credentials( + buildAddressV2Credentials(new Address(MUXED_CONTRACT_ADDRESS).toSCAddress(), 0L)) + .rootInvocation(buildInvocation()) + .build(); + try { + Auth.authorizeEntry(entry, signer, 654656L, Network.TESTNET); + fail(); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("Authorization address must be a classic account")); + } + } + + @Test + public void testBuildWithDelegatesEntryRejectsMuxedContractCredentials() { + SorobanAuthorizationEntry entry = + SorobanAuthorizationEntry.builder() + .credentials( + buildAddressV2Credentials(new Address(MUXED_CONTRACT_ADDRESS).toSCAddress(), 0L)) + .rootInvocation(buildInvocation()) + .build(); + try { + Auth.buildWithDelegatesEntry(entry, 654656L, Collections.emptyList(), null); + fail(); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("Authorization address must be a classic account")); + } + } + + @Test + public void testSignAuthorizeEntryWithMuxedContractForAddressThrows() { + KeyPair signer = + KeyPair.fromSecretSeed("SAEZSI6DY7AXJFIYA4PM6SIBNEYYXIEM2MSOTHFGKHDW32MBQ7KVO6EN"); + SorobanAuthorizationEntry entry = + SorobanAuthorizationEntry.builder() + .credentials( + buildAddressV2Credentials(new Address(signer.getAccountId()).toSCAddress(), 0L)) + .rootInvocation(buildInvocation()) + .build(); + try { + Auth.authorizeEntry(entry, signer, 654656L, Network.TESTNET, MUXED_CONTRACT_ADDRESS); + fail(); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("Authorization address must be a classic account")); + } + } + + @Test + public void testBuildWithDelegatesEntryRejectsMuxedContractDelegate() { + KeyPair signer = + KeyPair.fromSecretSeed("SAEZSI6DY7AXJFIYA4PM6SIBNEYYXIEM2MSOTHFGKHDW32MBQ7KVO6EN"); + SorobanAuthorizationEntry entry = + SorobanAuthorizationEntry.builder() + .credentials( + buildAddressV2Credentials(new Address(signer.getAccountId()).toSCAddress(), 0L)) + .rootInvocation(buildInvocation()) + .build(); + try { + Auth.buildWithDelegatesEntry( + entry, + 654656L, + Collections.singletonList(new Auth.DelegateSignature(MUXED_CONTRACT_ADDRESS, null, null)), + null); + fail(); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("Authorization address must be a classic account")); + } + } + @Test public void testBuildWithDelegatesEntryWithDuplicateDelegateAddressThrows() { KeyPair signer = diff --git a/src/test/java/org/stellar/sdk/IntegrationUtils.java b/src/test/java/org/stellar/sdk/IntegrationUtils.java index 2c8fc3c46..d297477de 100644 --- a/src/test/java/org/stellar/sdk/IntegrationUtils.java +++ b/src/test/java/org/stellar/sdk/IntegrationUtils.java @@ -14,13 +14,13 @@ import org.stellar.sdk.operations.ChangeTrustOperation; import org.stellar.sdk.operations.InvokeHostFunctionOperation; import org.stellar.sdk.operations.PaymentOperation; -import org.stellar.sdk.responses.AccountResponse; +import org.stellar.sdk.responses.sorobanrpc.GetTransactionResponse; +import org.stellar.sdk.responses.sorobanrpc.SendTransactionResponse; import org.stellar.sdk.scval.Scv; import org.stellar.sdk.xdr.SCVal; public class IntegrationUtils { public static final String RPC_URL = "http://127.0.0.1:8000/rpc"; - public static final String HORIZON_URL = "http://127.0.0.1:8000"; public static final String FRIEND_BOT_URL = "http://127.0.0.1:8000/friendbot"; public static final Network NETWORK = Network.STANDALONE; @@ -140,8 +140,8 @@ public static BigInteger getBalanceForContract(String contractId, Asset asset, K public static void issueAsset( String assetCode, KeyPair issuer, KeyPair receiver, BigDecimal amount) { Asset asset = Asset.createNonNativeAsset(assetCode, issuer.getAccountId()); - try (Server server = new Server(HORIZON_URL)) { - AccountResponse issuerAccount = server.accounts().account(issuer.getAccountId()); + try (SorobanServer server = new SorobanServer(RPC_URL)) { + TransactionBuilderAccount issuerAccount = server.getAccount(issuer.getAccountId()); Transaction transaction = new TransactionBuilder(issuerAccount, NETWORK) @@ -164,7 +164,29 @@ public static void issueAsset( transaction.sign(issuer); transaction.sign(receiver); - server.submitTransaction(transaction); + submitTransaction(transaction); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** Submits the transaction via RPC and waits until it succeeds, throwing otherwise. */ + public static GetTransactionResponse submitTransaction(Transaction transaction) { + try (SorobanServer server = new SorobanServer(RPC_URL)) { + SendTransactionResponse sendResponse = server.sendTransaction(transaction); + if (sendResponse.getStatus() != SendTransactionResponse.SendTransactionStatus.PENDING) { + throw new RuntimeException("Transaction submission failed: " + sendResponse); + } + GetTransactionResponse getResponse = server.pollTransaction(sendResponse.getHash()); + if (getResponse.getStatus() != GetTransactionResponse.GetTransactionStatus.SUCCESS) { + throw new RuntimeException("Transaction failed: " + getResponse); + } + return getResponse; + } catch (IOException e) { + throw new RuntimeException(e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); } } } diff --git a/src/test/java/org/stellar/sdk/SorobanServerTest.java b/src/test/java/org/stellar/sdk/SorobanServerTest.java index bb105018c..bb647e190 100644 --- a/src/test/java/org/stellar/sdk/SorobanServerTest.java +++ b/src/test/java/org/stellar/sdk/SorobanServerTest.java @@ -17,6 +17,7 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; +import java.util.LinkedHashMap; import java.util.List; import java.util.Optional; import okhttp3.HttpUrl; @@ -53,6 +54,7 @@ import org.stellar.sdk.responses.sorobanrpc.GetVersionInfoResponse; import org.stellar.sdk.responses.sorobanrpc.SendTransactionResponse; import org.stellar.sdk.responses.sorobanrpc.SimulateTransactionResponse; +import org.stellar.sdk.scval.Scv; import org.stellar.sdk.xdr.ContractDataDurability; import org.stellar.sdk.xdr.ContractExecutable; import org.stellar.sdk.xdr.ContractExecutableType; @@ -1699,6 +1701,45 @@ public MockResponse dispatch(@NotNull RecordedRequest recordedRequest) mockWebServer.close(); } + @Test + public void testGetContractDataRejectsKeyContainingMuxedAddress() throws IOException { + String contractId = "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"; + SCVal muxedContract = + Scv.toAddress("WA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAAAAAAAAAWWC"); + SCVal muxedAccount = + Scv.toAddress("MAQAA5L65LSYH7CQ3VTJ7F3HHLGCL3DSLAR2Y47263D56MNNGHSQSAAAAAAAAAAE2LP26"); + LinkedHashMap nestedMap = new LinkedHashMap<>(); + nestedMap.put(Scv.toSymbol("owner"), Scv.toVec(singletonList(muxedAccount))); + List keys = + Arrays.asList( + muxedContract, + Scv.toVec(Arrays.asList(Scv.toSymbol("Balance"), muxedContract)), + Scv.toMap(nestedMap)); + try (SorobanServer server = new SorobanServer("http://localhost:8000/rpc")) { + for (SCVal key : keys) { + try { + server.getContractData(contractId, key, SorobanServer.Durability.PERSISTENT); + fail(); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("cannot contain muxed addresses")); + } + } + } + } + + @Test + public void testGetContractDataRejectsMuxedContract() throws IOException { + try (SorobanServer server = new SorobanServer("http://localhost:8000/rpc")) { + server.getContractData( + "WA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAAAAAAAAAWWC", + Scv.toSymbol("COUNTER"), + SorobanServer.Durability.PERSISTENT); + fail(); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("Invalid contract ID")); + } + } + private Transaction buildSorobanTransaction( SorobanTransactionData sorobanData, List auth) { String contractId = "CDU3PZ4LXVETIFVLS33RDXLD63JZ5GXS7PCV2DJ7BBT6EBPA2AB7YR5H"; diff --git a/src/test/java/org/stellar/sdk/TransactionBuilderTest.java b/src/test/java/org/stellar/sdk/TransactionBuilderTest.java index 0cf3e1310..c568747f6 100644 --- a/src/test/java/org/stellar/sdk/TransactionBuilderTest.java +++ b/src/test/java/org/stellar/sdk/TransactionBuilderTest.java @@ -1069,4 +1069,32 @@ private static org.stellar.sdk.xdr.TimeBounds buildTimeBounds( .maxTime(new TimePoint(new Uint64(new XdrUnsignedHyperInteger(maxTime)))) .build(); } + + @Test + public void testBuildRestoreAssetBalanceEntryTransactionRejectsAccount() { + Account account = new Account("GDJJRRMBK4IWLEPJGIE6SXD2LP7REGZODU7WDC3I2D6MR37F4XSHBKX2", 1L); + TransactionBuilder builder = new TransactionBuilder(account, Network.TESTNET); + try { + builder.buildRestoreAssetBalanceEntryTransaction( + account.getAccountId(), Asset.createNativeAsset(), null); + fail(); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("balanceOwner is not a valid contract address")); + } + } + + @Test + public void testBuildRestoreAssetBalanceEntryTransactionRejectsMuxedContract() { + Account account = new Account("GDJJRRMBK4IWLEPJGIE6SXD2LP7REGZODU7WDC3I2D6MR37F4XSHBKX2", 1L); + TransactionBuilder builder = new TransactionBuilder(account, Network.TESTNET); + try { + builder.buildRestoreAssetBalanceEntryTransaction( + "WA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAAAAAAAAAWWC", + Asset.createNativeAsset(), + null); + fail(); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("balanceOwner is not a valid contract address")); + } + } } diff --git a/src/test/java/org/stellar/sdk/TransactionTest.java b/src/test/java/org/stellar/sdk/TransactionTest.java index 6ebe89c2f..3ff48aad1 100644 --- a/src/test/java/org/stellar/sdk/TransactionTest.java +++ b/src/test/java/org/stellar/sdk/TransactionTest.java @@ -10,12 +10,16 @@ import java.io.IOException; import java.math.BigDecimal; import java.math.BigInteger; +import java.nio.ByteBuffer; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import org.junit.Test; +import org.stellar.sdk.contract.AssembledTransaction; +import org.stellar.sdk.contract.ContractClient; import org.stellar.sdk.operations.BumpSequenceOperation; import org.stellar.sdk.operations.CreateAccountOperation; import org.stellar.sdk.operations.ExtendFootprintTTLOperation; @@ -24,6 +28,7 @@ import org.stellar.sdk.operations.RestoreFootprintOperation; import org.stellar.sdk.scval.Scv; import org.stellar.sdk.xdr.ContractDataDurability; +import org.stellar.sdk.xdr.ContractEvent; import org.stellar.sdk.xdr.ContractExecutable; import org.stellar.sdk.xdr.ContractExecutableType; import org.stellar.sdk.xdr.ContractIDPreimage; @@ -515,8 +520,8 @@ public void testIntegrationPaymentToContractTransactionWithNativeAsset() throws IntegrationUtils.fundAccount(kp.getAccountId()); IntegrationUtils.createAssetContract(asset, kp); String destination = IntegrationUtils.getRandomContractId(kp); - Server server = new Server(IntegrationUtils.HORIZON_URL); - TransactionBuilderAccount sourceAccount = server.loadAccount(kp.getAccountId()); + SorobanServer server = new SorobanServer(IntegrationUtils.RPC_URL); + TransactionBuilderAccount sourceAccount = server.getAccount(kp.getAccountId()); Transaction transaction1 = new TransactionBuilder(sourceAccount, IntegrationUtils.NETWORK) @@ -524,7 +529,7 @@ public void testIntegrationPaymentToContractTransactionWithNativeAsset() throws .setTimeout(300) .buildPaymentToContractTransaction(destination, asset, amount1, null); transaction1.sign(kp); - server.submitTransaction(transaction1); + IntegrationUtils.submitTransaction(transaction1); BigInteger balance1 = IntegrationUtils.getBalanceForContract(destination, asset, kp); assertEquals(1001250000L, balance1.longValue()); @@ -534,7 +539,7 @@ public void testIntegrationPaymentToContractTransactionWithNativeAsset() throws .setTimeout(300) .buildPaymentToContractTransaction(destination, asset, amount2, null); transaction2.sign(kp); - server.submitTransaction(transaction2); + IntegrationUtils.submitTransaction(transaction2); BigInteger balance2 = IntegrationUtils.getBalanceForContract(destination, asset, kp); assertEquals(2209141000L, balance2.longValue()); @@ -544,7 +549,7 @@ public void testIntegrationPaymentToContractTransactionWithNativeAsset() throws .setTimeout(300) .buildRestoreAssetBalanceEntryTransaction(destination, asset, null); transaction3.sign(kp); - server.submitTransaction(transaction3); + IntegrationUtils.submitTransaction(transaction3); server.close(); } @@ -562,8 +567,8 @@ public void testIntegrationPaymentToContractTransactionWithAlphanum4Asset() thro IntegrationUtils.issueAsset("CAT", issuerKp, kp, new BigDecimal("1000")); IntegrationUtils.createAssetContract(asset, kp); String destination = IntegrationUtils.getRandomContractId(kp); - Server server = new Server(IntegrationUtils.HORIZON_URL); - TransactionBuilderAccount sourceAccount = server.loadAccount(kp.getAccountId()); + SorobanServer server = new SorobanServer(IntegrationUtils.RPC_URL); + TransactionBuilderAccount sourceAccount = server.getAccount(kp.getAccountId()); Transaction transaction1 = new TransactionBuilder(sourceAccount, IntegrationUtils.NETWORK) @@ -571,7 +576,7 @@ public void testIntegrationPaymentToContractTransactionWithAlphanum4Asset() thro .setTimeout(300) .buildPaymentToContractTransaction(destination, asset, amount1, null); transaction1.sign(kp); - server.submitTransaction(transaction1); + IntegrationUtils.submitTransaction(transaction1); BigInteger balance1 = IntegrationUtils.getBalanceForContract(destination, asset, kp); assertEquals(1001250000L, balance1.longValue()); @@ -581,7 +586,7 @@ public void testIntegrationPaymentToContractTransactionWithAlphanum4Asset() thro .setTimeout(300) .buildPaymentToContractTransaction(destination, asset, amount2, null); transaction2.sign(kp); - server.submitTransaction(transaction2); + IntegrationUtils.submitTransaction(transaction2); BigInteger balance2 = IntegrationUtils.getBalanceForContract(destination, asset, kp); assertEquals(2209141000L, balance2.longValue()); @@ -591,7 +596,7 @@ public void testIntegrationPaymentToContractTransactionWithAlphanum4Asset() thro .setTimeout(300) .buildRestoreAssetBalanceEntryTransaction(destination, asset, null); transaction3.sign(kp); - server.submitTransaction(transaction3); + IntegrationUtils.submitTransaction(transaction3); server.close(); } @@ -609,8 +614,8 @@ public void testIntegrationPaymentToContractTransactionWithAlphanum12Asset() thr IntegrationUtils.issueAsset("BANANA", issuerKp, kp, new BigDecimal("1000")); IntegrationUtils.createAssetContract(asset, kp); String destination = IntegrationUtils.getRandomContractId(kp); - Server server = new Server(IntegrationUtils.HORIZON_URL); - TransactionBuilderAccount sourceAccount = server.loadAccount(kp.getAccountId()); + SorobanServer server = new SorobanServer(IntegrationUtils.RPC_URL); + TransactionBuilderAccount sourceAccount = server.getAccount(kp.getAccountId()); Transaction transaction1 = new TransactionBuilder(sourceAccount, IntegrationUtils.NETWORK) @@ -618,7 +623,7 @@ public void testIntegrationPaymentToContractTransactionWithAlphanum12Asset() thr .setTimeout(300) .buildPaymentToContractTransaction(destination, asset, amount1, null); transaction1.sign(kp); - server.submitTransaction(transaction1); + IntegrationUtils.submitTransaction(transaction1); BigInteger balance1 = IntegrationUtils.getBalanceForContract(destination, asset, kp); assertEquals(1001250000L, balance1.longValue()); @@ -628,7 +633,7 @@ public void testIntegrationPaymentToContractTransactionWithAlphanum12Asset() thr .setTimeout(300) .buildPaymentToContractTransaction(destination, asset, amount2, null); transaction2.sign(kp); - server.submitTransaction(transaction2); + IntegrationUtils.submitTransaction(transaction2); BigInteger balance2 = IntegrationUtils.getBalanceForContract(destination, asset, kp); assertEquals(2209141000L, balance2.longValue()); @@ -638,7 +643,7 @@ public void testIntegrationPaymentToContractTransactionWithAlphanum12Asset() thr .setTimeout(300) .buildRestoreAssetBalanceEntryTransaction(destination, asset, null); transaction3.sign(kp); - server.submitTransaction(transaction3); + IntegrationUtils.submitTransaction(transaction3); server.close(); } @@ -654,8 +659,8 @@ public void testIntegrationPaymentToContractTransactionWithAssetIssuerAsSender() Asset asset = Asset.createNonNativeAsset("CAT", kp.getAccountId()); IntegrationUtils.createAssetContract(asset, kp); String destination = IntegrationUtils.getRandomContractId(kp); - Server server = new Server(IntegrationUtils.HORIZON_URL); - TransactionBuilderAccount sourceAccount = server.loadAccount(kp.getAccountId()); + SorobanServer server = new SorobanServer(IntegrationUtils.RPC_URL); + TransactionBuilderAccount sourceAccount = server.getAccount(kp.getAccountId()); Transaction transaction1 = new TransactionBuilder(sourceAccount, IntegrationUtils.NETWORK) @@ -663,7 +668,7 @@ public void testIntegrationPaymentToContractTransactionWithAssetIssuerAsSender() .setTimeout(300) .buildPaymentToContractTransaction(destination, asset, amount1, null); transaction1.sign(kp); - server.submitTransaction(transaction1); + IntegrationUtils.submitTransaction(transaction1); BigInteger balance1 = IntegrationUtils.getBalanceForContract(destination, asset, kp); assertEquals(1001250000L, balance1.longValue()); @@ -673,7 +678,7 @@ public void testIntegrationPaymentToContractTransactionWithAssetIssuerAsSender() .setTimeout(300) .buildPaymentToContractTransaction(destination, asset, amount2, null); transaction2.sign(kp); - server.submitTransaction(transaction2); + IntegrationUtils.submitTransaction(transaction2); BigInteger balance2 = IntegrationUtils.getBalanceForContract(destination, asset, kp); assertEquals(2209141000L, balance2.longValue()); @@ -683,7 +688,7 @@ public void testIntegrationPaymentToContractTransactionWithAssetIssuerAsSender() .setTimeout(300) .buildRestoreAssetBalanceEntryTransaction(destination, asset, null); transaction3.sign(kp); - server.submitTransaction(transaction3); + IntegrationUtils.submitTransaction(transaction3); server.close(); } @@ -700,8 +705,8 @@ public void testIntegrationPaymentToContractTransactionWithDifferentSource() thr IntegrationUtils.fundAccount(opSource.getAccountId()); IntegrationUtils.createAssetContract(asset, kp); String destination = IntegrationUtils.getRandomContractId(kp); - Server server = new Server(IntegrationUtils.HORIZON_URL); - TransactionBuilderAccount sourceAccount = server.loadAccount(kp.getAccountId()); + SorobanServer server = new SorobanServer(IntegrationUtils.RPC_URL); + TransactionBuilderAccount sourceAccount = server.getAccount(kp.getAccountId()); Transaction transaction1 = new TransactionBuilder(sourceAccount, IntegrationUtils.NETWORK) @@ -711,7 +716,7 @@ public void testIntegrationPaymentToContractTransactionWithDifferentSource() thr destination, asset, amount1, opSource.getAccountId()); transaction1.sign(kp); transaction1.sign(opSource); - server.submitTransaction(transaction1); + IntegrationUtils.submitTransaction(transaction1); BigInteger balance1 = IntegrationUtils.getBalanceForContract(destination, asset, kp); assertEquals(1001250000L, balance1.longValue()); @@ -723,7 +728,7 @@ public void testIntegrationPaymentToContractTransactionWithDifferentSource() thr destination, asset, amount2, opSource.getAccountId()); transaction2.sign(kp); transaction2.sign(opSource); - server.submitTransaction(transaction2); + IntegrationUtils.submitTransaction(transaction2); BigInteger balance2 = IntegrationUtils.getBalanceForContract(destination, asset, kp); assertEquals(2209141000L, balance2.longValue()); @@ -734,11 +739,71 @@ public void testIntegrationPaymentToContractTransactionWithDifferentSource() thr .buildRestoreAssetBalanceEntryTransaction(destination, asset, opSource.getAccountId()); transaction3.sign(kp); transaction3.sign(opSource); - server.submitTransaction(transaction3); + IntegrationUtils.submitTransaction(transaction3); server.close(); } + @Test + public void testIntegrationTransferToMuxedContract() throws IOException { + Asset asset = Asset.createNativeAsset(); + KeyPair kp = KeyPair.random(); + IntegrationUtils.fundAccount(kp.getAccountId()); + IntegrationUtils.createAssetContract(asset, kp); + String contractId = IntegrationUtils.getRandomContractId(kp); + String muxedContractId = + Address.fromMuxedContract( + ByteBuffer.allocate(40) + .put(StrKey.decodeContract(contractId)) + .putLong(Long.MIN_VALUE) // 2^63, exceeds the max signed 64-bit integer + .array()) + .toString(); + assertTrue(muxedContractId.startsWith("W")); + + // SAC `transfer` takes a MuxedAddress `to`; the balance is credited to the base contract. + AssembledTransaction transfer; + try (ContractClient client = + new ContractClient( + asset.getContractId(IntegrationUtils.NETWORK), + IntegrationUtils.RPC_URL, + IntegrationUtils.NETWORK)) { + transfer = + client.invoke( + "transfer", + Arrays.asList( + Scv.toAddress(kp.getAccountId()), + Scv.toAddress(muxedContractId), + Scv.toInt128(BigInteger.valueOf(1001250000L))), + kp.getAccountId(), + kp, + null, + 100); + transfer.signAndSubmit(kp, false); + } + + // CAP-67 event format: the `to` topic is the base contract, and the data carries the mux ID. + List transferEvents = new ArrayList<>(); + for (ContractEvent event : + transfer.getGetTransactionResponse().getEvents().parseContractEventsXdr().get(0)) { + SCVal[] topics = event.getBody().getV0().getTopics(); + if (Scv.toSymbol("transfer").equals(topics[0])) { + transferEvents.add(event); + } + } + assertEquals(1, transferEvents.size()); + ContractEvent.ContractEventBody.ContractEventV0 transferEvent = + transferEvents.get(0).getBody().getV0(); + assertEquals(Scv.toAddress(contractId), transferEvent.getTopics()[2]); + LinkedHashMap data = Scv.fromMap(transferEvent.getData()); + assertEquals(BigInteger.valueOf(1001250000L), Scv.fromInt128(data.get(Scv.toSymbol("amount")))); + assertEquals( + new BigInteger("9223372036854775808"), + Scv.fromUint64(data.get(Scv.toSymbol("to_muxed_id")))); + + BigInteger balance = IntegrationUtils.getBalanceForContract(contractId, asset, kp); + assertEquals(1001250000L, balance.longValue()); + } + @Test public void testSignExtraSignersPayloadOneExtraSigner() { KeyPair source = diff --git a/src/test/java/org/stellar/sdk/responses/EffectResponseTest.java b/src/test/java/org/stellar/sdk/responses/EffectResponseTest.java index dfb9f55b0..4c406a9eb 100644 --- a/src/test/java/org/stellar/sdk/responses/EffectResponseTest.java +++ b/src/test/java/org/stellar/sdk/responses/EffectResponseTest.java @@ -3,9 +3,11 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import java.io.IOException; +import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; @@ -373,6 +375,27 @@ public void testContractCredited() throws IOException { assertEquals("100.0000000", response.getAmount()); assertEquals( "CDCYWK73YTYFJZZSJ5V7EDFNHYBG4QN3VUNG2IGD27KJDDPNCZKBCBXK", response.getContract()); + assertNull(response.getContractMuxed()); + assertNull(response.getContractMuxedId()); + } + + @Test + public void testContractCreditedToMuxedContract() throws IOException { + String filePath = "src/test/resources/responses/effects/contract_credited_muxed.json"; + String json = new String(Files.readAllBytes(Paths.get(filePath))); + ContractCreditedEffectResponse response = + (ContractCreditedEffectResponse) + GsonSingleton.getInstance().fromJson(json, EffectResponse.class); + + assertEquals("contract_credited", response.getType()); + assertEquals(new AssetTypeNative(), response.getAsset()); + assertEquals("100.0000000", response.getAmount()); + assertEquals( + "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA", response.getContract()); + assertEquals( + "WA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAACWJY", + response.getContractMuxed()); + assertEquals(new BigInteger("9223372036854775808"), response.getContractMuxedId()); } @Test diff --git a/src/test/java/org/stellar/sdk/responses/sorobanrpc/GetEventsDeserializerTest.java b/src/test/java/org/stellar/sdk/responses/sorobanrpc/GetEventsDeserializerTest.java index bd28cc3a5..b8b56df23 100644 --- a/src/test/java/org/stellar/sdk/responses/sorobanrpc/GetEventsDeserializerTest.java +++ b/src/test/java/org/stellar/sdk/responses/sorobanrpc/GetEventsDeserializerTest.java @@ -2,6 +2,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import com.google.gson.reflect.TypeToken; import java.io.IOException; @@ -56,4 +57,24 @@ public void testDeserialize() throws IOException { GetEventsResponse.EventInfo eventInfo2 = getEventsResponse.getResult().getEvents().get(2); assertEquals(eventInfo2.getType(), EventFilterType.DIAGNOSTIC); } + + @Test + public void testDeserializeWithoutInSuccessfulContractCall() { + // Stellar RPC no longer returns inSuccessfulContractCall (stellar-rpc#1050). + String json = + "{\"jsonrpc\":\"2.0\",\"id\":\"1\",\"result\":{\"latestLedger\":100," + + "\"cursor\":\"0000000463856472064-0000000000\",\"events\":[{\"type\":\"contract\"," + + "\"ledger\":108,\"ledgerClosedAt\":\"2023-07-23T14:47:01Z\"," + + "\"contractId\":\"CBQHNAXSI55GX2GN6D67GK7BHVPSLJUGZQEU7WJ5LKR5PNUCGLIMAO4K\"," + + "\"id\":\"0000000463856472064-0000000000\",\"topic\":[\"AAAADwAAAAdDT1VOVEVSAA==\"]," + + "\"value\":\"AAAAAwAAAAE=\",\"txHash\":" + + "\"db86e94aa98b7d38213c041ebbb727fbaabf0b7c435de594f36c2d51fc61926d\"}]}}"; + + SorobanRpcResponse getEventsResponse = + GsonSingleton.getInstance() + .fromJson(json, new TypeToken>() {}.getType()); + GetEventsResponse.EventInfo eventInfo = getEventsResponse.getResult().getEvents().get(0); + assertEquals(eventInfo.getValue(), "AAAAAwAAAAE="); + assertNull(eventInfo.getInSuccessfulContractCall()); + } } diff --git a/src/test/kotlin/org/stellar/sdk/AddressTest.kt b/src/test/kotlin/org/stellar/sdk/AddressTest.kt index c68acadf9..c2c45ef6c 100644 --- a/src/test/kotlin/org/stellar/sdk/AddressTest.kt +++ b/src/test/kotlin/org/stellar/sdk/AddressTest.kt @@ -4,8 +4,10 @@ import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe import java.io.ByteArrayInputStream +import java.math.BigInteger import java.util.Base64 import org.stellar.sdk.xdr.SCAddress +import org.stellar.sdk.xdr.SCAddressType import org.stellar.sdk.xdr.SCVal import org.stellar.sdk.xdr.XdrDataInputStream @@ -16,6 +18,10 @@ class AddressTest : val muxedAccountId = "MAQAA5L65LSYH7CQ3VTJ7F3HHLGCL3DSLAR2Y47263D56MNNGHSQSAAAAAAAAAAE2LP26" val claimableBalanceId = "BAAD6DBUX6J22DMZOHIEZTEQ64CVCHEDRKWZONFEUL5Q26QD7R76RGR4TU" val liquidityPoolId = "LA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUPJN" + // https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0023.md + val muxedContractId = "WA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAAAAAAAAAWWC" + val muxedContractIdWithLargeId = + "WA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAACWJY" val secretKey = "SB65MHFA2Z342DX4FNKHH2KCNR5JRM7GIVTWQLKG5Z6L3AAH4UZLZV4E" val invalidAddress = "GINVALID" @@ -50,6 +56,20 @@ class AddressTest : address.addressType shouldBe Address.AddressType.LIQUIDITY_POOL } + test("should throw exception for claimable balance ID with a non-zero type") { + val exception = + shouldThrow { + Address("BAAT6DBUX6J22DMZOHIEZTEQ64CVCHEDRKWZONFEUL5Q26QD7R76RGXACA") + } + exception.message shouldBe "Unsupported address type" + } + + test("should create address from muxed contract address") { + val address = Address(muxedContractId) + address.toString() shouldBe muxedContractId + address.addressType shouldBe Address.AddressType.MUXED_CONTRACT + } + test("should throw exception for invalid address") { val exception = shouldThrow { Address(invalidAddress) } exception.message shouldBe "Unsupported address type" @@ -212,6 +232,44 @@ class AddressTest : } } + context("muxed contract") { + test("should create address from muxed contract bytes") { + val bytes = StrKey.decodeMuxedContract(muxedContractId) + val address = Address.fromMuxedContract(bytes) + address.toString() shouldBe muxedContractId + address.addressType shouldBe Address.AddressType.MUXED_CONTRACT + address.bytes shouldBe bytes + } + + test("should convert muxed contract to SCAddress") { + val scAddress = Address(muxedContractIdWithLargeId).toSCAddress() + scAddress.discriminant shouldBe SCAddressType.SC_ADDRESS_TYPE_MUXED_CONTRACT + scAddress.muxedContract.id.uint64.number shouldBe BigInteger("9223372036854775808") + scAddress.muxedContract.contractId.contractID.hash shouldBe + StrKey.decodeContract(contractId) + } + + test("should round trip through SCAddress XDR") { + for (strKey in listOf(muxedContractId, muxedContractIdWithLargeId)) { + val xdr = Address(strKey).toSCAddress().toXdrBase64() + val address = Address.fromSCAddress(SCAddress.fromXdrBase64(xdr)) + address.toString() shouldBe strKey + address.addressType shouldBe Address.AddressType.MUXED_CONTRACT + } + } + + test("should round trip through SCVal") { + val address = Address.fromSCVal(Address(muxedContractId).toSCVal()) + address.toString() shouldBe muxedContractId + } + + test("should be equal by value and differ from the base contract") { + Address(muxedContractId) shouldBe Address(muxedContractId) + (Address(muxedContractId) == Address(muxedContractIdWithLargeId)) shouldBe false + (Address(muxedContractId) == Address(contractId)) shouldBe false + } + } + context("SCVal conversion") { test("should convert address to SCVal") { val address = Address(contractId) diff --git a/src/test/kotlin/org/stellar/sdk/StrKeyTest.kt b/src/test/kotlin/org/stellar/sdk/StrKeyTest.kt index e8e303c30..c8aa9e573 100644 --- a/src/test/kotlin/org/stellar/sdk/StrKeyTest.kt +++ b/src/test/kotlin/org/stellar/sdk/StrKeyTest.kt @@ -191,6 +191,21 @@ class StrKeyTest : StrKey.isValidClaimableBalance( "BAT7RRVQBTQ3TEDLDICV7GNX6GE3KVQFJCNOESUI4QNA4TYYR44P2FS5PZYQ" ) shouldBe false + // SEP-23: invalid claimable balance type (first byte of binary key is not 0) + StrKey.isValidClaimableBalance( + "BAAT6DBUX6J22DMZOHIEZTEQ64CVCHEDRKWZONFEUL5Q26QD7R76RGXACA" + ) shouldBe false + } + + test("decode rejects a non-zero claimable balance type") { + val exception = + shouldThrow { + StrKey.decodeClaimableBalance( + "BAAT6DBUX6J22DMZOHIEZTEQ64CVCHEDRKWZONFEUL5Q26QD7R76RGXACA" + ) + } + exception.message shouldBe + "Invalid claimable balance type, expected 0 (CLAIMABLE_BALANCE_ID_TYPE_V0), got 1" } } @@ -221,6 +236,41 @@ class StrKeyTest : } } + context("MUXED_CONTRACT") { + // https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0023.md + test("encode and decode") { + val rawKey = + Util.hexToBytes( + "3f0c34bf93ad0d9971d04ccc90f705511c838aad9734a4a2fb0d7a03fc7fe89a0000000000000000" + ) + val encoded = "WA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAAAAAAAAAWWC" + StrKey.encodeMuxedContract(rawKey) shouldBe encoded + StrKey.decodeMuxedContract(encoded) shouldBe rawKey + } + + test("encode and decode with id exceeding max signed 64-bit integer") { + val rawKey = + Util.hexToBytes( + "3f0c34bf93ad0d9971d04ccc90f705511c838aad9734a4a2fb0d7a03fc7fe89a8000000000000000" + ) + val encoded = "WA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAACWJY" + StrKey.encodeMuxedContract(rawKey) shouldBe encoded + StrKey.decodeMuxedContract(encoded) shouldBe rawKey + } + + test("is valid") { + StrKey.isValidMuxedContract( + "WA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAAAAAAAAAWWC" + ) shouldBe true + StrKey.isValidMuxedContract( + "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA" + ) shouldBe false + StrKey.isValidMuxedContract( + "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK" + ) shouldBe false + } + } + context("SIGNED_PAYLOAD") { test("encode and decode with 64-byte payload") { val rawKey = @@ -355,9 +405,13 @@ class StrKeyTest : "PA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAOQCAQDAQCQMBYIBEFAWDANBYHRAEISCMKBKFQXDAMRUGY4Z2PQ", "PA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAOQCAQDAQCQMBYIBEFAWDANBYHRAEISCMKBKFQXDAMRUGY4DXFH6", "BAAD6DBUX6J22DMZOHIEZTEQ64CVCHEDRKWZONFEUL5Q26QD7R76RGR4TV", - // We should consider this valid - // Invalid claimablebalance type (first byte of binary key is not 0), - // "BAAT6DBUX6J22DMZOHIEZTEQ64CVCHEDRKWZONFEUL5Q26QD7R76RGXACA" + // Invalid claimable balance type (first byte of binary key is not 0) + "BAAT6DBUX6J22DMZOHIEZTEQ64CVCHEDRKWZONFEUL5Q26QD7R76RGXACA", + "WA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAAAAAAAAAWWD", + "WA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAAAAAAAAAWWCA", + "WA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAAAAAAAAAAIOUI", + "W47QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAAAAAAAADXHW", + "WA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAAAAAAAAAWWA", "", ) { key -> shouldThrow { StrKey.decodeEd25519PublicKey(key) } @@ -369,6 +423,7 @@ class StrKeyTest : shouldThrow { StrKey.decodeContract(key) } shouldThrow { StrKey.decodeLiquidityPool(key) } shouldThrow { StrKey.decodeClaimableBalance(key) } + shouldThrow { StrKey.decodeMuxedContract(key) } } } @@ -394,6 +449,9 @@ class StrKeyTest : StrKey.decodeVersionByte( "BAAD6DBUX6J22DMZOHIEZTEQ64CVCHEDRKWZONFEUL5Q26QD7R76RGR4TU" ) shouldBe StrKey.VersionByte.CLAIMABLE_BALANCE + StrKey.decodeVersionByte( + "WA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAAAAAAAAAWWC" + ) shouldBe StrKey.VersionByte.MUXED_CONTRACT } test("decode version byte throws exception for invalid input") { diff --git a/src/test/kotlin/org/stellar/sdk/contract/ContractSpecTest.kt b/src/test/kotlin/org/stellar/sdk/contract/ContractSpecTest.kt index ffd5d8cab..f444f510c 100644 --- a/src/test/kotlin/org/stellar/sdk/contract/ContractSpecTest.kt +++ b/src/test/kotlin/org/stellar/sdk/contract/ContractSpecTest.kt @@ -49,7 +49,7 @@ private fun eventEntry(name: String): SCSpecEntry { SCSpecEventV0().apply { doc = XdrString(ByteArray(0)) lib = XdrString(ByteArray(0)) - this.name = symbol(name) + this.name = XdrString(name.toByteArray(Charsets.UTF_8)) prefixTopics = arrayOf() params = arrayOf() dataFormat = SCSpecEventDataFormat.values().first() diff --git a/src/test/kotlin/org/stellar/sdk/contract/SorobanServerContractIntrospectionTest.kt b/src/test/kotlin/org/stellar/sdk/contract/SorobanServerContractIntrospectionTest.kt index a87463d4e..e3804a902 100644 --- a/src/test/kotlin/org/stellar/sdk/contract/SorobanServerContractIntrospectionTest.kt +++ b/src/test/kotlin/org/stellar/sdk/contract/SorobanServerContractIntrospectionTest.kt @@ -133,14 +133,14 @@ private fun singleEntryJson(xdr: String): String = private fun emptyEntriesJson(): String = """ - { - "jsonrpc": "2.0", - "id": "id", - "result": { - "entries": [], - "latestLedger": "100" - } + { + "jsonrpc": "2.0", + "id": "id", + "result": { + "entries": [], + "latestLedger": "100" } + } """ .trimIndent() diff --git a/src/test/kotlin/org/stellar/sdk/scval/ScvComparatorTest.kt b/src/test/kotlin/org/stellar/sdk/scval/ScvComparatorTest.kt index e89d69818..2144ff9d1 100644 --- a/src/test/kotlin/org/stellar/sdk/scval/ScvComparatorTest.kt +++ b/src/test/kotlin/org/stellar/sdk/scval/ScvComparatorTest.kt @@ -14,6 +14,7 @@ import org.stellar.sdk.xdr.ContractExecutableType import org.stellar.sdk.xdr.ContractID import org.stellar.sdk.xdr.Hash import org.stellar.sdk.xdr.Int64 +import org.stellar.sdk.xdr.MuxedContract import org.stellar.sdk.xdr.MuxedEd25519Account import org.stellar.sdk.xdr.PoolID import org.stellar.sdk.xdr.PublicKey @@ -417,7 +418,9 @@ class ScvComparatorTest : } context("compareScAddress") { - test("address type ordering: account < contract < muxed < claimable < pool") { + test( + "address type ordering: account < contract < muxed < claimable < pool < muxed contract" + ) { val addrs = arrayOf( accountAddress(bytes32(0x00)), @@ -425,6 +428,7 @@ class ScvComparatorTest : muxedAddress(0, bytes32(0x00)), claimableBalanceAddress(bytes32(0x00)), liquidityPoolAddress(bytes32(0x00)), + muxedContractAddress(0, bytes32(0x00)), ) for (i in 0 until addrs.size - 1) { ScvComparator.compareScAddress(addrs[i], addrs[i + 1]) shouldBeLessThan 0 @@ -481,6 +485,21 @@ class ScvComparatorTest : muxedAddress(5, bytes32(0xAB)), ) shouldBe 0 } + + test("muxed contract: id first, then contract ID") { + ScvComparator.compareScAddress( + muxedContractAddress(1, bytes32(0xFF)), + muxedContractAddress(2, bytes32(0x00)), + ) shouldBeLessThan 0 + ScvComparator.compareScAddress( + muxedContractAddress(1, bytes32(0x00)), + muxedContractAddress(1, bytes32Last(0x01)), + ) shouldBeLessThan 0 + ScvComparator.compareScAddress( + muxedContractAddress(5, bytes32(0xAB)), + muxedContractAddress(5, bytes32(0xAB)), + ) shouldBe 0 + } } context("compareContractExecutable") { @@ -728,5 +747,16 @@ class ScvComparatorTest : .discriminant(SCAddressType.SC_ADDRESS_TYPE_LIQUIDITY_POOL) .liquidityPoolId(PoolID(Hash(hash))) .build() + + private fun muxedContractAddress(id: Long, hash: ByteArray): SCAddress = + SCAddress.builder() + .discriminant(SCAddressType.SC_ADDRESS_TYPE_MUXED_CONTRACT) + .muxedContract( + MuxedContract.builder() + .id(Uint64(XdrUnsignedHyperInteger(BigInteger.valueOf(id)))) + .contractId(ContractID(Hash(hash))) + .build() + ) + .build() } } diff --git a/src/test/resources/responses/effects/contract_credited_muxed.json b/src/test/resources/responses/effects/contract_credited_muxed.json new file mode 100644 index 000000000..6ba0d07de --- /dev/null +++ b/src/test/resources/responses/effects/contract_credited_muxed.json @@ -0,0 +1,24 @@ +{ + "_links": { + "operation": { + "href": "http://100.83.15.43:8000/operations/21517786157057" + }, + "succeeds": { + "href": "http://100.83.15.43:8000/effects?order=desc&cursor=21517786157057-2" + }, + "precedes": { + "href": "http://100.83.15.43:8000/effects?order=asc&cursor=21517786157057-2" + } + }, + "id": "0000021517786157057-0000000002", + "paging_token": "21517786157057-2", + "account": "GDAT5HWTGIU4TSSZ4752OUC4SABDLTLZFRPZUJ3D6LKBNEPA7V2CIG54", + "type": "contract_credited", + "type_i": 96, + "created_at": "2023-09-19T05:43:12Z", + "asset_type": "native", + "contract": "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA", + "contract_muxed": "WA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAACWJY", + "contract_muxed_id": "9223372036854775808", + "amount": "100.0000000" +} diff --git a/xdr/Stellar-contract-config-setting.x b/xdr/Stellar-contract-config-setting.x index 80c0d7fcf..bab260a98 100644 --- a/xdr/Stellar-contract-config-setting.x +++ b/xdr/Stellar-contract-config-setting.x @@ -296,7 +296,25 @@ enum ContractCostType { // Cost of performing BN254 scalar element inversion Bn254FrInv = 84, // Cost of performing BN254 G1 multi-scalar multiplication (MSM) - Bn254G1Msm = 85 + Bn254G1Msm = 85, + // Cost of decoding and expanding an ML-DSA-44 verifying key + MlDsa44DecodeVerifyingKey = 86, + // Cost of decoding and expanding an ML-DSA-65 verifying key + MlDsa65DecodeVerifyingKey = 87, + // Cost of decoding and expanding an ML-DSA-87 verifying key + MlDsa87DecodeVerifyingKey = 88, + // Cost of decoding an ML-DSA-44 signature + MlDsa44DecodeSignature = 89, + // Cost of decoding an ML-DSA-65 signature + MlDsa65DecodeSignature = 90, + // Cost of decoding an ML-DSA-87 signature + MlDsa87DecodeSignature = 91, + // Cost of verifying an ML-DSA-44 signature, linear in message + context length + VerifyMlDsa44Sig = 92, + // Cost of verifying an ML-DSA-65 signature, linear in message + context length + VerifyMlDsa65Sig = 93, + // Cost of verifying an ML-DSA-87 signature, linear in message + context length + VerifyMlDsa87Sig = 94 }; struct ContractCostParamEntry { diff --git a/xdr/Stellar-contract-spec.x b/xdr/Stellar-contract-spec.x index 33c4cec1e..d87e643e4 100644 --- a/xdr/Stellar-contract-spec.x +++ b/xdr/Stellar-contract-spec.x @@ -12,6 +12,8 @@ namespace stellar const SC_SPEC_DOC_LIMIT = 1024; +const SC_SPEC_TYPE_NAME_LIMIT = 1024; + enum SCSpecType { SC_SPEC_TYPE_VAL = 0, @@ -82,7 +84,7 @@ struct SCSpecTypeBytesN struct SCSpecTypeUDT { - string name<60>; + string name; }; union SCSpecTypeDef switch (SCSpecType type) @@ -134,7 +136,7 @@ struct SCSpecUDTStructV0 { string doc; string lib<80>; - string name<60>; + string name; SCSpecUDTStructFieldV0 fields<>; }; @@ -169,7 +171,7 @@ struct SCSpecUDTUnionV0 { string doc; string lib<80>; - string name<60>; + string name; SCSpecUDTUnionCaseV0 cases<>; }; @@ -184,7 +186,7 @@ struct SCSpecUDTEnumV0 { string doc; string lib<80>; - string name<60>; + string name; SCSpecUDTEnumCaseV0 cases<>; }; @@ -199,7 +201,7 @@ struct SCSpecUDTErrorEnumV0 { string doc; string lib<80>; - string name<60>; + string name; SCSpecUDTErrorEnumCaseV0 cases<>; }; @@ -243,7 +245,7 @@ struct SCSpecEventV0 { string doc; string lib<80>; - SCSymbol name; + string name; SCSymbol prefixTopics<2>; SCSpecEventParamV0 params<>; SCSpecEventDataFormat dataFormat; diff --git a/xdr/Stellar-contract.x b/xdr/Stellar-contract.x index 727517311..ad04b1bc3 100644 --- a/xdr/Stellar-contract.x +++ b/xdr/Stellar-contract.x @@ -182,7 +182,8 @@ enum SCAddressType SC_ADDRESS_TYPE_CONTRACT = 1, SC_ADDRESS_TYPE_MUXED_ACCOUNT = 2, SC_ADDRESS_TYPE_CLAIMABLE_BALANCE = 3, - SC_ADDRESS_TYPE_LIQUIDITY_POOL = 4 + SC_ADDRESS_TYPE_LIQUIDITY_POOL = 4, + SC_ADDRESS_TYPE_MUXED_CONTRACT = 5 }; struct MuxedEd25519Account @@ -191,6 +192,12 @@ struct MuxedEd25519Account uint256 ed25519; }; +struct MuxedContract +{ + uint64 id; + ContractID contractId; +}; + union SCAddress switch (SCAddressType type) { case SC_ADDRESS_TYPE_ACCOUNT: @@ -203,6 +210,8 @@ case SC_ADDRESS_TYPE_CLAIMABLE_BALANCE: ClaimableBalanceID claimableBalanceId; case SC_ADDRESS_TYPE_LIQUIDITY_POOL: PoolID liquidityPoolId; +case SC_ADDRESS_TYPE_MUXED_CONTRACT: + MuxedContract muxedContract; }; struct ContractExecutableExternalRef { diff --git a/xdr/Stellar-ledger.x b/xdr/Stellar-ledger.x index 9acf1d773..47726e3fa 100644 --- a/xdr/Stellar-ledger.x +++ b/xdr/Stellar-ledger.x @@ -14,7 +14,9 @@ enum StellarValueType { STELLAR_VALUE_BASIC = 0, STELLAR_VALUE_SIGNED = 1, - STELLAR_VALUE_EMPTY_TX_SET = 2 + STELLAR_VALUE_EMPTY_TX_SET = 2, + STELLAR_VALUE_SIGNED_MS = 3, + STELLAR_VALUE_EMPTY_TX_SET_MS = 4 }; struct LedgerCloseValueSignature @@ -52,6 +54,21 @@ struct StellarValue uint32 previousLedgerVersion; LedgerCloseValueSignature lcValueSignature; } proposedValue; + case STELLAR_VALUE_SIGNED_MS: + struct + { + TimePointMs closeTimeMs; // closeTime == closeTimeMs / 1000 + LedgerCloseValueSignature lcValueSignature; + } signedMsValue; + case STELLAR_VALUE_EMPTY_TX_SET_MS: + struct + { + TimePointMs closeTimeMs; // closeTime == closeTimeMs / 1000 + Hash txSetHash; + Hash previousLedgerHash; + uint32 previousLedgerVersion; + LedgerCloseValueSignature lcValueSignature; + } proposedMsValue; } ext; }; diff --git a/xdr/Stellar-types.x b/xdr/Stellar-types.x index 4b5914e44..017f45d3c 100644 --- a/xdr/Stellar-types.x +++ b/xdr/Stellar-types.x @@ -16,6 +16,8 @@ typedef hyper int64; typedef uint64 TimePoint; typedef uint64 Duration; +// Milliseconds since the Unix epoch. TimePoint is whole seconds. +typedef uint64 TimePointMs; // An ExtensionPoint is always marshaled as a 32-bit 0 value. At a // later point, it can be replaced by a different union so as to