From a342f8912785dfa5e241ec4da2500d29da2a2319 Mon Sep 17 00:00:00 2001 From: Jun Luo Date: Thu, 5 Nov 2026 10:29:02 +0800 Subject: [PATCH] fix: order same-code assets by the issuers' raw key bytes. --- CHANGELOG.md | 3 ++ src/main/java/org/stellar/sdk/Asset.java | 3 +- .../sdk/AssetTypeCreditAlphaNum12.java | 4 +- .../stellar/sdk/AssetTypeCreditAlphaNum4.java | 4 +- .../org/stellar/sdk/LiquidityPoolTest.java | 39 +++++++++++++++++++ 5 files changed, 50 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40de1b867..1ed158fc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Pending +### Update +- fix: `Asset.compareTo` orders same-code assets by the issuers' raw Ed25519 key bytes instead of their strkey text, matching the order the network requires. + ## 6.0.0 **This release adds support for Protocol 30.** diff --git a/src/main/java/org/stellar/sdk/Asset.java b/src/main/java/org/stellar/sdk/Asset.java index 724bc769b..35432c14e 100644 --- a/src/main/java/org/stellar/sdk/Asset.java +++ b/src/main/java/org/stellar/sdk/Asset.java @@ -100,7 +100,8 @@ public static Asset fromXdr(org.stellar.sdk.xdr.Asset xdr) { *
    *
  1. First compare the type (eg. native before alphanum4 before alphanum12). *
  2. If the types are equal, compare the assets codes. - *
  3. If the asset codes are equal, compare the issuers. + *
  4. If the asset codes are equal, compare the issuers by their raw Ed25519 public key bytes, + * as the network requires, not by their strkey text. *
* * @param other the object to be compared. diff --git a/src/main/java/org/stellar/sdk/AssetTypeCreditAlphaNum12.java b/src/main/java/org/stellar/sdk/AssetTypeCreditAlphaNum12.java index 9db076f3a..f4c0e0b7c 100644 --- a/src/main/java/org/stellar/sdk/AssetTypeCreditAlphaNum12.java +++ b/src/main/java/org/stellar/sdk/AssetTypeCreditAlphaNum12.java @@ -63,6 +63,8 @@ public int compareTo(@NonNull Asset other) { if (!this.getCode().equals(o.getCode())) { return this.getCode().compareTo(o.getCode()); } - return this.getIssuer().compareTo(o.getIssuer()); + return Util.compareBytesUnsigned( + StrKey.decodeEd25519PublicKey(this.getIssuer()), + StrKey.decodeEd25519PublicKey(o.getIssuer())); } } diff --git a/src/main/java/org/stellar/sdk/AssetTypeCreditAlphaNum4.java b/src/main/java/org/stellar/sdk/AssetTypeCreditAlphaNum4.java index 018876f61..a79464302 100644 --- a/src/main/java/org/stellar/sdk/AssetTypeCreditAlphaNum4.java +++ b/src/main/java/org/stellar/sdk/AssetTypeCreditAlphaNum4.java @@ -66,6 +66,8 @@ public int compareTo(@NonNull Asset other) { return this.getCode().compareTo(o.getCode()); } - return this.getIssuer().compareTo(o.getIssuer()); + return Util.compareBytesUnsigned( + StrKey.decodeEd25519PublicKey(this.getIssuer()), + StrKey.decodeEd25519PublicKey(o.getIssuer())); } } diff --git a/src/test/java/org/stellar/sdk/LiquidityPoolTest.java b/src/test/java/org/stellar/sdk/LiquidityPoolTest.java index 7f2a57529..b966c8b5f 100644 --- a/src/test/java/org/stellar/sdk/LiquidityPoolTest.java +++ b/src/test/java/org/stellar/sdk/LiquidityPoolTest.java @@ -1,6 +1,8 @@ package org.stellar.sdk; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.IOException; @@ -130,6 +132,43 @@ public void testLexicographicOrderAssetIssuer() { } } + @Test + public void testIssuersAreOrderedByRawKeyBytes() { + // Issuers of testnet pool c2a730127d006897b021d6fcb279da68b4f65f9b6a408c74eb2cdd85ea862d5e. + // GDRQ sorts after GD32 as strkey text, but before it as raw key bytes (e305... < f7a4...). + for (String code : new String[] {"USD", "BANANA"}) { + Asset assetA = + Asset.create(code + ":GDRQL6EF772CANMYX55HXMU463OLAX46ZRCPCWS4KVL66ZS4BYA7OYDL"); + Asset assetB = + Asset.create(code + ":GD32JQHFWLIROCNQ564GLCB2M7T4GMQAP5UKHMVT64OQISWL2LHYK2HO"); + assertTrue(assetA.compareTo(assetB) < 0); + assertTrue(assetB.compareTo(assetA) > 0); + new LiquidityPool(assetA, assetB); + assertThrows(IllegalArgumentException.class, () -> new LiquidityPool(assetB, assetA)); + } + } + + @Test + public void testLiquidityPoolIdWithIssuersOrderedByRawKeyBytes() { + Asset assetA = Asset.create("USD:GDRQL6EF772CANMYX55HXMU463OLAX46ZRCPCWS4KVL66ZS4BYA7OYDL"); + Asset assetB = Asset.create("USD:GD32JQHFWLIROCNQ564GLCB2M7T4GMQAP5UKHMVT64OQISWL2LHYK2HO"); + assertEquals( + "c2a730127d006897b021d6fcb279da68b4f65f9b6a408c74eb2cdd85ea862d5e", + new LiquidityPool(assetA, assetB).getLiquidityPoolId()); + } + + @Test + public void testDecodeTransactionWithIssuersOrderedByRawKeyBytes() throws IOException { + // Testnet transaction 0126748091930959a3aefac9296e1773b947f964a9b73f37ce856c4df32d346f + String envelopeXdr = + "AAAAAgAAAAADeR2SwEvKvthWOdthGC/c1Bxym7fafJmtjj5EyvmvxAAAASwAVEAXAAAAAQAAAAAAAAAAAAAAAwAAAAAAAAAGAAAAAVVTRAAAAAAA4wX4hf/0IDWYv3p7spz23LBfnsxE8VpcVVfvZlwOAfd//////////wAAAAAAAAAGAAAAAVVTRAAAAAAA96TA5bLRFwmw77hliDpn58MyAH9oo7Kz9x0ESsvSz4V//////////wAAAAAAAAAGAAAAAwAAAAAAAAABVVNEAAAAAADjBfiF//QgNZi/enuynPbcsF+ezETxWlxVV+9mXA4B9wAAAAFVU0QAAAAAAPekwOWy0RcJsO+4ZYg6Z+fDMgB/aKOys/cdBErL0s+FAAAAHn//////////AAAAAAAAAAHK+a/EAAAAQMtA5QpAy6I3ILVmPZcRA3SJhGo4CiMT7K7dXQt7Qo1Prip/OedNocMY2QloSRle4hYvhJilIiOyoD85l3+WrQM="; + AbstractTransaction transaction = + AbstractTransaction.fromEnvelopeXdr(envelopeXdr, Network.TESTNET); + assertEquals(envelopeXdr, transaction.toEnvelopeXdrBase64()); + assertEquals( + "0126748091930959a3aefac9296e1773b947f964a9b73f37ce856c4df32d346f", transaction.hashHex()); + } + @Value static class OrderTestCase { Asset assetA;