diff --git a/README.md b/README.md
index 3baedf3b74..457fb13572 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
[](https://github.com/stellar/anchor-platform/blob/develop/LICENSE)
[](https://github.com/stellar/anchor-platform/releases)
-[](https://hub.docker.com/r/stellar/anchor-platform/tags?page=1&name=4.2.0)
+[](https://hub.docker.com/r/stellar/anchor-platform/tags?page=1&name=4.2.1)

diff --git a/build.gradle.kts b/build.gradle.kts
index 509fe8da90..5c72e89cc4 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -213,7 +213,7 @@ subprojects {
allprojects {
group = "org.stellar.anchor-sdk"
- version = "4.2.0"
+ version = "4.2.1"
tasks.jar {
manifest {
diff --git a/core/src/main/java/org/stellar/anchor/sep10/Sep10Service.java b/core/src/main/java/org/stellar/anchor/sep10/Sep10Service.java
index b27165afcb..677094d2a0 100644
--- a/core/src/main/java/org/stellar/anchor/sep10/Sep10Service.java
+++ b/core/src/main/java/org/stellar/anchor/sep10/Sep10Service.java
@@ -2,6 +2,7 @@
import static java.lang.String.format;
import static org.stellar.anchor.util.Log.*;
+import static org.stellar.anchor.util.MemoHelper.makeMemoId;
import static org.stellar.anchor.util.MetricConstants.SEP10_CHALLENGE_CREATED;
import static org.stellar.anchor.util.MetricConstants.SEP10_CHALLENGE_VALIDATED;
import static org.stellar.anchor.util.StringHelper.isEmpty;
@@ -199,15 +200,10 @@ Transaction newChallenge(ChallengeRequest request, String clientSigningKey, Memo
@Override
public Memo validateChallengeRequestMemo(ChallengeRequest request) throws SepException {
- // Validate memo. It should be 64-bit positive integer if not null.
+ // Validate memo. It should be a positive uint64 integer if not null.
try {
if (request.getMemo() != null) {
- long memoLong = Long.parseUnsignedLong(request.getMemo());
- if (memoLong <= 0) {
- infoF("Invalid memo value: {}", request.getMemo());
- throw new SepValidationException(format("Invalid memo value: %s", request.getMemo()));
- }
- return new MemoId(memoLong);
+ return makeMemoId(request.getMemo());
} else {
return null;
}
@@ -511,10 +507,14 @@ String fetchClientDomain(ChallengeTransaction challenge) {
ChallengeTransaction parseChallenge(ValidationRequest request) throws SepValidationException {
- if (request == null || request.getTransaction() == null) {
+ if (request == null || isEmpty(request.getTransaction())) {
throw new SepValidationException("{transaction} is required.");
}
+ if (request.getTransaction().length() > 50_000) {
+ throw new SepValidationException("transaction exceeds maximum allowed size");
+ }
+
String transaction = request.getTransaction();
Network network = new Network(stellarNetworkConfig.getStellarNetworkPassphrase());
String homeDomain = extractHomeDomainFromChallengeXdr(transaction, network);
diff --git a/core/src/main/java/org/stellar/anchor/sep12/Sep12Service.java b/core/src/main/java/org/stellar/anchor/sep12/Sep12Service.java
index 75e6350c6d..21e9019541 100644
--- a/core/src/main/java/org/stellar/anchor/sep12/Sep12Service.java
+++ b/core/src/main/java/org/stellar/anchor/sep12/Sep12Service.java
@@ -51,24 +51,8 @@ public Sep12Service(
Log.info("Sep12Service initialized.");
}
- public void populateRequestFromTransactionId(Sep12CustomerRequestBase requestBase)
- throws SepNotFoundException {
- if (requestBase.getTransactionId() != null) {
- try {
- GetTransactionResponse txn =
- platformApiClient.getTransaction(requestBase.getTransactionId());
- requestBase.setAccount(txn.getCustomers().getSender().getAccount());
- requestBase.setMemo(txn.getCustomers().getSender().getMemo());
- } catch (Exception e) {
- throw new SepNotFoundException("The transaction specified does not exist");
- }
- }
- }
-
public Sep12GetCustomerResponse getCustomer(WebAuthJwt token, Sep12GetCustomerRequest request)
throws AnchorException {
- populateRequestFromTransactionId(request);
-
validateGetOrPutRequest(request, token);
if (request.getAccount() == null && token.getAccount() != null) {
request.setAccount(token.getAccount());
@@ -85,8 +69,6 @@ public Sep12GetCustomerResponse getCustomer(WebAuthJwt token, Sep12GetCustomerRe
public Sep12PutCustomerResponse putCustomer(WebAuthJwt token, Sep12PutCustomerRequest request)
throws AnchorException {
- populateRequestFromTransactionId(request);
-
validateGetOrPutRequest(request, token);
if (request.getAccount() == null && token.getAccount() != null) {
@@ -179,6 +161,22 @@ void validateGetOrPutRequest(Sep12CustomerRequestBase requestBase, WebAuthJwt to
// sep31-sender, sep-31-receiver) to get the customer account and memo
GetTransactionResponse txn =
platformApiClient.getTransaction(requestBase.getTransactionId());
+
+ // Verify transaction ownership.
+ // SEP-31 stores the muxed M-address in creator.account, while SEP-6/24 store the
+ // base G-address. Match against the corresponding token field accordingly.
+ StellarId creator = txn.getCreator();
+ String creatorAccount = creator != null ? creator.getAccount() : null;
+ String tokenAccount =
+ creatorAccount != null && creatorAccount.startsWith("M")
+ ? token.getMuxedAccount()
+ : token.getAccount();
+ if (creator == null
+ || !Objects.equals(creatorAccount, tokenAccount)
+ || !Objects.equals(creator.getMemo(), token.getAccountMemo())) {
+ throw new Exception("ownership check failed");
+ }
+
StellarId customer =
"sep31-receiver".equals(requestBase.getType())
? txn.getCustomers().getReceiver()
diff --git a/core/src/main/java/org/stellar/anchor/sep45/Sep45Service.java b/core/src/main/java/org/stellar/anchor/sep45/Sep45Service.java
index 96681e65fb..97cc8a5486 100644
--- a/core/src/main/java/org/stellar/anchor/sep45/Sep45Service.java
+++ b/core/src/main/java/org/stellar/anchor/sep45/Sep45Service.java
@@ -180,7 +180,7 @@ public ValidationResponse validate(ValidationRequest request) throws AnchorExcep
throw new BadRequestException("authorization_entries is required");
}
- if (request.getAuthorizationEntries().length() > 100_000) {
+ if (request.getAuthorizationEntries().length() > 50_000) {
throw new BadRequestException("authorization_entries exceeds maximum allowed size");
}
diff --git a/core/src/main/java/org/stellar/anchor/util/ClientDomainHelper.java b/core/src/main/java/org/stellar/anchor/util/ClientDomainHelper.java
index 078ebb7d70..e27a949820 100644
--- a/core/src/main/java/org/stellar/anchor/util/ClientDomainHelper.java
+++ b/core/src/main/java/org/stellar/anchor/util/ClientDomainHelper.java
@@ -4,6 +4,9 @@
import static org.stellar.anchor.util.Log.infoF;
import java.io.IOException;
+import java.net.InetAddress;
+import java.net.URI;
+import java.net.UnknownHostException;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -23,6 +26,13 @@ public class ClientDomainHelper {
*/
public static String fetchSigningKeyFromClientDomain(String clientDomain, boolean allowHttpRetry)
throws SepException {
+ // allowHttpRetry is true for non-public networks (testnet/dev) and false for mainnet.
+ // We only enforce SSRF protection on public network because test/dev environments
+ // legitimately use localhost and internal addresses as client_domain.
+ if (!allowHttpRetry) {
+ validateDomainNotPrivateNetwork(clientDomain);
+ }
+
String clientSigningKey = "";
String url = "https://" + clientDomain + "/.well-known/stellar.toml";
try {
@@ -119,4 +129,52 @@ public static String getDefaultDomainName(List patternsAndDomains) {
}
return null;
}
+
+ /**
+ * Validates that a client domain does not resolve to a private, loopback, or link-local IP
+ * address. This prevents SSRF attacks where an attacker supplies a domain that resolves to
+ * internal network addresses.
+ *
+ * @param clientDomain The domain to validate.
+ * @throws SepException if the domain resolves to a non-public IP address.
+ */
+ public static void validateDomainNotPrivateNetwork(String clientDomain) throws SepException {
+ String hostname = extractHostname(clientDomain);
+
+ try {
+ InetAddress[] addresses = InetAddress.getAllByName(hostname);
+ for (InetAddress address : addresses) {
+ if (address.isLoopbackAddress()
+ || address.isSiteLocalAddress()
+ || address.isLinkLocalAddress()
+ || address.isAnyLocalAddress()) {
+ infoF("client_domain {} resolves to non-public address {}", clientDomain, address);
+ throw new SepException("client_domain resolves to a non-public address");
+ }
+ }
+ } catch (UnknownHostException e) {
+ infoF("client_domain {} could not be resolved", clientDomain);
+ throw new SepException("client_domain could not be resolved");
+ }
+ }
+
+ /**
+ * Extracts the hostname from a client domain string, correctly handling IPv6 literals (e.g.
+ * [::1]:8080) and domains with ports.
+ */
+ static String extractHostname(String clientDomain) {
+ try {
+ URI uri = new URI("https://" + clientDomain);
+ String host = uri.getHost();
+ if (host != null) {
+ if (host.startsWith("[") && host.endsWith("]")) {
+ host = host.substring(1, host.length() - 1);
+ }
+ return host;
+ }
+ } catch (Exception ignored) {
+ // Fall through
+ }
+ return clientDomain;
+ }
}
diff --git a/core/src/main/java/org/stellar/anchor/util/MemoHelper.java b/core/src/main/java/org/stellar/anchor/util/MemoHelper.java
index bd8777a0cb..061a347971 100644
--- a/core/src/main/java/org/stellar/anchor/util/MemoHelper.java
+++ b/core/src/main/java/org/stellar/anchor/util/MemoHelper.java
@@ -3,6 +3,7 @@
import static org.stellar.anchor.util.StringHelper.isEmpty;
import static org.stellar.sdk.xdr.MemoType.*;
+import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import org.apache.commons.codec.DecoderException;
@@ -78,7 +79,7 @@ public static Memo makeMemo(String memo, MemoType memoType) throws SepException
try {
switch (memoType) {
case MEMO_ID:
- return new MemoId(Long.parseLong(memo));
+ return makeMemoId(memo);
case MEMO_TEXT:
return new MemoText(memo);
case MEMO_HASH:
@@ -99,6 +100,20 @@ public static Memo makeMemo(String memo, MemoType memoType) throws SepException
}
}
+ /**
+ * Creates a MemoId from a string, supporting the full uint64 range (1 to
+ * 18,446,744,073,709,551,615) as defined by the Stellar protocol.
+ */
+ public static MemoId makeMemoId(String memo) {
+ try {
+ return new MemoId(new BigInteger(memo));
+ } catch (IllegalArgumentException e) {
+ NumberFormatException nfe = new NumberFormatException(e.getMessage());
+ nfe.initCause(e);
+ throw nfe;
+ }
+ }
+
public static String convertBase64ToHex(String memo) {
return Hex.encodeHexString(Base64.getDecoder().decode(memo.getBytes()));
}
@@ -160,7 +175,7 @@ public static String xdrMemoToString(org.stellar.sdk.xdr.Memo memoXdr) {
return switch (memoXdr.getDiscriminant()) {
case MEMO_NONE -> null; // No memo
case MEMO_TEXT -> new String(memoXdr.getText().getBytes(), StandardCharsets.UTF_8);
- case MEMO_ID -> String.valueOf(memoXdr.getId().getUint64().getNumber().longValue());
+ case MEMO_ID -> memoXdr.getId().getUint64().getNumber().toString();
case MEMO_HASH, MEMO_RETURN ->
Base64.getEncoder().encodeToString(memoXdr.getHash().getHash());
};
diff --git a/core/src/test/kotlin/org/stellar/anchor/sep10/Sep10ServiceTest.kt b/core/src/test/kotlin/org/stellar/anchor/sep10/Sep10ServiceTest.kt
index 1350ed2c7a..61fde73a89 100644
--- a/core/src/test/kotlin/org/stellar/anchor/sep10/Sep10ServiceTest.kt
+++ b/core/src/test/kotlin/org/stellar/anchor/sep10/Sep10ServiceTest.kt
@@ -156,8 +156,13 @@ internal class Sep10ServiceTest {
@ParameterizedTest
@CsvSource(value = ["true,test.client.stellar.org", "false,test.client.stellar.org", "false,"])
- @LockAndMockStatic([NetUtil::class, Sep10Challenge::class])
+ @LockAndMockStatic([NetUtil::class, Sep10Challenge::class, ClientDomainHelper::class])
fun `test create challenge ok`(clientAttributionRequired: Boolean, clientDomain: String?) {
+ every { ClientDomainHelper.validateDomainNotPrivateNetwork(any()) } just Runs
+ every { ClientDomainHelper.fetchSigningKeyFromClientDomain(any(), any()) } answers
+ {
+ callOriginal()
+ }
every { NetUtil.fetch(any()) } returns TEST_CLIENT_TOML
every { sep10Config.isClientAttributionRequired } returns clientAttributionRequired
@@ -275,6 +280,14 @@ internal class Sep10ServiceTest {
assertThrows { sep10Service.validateChallenge(vr) }
}
+ @Test
+ fun `Test validate challenge rejects oversized transaction`() {
+ val vr = ValidationRequest()
+ vr.transaction = "A".repeat(50_001)
+ val ex = assertThrows { sep10Service.validateChallenge(vr) }
+ assertEquals("transaction exceeds maximum allowed size", ex.message)
+ }
+
@Test
@LockAndMockStatic([Sep10Challenge::class])
fun `Test validate challenge with bad home domain failure`() {
@@ -298,8 +311,13 @@ internal class Sep10ServiceTest {
}
@Test
- @LockAndMockStatic([NetUtil::class])
+ @LockAndMockStatic([NetUtil::class, ClientDomainHelper::class])
fun `Test create challenge with wildcard matched home domain success`() {
+ every { ClientDomainHelper.validateDomainNotPrivateNetwork(any()) } just Runs
+ every { ClientDomainHelper.fetchSigningKeyFromClientDomain(any(), any()) } answers
+ {
+ callOriginal()
+ }
every { NetUtil.fetch(any()) } returns TEST_CLIENT_TOML
val cr =
ChallengeRequest.builder()
@@ -314,8 +332,13 @@ internal class Sep10ServiceTest {
}
@Test
- @LockAndMockStatic([NetUtil::class, Sep10Challenge::class])
+ @LockAndMockStatic([NetUtil::class, Sep10Challenge::class, ClientDomainHelper::class])
fun `Test create challenge request with empty memo`() {
+ every { ClientDomainHelper.validateDomainNotPrivateNetwork(any()) } just Runs
+ every { ClientDomainHelper.fetchSigningKeyFromClientDomain(any(), any()) } answers
+ {
+ callOriginal()
+ }
every { NetUtil.fetch(any()) } returns TEST_CLIENT_TOML
val cr =
ChallengeRequest.builder()
@@ -378,7 +401,7 @@ internal class Sep10ServiceTest {
}
@ParameterizedTest
- @ValueSource(strings = ["ABC", "12AB", "-1", "0", Integer.MIN_VALUE.toString()])
+ @ValueSource(strings = ["ABC", "12AB", "-1", Integer.MIN_VALUE.toString()])
fun `test createChallenge() with bad memo`(badMemo: String) {
every { sep10Config.isClientAttributionRequired } returns false
val cr =
@@ -456,8 +479,13 @@ internal class Sep10ServiceTest {
}
@Test
- @LockAndMockStatic([NetUtil::class])
+ @LockAndMockStatic([NetUtil::class, ClientDomainHelper::class])
fun `test getClientAccountId failure`() {
+ every { ClientDomainHelper.validateDomainNotPrivateNetwork(any()) } just Runs
+ every { ClientDomainHelper.fetchSigningKeyFromClientDomain(any(), any()) } answers
+ {
+ callOriginal()
+ }
every { NetUtil.fetch(any()) } returns
" NETWORK_PASSPHRASE=\"Public Global Stellar Network ; September 2015\"\n"
diff --git a/core/src/test/kotlin/org/stellar/anchor/sep12/Sep12ServiceTest.kt b/core/src/test/kotlin/org/stellar/anchor/sep12/Sep12ServiceTest.kt
index 51ac0de608..e5969426fb 100644
--- a/core/src/test/kotlin/org/stellar/anchor/sep12/Sep12ServiceTest.kt
+++ b/core/src/test/kotlin/org/stellar/anchor/sep12/Sep12ServiceTest.kt
@@ -222,9 +222,10 @@ class Sep12ServiceTest {
}
@Test
- fun `test get and put sets request account and memo using transaction`() {
+ fun `test transaction ownership check rejects mismatched creator`() {
val transaction =
GetTransactionResponse.builder()
+ .creator(StellarId.builder().account("GOTHER_ACCOUNT").build())
.customers(
Customers.builder()
.sender(StellarId.builder().account(TEST_ACCOUNT).memo(TEST_MEMO).build())
@@ -233,16 +234,50 @@ class Sep12ServiceTest {
.build()
every { platformApiClient.getTransaction(any()) } returns transaction
- val putRequest = Sep12PutCustomerRequest.builder().transactionId(TEST_TRANSACTION_ID).build()
- assertDoesNotThrow { sep12Service.populateRequestFromTransactionId(putRequest) }
- assertEquals(TEST_ACCOUNT, putRequest.account)
- assertEquals(TEST_MEMO, putRequest.memo)
+ val request = Sep12GetCustomerRequest.builder().transactionId(TEST_TRANSACTION_ID).build()
+ val jwtToken = createJwtToken(TEST_ACCOUNT)
+ val ex: SepException = assertThrows { sep12Service.validateGetOrPutRequest(request, jwtToken) }
+ assertInstanceOf(SepNotAuthorizedException::class.java, ex)
+ assertEquals("The transaction specified does not exist", ex.message)
+ }
+
+ @Test
+ fun `test transaction ownership check rejects null creator`() {
+ val transaction =
+ GetTransactionResponse.builder()
+ .customers(
+ Customers.builder()
+ .sender(StellarId.builder().account(TEST_ACCOUNT).memo(TEST_MEMO).build())
+ .build()
+ )
+ .build()
+ every { platformApiClient.getTransaction(any()) } returns transaction
+
+ val request = Sep12GetCustomerRequest.builder().transactionId(TEST_TRANSACTION_ID).build()
+ val jwtToken = createJwtToken(TEST_ACCOUNT)
+ val ex: SepException = assertThrows { sep12Service.validateGetOrPutRequest(request, jwtToken) }
+ assertInstanceOf(SepNotAuthorizedException::class.java, ex)
+ assertEquals("The transaction specified does not exist", ex.message)
+ }
- val getRequestBase =
- Sep12GetCustomerRequest.builder().transactionId(TEST_TRANSACTION_ID).build()
- assertDoesNotThrow { sep12Service.populateRequestFromTransactionId(getRequestBase) }
- assertEquals(TEST_ACCOUNT, getRequestBase.account)
- assertEquals(TEST_MEMO, getRequestBase.memo)
+ @Test
+ fun `test transaction ownership check allows matching creator`() {
+ val transaction =
+ GetTransactionResponse.builder()
+ .creator(StellarId.builder().account(TEST_ACCOUNT).build())
+ .customers(
+ Customers.builder()
+ .sender(StellarId.builder().account(TEST_ACCOUNT).memo(TEST_MEMO).build())
+ .build()
+ )
+ .build()
+ every { platformApiClient.getTransaction(any()) } returns transaction
+
+ val request = Sep12GetCustomerRequest.builder().transactionId(TEST_TRANSACTION_ID).build()
+ val jwtToken = createJwtToken(TEST_ACCOUNT)
+ assertDoesNotThrow { sep12Service.validateGetOrPutRequest(request, jwtToken) }
+ assertEquals(TEST_ACCOUNT, request.account)
+ assertEquals(TEST_MEMO, request.memo)
}
@Test
@@ -324,6 +359,7 @@ class Sep12ServiceTest {
every { eventSession.publish(capture(kycUpdateEventSlot)) } returns Unit
every { platformApiClient.getTransaction(any()) } returns
GetTransactionResponse.builder()
+ .creator(StellarId.builder().account(TEST_ACCOUNT).build())
.customers(
Customers.builder()
.sender(StellarId.builder().account(TEST_ACCOUNT).memo(TEST_MEMO).build())
diff --git a/core/src/test/kotlin/org/stellar/anchor/sep45/Sep45ServiceTest.kt b/core/src/test/kotlin/org/stellar/anchor/sep45/Sep45ServiceTest.kt
index f355964fee..1c25cb7136 100644
--- a/core/src/test/kotlin/org/stellar/anchor/sep45/Sep45ServiceTest.kt
+++ b/core/src/test/kotlin/org/stellar/anchor/sep45/Sep45ServiceTest.kt
@@ -192,6 +192,16 @@ class Sep45ServiceTest {
assertEquals("authorization_entries is required", ex.message)
}
+ @Test
+ fun `test validate throws BadRequestException when auth entries exceed max size`() {
+ val validationRequest =
+ ValidationRequest.builder().authorizationEntries("A".repeat(50_001)).build()
+
+ val ex =
+ assertThrows(BadRequestException::class.java) { sep45Service.validate(validationRequest) }
+ assertEquals("authorization_entries exceeds maximum allowed size", ex.message)
+ }
+
@Test
fun `test validate throws BadRequestException when auth entries list empty`() {
val emptyAuth = SorobanAuthorizationEntries(arrayOf())
diff --git a/core/src/test/kotlin/org/stellar/anchor/util/ClientDomainHelperTest.kt b/core/src/test/kotlin/org/stellar/anchor/util/ClientDomainHelperTest.kt
new file mode 100644
index 0000000000..e7e34594a1
--- /dev/null
+++ b/core/src/test/kotlin/org/stellar/anchor/util/ClientDomainHelperTest.kt
@@ -0,0 +1,93 @@
+package org.stellar.anchor.util
+
+import org.junit.jupiter.api.Assertions.assertDoesNotThrow
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertThrows
+import org.junit.jupiter.api.Test
+import org.stellar.anchor.api.exception.SepException
+
+class ClientDomainHelperTest {
+
+ @Test
+ fun `test loopback address is rejected`() {
+ assertThrows(SepException::class.java) {
+ ClientDomainHelper.validateDomainNotPrivateNetwork("127.0.0.1")
+ }
+ }
+
+ @Test
+ fun `test localhost is rejected`() {
+ assertThrows(SepException::class.java) {
+ ClientDomainHelper.validateDomainNotPrivateNetwork("localhost")
+ }
+ }
+
+ @Test
+ fun `test link-local address is rejected`() {
+ assertThrows(SepException::class.java) {
+ ClientDomainHelper.validateDomainNotPrivateNetwork("169.254.169.254")
+ }
+ }
+
+ @Test
+ fun `test private 10-x address is rejected`() {
+ assertThrows(SepException::class.java) {
+ ClientDomainHelper.validateDomainNotPrivateNetwork("10.0.0.1")
+ }
+ }
+
+ @Test
+ fun `test private 192-168 address is rejected`() {
+ assertThrows(SepException::class.java) {
+ ClientDomainHelper.validateDomainNotPrivateNetwork("192.168.1.1")
+ }
+ }
+
+ @Test
+ fun `test private 172-16 address is rejected`() {
+ assertThrows(SepException::class.java) {
+ ClientDomainHelper.validateDomainNotPrivateNetwork("172.16.0.1")
+ }
+ }
+
+ @Test
+ fun `test domain with port strips port before resolving`() {
+ assertThrows(SepException::class.java) {
+ ClientDomainHelper.validateDomainNotPrivateNetwork("127.0.0.1:8080")
+ }
+ }
+
+ @Test
+ fun `test unresolvable domain is rejected`() {
+ assertThrows(SepException::class.java) {
+ ClientDomainHelper.validateDomainNotPrivateNetwork(
+ "this-domain-does-not-exist-xyz123.invalid"
+ )
+ }
+ }
+
+ @Test
+ fun `test public address is accepted`() {
+ assertDoesNotThrow { ClientDomainHelper.validateDomainNotPrivateNetwork("8.8.8.8") }
+ }
+
+ @Test
+ fun `test extractHostname with plain domain`() {
+ assertEquals("example.com", ClientDomainHelper.extractHostname("example.com"))
+ }
+
+ @Test
+ fun `test extractHostname with domain and port`() {
+ assertEquals("example.com", ClientDomainHelper.extractHostname("example.com:8080"))
+ }
+
+ @Test
+ fun `test extractHostname with bracketed IPv6`() {
+ assertEquals("::1", ClientDomainHelper.extractHostname("[::1]"))
+ }
+
+ @Test
+ fun `test extractHostname with bracketed IPv6 and port`() {
+ assertEquals("::1", ClientDomainHelper.extractHostname("[::1]:8080"))
+ }
+}
diff --git a/core/src/test/kotlin/org/stellar/anchor/util/MemoHelperTest.kt b/core/src/test/kotlin/org/stellar/anchor/util/MemoHelperTest.kt
index 856ce2642d..06cc57df0c 100644
--- a/core/src/test/kotlin/org/stellar/anchor/util/MemoHelperTest.kt
+++ b/core/src/test/kotlin/org/stellar/anchor/util/MemoHelperTest.kt
@@ -63,6 +63,47 @@ internal class MemoHelperTest {
)
}
+ @Test
+ fun `test makeMemoId with valid uint64 values`() {
+ // Small value - should not throw
+ makeMemoId("1234")
+
+ // Max signed long - should not throw
+ makeMemoId("9223372036854775807")
+
+ // Above signed long max (valid uint64)
+ makeMemoId("11872666534918305457")
+
+ // Max uint64 - should not throw
+ makeMemoId("18446744073709551615")
+ }
+
+ @Test
+ fun `test makeMemo rejects invalid memo id values`() {
+ assertThrows { makeMemo("-1", "id") }
+ assertThrows { makeMemo("18446744073709551616", "id") }
+ assertThrows { makeMemo("abc", "id") }
+ }
+
+ @Test
+ fun `test xdrMemoToString round-trip with uint64 memo ids`() {
+ // Round-trip: makeMemoId -> toXdr -> xdrMemoToString should preserve the original value
+ val testValues =
+ listOf("1234", "9223372036854775807", "11872666534918305457", "18446744073709551615")
+ for (value in testValues) {
+ val memo = makeMemoId(value)
+ val xdr = toXdr(memo)
+ assertEquals(value, xdrMemoToString(xdr))
+ }
+ }
+
+ @Test
+ fun `test xdrMemoToString with null and none`() {
+ assertEquals(null, xdrMemoToString(null))
+ val noneMemo = toXdr(null)
+ assertEquals(null, xdrMemoToString(noneMemo))
+ }
+
@Test
fun `test toXdr()`() {
val memoId: MemoId = makeMemo("123", MEMO_ID) as MemoId
diff --git a/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/e2etest/Sep6End2EndTest.kt b/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/e2etest/Sep6End2EndTest.kt
index 58246eff44..6b48999daf 100644
--- a/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/e2etest/Sep6End2EndTest.kt
+++ b/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/e2etest/Sep6End2EndTest.kt
@@ -31,7 +31,6 @@ import org.stellar.reference.wallet.WalletServerClient
import org.stellar.sdk.Asset
import org.stellar.sdk.KeyPair
import org.stellar.sdk.MuxedAccount
-import org.stellar.walletsdk.anchor.customer
import org.stellar.walletsdk.asset.IssuedAssetId
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@@ -119,9 +118,8 @@ open class Sep6End2EndTest : IntegrationTestBase(TestConfig()) {
// Supply missing KYC info to continue with the transaction
val additionalRequiredFields =
- anchor
- .customer(token)
- .get(transactionId = deposit.id)
+ wallet.sep12
+ .getCustomer(transactionId = deposit.id)!!
.fields
?.filter { it.key != null && it.value?.optional == false }
?.map { it.key!! }
diff --git a/platform/src/main/java/org/stellar/anchor/platform/observer/stellar/DefaultPaymentListener.java b/platform/src/main/java/org/stellar/anchor/platform/observer/stellar/DefaultPaymentListener.java
index 8ab61e895a..03f378723c 100644
--- a/platform/src/main/java/org/stellar/anchor/platform/observer/stellar/DefaultPaymentListener.java
+++ b/platform/src/main/java/org/stellar/anchor/platform/observer/stellar/DefaultPaymentListener.java
@@ -149,7 +149,7 @@ void processAndDispatchLedgerPayment(
&& accountType(ledgerPayment.getTo()) == Muxed) {
MuxedAccount muxedAccount = new MuxedAccount(ledgerPayment.getTo());
toAccount = muxedAccount.getAccountId();
- memo = Memo.id(Objects.requireNonNull(muxedAccount.getMuxedId()).longValue());
+ memo = Memo.id(Objects.requireNonNull(muxedAccount.getMuxedId()));
} else {
toAccount = ledgerPayment.getTo();
memo = Memo.fromXdr(ledgerTransaction.getMemo());
diff --git a/service-runner/src/main/resources/version-info.properties b/service-runner/src/main/resources/version-info.properties
index 1171e9d55f..80affa40aa 100644
--- a/service-runner/src/main/resources/version-info.properties
+++ b/service-runner/src/main/resources/version-info.properties
@@ -1 +1 @@
-version=4.2.0
+version=4.2.1