From 90ef49abb301f7981bd4465461e4fe9e95504814 Mon Sep 17 00:00:00 2001 From: Jun Luo <4catcode@gmail.com> Date: Fri, 25 Jul 2025 10:03:06 +0800 Subject: [PATCH 1/3] refactor: add validation for asset issuer. --- CHANGELOG.md | 3 +++ .../stellar/sdk/AssetTypeCreditAlphaNum.java | 19 +++++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ccb687a30..ea2dee7eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Pending +### Update: +- refactor: add validation for asset issuer. ([#725](https://github.com/stellar/java-stellar-sdk/pull/725)) + ## 2.0.0-beta1 ### Update: diff --git a/src/main/java/org/stellar/sdk/AssetTypeCreditAlphaNum.java b/src/main/java/org/stellar/sdk/AssetTypeCreditAlphaNum.java index 5539acb1d..223a1db73 100644 --- a/src/main/java/org/stellar/sdk/AssetTypeCreditAlphaNum.java +++ b/src/main/java/org/stellar/sdk/AssetTypeCreditAlphaNum.java @@ -1,6 +1,5 @@ package org.stellar.sdk; -import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NonNull; @@ -12,7 +11,6 @@ * href="https://developers.stellar.org/docs/learn/fundamentals/stellar-data-structures/assets" * target="_blank">Assets */ -@AllArgsConstructor @EqualsAndHashCode(callSuper = false) @Getter public abstract class AssetTypeCreditAlphaNum extends Asset { @@ -22,6 +20,23 @@ public abstract class AssetTypeCreditAlphaNum extends Asset { /** Asset issuer */ @NonNull protected final String issuer; + /** + * Class constructor + * + * @param code Asset code + * @param issuer Asset issuer + * @throws IllegalArgumentException when code is invalid or issuer is not a valid Ed25519 public + * key + */ + public AssetTypeCreditAlphaNum(@NonNull String code, @NonNull String issuer) { + if (!StrKey.isValidEd25519PublicKey(issuer)) { + throw new IllegalArgumentException("Invalid issuer: " + issuer); + } + + this.code = code; + this.issuer = issuer; + } + @Override public String toString() { return getCode() + ":" + getIssuer(); From de2137d6fcd50a55a72eba9216798c2c857c002f Mon Sep 17 00:00:00 2001 From: Jun Luo <4catcode@gmail.com> Date: Fri, 25 Jul 2025 10:04:07 +0800 Subject: [PATCH 2/3] refactor: simplify asset creation from XDR by using factory methods --- src/main/java/org/stellar/sdk/Asset.java | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/stellar/sdk/Asset.java b/src/main/java/org/stellar/sdk/Asset.java index 7b32a44d5..88704bcfb 100644 --- a/src/main/java/org/stellar/sdk/Asset.java +++ b/src/main/java/org/stellar/sdk/Asset.java @@ -64,24 +64,13 @@ public static Asset create(String type, String code, String issuer) { * @param xdr XDR object */ public static Asset fromXdr(org.stellar.sdk.xdr.Asset xdr) { - String accountId; switch (xdr.getDiscriminant()) { case ASSET_TYPE_NATIVE: return new AssetTypeNative(); case ASSET_TYPE_CREDIT_ALPHANUM4: - String assetCode4 = - Util.paddedByteArrayToString(xdr.getAlphaNum4().getAssetCode().getAssetCode4()); - accountId = - StrKey.encodeEd25519PublicKey( - xdr.getAlphaNum4().getIssuer().getAccountID().getEd25519().getUint256()); - return new AssetTypeCreditAlphaNum4(assetCode4, accountId); + return AssetTypeCreditAlphaNum4.fromXdr(xdr.getAlphaNum4()); case ASSET_TYPE_CREDIT_ALPHANUM12: - String assetCode12 = - Util.paddedByteArrayToString(xdr.getAlphaNum12().getAssetCode().getAssetCode12()); - accountId = - StrKey.encodeEd25519PublicKey( - xdr.getAlphaNum12().getIssuer().getAccountID().getEd25519().getUint256()); - return new AssetTypeCreditAlphaNum12(assetCode12, accountId); + return AssetTypeCreditAlphaNum12.fromXdr(xdr.getAlphaNum12()); default: throw new IllegalArgumentException("Unknown asset type " + xdr.getDiscriminant()); } From 43fd1ae9c7ca38b45e677ccf887bcba90aa0dc71 Mon Sep 17 00:00:00 2001 From: Jun Luo <4catcode@gmail.com> Date: Fri, 25 Jul 2025 10:04:34 +0800 Subject: [PATCH 3/3] test: migrate `AssetTest` to Kotlin. --- src/test/java/org/stellar/sdk/AssetTest.java | 238 ----------- src/test/kotlin/org/stellar/sdk/AssetTest.kt | 422 +++++++++++++++++++ 2 files changed, 422 insertions(+), 238 deletions(-) delete mode 100644 src/test/java/org/stellar/sdk/AssetTest.java create mode 100644 src/test/kotlin/org/stellar/sdk/AssetTest.kt diff --git a/src/test/java/org/stellar/sdk/AssetTest.java b/src/test/java/org/stellar/sdk/AssetTest.java deleted file mode 100644 index be017197b..000000000 --- a/src/test/java/org/stellar/sdk/AssetTest.java +++ /dev/null @@ -1,238 +0,0 @@ -package org.stellar.sdk; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertTrue; -import static org.stellar.sdk.Asset.create; -import static org.stellar.sdk.Asset.createNativeAsset; -import static org.stellar.sdk.Asset.createNonNativeAsset; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import org.junit.Test; - -/** Created by andrewrogers on 7/1/15. */ -public class AssetTest { - Asset xlm = createNativeAsset(); - - @Test - public void testAssetTypeNative() { - AssetTypeNative asset = new AssetTypeNative(); - org.stellar.sdk.xdr.Asset xdr = asset.toXdr(); - Asset parsedAsset = Asset.fromXdr(xdr); - assertTrue(parsedAsset instanceof AssetTypeNative); - } - - @Test - public void testAssetTypeCreditAlphaNum4() { - String code = "USDA"; - KeyPair issuer = KeyPair.random(); - AssetTypeCreditAlphaNum4 asset = new AssetTypeCreditAlphaNum4(code, issuer.getAccountId()); - org.stellar.sdk.xdr.Asset xdr = asset.toXdr(); - AssetTypeCreditAlphaNum4 parsedAsset = (AssetTypeCreditAlphaNum4) Asset.fromXdr(xdr); - assertEquals(code, asset.getCode()); - assertEquals(issuer.getAccountId(), parsedAsset.getIssuer()); - } - - @Test - public void testAssetTypeCreditAlphaNum12() { - String code = "TESTTEST"; - KeyPair issuer = KeyPair.random(); - AssetTypeCreditAlphaNum12 asset = new AssetTypeCreditAlphaNum12(code, issuer.getAccountId()); - org.stellar.sdk.xdr.Asset xdr = asset.toXdr(); - AssetTypeCreditAlphaNum12 parsedAsset = (AssetTypeCreditAlphaNum12) Asset.fromXdr(xdr); - assertEquals(code, asset.getCode()); - assertEquals(issuer.getAccountId(), parsedAsset.getIssuer()); - } - - @Test - public void testHashCode() { - String issuer1 = KeyPair.random().getAccountId(); - String issuer2 = KeyPair.random().getAccountId(); - - // Equal - assertEquals(new AssetTypeNative().hashCode(), new AssetTypeNative().hashCode()); - assertEquals( - new AssetTypeCreditAlphaNum4("USD", issuer1).hashCode(), - new AssetTypeCreditAlphaNum4("USD", issuer1).hashCode()); - assertEquals( - new AssetTypeCreditAlphaNum12("ABCDE", issuer1).hashCode(), - new AssetTypeCreditAlphaNum12("ABCDE", issuer1).hashCode()); - - // Not equal - assertNotEquals( - new AssetTypeNative().hashCode(), new AssetTypeCreditAlphaNum4("USD", issuer1).hashCode()); - assertNotEquals( - new AssetTypeNative().hashCode(), - new AssetTypeCreditAlphaNum12("ABCDE", issuer1).hashCode()); - assertNotEquals( - new AssetTypeCreditAlphaNum4("EUR", issuer1).hashCode(), - new AssetTypeCreditAlphaNum4("USD", issuer1).hashCode()); - assertNotEquals( - new AssetTypeCreditAlphaNum4("EUR", issuer1).hashCode(), - new AssetTypeCreditAlphaNum4("EUR", issuer2).hashCode()); - assertNotEquals( - new AssetTypeCreditAlphaNum4("EUR", issuer1).hashCode(), - new AssetTypeCreditAlphaNum12("EUROPE", issuer1).hashCode()); - assertNotEquals( - new AssetTypeCreditAlphaNum4("EUR", issuer1).hashCode(), - new AssetTypeCreditAlphaNum12("EUROPE", issuer2).hashCode()); - assertNotEquals( - new AssetTypeCreditAlphaNum12("ABCDE", issuer1).hashCode(), - new AssetTypeCreditAlphaNum12("EDCBA", issuer1).hashCode()); - assertNotEquals( - new AssetTypeCreditAlphaNum12("ABCDE", issuer1).hashCode(), - new AssetTypeCreditAlphaNum12("ABCDE", issuer2).hashCode()); - } - - @Test - public void testAssetEquals() { - String issuer1 = KeyPair.random().getAccountId(); - String issuer2 = KeyPair.random().getAccountId(); - - assertEquals(new AssetTypeNative(), new AssetTypeNative()); - assertEquals( - new AssetTypeCreditAlphaNum4("USD", issuer1), new AssetTypeCreditAlphaNum4("USD", issuer1)); - assertEquals( - new AssetTypeCreditAlphaNum12("ABCDE", issuer1), - new AssetTypeCreditAlphaNum12("ABCDE", issuer1)); - - assertNotEquals(new AssetTypeNative(), new AssetTypeCreditAlphaNum4("USD", issuer1)); - assertNotEquals(new AssetTypeNative(), new AssetTypeCreditAlphaNum12("ABCDE", issuer1)); - assertNotEquals( - new AssetTypeCreditAlphaNum4("EUR", issuer1), new AssetTypeCreditAlphaNum4("USD", issuer1)); - assertNotEquals( - new AssetTypeCreditAlphaNum4("EUR", issuer1), new AssetTypeCreditAlphaNum4("EUR", issuer2)); - assertNotEquals( - new AssetTypeCreditAlphaNum12("ABCDE", issuer1), - new AssetTypeCreditAlphaNum12("EDCBA", issuer1)); - assertNotEquals( - new AssetTypeCreditAlphaNum12("ABCDE", issuer1), - new AssetTypeCreditAlphaNum12("ABCDE", issuer2)); - } - - @Test - public void testAssetCompareTo0IfAssetsEqual() { - Asset assetA = - createNonNativeAsset("ARST", "GB7TAYRUZGE6TVT7NHP5SMIZRNQA6PLM423EYISAOAP3MKYIQMVYP2JO"); - Asset assetB = - createNonNativeAsset("USD", "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ"); - - assertEquals(0, xlm.compareTo(xlm)); - assertEquals(0, assetA.compareTo(assetA)); - assertEquals(0, assetB.compareTo(assetB)); - } - - @Test - public void testAssetCompareToOrderingByType() { - Asset anum4 = - createNonNativeAsset("ARSZ", "GB7TAYRUZGE6TVT7NHP5SMIZRNQA6PLM423EYISAOAP3MKYIQMVYP2JO"); - Asset anum12 = - createNonNativeAsset( - "ARSTANUM12", "GB7TAYRUZGE6TVT7NHP5SMIZRNQA6PLM423EYISAOAP3MKYIQMVYP2JO"); - - assertEquals(0, xlm.compareTo(xlm)); - assertEquals(-1, xlm.compareTo(anum4)); - assertEquals(-1, xlm.compareTo(anum12)); - - assertEquals(1, anum4.compareTo(xlm)); - assertEquals(0, anum4.compareTo(anum4)); - assertEquals(-1, anum4.compareTo(anum12)); - - assertEquals(1, anum12.compareTo(xlm)); - assertEquals(1, anum12.compareTo(anum4)); - assertEquals(0, anum12.compareTo(anum12)); - } - - @Test - public void testAssetCompareToOrderingByCode() { - Asset assetARST = - createNonNativeAsset("ARST", "GB7TAYRUZGE6TVT7NHP5SMIZRNQA6PLM423EYISAOAP3MKYIQMVYP2JO"); - Asset assetUSDX = - createNonNativeAsset("USDX", "GB7TAYRUZGE6TVT7NHP5SMIZRNQA6PLM423EYISAOAP3MKYIQMVYP2JO"); - - assertEquals(0, assetARST.compareTo(assetARST)); - assertTrue(assetARST.compareTo(assetUSDX) < 0); - - assertTrue(assetUSDX.compareTo(assetARST) > 0); - assertEquals(0, assetUSDX.compareTo(assetUSDX)); - } - - @Test - public void testAssetCompareToOrderingByIssuer() { - Asset assetIssuerA = - createNonNativeAsset("ARST", "GB7TAYRUZGE6TVT7NHP5SMIZRNQA6PLM423EYISAOAP3MKYIQMVYP2JO"); - Asset assetIssuerB = - createNonNativeAsset("ARST", "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ"); - - assertTrue(assetIssuerA.compareTo(assetIssuerB) < 0); - assertEquals(0, assetIssuerA.compareTo(assetIssuerA)); - - assertTrue(assetIssuerB.compareTo(assetIssuerA) > 0); - assertEquals(0, assetIssuerB.compareTo(assetIssuerB)); - } - - @Test - public void testAssetsAreSortable() { - // Native is always first - Asset a = create("native"); - // Type is Alphanum4 - Asset b = - createNonNativeAsset("BCDE", "GB7TAYRUZGE6TVT7NHP5SMIZRNQA6PLM423EYISAOAP3MKYIQMVYP2JO"); - - // Type is Alphanum12 - Asset c = - createNonNativeAsset("ABCD1", "GB7TAYRUZGE6TVT7NHP5SMIZRNQA6PLM423EYISAOAP3MKYIQMVYP2JO"); - - // Code is > - Asset d = - createNonNativeAsset("ABCD2", "GB7TAYRUZGE6TVT7NHP5SMIZRNQA6PLM423EYISAOAP3MKYIQMVYP2JO"); - - // Issuer is > - Asset e = - createNonNativeAsset("ABCD2", "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ"); - - Asset[] expected = {a, b, c, d, e}; - - // Basic sorting check that it doesn't reorder stuff - List assets = Arrays.asList(new Asset[] {a, b, c, d, e}); - Collections.sort(assets); - assertArrayEquals(expected, assets.toArray()); - - // Reverse it and check it still sorts the same - Collections.reverse(assets); - Collections.sort(assets); - assertArrayEquals(expected, assets.toArray()); - - // Shuffle it and check it still sorts to the same - Collections.shuffle(assets); - Collections.sort(assets); - assertArrayEquals(expected, assets.toArray()); - } - - @Test - public void testGetContractId() { - // native + testnet - assertEquals( - "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC", - new AssetTypeNative().getContractId(Network.TESTNET)); - // native + public - assertEquals( - "CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA", - new AssetTypeNative().getContractId(Network.PUBLIC)); - // alphanum4 + public - assertEquals( - "CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75", - new AssetTypeCreditAlphaNum4( - "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN") - .getContractId(Network.PUBLIC)); - // alphanum12 + public - assertEquals( - "CDOFW7HNKLUZRLFZST4EW7V3AV4JI5IHMT6BPXXSY2IEFZ4NE5TWU2P4", - new AssetTypeCreditAlphaNum12( - "yUSDC", "GDGTVWSM4MGS4T7Z6W4RPWOCHE2I6RDFCIFZGS3DOA63LWQTRNZNTTFF") - .getContractId(Network.PUBLIC)); - } -} diff --git a/src/test/kotlin/org/stellar/sdk/AssetTest.kt b/src/test/kotlin/org/stellar/sdk/AssetTest.kt new file mode 100644 index 000000000..a838ed877 --- /dev/null +++ b/src/test/kotlin/org/stellar/sdk/AssetTest.kt @@ -0,0 +1,422 @@ +package org.stellar.sdk + +import io.kotest.assertions.throwables.shouldThrow +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 io.kotest.matchers.shouldNotBe +import org.stellar.sdk.xdr.AssetType + +class AssetTest : + FunSpec({ + val validIssuer = "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ" + val anotherValidIssuer = "GB7TAYRUZGE6TVT7NHP5SMIZRNQA6PLM423EYISAOAP3MKYIQMVYP2JO" + val invalidIssuer = "GCEZWKCA5" + + context("Native") { + test("should create native asset") { + val asset = AssetTypeNative() + asset.type shouldBe AssetType.ASSET_TYPE_NATIVE + asset.toString() shouldBe "native" + } + + test("should create native asset using factory method") { + val asset = Asset.createNativeAsset() + (asset is AssetTypeNative) shouldBe true + asset.type shouldBe AssetType.ASSET_TYPE_NATIVE + } + + test("should create native asset from canonical form") { + val asset = Asset.create("native") + (asset is AssetTypeNative) shouldBe true + asset.type shouldBe AssetType.ASSET_TYPE_NATIVE + } + + test("should convert to XDR and back") { + val asset = AssetTypeNative() + val xdr = asset.toXdr() + val parsedAsset = Asset.fromXdr(xdr) + (parsedAsset is AssetTypeNative) shouldBe true + parsedAsset shouldBe asset + } + } + + context("AssetTypeCreditAlphaNum4") { + context("constructor validation") { + test("should create valid 1-character asset") { + val asset = AssetTypeCreditAlphaNum4("A", validIssuer) + asset.code shouldBe "A" + asset.issuer shouldBe validIssuer + asset.type shouldBe AssetType.ASSET_TYPE_CREDIT_ALPHANUM4 + } + + test("should create valid 4-character asset") { + val asset = AssetTypeCreditAlphaNum4("ABCD", validIssuer) + asset.code shouldBe "ABCD" + asset.issuer shouldBe validIssuer + asset.type shouldBe AssetType.ASSET_TYPE_CREDIT_ALPHANUM4 + } + + test("should throw exception for empty code") { + val exception = + shouldThrow { AssetTypeCreditAlphaNum4("", validIssuer) } + exception.message shouldBe "The length of code must be between 1 and 4 characters." + } + + test("should throw exception for code longer than 4 characters") { + val exception = + shouldThrow { + AssetTypeCreditAlphaNum4("TOOLONG", validIssuer) + } + exception.message shouldBe "The length of code must be between 1 and 4 characters." + } + + test("should throw exception for invalid issuer") { + val exception = + shouldThrow { AssetTypeCreditAlphaNum4("USD", invalidIssuer) } + exception.message shouldBe "Invalid issuer: $invalidIssuer" + } + } + + test("should create from factory method") { + val asset = Asset.createNonNativeAsset("USD", validIssuer) + (asset is AssetTypeCreditAlphaNum4) shouldBe true + val alphaNum4 = asset as AssetTypeCreditAlphaNum4 + alphaNum4.code shouldBe "USD" + alphaNum4.issuer shouldBe validIssuer + } + + test("should create from canonical form") { + val asset = Asset.create("USD:$validIssuer") + (asset is AssetTypeCreditAlphaNum4) shouldBe true + val alphaNum4 = asset as AssetTypeCreditAlphaNum4 + alphaNum4.code shouldBe "USD" + alphaNum4.issuer shouldBe validIssuer + } + + test("should convert to XDR and back") { + val asset = AssetTypeCreditAlphaNum4("USD", validIssuer) + val xdr = asset.toXdr() + val parsedAsset = Asset.fromXdr(xdr) + (parsedAsset is AssetTypeCreditAlphaNum4) shouldBe true + val parsedAlphaNum4 = parsedAsset as AssetTypeCreditAlphaNum4 + parsedAlphaNum4.code shouldBe "USD" + parsedAlphaNum4.issuer shouldBe validIssuer + parsedAsset shouldBe asset + } + + test("should have correct toString format") { + val asset = AssetTypeCreditAlphaNum4("USD", validIssuer) + asset.toString() shouldBe "USD:$validIssuer" + } + } + + context("AssetTypeCreditAlphaNum12") { + context("constructor validation") { + test("should create valid 5-character asset") { + val asset = AssetTypeCreditAlphaNum12("ABCDE", validIssuer) + asset.code shouldBe "ABCDE" + asset.issuer shouldBe validIssuer + asset.type shouldBe AssetType.ASSET_TYPE_CREDIT_ALPHANUM12 + } + + test("should create valid 12-character asset") { + val asset = AssetTypeCreditAlphaNum12("ABCDEFGHIJKL", validIssuer) + asset.code shouldBe "ABCDEFGHIJKL" + asset.issuer shouldBe validIssuer + asset.type shouldBe AssetType.ASSET_TYPE_CREDIT_ALPHANUM12 + } + + test("should throw exception for code shorter than 5 characters") { + val exception = + shouldThrow { AssetTypeCreditAlphaNum12("ABCD", validIssuer) } + exception.message shouldBe "The length of code must be between 5 and 12 characters." + } + + test("should throw exception for code longer than 12 characters") { + val exception = + shouldThrow { + AssetTypeCreditAlphaNum12("TOOLONGASSETCODE", validIssuer) + } + exception.message shouldBe "The length of code must be between 5 and 12 characters." + } + + test("should throw exception for invalid issuer") { + val exception = + shouldThrow { + AssetTypeCreditAlphaNum12("LONGCODE", invalidIssuer) + } + exception.message shouldBe "Invalid issuer: $invalidIssuer" + } + } + + test("should create from factory method") { + val asset = Asset.createNonNativeAsset("TESTASSET", validIssuer) + (asset is AssetTypeCreditAlphaNum12) shouldBe true + val alphaNum12 = asset as AssetTypeCreditAlphaNum12 + alphaNum12.code shouldBe "TESTASSET" + alphaNum12.issuer shouldBe validIssuer + } + + test("should create from canonical form") { + val asset = Asset.create("TESTASSET:$validIssuer") + (asset is AssetTypeCreditAlphaNum12) shouldBe true + val alphaNum12 = asset as AssetTypeCreditAlphaNum12 + alphaNum12.code shouldBe "TESTASSET" + alphaNum12.issuer shouldBe validIssuer + } + + test("should convert to XDR and back") { + val asset = AssetTypeCreditAlphaNum12("TESTASSET", validIssuer) + val xdr = asset.toXdr() + val parsedAsset = Asset.fromXdr(xdr) + (parsedAsset is AssetTypeCreditAlphaNum12) shouldBe true + val parsedAlphaNum12 = parsedAsset as AssetTypeCreditAlphaNum12 + parsedAlphaNum12.code shouldBe "TESTASSET" + parsedAlphaNum12.issuer shouldBe validIssuer + parsedAsset shouldBe asset + } + + test("should have correct toString format") { + val asset = AssetTypeCreditAlphaNum12("TESTASSET", validIssuer) + asset.toString() shouldBe "TESTASSET:$validIssuer" + } + } + + context("Asset equality and hashCode") { + test("should be equal when same type and properties") { + val native1 = AssetTypeNative() + val native2 = AssetTypeNative() + native1 shouldBe native2 + native1.hashCode() shouldBe native2.hashCode() + + val alphanum4A = AssetTypeCreditAlphaNum4("USD", validIssuer) + val alphanum4B = AssetTypeCreditAlphaNum4("USD", validIssuer) + alphanum4A shouldBe alphanum4B + alphanum4A.hashCode() shouldBe alphanum4B.hashCode() + + val alphanum12A = AssetTypeCreditAlphaNum12("TESTASSET", validIssuer) + val alphanum12B = AssetTypeCreditAlphaNum12("TESTASSET", validIssuer) + alphanum12A shouldBe alphanum12B + alphanum12A.hashCode() shouldBe alphanum12B.hashCode() + } + + test("should not be equal when different types") { + val native = AssetTypeNative() + val alphanum4 = AssetTypeCreditAlphaNum4("USD", validIssuer) + val alphanum12 = AssetTypeCreditAlphaNum12("TESTASSET", validIssuer) + + native shouldNotBe alphanum4 + native shouldNotBe alphanum12 + alphanum4 shouldNotBe alphanum12 + native.hashCode() shouldNotBe alphanum4.hashCode() + native.hashCode() shouldNotBe alphanum12.hashCode() + alphanum4.hashCode() shouldNotBe alphanum12.hashCode() + } + + test("should not be equal when different codes") { + val asset1 = AssetTypeCreditAlphaNum4("USD", validIssuer) + val asset2 = AssetTypeCreditAlphaNum4("EUR", validIssuer) + asset1 shouldNotBe asset2 + asset1.hashCode() shouldNotBe asset2.hashCode() + + val asset3 = AssetTypeCreditAlphaNum12("TESTASSET", validIssuer) + val asset4 = AssetTypeCreditAlphaNum12("ANOTHERASSET", validIssuer) + asset3 shouldNotBe asset4 + asset3.hashCode() shouldNotBe asset4.hashCode() + } + + test("should not be equal when different issuers") { + val asset1 = AssetTypeCreditAlphaNum4("USD", validIssuer) + val asset2 = AssetTypeCreditAlphaNum4("USD", anotherValidIssuer) + asset1 shouldNotBe asset2 + asset1.hashCode() shouldNotBe asset2.hashCode() + + val asset3 = AssetTypeCreditAlphaNum12("TESTASSET", validIssuer) + val asset4 = AssetTypeCreditAlphaNum12("TESTASSET", anotherValidIssuer) + asset3 shouldNotBe asset4 + asset3.hashCode() shouldNotBe asset4.hashCode() + } + } + + context("Asset comparison and sorting") { + test("should compare to 0 if assets are equal") { + val native1 = AssetTypeNative() + val native2 = AssetTypeNative() + native1.compareTo(native2) shouldBe 0 + + val alphanum4A = AssetTypeCreditAlphaNum4("USD", validIssuer) + val alphanum4B = AssetTypeCreditAlphaNum4("USD", validIssuer) + alphanum4A.compareTo(alphanum4B) shouldBe 0 + + val alphanum12A = AssetTypeCreditAlphaNum12("TESTASSET", validIssuer) + val alphanum12B = AssetTypeCreditAlphaNum12("TESTASSET", validIssuer) + alphanum12A.compareTo(alphanum12B) shouldBe 0 + } + + test("should order by asset type: native < alphanum4 < alphanum12") { + val native = AssetTypeNative() + val alphanum4 = AssetTypeCreditAlphaNum4("USD", validIssuer) + val alphanum12 = AssetTypeCreditAlphaNum12("TESTASSET", validIssuer) + + // native < alphanum4 + native.compareTo(alphanum4) shouldBeLessThan 0 + alphanum4.compareTo(native) shouldBeGreaterThan 0 + + // native < alphanum12 + native.compareTo(alphanum12) shouldBeLessThan 0 + alphanum12.compareTo(native) shouldBeGreaterThan 0 + + // alphanum4 < alphanum12 + alphanum4.compareTo(alphanum12) shouldBeLessThan 0 + alphanum12.compareTo(alphanum4) shouldBeGreaterThan 0 + } + + test("should order by asset code when same type") { + val assetA = AssetTypeCreditAlphaNum4("ARST", validIssuer) + val assetB = AssetTypeCreditAlphaNum4("USD", validIssuer) + + assetA.compareTo(assetB) shouldBeLessThan 0 + assetB.compareTo(assetA) shouldBeGreaterThan 0 + + val assetC = AssetTypeCreditAlphaNum12("ABCDFG", validIssuer) + val assetD = AssetTypeCreditAlphaNum12("HIJKLM", validIssuer) + assetC.compareTo(assetD) shouldBeLessThan 0 + assetD.compareTo(assetC) shouldBeGreaterThan 0 + } + + test("should order uppercase before lowercase") { + val assetUpper = AssetTypeCreditAlphaNum4("ARST", validIssuer) + val assetLower = AssetTypeCreditAlphaNum4("arst", validIssuer) + + assetUpper.compareTo(assetLower) shouldBeLessThan 0 + assetLower.compareTo(assetUpper) shouldBeGreaterThan 0 + + val assetUpper12 = AssetTypeCreditAlphaNum12("ABCDE", validIssuer) + val assetLower12 = AssetTypeCreditAlphaNum12("abcde", validIssuer) + assetUpper12.compareTo(assetLower12) shouldBeLessThan 0 + assetLower12.compareTo(assetUpper12) shouldBeGreaterThan 0 + } + + test("should order by issuer when same code") { + val asset1 = + AssetTypeCreditAlphaNum4( + "USD", + "GB7TAYRUZGE6TVT7NHP5SMIZRNQA6PLM423EYISAOAP3MKYIQMVYP2JO", + ) + val asset2 = + AssetTypeCreditAlphaNum4( + "USD", + "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ", + ) + + asset1.compareTo(asset2) shouldBeLessThan 0 + asset2.compareTo(asset1) shouldBeGreaterThan 0 + + val asset3 = + AssetTypeCreditAlphaNum12( + "TESTASSET", + "GB7TAYRUZGE6TVT7NHP5SMIZRNQA6PLM423EYISAOAP3MKYIQMVYP2JO", + ) + val asset4 = + AssetTypeCreditAlphaNum12( + "TESTASSET", + "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ", + ) + asset3.compareTo(asset4) shouldBeLessThan 0 + asset4.compareTo(asset3) shouldBeGreaterThan 0 + } + + test("should sort assets correctly") { + val native = AssetTypeNative() + val alphanum4First = + AssetTypeCreditAlphaNum4( + "ABCD", + "GB7TAYRUZGE6TVT7NHP5SMIZRNQA6PLM423EYISAOAP3MKYIQMVYP2JO", + ) + val alphanum4Second = + AssetTypeCreditAlphaNum4( + "BCDE", + "GB7TAYRUZGE6TVT7NHP5SMIZRNQA6PLM423EYISAOAP3MKYIQMVYP2JO", + ) + val alphanum12 = + AssetTypeCreditAlphaNum12( + "ABCDEFGH", + "GB7TAYRUZGE6TVT7NHP5SMIZRNQA6PLM423EYISAOAP3MKYIQMVYP2JO", + ) + + val assets = listOf(alphanum12, alphanum4Second, native, alphanum4First) + val sortedAssets = assets.sorted() + + sortedAssets shouldBe listOf(native, alphanum4First, alphanum4Second, alphanum12) + } + } + + context("Asset creation methods") { + test("should create asset from canonical form with invalid format") { + val exception1 = shouldThrow { Asset.create("invalidformat") } + exception1.message shouldBe "invalid asset invalidformat" + + val exception2 = shouldThrow { Asset.create("USD:ISSUER:EXTRA") } + exception2.message shouldBe "invalid asset USD:ISSUER:EXTRA" + } + + test("should create asset with create method using type parameter") { + val nativeAsset = Asset.create("native", null, null) + (nativeAsset is AssetTypeNative) shouldBe true + + val alphaNum4Asset = Asset.create(null, "USD", validIssuer) + (alphaNum4Asset is AssetTypeCreditAlphaNum4) shouldBe true + + val explicitAlphaNum4Asset = Asset.create("credit_alphanum4", "USD", validIssuer) + (explicitAlphaNum4Asset is AssetTypeCreditAlphaNum4) shouldBe true + } + + test("should throw exception for invalid asset code length") { + val exception = + shouldThrow { Asset.createNonNativeAsset("", validIssuer) } + exception.message shouldBe "The length of code must be between 1 and 12 characters." + + val exception2 = + shouldThrow { + Asset.createNonNativeAsset("TOOLONGASSETCODE123", validIssuer) + } + exception2.message shouldBe "The length of code must be between 1 and 12 characters." + } + } + + context("Contract ID generation") { + test("should generate correct contract ID for native asset") { + val asset = AssetTypeNative() + val testnetContractId = asset.getContractId(Network.TESTNET) + testnetContractId shouldBe "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC" + val publicContractId = asset.getContractId(Network.PUBLIC) + publicContractId shouldBe "CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA" + } + + test("should generate correct contract ID for alphanum4 asset") { + val asset = + AssetTypeCreditAlphaNum4( + "USD", + "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ", + ) + val testnetContractId = asset.getContractId(Network.TESTNET) + testnetContractId shouldBe "CBIMQ4GRFDK27OR6MUIK2VO2ZXRFPWATPNCAHPTUS5MSJCUB3G2MBHGV" + val publicContractId = asset.getContractId(Network.PUBLIC) + publicContractId shouldBe "CBPDHP5DT6HQ6KPGHMULFSU2NOHEPAACU44B6B5ISCNVRDM62M4B7INK" + } + + test("should generate correct contract ID for alphanum12 asset") { + val asset = + AssetTypeCreditAlphaNum12( + "TESTASSET", + "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ", + ) + val testnetContractId = asset.getContractId(Network.TESTNET) + testnetContractId shouldBe "CAQHXOK2TFOQ6OQFAJ5HL5VG54OGSQJNGGSBQ3SHBPXJONWTSJXRL6P5" + val publicContractId = asset.getContractId(Network.PUBLIC) + publicContractId shouldBe "CAYBCFNYUBURFRBK2WL52QXDHV42WK33ZQHZZI3SQ5KWEXB55OLLYL6F" + } + } + })