From 2f6a82024cc7516b59a6c168c35d21afd37d6e4c Mon Sep 17 00:00:00 2001
From: Jun Luo <4catcode@gmail.com>
Date: Wed, 4 Mar 2026 09:41:06 +0800
Subject: [PATCH 1/6] feat: sort ScMap entries by key in `Scv.toMap` following
Soroban runtime ordering
---
CHANGELOG.md | 1 +
.../stellar/sdk/scval/ScValComparator.java | 316 ++++++++++++
src/main/java/org/stellar/sdk/scval/Scv.java | 18 +
.../java/org/stellar/sdk/scval/ScvMap.java | 8 +-
.../sdk/scval/ScValComparatorTest.java | 462 ++++++++++++++++++
.../org/stellar/sdk/scval/ScvMapTest.java | 87 +++-
6 files changed, 887 insertions(+), 5 deletions(-)
create mode 100644 src/main/java/org/stellar/sdk/scval/ScValComparator.java
create mode 100644 src/test/java/org/stellar/sdk/scval/ScValComparatorTest.java
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 24e69bed5..f936c5d52 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
# Changelog
## Pending
+- feat: sort `ScMap` entries by key in `Scv.toMap` following Soroban runtime ordering rules, as the network requires ScMap keys to be in ascending order.
## 2.2.3
diff --git a/src/main/java/org/stellar/sdk/scval/ScValComparator.java b/src/main/java/org/stellar/sdk/scval/ScValComparator.java
new file mode 100644
index 000000000..04fedadab
--- /dev/null
+++ b/src/main/java/org/stellar/sdk/scval/ScValComparator.java
@@ -0,0 +1,316 @@
+package org.stellar.sdk.scval;
+
+import java.util.Comparator;
+import org.stellar.sdk.xdr.ContractExecutable;
+import org.stellar.sdk.xdr.SCAddress;
+import org.stellar.sdk.xdr.SCMap;
+import org.stellar.sdk.xdr.SCMapEntry;
+import org.stellar.sdk.xdr.SCVal;
+import org.stellar.sdk.xdr.SCValType;
+
+/**
+ * Comparator for {@link SCVal} values following Soroban runtime ordering rules.
+ *
+ *
This mirrors Rust's {@code #[derive(Ord)]} on the {@code ScVal} enum in {@code
+ * rs-soroban-env}.
+ *
+ *
{
+ static final ScValComparator INSTANCE = new ScValComparator();
+
+ @Override
+ public int compare(SCVal a, SCVal b) {
+ return compareScVal(a, b);
+ }
+
+ static int compareScVal(SCVal a, SCVal b) {
+ if (a.getDiscriminant() != b.getDiscriminant()) {
+ return Integer.compare(a.getDiscriminant().getValue(), b.getDiscriminant().getValue());
+ }
+
+ SCValType t = a.getDiscriminant();
+ switch (t) {
+ case SCV_BOOL:
+ return Boolean.compare(a.getB(), b.getB());
+ case SCV_VOID:
+ case SCV_LEDGER_KEY_CONTRACT_INSTANCE:
+ return 0;
+ case SCV_U32:
+ return Long.compare(a.getU32().getUint32().getNumber(), b.getU32().getUint32().getNumber());
+ case SCV_I32:
+ return Integer.compare(a.getI32().getInt32(), b.getI32().getInt32());
+ case SCV_U64:
+ return a.getU64().getUint64().getNumber().compareTo(b.getU64().getUint64().getNumber());
+ case SCV_I64:
+ return Long.compare(a.getI64().getInt64(), b.getI64().getInt64());
+ case SCV_TIMEPOINT:
+ return a.getTimepoint()
+ .getTimePoint()
+ .getUint64()
+ .getNumber()
+ .compareTo(b.getTimepoint().getTimePoint().getUint64().getNumber());
+ case SCV_DURATION:
+ return a.getDuration()
+ .getDuration()
+ .getUint64()
+ .getNumber()
+ .compareTo(b.getDuration().getDuration().getUint64().getNumber());
+ case SCV_U128:
+ {
+ int c =
+ a.getU128()
+ .getHi()
+ .getUint64()
+ .getNumber()
+ .compareTo(b.getU128().getHi().getUint64().getNumber());
+ if (c != 0) return c;
+ return a.getU128()
+ .getLo()
+ .getUint64()
+ .getNumber()
+ .compareTo(b.getU128().getLo().getUint64().getNumber());
+ }
+ case SCV_I128:
+ {
+ int c = Long.compare(a.getI128().getHi().getInt64(), b.getI128().getHi().getInt64());
+ if (c != 0) return c;
+ return a.getI128()
+ .getLo()
+ .getUint64()
+ .getNumber()
+ .compareTo(b.getI128().getLo().getUint64().getNumber());
+ }
+ case SCV_U256:
+ {
+ int c =
+ a.getU256()
+ .getHi_hi()
+ .getUint64()
+ .getNumber()
+ .compareTo(b.getU256().getHi_hi().getUint64().getNumber());
+ if (c != 0) return c;
+ c =
+ a.getU256()
+ .getHi_lo()
+ .getUint64()
+ .getNumber()
+ .compareTo(b.getU256().getHi_lo().getUint64().getNumber());
+ if (c != 0) return c;
+ c =
+ a.getU256()
+ .getLo_hi()
+ .getUint64()
+ .getNumber()
+ .compareTo(b.getU256().getLo_hi().getUint64().getNumber());
+ if (c != 0) return c;
+ return a.getU256()
+ .getLo_lo()
+ .getUint64()
+ .getNumber()
+ .compareTo(b.getU256().getLo_lo().getUint64().getNumber());
+ }
+ case SCV_I256:
+ {
+ int c =
+ Long.compare(a.getI256().getHi_hi().getInt64(), b.getI256().getHi_hi().getInt64());
+ if (c != 0) return c;
+ c =
+ a.getI256()
+ .getHi_lo()
+ .getUint64()
+ .getNumber()
+ .compareTo(b.getI256().getHi_lo().getUint64().getNumber());
+ if (c != 0) return c;
+ c =
+ a.getI256()
+ .getLo_hi()
+ .getUint64()
+ .getNumber()
+ .compareTo(b.getI256().getLo_hi().getUint64().getNumber());
+ if (c != 0) return c;
+ return a.getI256()
+ .getLo_lo()
+ .getUint64()
+ .getNumber()
+ .compareTo(b.getI256().getLo_lo().getUint64().getNumber());
+ }
+ case SCV_BYTES:
+ return compareByteArrays(a.getBytes().getSCBytes(), b.getBytes().getSCBytes());
+ case SCV_STRING:
+ return compareByteArrays(
+ a.getStr().getSCString().getBytes(), b.getStr().getSCString().getBytes());
+ case SCV_SYMBOL:
+ return compareByteArrays(
+ a.getSym().getSCSymbol().getBytes(), b.getSym().getSCSymbol().getBytes());
+ case SCV_VEC:
+ {
+ SCVal[] av = a.getVec().getSCVec();
+ SCVal[] bv = b.getVec().getSCVec();
+ int len = Math.min(av.length, bv.length);
+ for (int i = 0; i < len; i++) {
+ int c = compareScVal(av[i], bv[i]);
+ if (c != 0) return c;
+ }
+ return Integer.compare(av.length, bv.length);
+ }
+ case SCV_MAP:
+ {
+ SCMapEntry[] am = a.getMap().getSCMap();
+ SCMapEntry[] bm = b.getMap().getSCMap();
+ int len = Math.min(am.length, bm.length);
+ for (int i = 0; i < len; i++) {
+ int c = compareScVal(am[i].getKey(), bm[i].getKey());
+ if (c != 0) return c;
+ c = compareScVal(am[i].getVal(), bm[i].getVal());
+ if (c != 0) return c;
+ }
+ return Integer.compare(am.length, bm.length);
+ }
+ case SCV_ADDRESS:
+ return compareScAddress(a.getAddress(), b.getAddress());
+ case SCV_ERROR:
+ {
+ int c =
+ Integer.compare(
+ a.getError().getDiscriminant().getValue(),
+ b.getError().getDiscriminant().getValue());
+ if (c != 0) return c;
+ switch (a.getError().getDiscriminant()) {
+ case SCE_CONTRACT:
+ return Long.compare(
+ a.getError().getContractCode().getUint32().getNumber(),
+ b.getError().getContractCode().getUint32().getNumber());
+ default:
+ return Integer.compare(
+ a.getError().getCode().getValue(), b.getError().getCode().getValue());
+ }
+ }
+ case SCV_CONTRACT_INSTANCE:
+ {
+ int c =
+ compareContractExecutable(
+ a.getInstance().getExecutable(), b.getInstance().getExecutable());
+ if (c != 0) return c;
+ return compareOptionalScMap(a.getInstance().getStorage(), b.getInstance().getStorage());
+ }
+ case SCV_LEDGER_KEY_NONCE:
+ return Long.compare(
+ a.getNonce_key().getNonce().getInt64(), b.getNonce_key().getNonce().getInt64());
+ default:
+ throw new IllegalArgumentException("Unsupported SCVal type: " + t);
+ }
+ }
+
+ static int compareScAddress(SCAddress a, SCAddress b) {
+ int c = Integer.compare(a.getDiscriminant().getValue(), b.getDiscriminant().getValue());
+ if (c != 0) return c;
+
+ switch (a.getDiscriminant()) {
+ case SC_ADDRESS_TYPE_ACCOUNT:
+ return compareByteArrays(
+ a.getAccountId().getAccountID().getEd25519().getUint256(),
+ b.getAccountId().getAccountID().getEd25519().getUint256());
+ case SC_ADDRESS_TYPE_CONTRACT:
+ return compareByteArrays(
+ a.getContractId().getContractID().getHash(),
+ b.getContractId().getContractID().getHash());
+ case SC_ADDRESS_TYPE_MUXED_ACCOUNT:
+ {
+ int r =
+ a.getMuxedAccount()
+ .getId()
+ .getUint64()
+ .getNumber()
+ .compareTo(b.getMuxedAccount().getId().getUint64().getNumber());
+ if (r != 0) return r;
+ return compareByteArrays(
+ a.getMuxedAccount().getEd25519().getUint256(),
+ b.getMuxedAccount().getEd25519().getUint256());
+ }
+ case SC_ADDRESS_TYPE_CLAIMABLE_BALANCE:
+ if (a.getClaimableBalanceId().getDiscriminant()
+ != org.stellar.sdk.xdr.ClaimableBalanceIDType.CLAIMABLE_BALANCE_ID_TYPE_V0) {
+ throw new IllegalArgumentException(
+ "Unsupported ClaimableBalanceID type: "
+ + a.getClaimableBalanceId().getDiscriminant());
+ }
+ return compareByteArrays(
+ a.getClaimableBalanceId().getV0().getHash(),
+ b.getClaimableBalanceId().getV0().getHash());
+ case SC_ADDRESS_TYPE_LIQUIDITY_POOL:
+ return compareByteArrays(
+ a.getLiquidityPoolId().getPoolID().getHash(),
+ b.getLiquidityPoolId().getPoolID().getHash());
+ default:
+ throw new IllegalArgumentException("Unsupported SCAddress type: " + a.getDiscriminant());
+ }
+ }
+
+ static int compareContractExecutable(ContractExecutable a, ContractExecutable b) {
+ int c = Integer.compare(a.getDiscriminant().getValue(), b.getDiscriminant().getValue());
+ if (c != 0) return c;
+
+ switch (a.getDiscriminant()) {
+ case CONTRACT_EXECUTABLE_WASM:
+ return compareByteArrays(a.getWasm_hash().getHash(), b.getWasm_hash().getHash());
+ case CONTRACT_EXECUTABLE_STELLAR_ASSET:
+ return 0;
+ default:
+ throw new IllegalArgumentException(
+ "Unsupported ContractExecutable type: " + a.getDiscriminant());
+ }
+ }
+
+ static int compareOptionalScMap(SCMap a, SCMap b) {
+ if (a == null && b == null) return 0;
+ if (a == null) return -1;
+ if (b == null) return 1;
+
+ SCMapEntry[] am = a.getSCMap();
+ SCMapEntry[] bm = b.getSCMap();
+ int len = Math.min(am.length, bm.length);
+ for (int i = 0; i < len; i++) {
+ int c = compareScVal(am[i].getKey(), bm[i].getKey());
+ if (c != 0) return c;
+ c = compareScVal(am[i].getVal(), bm[i].getVal());
+ if (c != 0) return c;
+ }
+ return Integer.compare(am.length, bm.length);
+ }
+
+ /** Lexicographic unsigned byte comparison. */
+ private static int compareByteArrays(byte[] a, byte[] b) {
+ int len = Math.min(a.length, b.length);
+ for (int i = 0; i < len; i++) {
+ int cmp = Integer.compare(a[i] & 0xFF, b[i] & 0xFF);
+ if (cmp != 0) return cmp;
+ }
+ return Integer.compare(a.length, b.length);
+ }
+}
diff --git a/src/main/java/org/stellar/sdk/scval/Scv.java b/src/main/java/org/stellar/sdk/scval/Scv.java
index 688b8be53..11d45eb1a 100644
--- a/src/main/java/org/stellar/sdk/scval/Scv.java
+++ b/src/main/java/org/stellar/sdk/scval/Scv.java
@@ -269,9 +269,27 @@ public static long fromLedgerKeyNonce(SCVal scVal) {
/**
* Build a {@link SCVal} with the type of {@link SCValType#SCV_MAP}.
*
+ * The entries are sorted by key following Soroban runtime ordering rules, as the network
+ * requires ScMap keys to be in ascending order.
+ *
+ * @param map map to convert
+ * @return {@link SCVal} with the type of {@link SCValType#SCV_MAP}
+ */
+ public static SCVal toMap(Map map) {
+ return ScvMap.toSCVal(map);
+ }
+
+ /**
+ * Build a {@link SCVal} with the type of {@link SCValType#SCV_MAP}.
+ *
+ * The entries are sorted by key following Soroban runtime ordering rules, as the network
+ * requires ScMap keys to be in ascending order.
+ *
* @param map map to convert
* @return {@link SCVal} with the type of {@link SCValType#SCV_MAP}
+ * @deprecated Use {@link #toMap(Map)} instead.
*/
+ @Deprecated
public static SCVal toMap(LinkedHashMap map) {
return ScvMap.toSCVal(map);
}
diff --git a/src/main/java/org/stellar/sdk/scval/ScvMap.java b/src/main/java/org/stellar/sdk/scval/ScvMap.java
index 60f0c6700..2e4c0f61b 100644
--- a/src/main/java/org/stellar/sdk/scval/ScvMap.java
+++ b/src/main/java/org/stellar/sdk/scval/ScvMap.java
@@ -1,5 +1,6 @@
package org.stellar.sdk.scval;
+import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import org.stellar.sdk.xdr.SCMap;
@@ -11,14 +12,15 @@
class ScvMap {
private static final SCValType TYPE = SCValType.SCV_MAP;
- // we want to keep the order of the map entries
- // this ensures that the generated XDR is deterministic.
- static SCVal toSCVal(LinkedHashMap value) {
+ // Entries are sorted by key following Soroban runtime ordering rules,
+ // as the network requires ScMap keys to be in ascending order.
+ static SCVal toSCVal(Map value) {
SCMapEntry[] scMapEntries = new SCMapEntry[value.size()];
int i = 0;
for (Map.Entry entry : value.entrySet()) {
scMapEntries[i++] = SCMapEntry.builder().key(entry.getKey()).val(entry.getValue()).build();
}
+ Arrays.sort(scMapEntries, (a, b) -> ScValComparator.compareScVal(a.getKey(), b.getKey()));
return SCVal.builder().discriminant(TYPE).map(new SCMap(scMapEntries)).build();
}
diff --git a/src/test/java/org/stellar/sdk/scval/ScValComparatorTest.java b/src/test/java/org/stellar/sdk/scval/ScValComparatorTest.java
new file mode 100644
index 000000000..16f0419c1
--- /dev/null
+++ b/src/test/java/org/stellar/sdk/scval/ScValComparatorTest.java
@@ -0,0 +1,462 @@
+package org.stellar.sdk.scval;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.math.BigInteger;
+import java.util.Arrays;
+import org.junit.Test;
+import org.stellar.sdk.xdr.AccountID;
+import org.stellar.sdk.xdr.ClaimableBalanceID;
+import org.stellar.sdk.xdr.ClaimableBalanceIDType;
+import org.stellar.sdk.xdr.ContractExecutable;
+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.MuxedEd25519Account;
+import org.stellar.sdk.xdr.PoolID;
+import org.stellar.sdk.xdr.PublicKey;
+import org.stellar.sdk.xdr.PublicKeyType;
+import org.stellar.sdk.xdr.SCAddress;
+import org.stellar.sdk.xdr.SCAddressType;
+import org.stellar.sdk.xdr.SCContractInstance;
+import org.stellar.sdk.xdr.SCError;
+import org.stellar.sdk.xdr.SCErrorCode;
+import org.stellar.sdk.xdr.SCErrorType;
+import org.stellar.sdk.xdr.SCMap;
+import org.stellar.sdk.xdr.SCMapEntry;
+import org.stellar.sdk.xdr.SCNonceKey;
+import org.stellar.sdk.xdr.SCVal;
+import org.stellar.sdk.xdr.SCValType;
+import org.stellar.sdk.xdr.Uint256;
+import org.stellar.sdk.xdr.Uint32;
+import org.stellar.sdk.xdr.Uint64;
+import org.stellar.sdk.xdr.XdrUnsignedHyperInteger;
+import org.stellar.sdk.xdr.XdrUnsignedInteger;
+
+public class ScValComparatorTest {
+
+ private static byte[] bytes32(int fill) {
+ byte[] b = new byte[32];
+ Arrays.fill(b, (byte) fill);
+ return b;
+ }
+
+ private static byte[] bytes32Last(int last) {
+ byte[] b = new byte[32];
+ b[31] = (byte) last;
+ return b;
+ }
+
+ @Test
+ public void testCrossTypeOrdering() {
+ // SCV_BOOL(0) < SCV_VOID(1) < SCV_U32(3) < SCV_SYMBOL(15)
+ assertTrue(ScValComparator.compareScVal(Scv.toBoolean(true), Scv.toVoid()) < 0);
+ assertTrue(ScValComparator.compareScVal(Scv.toVoid(), Scv.toUint32(0)) < 0);
+ assertTrue(ScValComparator.compareScVal(Scv.toUint32(0), Scv.toSymbol("x")) < 0);
+ assertTrue(ScValComparator.compareScVal(Scv.toSymbol("x"), Scv.toVoid()) > 0);
+ }
+
+ @Test
+ public void testBool() {
+ assertTrue(ScValComparator.compareScVal(Scv.toBoolean(false), Scv.toBoolean(true)) < 0);
+ assertTrue(ScValComparator.compareScVal(Scv.toBoolean(true), Scv.toBoolean(false)) > 0);
+ assertEquals(0, ScValComparator.compareScVal(Scv.toBoolean(true), Scv.toBoolean(true)));
+ }
+
+ @Test
+ public void testVoid() {
+ assertEquals(0, ScValComparator.compareScVal(Scv.toVoid(), Scv.toVoid()));
+ }
+
+ @Test
+ public void testU32() {
+ assertTrue(ScValComparator.compareScVal(Scv.toUint32(1), Scv.toUint32(2)) < 0);
+ assertEquals(0, ScValComparator.compareScVal(Scv.toUint32(0), Scv.toUint32(0)));
+ }
+
+ @Test
+ public void testI32SignedOrder() {
+ assertTrue(ScValComparator.compareScVal(Scv.toInt32(-10), Scv.toInt32(-1)) < 0);
+ assertTrue(ScValComparator.compareScVal(Scv.toInt32(-1), Scv.toInt32(0)) < 0);
+ assertEquals(0, ScValComparator.compareScVal(Scv.toInt32(5), Scv.toInt32(5)));
+ }
+
+ @Test
+ public void testU64() {
+ BigInteger maxU64 = new BigInteger("18446744073709551615");
+ assertTrue(
+ ScValComparator.compareScVal(Scv.toUint64(BigInteger.ZERO), Scv.toUint64(maxU64)) < 0);
+ }
+
+ @Test
+ public void testI64() {
+ assertTrue(ScValComparator.compareScVal(Scv.toInt64(-1), Scv.toInt64(0)) < 0);
+ assertTrue(
+ ScValComparator.compareScVal(Scv.toInt64(Long.MIN_VALUE), Scv.toInt64(Long.MAX_VALUE)) < 0);
+ }
+
+ @Test
+ public void testTimepoint() {
+ assertTrue(
+ ScValComparator.compareScVal(
+ Scv.toTimePoint(BigInteger.ONE), Scv.toTimePoint(BigInteger.valueOf(2)))
+ < 0);
+ }
+
+ @Test
+ public void testDuration() {
+ assertTrue(
+ ScValComparator.compareScVal(
+ Scv.toDuration(BigInteger.ONE), Scv.toDuration(BigInteger.valueOf(2)))
+ < 0);
+ }
+
+ @Test
+ public void testU128() {
+ BigInteger twoTo64 = BigInteger.ONE.shiftLeft(64);
+ // hi differs
+ assertTrue(
+ ScValComparator.compareScVal(
+ Scv.toUint128(twoTo64.subtract(BigInteger.ONE)), Scv.toUint128(twoTo64))
+ < 0);
+ assertEquals(
+ 0,
+ ScValComparator.compareScVal(
+ Scv.toUint128(BigInteger.ZERO), Scv.toUint128(BigInteger.ZERO)));
+ }
+
+ @Test
+ public void testI128SignedOrder() {
+ BigInteger minI128 = BigInteger.ONE.shiftLeft(127).negate();
+ assertTrue(
+ ScValComparator.compareScVal(Scv.toInt128(minI128), Scv.toInt128(BigInteger.ZERO)) < 0);
+ assertTrue(
+ ScValComparator.compareScVal(
+ Scv.toInt128(BigInteger.valueOf(-1)), Scv.toInt128(BigInteger.ZERO))
+ < 0);
+ }
+
+ @Test
+ public void testU256() {
+ assertTrue(
+ ScValComparator.compareScVal(Scv.toUint256(BigInteger.ZERO), Scv.toUint256(BigInteger.ONE))
+ < 0);
+ }
+
+ @Test
+ public void testI256SignedOrder() {
+ BigInteger minI256 = BigInteger.ONE.shiftLeft(255).negate();
+ assertTrue(
+ ScValComparator.compareScVal(Scv.toInt256(minI256), Scv.toInt256(BigInteger.ZERO)) < 0);
+ assertTrue(
+ ScValComparator.compareScVal(
+ Scv.toInt256(BigInteger.valueOf(-1)), Scv.toInt256(BigInteger.ZERO))
+ < 0);
+ }
+
+ @Test
+ public void testBytes() {
+ assertTrue(
+ ScValComparator.compareScVal(
+ Scv.toBytes(new byte[] {0x61, 0x62}), Scv.toBytes(new byte[] {0x61, 0x62, 0x63}))
+ < 0);
+ assertEquals(
+ 0,
+ ScValComparator.compareScVal(
+ Scv.toBytes(new byte[] {0x61}), Scv.toBytes(new byte[] {0x61})));
+ }
+
+ @Test
+ public void testString() {
+ assertTrue(ScValComparator.compareScVal(Scv.toString("abc"), Scv.toString("abd")) < 0);
+ assertTrue(ScValComparator.compareScVal(Scv.toString("ab"), Scv.toString("abc")) < 0);
+ }
+
+ @Test
+ public void testSymbol() {
+ assertTrue(ScValComparator.compareScVal(Scv.toSymbol("alpha"), Scv.toSymbol("bravo")) < 0);
+ assertEquals(0, ScValComparator.compareScVal(Scv.toSymbol("x"), Scv.toSymbol("x")));
+ }
+
+ @Test
+ public void testVec() {
+ java.util.List a = Arrays.asList(Scv.toUint32(1), Scv.toUint32(2));
+ java.util.List b = Arrays.asList(Scv.toUint32(1), Scv.toUint32(3));
+ assertTrue(ScValComparator.compareScVal(Scv.toVec(a), Scv.toVec(b)) < 0);
+ // shorter < longer when prefix matches
+ assertTrue(
+ ScValComparator.compareScVal(
+ Scv.toVec(Arrays.asList(Scv.toUint32(1))),
+ Scv.toVec(Arrays.asList(Scv.toUint32(1), Scv.toUint32(2))))
+ < 0);
+ }
+
+ @Test
+ public void testMap() {
+ SCVal a = makeMap(new SCMapEntry[] {entry(Scv.toUint32(1), Scv.toVoid())});
+ SCVal b = makeMap(new SCMapEntry[] {entry(Scv.toUint32(2), Scv.toVoid())});
+ assertTrue(ScValComparator.compareScVal(a, b) < 0);
+ // compare by val when keys equal
+ SCVal c = makeMap(new SCMapEntry[] {entry(Scv.toUint32(1), Scv.toInt32(10))});
+ SCVal d = makeMap(new SCMapEntry[] {entry(Scv.toUint32(1), Scv.toInt32(20))});
+ assertTrue(ScValComparator.compareScVal(c, d) < 0);
+ // shorter < longer
+ SCVal e =
+ makeMap(
+ new SCMapEntry[] {
+ entry(Scv.toUint32(1), Scv.toVoid()), entry(Scv.toUint32(2), Scv.toVoid())
+ });
+ assertTrue(ScValComparator.compareScVal(a, e) < 0);
+ }
+
+ @Test
+ public void testError() {
+ SCVal contractErr = makeErrorContract(1);
+ SCVal wasmErr = makeErrorWasm(SCErrorCode.SCEC_ARITH_DOMAIN);
+ // SCE_CONTRACT(0) < SCE_WASM_VM(1)
+ assertTrue(ScValComparator.compareScVal(contractErr, wasmErr) < 0);
+ // same type, different code
+ assertTrue(ScValComparator.compareScVal(makeErrorContract(1), makeErrorContract(2)) < 0);
+ assertEquals(0, ScValComparator.compareScVal(makeErrorContract(5), makeErrorContract(5)));
+ }
+
+ @Test
+ public void testContractInstance() {
+ SCVal wasm = makeWasmInstance(bytes32(0x00), null);
+ SCVal asset = makeStellarAssetInstance(null);
+ // WASM(0) < STELLAR_ASSET(1)
+ assertTrue(ScValComparator.compareScVal(wasm, asset) < 0);
+ // different wasm hash
+ assertTrue(
+ ScValComparator.compareScVal(
+ makeWasmInstance(bytes32(0x00), null), makeWasmInstance(bytes32Last(0x01), null))
+ < 0);
+ // null storage < non-null storage
+ SCMap storage = new SCMap(new SCMapEntry[] {entry(Scv.toUint32(1), Scv.toVoid())});
+ assertTrue(ScValComparator.compareScVal(asset, makeStellarAssetInstance(storage)) < 0);
+ }
+
+ @Test
+ public void testLedgerKeyNonce() {
+ assertTrue(ScValComparator.compareScVal(makeNonce(-1), makeNonce(0)) < 0);
+ assertEquals(0, ScValComparator.compareScVal(makeNonce(42), makeNonce(42)));
+ }
+
+ @Test
+ public void testLedgerKeyContractInstance() {
+ SCVal a = SCVal.builder().discriminant(SCValType.SCV_LEDGER_KEY_CONTRACT_INSTANCE).build();
+ SCVal b = SCVal.builder().discriminant(SCValType.SCV_LEDGER_KEY_CONTRACT_INSTANCE).build();
+ assertEquals(0, ScValComparator.compareScVal(a, b));
+ }
+
+ @Test
+ public void testAddressTypeOrdering() {
+ // account(0) < contract(1) < muxed(2) < claimable(3) < pool(4)
+ SCAddress[] addrs = {
+ accountAddress(bytes32(0x00)),
+ contractAddress(bytes32(0x00)),
+ muxedAddress(0, bytes32(0x00)),
+ claimableBalanceAddress(bytes32(0x00)),
+ liquidityPoolAddress(bytes32(0x00)),
+ };
+ for (int i = 0; i < addrs.length - 1; i++) {
+ assertTrue(ScValComparator.compareScAddress(addrs[i], addrs[i + 1]) < 0);
+ }
+ }
+
+ @Test
+ public void testAddressSameType() {
+ // account: compare by ed25519
+ assertTrue(
+ ScValComparator.compareScAddress(
+ accountAddress(bytes32(0x00)), accountAddress(bytes32Last(0x01)))
+ < 0);
+ // contract: compare by hash
+ assertTrue(
+ ScValComparator.compareScAddress(
+ contractAddress(bytes32(0x00)), contractAddress(bytes32(0xFF)))
+ < 0);
+ // muxed: id first, then ed25519
+ assertTrue(
+ ScValComparator.compareScAddress(
+ muxedAddress(1, bytes32(0xFF)), muxedAddress(2, bytes32(0x00)))
+ < 0);
+ assertTrue(
+ ScValComparator.compareScAddress(
+ muxedAddress(1, bytes32(0x00)), muxedAddress(1, bytes32Last(0x01)))
+ < 0);
+ assertEquals(
+ 0,
+ ScValComparator.compareScAddress(
+ muxedAddress(5, bytes32(0xAB)), muxedAddress(5, bytes32(0xAB))));
+ }
+
+ @Test
+ public void testContractExecutable() {
+ ContractExecutable wasm =
+ ContractExecutable.builder()
+ .discriminant(ContractExecutableType.CONTRACT_EXECUTABLE_WASM)
+ .wasm_hash(new Hash(bytes32(0x00)))
+ .build();
+ ContractExecutable asset =
+ ContractExecutable.builder()
+ .discriminant(ContractExecutableType.CONTRACT_EXECUTABLE_STELLAR_ASSET)
+ .build();
+ assertTrue(ScValComparator.compareContractExecutable(wasm, asset) < 0);
+ assertEquals(0, ScValComparator.compareContractExecutable(asset, asset));
+ }
+
+ @Test
+ public void testOptionalScMap() {
+ SCMap m1 = new SCMap(new SCMapEntry[] {entry(Scv.toUint32(1), Scv.toVoid())});
+ SCMap m2 = new SCMap(new SCMapEntry[] {entry(Scv.toUint32(2), Scv.toVoid())});
+
+ assertEquals(0, ScValComparator.compareOptionalScMap(null, null));
+ assertTrue(ScValComparator.compareOptionalScMap(null, m1) < 0);
+ assertTrue(ScValComparator.compareOptionalScMap(m1, null) > 0);
+ assertTrue(ScValComparator.compareOptionalScMap(m1, m2) < 0);
+ assertEquals(0, ScValComparator.compareOptionalScMap(m1, m1));
+ }
+
+ @Test
+ public void testAntisymmetry() {
+ SCVal[][] pairs = {
+ {Scv.toBoolean(false), Scv.toBoolean(true)},
+ {Scv.toUint32(1), Scv.toUint32(2)},
+ {Scv.toInt32(-10), Scv.toInt32(10)},
+ {Scv.toSymbol("a"), Scv.toSymbol("b")},
+ // cross-type
+ {Scv.toBoolean(false), Scv.toUint32(0)},
+ {Scv.toInt32(0), Scv.toSymbol("x")},
+ };
+ for (int i = 0; i < pairs.length; i++) {
+ assertEquals(
+ "Antisymmetry failed for pair " + i,
+ ScValComparator.compareScVal(pairs[i][0], pairs[i][1]),
+ -ScValComparator.compareScVal(pairs[i][1], pairs[i][0]));
+ }
+ }
+
+ @Test
+ public void testTransitivity() {
+ // SCV_BOOL(0) < SCV_U32(3) < SCV_SYMBOL(15)
+ SCVal a = Scv.toBoolean(true);
+ SCVal b = Scv.toUint32(0);
+ SCVal c = Scv.toSymbol("x");
+ assertTrue(ScValComparator.compareScVal(a, b) < 0);
+ assertTrue(ScValComparator.compareScVal(b, c) < 0);
+ assertTrue(ScValComparator.compareScVal(a, c) < 0);
+ }
+
+ private static SCMapEntry entry(SCVal key, SCVal val) {
+ return SCMapEntry.builder().key(key).val(val).build();
+ }
+
+ private static SCVal makeMap(SCMapEntry[] entries) {
+ return SCVal.builder().discriminant(SCValType.SCV_MAP).map(new SCMap(entries)).build();
+ }
+
+ private static SCVal makeErrorContract(int code) {
+ return SCVal.builder()
+ .discriminant(SCValType.SCV_ERROR)
+ .error(
+ SCError.builder()
+ .discriminant(SCErrorType.SCE_CONTRACT)
+ .contractCode(new Uint32(new XdrUnsignedInteger((long) code)))
+ .build())
+ .build();
+ }
+
+ private static SCVal makeErrorWasm(SCErrorCode code) {
+ return SCVal.builder()
+ .discriminant(SCValType.SCV_ERROR)
+ .error(SCError.builder().discriminant(SCErrorType.SCE_WASM_VM).code(code).build())
+ .build();
+ }
+
+ private static SCVal makeWasmInstance(byte[] wasmHash, SCMap storage) {
+ return SCVal.builder()
+ .discriminant(SCValType.SCV_CONTRACT_INSTANCE)
+ .instance(
+ SCContractInstance.builder()
+ .executable(
+ ContractExecutable.builder()
+ .discriminant(ContractExecutableType.CONTRACT_EXECUTABLE_WASM)
+ .wasm_hash(new Hash(wasmHash))
+ .build())
+ .storage(storage)
+ .build())
+ .build();
+ }
+
+ private static SCVal makeStellarAssetInstance(SCMap storage) {
+ return SCVal.builder()
+ .discriminant(SCValType.SCV_CONTRACT_INSTANCE)
+ .instance(
+ SCContractInstance.builder()
+ .executable(
+ ContractExecutable.builder()
+ .discriminant(ContractExecutableType.CONTRACT_EXECUTABLE_STELLAR_ASSET)
+ .build())
+ .storage(storage)
+ .build())
+ .build();
+ }
+
+ private static SCVal makeNonce(long v) {
+ return SCVal.builder()
+ .discriminant(SCValType.SCV_LEDGER_KEY_NONCE)
+ .nonce_key(SCNonceKey.builder().nonce(new Int64(v)).build())
+ .build();
+ }
+
+ private static SCAddress accountAddress(byte[] key) {
+ return SCAddress.builder()
+ .discriminant(SCAddressType.SC_ADDRESS_TYPE_ACCOUNT)
+ .accountId(
+ new AccountID(
+ PublicKey.builder()
+ .discriminant(PublicKeyType.PUBLIC_KEY_TYPE_ED25519)
+ .ed25519(new Uint256(key))
+ .build()))
+ .build();
+ }
+
+ private static SCAddress contractAddress(byte[] hash) {
+ return SCAddress.builder()
+ .discriminant(SCAddressType.SC_ADDRESS_TYPE_CONTRACT)
+ .contractId(new ContractID(new Hash(hash)))
+ .build();
+ }
+
+ private static SCAddress muxedAddress(long id, byte[] key) {
+ return SCAddress.builder()
+ .discriminant(SCAddressType.SC_ADDRESS_TYPE_MUXED_ACCOUNT)
+ .muxedAccount(
+ MuxedEd25519Account.builder()
+ .id(new Uint64(new XdrUnsignedHyperInteger(BigInteger.valueOf(id))))
+ .ed25519(new Uint256(key))
+ .build())
+ .build();
+ }
+
+ private static SCAddress claimableBalanceAddress(byte[] hash) {
+ return SCAddress.builder()
+ .discriminant(SCAddressType.SC_ADDRESS_TYPE_CLAIMABLE_BALANCE)
+ .claimableBalanceId(
+ ClaimableBalanceID.builder()
+ .discriminant(ClaimableBalanceIDType.CLAIMABLE_BALANCE_ID_TYPE_V0)
+ .v0(new Hash(hash))
+ .build())
+ .build();
+ }
+
+ private static SCAddress liquidityPoolAddress(byte[] hash) {
+ return SCAddress.builder()
+ .discriminant(SCAddressType.SC_ADDRESS_TYPE_LIQUIDITY_POOL)
+ .liquidityPoolId(new PoolID(new Hash(hash)))
+ .build();
+ }
+}
diff --git a/src/test/java/org/stellar/sdk/scval/ScvMapTest.java b/src/test/java/org/stellar/sdk/scval/ScvMapTest.java
index 6d63de544..5c2a03b81 100644
--- a/src/test/java/org/stellar/sdk/scval/ScvMapTest.java
+++ b/src/test/java/org/stellar/sdk/scval/ScvMapTest.java
@@ -1,6 +1,8 @@
package org.stellar.sdk.scval;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
import java.util.LinkedHashMap;
import org.junit.Test;
@@ -22,16 +24,97 @@ public void testScvMap() {
.map(
new SCMap(
new SCMapEntry[] {
+ SCMapEntry.builder().key(Scv.toString("key2")).val(Scv.toInt32(123)).build(),
SCMapEntry.builder()
.key(Scv.toSymbol("key1"))
.val(Scv.toString("value1"))
.build(),
- SCMapEntry.builder().key(Scv.toString("key2")).val(Scv.toInt32(123)).build(),
}))
.build();
SCVal actualScVal = Scv.toMap(value);
assertEquals(expectedScVal, actualScVal);
- assertEquals(value, Scv.fromMap(actualScVal));
+ }
+
+ @Test
+ public void testToMapSortsKeys() {
+ LinkedHashMap value = new LinkedHashMap<>();
+ value.put(Scv.toUint32(3), Scv.toVoid());
+ value.put(Scv.toUint32(1), Scv.toVoid());
+ value.put(Scv.toUint32(2), Scv.toVoid());
+
+ SCVal scVal = Scv.toMap(value);
+ assertNotNull(scVal.getMap());
+ SCMapEntry[] entries = scVal.getMap().getSCMap();
+ assertEquals(Scv.toUint32(1), entries[0].getKey());
+ assertEquals(Scv.toUint32(2), entries[1].getKey());
+ assertEquals(Scv.toUint32(3), entries[2].getKey());
+ }
+
+ @Test
+ public void testToMapStrictlyIncreasing() {
+ LinkedHashMap value = new LinkedHashMap<>();
+ value.put(Scv.toSymbol("z"), Scv.toVoid());
+ value.put(Scv.toSymbol("a"), Scv.toVoid());
+ value.put(Scv.toUint32(100), Scv.toVoid());
+ value.put(Scv.toBoolean(false), Scv.toVoid());
+ value.put(Scv.toInt32(-1), Scv.toVoid());
+
+ SCVal scVal = Scv.toMap(value);
+ assertNotNull(scVal.getMap());
+ SCMapEntry[] entries = scVal.getMap().getSCMap();
+ for (int i = 0; i < entries.length - 1; i++) {
+ assertTrue(
+ "keys[" + i + "] not strictly less than keys[" + (i + 1) + "]",
+ ScValComparator.compareScVal(entries[i].getKey(), entries[i + 1].getKey()) < 0);
+ }
+ }
+
+ @Test
+ public void testToMapMixedTypes() {
+ LinkedHashMap value = new LinkedHashMap<>();
+ value.put(Scv.toSymbol("x"), Scv.toInt32(1));
+ value.put(Scv.toUint32(42), Scv.toInt32(2));
+ value.put(Scv.toBoolean(true), Scv.toInt32(3));
+
+ SCVal scVal = Scv.toMap(value);
+ assertNotNull(scVal.getMap());
+ SCMapEntry[] entries = scVal.getMap().getSCMap();
+ // SCV_BOOL(0) < SCV_U32(3) < SCV_SYMBOL(15)
+ assertEquals(Scv.toBoolean(true), entries[0].getKey());
+ assertEquals(Scv.toUint32(42), entries[1].getKey());
+ assertEquals(Scv.toSymbol("x"), entries[2].getKey());
+ }
+
+ @Test
+ public void testToMapSignedNegativeBoundaries() {
+ LinkedHashMap value = new LinkedHashMap<>();
+ value.put(Scv.toInt32(0), Scv.toVoid());
+ value.put(Scv.toInt32(Integer.MIN_VALUE), Scv.toVoid());
+ value.put(Scv.toInt32(Integer.MAX_VALUE), Scv.toVoid());
+ value.put(Scv.toInt32(-1), Scv.toVoid());
+
+ SCVal scVal = Scv.toMap(value);
+ assertNotNull(scVal.getMap());
+ SCMapEntry[] entries = scVal.getMap().getSCMap();
+ assertEquals(Scv.toInt32(Integer.MIN_VALUE), entries[0].getKey());
+ assertEquals(Scv.toInt32(-1), entries[1].getKey());
+ assertEquals(Scv.toInt32(0), entries[2].getKey());
+ assertEquals(Scv.toInt32(Integer.MAX_VALUE), entries[3].getKey());
+ }
+
+ @Test
+ public void testToMapAlreadySortedIdempotent() {
+ LinkedHashMap value = new LinkedHashMap<>();
+ value.put(Scv.toUint32(1), Scv.toInt32(10));
+ value.put(Scv.toUint32(2), Scv.toInt32(20));
+ value.put(Scv.toUint32(3), Scv.toInt32(30));
+
+ SCVal scVal = Scv.toMap(value);
+ assertNotNull(scVal.getMap());
+ SCMapEntry[] entries = scVal.getMap().getSCMap();
+ assertEquals(Scv.toUint32(1), entries[0].getKey());
+ assertEquals(Scv.toUint32(2), entries[1].getKey());
+ assertEquals(Scv.toUint32(3), entries[2].getKey());
}
}
From 2cd929875fd97d3d62d05c8381df2eef269905df Mon Sep 17 00:00:00 2001
From: Jun Luo <4catcode@gmail.com>
Date: Wed, 4 Mar 2026 11:10:17 +0800
Subject: [PATCH 2/6] feat: sort ScMap entries by key in `Scv.toMap` following
Soroban runtime ordering
---
.../sdk/scval/ScValComparatorTest.java | 462 ----------------
.../stellar/sdk/scval/ScValComparatorTest.kt | 503 ++++++++++++++++++
2 files changed, 503 insertions(+), 462 deletions(-)
delete mode 100644 src/test/java/org/stellar/sdk/scval/ScValComparatorTest.java
create mode 100644 src/test/kotlin/org/stellar/sdk/scval/ScValComparatorTest.kt
diff --git a/src/test/java/org/stellar/sdk/scval/ScValComparatorTest.java b/src/test/java/org/stellar/sdk/scval/ScValComparatorTest.java
deleted file mode 100644
index 16f0419c1..000000000
--- a/src/test/java/org/stellar/sdk/scval/ScValComparatorTest.java
+++ /dev/null
@@ -1,462 +0,0 @@
-package org.stellar.sdk.scval;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-
-import java.math.BigInteger;
-import java.util.Arrays;
-import org.junit.Test;
-import org.stellar.sdk.xdr.AccountID;
-import org.stellar.sdk.xdr.ClaimableBalanceID;
-import org.stellar.sdk.xdr.ClaimableBalanceIDType;
-import org.stellar.sdk.xdr.ContractExecutable;
-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.MuxedEd25519Account;
-import org.stellar.sdk.xdr.PoolID;
-import org.stellar.sdk.xdr.PublicKey;
-import org.stellar.sdk.xdr.PublicKeyType;
-import org.stellar.sdk.xdr.SCAddress;
-import org.stellar.sdk.xdr.SCAddressType;
-import org.stellar.sdk.xdr.SCContractInstance;
-import org.stellar.sdk.xdr.SCError;
-import org.stellar.sdk.xdr.SCErrorCode;
-import org.stellar.sdk.xdr.SCErrorType;
-import org.stellar.sdk.xdr.SCMap;
-import org.stellar.sdk.xdr.SCMapEntry;
-import org.stellar.sdk.xdr.SCNonceKey;
-import org.stellar.sdk.xdr.SCVal;
-import org.stellar.sdk.xdr.SCValType;
-import org.stellar.sdk.xdr.Uint256;
-import org.stellar.sdk.xdr.Uint32;
-import org.stellar.sdk.xdr.Uint64;
-import org.stellar.sdk.xdr.XdrUnsignedHyperInteger;
-import org.stellar.sdk.xdr.XdrUnsignedInteger;
-
-public class ScValComparatorTest {
-
- private static byte[] bytes32(int fill) {
- byte[] b = new byte[32];
- Arrays.fill(b, (byte) fill);
- return b;
- }
-
- private static byte[] bytes32Last(int last) {
- byte[] b = new byte[32];
- b[31] = (byte) last;
- return b;
- }
-
- @Test
- public void testCrossTypeOrdering() {
- // SCV_BOOL(0) < SCV_VOID(1) < SCV_U32(3) < SCV_SYMBOL(15)
- assertTrue(ScValComparator.compareScVal(Scv.toBoolean(true), Scv.toVoid()) < 0);
- assertTrue(ScValComparator.compareScVal(Scv.toVoid(), Scv.toUint32(0)) < 0);
- assertTrue(ScValComparator.compareScVal(Scv.toUint32(0), Scv.toSymbol("x")) < 0);
- assertTrue(ScValComparator.compareScVal(Scv.toSymbol("x"), Scv.toVoid()) > 0);
- }
-
- @Test
- public void testBool() {
- assertTrue(ScValComparator.compareScVal(Scv.toBoolean(false), Scv.toBoolean(true)) < 0);
- assertTrue(ScValComparator.compareScVal(Scv.toBoolean(true), Scv.toBoolean(false)) > 0);
- assertEquals(0, ScValComparator.compareScVal(Scv.toBoolean(true), Scv.toBoolean(true)));
- }
-
- @Test
- public void testVoid() {
- assertEquals(0, ScValComparator.compareScVal(Scv.toVoid(), Scv.toVoid()));
- }
-
- @Test
- public void testU32() {
- assertTrue(ScValComparator.compareScVal(Scv.toUint32(1), Scv.toUint32(2)) < 0);
- assertEquals(0, ScValComparator.compareScVal(Scv.toUint32(0), Scv.toUint32(0)));
- }
-
- @Test
- public void testI32SignedOrder() {
- assertTrue(ScValComparator.compareScVal(Scv.toInt32(-10), Scv.toInt32(-1)) < 0);
- assertTrue(ScValComparator.compareScVal(Scv.toInt32(-1), Scv.toInt32(0)) < 0);
- assertEquals(0, ScValComparator.compareScVal(Scv.toInt32(5), Scv.toInt32(5)));
- }
-
- @Test
- public void testU64() {
- BigInteger maxU64 = new BigInteger("18446744073709551615");
- assertTrue(
- ScValComparator.compareScVal(Scv.toUint64(BigInteger.ZERO), Scv.toUint64(maxU64)) < 0);
- }
-
- @Test
- public void testI64() {
- assertTrue(ScValComparator.compareScVal(Scv.toInt64(-1), Scv.toInt64(0)) < 0);
- assertTrue(
- ScValComparator.compareScVal(Scv.toInt64(Long.MIN_VALUE), Scv.toInt64(Long.MAX_VALUE)) < 0);
- }
-
- @Test
- public void testTimepoint() {
- assertTrue(
- ScValComparator.compareScVal(
- Scv.toTimePoint(BigInteger.ONE), Scv.toTimePoint(BigInteger.valueOf(2)))
- < 0);
- }
-
- @Test
- public void testDuration() {
- assertTrue(
- ScValComparator.compareScVal(
- Scv.toDuration(BigInteger.ONE), Scv.toDuration(BigInteger.valueOf(2)))
- < 0);
- }
-
- @Test
- public void testU128() {
- BigInteger twoTo64 = BigInteger.ONE.shiftLeft(64);
- // hi differs
- assertTrue(
- ScValComparator.compareScVal(
- Scv.toUint128(twoTo64.subtract(BigInteger.ONE)), Scv.toUint128(twoTo64))
- < 0);
- assertEquals(
- 0,
- ScValComparator.compareScVal(
- Scv.toUint128(BigInteger.ZERO), Scv.toUint128(BigInteger.ZERO)));
- }
-
- @Test
- public void testI128SignedOrder() {
- BigInteger minI128 = BigInteger.ONE.shiftLeft(127).negate();
- assertTrue(
- ScValComparator.compareScVal(Scv.toInt128(minI128), Scv.toInt128(BigInteger.ZERO)) < 0);
- assertTrue(
- ScValComparator.compareScVal(
- Scv.toInt128(BigInteger.valueOf(-1)), Scv.toInt128(BigInteger.ZERO))
- < 0);
- }
-
- @Test
- public void testU256() {
- assertTrue(
- ScValComparator.compareScVal(Scv.toUint256(BigInteger.ZERO), Scv.toUint256(BigInteger.ONE))
- < 0);
- }
-
- @Test
- public void testI256SignedOrder() {
- BigInteger minI256 = BigInteger.ONE.shiftLeft(255).negate();
- assertTrue(
- ScValComparator.compareScVal(Scv.toInt256(minI256), Scv.toInt256(BigInteger.ZERO)) < 0);
- assertTrue(
- ScValComparator.compareScVal(
- Scv.toInt256(BigInteger.valueOf(-1)), Scv.toInt256(BigInteger.ZERO))
- < 0);
- }
-
- @Test
- public void testBytes() {
- assertTrue(
- ScValComparator.compareScVal(
- Scv.toBytes(new byte[] {0x61, 0x62}), Scv.toBytes(new byte[] {0x61, 0x62, 0x63}))
- < 0);
- assertEquals(
- 0,
- ScValComparator.compareScVal(
- Scv.toBytes(new byte[] {0x61}), Scv.toBytes(new byte[] {0x61})));
- }
-
- @Test
- public void testString() {
- assertTrue(ScValComparator.compareScVal(Scv.toString("abc"), Scv.toString("abd")) < 0);
- assertTrue(ScValComparator.compareScVal(Scv.toString("ab"), Scv.toString("abc")) < 0);
- }
-
- @Test
- public void testSymbol() {
- assertTrue(ScValComparator.compareScVal(Scv.toSymbol("alpha"), Scv.toSymbol("bravo")) < 0);
- assertEquals(0, ScValComparator.compareScVal(Scv.toSymbol("x"), Scv.toSymbol("x")));
- }
-
- @Test
- public void testVec() {
- java.util.List a = Arrays.asList(Scv.toUint32(1), Scv.toUint32(2));
- java.util.List b = Arrays.asList(Scv.toUint32(1), Scv.toUint32(3));
- assertTrue(ScValComparator.compareScVal(Scv.toVec(a), Scv.toVec(b)) < 0);
- // shorter < longer when prefix matches
- assertTrue(
- ScValComparator.compareScVal(
- Scv.toVec(Arrays.asList(Scv.toUint32(1))),
- Scv.toVec(Arrays.asList(Scv.toUint32(1), Scv.toUint32(2))))
- < 0);
- }
-
- @Test
- public void testMap() {
- SCVal a = makeMap(new SCMapEntry[] {entry(Scv.toUint32(1), Scv.toVoid())});
- SCVal b = makeMap(new SCMapEntry[] {entry(Scv.toUint32(2), Scv.toVoid())});
- assertTrue(ScValComparator.compareScVal(a, b) < 0);
- // compare by val when keys equal
- SCVal c = makeMap(new SCMapEntry[] {entry(Scv.toUint32(1), Scv.toInt32(10))});
- SCVal d = makeMap(new SCMapEntry[] {entry(Scv.toUint32(1), Scv.toInt32(20))});
- assertTrue(ScValComparator.compareScVal(c, d) < 0);
- // shorter < longer
- SCVal e =
- makeMap(
- new SCMapEntry[] {
- entry(Scv.toUint32(1), Scv.toVoid()), entry(Scv.toUint32(2), Scv.toVoid())
- });
- assertTrue(ScValComparator.compareScVal(a, e) < 0);
- }
-
- @Test
- public void testError() {
- SCVal contractErr = makeErrorContract(1);
- SCVal wasmErr = makeErrorWasm(SCErrorCode.SCEC_ARITH_DOMAIN);
- // SCE_CONTRACT(0) < SCE_WASM_VM(1)
- assertTrue(ScValComparator.compareScVal(contractErr, wasmErr) < 0);
- // same type, different code
- assertTrue(ScValComparator.compareScVal(makeErrorContract(1), makeErrorContract(2)) < 0);
- assertEquals(0, ScValComparator.compareScVal(makeErrorContract(5), makeErrorContract(5)));
- }
-
- @Test
- public void testContractInstance() {
- SCVal wasm = makeWasmInstance(bytes32(0x00), null);
- SCVal asset = makeStellarAssetInstance(null);
- // WASM(0) < STELLAR_ASSET(1)
- assertTrue(ScValComparator.compareScVal(wasm, asset) < 0);
- // different wasm hash
- assertTrue(
- ScValComparator.compareScVal(
- makeWasmInstance(bytes32(0x00), null), makeWasmInstance(bytes32Last(0x01), null))
- < 0);
- // null storage < non-null storage
- SCMap storage = new SCMap(new SCMapEntry[] {entry(Scv.toUint32(1), Scv.toVoid())});
- assertTrue(ScValComparator.compareScVal(asset, makeStellarAssetInstance(storage)) < 0);
- }
-
- @Test
- public void testLedgerKeyNonce() {
- assertTrue(ScValComparator.compareScVal(makeNonce(-1), makeNonce(0)) < 0);
- assertEquals(0, ScValComparator.compareScVal(makeNonce(42), makeNonce(42)));
- }
-
- @Test
- public void testLedgerKeyContractInstance() {
- SCVal a = SCVal.builder().discriminant(SCValType.SCV_LEDGER_KEY_CONTRACT_INSTANCE).build();
- SCVal b = SCVal.builder().discriminant(SCValType.SCV_LEDGER_KEY_CONTRACT_INSTANCE).build();
- assertEquals(0, ScValComparator.compareScVal(a, b));
- }
-
- @Test
- public void testAddressTypeOrdering() {
- // account(0) < contract(1) < muxed(2) < claimable(3) < pool(4)
- SCAddress[] addrs = {
- accountAddress(bytes32(0x00)),
- contractAddress(bytes32(0x00)),
- muxedAddress(0, bytes32(0x00)),
- claimableBalanceAddress(bytes32(0x00)),
- liquidityPoolAddress(bytes32(0x00)),
- };
- for (int i = 0; i < addrs.length - 1; i++) {
- assertTrue(ScValComparator.compareScAddress(addrs[i], addrs[i + 1]) < 0);
- }
- }
-
- @Test
- public void testAddressSameType() {
- // account: compare by ed25519
- assertTrue(
- ScValComparator.compareScAddress(
- accountAddress(bytes32(0x00)), accountAddress(bytes32Last(0x01)))
- < 0);
- // contract: compare by hash
- assertTrue(
- ScValComparator.compareScAddress(
- contractAddress(bytes32(0x00)), contractAddress(bytes32(0xFF)))
- < 0);
- // muxed: id first, then ed25519
- assertTrue(
- ScValComparator.compareScAddress(
- muxedAddress(1, bytes32(0xFF)), muxedAddress(2, bytes32(0x00)))
- < 0);
- assertTrue(
- ScValComparator.compareScAddress(
- muxedAddress(1, bytes32(0x00)), muxedAddress(1, bytes32Last(0x01)))
- < 0);
- assertEquals(
- 0,
- ScValComparator.compareScAddress(
- muxedAddress(5, bytes32(0xAB)), muxedAddress(5, bytes32(0xAB))));
- }
-
- @Test
- public void testContractExecutable() {
- ContractExecutable wasm =
- ContractExecutable.builder()
- .discriminant(ContractExecutableType.CONTRACT_EXECUTABLE_WASM)
- .wasm_hash(new Hash(bytes32(0x00)))
- .build();
- ContractExecutable asset =
- ContractExecutable.builder()
- .discriminant(ContractExecutableType.CONTRACT_EXECUTABLE_STELLAR_ASSET)
- .build();
- assertTrue(ScValComparator.compareContractExecutable(wasm, asset) < 0);
- assertEquals(0, ScValComparator.compareContractExecutable(asset, asset));
- }
-
- @Test
- public void testOptionalScMap() {
- SCMap m1 = new SCMap(new SCMapEntry[] {entry(Scv.toUint32(1), Scv.toVoid())});
- SCMap m2 = new SCMap(new SCMapEntry[] {entry(Scv.toUint32(2), Scv.toVoid())});
-
- assertEquals(0, ScValComparator.compareOptionalScMap(null, null));
- assertTrue(ScValComparator.compareOptionalScMap(null, m1) < 0);
- assertTrue(ScValComparator.compareOptionalScMap(m1, null) > 0);
- assertTrue(ScValComparator.compareOptionalScMap(m1, m2) < 0);
- assertEquals(0, ScValComparator.compareOptionalScMap(m1, m1));
- }
-
- @Test
- public void testAntisymmetry() {
- SCVal[][] pairs = {
- {Scv.toBoolean(false), Scv.toBoolean(true)},
- {Scv.toUint32(1), Scv.toUint32(2)},
- {Scv.toInt32(-10), Scv.toInt32(10)},
- {Scv.toSymbol("a"), Scv.toSymbol("b")},
- // cross-type
- {Scv.toBoolean(false), Scv.toUint32(0)},
- {Scv.toInt32(0), Scv.toSymbol("x")},
- };
- for (int i = 0; i < pairs.length; i++) {
- assertEquals(
- "Antisymmetry failed for pair " + i,
- ScValComparator.compareScVal(pairs[i][0], pairs[i][1]),
- -ScValComparator.compareScVal(pairs[i][1], pairs[i][0]));
- }
- }
-
- @Test
- public void testTransitivity() {
- // SCV_BOOL(0) < SCV_U32(3) < SCV_SYMBOL(15)
- SCVal a = Scv.toBoolean(true);
- SCVal b = Scv.toUint32(0);
- SCVal c = Scv.toSymbol("x");
- assertTrue(ScValComparator.compareScVal(a, b) < 0);
- assertTrue(ScValComparator.compareScVal(b, c) < 0);
- assertTrue(ScValComparator.compareScVal(a, c) < 0);
- }
-
- private static SCMapEntry entry(SCVal key, SCVal val) {
- return SCMapEntry.builder().key(key).val(val).build();
- }
-
- private static SCVal makeMap(SCMapEntry[] entries) {
- return SCVal.builder().discriminant(SCValType.SCV_MAP).map(new SCMap(entries)).build();
- }
-
- private static SCVal makeErrorContract(int code) {
- return SCVal.builder()
- .discriminant(SCValType.SCV_ERROR)
- .error(
- SCError.builder()
- .discriminant(SCErrorType.SCE_CONTRACT)
- .contractCode(new Uint32(new XdrUnsignedInteger((long) code)))
- .build())
- .build();
- }
-
- private static SCVal makeErrorWasm(SCErrorCode code) {
- return SCVal.builder()
- .discriminant(SCValType.SCV_ERROR)
- .error(SCError.builder().discriminant(SCErrorType.SCE_WASM_VM).code(code).build())
- .build();
- }
-
- private static SCVal makeWasmInstance(byte[] wasmHash, SCMap storage) {
- return SCVal.builder()
- .discriminant(SCValType.SCV_CONTRACT_INSTANCE)
- .instance(
- SCContractInstance.builder()
- .executable(
- ContractExecutable.builder()
- .discriminant(ContractExecutableType.CONTRACT_EXECUTABLE_WASM)
- .wasm_hash(new Hash(wasmHash))
- .build())
- .storage(storage)
- .build())
- .build();
- }
-
- private static SCVal makeStellarAssetInstance(SCMap storage) {
- return SCVal.builder()
- .discriminant(SCValType.SCV_CONTRACT_INSTANCE)
- .instance(
- SCContractInstance.builder()
- .executable(
- ContractExecutable.builder()
- .discriminant(ContractExecutableType.CONTRACT_EXECUTABLE_STELLAR_ASSET)
- .build())
- .storage(storage)
- .build())
- .build();
- }
-
- private static SCVal makeNonce(long v) {
- return SCVal.builder()
- .discriminant(SCValType.SCV_LEDGER_KEY_NONCE)
- .nonce_key(SCNonceKey.builder().nonce(new Int64(v)).build())
- .build();
- }
-
- private static SCAddress accountAddress(byte[] key) {
- return SCAddress.builder()
- .discriminant(SCAddressType.SC_ADDRESS_TYPE_ACCOUNT)
- .accountId(
- new AccountID(
- PublicKey.builder()
- .discriminant(PublicKeyType.PUBLIC_KEY_TYPE_ED25519)
- .ed25519(new Uint256(key))
- .build()))
- .build();
- }
-
- private static SCAddress contractAddress(byte[] hash) {
- return SCAddress.builder()
- .discriminant(SCAddressType.SC_ADDRESS_TYPE_CONTRACT)
- .contractId(new ContractID(new Hash(hash)))
- .build();
- }
-
- private static SCAddress muxedAddress(long id, byte[] key) {
- return SCAddress.builder()
- .discriminant(SCAddressType.SC_ADDRESS_TYPE_MUXED_ACCOUNT)
- .muxedAccount(
- MuxedEd25519Account.builder()
- .id(new Uint64(new XdrUnsignedHyperInteger(BigInteger.valueOf(id))))
- .ed25519(new Uint256(key))
- .build())
- .build();
- }
-
- private static SCAddress claimableBalanceAddress(byte[] hash) {
- return SCAddress.builder()
- .discriminant(SCAddressType.SC_ADDRESS_TYPE_CLAIMABLE_BALANCE)
- .claimableBalanceId(
- ClaimableBalanceID.builder()
- .discriminant(ClaimableBalanceIDType.CLAIMABLE_BALANCE_ID_TYPE_V0)
- .v0(new Hash(hash))
- .build())
- .build();
- }
-
- private static SCAddress liquidityPoolAddress(byte[] hash) {
- return SCAddress.builder()
- .discriminant(SCAddressType.SC_ADDRESS_TYPE_LIQUIDITY_POOL)
- .liquidityPoolId(new PoolID(new Hash(hash)))
- .build();
- }
-}
diff --git a/src/test/kotlin/org/stellar/sdk/scval/ScValComparatorTest.kt b/src/test/kotlin/org/stellar/sdk/scval/ScValComparatorTest.kt
new file mode 100644
index 000000000..2b86ec3ec
--- /dev/null
+++ b/src/test/kotlin/org/stellar/sdk/scval/ScValComparatorTest.kt
@@ -0,0 +1,503 @@
+package org.stellar.sdk.scval
+
+import io.kotest.core.spec.style.FunSpec
+import io.kotest.matchers.ints.shouldBeGreaterThan
+import io.kotest.matchers.ints.shouldBeLessThan
+import io.kotest.matchers.shouldBe
+import java.math.BigInteger
+import org.stellar.sdk.xdr.AccountID
+import org.stellar.sdk.xdr.ClaimableBalanceID
+import org.stellar.sdk.xdr.ClaimableBalanceIDType
+import org.stellar.sdk.xdr.ContractExecutable
+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.MuxedEd25519Account
+import org.stellar.sdk.xdr.PoolID
+import org.stellar.sdk.xdr.PublicKey
+import org.stellar.sdk.xdr.PublicKeyType
+import org.stellar.sdk.xdr.SCAddress
+import org.stellar.sdk.xdr.SCAddressType
+import org.stellar.sdk.xdr.SCContractInstance
+import org.stellar.sdk.xdr.SCError
+import org.stellar.sdk.xdr.SCErrorCode
+import org.stellar.sdk.xdr.SCErrorType
+import org.stellar.sdk.xdr.SCMap
+import org.stellar.sdk.xdr.SCMapEntry
+import org.stellar.sdk.xdr.SCNonceKey
+import org.stellar.sdk.xdr.SCVal
+import org.stellar.sdk.xdr.SCValType
+import org.stellar.sdk.xdr.Uint256
+import org.stellar.sdk.xdr.Uint32
+import org.stellar.sdk.xdr.Uint64
+import org.stellar.sdk.xdr.XdrUnsignedHyperInteger
+import org.stellar.sdk.xdr.XdrUnsignedInteger
+
+class ScValComparatorTest :
+ FunSpec({
+ context("cross-type ordering") {
+ test("SCV_BOOL < SCV_VOID < SCV_U32 < SCV_SYMBOL") {
+ ScValComparator.compareScVal(Scv.toBoolean(true), Scv.toVoid()) shouldBeLessThan 0
+ ScValComparator.compareScVal(Scv.toVoid(), Scv.toUint32(0)) shouldBeLessThan 0
+ ScValComparator.compareScVal(Scv.toUint32(0), Scv.toSymbol("x")) shouldBeLessThan 0
+ ScValComparator.compareScVal(Scv.toSymbol("x"), Scv.toVoid()) shouldBeGreaterThan 0
+ }
+ }
+
+ context("SCV_BOOL") {
+ test("false < true") {
+ ScValComparator.compareScVal(Scv.toBoolean(false), Scv.toBoolean(true)) shouldBeLessThan 0
+ ScValComparator.compareScVal(Scv.toBoolean(true), Scv.toBoolean(false)) shouldBeGreaterThan
+ 0
+ ScValComparator.compareScVal(Scv.toBoolean(true), Scv.toBoolean(true)) shouldBe 0
+ }
+ }
+
+ context("SCV_VOID") {
+ test("void == void") { ScValComparator.compareScVal(Scv.toVoid(), Scv.toVoid()) shouldBe 0 }
+ }
+
+ context("SCV_U32") {
+ test("numeric ordering") {
+ ScValComparator.compareScVal(Scv.toUint32(1), Scv.toUint32(2)) shouldBeLessThan 0
+ ScValComparator.compareScVal(Scv.toUint32(0), Scv.toUint32(0)) shouldBe 0
+ }
+ }
+
+ context("SCV_I32") {
+ test("signed ordering") {
+ ScValComparator.compareScVal(Scv.toInt32(-10), Scv.toInt32(-1)) shouldBeLessThan 0
+ ScValComparator.compareScVal(Scv.toInt32(-1), Scv.toInt32(0)) shouldBeLessThan 0
+ ScValComparator.compareScVal(Scv.toInt32(5), Scv.toInt32(5)) shouldBe 0
+ }
+ }
+
+ context("SCV_U64") {
+ test("unsigned ordering including max") {
+ val maxU64 = BigInteger("18446744073709551615")
+ ScValComparator.compareScVal(
+ Scv.toUint64(BigInteger.ZERO),
+ Scv.toUint64(maxU64),
+ ) shouldBeLessThan 0
+ }
+ }
+
+ context("SCV_I64") {
+ test("signed ordering") {
+ ScValComparator.compareScVal(Scv.toInt64(-1), Scv.toInt64(0)) shouldBeLessThan 0
+ ScValComparator.compareScVal(
+ Scv.toInt64(Long.MIN_VALUE),
+ Scv.toInt64(Long.MAX_VALUE),
+ ) shouldBeLessThan 0
+ }
+ }
+
+ context("SCV_TIMEPOINT") {
+ test("numeric ordering") {
+ ScValComparator.compareScVal(
+ Scv.toTimePoint(BigInteger.ONE),
+ Scv.toTimePoint(BigInteger.valueOf(2)),
+ ) shouldBeLessThan 0
+ }
+ }
+
+ context("SCV_DURATION") {
+ test("numeric ordering") {
+ ScValComparator.compareScVal(
+ Scv.toDuration(BigInteger.ONE),
+ Scv.toDuration(BigInteger.valueOf(2)),
+ ) shouldBeLessThan 0
+ }
+ }
+
+ context("SCV_U128") {
+ test("tuple (hi, lo) ordering") {
+ val twoTo64 = BigInteger.ONE.shiftLeft(64)
+ ScValComparator.compareScVal(
+ Scv.toUint128(twoTo64.subtract(BigInteger.ONE)),
+ Scv.toUint128(twoTo64),
+ ) shouldBeLessThan 0
+ ScValComparator.compareScVal(
+ Scv.toUint128(BigInteger.ZERO),
+ Scv.toUint128(BigInteger.ZERO),
+ ) shouldBe 0
+ }
+ }
+
+ context("SCV_I128") {
+ test("signed ordering including negative") {
+ val minI128 = BigInteger.ONE.shiftLeft(127).negate()
+ ScValComparator.compareScVal(
+ Scv.toInt128(minI128),
+ Scv.toInt128(BigInteger.ZERO),
+ ) shouldBeLessThan 0
+ ScValComparator.compareScVal(
+ Scv.toInt128(BigInteger.valueOf(-1)),
+ Scv.toInt128(BigInteger.ZERO),
+ ) shouldBeLessThan 0
+ }
+ }
+
+ context("SCV_U256") {
+ test("numeric ordering") {
+ ScValComparator.compareScVal(
+ Scv.toUint256(BigInteger.ZERO),
+ Scv.toUint256(BigInteger.ONE),
+ ) shouldBeLessThan 0
+ }
+ }
+
+ context("SCV_I256") {
+ test("signed ordering including negative") {
+ val minI256 = BigInteger.ONE.shiftLeft(255).negate()
+ ScValComparator.compareScVal(
+ Scv.toInt256(minI256),
+ Scv.toInt256(BigInteger.ZERO),
+ ) shouldBeLessThan 0
+ ScValComparator.compareScVal(
+ Scv.toInt256(BigInteger.valueOf(-1)),
+ Scv.toInt256(BigInteger.ZERO),
+ ) shouldBeLessThan 0
+ }
+ }
+
+ context("SCV_BYTES") {
+ test("lexicographic ordering and prefix rule") {
+ ScValComparator.compareScVal(
+ Scv.toBytes(byteArrayOf(0x61, 0x62)),
+ Scv.toBytes(byteArrayOf(0x61, 0x62, 0x63)),
+ ) shouldBeLessThan 0
+ ScValComparator.compareScVal(
+ Scv.toBytes(byteArrayOf(0x61)),
+ Scv.toBytes(byteArrayOf(0x61)),
+ ) shouldBe 0
+ }
+ }
+
+ context("SCV_STRING") {
+ test("lexicographic ordering and prefix rule") {
+ ScValComparator.compareScVal(Scv.toString("abc"), Scv.toString("abd")) shouldBeLessThan 0
+ ScValComparator.compareScVal(Scv.toString("ab"), Scv.toString("abc")) shouldBeLessThan 0
+ }
+ }
+
+ context("SCV_SYMBOL") {
+ test("lexicographic ordering") {
+ ScValComparator.compareScVal(Scv.toSymbol("alpha"), Scv.toSymbol("bravo")) shouldBeLessThan
+ 0
+ ScValComparator.compareScVal(Scv.toSymbol("x"), Scv.toSymbol("x")) shouldBe 0
+ }
+ }
+
+ context("SCV_VEC") {
+ test("element-by-element ordering") {
+ val a = listOf(Scv.toUint32(1), Scv.toUint32(2))
+ val b = listOf(Scv.toUint32(1), Scv.toUint32(3))
+ ScValComparator.compareScVal(Scv.toVec(a), Scv.toVec(b)) shouldBeLessThan 0
+ }
+
+ test("shorter < longer when prefix matches") {
+ ScValComparator.compareScVal(
+ Scv.toVec(listOf(Scv.toUint32(1))),
+ Scv.toVec(listOf(Scv.toUint32(1), Scv.toUint32(2))),
+ ) shouldBeLessThan 0
+ }
+ }
+
+ context("SCV_MAP") {
+ test("compare by key") {
+ val a = makeMap(arrayOf(entry(Scv.toUint32(1), Scv.toVoid())))
+ val b = makeMap(arrayOf(entry(Scv.toUint32(2), Scv.toVoid())))
+ ScValComparator.compareScVal(a, b) shouldBeLessThan 0
+ }
+
+ test("compare by val when keys equal") {
+ val c = makeMap(arrayOf(entry(Scv.toUint32(1), Scv.toInt32(10))))
+ val d = makeMap(arrayOf(entry(Scv.toUint32(1), Scv.toInt32(20))))
+ ScValComparator.compareScVal(c, d) shouldBeLessThan 0
+ }
+
+ test("shorter < longer") {
+ val a = makeMap(arrayOf(entry(Scv.toUint32(1), Scv.toVoid())))
+ val e =
+ makeMap(
+ arrayOf(entry(Scv.toUint32(1), Scv.toVoid()), entry(Scv.toUint32(2), Scv.toVoid()))
+ )
+ ScValComparator.compareScVal(a, e) shouldBeLessThan 0
+ }
+ }
+
+ context("SCV_ERROR") {
+ test("SCE_CONTRACT < SCE_WASM_VM") {
+ val contractErr = makeErrorContract(1)
+ val wasmErr = makeErrorWasm(SCErrorCode.SCEC_ARITH_DOMAIN)
+ ScValComparator.compareScVal(contractErr, wasmErr) shouldBeLessThan 0
+ }
+
+ test("same type, different code") {
+ ScValComparator.compareScVal(makeErrorContract(1), makeErrorContract(2)) shouldBeLessThan 0
+ ScValComparator.compareScVal(makeErrorContract(5), makeErrorContract(5)) shouldBe 0
+ }
+ }
+
+ context("SCV_CONTRACT_INSTANCE") {
+ test("WASM < STELLAR_ASSET") {
+ val wasm = makeWasmInstance(bytes32(0x00), null)
+ val asset = makeStellarAssetInstance(null)
+ ScValComparator.compareScVal(wasm, asset) shouldBeLessThan 0
+ }
+
+ test("different wasm hash") {
+ ScValComparator.compareScVal(
+ makeWasmInstance(bytes32(0x00), null),
+ makeWasmInstance(bytes32Last(0x01), null),
+ ) shouldBeLessThan 0
+ }
+
+ test("null storage < non-null storage") {
+ val asset = makeStellarAssetInstance(null)
+ val storage = SCMap(arrayOf(entry(Scv.toUint32(1), Scv.toVoid())))
+ ScValComparator.compareScVal(asset, makeStellarAssetInstance(storage)) shouldBeLessThan 0
+ }
+ }
+
+ context("SCV_LEDGER_KEY_NONCE") {
+ test("signed ordering") {
+ ScValComparator.compareScVal(makeNonce(-1), makeNonce(0)) shouldBeLessThan 0
+ ScValComparator.compareScVal(makeNonce(42), makeNonce(42)) shouldBe 0
+ }
+ }
+
+ context("SCV_LEDGER_KEY_CONTRACT_INSTANCE") {
+ test("always equal") {
+ val a = SCVal.builder().discriminant(SCValType.SCV_LEDGER_KEY_CONTRACT_INSTANCE).build()
+ val b = SCVal.builder().discriminant(SCValType.SCV_LEDGER_KEY_CONTRACT_INSTANCE).build()
+ ScValComparator.compareScVal(a, b) shouldBe 0
+ }
+ }
+
+ context("compareScAddress") {
+ test("address type ordering: account < contract < muxed < claimable < pool") {
+ val addrs =
+ arrayOf(
+ accountAddress(bytes32(0x00)),
+ contractAddress(bytes32(0x00)),
+ muxedAddress(0, bytes32(0x00)),
+ claimableBalanceAddress(bytes32(0x00)),
+ liquidityPoolAddress(bytes32(0x00)),
+ )
+ for (i in 0 until addrs.size - 1) {
+ ScValComparator.compareScAddress(addrs[i], addrs[i + 1]) shouldBeLessThan 0
+ }
+ }
+
+ test("account: compare by ed25519") {
+ ScValComparator.compareScAddress(
+ accountAddress(bytes32(0x00)),
+ accountAddress(bytes32Last(0x01)),
+ ) shouldBeLessThan 0
+ }
+
+ test("contract: compare by hash") {
+ ScValComparator.compareScAddress(
+ contractAddress(bytes32(0x00)),
+ contractAddress(bytes32(0xFF)),
+ ) shouldBeLessThan 0
+ }
+
+ test("muxed: id first, then ed25519") {
+ ScValComparator.compareScAddress(
+ muxedAddress(1, bytes32(0xFF)),
+ muxedAddress(2, bytes32(0x00)),
+ ) shouldBeLessThan 0
+ ScValComparator.compareScAddress(
+ muxedAddress(1, bytes32(0x00)),
+ muxedAddress(1, bytes32Last(0x01)),
+ ) shouldBeLessThan 0
+ ScValComparator.compareScAddress(
+ muxedAddress(5, bytes32(0xAB)),
+ muxedAddress(5, bytes32(0xAB)),
+ ) shouldBe 0
+ }
+ }
+
+ context("compareContractExecutable") {
+ test("WASM < STELLAR_ASSET") {
+ val wasm =
+ ContractExecutable.builder()
+ .discriminant(ContractExecutableType.CONTRACT_EXECUTABLE_WASM)
+ .wasm_hash(Hash(bytes32(0x00)))
+ .build()
+ val asset =
+ ContractExecutable.builder()
+ .discriminant(ContractExecutableType.CONTRACT_EXECUTABLE_STELLAR_ASSET)
+ .build()
+ ScValComparator.compareContractExecutable(wasm, asset) shouldBeLessThan 0
+ ScValComparator.compareContractExecutable(asset, asset) shouldBe 0
+ }
+ }
+
+ context("compareOptionalScMap") {
+ test("null handling and ordering") {
+ val m1 = SCMap(arrayOf(entry(Scv.toUint32(1), Scv.toVoid())))
+ val m2 = SCMap(arrayOf(entry(Scv.toUint32(2), Scv.toVoid())))
+
+ ScValComparator.compareOptionalScMap(null, null) shouldBe 0
+ ScValComparator.compareOptionalScMap(null, m1) shouldBeLessThan 0
+ ScValComparator.compareOptionalScMap(m1, null) shouldBeGreaterThan 0
+ ScValComparator.compareOptionalScMap(m1, m2) shouldBeLessThan 0
+ ScValComparator.compareOptionalScMap(m1, m1) shouldBe 0
+ }
+ }
+
+ context("antisymmetry") {
+ test("compare(a,b) == -compare(b,a)") {
+ val pairs =
+ arrayOf(
+ arrayOf(Scv.toBoolean(false), Scv.toBoolean(true)),
+ arrayOf(Scv.toUint32(1), Scv.toUint32(2)),
+ arrayOf(Scv.toInt32(-10), Scv.toInt32(10)),
+ arrayOf(Scv.toSymbol("a"), Scv.toSymbol("b")),
+ arrayOf(Scv.toBoolean(false), Scv.toUint32(0)),
+ arrayOf(Scv.toInt32(0), Scv.toSymbol("x")),
+ )
+ for ((i, pair) in pairs.withIndex()) {
+ ScValComparator.compareScVal(pair[0], pair[1]) shouldBe
+ -ScValComparator.compareScVal(pair[1], pair[0])
+ }
+ }
+ }
+
+ context("transitivity") {
+ test("a < b < c implies a < c") {
+ val a = Scv.toBoolean(true)
+ val b = Scv.toUint32(0)
+ val c = Scv.toSymbol("x")
+ ScValComparator.compareScVal(a, b) shouldBeLessThan 0
+ ScValComparator.compareScVal(b, c) shouldBeLessThan 0
+ ScValComparator.compareScVal(a, c) shouldBeLessThan 0
+ }
+ }
+ }) {
+ companion object {
+ private fun bytes32(fill: Int): ByteArray {
+ val b = ByteArray(32)
+ b.fill(fill.toByte())
+ return b
+ }
+
+ private fun bytes32Last(last: Int): ByteArray {
+ val b = ByteArray(32)
+ b[31] = last.toByte()
+ return b
+ }
+
+ private fun entry(key: SCVal, value: SCVal): SCMapEntry =
+ SCMapEntry.builder().key(key).`val`(value).build()
+
+ private fun makeMap(entries: Array): SCVal =
+ SCVal.builder().discriminant(SCValType.SCV_MAP).map(SCMap(entries)).build()
+
+ private fun makeErrorContract(code: Int): SCVal =
+ SCVal.builder()
+ .discriminant(SCValType.SCV_ERROR)
+ .error(
+ SCError.builder()
+ .discriminant(SCErrorType.SCE_CONTRACT)
+ .contractCode(Uint32(XdrUnsignedInteger(code.toLong())))
+ .build()
+ )
+ .build()
+
+ private fun makeErrorWasm(code: SCErrorCode): SCVal =
+ SCVal.builder()
+ .discriminant(SCValType.SCV_ERROR)
+ .error(SCError.builder().discriminant(SCErrorType.SCE_WASM_VM).code(code).build())
+ .build()
+
+ private fun makeWasmInstance(wasmHash: ByteArray, storage: SCMap?): SCVal =
+ SCVal.builder()
+ .discriminant(SCValType.SCV_CONTRACT_INSTANCE)
+ .instance(
+ SCContractInstance.builder()
+ .executable(
+ ContractExecutable.builder()
+ .discriminant(ContractExecutableType.CONTRACT_EXECUTABLE_WASM)
+ .wasm_hash(Hash(wasmHash))
+ .build()
+ )
+ .storage(storage)
+ .build()
+ )
+ .build()
+
+ private fun makeStellarAssetInstance(storage: SCMap?): SCVal =
+ SCVal.builder()
+ .discriminant(SCValType.SCV_CONTRACT_INSTANCE)
+ .instance(
+ SCContractInstance.builder()
+ .executable(
+ ContractExecutable.builder()
+ .discriminant(ContractExecutableType.CONTRACT_EXECUTABLE_STELLAR_ASSET)
+ .build()
+ )
+ .storage(storage)
+ .build()
+ )
+ .build()
+
+ private fun makeNonce(v: Long): SCVal =
+ SCVal.builder()
+ .discriminant(SCValType.SCV_LEDGER_KEY_NONCE)
+ .nonce_key(SCNonceKey.builder().nonce(Int64(v)).build())
+ .build()
+
+ private fun accountAddress(key: ByteArray): SCAddress =
+ SCAddress.builder()
+ .discriminant(SCAddressType.SC_ADDRESS_TYPE_ACCOUNT)
+ .accountId(
+ AccountID(
+ PublicKey.builder()
+ .discriminant(PublicKeyType.PUBLIC_KEY_TYPE_ED25519)
+ .ed25519(Uint256(key))
+ .build()
+ )
+ )
+ .build()
+
+ private fun contractAddress(hash: ByteArray): SCAddress =
+ SCAddress.builder()
+ .discriminant(SCAddressType.SC_ADDRESS_TYPE_CONTRACT)
+ .contractId(ContractID(Hash(hash)))
+ .build()
+
+ private fun muxedAddress(id: Long, key: ByteArray): SCAddress =
+ SCAddress.builder()
+ .discriminant(SCAddressType.SC_ADDRESS_TYPE_MUXED_ACCOUNT)
+ .muxedAccount(
+ MuxedEd25519Account.builder()
+ .id(Uint64(XdrUnsignedHyperInteger(BigInteger.valueOf(id))))
+ .ed25519(Uint256(key))
+ .build()
+ )
+ .build()
+
+ private fun claimableBalanceAddress(hash: ByteArray): SCAddress =
+ SCAddress.builder()
+ .discriminant(SCAddressType.SC_ADDRESS_TYPE_CLAIMABLE_BALANCE)
+ .claimableBalanceId(
+ ClaimableBalanceID.builder()
+ .discriminant(ClaimableBalanceIDType.CLAIMABLE_BALANCE_ID_TYPE_V0)
+ .v0(Hash(hash))
+ .build()
+ )
+ .build()
+
+ private fun liquidityPoolAddress(hash: ByteArray): SCAddress =
+ SCAddress.builder()
+ .discriminant(SCAddressType.SC_ADDRESS_TYPE_LIQUIDITY_POOL)
+ .liquidityPoolId(PoolID(Hash(hash)))
+ .build()
+ }
+}
From 103a66cd7745f042fc59735e02111b2f8815b095 Mon Sep 17 00:00:00 2001
From: Jun Luo <4catcode@gmail.com>
Date: Wed, 4 Mar 2026 11:57:44 +0800
Subject: [PATCH 3/6] feat: sort ScMap entries by key in `Scv.toMap` following
Soroban runtime ordering
---
.../stellar/sdk/scval/ScValComparatorTest.kt | 120 +++++++++++++++++-
1 file changed, 119 insertions(+), 1 deletion(-)
diff --git a/src/test/kotlin/org/stellar/sdk/scval/ScValComparatorTest.kt b/src/test/kotlin/org/stellar/sdk/scval/ScValComparatorTest.kt
index 2b86ec3ec..12754572c 100644
--- a/src/test/kotlin/org/stellar/sdk/scval/ScValComparatorTest.kt
+++ b/src/test/kotlin/org/stellar/sdk/scval/ScValComparatorTest.kt
@@ -112,7 +112,7 @@ class ScValComparatorTest :
}
context("SCV_U128") {
- test("tuple (hi, lo) ordering") {
+ test("tuple (hi, lo) ordering - hi differs") {
val twoTo64 = BigInteger.ONE.shiftLeft(64)
ScValComparator.compareScVal(
Scv.toUint128(twoTo64.subtract(BigInteger.ONE)),
@@ -123,6 +123,15 @@ class ScValComparatorTest :
Scv.toUint128(BigInteger.ZERO),
) shouldBe 0
}
+
+ test("tuple (hi, lo) ordering - same hi, lo differs") {
+ val twoTo64 = BigInteger.ONE.shiftLeft(64)
+ // both have hi=1, but lo differs: 1*2^64+0 vs 1*2^64+1
+ ScValComparator.compareScVal(
+ Scv.toUint128(twoTo64),
+ Scv.toUint128(twoTo64.add(BigInteger.ONE)),
+ ) shouldBeLessThan 0
+ }
}
context("SCV_I128") {
@@ -137,6 +146,14 @@ class ScValComparatorTest :
Scv.toInt128(BigInteger.ZERO),
) shouldBeLessThan 0
}
+
+ test("same hi, lo differs") {
+ // 1 and 2 both have hi=0, lo differs
+ ScValComparator.compareScVal(
+ Scv.toInt128(BigInteger.ONE),
+ Scv.toInt128(BigInteger.valueOf(2)),
+ ) shouldBeLessThan 0
+ }
}
context("SCV_U256") {
@@ -146,6 +163,32 @@ class ScValComparatorTest :
Scv.toUint256(BigInteger.ONE),
) shouldBeLessThan 0
}
+
+ test("each component level determines ordering") {
+ val shift64 = BigInteger.ONE.shiftLeft(64)
+ val shift128 = BigInteger.ONE.shiftLeft(128)
+ val shift192 = BigInteger.ONE.shiftLeft(192)
+ // hi_hi differs
+ ScValComparator.compareScVal(
+ Scv.toUint256(shift192),
+ Scv.toUint256(shift192.add(shift192)),
+ ) shouldBeLessThan 0
+ // same hi_hi, hi_lo differs
+ ScValComparator.compareScVal(
+ Scv.toUint256(shift128),
+ Scv.toUint256(shift128.add(shift128)),
+ ) shouldBeLessThan 0
+ // same hi_hi+hi_lo, lo_hi differs
+ ScValComparator.compareScVal(
+ Scv.toUint256(shift64),
+ Scv.toUint256(shift64.add(shift64)),
+ ) shouldBeLessThan 0
+ // same hi_hi+hi_lo+lo_hi, lo_lo differs
+ ScValComparator.compareScVal(
+ Scv.toUint256(BigInteger.ONE),
+ Scv.toUint256(BigInteger.valueOf(2)),
+ ) shouldBeLessThan 0
+ }
}
context("SCV_I256") {
@@ -160,6 +203,26 @@ class ScValComparatorTest :
Scv.toInt256(BigInteger.ZERO),
) shouldBeLessThan 0
}
+
+ test("each component level determines ordering") {
+ val shift64 = BigInteger.ONE.shiftLeft(64)
+ val shift128 = BigInteger.ONE.shiftLeft(128)
+ // same hi_hi(0), hi_lo differs
+ ScValComparator.compareScVal(
+ Scv.toInt256(shift128),
+ Scv.toInt256(shift128.add(shift128)),
+ ) shouldBeLessThan 0
+ // same hi_hi+hi_lo(0), lo_hi differs
+ ScValComparator.compareScVal(
+ Scv.toInt256(shift64),
+ Scv.toInt256(shift64.add(shift64)),
+ ) shouldBeLessThan 0
+ // same hi_hi+hi_lo+lo_hi(0), lo_lo differs
+ ScValComparator.compareScVal(
+ Scv.toInt256(BigInteger.ONE),
+ Scv.toInt256(BigInteger.valueOf(2)),
+ ) shouldBeLessThan 0
+ }
}
context("SCV_BYTES") {
@@ -173,6 +236,13 @@ class ScValComparatorTest :
Scv.toBytes(byteArrayOf(0x61)),
) shouldBe 0
}
+
+ test("unsigned byte comparison: 0x80 > 0x7F") {
+ ScValComparator.compareScVal(
+ Scv.toBytes(byteArrayOf(0x7F)),
+ Scv.toBytes(byteArrayOf(0x80.toByte())),
+ ) shouldBeLessThan 0
+ }
}
context("SCV_STRING") {
@@ -203,6 +273,11 @@ class ScValComparatorTest :
Scv.toVec(listOf(Scv.toUint32(1), Scv.toUint32(2))),
) shouldBeLessThan 0
}
+
+ test("equal vecs") {
+ val v = listOf(Scv.toUint32(1), Scv.toUint32(2))
+ ScValComparator.compareScVal(Scv.toVec(v), Scv.toVec(v)) shouldBe 0
+ }
}
context("SCV_MAP") {
@@ -226,6 +301,12 @@ class ScValComparatorTest :
)
ScValComparator.compareScVal(a, e) shouldBeLessThan 0
}
+
+ test("equal maps") {
+ val a = makeMap(arrayOf(entry(Scv.toUint32(1), Scv.toInt32(10))))
+ val b = makeMap(arrayOf(entry(Scv.toUint32(1), Scv.toInt32(10))))
+ ScValComparator.compareScVal(a, b) shouldBe 0
+ }
}
context("SCV_ERROR") {
@@ -239,6 +320,13 @@ class ScValComparatorTest :
ScValComparator.compareScVal(makeErrorContract(1), makeErrorContract(2)) shouldBeLessThan 0
ScValComparator.compareScVal(makeErrorContract(5), makeErrorContract(5)) shouldBe 0
}
+
+ test("non-contract error: compare by error code") {
+ val wasmErr1 = makeErrorWasm(SCErrorCode.SCEC_ARITH_DOMAIN)
+ val wasmErr2 = makeErrorWasm(SCErrorCode.SCEC_UNEXPECTED_SIZE)
+ ScValComparator.compareScVal(wasmErr1, wasmErr2) shouldBeLessThan 0
+ ScValComparator.compareScVal(wasmErr1, wasmErr1) shouldBe 0
+ }
}
context("SCV_CONTRACT_INSTANCE") {
@@ -306,6 +394,28 @@ class ScValComparatorTest :
) shouldBeLessThan 0
}
+ test("claimable balance: compare by hash") {
+ ScValComparator.compareScAddress(
+ claimableBalanceAddress(bytes32(0x00)),
+ claimableBalanceAddress(bytes32Last(0x01)),
+ ) shouldBeLessThan 0
+ ScValComparator.compareScAddress(
+ claimableBalanceAddress(bytes32(0xAA)),
+ claimableBalanceAddress(bytes32(0xAA)),
+ ) shouldBe 0
+ }
+
+ test("liquidity pool: compare by hash") {
+ ScValComparator.compareScAddress(
+ liquidityPoolAddress(bytes32(0x00)),
+ liquidityPoolAddress(bytes32Last(0x01)),
+ ) shouldBeLessThan 0
+ ScValComparator.compareScAddress(
+ liquidityPoolAddress(bytes32(0xBB)),
+ liquidityPoolAddress(bytes32(0xBB)),
+ ) shouldBe 0
+ }
+
test("muxed: id first, then ed25519") {
ScValComparator.compareScAddress(
muxedAddress(1, bytes32(0xFF)),
@@ -351,6 +461,14 @@ class ScValComparatorTest :
}
}
+ context("Comparator interface") {
+ test("INSTANCE.compare delegates to compareScVal") {
+ ScValComparator.INSTANCE.compare(Scv.toUint32(1), Scv.toUint32(2)) shouldBeLessThan 0
+ ScValComparator.INSTANCE.compare(Scv.toUint32(2), Scv.toUint32(1)) shouldBeGreaterThan 0
+ ScValComparator.INSTANCE.compare(Scv.toUint32(1), Scv.toUint32(1)) shouldBe 0
+ }
+ }
+
context("antisymmetry") {
test("compare(a,b) == -compare(b,a)") {
val pairs =
From ec0ad0c49094661a027b005f3800b8a96b868607 Mon Sep 17 00:00:00 2001
From: Jun Luo <4catcode@gmail.com>
Date: Wed, 4 Mar 2026 12:06:28 +0800
Subject: [PATCH 4/6] feat: sort ScMap entries by key in `Scv.toMap` following
Soroban runtime ordering
---
...cValComparator.java => ScvComparator.java} | 4 +-
.../java/org/stellar/sdk/scval/ScvMap.java | 2 +-
.../org/stellar/sdk/scval/ScvMapTest.java | 2 +-
...ComparatorTest.kt => ScvComparatorTest.kt} | 172 +++++++++---------
4 files changed, 89 insertions(+), 91 deletions(-)
rename src/main/java/org/stellar/sdk/scval/{ScValComparator.java => ScvComparator.java} (99%)
rename src/test/kotlin/org/stellar/sdk/scval/{ScValComparatorTest.kt => ScvComparatorTest.kt} (75%)
diff --git a/src/main/java/org/stellar/sdk/scval/ScValComparator.java b/src/main/java/org/stellar/sdk/scval/ScvComparator.java
similarity index 99%
rename from src/main/java/org/stellar/sdk/scval/ScValComparator.java
rename to src/main/java/org/stellar/sdk/scval/ScvComparator.java
index 04fedadab..e889c7a34 100644
--- a/src/main/java/org/stellar/sdk/scval/ScValComparator.java
+++ b/src/main/java/org/stellar/sdk/scval/ScvComparator.java
@@ -41,8 +41,8 @@
*
*
*/
-class ScValComparator implements Comparator {
- static final ScValComparator INSTANCE = new ScValComparator();
+class ScvComparator implements Comparator {
+ static final ScvComparator INSTANCE = new ScvComparator();
@Override
public int compare(SCVal a, SCVal b) {
diff --git a/src/main/java/org/stellar/sdk/scval/ScvMap.java b/src/main/java/org/stellar/sdk/scval/ScvMap.java
index 2e4c0f61b..68132ce63 100644
--- a/src/main/java/org/stellar/sdk/scval/ScvMap.java
+++ b/src/main/java/org/stellar/sdk/scval/ScvMap.java
@@ -20,7 +20,7 @@ static SCVal toSCVal(Map value) {
for (Map.Entry entry : value.entrySet()) {
scMapEntries[i++] = SCMapEntry.builder().key(entry.getKey()).val(entry.getValue()).build();
}
- Arrays.sort(scMapEntries, (a, b) -> ScValComparator.compareScVal(a.getKey(), b.getKey()));
+ Arrays.sort(scMapEntries, (a, b) -> ScvComparator.compareScVal(a.getKey(), b.getKey()));
return SCVal.builder().discriminant(TYPE).map(new SCMap(scMapEntries)).build();
}
diff --git a/src/test/java/org/stellar/sdk/scval/ScvMapTest.java b/src/test/java/org/stellar/sdk/scval/ScvMapTest.java
index 5c2a03b81..dca0dae79 100644
--- a/src/test/java/org/stellar/sdk/scval/ScvMapTest.java
+++ b/src/test/java/org/stellar/sdk/scval/ScvMapTest.java
@@ -66,7 +66,7 @@ public void testToMapStrictlyIncreasing() {
for (int i = 0; i < entries.length - 1; i++) {
assertTrue(
"keys[" + i + "] not strictly less than keys[" + (i + 1) + "]",
- ScValComparator.compareScVal(entries[i].getKey(), entries[i + 1].getKey()) < 0);
+ ScvComparator.compareScVal(entries[i].getKey(), entries[i + 1].getKey()) < 0);
}
}
diff --git a/src/test/kotlin/org/stellar/sdk/scval/ScValComparatorTest.kt b/src/test/kotlin/org/stellar/sdk/scval/ScvComparatorTest.kt
similarity index 75%
rename from src/test/kotlin/org/stellar/sdk/scval/ScValComparatorTest.kt
rename to src/test/kotlin/org/stellar/sdk/scval/ScvComparatorTest.kt
index 12754572c..125a000b1 100644
--- a/src/test/kotlin/org/stellar/sdk/scval/ScValComparatorTest.kt
+++ b/src/test/kotlin/org/stellar/sdk/scval/ScvComparatorTest.kt
@@ -34,49 +34,48 @@ import org.stellar.sdk.xdr.Uint64
import org.stellar.sdk.xdr.XdrUnsignedHyperInteger
import org.stellar.sdk.xdr.XdrUnsignedInteger
-class ScValComparatorTest :
+class ScvComparatorTest :
FunSpec({
context("cross-type ordering") {
test("SCV_BOOL < SCV_VOID < SCV_U32 < SCV_SYMBOL") {
- ScValComparator.compareScVal(Scv.toBoolean(true), Scv.toVoid()) shouldBeLessThan 0
- ScValComparator.compareScVal(Scv.toVoid(), Scv.toUint32(0)) shouldBeLessThan 0
- ScValComparator.compareScVal(Scv.toUint32(0), Scv.toSymbol("x")) shouldBeLessThan 0
- ScValComparator.compareScVal(Scv.toSymbol("x"), Scv.toVoid()) shouldBeGreaterThan 0
+ ScvComparator.compareScVal(Scv.toBoolean(true), Scv.toVoid()) shouldBeLessThan 0
+ ScvComparator.compareScVal(Scv.toVoid(), Scv.toUint32(0)) shouldBeLessThan 0
+ ScvComparator.compareScVal(Scv.toUint32(0), Scv.toSymbol("x")) shouldBeLessThan 0
+ ScvComparator.compareScVal(Scv.toSymbol("x"), Scv.toVoid()) shouldBeGreaterThan 0
}
}
context("SCV_BOOL") {
test("false < true") {
- ScValComparator.compareScVal(Scv.toBoolean(false), Scv.toBoolean(true)) shouldBeLessThan 0
- ScValComparator.compareScVal(Scv.toBoolean(true), Scv.toBoolean(false)) shouldBeGreaterThan
- 0
- ScValComparator.compareScVal(Scv.toBoolean(true), Scv.toBoolean(true)) shouldBe 0
+ ScvComparator.compareScVal(Scv.toBoolean(false), Scv.toBoolean(true)) shouldBeLessThan 0
+ ScvComparator.compareScVal(Scv.toBoolean(true), Scv.toBoolean(false)) shouldBeGreaterThan 0
+ ScvComparator.compareScVal(Scv.toBoolean(true), Scv.toBoolean(true)) shouldBe 0
}
}
context("SCV_VOID") {
- test("void == void") { ScValComparator.compareScVal(Scv.toVoid(), Scv.toVoid()) shouldBe 0 }
+ test("void == void") { ScvComparator.compareScVal(Scv.toVoid(), Scv.toVoid()) shouldBe 0 }
}
context("SCV_U32") {
test("numeric ordering") {
- ScValComparator.compareScVal(Scv.toUint32(1), Scv.toUint32(2)) shouldBeLessThan 0
- ScValComparator.compareScVal(Scv.toUint32(0), Scv.toUint32(0)) shouldBe 0
+ ScvComparator.compareScVal(Scv.toUint32(1), Scv.toUint32(2)) shouldBeLessThan 0
+ ScvComparator.compareScVal(Scv.toUint32(0), Scv.toUint32(0)) shouldBe 0
}
}
context("SCV_I32") {
test("signed ordering") {
- ScValComparator.compareScVal(Scv.toInt32(-10), Scv.toInt32(-1)) shouldBeLessThan 0
- ScValComparator.compareScVal(Scv.toInt32(-1), Scv.toInt32(0)) shouldBeLessThan 0
- ScValComparator.compareScVal(Scv.toInt32(5), Scv.toInt32(5)) shouldBe 0
+ ScvComparator.compareScVal(Scv.toInt32(-10), Scv.toInt32(-1)) shouldBeLessThan 0
+ ScvComparator.compareScVal(Scv.toInt32(-1), Scv.toInt32(0)) shouldBeLessThan 0
+ ScvComparator.compareScVal(Scv.toInt32(5), Scv.toInt32(5)) shouldBe 0
}
}
context("SCV_U64") {
test("unsigned ordering including max") {
val maxU64 = BigInteger("18446744073709551615")
- ScValComparator.compareScVal(
+ ScvComparator.compareScVal(
Scv.toUint64(BigInteger.ZERO),
Scv.toUint64(maxU64),
) shouldBeLessThan 0
@@ -85,8 +84,8 @@ class ScValComparatorTest :
context("SCV_I64") {
test("signed ordering") {
- ScValComparator.compareScVal(Scv.toInt64(-1), Scv.toInt64(0)) shouldBeLessThan 0
- ScValComparator.compareScVal(
+ ScvComparator.compareScVal(Scv.toInt64(-1), Scv.toInt64(0)) shouldBeLessThan 0
+ ScvComparator.compareScVal(
Scv.toInt64(Long.MIN_VALUE),
Scv.toInt64(Long.MAX_VALUE),
) shouldBeLessThan 0
@@ -95,7 +94,7 @@ class ScValComparatorTest :
context("SCV_TIMEPOINT") {
test("numeric ordering") {
- ScValComparator.compareScVal(
+ ScvComparator.compareScVal(
Scv.toTimePoint(BigInteger.ONE),
Scv.toTimePoint(BigInteger.valueOf(2)),
) shouldBeLessThan 0
@@ -104,7 +103,7 @@ class ScValComparatorTest :
context("SCV_DURATION") {
test("numeric ordering") {
- ScValComparator.compareScVal(
+ ScvComparator.compareScVal(
Scv.toDuration(BigInteger.ONE),
Scv.toDuration(BigInteger.valueOf(2)),
) shouldBeLessThan 0
@@ -114,11 +113,11 @@ class ScValComparatorTest :
context("SCV_U128") {
test("tuple (hi, lo) ordering - hi differs") {
val twoTo64 = BigInteger.ONE.shiftLeft(64)
- ScValComparator.compareScVal(
+ ScvComparator.compareScVal(
Scv.toUint128(twoTo64.subtract(BigInteger.ONE)),
Scv.toUint128(twoTo64),
) shouldBeLessThan 0
- ScValComparator.compareScVal(
+ ScvComparator.compareScVal(
Scv.toUint128(BigInteger.ZERO),
Scv.toUint128(BigInteger.ZERO),
) shouldBe 0
@@ -127,7 +126,7 @@ class ScValComparatorTest :
test("tuple (hi, lo) ordering - same hi, lo differs") {
val twoTo64 = BigInteger.ONE.shiftLeft(64)
// both have hi=1, but lo differs: 1*2^64+0 vs 1*2^64+1
- ScValComparator.compareScVal(
+ ScvComparator.compareScVal(
Scv.toUint128(twoTo64),
Scv.toUint128(twoTo64.add(BigInteger.ONE)),
) shouldBeLessThan 0
@@ -137,11 +136,11 @@ class ScValComparatorTest :
context("SCV_I128") {
test("signed ordering including negative") {
val minI128 = BigInteger.ONE.shiftLeft(127).negate()
- ScValComparator.compareScVal(
+ ScvComparator.compareScVal(
Scv.toInt128(minI128),
Scv.toInt128(BigInteger.ZERO),
) shouldBeLessThan 0
- ScValComparator.compareScVal(
+ ScvComparator.compareScVal(
Scv.toInt128(BigInteger.valueOf(-1)),
Scv.toInt128(BigInteger.ZERO),
) shouldBeLessThan 0
@@ -149,7 +148,7 @@ class ScValComparatorTest :
test("same hi, lo differs") {
// 1 and 2 both have hi=0, lo differs
- ScValComparator.compareScVal(
+ ScvComparator.compareScVal(
Scv.toInt128(BigInteger.ONE),
Scv.toInt128(BigInteger.valueOf(2)),
) shouldBeLessThan 0
@@ -158,7 +157,7 @@ class ScValComparatorTest :
context("SCV_U256") {
test("numeric ordering") {
- ScValComparator.compareScVal(
+ ScvComparator.compareScVal(
Scv.toUint256(BigInteger.ZERO),
Scv.toUint256(BigInteger.ONE),
) shouldBeLessThan 0
@@ -169,22 +168,22 @@ class ScValComparatorTest :
val shift128 = BigInteger.ONE.shiftLeft(128)
val shift192 = BigInteger.ONE.shiftLeft(192)
// hi_hi differs
- ScValComparator.compareScVal(
+ ScvComparator.compareScVal(
Scv.toUint256(shift192),
Scv.toUint256(shift192.add(shift192)),
) shouldBeLessThan 0
// same hi_hi, hi_lo differs
- ScValComparator.compareScVal(
+ ScvComparator.compareScVal(
Scv.toUint256(shift128),
Scv.toUint256(shift128.add(shift128)),
) shouldBeLessThan 0
// same hi_hi+hi_lo, lo_hi differs
- ScValComparator.compareScVal(
+ ScvComparator.compareScVal(
Scv.toUint256(shift64),
Scv.toUint256(shift64.add(shift64)),
) shouldBeLessThan 0
// same hi_hi+hi_lo+lo_hi, lo_lo differs
- ScValComparator.compareScVal(
+ ScvComparator.compareScVal(
Scv.toUint256(BigInteger.ONE),
Scv.toUint256(BigInteger.valueOf(2)),
) shouldBeLessThan 0
@@ -194,11 +193,11 @@ class ScValComparatorTest :
context("SCV_I256") {
test("signed ordering including negative") {
val minI256 = BigInteger.ONE.shiftLeft(255).negate()
- ScValComparator.compareScVal(
+ ScvComparator.compareScVal(
Scv.toInt256(minI256),
Scv.toInt256(BigInteger.ZERO),
) shouldBeLessThan 0
- ScValComparator.compareScVal(
+ ScvComparator.compareScVal(
Scv.toInt256(BigInteger.valueOf(-1)),
Scv.toInt256(BigInteger.ZERO),
) shouldBeLessThan 0
@@ -208,17 +207,17 @@ class ScValComparatorTest :
val shift64 = BigInteger.ONE.shiftLeft(64)
val shift128 = BigInteger.ONE.shiftLeft(128)
// same hi_hi(0), hi_lo differs
- ScValComparator.compareScVal(
+ ScvComparator.compareScVal(
Scv.toInt256(shift128),
Scv.toInt256(shift128.add(shift128)),
) shouldBeLessThan 0
// same hi_hi+hi_lo(0), lo_hi differs
- ScValComparator.compareScVal(
+ ScvComparator.compareScVal(
Scv.toInt256(shift64),
Scv.toInt256(shift64.add(shift64)),
) shouldBeLessThan 0
// same hi_hi+hi_lo+lo_hi(0), lo_lo differs
- ScValComparator.compareScVal(
+ ScvComparator.compareScVal(
Scv.toInt256(BigInteger.ONE),
Scv.toInt256(BigInteger.valueOf(2)),
) shouldBeLessThan 0
@@ -227,18 +226,18 @@ class ScValComparatorTest :
context("SCV_BYTES") {
test("lexicographic ordering and prefix rule") {
- ScValComparator.compareScVal(
+ ScvComparator.compareScVal(
Scv.toBytes(byteArrayOf(0x61, 0x62)),
Scv.toBytes(byteArrayOf(0x61, 0x62, 0x63)),
) shouldBeLessThan 0
- ScValComparator.compareScVal(
+ ScvComparator.compareScVal(
Scv.toBytes(byteArrayOf(0x61)),
Scv.toBytes(byteArrayOf(0x61)),
) shouldBe 0
}
test("unsigned byte comparison: 0x80 > 0x7F") {
- ScValComparator.compareScVal(
+ ScvComparator.compareScVal(
Scv.toBytes(byteArrayOf(0x7F)),
Scv.toBytes(byteArrayOf(0x80.toByte())),
) shouldBeLessThan 0
@@ -247,16 +246,15 @@ class ScValComparatorTest :
context("SCV_STRING") {
test("lexicographic ordering and prefix rule") {
- ScValComparator.compareScVal(Scv.toString("abc"), Scv.toString("abd")) shouldBeLessThan 0
- ScValComparator.compareScVal(Scv.toString("ab"), Scv.toString("abc")) shouldBeLessThan 0
+ ScvComparator.compareScVal(Scv.toString("abc"), Scv.toString("abd")) shouldBeLessThan 0
+ ScvComparator.compareScVal(Scv.toString("ab"), Scv.toString("abc")) shouldBeLessThan 0
}
}
context("SCV_SYMBOL") {
test("lexicographic ordering") {
- ScValComparator.compareScVal(Scv.toSymbol("alpha"), Scv.toSymbol("bravo")) shouldBeLessThan
- 0
- ScValComparator.compareScVal(Scv.toSymbol("x"), Scv.toSymbol("x")) shouldBe 0
+ ScvComparator.compareScVal(Scv.toSymbol("alpha"), Scv.toSymbol("bravo")) shouldBeLessThan 0
+ ScvComparator.compareScVal(Scv.toSymbol("x"), Scv.toSymbol("x")) shouldBe 0
}
}
@@ -264,11 +262,11 @@ class ScValComparatorTest :
test("element-by-element ordering") {
val a = listOf(Scv.toUint32(1), Scv.toUint32(2))
val b = listOf(Scv.toUint32(1), Scv.toUint32(3))
- ScValComparator.compareScVal(Scv.toVec(a), Scv.toVec(b)) shouldBeLessThan 0
+ ScvComparator.compareScVal(Scv.toVec(a), Scv.toVec(b)) shouldBeLessThan 0
}
test("shorter < longer when prefix matches") {
- ScValComparator.compareScVal(
+ ScvComparator.compareScVal(
Scv.toVec(listOf(Scv.toUint32(1))),
Scv.toVec(listOf(Scv.toUint32(1), Scv.toUint32(2))),
) shouldBeLessThan 0
@@ -276,7 +274,7 @@ class ScValComparatorTest :
test("equal vecs") {
val v = listOf(Scv.toUint32(1), Scv.toUint32(2))
- ScValComparator.compareScVal(Scv.toVec(v), Scv.toVec(v)) shouldBe 0
+ ScvComparator.compareScVal(Scv.toVec(v), Scv.toVec(v)) shouldBe 0
}
}
@@ -284,13 +282,13 @@ class ScValComparatorTest :
test("compare by key") {
val a = makeMap(arrayOf(entry(Scv.toUint32(1), Scv.toVoid())))
val b = makeMap(arrayOf(entry(Scv.toUint32(2), Scv.toVoid())))
- ScValComparator.compareScVal(a, b) shouldBeLessThan 0
+ ScvComparator.compareScVal(a, b) shouldBeLessThan 0
}
test("compare by val when keys equal") {
val c = makeMap(arrayOf(entry(Scv.toUint32(1), Scv.toInt32(10))))
val d = makeMap(arrayOf(entry(Scv.toUint32(1), Scv.toInt32(20))))
- ScValComparator.compareScVal(c, d) shouldBeLessThan 0
+ ScvComparator.compareScVal(c, d) shouldBeLessThan 0
}
test("shorter < longer") {
@@ -299,13 +297,13 @@ class ScValComparatorTest :
makeMap(
arrayOf(entry(Scv.toUint32(1), Scv.toVoid()), entry(Scv.toUint32(2), Scv.toVoid()))
)
- ScValComparator.compareScVal(a, e) shouldBeLessThan 0
+ ScvComparator.compareScVal(a, e) shouldBeLessThan 0
}
test("equal maps") {
val a = makeMap(arrayOf(entry(Scv.toUint32(1), Scv.toInt32(10))))
val b = makeMap(arrayOf(entry(Scv.toUint32(1), Scv.toInt32(10))))
- ScValComparator.compareScVal(a, b) shouldBe 0
+ ScvComparator.compareScVal(a, b) shouldBe 0
}
}
@@ -313,19 +311,19 @@ class ScValComparatorTest :
test("SCE_CONTRACT < SCE_WASM_VM") {
val contractErr = makeErrorContract(1)
val wasmErr = makeErrorWasm(SCErrorCode.SCEC_ARITH_DOMAIN)
- ScValComparator.compareScVal(contractErr, wasmErr) shouldBeLessThan 0
+ ScvComparator.compareScVal(contractErr, wasmErr) shouldBeLessThan 0
}
test("same type, different code") {
- ScValComparator.compareScVal(makeErrorContract(1), makeErrorContract(2)) shouldBeLessThan 0
- ScValComparator.compareScVal(makeErrorContract(5), makeErrorContract(5)) shouldBe 0
+ ScvComparator.compareScVal(makeErrorContract(1), makeErrorContract(2)) shouldBeLessThan 0
+ ScvComparator.compareScVal(makeErrorContract(5), makeErrorContract(5)) shouldBe 0
}
test("non-contract error: compare by error code") {
val wasmErr1 = makeErrorWasm(SCErrorCode.SCEC_ARITH_DOMAIN)
val wasmErr2 = makeErrorWasm(SCErrorCode.SCEC_UNEXPECTED_SIZE)
- ScValComparator.compareScVal(wasmErr1, wasmErr2) shouldBeLessThan 0
- ScValComparator.compareScVal(wasmErr1, wasmErr1) shouldBe 0
+ ScvComparator.compareScVal(wasmErr1, wasmErr2) shouldBeLessThan 0
+ ScvComparator.compareScVal(wasmErr1, wasmErr1) shouldBe 0
}
}
@@ -333,11 +331,11 @@ class ScValComparatorTest :
test("WASM < STELLAR_ASSET") {
val wasm = makeWasmInstance(bytes32(0x00), null)
val asset = makeStellarAssetInstance(null)
- ScValComparator.compareScVal(wasm, asset) shouldBeLessThan 0
+ ScvComparator.compareScVal(wasm, asset) shouldBeLessThan 0
}
test("different wasm hash") {
- ScValComparator.compareScVal(
+ ScvComparator.compareScVal(
makeWasmInstance(bytes32(0x00), null),
makeWasmInstance(bytes32Last(0x01), null),
) shouldBeLessThan 0
@@ -346,14 +344,14 @@ class ScValComparatorTest :
test("null storage < non-null storage") {
val asset = makeStellarAssetInstance(null)
val storage = SCMap(arrayOf(entry(Scv.toUint32(1), Scv.toVoid())))
- ScValComparator.compareScVal(asset, makeStellarAssetInstance(storage)) shouldBeLessThan 0
+ ScvComparator.compareScVal(asset, makeStellarAssetInstance(storage)) shouldBeLessThan 0
}
}
context("SCV_LEDGER_KEY_NONCE") {
test("signed ordering") {
- ScValComparator.compareScVal(makeNonce(-1), makeNonce(0)) shouldBeLessThan 0
- ScValComparator.compareScVal(makeNonce(42), makeNonce(42)) shouldBe 0
+ ScvComparator.compareScVal(makeNonce(-1), makeNonce(0)) shouldBeLessThan 0
+ ScvComparator.compareScVal(makeNonce(42), makeNonce(42)) shouldBe 0
}
}
@@ -361,7 +359,7 @@ class ScValComparatorTest :
test("always equal") {
val a = SCVal.builder().discriminant(SCValType.SCV_LEDGER_KEY_CONTRACT_INSTANCE).build()
val b = SCVal.builder().discriminant(SCValType.SCV_LEDGER_KEY_CONTRACT_INSTANCE).build()
- ScValComparator.compareScVal(a, b) shouldBe 0
+ ScvComparator.compareScVal(a, b) shouldBe 0
}
}
@@ -376,56 +374,56 @@ class ScValComparatorTest :
liquidityPoolAddress(bytes32(0x00)),
)
for (i in 0 until addrs.size - 1) {
- ScValComparator.compareScAddress(addrs[i], addrs[i + 1]) shouldBeLessThan 0
+ ScvComparator.compareScAddress(addrs[i], addrs[i + 1]) shouldBeLessThan 0
}
}
test("account: compare by ed25519") {
- ScValComparator.compareScAddress(
+ ScvComparator.compareScAddress(
accountAddress(bytes32(0x00)),
accountAddress(bytes32Last(0x01)),
) shouldBeLessThan 0
}
test("contract: compare by hash") {
- ScValComparator.compareScAddress(
+ ScvComparator.compareScAddress(
contractAddress(bytes32(0x00)),
contractAddress(bytes32(0xFF)),
) shouldBeLessThan 0
}
test("claimable balance: compare by hash") {
- ScValComparator.compareScAddress(
+ ScvComparator.compareScAddress(
claimableBalanceAddress(bytes32(0x00)),
claimableBalanceAddress(bytes32Last(0x01)),
) shouldBeLessThan 0
- ScValComparator.compareScAddress(
+ ScvComparator.compareScAddress(
claimableBalanceAddress(bytes32(0xAA)),
claimableBalanceAddress(bytes32(0xAA)),
) shouldBe 0
}
test("liquidity pool: compare by hash") {
- ScValComparator.compareScAddress(
+ ScvComparator.compareScAddress(
liquidityPoolAddress(bytes32(0x00)),
liquidityPoolAddress(bytes32Last(0x01)),
) shouldBeLessThan 0
- ScValComparator.compareScAddress(
+ ScvComparator.compareScAddress(
liquidityPoolAddress(bytes32(0xBB)),
liquidityPoolAddress(bytes32(0xBB)),
) shouldBe 0
}
test("muxed: id first, then ed25519") {
- ScValComparator.compareScAddress(
+ ScvComparator.compareScAddress(
muxedAddress(1, bytes32(0xFF)),
muxedAddress(2, bytes32(0x00)),
) shouldBeLessThan 0
- ScValComparator.compareScAddress(
+ ScvComparator.compareScAddress(
muxedAddress(1, bytes32(0x00)),
muxedAddress(1, bytes32Last(0x01)),
) shouldBeLessThan 0
- ScValComparator.compareScAddress(
+ ScvComparator.compareScAddress(
muxedAddress(5, bytes32(0xAB)),
muxedAddress(5, bytes32(0xAB)),
) shouldBe 0
@@ -443,8 +441,8 @@ class ScValComparatorTest :
ContractExecutable.builder()
.discriminant(ContractExecutableType.CONTRACT_EXECUTABLE_STELLAR_ASSET)
.build()
- ScValComparator.compareContractExecutable(wasm, asset) shouldBeLessThan 0
- ScValComparator.compareContractExecutable(asset, asset) shouldBe 0
+ ScvComparator.compareContractExecutable(wasm, asset) shouldBeLessThan 0
+ ScvComparator.compareContractExecutable(asset, asset) shouldBe 0
}
}
@@ -453,19 +451,19 @@ class ScValComparatorTest :
val m1 = SCMap(arrayOf(entry(Scv.toUint32(1), Scv.toVoid())))
val m2 = SCMap(arrayOf(entry(Scv.toUint32(2), Scv.toVoid())))
- ScValComparator.compareOptionalScMap(null, null) shouldBe 0
- ScValComparator.compareOptionalScMap(null, m1) shouldBeLessThan 0
- ScValComparator.compareOptionalScMap(m1, null) shouldBeGreaterThan 0
- ScValComparator.compareOptionalScMap(m1, m2) shouldBeLessThan 0
- ScValComparator.compareOptionalScMap(m1, m1) shouldBe 0
+ ScvComparator.compareOptionalScMap(null, null) shouldBe 0
+ ScvComparator.compareOptionalScMap(null, m1) shouldBeLessThan 0
+ ScvComparator.compareOptionalScMap(m1, null) shouldBeGreaterThan 0
+ ScvComparator.compareOptionalScMap(m1, m2) shouldBeLessThan 0
+ ScvComparator.compareOptionalScMap(m1, m1) shouldBe 0
}
}
context("Comparator interface") {
test("INSTANCE.compare delegates to compareScVal") {
- ScValComparator.INSTANCE.compare(Scv.toUint32(1), Scv.toUint32(2)) shouldBeLessThan 0
- ScValComparator.INSTANCE.compare(Scv.toUint32(2), Scv.toUint32(1)) shouldBeGreaterThan 0
- ScValComparator.INSTANCE.compare(Scv.toUint32(1), Scv.toUint32(1)) shouldBe 0
+ ScvComparator.INSTANCE.compare(Scv.toUint32(1), Scv.toUint32(2)) shouldBeLessThan 0
+ ScvComparator.INSTANCE.compare(Scv.toUint32(2), Scv.toUint32(1)) shouldBeGreaterThan 0
+ ScvComparator.INSTANCE.compare(Scv.toUint32(1), Scv.toUint32(1)) shouldBe 0
}
}
@@ -481,8 +479,8 @@ class ScValComparatorTest :
arrayOf(Scv.toInt32(0), Scv.toSymbol("x")),
)
for ((i, pair) in pairs.withIndex()) {
- ScValComparator.compareScVal(pair[0], pair[1]) shouldBe
- -ScValComparator.compareScVal(pair[1], pair[0])
+ ScvComparator.compareScVal(pair[0], pair[1]) shouldBe
+ -ScvComparator.compareScVal(pair[1], pair[0])
}
}
}
@@ -492,9 +490,9 @@ class ScValComparatorTest :
val a = Scv.toBoolean(true)
val b = Scv.toUint32(0)
val c = Scv.toSymbol("x")
- ScValComparator.compareScVal(a, b) shouldBeLessThan 0
- ScValComparator.compareScVal(b, c) shouldBeLessThan 0
- ScValComparator.compareScVal(a, c) shouldBeLessThan 0
+ ScvComparator.compareScVal(a, b) shouldBeLessThan 0
+ ScvComparator.compareScVal(b, c) shouldBeLessThan 0
+ ScvComparator.compareScVal(a, c) shouldBeLessThan 0
}
}
}) {
From 1f14012e5bdb2221fc11345ca2c20ff0b5276d20 Mon Sep 17 00:00:00 2001
From: Jun Luo <4catcode@gmail.com>
Date: Wed, 4 Mar 2026 14:58:59 +0800
Subject: [PATCH 5/6] feat: sort ScMap entries by key in `Scv.toMap` following
Soroban runtime ordering
---
CHANGELOG.md | 2 +-
.../org/stellar/sdk/scval/ScvComparator.java | 72 +++++++++----------
.../org/stellar/sdk/scval/ScvMapTest.java | 12 ++--
3 files changed, 44 insertions(+), 42 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f936c5d52..11e20b0bc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,7 @@
# Changelog
## Pending
-- feat: sort `ScMap` entries by key in `Scv.toMap` following Soroban runtime ordering rules, as the network requires ScMap keys to be in ascending order.
+- feat: sort `ScMap` entries by key in `Scv.toMap` following Soroban runtime ordering rules, as the network requires ScMap keys to be in ascending order. `Scv.toMap` now accepts `Map`; the previous `toMap(LinkedHashMap)` overload is deprecated.
## 2.2.3
diff --git a/src/main/java/org/stellar/sdk/scval/ScvComparator.java b/src/main/java/org/stellar/sdk/scval/ScvComparator.java
index e889c7a34..c39d064b8 100644
--- a/src/main/java/org/stellar/sdk/scval/ScvComparator.java
+++ b/src/main/java/org/stellar/sdk/scval/ScvComparator.java
@@ -83,13 +83,13 @@ static int compareScVal(SCVal a, SCVal b) {
.compareTo(b.getDuration().getDuration().getUint64().getNumber());
case SCV_U128:
{
- int c =
+ int cmp =
a.getU128()
.getHi()
.getUint64()
.getNumber()
.compareTo(b.getU128().getHi().getUint64().getNumber());
- if (c != 0) return c;
+ if (cmp != 0) return cmp;
return a.getU128()
.getLo()
.getUint64()
@@ -98,8 +98,8 @@ static int compareScVal(SCVal a, SCVal b) {
}
case SCV_I128:
{
- int c = Long.compare(a.getI128().getHi().getInt64(), b.getI128().getHi().getInt64());
- if (c != 0) return c;
+ int cmp = Long.compare(a.getI128().getHi().getInt64(), b.getI128().getHi().getInt64());
+ if (cmp != 0) return cmp;
return a.getI128()
.getLo()
.getUint64()
@@ -108,27 +108,27 @@ static int compareScVal(SCVal a, SCVal b) {
}
case SCV_U256:
{
- int c =
+ int cmp =
a.getU256()
.getHi_hi()
.getUint64()
.getNumber()
.compareTo(b.getU256().getHi_hi().getUint64().getNumber());
- if (c != 0) return c;
- c =
+ if (cmp != 0) return cmp;
+ cmp =
a.getU256()
.getHi_lo()
.getUint64()
.getNumber()
.compareTo(b.getU256().getHi_lo().getUint64().getNumber());
- if (c != 0) return c;
- c =
+ if (cmp != 0) return cmp;
+ cmp =
a.getU256()
.getLo_hi()
.getUint64()
.getNumber()
.compareTo(b.getU256().getLo_hi().getUint64().getNumber());
- if (c != 0) return c;
+ if (cmp != 0) return cmp;
return a.getU256()
.getLo_lo()
.getUint64()
@@ -137,23 +137,23 @@ static int compareScVal(SCVal a, SCVal b) {
}
case SCV_I256:
{
- int c =
+ int cmp =
Long.compare(a.getI256().getHi_hi().getInt64(), b.getI256().getHi_hi().getInt64());
- if (c != 0) return c;
- c =
+ if (cmp != 0) return cmp;
+ cmp =
a.getI256()
.getHi_lo()
.getUint64()
.getNumber()
.compareTo(b.getI256().getHi_lo().getUint64().getNumber());
- if (c != 0) return c;
- c =
+ if (cmp != 0) return cmp;
+ cmp =
a.getI256()
.getLo_hi()
.getUint64()
.getNumber()
.compareTo(b.getI256().getLo_hi().getUint64().getNumber());
- if (c != 0) return c;
+ if (cmp != 0) return cmp;
return a.getI256()
.getLo_lo()
.getUint64()
@@ -174,8 +174,8 @@ static int compareScVal(SCVal a, SCVal b) {
SCVal[] bv = b.getVec().getSCVec();
int len = Math.min(av.length, bv.length);
for (int i = 0; i < len; i++) {
- int c = compareScVal(av[i], bv[i]);
- if (c != 0) return c;
+ int cmp = compareScVal(av[i], bv[i]);
+ if (cmp != 0) return cmp;
}
return Integer.compare(av.length, bv.length);
}
@@ -185,10 +185,10 @@ static int compareScVal(SCVal a, SCVal b) {
SCMapEntry[] bm = b.getMap().getSCMap();
int len = Math.min(am.length, bm.length);
for (int i = 0; i < len; i++) {
- int c = compareScVal(am[i].getKey(), bm[i].getKey());
- if (c != 0) return c;
- c = compareScVal(am[i].getVal(), bm[i].getVal());
- if (c != 0) return c;
+ int cmp = compareScVal(am[i].getKey(), bm[i].getKey());
+ if (cmp != 0) return cmp;
+ cmp = compareScVal(am[i].getVal(), bm[i].getVal());
+ if (cmp != 0) return cmp;
}
return Integer.compare(am.length, bm.length);
}
@@ -196,11 +196,11 @@ static int compareScVal(SCVal a, SCVal b) {
return compareScAddress(a.getAddress(), b.getAddress());
case SCV_ERROR:
{
- int c =
+ int cmp =
Integer.compare(
a.getError().getDiscriminant().getValue(),
b.getError().getDiscriminant().getValue());
- if (c != 0) return c;
+ if (cmp != 0) return cmp;
switch (a.getError().getDiscriminant()) {
case SCE_CONTRACT:
return Long.compare(
@@ -213,10 +213,10 @@ static int compareScVal(SCVal a, SCVal b) {
}
case SCV_CONTRACT_INSTANCE:
{
- int c =
+ int cmp =
compareContractExecutable(
a.getInstance().getExecutable(), b.getInstance().getExecutable());
- if (c != 0) return c;
+ if (cmp != 0) return cmp;
return compareOptionalScMap(a.getInstance().getStorage(), b.getInstance().getStorage());
}
case SCV_LEDGER_KEY_NONCE:
@@ -228,8 +228,8 @@ static int compareScVal(SCVal a, SCVal b) {
}
static int compareScAddress(SCAddress a, SCAddress b) {
- int c = Integer.compare(a.getDiscriminant().getValue(), b.getDiscriminant().getValue());
- if (c != 0) return c;
+ int cmp = Integer.compare(a.getDiscriminant().getValue(), b.getDiscriminant().getValue());
+ if (cmp != 0) return cmp;
switch (a.getDiscriminant()) {
case SC_ADDRESS_TYPE_ACCOUNT:
@@ -242,13 +242,13 @@ static int compareScAddress(SCAddress a, SCAddress b) {
b.getContractId().getContractID().getHash());
case SC_ADDRESS_TYPE_MUXED_ACCOUNT:
{
- int r =
+ cmp =
a.getMuxedAccount()
.getId()
.getUint64()
.getNumber()
.compareTo(b.getMuxedAccount().getId().getUint64().getNumber());
- if (r != 0) return r;
+ if (cmp != 0) return cmp;
return compareByteArrays(
a.getMuxedAccount().getEd25519().getUint256(),
b.getMuxedAccount().getEd25519().getUint256());
@@ -273,8 +273,8 @@ static int compareScAddress(SCAddress a, SCAddress b) {
}
static int compareContractExecutable(ContractExecutable a, ContractExecutable b) {
- int c = Integer.compare(a.getDiscriminant().getValue(), b.getDiscriminant().getValue());
- if (c != 0) return c;
+ int cmp = Integer.compare(a.getDiscriminant().getValue(), b.getDiscriminant().getValue());
+ if (cmp != 0) return cmp;
switch (a.getDiscriminant()) {
case CONTRACT_EXECUTABLE_WASM:
@@ -296,10 +296,10 @@ static int compareOptionalScMap(SCMap a, SCMap b) {
SCMapEntry[] bm = b.getSCMap();
int len = Math.min(am.length, bm.length);
for (int i = 0; i < len; i++) {
- int c = compareScVal(am[i].getKey(), bm[i].getKey());
- if (c != 0) return c;
- c = compareScVal(am[i].getVal(), bm[i].getVal());
- if (c != 0) return c;
+ int cmp = compareScVal(am[i].getKey(), bm[i].getKey());
+ if (cmp != 0) return cmp;
+ cmp = compareScVal(am[i].getVal(), bm[i].getVal());
+ if (cmp != 0) return cmp;
}
return Integer.compare(am.length, bm.length);
}
diff --git a/src/test/java/org/stellar/sdk/scval/ScvMapTest.java b/src/test/java/org/stellar/sdk/scval/ScvMapTest.java
index dca0dae79..6bd97cac4 100644
--- a/src/test/java/org/stellar/sdk/scval/ScvMapTest.java
+++ b/src/test/java/org/stellar/sdk/scval/ScvMapTest.java
@@ -4,7 +4,9 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
+import java.util.HashMap;
import java.util.LinkedHashMap;
+import java.util.Map;
import org.junit.Test;
import org.stellar.sdk.xdr.SCMap;
import org.stellar.sdk.xdr.SCMapEntry;
@@ -38,7 +40,7 @@ public void testScvMap() {
@Test
public void testToMapSortsKeys() {
- LinkedHashMap value = new LinkedHashMap<>();
+ Map value = new HashMap<>();
value.put(Scv.toUint32(3), Scv.toVoid());
value.put(Scv.toUint32(1), Scv.toVoid());
value.put(Scv.toUint32(2), Scv.toVoid());
@@ -53,7 +55,7 @@ public void testToMapSortsKeys() {
@Test
public void testToMapStrictlyIncreasing() {
- LinkedHashMap value = new LinkedHashMap<>();
+ Map value = new HashMap<>();
value.put(Scv.toSymbol("z"), Scv.toVoid());
value.put(Scv.toSymbol("a"), Scv.toVoid());
value.put(Scv.toUint32(100), Scv.toVoid());
@@ -72,7 +74,7 @@ public void testToMapStrictlyIncreasing() {
@Test
public void testToMapMixedTypes() {
- LinkedHashMap value = new LinkedHashMap<>();
+ Map value = new HashMap<>();
value.put(Scv.toSymbol("x"), Scv.toInt32(1));
value.put(Scv.toUint32(42), Scv.toInt32(2));
value.put(Scv.toBoolean(true), Scv.toInt32(3));
@@ -88,7 +90,7 @@ public void testToMapMixedTypes() {
@Test
public void testToMapSignedNegativeBoundaries() {
- LinkedHashMap value = new LinkedHashMap<>();
+ Map value = new HashMap<>();
value.put(Scv.toInt32(0), Scv.toVoid());
value.put(Scv.toInt32(Integer.MIN_VALUE), Scv.toVoid());
value.put(Scv.toInt32(Integer.MAX_VALUE), Scv.toVoid());
@@ -105,7 +107,7 @@ public void testToMapSignedNegativeBoundaries() {
@Test
public void testToMapAlreadySortedIdempotent() {
- LinkedHashMap value = new LinkedHashMap<>();
+ Map value = new HashMap<>();
value.put(Scv.toUint32(1), Scv.toInt32(10));
value.put(Scv.toUint32(2), Scv.toInt32(20));
value.put(Scv.toUint32(3), Scv.toInt32(30));
From 1c8bf8af7d6ca14505036e732656c64113fbb4bf Mon Sep 17 00:00:00 2001
From: Jun Luo <4catcode@gmail.com>
Date: Wed, 4 Mar 2026 15:04:11 +0800
Subject: [PATCH 6/6] feat: sort ScMap entries by key in `Scv.toMap` following
Soroban runtime ordering
---
.../org/stellar/sdk/scval/ScvComparator.java | 18 ++++--------------
1 file changed, 4 insertions(+), 14 deletions(-)
diff --git a/src/main/java/org/stellar/sdk/scval/ScvComparator.java b/src/main/java/org/stellar/sdk/scval/ScvComparator.java
index c39d064b8..1af839ab0 100644
--- a/src/main/java/org/stellar/sdk/scval/ScvComparator.java
+++ b/src/main/java/org/stellar/sdk/scval/ScvComparator.java
@@ -180,18 +180,7 @@ static int compareScVal(SCVal a, SCVal b) {
return Integer.compare(av.length, bv.length);
}
case SCV_MAP:
- {
- SCMapEntry[] am = a.getMap().getSCMap();
- SCMapEntry[] bm = b.getMap().getSCMap();
- int len = Math.min(am.length, bm.length);
- for (int i = 0; i < len; i++) {
- int cmp = compareScVal(am[i].getKey(), bm[i].getKey());
- if (cmp != 0) return cmp;
- cmp = compareScVal(am[i].getVal(), bm[i].getVal());
- if (cmp != 0) return cmp;
- }
- return Integer.compare(am.length, bm.length);
- }
+ return compareMapEntries(a.getMap().getSCMap(), b.getMap().getSCMap());
case SCV_ADDRESS:
return compareScAddress(a.getAddress(), b.getAddress());
case SCV_ERROR:
@@ -291,9 +280,10 @@ static int compareOptionalScMap(SCMap a, SCMap b) {
if (a == null && b == null) return 0;
if (a == null) return -1;
if (b == null) return 1;
+ return compareMapEntries(a.getSCMap(), b.getSCMap());
+ }
- SCMapEntry[] am = a.getSCMap();
- SCMapEntry[] bm = b.getSCMap();
+ private static int compareMapEntries(SCMapEntry[] am, SCMapEntry[] bm) {
int len = Math.min(am.length, bm.length);
for (int i = 0; i < len; i++) {
int cmp = compareScVal(am[i].getKey(), bm[i].getKey());