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}. + * + *

Comparison rules: + * + *

    + *
  1. Cross-type: compare by {@link SCValType} discriminant value ({@code SCV_BOOL=0 < + * SCV_VOID=1 < ... < SCV_LEDGER_KEY_NONCE=21}). + *
  2. Same-type (by variant): + * + *
+ */ +class ScValComparator implements Comparator { + 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());