From a758fa799759583e40ac57b4504b28e65a8f251f Mon Sep 17 00:00:00 2001 From: Philip Liu <12836897+philipliu@users.noreply.github.com> Date: Thu, 2 Nov 2023 18:36:10 -0400 Subject: [PATCH] [ANCHOR-508] SEP-6: Custody integration (#1179) ### Description This implements the custody service RPC actions. This includes payments as well as refunds. ### Context SEP-6 will support custodians. ### Testing - `./gradlew test` ### Documentation - [stellar-docs](https://github.com/stellar/stellar-docs/pull/253) ### Known limitations N/A --- .../workflows/sub_gradle_test_and_build.yml | 4 +- .../org/stellar/anchor/config/Sep6Config.java | 8 + .../anchor/custody/CustodyService.java | 9 + .../anchor/sep6/Sep6DepositInfoGenerator.java | 16 + .../org/stellar/anchor/sep6/Sep6Service.java | 22 +- .../org/stellar/anchor/util/MemoHelper.java | 7 - .../anchor/util/TransactionHelper.java | 32 +- .../stellar/anchor/sep6/Sep6ServiceTest.kt | 16 - .../anchor/sep6/Sep6ServiceTestData.kt | 16 +- .../AnchorPlatformCustodyEnd2EndTest.kt | 8 + .../platform/AnchorPlatformEnd2EndTest.kt | 2 + .../platform/test/PlatformApiCustodyTests.kt | 16 +- .../anchor/platform/test/Sep6End2EndTest.kt | 8 +- .../stellar/anchor/platform/test/Sep6Tests.kt | 11 +- .../platform/PlatformServerBeans.java | 28 +- .../component/platform/RpcActionBeans.java | 3 + .../platform/component/sep/SepBeans.java | 6 - .../component/share/UtilityBeans.java | 16 +- .../platform/config/PropertySep6Config.java | 32 +- .../platform/rpc/DoStellarPaymentHandler.java | 119 ++-- .../platform/rpc/DoStellarRefundHandler.java | 29 +- .../NotifyOffchainFundsReceivedHandler.java | 21 +- .../platform/rpc/NotifyTrustSetHandler.java | 23 +- .../rpc/RequestOnchainFundsHandler.java | 45 +- .../platform/service/CustodyServiceImpl.java | 6 + .../Sep6DepositInfoCustodyGenerator.java | 23 + .../service/Sep6DepositInfoNoneGenerator.java | 14 + .../service/Sep6DepositInfoSelfGenerator.java | 32 ++ .../platform/service/TransactionService.java | 31 ++ .../config/anchor-config-default-values.yaml | 10 + .../config/anchor-config-schema-v1.yaml | 1 + .../anchor/platform/config/Sep6ConfigTest.kt | 36 +- .../rpc/DoStellarPaymentHandlerTest.kt | 338 +++++++++++- .../rpc/DoStellarRefundHandlerTest.kt | 515 +++++++++++++----- .../NotifyOffchainFundsReceivedHandlerTest.kt | 14 +- .../platform/rpc/NotifyTrustSetHandlerTest.kt | 352 +++++++++++- .../rpc/RequestOnchainFundsHandlerTest.kt | 190 ++++++- .../platform/service/CustodyServiceTest.kt | 154 +++++- .../Sep6DepositInfoCustodyGeneratorTest.kt | 44 ++ .../Sep6DepositInfoSelfGeneratorTest.kt | 49 ++ .../service/TransactionServiceTest.kt | 248 ++++++++- .../profiles/default-custody-rpc/config.env | 1 + .../profiles/default-custody/config.env | 1 + 43 files changed, 2206 insertions(+), 350 deletions(-) create mode 100644 core/src/main/java/org/stellar/anchor/sep6/Sep6DepositInfoGenerator.java create mode 100644 platform/src/main/java/org/stellar/anchor/platform/service/Sep6DepositInfoCustodyGenerator.java create mode 100644 platform/src/main/java/org/stellar/anchor/platform/service/Sep6DepositInfoNoneGenerator.java create mode 100644 platform/src/main/java/org/stellar/anchor/platform/service/Sep6DepositInfoSelfGenerator.java create mode 100644 platform/src/test/kotlin/org/stellar/anchor/platform/service/Sep6DepositInfoCustodyGeneratorTest.kt create mode 100644 platform/src/test/kotlin/org/stellar/anchor/platform/service/Sep6DepositInfoSelfGeneratorTest.kt diff --git a/.github/workflows/sub_gradle_test_and_build.yml b/.github/workflows/sub_gradle_test_and_build.yml index 4b7fcb1349..f847bb0bcc 100644 --- a/.github/workflows/sub_gradle_test_and_build.yml +++ b/.github/workflows/sub_gradle_test_and_build.yml @@ -42,7 +42,7 @@ jobs: # Prepare Stellar Validation Tests - name: Pull Stellar Validation Tests Docker Image - run: docker pull stellar/anchor-tests:v0.6.7 & + run: docker pull stellar/anchor-tests:v0.6.9 & # Set up JDK 11 - name: Set up JDK 11 @@ -109,7 +109,7 @@ jobs: - name: Run Stellar validation tool run: | - docker run --network host -v ${GITHUB_WORKSPACE}/platform/src/test/resources://config stellar/anchor-tests:v0.6.7 --home-domain http://host.docker.internal:8080 --seps 1 6 10 12 24 31 38 --sep-config //config/stellar-anchor-tests-sep-config.json --verbose + docker run --network host -v ${GITHUB_WORKSPACE}/platform/src/test/resources://config stellar/anchor-tests:v0.6.9 --home-domain http://host.docker.internal:8080 --seps 1 6 10 12 24 31 38 --sep-config //config/stellar-anchor-tests-sep-config.json --verbose - name: Upload Artifacts if: always() diff --git a/core/src/main/java/org/stellar/anchor/config/Sep6Config.java b/core/src/main/java/org/stellar/anchor/config/Sep6Config.java index 296ba04cad..1c3176f892 100644 --- a/core/src/main/java/org/stellar/anchor/config/Sep6Config.java +++ b/core/src/main/java/org/stellar/anchor/config/Sep6Config.java @@ -11,6 +11,8 @@ public interface Sep6Config { Features getFeatures(); + DepositInfoGeneratorType getDepositInfoGeneratorType(); + @Getter @Setter @AllArgsConstructor @@ -22,4 +24,10 @@ class Features { @SerializedName("claimable_balances") boolean claimableBalances; } + + enum DepositInfoGeneratorType { + SELF, + CUSTODY, + NONE + } } diff --git a/core/src/main/java/org/stellar/anchor/custody/CustodyService.java b/core/src/main/java/org/stellar/anchor/custody/CustodyService.java index 3ade2d82b2..2b3e5c1105 100644 --- a/core/src/main/java/org/stellar/anchor/custody/CustodyService.java +++ b/core/src/main/java/org/stellar/anchor/custody/CustodyService.java @@ -5,9 +5,18 @@ import org.stellar.anchor.api.rpc.method.DoStellarRefundRequest; import org.stellar.anchor.sep24.Sep24Transaction; import org.stellar.anchor.sep31.Sep31Transaction; +import org.stellar.anchor.sep6.Sep6Transaction; public interface CustodyService { + /** + * Create custody transaction for SEP6 transaction + * + * @param txn SEP6 transaction + * @throws AnchorException if error happens + */ + void createTransaction(Sep6Transaction txn) throws AnchorException; + /** * Create custody transaction for SEP24 transaction * diff --git a/core/src/main/java/org/stellar/anchor/sep6/Sep6DepositInfoGenerator.java b/core/src/main/java/org/stellar/anchor/sep6/Sep6DepositInfoGenerator.java new file mode 100644 index 0000000000..0fc26f6442 --- /dev/null +++ b/core/src/main/java/org/stellar/anchor/sep6/Sep6DepositInfoGenerator.java @@ -0,0 +1,16 @@ +package org.stellar.anchor.sep6; + +import org.stellar.anchor.api.exception.AnchorException; +import org.stellar.anchor.api.shared.SepDepositInfo; + +public interface Sep6DepositInfoGenerator { + + /** + * Gets the deposit info based on the input parameter. + * + * @param txn the original SEP-6 transaction the deposit info will be used for. + * @return a SepDepositInfo instance containing the destination address, memo and memoType. + * @throws AnchorException if the deposit info cannot be generated + */ + SepDepositInfo generate(Sep6Transaction txn) throws AnchorException; +} diff --git a/core/src/main/java/org/stellar/anchor/sep6/Sep6Service.java b/core/src/main/java/org/stellar/anchor/sep6/Sep6Service.java index f013587062..bff3b0a884 100644 --- a/core/src/main/java/org/stellar/anchor/sep6/Sep6Service.java +++ b/core/src/main/java/org/stellar/anchor/sep6/Sep6Service.java @@ -1,7 +1,6 @@ package org.stellar.anchor.sep6; import static org.stellar.anchor.util.MemoHelper.*; -import static org.stellar.sdk.xdr.MemoType.MEMO_HASH; import com.google.common.collect.ImmutableMap; import java.time.Instant; @@ -190,7 +189,6 @@ public StartDepositResponse depositExchange(Sep10Jwt token, StartDepositExchange .amountFee(amounts.getAmountFee()) .amountFeeAsset(amounts.getAmountFeeAsset()) .amountExpected(request.getAmount()) - .amountExpected(request.getAmount()) .startedAt(Instant.now()) .sep10Account(token.getAccount()) .sep10AccountMemo(token.getAccountMemo()) @@ -270,10 +268,7 @@ public StartWithdrawResponse withdraw(Sep10Jwt token, StartWithdrawRequest reque .startedAt(Instant.now()) .sep10Account(token.getAccount()) .sep10AccountMemo(token.getAccountMemo()) - .memo(generateMemo(id)) - .memoType(memoTypeAsString(MEMO_HASH)) .fromAccount(sourceAccount) - .withdrawAnchorAccount(asset.getDistributionAccount()) .refundMemo(request.getRefundMemo()) .refundMemoType(request.getRefundMemoType()); @@ -288,12 +283,7 @@ public StartWithdrawResponse withdraw(Sep10Jwt token, StartWithdrawRequest reque .transaction(TransactionHelper.toGetTransactionResponse(txn, assetService)) .build()); - return StartWithdrawResponse.builder() - .accountId(asset.getDistributionAccount()) - .id(txn.getId()) - .memo(txn.getMemo()) - .memoType(memoTypeAsString(MEMO_HASH)) - .build(); + return StartWithdrawResponse.builder().id(txn.getId()).build(); } public StartWithdrawResponse withdrawExchange( @@ -364,10 +354,7 @@ public StartWithdrawResponse withdrawExchange( .startedAt(Instant.now()) .sep10Account(token.getAccount()) .sep10AccountMemo(token.getAccountMemo()) - .memo(generateMemo(id)) - .memoType(memoTypeAsString(MEMO_HASH)) .fromAccount(sourceAccount) - .withdrawAnchorAccount(sellAsset.getDistributionAccount()) .refundMemo(request.getRefundMemo()) .refundMemoType(request.getRefundMemoType()) .quoteId(request.getQuoteId()); @@ -383,12 +370,7 @@ public StartWithdrawResponse withdrawExchange( .transaction(TransactionHelper.toGetTransactionResponse(txn, assetService)) .build()); - return StartWithdrawResponse.builder() - .accountId(sellAsset.getDistributionAccount()) - .id(txn.getId()) - .memo(txn.getMemo()) - .memoType(memoTypeAsString(MEMO_HASH)) - .build(); + return StartWithdrawResponse.builder().id(txn.getId()).build(); } public GetTransactionsResponse findTransactions(Sep10Jwt token, GetTransactionsRequest request) 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 e808866b0e..1a3ef53952 100644 --- a/core/src/main/java/org/stellar/anchor/util/MemoHelper.java +++ b/core/src/main/java/org/stellar/anchor/util/MemoHelper.java @@ -6,7 +6,6 @@ import java.util.Base64; import org.apache.commons.codec.DecoderException; import org.apache.commons.codec.binary.Hex; -import org.apache.commons.lang3.StringUtils; import org.stellar.anchor.api.exception.SepException; import org.stellar.anchor.api.exception.SepValidationException; import org.stellar.sdk.*; @@ -126,10 +125,4 @@ public static String memoAsString(Memo memo) throws SepException { throw new SepException("Unsupported value: " + memoTypeStr); } } - - public static String generateMemo(String transactionId) { - String memo = StringUtils.truncate(transactionId, 32); - memo = StringUtils.leftPad(memo, 32, "0"); - return new String(Base64.getEncoder().encode(memo.getBytes())); - } } diff --git a/core/src/main/java/org/stellar/anchor/util/TransactionHelper.java b/core/src/main/java/org/stellar/anchor/util/TransactionHelper.java index cdb8a46614..4ae9e63399 100644 --- a/core/src/main/java/org/stellar/anchor/util/TransactionHelper.java +++ b/core/src/main/java/org/stellar/anchor/util/TransactionHelper.java @@ -1,9 +1,8 @@ package org.stellar.anchor.util; -import static org.stellar.anchor.api.platform.PlatformTransactionData.Kind.DEPOSIT; -import static org.stellar.anchor.api.platform.PlatformTransactionData.Kind.RECEIVE; -import static org.stellar.anchor.api.platform.PlatformTransactionData.Kind.WITHDRAWAL; +import static org.stellar.anchor.api.platform.PlatformTransactionData.Kind.*; +import com.google.common.collect.ImmutableSet; import java.util.Optional; import javax.annotation.Nullable; import org.stellar.anchor.api.custody.CreateCustodyTransactionRequest; @@ -22,6 +21,33 @@ public class TransactionHelper { + public static CreateCustodyTransactionRequest toCustodyTransaction(Sep6Transaction txn) { + PlatformTransactionData.Kind kind = PlatformTransactionData.Kind.from(txn.getKind()); + return CreateCustodyTransactionRequest.builder() + .id(txn.getId()) + .memo(txn.getMemo()) + .memoType(txn.getMemoType()) + .protocol("6") + .fromAccount( + ImmutableSet.of(WITHDRAWAL, WITHDRAWAL_EXCHANGE).contains(kind) + ? txn.getFromAccount() + : null) + .toAccount( + ImmutableSet.of(DEPOSIT, DEPOSIT_EXCHANGE).contains(kind) + ? txn.getToAccount() + : txn.getWithdrawAnchorAccount()) + .amount( + ImmutableSet.of(DEPOSIT, DEPOSIT_EXCHANGE).contains(kind) + ? txn.getAmountOut() + : Optional.ofNullable(txn.getAmountExpected()).orElse(txn.getAmountIn())) + .asset( + ImmutableSet.of(DEPOSIT, DEPOSIT_EXCHANGE).contains(kind) + ? txn.getAmountOutAsset() + : txn.getAmountInAsset()) + .kind(txn.getKind()) + .build(); + } + public static CreateCustodyTransactionRequest toCustodyTransaction(Sep24Transaction txn) { return CreateCustodyTransactionRequest.builder() .id(txn.getId()) diff --git a/core/src/test/kotlin/org/stellar/anchor/sep6/Sep6ServiceTest.kt b/core/src/test/kotlin/org/stellar/anchor/sep6/Sep6ServiceTest.kt index f9db345161..4eec4f174d 100644 --- a/core/src/test/kotlin/org/stellar/anchor/sep6/Sep6ServiceTest.kt +++ b/core/src/test/kotlin/org/stellar/anchor/sep6/Sep6ServiceTest.kt @@ -657,8 +657,6 @@ class Sep6ServiceTest { JSONCompareMode.LENIENT ) assert(slotTxn.captured.id.isNotEmpty()) - assert(slotTxn.captured.memo.isNotEmpty()) - assertEquals(slotTxn.captured.memoType, "hash") assertNotNull(slotTxn.captured.startedAt) JSONAssert.assertEquals( @@ -668,8 +666,6 @@ class Sep6ServiceTest { ) assert(slotEvent.captured.id.isNotEmpty()) assert(slotEvent.captured.transaction.id.isNotEmpty()) - assert(slotEvent.captured.transaction.memo.isNotEmpty()) - assertEquals(slotEvent.captured.transaction.memoType, "hash") assertNotNull(slotEvent.captured.transaction.startedAt) // Verify response @@ -738,8 +734,6 @@ class Sep6ServiceTest { JSONCompareMode.LENIENT ) assert(slotTxn.captured.id.isNotEmpty()) - assert(slotTxn.captured.memo.isNotEmpty()) - assertEquals(slotTxn.captured.memoType, "hash") assertNotNull(slotTxn.captured.startedAt) JSONAssert.assertEquals( @@ -749,8 +743,6 @@ class Sep6ServiceTest { ) assert(slotEvent.captured.id.isNotEmpty()) assert(slotEvent.captured.transaction.id.isNotEmpty()) - assert(slotEvent.captured.transaction.memo.isNotEmpty()) - assertEquals(slotEvent.captured.transaction.memoType, "hash") assertNotNull(slotEvent.captured.transaction.startedAt) // Verify response @@ -945,8 +937,6 @@ class Sep6ServiceTest { JSONCompareMode.LENIENT ) assert(slotTxn.captured.id.isNotEmpty()) - assert(slotTxn.captured.memo.isNotEmpty()) - assertEquals(slotTxn.captured.memoType, "hash") assertNotNull(slotTxn.captured.startedAt) JSONAssert.assertEquals( @@ -956,8 +946,6 @@ class Sep6ServiceTest { ) assert(slotEvent.captured.id.isNotEmpty()) assert(slotEvent.captured.transaction.id.isNotEmpty()) - assert(slotEvent.captured.transaction.memo.isNotEmpty()) - assertEquals(slotEvent.captured.transaction.memoType, "hash") assertNotNull(slotEvent.captured.transaction.startedAt) // Verify response @@ -1018,8 +1006,6 @@ class Sep6ServiceTest { JSONCompareMode.LENIENT ) assert(slotTxn.captured.id.isNotEmpty()) - assert(slotTxn.captured.memo.isNotEmpty()) - assertEquals(slotTxn.captured.memoType, "hash") assertNotNull(slotTxn.captured.startedAt) JSONAssert.assertEquals( @@ -1029,8 +1015,6 @@ class Sep6ServiceTest { ) assert(slotEvent.captured.id.isNotEmpty()) assert(slotEvent.captured.transaction.id.isNotEmpty()) - assert(slotEvent.captured.transaction.memo.isNotEmpty()) - assertEquals(slotEvent.captured.transaction.memoType, "hash") assertNotNull(slotEvent.captured.transaction.startedAt) // Verify response diff --git a/core/src/test/kotlin/org/stellar/anchor/sep6/Sep6ServiceTestData.kt b/core/src/test/kotlin/org/stellar/anchor/sep6/Sep6ServiceTestData.kt index ddd558530f..3d02534883 100644 --- a/core/src/test/kotlin/org/stellar/anchor/sep6/Sep6ServiceTestData.kt +++ b/core/src/test/kotlin/org/stellar/anchor/sep6/Sep6ServiceTestData.kt @@ -388,10 +388,8 @@ class Sep6ServiceTestData { val withdrawResJson = """ { - "account_id": "GA7FYRB5VREZKOBIIKHG5AVTPFGWUBPOBF7LTYG4GTMFVIOOD2DWAL7I", - "memo_type": "hash" } - """ + """ .trimIndent() val withdrawTxnJson = @@ -411,9 +409,7 @@ class Sep6ServiceTestData { "amountExpected": "100", "sep10Account": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", "sep10AccountMemo": "123", - "withdrawAnchorAccount": "GA7FYRB5VREZKOBIIKHG5AVTPFGWUBPOBF7LTYG4GTMFVIOOD2DWAL7I", "fromAccount": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", - "memoType": "hash", "refundMemo": "some text", "refundMemoType": "text" } @@ -446,7 +442,6 @@ class Sep6ServiceTestData { "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" }, "source_account": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", - "memo_type": "hash", "refund_memo": "some text", "refund_memo_type": "text", "customers": { @@ -477,9 +472,7 @@ class Sep6ServiceTestData { "amountFeeAsset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP", "sep10Account": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", "sep10AccountMemo": "123", - "withdrawAnchorAccount": "GA7FYRB5VREZKOBIIKHG5AVTPFGWUBPOBF7LTYG4GTMFVIOOD2DWAL7I", "fromAccount": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", - "memoType": "hash", "refundMemo": "some text", "refundMemoType": "text" } @@ -503,7 +496,6 @@ class Sep6ServiceTestData { "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" }, "source_account": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", - "memo_type": "hash", "refund_memo": "some text", "refund_memo_type": "text", "customers": { @@ -538,9 +530,7 @@ class Sep6ServiceTestData { "amountExpected": "100", "sep10Account": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", "sep10AccountMemo": "123", - "withdrawAnchorAccount": "GA7FYRB5VREZKOBIIKHG5AVTPFGWUBPOBF7LTYG4GTMFVIOOD2DWAL7I", "fromAccount": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", - "memoType": "hash", "quoteId": "test-quote-id", "refundMemo": "some text", "refundMemoType": "text" @@ -569,7 +559,6 @@ class Sep6ServiceTestData { "amount_fee": { "amount": "2", "asset": "iso4217:USD" }, "quote_id": "test-quote-id", "source_account": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", - "memo_type": "hash", "refund_memo": "some text", "refund_memo_type": "text", "customers": { @@ -604,9 +593,7 @@ class Sep6ServiceTestData { "amountExpected": "100", "sep10Account": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", "sep10AccountMemo": "123", - "withdrawAnchorAccount": "GA7FYRB5VREZKOBIIKHG5AVTPFGWUBPOBF7LTYG4GTMFVIOOD2DWAL7I", "fromAccount": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", - "memoType": "hash", "refundMemo": "some text", "refundMemoType": "text" } @@ -637,7 +624,6 @@ class Sep6ServiceTestData { "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" }, "source_account": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO", - "memo_type": "hash", "refund_memo": "some text", "refund_memo_type": "text", "customers": { diff --git a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/AnchorPlatformCustodyEnd2EndTest.kt b/integration-tests/src/test/kotlin/org/stellar/anchor/platform/AnchorPlatformCustodyEnd2EndTest.kt index 11af11c975..b2e49eb590 100644 --- a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/AnchorPlatformCustodyEnd2EndTest.kt +++ b/integration-tests/src/test/kotlin/org/stellar/anchor/platform/AnchorPlatformCustodyEnd2EndTest.kt @@ -28,4 +28,12 @@ class AnchorPlatformCustodyEnd2EndTest : fun runSep24Test() { singleton.sep24CustodyE2eTests.testAll() } + + @Test + @Order(11) + fun runSep6Test() { + // The SEP-6 reference server implementation only implements RPC, so technically this test + // should be in the RPC test suite. + singleton.sep6E2eTests.testAll() + } } diff --git a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/AnchorPlatformEnd2EndTest.kt b/integration-tests/src/test/kotlin/org/stellar/anchor/platform/AnchorPlatformEnd2EndTest.kt index d504913ab8..8d38b58316 100644 --- a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/AnchorPlatformEnd2EndTest.kt +++ b/integration-tests/src/test/kotlin/org/stellar/anchor/platform/AnchorPlatformEnd2EndTest.kt @@ -31,6 +31,8 @@ class AnchorPlatformEnd2EndTest : AbstractIntegrationTest(TestConfig(testProfile @Test @Order(2) fun runSep6Test() { + // The SEP-6 reference server implementation only implements RPC, so technically this test + // should be in the RPC test suite. singleton.sep6E2eTests.testAll() } } diff --git a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/PlatformApiCustodyTests.kt b/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/PlatformApiCustodyTests.kt index b72618bce8..faa3f0cc50 100644 --- a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/PlatformApiCustodyTests.kt +++ b/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/PlatformApiCustodyTests.kt @@ -62,6 +62,14 @@ class PlatformApiCustodyTests(config: TestConfig, toml: Sep1Helper.TomlContent, `SEP-31 refunded do_stellar_refund`() } + private fun `SEP-6 deposit complete full`() { + // TODO(philip): add this after custody changes are merged + } + + private fun `SEP-6 withdraw full refund`() { + // TODO(philip): add this after custody changes are merged + } + /** * 1. incomplete -> notify_interactive_flow_complete * 2. pending_anchor -> request_offchain_funds @@ -70,7 +78,7 @@ class PlatformApiCustodyTests(config: TestConfig, toml: Sep1Helper.TomlContent, * 5. completed */ private fun `SEP-24 deposit complete full`() { - `test deposit flow`( + `test SEP-24 deposit flow`( SEP_24_DEPOSIT_COMPLETE_FULL_FLOW_ACTION_REQUESTS, SEP_24_DEPOSIT_COMPLETE_FULL_FLOW_ACTION_RESPONSES ) @@ -85,7 +93,7 @@ class PlatformApiCustodyTests(config: TestConfig, toml: Sep1Helper.TomlContent, * 6. refunded */ private fun `SEP-24 withdraw full refund`() { - `test withdraw flow`( + `test SEP-24 withdraw flow`( SEP_24_WITHDRAW_FULL_REFUND_FLOW_ACTION_REQUESTS, SEP_24_WITHDRAW_FULL_REFUND_FLOW_ACTION_RESPONSES ) @@ -104,7 +112,7 @@ class PlatformApiCustodyTests(config: TestConfig, toml: Sep1Helper.TomlContent, ) } - private fun `test deposit flow`(actionRequests: String, actionResponse: String) { + private fun `test SEP-24 deposit flow`(actionRequests: String, actionResponse: String) { val depositRequest = gson.fromJson(SEP_24_DEPOSIT_FLOW_REQUEST, HashMap::class.java) val depositResponse = sep24Client.deposit(depositRequest as HashMap) `test flow`(depositResponse.id, actionRequests, actionResponse) @@ -136,7 +144,7 @@ class PlatformApiCustodyTests(config: TestConfig, toml: Sep1Helper.TomlContent, `test flow`(receiveResponse.id, updatedActionRequests, updatedActionResponses) } - private fun `test withdraw flow`(actionRequests: String, actionResponse: String) { + private fun `test SEP-24 withdraw flow`(actionRequests: String, actionResponse: String) { val withdrawRequest = gson.fromJson(SEP_24_WITHDRAW_FLOW_REQUEST, HashMap::class.java) val withdrawResponse = sep24Client.withdraw(withdrawRequest as HashMap) `test flow`(withdrawResponse.id, actionRequests, actionResponse) diff --git a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep6End2EndTest.kt b/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep6End2EndTest.kt index fa68495e89..033bc8d793 100644 --- a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep6End2EndTest.kt +++ b/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep6End2EndTest.kt @@ -15,6 +15,7 @@ import org.stellar.anchor.api.shared.InstructionField import org.stellar.anchor.platform.CLIENT_WALLET_SECRET import org.stellar.anchor.platform.Sep6Client import org.stellar.anchor.platform.TestConfig +import org.stellar.anchor.util.GsonUtils import org.stellar.anchor.util.Log import org.stellar.reference.client.AnchorReferenceServerClient import org.stellar.reference.wallet.WalletServerClient @@ -152,12 +153,14 @@ class Sep6End2EndTest(val config: TestConfig, val jwt: String) { anchor.customer(token).add(additionalRequiredFields.associateWith { customerInfo[it]!! }) waitStatus(withdraw.id, PENDING_USR_TRANSFER_START, sep6Client) + val withdrawTxn = sep6Client.getTransaction(mapOf("id" to withdraw.id)).transaction + // Transfer the withdrawal amount to the Anchor val transfer = wallet .stellar() - .transaction(keypair, memo = Pair(MemoType.HASH, withdraw.memo)) - .transfer(withdraw.accountId, USDC, "1") + .transaction(keypair, memo = Pair(MemoType.HASH, withdrawTxn.withdrawMemo)) + .transfer(withdrawTxn.withdrawAnchorAccount, USDC, "1") .build() transfer.sign(keypair) wallet.stellar().submitTransaction(transfer) @@ -206,6 +209,7 @@ class Sep6End2EndTest(val config: TestConfig, val jwt: String) { if (expectedStatus.status != transaction.transaction.status) { Log.info("Transaction status: ${transaction.transaction.status}") } else { + Log.info("${GsonUtils.getInstance().toJson(transaction)}") Log.info( "Transaction status ${transaction.transaction.status} matched expected status $expectedStatus" ) diff --git a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep6Tests.kt b/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep6Tests.kt index ef65fae986..7efba7816b 100644 --- a/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep6Tests.kt +++ b/integration-tests/src/test/kotlin/org/stellar/anchor/platform/test/Sep6Tests.kt @@ -159,8 +159,7 @@ class Sep6Tests(val toml: TomlContent, jwt: String) { "transaction": { "kind": "withdrawal", "status": "incomplete", - "from": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", - "withdraw_memo_type": "hash" + "from": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" } } """ @@ -178,9 +177,7 @@ class Sep6Tests(val toml: TomlContent, jwt: String) { "amount_out_asset": "iso4217:USD", "amount_fee": "0", "amount_fee_asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP", - "from": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", - "withdraw_anchor_account": "GBN4NNCDGJO4XW4KQU3CBIESUJWFVBUZPOKUZHT7W7WRB7CWOA7BXVQF", - "withdraw_memo_type": "hash" + "from": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" } } """ @@ -198,9 +195,7 @@ class Sep6Tests(val toml: TomlContent, jwt: String) { "amount_out_asset": "iso4217:USD", "amount_fee": "1.00", "amount_fee_asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP", - "from": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", - "withdraw_anchor_account": "GBN4NNCDGJO4XW4KQU3CBIESUJWFVBUZPOKUZHT7W7WRB7CWOA7BXVQF", - "withdraw_memo_type": "hash" + "from": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG" } } """ diff --git a/platform/src/main/java/org/stellar/anchor/platform/component/platform/PlatformServerBeans.java b/platform/src/main/java/org/stellar/anchor/platform/component/platform/PlatformServerBeans.java index 3c87df8549..4824ea6770 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/component/platform/PlatformServerBeans.java +++ b/platform/src/main/java/org/stellar/anchor/platform/component/platform/PlatformServerBeans.java @@ -10,6 +10,7 @@ import org.stellar.anchor.auth.JwtService; import org.stellar.anchor.config.CustodyConfig; import org.stellar.anchor.config.Sep24Config; +import org.stellar.anchor.config.Sep6Config; import org.stellar.anchor.custody.CustodyService; import org.stellar.anchor.event.EventService; import org.stellar.anchor.filter.ApiKeyFilter; @@ -22,14 +23,12 @@ import org.stellar.anchor.platform.data.JdbcTransactionPendingTrustRepo; import org.stellar.anchor.platform.job.TrustlineCheckJob; import org.stellar.anchor.platform.rpc.NotifyTrustSetHandler; -import org.stellar.anchor.platform.service.Sep24DepositInfoCustodyGenerator; -import org.stellar.anchor.platform.service.Sep24DepositInfoNoneGenerator; -import org.stellar.anchor.platform.service.Sep24DepositInfoSelfGenerator; -import org.stellar.anchor.platform.service.TransactionService; +import org.stellar.anchor.platform.service.*; import org.stellar.anchor.sep24.Sep24DepositInfoGenerator; import org.stellar.anchor.sep24.Sep24TransactionStore; import org.stellar.anchor.sep31.Sep31TransactionStore; import org.stellar.anchor.sep38.Sep38QuoteStore; +import org.stellar.anchor.sep6.Sep6DepositInfoGenerator; import org.stellar.anchor.sep6.Sep6TransactionStore; @Configuration @@ -83,6 +82,25 @@ Sep24DepositInfoGenerator sep24DepositInfoGenerator( } } + @Bean + Sep6DepositInfoGenerator sep6DepositInfoGenerator( + Sep6Config sep6Config, AssetService assetService, Optional custodyApiClient) + throws InvalidConfigException { + switch (sep6Config.getDepositInfoGeneratorType()) { + case SELF: + return new Sep6DepositInfoSelfGenerator(assetService); + case CUSTODY: + return new Sep6DepositInfoCustodyGenerator( + custodyApiClient.orElseThrow( + () -> + new InvalidConfigException("Integration with custody service is not enabled"))); + case NONE: + return new Sep6DepositInfoNoneGenerator(); + default: + throw new RuntimeException("Not supported"); + } + } + @Bean TransactionService transactionService( Sep6TransactionStore txn6Store, @@ -91,6 +109,7 @@ TransactionService transactionService( Sep38QuoteStore quoteStore, AssetService assetService, EventService eventService, + Sep6DepositInfoGenerator sep6DepositInfoGenerator, Sep24DepositInfoGenerator sep24DepositInfoGenerator, CustodyService custodyService, CustodyConfig custodyConfig) { @@ -101,6 +120,7 @@ TransactionService transactionService( quoteStore, assetService, eventService, + sep6DepositInfoGenerator, sep24DepositInfoGenerator, custodyService, custodyConfig); diff --git a/platform/src/main/java/org/stellar/anchor/platform/component/platform/RpcActionBeans.java b/platform/src/main/java/org/stellar/anchor/platform/component/platform/RpcActionBeans.java index 5e1d6ea0d9..2d2b632be2 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/component/platform/RpcActionBeans.java +++ b/platform/src/main/java/org/stellar/anchor/platform/component/platform/RpcActionBeans.java @@ -39,6 +39,7 @@ import org.stellar.anchor.sep24.Sep24DepositInfoGenerator; import org.stellar.anchor.sep24.Sep24TransactionStore; import org.stellar.anchor.sep31.Sep31TransactionStore; +import org.stellar.anchor.sep6.Sep6DepositInfoGenerator; import org.stellar.anchor.sep6.Sep6TransactionStore; @Configuration @@ -447,6 +448,7 @@ RequestOnchainFundsHandler requestOnchainFundsHandler( AssetService assetService, CustodyService custodyService, CustodyConfig custodyConfig, + Sep6DepositInfoGenerator sep6DepositInfoGenerator, Sep24DepositInfoGenerator sep24DepositInfoGenerator, EventService eventService, MetricsService metricsService) { @@ -458,6 +460,7 @@ RequestOnchainFundsHandler requestOnchainFundsHandler( assetService, custodyService, custodyConfig, + sep6DepositInfoGenerator, sep24DepositInfoGenerator, eventService, metricsService); diff --git a/platform/src/main/java/org/stellar/anchor/platform/component/sep/SepBeans.java b/platform/src/main/java/org/stellar/anchor/platform/component/sep/SepBeans.java index dbc77940be..9a1ea1ba9d 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/component/sep/SepBeans.java +++ b/platform/src/main/java/org/stellar/anchor/platform/component/sep/SepBeans.java @@ -58,12 +58,6 @@ Sep1Config sep1Config() { return new PropertySep1Config(); } - @Bean - @ConfigurationProperties(prefix = "sep6") - Sep6Config sep6Config() { - return new PropertySep6Config(); - } - @Bean @ConfigurationProperties(prefix = "sep10") Sep10Config sep10Config( diff --git a/platform/src/main/java/org/stellar/anchor/platform/component/share/UtilityBeans.java b/platform/src/main/java/org/stellar/anchor/platform/component/share/UtilityBeans.java index b55e5850cc..16696fb538 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/component/share/UtilityBeans.java +++ b/platform/src/main/java/org/stellar/anchor/platform/component/share/UtilityBeans.java @@ -9,16 +9,10 @@ import org.springframework.context.annotation.DependsOn; import org.stellar.anchor.api.exception.NotSupportedException; import org.stellar.anchor.auth.JwtService; -import org.stellar.anchor.config.AppConfig; -import org.stellar.anchor.config.CustodyConfig; -import org.stellar.anchor.config.CustodySecretConfig; -import org.stellar.anchor.config.SecretConfig; +import org.stellar.anchor.config.*; import org.stellar.anchor.healthcheck.HealthCheckable; import org.stellar.anchor.horizon.Horizon; -import org.stellar.anchor.platform.config.PropertyAppConfig; -import org.stellar.anchor.platform.config.PropertyClientsConfig; -import org.stellar.anchor.platform.config.PropertySecretConfig; -import org.stellar.anchor.platform.config.PropertySep24Config; +import org.stellar.anchor.platform.config.*; import org.stellar.anchor.platform.service.HealthCheckService; import org.stellar.anchor.platform.service.SimpleMoreInfoUrlConstructor; import org.stellar.anchor.platform.validator.RequestValidator; @@ -57,6 +51,12 @@ PropertySep24Config sep24Config(SecretConfig secretConfig, CustodyConfig custody return new PropertySep24Config(secretConfig, custodyConfig); } + @Bean + @ConfigurationProperties(prefix = "sep6") + PropertySep6Config sep6Config(CustodyConfig custodyConfig) { + return new PropertySep6Config(custodyConfig); + } + /********************************** * Secret configurations */ diff --git a/platform/src/main/java/org/stellar/anchor/platform/config/PropertySep6Config.java b/platform/src/main/java/org/stellar/anchor/platform/config/PropertySep6Config.java index d195969cb8..ea2db86a95 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/config/PropertySep6Config.java +++ b/platform/src/main/java/org/stellar/anchor/platform/config/PropertySep6Config.java @@ -1,17 +1,23 @@ package org.stellar.anchor.platform.config; +import static org.stellar.anchor.config.Sep6Config.DepositInfoGeneratorType.CUSTODY; + import lombok.*; import org.springframework.validation.Errors; import org.springframework.validation.Validator; +import org.stellar.anchor.config.CustodyConfig; import org.stellar.anchor.config.Sep6Config; -@Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor +@Data public class PropertySep6Config implements Sep6Config, Validator { boolean enabled; Features features; + DepositInfoGeneratorType depositInfoGeneratorType; + CustodyConfig custodyConfig; + + public PropertySep6Config(CustodyConfig custodyConfig) { + this.custodyConfig = custodyConfig; + } @Override public boolean supports(@NonNull Class clazz) { @@ -34,6 +40,24 @@ public void validate(@NonNull Object target, @NonNull Errors errors) { "sep6-features-claimable-balances-invalid", "sep6.features.claimable_balances: claimable balances are not supported"); } + validateDepositInfoGeneratorType(errors); + } + } + + void validateDepositInfoGeneratorType(Errors errors) { + if (custodyConfig.isCustodyIntegrationEnabled() && CUSTODY != depositInfoGeneratorType) { + errors.rejectValue( + "depositInfoGeneratorType", + "sep6-deposit-info-generator-type", + String.format( + "[%s] deposit info generator type is not supported when custody integration is enabled", + depositInfoGeneratorType.toString().toLowerCase())); + } else if (!custodyConfig.isCustodyIntegrationEnabled() + && CUSTODY == depositInfoGeneratorType) { + errors.rejectValue( + "depositInfoGeneratorType", + "sep6-deposit-info-generator-type", + "[custody] deposit info generator type is not supported when custody integration is disabled"); } } } diff --git a/platform/src/main/java/org/stellar/anchor/platform/rpc/DoStellarPaymentHandler.java b/platform/src/main/java/org/stellar/anchor/platform/rpc/DoStellarPaymentHandler.java index 0ebf778a5f..cfcbabf348 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/rpc/DoStellarPaymentHandler.java +++ b/platform/src/main/java/org/stellar/anchor/platform/rpc/DoStellarPaymentHandler.java @@ -2,12 +2,15 @@ import static java.util.Collections.emptySet; import static org.stellar.anchor.api.platform.PlatformTransactionData.Kind.DEPOSIT; +import static org.stellar.anchor.api.platform.PlatformTransactionData.Kind.DEPOSIT_EXCHANGE; import static org.stellar.anchor.api.platform.PlatformTransactionData.Sep.SEP_24; +import static org.stellar.anchor.api.platform.PlatformTransactionData.Sep.SEP_6; import static org.stellar.anchor.api.rpc.method.RpcMethod.DO_STELLAR_PAYMENT; import static org.stellar.anchor.api.sep.SepTransactionStatus.PENDING_ANCHOR; import static org.stellar.anchor.api.sep.SepTransactionStatus.PENDING_STELLAR; import static org.stellar.anchor.api.sep.SepTransactionStatus.PENDING_TRUST; +import com.google.common.collect.ImmutableSet; import java.io.IOException; import java.time.Instant; import java.util.Set; @@ -26,10 +29,7 @@ import org.stellar.anchor.event.EventService; import org.stellar.anchor.horizon.Horizon; import org.stellar.anchor.metrics.MetricsService; -import org.stellar.anchor.platform.data.JdbcSep24Transaction; -import org.stellar.anchor.platform.data.JdbcSepTransaction; -import org.stellar.anchor.platform.data.JdbcTransactionPendingTrust; -import org.stellar.anchor.platform.data.JdbcTransactionPendingTrustRepo; +import org.stellar.anchor.platform.data.*; import org.stellar.anchor.platform.validator.RequestValidator; import org.stellar.anchor.sep24.Sep24TransactionStore; import org.stellar.anchor.sep31.Sep31TransactionStore; @@ -76,7 +76,7 @@ protected void validate(JdbcSepTransaction txn, DoStellarPaymentRequest request) if (!custodyConfig.isCustodyIntegrationEnabled()) { throw new InvalidRequestException( - String.format("RPC method[%s] requires disabled custody integration", getRpcMethod())); + String.format("RPC method[%s] requires enabled custody integration", getRpcMethod())); } } @@ -88,14 +88,25 @@ public RpcMethod getRpcMethod() { @Override protected SepTransactionStatus getNextStatus( JdbcSepTransaction txn, DoStellarPaymentRequest request) { - JdbcSep24Transaction txn24 = (JdbcSep24Transaction) txn; - boolean trustlineConfigured; + boolean trustlineConfigured = false; try { - trustlineConfigured = - horizon.isTrustlineConfigured(txn24.getToAccount(), txn24.getAmountOutAsset()); + switch (Sep.from(txn.getProtocol())) { + case SEP_6: + JdbcSep6Transaction txn6 = (JdbcSep6Transaction) txn; + trustlineConfigured = + horizon.isTrustlineConfigured(txn6.getToAccount(), txn6.getAmountOutAsset()); + break; + case SEP_24: + JdbcSep24Transaction txn24 = (JdbcSep24Transaction) txn; + trustlineConfigured = + horizon.isTrustlineConfigured(txn24.getToAccount(), txn24.getAmountOutAsset()); + break; + default: + break; + } } catch (IOException ex) { - trustlineConfigured = false; + // assume trustline is not configured } if (trustlineConfigured) { @@ -107,13 +118,25 @@ protected SepTransactionStatus getNextStatus( @Override protected Set getSupportedStatuses(JdbcSepTransaction txn) { - if (SEP_24 == Sep.from(txn.getProtocol())) { - JdbcSep24Transaction txn24 = (JdbcSep24Transaction) txn; - if (DEPOSIT == Kind.from(txn24.getKind())) { - if (areFundsReceived(txn24)) { - return Set.of(PENDING_ANCHOR); + switch (Sep.from(txn.getProtocol())) { + case SEP_6: + JdbcSep6Transaction txn6 = (JdbcSep6Transaction) txn; + if (ImmutableSet.of(DEPOSIT, DEPOSIT_EXCHANGE).contains(Kind.from(txn6.getKind()))) { + if (areFundsReceived(txn6)) { + return Set.of(PENDING_ANCHOR); + } } - } + break; + case SEP_24: + JdbcSep24Transaction txn24 = (JdbcSep24Transaction) txn; + if (DEPOSIT == Kind.from(txn24.getKind())) { + if (areFundsReceived(txn24)) { + return Set.of(PENDING_ANCHOR); + } + } + break; + default: + break; } return emptySet(); } @@ -121,26 +144,54 @@ protected Set getSupportedStatuses(JdbcSepTransaction txn) @Override protected void updateTransactionWithRpcRequest( JdbcSepTransaction txn, DoStellarPaymentRequest request) throws AnchorException { - JdbcSep24Transaction txn24 = (JdbcSep24Transaction) txn; - boolean trustlineConfigured; - try { - trustlineConfigured = - horizon.isTrustlineConfigured(txn24.getToAccount(), txn24.getAmountOutAsset()); - } catch (IOException ex) { - trustlineConfigured = false; - } + switch (Sep.from(txn.getProtocol())) { + case SEP_6: + JdbcSep6Transaction txn6 = (JdbcSep6Transaction) txn; - if (trustlineConfigured) { - custodyService.createTransactionPayment(txn24.getId(), null); - } else { - transactionPendingTrustRepo.save( - JdbcTransactionPendingTrust.builder() - .id(txn24.getId()) - .createdAt(Instant.now()) - .asset(txn24.getAmountOutAsset()) - .account(txn24.getToAccount()) - .build()); + try { + trustlineConfigured = + horizon.isTrustlineConfigured(txn6.getToAccount(), txn6.getAmountOutAsset()); + } catch (IOException ex) { + trustlineConfigured = false; + } + + if (trustlineConfigured) { + custodyService.createTransactionPayment(txn6.getId(), null); + } else { + transactionPendingTrustRepo.save( + JdbcTransactionPendingTrust.builder() + .id(txn6.getId()) + .createdAt(Instant.now()) + .asset(txn6.getAmountOutAsset()) + .account(txn6.getToAccount()) + .build()); + } + break; + case SEP_24: + JdbcSep24Transaction txn24 = (JdbcSep24Transaction) txn; + + try { + trustlineConfigured = + horizon.isTrustlineConfigured(txn24.getToAccount(), txn24.getAmountOutAsset()); + } catch (IOException ex) { + trustlineConfigured = false; + } + + if (trustlineConfigured) { + custodyService.createTransactionPayment(txn24.getId(), null); + } else { + transactionPendingTrustRepo.save( + JdbcTransactionPendingTrust.builder() + .id(txn24.getId()) + .createdAt(Instant.now()) + .asset(txn24.getAmountOutAsset()) + .account(txn24.getToAccount()) + .build()); + } + break; + default: + break; } } } diff --git a/platform/src/main/java/org/stellar/anchor/platform/rpc/DoStellarRefundHandler.java b/platform/src/main/java/org/stellar/anchor/platform/rpc/DoStellarRefundHandler.java index bb78ff52bb..6b019b6020 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/rpc/DoStellarRefundHandler.java +++ b/platform/src/main/java/org/stellar/anchor/platform/rpc/DoStellarRefundHandler.java @@ -1,8 +1,7 @@ package org.stellar.anchor.platform.rpc; import static java.util.Collections.emptySet; -import static org.stellar.anchor.api.platform.PlatformTransactionData.Kind.RECEIVE; -import static org.stellar.anchor.api.platform.PlatformTransactionData.Kind.WITHDRAWAL; +import static org.stellar.anchor.api.platform.PlatformTransactionData.Kind.*; import static org.stellar.anchor.api.platform.PlatformTransactionData.Sep.SEP_31; import static org.stellar.anchor.api.rpc.method.RpcMethod.DO_STELLAR_REFUND; import static org.stellar.anchor.api.sep.SepTransactionStatus.PENDING_ANCHOR; @@ -12,6 +11,7 @@ import static org.stellar.anchor.util.MathHelper.decimal; import static org.stellar.anchor.util.MathHelper.sum; +import com.google.common.collect.ImmutableSet; import java.math.BigDecimal; import java.util.Set; import org.stellar.anchor.api.exception.AnchorException; @@ -25,6 +25,7 @@ import org.stellar.anchor.api.rpc.method.RpcMethod; import org.stellar.anchor.api.sep.AssetInfo; import org.stellar.anchor.api.sep.SepTransactionStatus; +import org.stellar.anchor.api.shared.Refunds; import org.stellar.anchor.asset.AssetService; import org.stellar.anchor.config.CustodyConfig; import org.stellar.anchor.custody.CustodyService; @@ -32,6 +33,7 @@ import org.stellar.anchor.metrics.MetricsService; import org.stellar.anchor.platform.data.JdbcSep24Transaction; import org.stellar.anchor.platform.data.JdbcSep31Transaction; +import org.stellar.anchor.platform.data.JdbcSep6Transaction; import org.stellar.anchor.platform.data.JdbcSepTransaction; import org.stellar.anchor.platform.utils.AssetValidationUtils; import org.stellar.anchor.platform.validator.RequestValidator; @@ -120,6 +122,16 @@ protected SepTransactionStatus getNextStatus( BigDecimal totalRefunded; switch (Sep.from(txn.getProtocol())) { + case SEP_6: + JdbcSep6Transaction txn6 = (JdbcSep6Transaction) txn; + Refunds refunds = txn6.getRefunds(); + if (refunds == null || refunds.getPayments() == null) { + totalRefunded = sum(assetInfo, amount, amountFee); + } else { + totalRefunded = + sum(assetInfo, refunds.getAmountRefunded().getAmount(), amount, amountFee); + } + break; case SEP_24: JdbcSep24Transaction txn24 = (JdbcSep24Transaction) txn; Sep24Refunds sep24Refunds = txn24.getRefunds(); @@ -160,6 +172,14 @@ protected SepTransactionStatus getNextStatus( @Override protected Set getSupportedStatuses(JdbcSepTransaction txn) { switch (Sep.from(txn.getProtocol())) { + case SEP_6: + JdbcSep6Transaction txn6 = (JdbcSep6Transaction) txn; + if (ImmutableSet.of(WITHDRAWAL, WITHDRAWAL_EXCHANGE).contains(Kind.from(txn6.getKind()))) { + if (areFundsReceived(txn6)) { + return Set.of(PENDING_ANCHOR); + } + } + break; case SEP_24: JdbcSep24Transaction txn24 = (JdbcSep24Transaction) txn; if (WITHDRAWAL == Kind.from(txn24.getKind())) { @@ -179,6 +199,11 @@ protected Set getSupportedStatuses(JdbcSepTransaction txn) protected void updateTransactionWithRpcRequest( JdbcSepTransaction txn, DoStellarRefundRequest request) throws AnchorException { switch (Sep.from(txn.getProtocol())) { + case SEP_6: + JdbcSep6Transaction txn6 = (JdbcSep6Transaction) txn; + custodyService.createTransactionRefund( + request, txn6.getRefundMemo(), txn6.getRefundMemoType()); + break; case SEP_24: JdbcSep24Transaction txn24 = (JdbcSep24Transaction) txn; custodyService.createTransactionRefund( diff --git a/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyOffchainFundsReceivedHandler.java b/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyOffchainFundsReceivedHandler.java index 5da464b9fa..205b1ae27f 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyOffchainFundsReceivedHandler.java +++ b/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyOffchainFundsReceivedHandler.java @@ -177,12 +177,21 @@ protected void updateTransactionWithRpcRequest( txn.setAmountFee(request.getAmountFee().getAmount()); } - // TODO: add support for SEP-6 - if (SEP_24 == Sep.from(txn.getProtocol())) { - JdbcSep24Transaction txn24 = (JdbcSep24Transaction) txn; - if (custodyConfig.isCustodyIntegrationEnabled()) { - custodyService.createTransaction(txn24); - } + switch (Sep.from(txn.getProtocol())) { + case SEP_6: + JdbcSep6Transaction txn6 = (JdbcSep6Transaction) txn; + if (custodyConfig.isCustodyIntegrationEnabled()) { + custodyService.createTransaction(txn6); + } + break; + case SEP_24: + JdbcSep24Transaction txn24 = (JdbcSep24Transaction) txn; + if (custodyConfig.isCustodyIntegrationEnabled()) { + custodyService.createTransaction(txn24); + } + break; + default: + break; } } } diff --git a/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyTrustSetHandler.java b/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyTrustSetHandler.java index 1291bae071..836b9aae55 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyTrustSetHandler.java +++ b/platform/src/main/java/org/stellar/anchor/platform/rpc/NotifyTrustSetHandler.java @@ -2,12 +2,14 @@ import static java.util.Collections.emptySet; import static org.stellar.anchor.api.platform.PlatformTransactionData.Kind.DEPOSIT; +import static org.stellar.anchor.api.platform.PlatformTransactionData.Kind.DEPOSIT_EXCHANGE; import static org.stellar.anchor.api.platform.PlatformTransactionData.Sep.SEP_24; import static org.stellar.anchor.api.rpc.method.RpcMethod.NOTIFY_TRUST_SET; import static org.stellar.anchor.api.sep.SepTransactionStatus.PENDING_ANCHOR; import static org.stellar.anchor.api.sep.SepTransactionStatus.PENDING_STELLAR; import static org.stellar.anchor.api.sep.SepTransactionStatus.PENDING_TRUST; +import com.google.common.collect.ImmutableSet; import java.util.Set; import org.stellar.anchor.api.exception.AnchorException; import org.stellar.anchor.api.exception.BadRequestException; @@ -24,6 +26,7 @@ import org.stellar.anchor.metrics.MetricsService; import org.stellar.anchor.platform.config.PropertyCustodyConfig; import org.stellar.anchor.platform.data.JdbcSep24Transaction; +import org.stellar.anchor.platform.data.JdbcSep6Transaction; import org.stellar.anchor.platform.data.JdbcSepTransaction; import org.stellar.anchor.platform.validator.RequestValidator; import org.stellar.anchor.sep24.Sep24TransactionStore; @@ -81,11 +84,21 @@ protected SepTransactionStatus getNextStatus( @Override protected Set getSupportedStatuses(JdbcSepTransaction txn) { - if (SEP_24 == Sep.from(txn.getProtocol())) { - JdbcSep24Transaction txn24 = (JdbcSep24Transaction) txn; - if (DEPOSIT == Kind.from(txn24.getKind())) { - return Set.of(PENDING_TRUST); - } + switch (Sep.from(txn.getProtocol())) { + case SEP_6: + JdbcSep6Transaction txn6 = (JdbcSep6Transaction) txn; + if (ImmutableSet.of(DEPOSIT, DEPOSIT_EXCHANGE).contains(Kind.from(txn6.getKind()))) { + return Set.of(PENDING_TRUST); + } + break; + case SEP_24: + JdbcSep24Transaction txn24 = (JdbcSep24Transaction) txn; + if (DEPOSIT == Kind.from(txn24.getKind())) { + return Set.of(PENDING_TRUST); + } + break; + default: + break; } return emptySet(); } diff --git a/platform/src/main/java/org/stellar/anchor/platform/rpc/RequestOnchainFundsHandler.java b/platform/src/main/java/org/stellar/anchor/platform/rpc/RequestOnchainFundsHandler.java index a71a1c2652..e43ea94a6b 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/rpc/RequestOnchainFundsHandler.java +++ b/platform/src/main/java/org/stellar/anchor/platform/rpc/RequestOnchainFundsHandler.java @@ -35,11 +35,13 @@ import org.stellar.anchor.platform.data.JdbcSep6Transaction; import org.stellar.anchor.platform.data.JdbcSepTransaction; import org.stellar.anchor.platform.service.Sep24DepositInfoNoneGenerator; +import org.stellar.anchor.platform.service.Sep6DepositInfoNoneGenerator; import org.stellar.anchor.platform.utils.AssetValidationUtils; import org.stellar.anchor.platform.validator.RequestValidator; import org.stellar.anchor.sep24.Sep24DepositInfoGenerator; import org.stellar.anchor.sep24.Sep24TransactionStore; import org.stellar.anchor.sep31.Sep31TransactionStore; +import org.stellar.anchor.sep6.Sep6DepositInfoGenerator; import org.stellar.anchor.sep6.Sep6TransactionStore; import org.stellar.anchor.util.CustodyUtils; import org.stellar.sdk.Memo; @@ -48,6 +50,7 @@ public class RequestOnchainFundsHandler extends RpcMethodHandler custodyApiClient) { this.custodyApiClient = custodyApiClient; } + @Override + public void createTransaction(Sep6Transaction txn) throws AnchorException { + create(toCustodyTransaction(txn)); + } + @Override public void createTransaction(Sep24Transaction txn) throws AnchorException { create(toCustodyTransaction(txn)); diff --git a/platform/src/main/java/org/stellar/anchor/platform/service/Sep6DepositInfoCustodyGenerator.java b/platform/src/main/java/org/stellar/anchor/platform/service/Sep6DepositInfoCustodyGenerator.java new file mode 100644 index 0000000000..37d852b93f --- /dev/null +++ b/platform/src/main/java/org/stellar/anchor/platform/service/Sep6DepositInfoCustodyGenerator.java @@ -0,0 +1,23 @@ +package org.stellar.anchor.platform.service; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.stellar.anchor.api.custody.GenerateDepositAddressResponse; +import org.stellar.anchor.api.exception.AnchorException; +import org.stellar.anchor.api.shared.SepDepositInfo; +import org.stellar.anchor.platform.apiclient.CustodyApiClient; +import org.stellar.anchor.sep6.Sep6DepositInfoGenerator; +import org.stellar.anchor.sep6.Sep6Transaction; + +@RequiredArgsConstructor +public class Sep6DepositInfoCustodyGenerator implements Sep6DepositInfoGenerator { + @NonNull private final CustodyApiClient custodyApiClient; + + @Override + public SepDepositInfo generate(Sep6Transaction txn) throws AnchorException { + GenerateDepositAddressResponse depositAddress = + custodyApiClient.generateDepositAddress(txn.getAmountInAsset()); + return new SepDepositInfo( + depositAddress.getAddress(), depositAddress.getMemo(), depositAddress.getMemoType()); + } +} diff --git a/platform/src/main/java/org/stellar/anchor/platform/service/Sep6DepositInfoNoneGenerator.java b/platform/src/main/java/org/stellar/anchor/platform/service/Sep6DepositInfoNoneGenerator.java new file mode 100644 index 0000000000..69090796cb --- /dev/null +++ b/platform/src/main/java/org/stellar/anchor/platform/service/Sep6DepositInfoNoneGenerator.java @@ -0,0 +1,14 @@ +package org.stellar.anchor.platform.service; + +import org.stellar.anchor.api.exception.AnchorException; +import org.stellar.anchor.api.exception.BadRequestException; +import org.stellar.anchor.api.shared.SepDepositInfo; +import org.stellar.anchor.sep6.Sep6DepositInfoGenerator; +import org.stellar.anchor.sep6.Sep6Transaction; + +public class Sep6DepositInfoNoneGenerator implements Sep6DepositInfoGenerator { + @Override + public SepDepositInfo generate(Sep6Transaction txn) throws AnchorException { + throw new BadRequestException("SEP-6 deposit info generation is disabled"); + } +} diff --git a/platform/src/main/java/org/stellar/anchor/platform/service/Sep6DepositInfoSelfGenerator.java b/platform/src/main/java/org/stellar/anchor/platform/service/Sep6DepositInfoSelfGenerator.java new file mode 100644 index 0000000000..7aa6c9b1bd --- /dev/null +++ b/platform/src/main/java/org/stellar/anchor/platform/service/Sep6DepositInfoSelfGenerator.java @@ -0,0 +1,32 @@ +package org.stellar.anchor.platform.service; + +import static org.stellar.anchor.util.MemoHelper.memoTypeAsString; +import static org.stellar.sdk.xdr.MemoType.MEMO_HASH; + +import java.util.Base64; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.stellar.anchor.api.exception.AnchorException; +import org.stellar.anchor.api.sep.AssetInfo; +import org.stellar.anchor.api.shared.SepDepositInfo; +import org.stellar.anchor.asset.AssetService; +import org.stellar.anchor.sep6.Sep6DepositInfoGenerator; +import org.stellar.anchor.sep6.Sep6Transaction; + +@RequiredArgsConstructor +public class Sep6DepositInfoSelfGenerator implements Sep6DepositInfoGenerator { + @NonNull private final AssetService assetService; + + @Override + public SepDepositInfo generate(Sep6Transaction txn) throws AnchorException { + AssetInfo assetInfo = + assetService.getAsset(txn.getRequestAssetCode(), txn.getRequestAssetIssuer()); + + String memo = StringUtils.truncate(txn.getId(), 32); + memo = StringUtils.leftPad(memo, 32, '0'); + memo = new String(Base64.getEncoder().encode(memo.getBytes())); + return new SepDepositInfo( + assetInfo.getDistributionAccount(), memo, memoTypeAsString(MEMO_HASH)); + } +} diff --git a/platform/src/main/java/org/stellar/anchor/platform/service/TransactionService.java b/platform/src/main/java/org/stellar/anchor/platform/service/TransactionService.java index 2e8b0d1ffb..c8fafecd94 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/service/TransactionService.java +++ b/platform/src/main/java/org/stellar/anchor/platform/service/TransactionService.java @@ -15,6 +15,7 @@ import static org.stellar.anchor.util.MemoHelper.makeMemo; import static org.stellar.anchor.util.MetricConstants.*; +import com.google.common.collect.ImmutableSet; import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.Metrics; import java.time.Instant; @@ -59,6 +60,7 @@ import org.stellar.anchor.sep31.Sep31TransactionStore; import org.stellar.anchor.sep38.Sep38Quote; import org.stellar.anchor.sep38.Sep38QuoteStore; +import org.stellar.anchor.sep6.Sep6DepositInfoGenerator; import org.stellar.anchor.sep6.Sep6Transaction; import org.stellar.anchor.sep6.Sep6TransactionStore; import org.stellar.anchor.util.*; @@ -78,6 +80,8 @@ public class TransactionService { private final List assets; private final Session eventSession; private final AssetService assetService; + + private final Sep6DepositInfoGenerator sep6DepositInfoGenerator; private final Sep24DepositInfoGenerator sep24DepositInfoGenerator; private final CustodyService custodyService; private final CustodyConfig custodyConfig; @@ -118,6 +122,7 @@ public TransactionService( Sep38QuoteStore quoteStore, AssetService assetService, EventService eventService, + Sep6DepositInfoGenerator sep6DepositInfoGenerator, Sep24DepositInfoGenerator sep24DepositInfoGenerator, CustodyService custodyService, CustodyConfig custodyConfig) { @@ -128,6 +133,7 @@ public TransactionService( this.assets = assetService.listAllAssets(); this.eventSession = eventService.createSession(this.getClass().getName(), TRANSACTION); this.assetService = assetService; + this.sep6DepositInfoGenerator = sep6DepositInfoGenerator; this.sep24DepositInfoGenerator = sep24DepositInfoGenerator; this.custodyService = custodyService; this.custodyConfig = custodyConfig; @@ -258,6 +264,30 @@ private GetTransactionResponse patchTransaction(PatchTransactionRequest patch) JdbcSep6Transaction sep6Transaction = (JdbcSep6Transaction) txn; Log.infoF( "Updating SEP-6 transaction: {}", GsonUtils.getInstance().toJson(sep6Transaction)); + + boolean shouldCreateDepositTxn = + ImmutableSet.of(Kind.DEPOSIT, Kind.DEPOSIT_EXCHANGE) + .contains(Kind.from(sep6Transaction.getKind())) + // TODO: check if this is correct + && txn.getStatus().equals(PENDING_ANCHOR.toString()); + boolean shouldCreateWithdrawTxn = + ImmutableSet.of(Kind.WITHDRAWAL, Kind.WITHDRAWAL_EXCHANGE) + .contains(Kind.from(sep6Transaction.getKind())) + && txn.getStatus().equals(PENDING_USR_TRANSFER_START.toString()); + + if (sep6Transaction.getMemo() == null && shouldCreateWithdrawTxn) { + SepDepositInfo sep6DepositInfo = sep6DepositInfoGenerator.generate(sep6Transaction); + sep6Transaction.setWithdrawAnchorAccount(sep6DepositInfo.getStellarAddress()); + sep6Transaction.setMemo(sep6DepositInfo.getMemo()); + sep6Transaction.setMemoType(sep6DepositInfo.getMemoType()); + } + + if (custodyConfig.isCustodyIntegrationEnabled() + && !lastStatus.equals(sep6Transaction.getStatus()) + && (shouldCreateDepositTxn || shouldCreateWithdrawTxn)) { + custodyService.createTransaction(sep6Transaction); + } + txn6Store.save(sep6Transaction); eventSession.publish( AnchorEvent.builder() @@ -267,6 +297,7 @@ private GetTransactionResponse patchTransaction(PatchTransactionRequest patch) .transaction( TransactionHelper.toGetTransactionResponse(sep6Transaction, assetService)) .build()); + patchSep6TransactionCounter.increment(); break; case "24": JdbcSep24Transaction sep24Txn = (JdbcSep24Transaction) txn; diff --git a/platform/src/main/resources/config/anchor-config-default-values.yaml b/platform/src/main/resources/config/anchor-config-default-values.yaml index 91e25f78aa..e54b31a581 100644 --- a/platform/src/main/resources/config/anchor-config-default-values.yaml +++ b/platform/src/main/resources/config/anchor-config-default-values.yaml @@ -301,6 +301,16 @@ sep6: features: account_creation: false claimable_balances: false + ## @param: deposit_info_generator_type + ## @default: self + ## Used to choose how the SEP-6 deposit information will be generated, which includes the + ## deposit address, memo and memo type. + ## @supported_values: + ## self: the memo and memo type are generated in the local code, and the distribution account is used for the deposit address. + ## custody: the memo and memo type are generated through Custody API, for example Fireblocks, as well as the deposit address. + ## none: deposit address, memo and memo type should be provided by the business in PATCH/RPC request. + # + deposit_info_generator_type: self ###################### # SEP-10 Configuration diff --git a/platform/src/main/resources/config/anchor-config-schema-v1.yaml b/platform/src/main/resources/config/anchor-config-schema-v1.yaml index 136de05c91..950763cda0 100644 --- a/platform/src/main/resources/config/anchor-config-schema-v1.yaml +++ b/platform/src/main/resources/config/anchor-config-schema-v1.yaml @@ -95,6 +95,7 @@ sep31.enabled: sep31.payment_type: sep38.enabled: sep38.sep10_enforced: +sep6.deposit_info_generator_type: sep6.enabled: sep6.features.account_creation: sep6.features.claimable_balances: diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/config/Sep6ConfigTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/config/Sep6ConfigTest.kt index f046eb8441..cddcab78eb 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/config/Sep6ConfigTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/config/Sep6ConfigTest.kt @@ -1,21 +1,33 @@ package org.stellar.anchor.platform.config +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource import org.springframework.validation.BindException import org.springframework.validation.Errors +import org.stellar.anchor.config.CustodyConfig import org.stellar.anchor.config.Sep6Config class Sep6ConfigTest { + @MockK(relaxed = true) lateinit var custodyConfig: CustodyConfig lateinit var config: PropertySep6Config lateinit var errors: Errors @BeforeEach fun setUp() { - config = PropertySep6Config() - config.enabled = true - config.features = Sep6Config.Features(false, false) + MockKAnnotations.init(this, relaxUnitFun = true) + every { custodyConfig.isCustodyIntegrationEnabled } returns true + config = + PropertySep6Config(custodyConfig).apply { + enabled = true + features = Sep6Config.Features(false, false) + depositInfoGeneratorType = Sep6Config.DepositInfoGeneratorType.CUSTODY + } errors = BindException(config, "config") } @@ -58,4 +70,22 @@ class Sep6ConfigTest { config.validate(config, errors) Assertions.assertEquals("sep6-features-claimable-balances-invalid", errors.allErrors[0].code) } + + @CsvSource(value = ["NONE", "SELF"]) + @ParameterizedTest + fun `test validation rejecting custody enabled and non-custodial deposit info generator`( + type: String + ) { + config.depositInfoGeneratorType = Sep6Config.DepositInfoGeneratorType.valueOf(type) + config.validate(config, errors) + Assertions.assertEquals("sep6-deposit-info-generator-type", errors.allErrors[0].code) + } + + @Test + fun `test validation rejecting custody disabled and custodial deposit generator`() { + every { custodyConfig.isCustodyIntegrationEnabled } returns false + config.depositInfoGeneratorType = Sep6Config.DepositInfoGeneratorType.CUSTODY + config.validate(config, errors) + Assertions.assertEquals("sep6-deposit-info-generator-type", errors.allErrors[0].code) + } } diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/rpc/DoStellarPaymentHandlerTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/rpc/DoStellarPaymentHandlerTest.kt index dd395a1835..d5551214cf 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/rpc/DoStellarPaymentHandlerTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/rpc/DoStellarPaymentHandlerTest.kt @@ -9,6 +9,8 @@ import kotlin.test.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource import org.skyscreamer.jsonassert.JSONAssert import org.skyscreamer.jsonassert.JSONCompareMode import org.stellar.anchor.api.event.AnchorEvent @@ -16,12 +18,14 @@ import org.stellar.anchor.api.event.AnchorEvent.Type.TRANSACTION_STATUS_CHANGED import org.stellar.anchor.api.exception.rpc.InvalidParamsException import org.stellar.anchor.api.exception.rpc.InvalidRequestException import org.stellar.anchor.api.platform.GetTransactionResponse +import org.stellar.anchor.api.platform.PlatformTransactionData import org.stellar.anchor.api.platform.PlatformTransactionData.Kind.DEPOSIT -import org.stellar.anchor.api.platform.PlatformTransactionData.Sep.SEP_24 -import org.stellar.anchor.api.platform.PlatformTransactionData.Sep.SEP_38 +import org.stellar.anchor.api.platform.PlatformTransactionData.Sep.* import org.stellar.anchor.api.rpc.method.DoStellarPaymentRequest import org.stellar.anchor.api.sep.SepTransactionStatus.* import org.stellar.anchor.api.shared.Amount +import org.stellar.anchor.api.shared.Customers +import org.stellar.anchor.api.shared.StellarId import org.stellar.anchor.asset.AssetService import org.stellar.anchor.config.CustodyConfig import org.stellar.anchor.custody.CustodyService @@ -31,6 +35,7 @@ import org.stellar.anchor.event.EventService.Session import org.stellar.anchor.horizon.Horizon import org.stellar.anchor.metrics.MetricsService import org.stellar.anchor.platform.data.JdbcSep24Transaction +import org.stellar.anchor.platform.data.JdbcSep6Transaction import org.stellar.anchor.platform.data.JdbcTransactionPendingTrust import org.stellar.anchor.platform.data.JdbcTransactionPendingTrustRepo import org.stellar.anchor.platform.service.AnchorMetrics.PLATFORM_RPC_TRANSACTION @@ -108,6 +113,7 @@ class DoStellarPaymentHandlerTest { txn24.transferReceivedAt = Instant.now() val spyTxn24 = spyk(txn24) + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns spyTxn24 every { txn31Store.findByTransactionId(any()) } returns null every { spyTxn24.protocol } returns SEP_38.sep.toString() @@ -119,99 +125,108 @@ class DoStellarPaymentHandlerTest { ex.message ) + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn24Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 0) { sepTransactionCounter.increment() } } @Test - fun test_handle_unsupportedStatus() { + fun test_handle_invalidRequest() { val request = DoStellarPaymentRequest.builder().transactionId(TX_ID).build() val txn24 = JdbcSep24Transaction() - txn24.status = PENDING_TRUST.toString() + txn24.status = PENDING_ANCHOR.toString() txn24.kind = DEPOSIT.kind txn24.transferReceivedAt = Instant.now() + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns txn24 every { txn31Store.findByTransactionId(any()) } returns null - every { custodyConfig.isCustodyIntegrationEnabled } returns true + every { requestValidator.validate(request) } throws + InvalidParamsException(VALIDATION_ERROR_MESSAGE) - val ex = assertThrows { handler.handle(request) } - assertEquals( - "RPC method[do_stellar_payment] is not supported. Status[pending_trust], kind[deposit], protocol[24], funds received[true]", - ex.message - ) + val ex = assertThrows { handler.handle(request) } + assertEquals(VALIDATION_ERROR_MESSAGE, ex.message?.trimIndent()) + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn24Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 0) { sepTransactionCounter.increment() } } @Test - fun test_handle_handle_custodyIntegrationDisabled() { + fun test_handle_sep24_unsupportedStatus() { val request = DoStellarPaymentRequest.builder().transactionId(TX_ID).build() val txn24 = JdbcSep24Transaction() - txn24.status = PENDING_ANCHOR.toString() + txn24.status = PENDING_TRUST.toString() txn24.kind = DEPOSIT.kind txn24.transferReceivedAt = Instant.now() + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns txn24 every { txn31Store.findByTransactionId(any()) } returns null - every { custodyConfig.isCustodyIntegrationEnabled } returns false + every { custodyConfig.isCustodyIntegrationEnabled } returns true val ex = assertThrows { handler.handle(request) } - assertEquals("RPC method[do_stellar_payment] requires disabled custody integration", ex.message) + assertEquals( + "RPC method[do_stellar_payment] is not supported. Status[pending_trust], kind[deposit], protocol[24], funds received[true]", + ex.message + ) + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn24Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 0) { sepTransactionCounter.increment() } } @Test - fun test_handle_transferNotReceived() { + fun test_handle_sep24_custodyIntegrationDisabled() { val request = DoStellarPaymentRequest.builder().transactionId(TX_ID).build() val txn24 = JdbcSep24Transaction() txn24.status = PENDING_ANCHOR.toString() txn24.kind = DEPOSIT.kind + txn24.transferReceivedAt = Instant.now() + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns txn24 every { txn31Store.findByTransactionId(any()) } returns null - every { custodyConfig.isCustodyIntegrationEnabled } returns true + every { custodyConfig.isCustodyIntegrationEnabled } returns false val ex = assertThrows { handler.handle(request) } - assertEquals( - "RPC method[do_stellar_payment] is not supported. Status[pending_anchor], kind[deposit], protocol[24], funds received[false]", - ex.message - ) + assertEquals("RPC method[do_stellar_payment] requires enabled custody integration", ex.message) + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn24Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 0) { sepTransactionCounter.increment() } } @Test - fun test_handle_invalidRequest() { + fun test_handle_sep24_transferNotReceived() { val request = DoStellarPaymentRequest.builder().transactionId(TX_ID).build() val txn24 = JdbcSep24Transaction() txn24.status = PENDING_ANCHOR.toString() txn24.kind = DEPOSIT.kind - txn24.transferReceivedAt = Instant.now() + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns txn24 every { txn31Store.findByTransactionId(any()) } returns null - every { requestValidator.validate(request) } throws - InvalidParamsException(VALIDATION_ERROR_MESSAGE) + every { custodyConfig.isCustodyIntegrationEnabled } returns true - val ex = assertThrows { handler.handle(request) } - assertEquals(VALIDATION_ERROR_MESSAGE, ex.message?.trimIndent()) + val ex = assertThrows { handler.handle(request) } + assertEquals( + "RPC method[do_stellar_payment] is not supported. Status[pending_anchor], kind[deposit], protocol[24], funds received[false]", + ex.message + ) + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn24Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 0) { sepTransactionCounter.increment() } } @Test - fun test_handle_ok_trustlineConfigured() { + fun test_handle_sep24_ok_trustlineConfigured() { val transferReceivedAt = Instant.now() val request = DoStellarPaymentRequest.builder().transactionId(TX_ID).build() val txn24 = JdbcSep24Transaction() @@ -224,6 +239,7 @@ class DoStellarPaymentHandlerTest { val sep24TxnCapture = slot() val anchorEventCapture = slot() + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns txn24 every { txn31Store.findByTransactionId(any()) } returns null every { txn24Store.save(capture(sep24TxnCapture)) } returns null @@ -237,6 +253,7 @@ class DoStellarPaymentHandlerTest { val response = handler.handle(request) val endDate = Instant.now() + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 0) { transactionPendingTrustRepo.save(any()) } verify(exactly = 1) { custodyService.createTransactionPayment(TX_ID, null) } @@ -291,7 +308,7 @@ class DoStellarPaymentHandlerTest { } @Test - fun test_handle_ok_trustlineNotConfigured() { + fun test_handle_sep24_ok_trustlineNotConfigured() { val transferReceivedAt = Instant.now() val request = DoStellarPaymentRequest.builder().transactionId(TX_ID).build() val txn24 = JdbcSep24Transaction() @@ -305,12 +322,14 @@ class DoStellarPaymentHandlerTest { val txnPendingTrustCapture = slot() val anchorEventCapture = slot() + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns txn24 every { txn31Store.findByTransactionId(any()) } returns null every { txn24Store.save(capture(sep24TxnCapture)) } returns null every { custodyConfig.isCustodyIntegrationEnabled } returns true every { horizon.isTrustlineConfigured(TO_ACCOUNT, AMOUNT_OUT_ASSET) } returns false - every { transactionPendingTrustRepo.save(capture(txnPendingTrustCapture)) } returns null + every { transactionPendingTrustRepo.save(capture(txnPendingTrustCapture)) } returns + JdbcTransactionPendingTrust() every { eventSession.publish(capture(anchorEventCapture)) } just Runs every { metricsService.counter(PLATFORM_RPC_TRANSACTION, "SEP", "sep24") } returns sepTransactionCounter @@ -319,6 +338,7 @@ class DoStellarPaymentHandlerTest { val response = handler.handle(request) val endDate = Instant.now() + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 0) { custodyService.createTransactionPayment(any(), any()) } verify(exactly = 1) { sepTransactionCounter.increment() } @@ -384,4 +404,264 @@ class DoStellarPaymentHandlerTest { assertTrue(txnPendingTrustCapture.captured.createdAt >= startDate) assertTrue(txnPendingTrustCapture.captured.createdAt <= endDate) } + + @CsvSource(value = ["deposit", "deposit-exchange"]) + @ParameterizedTest + fun test_handle_sep6_unsupportedStatus(kind: String) { + val request = DoStellarPaymentRequest.builder().transactionId(TX_ID).build() + val txn6 = JdbcSep6Transaction() + txn6.status = PENDING_TRUST.toString() + txn6.kind = kind + txn6.transferReceivedAt = Instant.now() + + every { txn6Store.findByTransactionId(TX_ID) } returns txn6 + every { txn24Store.findByTransactionId(any()) } returns null + every { txn31Store.findByTransactionId(any()) } returns null + every { custodyConfig.isCustodyIntegrationEnabled } returns true + + val ex = assertThrows { handler.handle(request) } + assertEquals( + "RPC method[do_stellar_payment] is not supported. Status[pending_trust], kind[$kind], protocol[6], funds received[true]", + ex.message + ) + + verify(exactly = 0) { txn6Store.save(any()) } + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 0) { sepTransactionCounter.increment() } + } + + @CsvSource(value = ["deposit", "deposit-exchange"]) + @ParameterizedTest + fun test_handle_sep6_custodyIntegrationDisabled(kind: String) { + val request = DoStellarPaymentRequest.builder().transactionId(TX_ID).build() + val txn6 = JdbcSep6Transaction() + txn6.status = PENDING_ANCHOR.toString() + txn6.kind = kind + txn6.transferReceivedAt = Instant.now() + + every { txn6Store.findByTransactionId(TX_ID) } returns txn6 + every { txn24Store.findByTransactionId(any()) } returns null + every { txn31Store.findByTransactionId(any()) } returns null + every { custodyConfig.isCustodyIntegrationEnabled } returns false + + val ex = assertThrows { handler.handle(request) } + assertEquals("RPC method[do_stellar_payment] requires enabled custody integration", ex.message) + + verify(exactly = 0) { txn6Store.save(any()) } + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 0) { sepTransactionCounter.increment() } + } + + @CsvSource(value = ["deposit", "deposit-exchange"]) + @ParameterizedTest + fun test_handle_sep6_transferNotReceived(kind: String) { + val request = DoStellarPaymentRequest.builder().transactionId(TX_ID).build() + val txn6 = JdbcSep6Transaction() + txn6.status = PENDING_ANCHOR.toString() + txn6.kind = kind + + every { txn6Store.findByTransactionId(TX_ID) } returns txn6 + every { txn24Store.findByTransactionId(TX_ID) } returns null + every { txn31Store.findByTransactionId(any()) } returns null + every { custodyConfig.isCustodyIntegrationEnabled } returns true + + val ex = assertThrows { handler.handle(request) } + assertEquals( + "RPC method[do_stellar_payment] is not supported. Status[pending_anchor], kind[$kind], protocol[6], funds received[false]", + ex.message + ) + + verify(exactly = 0) { txn6Store.save(any()) } + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 0) { sepTransactionCounter.increment() } + } + + @CsvSource(value = ["deposit", "deposit-exchange"]) + @ParameterizedTest + fun test_handle_sep6_ok_trustlineConfigured(kind: String) { + val transferReceivedAt = Instant.now() + val request = DoStellarPaymentRequest.builder().transactionId(TX_ID).build() + val txn6 = JdbcSep6Transaction() + txn6.id = TX_ID + txn6.status = PENDING_ANCHOR.toString() + txn6.kind = kind + txn6.transferReceivedAt = transferReceivedAt + txn6.toAccount = TO_ACCOUNT + txn6.amountOutAsset = AMOUNT_OUT_ASSET + val sep6TxnCapture = slot() + val anchorEventCapture = slot() + + every { txn6Store.findByTransactionId(TX_ID) } returns txn6 + every { txn24Store.findByTransactionId(any()) } returns null + every { txn31Store.findByTransactionId(any()) } returns null + every { txn6Store.save(capture(sep6TxnCapture)) } returns null + every { custodyConfig.isCustodyIntegrationEnabled } returns true + every { horizon.isTrustlineConfigured(TO_ACCOUNT, AMOUNT_OUT_ASSET) } returns true + every { eventSession.publish(capture(anchorEventCapture)) } just Runs + every { metricsService.counter(PLATFORM_RPC_TRANSACTION, "SEP", "sep6") } returns + sepTransactionCounter + + val startDate = Instant.now() + val response = handler.handle(request) + val endDate = Instant.now() + + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 0) { transactionPendingTrustRepo.save(any()) } + verify(exactly = 1) { custodyService.createTransactionPayment(TX_ID, null) } + verify(exactly = 1) { sepTransactionCounter.increment() } + + val expectedSep6Txn = JdbcSep6Transaction() + expectedSep6Txn.id = TX_ID + expectedSep6Txn.kind = kind + expectedSep6Txn.status = PENDING_STELLAR.toString() + expectedSep6Txn.updatedAt = sep6TxnCapture.captured.updatedAt + expectedSep6Txn.transferReceivedAt = transferReceivedAt + expectedSep6Txn.toAccount = TO_ACCOUNT + expectedSep6Txn.amountOutAsset = AMOUNT_OUT_ASSET + + JSONAssert.assertEquals( + gson.toJson(expectedSep6Txn), + gson.toJson(sep6TxnCapture.captured), + JSONCompareMode.STRICT + ) + + val expectedResponse = GetTransactionResponse() + expectedResponse.id = TX_ID + expectedResponse.sep = SEP_6 + expectedResponse.kind = PlatformTransactionData.Kind.from(kind) + expectedResponse.status = PENDING_STELLAR + expectedResponse.amountExpected = Amount(null, "") + expectedResponse.updatedAt = sep6TxnCapture.captured.updatedAt + expectedResponse.transferReceivedAt = transferReceivedAt + expectedResponse.destinationAccount = TO_ACCOUNT + expectedResponse.customers = Customers(StellarId(null, null, null), StellarId(null, null, null)) + + JSONAssert.assertEquals( + gson.toJson(expectedResponse), + gson.toJson(response), + JSONCompareMode.STRICT + ) + + val expectedEvent = + AnchorEvent.builder() + .id(anchorEventCapture.captured.id) + .sep(SEP_6.sep.toString()) + .type(TRANSACTION_STATUS_CHANGED) + .transaction(expectedResponse) + .build() + + JSONAssert.assertEquals( + gson.toJson(expectedEvent), + gson.toJson(anchorEventCapture.captured), + JSONCompareMode.STRICT + ) + + assertTrue(sep6TxnCapture.captured.updatedAt >= startDate) + assertTrue(sep6TxnCapture.captured.updatedAt <= endDate) + } + + @CsvSource(value = ["deposit", "deposit-exchange"]) + @ParameterizedTest + fun test_handle_sep6_ok_trustlineNotConfigured(kind: String) { + val transferReceivedAt = Instant.now() + val request = DoStellarPaymentRequest.builder().transactionId(TX_ID).build() + val txn6 = JdbcSep6Transaction() + txn6.id = TX_ID + txn6.status = PENDING_ANCHOR.toString() + txn6.kind = kind + txn6.transferReceivedAt = transferReceivedAt + txn6.toAccount = TO_ACCOUNT + txn6.amountOutAsset = AMOUNT_OUT_ASSET + val sep6TxnCapture = slot() + val txnPendingTrustCapture = slot() + val anchorEventCapture = slot() + + every { txn6Store.findByTransactionId(TX_ID) } returns txn6 + every { txn24Store.findByTransactionId(any()) } returns null + every { txn31Store.findByTransactionId(any()) } returns null + every { txn6Store.save(capture(sep6TxnCapture)) } returns null + every { custodyConfig.isCustodyIntegrationEnabled } returns true + every { horizon.isTrustlineConfigured(TO_ACCOUNT, AMOUNT_OUT_ASSET) } returns false + every { transactionPendingTrustRepo.save(capture(txnPendingTrustCapture)) } returns + JdbcTransactionPendingTrust() + every { eventSession.publish(capture(anchorEventCapture)) } just Runs + every { metricsService.counter(PLATFORM_RPC_TRANSACTION, "SEP", "sep6") } returns + sepTransactionCounter + + val startDate = Instant.now() + val response = handler.handle(request) + val endDate = Instant.now() + + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 0) { custodyService.createTransactionPayment(any(), any()) } + verify(exactly = 1) { sepTransactionCounter.increment() } + + val expectedSep6Txn = JdbcSep6Transaction() + expectedSep6Txn.id = TX_ID + expectedSep6Txn.kind = kind + expectedSep6Txn.status = PENDING_TRUST.toString() + expectedSep6Txn.updatedAt = sep6TxnCapture.captured.updatedAt + expectedSep6Txn.transferReceivedAt = transferReceivedAt + expectedSep6Txn.toAccount = TO_ACCOUNT + expectedSep6Txn.amountOutAsset = AMOUNT_OUT_ASSET + + JSONAssert.assertEquals( + gson.toJson(expectedSep6Txn), + gson.toJson(sep6TxnCapture.captured), + JSONCompareMode.STRICT + ) + + val expectedResponse = GetTransactionResponse() + expectedResponse.id = TX_ID + expectedResponse.sep = SEP_6 + expectedResponse.kind = PlatformTransactionData.Kind.from(kind) + expectedResponse.status = PENDING_TRUST + expectedResponse.amountExpected = Amount(null, "") + expectedResponse.updatedAt = sep6TxnCapture.captured.updatedAt + expectedResponse.transferReceivedAt = transferReceivedAt + expectedResponse.destinationAccount = TO_ACCOUNT + expectedResponse.customers = Customers(StellarId(null, null, null), StellarId(null, null, null)) + + JSONAssert.assertEquals( + gson.toJson(expectedResponse), + gson.toJson(response), + JSONCompareMode.STRICT + ) + + val expectedEvent = + AnchorEvent.builder() + .id(anchorEventCapture.captured.id) + .sep(SEP_6.sep.toString()) + .type(TRANSACTION_STATUS_CHANGED) + .transaction(expectedResponse) + .build() + + JSONAssert.assertEquals( + gson.toJson(expectedEvent), + gson.toJson(anchorEventCapture.captured), + JSONCompareMode.STRICT + ) + + val expectedTxnPendingTrust = JdbcTransactionPendingTrust() + expectedTxnPendingTrust.id = TX_ID + expectedTxnPendingTrust.asset = AMOUNT_OUT_ASSET + expectedTxnPendingTrust.account = TO_ACCOUNT + expectedTxnPendingTrust.createdAt = txnPendingTrustCapture.captured.createdAt + + JSONAssert.assertEquals( + gson.toJson(expectedTxnPendingTrust), + gson.toJson(txnPendingTrustCapture.captured), + JSONCompareMode.STRICT + ) + + assertTrue(sep6TxnCapture.captured.updatedAt >= startDate) + assertTrue(sep6TxnCapture.captured.updatedAt <= endDate) + assertTrue(txnPendingTrustCapture.captured.createdAt >= startDate) + assertTrue(txnPendingTrustCapture.captured.createdAt <= endDate) + } } diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/rpc/DoStellarRefundHandlerTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/rpc/DoStellarRefundHandlerTest.kt index 868a24b07e..ae3b3567b7 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/rpc/DoStellarRefundHandlerTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/rpc/DoStellarRefundHandlerTest.kt @@ -9,6 +9,8 @@ import kotlin.test.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource import org.skyscreamer.jsonassert.JSONAssert import org.skyscreamer.jsonassert.JSONCompareMode import org.stellar.anchor.api.event.AnchorEvent @@ -17,12 +19,14 @@ import org.stellar.anchor.api.exception.BadRequestException import org.stellar.anchor.api.exception.rpc.InvalidParamsException import org.stellar.anchor.api.exception.rpc.InvalidRequestException import org.stellar.anchor.api.platform.GetTransactionResponse +import org.stellar.anchor.api.platform.PlatformTransactionData import org.stellar.anchor.api.platform.PlatformTransactionData.Kind.DEPOSIT import org.stellar.anchor.api.platform.PlatformTransactionData.Kind.RECEIVE import org.stellar.anchor.api.platform.PlatformTransactionData.Kind.WITHDRAWAL import org.stellar.anchor.api.platform.PlatformTransactionData.Sep.SEP_24 import org.stellar.anchor.api.platform.PlatformTransactionData.Sep.SEP_31 import org.stellar.anchor.api.platform.PlatformTransactionData.Sep.SEP_38 +import org.stellar.anchor.api.platform.PlatformTransactionData.Sep.SEP_6 import org.stellar.anchor.api.rpc.method.AmountAssetRequest import org.stellar.anchor.api.rpc.method.DoStellarRefundRequest import org.stellar.anchor.api.sep.SepTransactionStatus @@ -31,6 +35,8 @@ import org.stellar.anchor.api.sep.SepTransactionStatus.PENDING_ANCHOR import org.stellar.anchor.api.sep.SepTransactionStatus.PENDING_RECEIVER import org.stellar.anchor.api.shared.Amount import org.stellar.anchor.api.shared.Customers +import org.stellar.anchor.api.shared.RefundPayment +import org.stellar.anchor.api.shared.Refunds import org.stellar.anchor.api.shared.StellarId import org.stellar.anchor.asset.AssetService import org.stellar.anchor.asset.DefaultAssetService @@ -46,6 +52,7 @@ import org.stellar.anchor.platform.data.JdbcSep24Transaction import org.stellar.anchor.platform.data.JdbcSep31RefundPayment import org.stellar.anchor.platform.data.JdbcSep31Refunds import org.stellar.anchor.platform.data.JdbcSep31Transaction +import org.stellar.anchor.platform.data.JdbcSep6Transaction import org.stellar.anchor.platform.service.AnchorMetrics.PLATFORM_RPC_TRANSACTION import org.stellar.anchor.platform.validator.RequestValidator import org.stellar.anchor.sep24.Sep24TransactionStore @@ -117,6 +124,7 @@ class DoStellarRefundHandlerTest { txn24.status = PENDING_ANCHOR.toString() val spyTxn24 = spyk(txn24) + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns spyTxn24 every { txn31Store.findByTransactionId(any()) } returns null every { spyTxn24.protocol } returns SEP_38.sep.toString() @@ -127,48 +135,7 @@ class DoStellarRefundHandlerTest { ex.message ) - verify(exactly = 0) { txn24Store.save(any()) } - verify(exactly = 0) { txn31Store.save(any()) } - verify(exactly = 0) { sepTransactionCounter.increment() } - } - - @Test - fun test_handle_unsupportedKind() { - val request = DoStellarRefundRequest.builder().transactionId(TX_ID).build() - val txn24 = JdbcSep24Transaction() - txn24.status = INCOMPLETE.toString() - txn24.kind = DEPOSIT.kind - - every { txn24Store.findByTransactionId(TX_ID) } returns txn24 - every { txn31Store.findByTransactionId(any()) } returns null - - val ex = assertThrows { handler.handle(request) } - assertEquals( - "RPC method[do_stellar_refund] is not supported. Status[incomplete], kind[deposit], protocol[24], funds received[false]", - ex.message - ) - - verify(exactly = 0) { txn24Store.save(any()) } - verify(exactly = 0) { txn31Store.save(any()) } - verify(exactly = 0) { sepTransactionCounter.increment() } - } - - @Test - fun test_handle_unsupportedStatus() { - val request = DoStellarRefundRequest.builder().transactionId(TX_ID).build() - val txn24 = JdbcSep24Transaction() - txn24.status = INCOMPLETE.toString() - txn24.kind = WITHDRAWAL.kind - - every { txn24Store.findByTransactionId(TX_ID) } returns txn24 - every { txn31Store.findByTransactionId(any()) } returns null - - val ex = assertThrows { handler.handle(request) } - assertEquals( - "RPC method[do_stellar_refund] is not supported. Status[incomplete], kind[withdrawal], protocol[24], funds received[false]", - ex.message - ) - + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn24Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 0) { sepTransactionCounter.increment() } @@ -182,6 +149,7 @@ class DoStellarRefundHandlerTest { txn24.transferReceivedAt = Instant.now() txn24.kind = WITHDRAWAL.kind + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns txn24 every { txn31Store.findByTransactionId(any()) } returns null every { requestValidator.validate(request) } throws @@ -190,30 +158,7 @@ class DoStellarRefundHandlerTest { val ex = assertThrows { handler.handle(request) } assertEquals(VALIDATION_ERROR_MESSAGE, ex.message?.trimIndent()) - verify(exactly = 0) { txn24Store.save(any()) } - verify(exactly = 0) { txn31Store.save(any()) } - verify(exactly = 0) { sepTransactionCounter.increment() } - } - - @Test - fun test_handle_disabledCustodyIntegration() { - val request = DoStellarRefundRequest.builder().transactionId(TX_ID).build() - val txn24 = JdbcSep24Transaction() - txn24.status = PENDING_ANCHOR.toString() - txn24.requestAssetCode = FIAT_USD_CODE - txn24.amountOutAsset = STELLAR_USDC - txn24.amountFeeAsset = FIAT_USD - txn24.transferReceivedAt = Instant.now() - txn24.kind = WITHDRAWAL.kind - val sep24TxnCapture = slot() - - every { txn24Store.findByTransactionId(TX_ID) } returns txn24 - every { txn31Store.findByTransactionId(any()) } returns null - every { txn24Store.save(capture(sep24TxnCapture)) } returns null - - val ex = assertThrows { handler.handle(request) } - assertEquals("RPC method[do_stellar_refund] requires enabled custody integration", ex.message) - + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn24Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 0) { sepTransactionCounter.increment() } @@ -241,6 +186,7 @@ class DoStellarRefundHandlerTest { txn24.kind = WITHDRAWAL.kind val sep24TxnCapture = slot() + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns txn24 every { txn31Store.findByTransactionId(any()) } returns null every { txn24Store.save(capture(sep24TxnCapture)) } returns null @@ -255,6 +201,7 @@ class DoStellarRefundHandlerTest { ex = assertThrows { handler.handle(request) } assertEquals("refund.amountFee.amount should be non-negative", ex.message) + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn24Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 0) { sepTransactionCounter.increment() } @@ -282,6 +229,7 @@ class DoStellarRefundHandlerTest { txn24.kind = WITHDRAWAL.kind val sep24TxnCapture = slot() + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns txn24 every { txn31Store.findByTransactionId(any()) } returns null every { txn24Store.save(capture(sep24TxnCapture)) } returns null @@ -299,13 +247,140 @@ class DoStellarRefundHandlerTest { ex.message ) + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn24Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 0) { sepTransactionCounter.increment() } } @Test - fun test_handle_ok_sep24() { + fun test_handle_sep24_unsupportedKind() { + val request = DoStellarRefundRequest.builder().transactionId(TX_ID).build() + val txn24 = JdbcSep24Transaction() + txn24.status = INCOMPLETE.toString() + txn24.kind = DEPOSIT.kind + + every { txn6Store.findByTransactionId(any()) } returns null + every { txn24Store.findByTransactionId(TX_ID) } returns txn24 + every { txn31Store.findByTransactionId(any()) } returns null + + val ex = assertThrows { handler.handle(request) } + assertEquals( + "RPC method[do_stellar_refund] is not supported. Status[incomplete], kind[deposit], protocol[24], funds received[false]", + ex.message + ) + + verify(exactly = 0) { txn6Store.save(any()) } + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 0) { sepTransactionCounter.increment() } + } + + @Test + fun test_handle_sep24_unsupportedStatus() { + val request = DoStellarRefundRequest.builder().transactionId(TX_ID).build() + val txn24 = JdbcSep24Transaction() + txn24.status = INCOMPLETE.toString() + txn24.kind = WITHDRAWAL.kind + + every { txn6Store.findByTransactionId(any()) } returns null + every { txn24Store.findByTransactionId(TX_ID) } returns txn24 + every { txn31Store.findByTransactionId(any()) } returns null + + val ex = assertThrows { handler.handle(request) } + assertEquals( + "RPC method[do_stellar_refund] is not supported. Status[incomplete], kind[withdrawal], protocol[24], funds received[false]", + ex.message + ) + + verify(exactly = 0) { txn6Store.save(any()) } + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 0) { sepTransactionCounter.increment() } + } + + @Test + fun test_handle_sep24_disabledCustodyIntegration() { + val request = DoStellarRefundRequest.builder().transactionId(TX_ID).build() + val txn24 = JdbcSep24Transaction() + txn24.status = PENDING_ANCHOR.toString() + txn24.requestAssetCode = FIAT_USD_CODE + txn24.amountOutAsset = STELLAR_USDC + txn24.amountFeeAsset = FIAT_USD + txn24.transferReceivedAt = Instant.now() + txn24.kind = WITHDRAWAL.kind + val sep24TxnCapture = slot() + + every { txn6Store.findByTransactionId(any()) } returns null + every { txn24Store.findByTransactionId(TX_ID) } returns txn24 + every { txn31Store.findByTransactionId(any()) } returns null + every { txn24Store.save(capture(sep24TxnCapture)) } returns null + + val ex = assertThrows { handler.handle(request) } + assertEquals("RPC method[do_stellar_refund] requires enabled custody integration", ex.message) + + verify(exactly = 0) { txn6Store.save(any()) } + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 0) { sepTransactionCounter.increment() } + } + + @Test + fun test_handle_sep24_sent_more_then_amount_in() { + val transferReceivedAt = Instant.now() + val request = + DoStellarRefundRequest.builder() + .transactionId(TX_ID) + .refund( + DoStellarRefundRequest.Refund.builder() + .amount(AmountAssetRequest("1", STELLAR_USDC)) + .amountFee(AmountAssetRequest("0.1", STELLAR_USDC)) + .build() + ) + .build() + val txn24 = JdbcSep24Transaction() + txn24.status = PENDING_ANCHOR.toString() + txn24.kind = WITHDRAWAL.kind + txn24.transferReceivedAt = transferReceivedAt + txn24.requestAssetCode = FIAT_USD_CODE + txn24.amountInAsset = STELLAR_USDC + txn24.amountIn = "1.1" + txn24.amountOutAsset = FIAT_USD + txn24.amountOut = "1" + txn24.amountFeeAsset = STELLAR_USDC + txn24.amountFee = "0.1" + txn24.refundMemo = MEMO + txn24.refundMemoType = MEMO_TYPE + + val payment = JdbcSep24RefundPayment() + payment.id = "1" + payment.amount = "0.1" + payment.fee = "0" + val refunds = JdbcSep24Refunds() + refunds.amountRefunded = "1" + refunds.amountFee = "0.1" + refunds.payments = listOf(payment) + txn24.refunds = refunds + + val sep24TxnCapture = slot() + + every { txn6Store.findByTransactionId(any()) } returns null + every { txn24Store.findByTransactionId(TX_ID) } returns txn24 + every { txn31Store.findByTransactionId(any()) } returns null + every { txn24Store.save(capture(sep24TxnCapture)) } returns null + every { custodyConfig.isCustodyIntegrationEnabled } returns true + + val ex = assertThrows { handler.handle(request) } + assertEquals("Refund amount exceeds amount_in", ex.message) + + verify(exactly = 0) { txn6Store.save(any()) } + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 0) { sepTransactionCounter.increment() } + } + + @Test + fun test_handle_sep24_ok() { val transferReceivedAt = Instant.now() val request = DoStellarRefundRequest.builder() @@ -333,6 +408,7 @@ class DoStellarRefundHandlerTest { val sep24TxnCapture = slot() val anchorEventCapture = slot() + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns txn24 every { txn31Store.findByTransactionId(any()) } returns null every { txn24Store.save(capture(sep24TxnCapture)) } returns null @@ -345,6 +421,7 @@ class DoStellarRefundHandlerTest { val response = handler.handle(request) val endDate = Instant.now() + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 1) { sepTransactionCounter.increment() } @@ -430,7 +507,8 @@ class DoStellarRefundHandlerTest { val sep31TxnCapture = slot() val anchorEventCapture = slot() - every { txn24Store.findByTransactionId(TX_ID) } returns null + every { txn6Store.findByTransactionId(any()) } returns null + every { txn24Store.findByTransactionId(any()) } returns null every { txn31Store.findByTransactionId(TX_ID) } returns txn31 every { txn31Store.save(capture(sep31TxnCapture)) } returns null every { custodyConfig.isCustodyIntegrationEnabled } returns true @@ -495,58 +573,6 @@ class DoStellarRefundHandlerTest { assertTrue(sep31TxnCapture.captured.updatedAt <= endDate) } - @Test - fun test_handle_sep24_sent_more_then_amount_in() { - val transferReceivedAt = Instant.now() - val request = - DoStellarRefundRequest.builder() - .transactionId(TX_ID) - .refund( - DoStellarRefundRequest.Refund.builder() - .amount(AmountAssetRequest("1", STELLAR_USDC)) - .amountFee(AmountAssetRequest("0.1", STELLAR_USDC)) - .build() - ) - .build() - val txn24 = JdbcSep24Transaction() - txn24.status = PENDING_ANCHOR.toString() - txn24.kind = WITHDRAWAL.kind - txn24.transferReceivedAt = transferReceivedAt - txn24.requestAssetCode = FIAT_USD_CODE - txn24.amountInAsset = STELLAR_USDC - txn24.amountIn = "1.1" - txn24.amountOutAsset = FIAT_USD - txn24.amountOut = "1" - txn24.amountFeeAsset = STELLAR_USDC - txn24.amountFee = "0.1" - txn24.refundMemo = MEMO - txn24.refundMemoType = MEMO_TYPE - - val payment = JdbcSep24RefundPayment() - payment.id = "1" - payment.amount = "0.1" - payment.fee = "0" - val refunds = JdbcSep24Refunds() - refunds.amountRefunded = "1" - refunds.amountFee = "0.1" - refunds.payments = listOf(payment) - txn24.refunds = refunds - - val sep24TxnCapture = slot() - - every { txn24Store.findByTransactionId(TX_ID) } returns txn24 - every { txn31Store.findByTransactionId(TX_ID) } returns null - every { txn24Store.save(capture(sep24TxnCapture)) } returns null - every { custodyConfig.isCustodyIntegrationEnabled } returns true - - val ex = assertThrows { handler.handle(request) } - assertEquals("Refund amount exceeds amount_in", ex.message) - - verify(exactly = 0) { txn24Store.save(any()) } - verify(exactly = 0) { txn31Store.save(any()) } - verify(exactly = 0) { sepTransactionCounter.increment() } - } - @Test fun test_handle_sep31_sent_more_then_amount_in() { val transferReceivedAt = Instant.now() @@ -571,7 +597,8 @@ class DoStellarRefundHandlerTest { txn31.amountFee = "0.1" val sep31TxnCapture = slot() - every { txn24Store.findByTransactionId(TX_ID) } returns null + every { txn6Store.findByTransactionId(any()) } returns null + every { txn24Store.findByTransactionId(any()) } returns null every { txn31Store.findByTransactionId(TX_ID) } returns txn31 every { txn31Store.save(capture(sep31TxnCapture)) } returns null every { custodyConfig.isCustodyIntegrationEnabled } returns true @@ -579,6 +606,7 @@ class DoStellarRefundHandlerTest { val ex = assertThrows { handler.handle(request) } assertEquals("Refund amount exceeds amount_in", ex.message) + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn24Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 0) { sepTransactionCounter.increment() } @@ -608,14 +636,16 @@ class DoStellarRefundHandlerTest { txn31.amountFee = "0.1" val sep31TxnCapture = slot() + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns null - every { txn31Store.findByTransactionId(TX_ID) } returns txn31 + every { txn31Store.findByTransactionId(any()) } returns txn31 every { txn31Store.save(capture(sep31TxnCapture)) } returns null every { custodyConfig.isCustodyIntegrationEnabled } returns true val ex = assertThrows { handler.handle(request) } assertEquals("Refund amount is less than amount_in", ex.message) + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn24Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 0) { sepTransactionCounter.increment() } @@ -654,7 +684,8 @@ class DoStellarRefundHandlerTest { txn31.refunds = refunds val sep31TxnCapture = slot() - every { txn24Store.findByTransactionId(TX_ID) } returns null + every { txn6Store.findByTransactionId(any()) } returns null + every { txn24Store.findByTransactionId(any()) } returns null every { txn31Store.findByTransactionId(TX_ID) } returns txn31 every { txn31Store.save(capture(sep31TxnCapture)) } returns null every { custodyConfig.isCustodyIntegrationEnabled } returns true @@ -665,8 +696,246 @@ class DoStellarRefundHandlerTest { ex.message ) + verify(exactly = 0) { txn6Store.save(any()) } + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 0) { sepTransactionCounter.increment() } + } + + @CsvSource(value = ["deposit", "deposit-exchange"]) + @ParameterizedTest + fun test_handle_sep6_unsupportedKind(kind: String) { + val request = DoStellarRefundRequest.builder().transactionId(TX_ID).build() + val txn6 = JdbcSep6Transaction() + txn6.status = INCOMPLETE.toString() + txn6.kind = kind + + every { txn6Store.findByTransactionId(TX_ID) } returns txn6 + every { txn24Store.findByTransactionId(any()) } returns null + every { txn31Store.findByTransactionId(any()) } returns null + + val ex = assertThrows { handler.handle(request) } + assertEquals( + "RPC method[do_stellar_refund] is not supported. Status[incomplete], kind[$kind], protocol[6], funds received[false]", + ex.message + ) + + verify(exactly = 0) { txn6Store.save(any()) } + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 0) { sepTransactionCounter.increment() } + } + + @CsvSource(value = ["withdrawal", "withdrawal-exchange"]) + @ParameterizedTest + fun test_handle_sep6_unsupportedStatus(kind: String) { + val request = DoStellarRefundRequest.builder().transactionId(TX_ID).build() + val txn6 = JdbcSep6Transaction() + txn6.status = INCOMPLETE.toString() + txn6.kind = kind + + every { txn6Store.findByTransactionId(TX_ID) } returns txn6 + every { txn24Store.findByTransactionId(any()) } returns null + every { txn31Store.findByTransactionId(any()) } returns null + + val ex = assertThrows { handler.handle(request) } + assertEquals( + "RPC method[do_stellar_refund] is not supported. Status[incomplete], kind[$kind], protocol[6], funds received[false]", + ex.message + ) + + verify(exactly = 0) { txn6Store.save(any()) } + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 0) { sepTransactionCounter.increment() } + } + + @CsvSource(value = ["withdrawal", "withdrawal-exchange"]) + @ParameterizedTest + fun test_handle_sep6_disabledCustodyIntegration(kind: String) { + val request = DoStellarRefundRequest.builder().transactionId(TX_ID).build() + val txn6 = JdbcSep6Transaction() + txn6.status = PENDING_ANCHOR.toString() + txn6.requestAssetCode = FIAT_USD_CODE + txn6.amountOutAsset = STELLAR_USDC + txn6.amountFeeAsset = FIAT_USD + txn6.transferReceivedAt = Instant.now() + txn6.kind = kind + val sep6TxnCapture = slot() + + every { txn6Store.findByTransactionId(TX_ID) } returns txn6 + every { txn24Store.findByTransactionId(any()) } returns null + every { txn31Store.findByTransactionId(any()) } returns null + every { txn6Store.save(capture(sep6TxnCapture)) } returns null + every { custodyConfig.isCustodyIntegrationEnabled } returns false + + val ex = assertThrows { handler.handle(request) } + assertEquals("RPC method[do_stellar_refund] requires enabled custody integration", ex.message) + + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn24Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 0) { sepTransactionCounter.increment() } } + + @CsvSource(value = ["withdrawal", "withdrawal-exchange"]) + @ParameterizedTest + fun test_handle_sep6_sent_more_then_amount_in(kind: String) { + val transferReceivedAt = Instant.now() + val request = + DoStellarRefundRequest.builder() + .transactionId(TX_ID) + .refund( + DoStellarRefundRequest.Refund.builder() + .amount(AmountAssetRequest("1", STELLAR_USDC)) + .amountFee(AmountAssetRequest("0.1", STELLAR_USDC)) + .build() + ) + .build() + val txn6 = JdbcSep6Transaction() + txn6.status = PENDING_ANCHOR.toString() + txn6.kind = kind + txn6.transferReceivedAt = transferReceivedAt + txn6.requestAssetCode = FIAT_USD_CODE + txn6.amountInAsset = STELLAR_USDC + txn6.amountIn = "1.1" + txn6.amountOutAsset = FIAT_USD + txn6.amountOut = "1" + txn6.amountFeeAsset = STELLAR_USDC + txn6.amountFee = "0.1" + txn6.refundMemo = MEMO + txn6.refundMemoType = MEMO_TYPE + + val payment = RefundPayment() + payment.id = "1" + payment.amount = Amount("0.1", STELLAR_USDC) + payment.fee = Amount("0", STELLAR_USDC) + val refunds = Refunds() + refunds.amountRefunded = Amount("1", STELLAR_USDC) + refunds.amountFee = Amount("0.1", STELLAR_USDC) + refunds.payments = arrayOf(payment) + txn6.refunds = refunds + + val sep24TxnCapture = slot() + + every { txn6Store.findByTransactionId(TX_ID) } returns txn6 + every { txn24Store.findByTransactionId(any()) } returns null + every { txn31Store.findByTransactionId(any()) } returns null + every { txn24Store.save(capture(sep24TxnCapture)) } returns null + every { custodyConfig.isCustodyIntegrationEnabled } returns true + + val ex = assertThrows { handler.handle(request) } + assertEquals("Refund amount exceeds amount_in", ex.message) + + verify(exactly = 0) { txn6Store.save(any()) } + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 0) { sepTransactionCounter.increment() } + } + + @CsvSource(value = ["withdrawal", "withdrawal-exchange"]) + @ParameterizedTest + fun test_handle_sep6_ok(kind: String) { + val transferReceivedAt = Instant.now() + val request = + DoStellarRefundRequest.builder() + .transactionId(TX_ID) + .refund( + DoStellarRefundRequest.Refund.builder() + .amount(AmountAssetRequest("1", STELLAR_USDC)) + .amountFee(AmountAssetRequest("0.1", FIAT_USD)) + .build() + ) + .build() + val txn6 = JdbcSep6Transaction() + txn6.status = PENDING_ANCHOR.toString() + txn6.kind = kind + txn6.transferReceivedAt = transferReceivedAt + txn6.requestAssetCode = FIAT_USD_CODE + txn6.amountInAsset = STELLAR_USDC + txn6.amountIn = "1.1" + txn6.amountOutAsset = STELLAR_USDC + txn6.amountOut = "1" + txn6.amountFeeAsset = FIAT_USD + txn6.amountFee = "0.1" + txn6.refundMemo = MEMO + txn6.refundMemoType = MEMO_TYPE + val sep6TxnCapture = slot() + val anchorEventCapture = slot() + + every { txn6Store.findByTransactionId(TX_ID) } returns txn6 + every { txn24Store.findByTransactionId(any()) } returns null + every { txn31Store.findByTransactionId(any()) } returns null + every { txn6Store.save(capture(sep6TxnCapture)) } returns null + every { custodyConfig.isCustodyIntegrationEnabled } returns true + every { eventSession.publish(capture(anchorEventCapture)) } just Runs + every { metricsService.counter(PLATFORM_RPC_TRANSACTION, "SEP", "sep6") } returns + sepTransactionCounter + + val startDate = Instant.now() + val response = handler.handle(request) + val endDate = Instant.now() + + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 1) { sepTransactionCounter.increment() } + + val expectedSep6Txn = JdbcSep6Transaction() + expectedSep6Txn.kind = kind + expectedSep6Txn.status = SepTransactionStatus.PENDING_STELLAR.toString() + expectedSep6Txn.updatedAt = sep6TxnCapture.captured.updatedAt + expectedSep6Txn.requestAssetCode = FIAT_USD_CODE + expectedSep6Txn.amountInAsset = STELLAR_USDC + expectedSep6Txn.amountIn = "1.1" + expectedSep6Txn.amountOutAsset = STELLAR_USDC + expectedSep6Txn.amountOut = "1" + expectedSep6Txn.amountFeeAsset = FIAT_USD + expectedSep6Txn.amountFee = "0.1" + expectedSep6Txn.refundMemo = MEMO + expectedSep6Txn.refundMemoType = MEMO_TYPE + expectedSep6Txn.transferReceivedAt = transferReceivedAt + + JSONAssert.assertEquals( + gson.toJson(expectedSep6Txn), + gson.toJson(sep6TxnCapture.captured), + JSONCompareMode.STRICT + ) + + val expectedResponse = GetTransactionResponse() + expectedResponse.sep = SEP_6 + expectedResponse.kind = PlatformTransactionData.Kind.from(kind) + expectedResponse.status = SepTransactionStatus.PENDING_STELLAR + expectedResponse.amountExpected = Amount(null, FIAT_USD) + expectedResponse.amountIn = Amount("1.1", STELLAR_USDC) + expectedResponse.amountOut = Amount("1", STELLAR_USDC) + expectedResponse.amountFee = Amount("0.1", FIAT_USD) + expectedResponse.updatedAt = sep6TxnCapture.captured.updatedAt + expectedResponse.transferReceivedAt = transferReceivedAt + expectedResponse.refundMemo = MEMO + expectedResponse.refundMemoType = MEMO_TYPE + expectedResponse.customers = Customers(StellarId(null, null, null), StellarId(null, null, null)) + + JSONAssert.assertEquals( + gson.toJson(expectedResponse), + gson.toJson(response), + JSONCompareMode.STRICT + ) + + val expectedEvent = + AnchorEvent.builder() + .id(anchorEventCapture.captured.id) + .sep(SEP_6.sep.toString()) + .type(TRANSACTION_STATUS_CHANGED) + .transaction(expectedResponse) + .build() + + JSONAssert.assertEquals( + gson.toJson(expectedEvent), + gson.toJson(anchorEventCapture.captured), + JSONCompareMode.STRICT + ) + + assertTrue(sep6TxnCapture.captured.updatedAt >= startDate) + assertTrue(sep6TxnCapture.captured.updatedAt <= endDate) + } } diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/rpc/NotifyOffchainFundsReceivedHandlerTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/rpc/NotifyOffchainFundsReceivedHandlerTest.kt index 8fda60b598..8d06824153 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/rpc/NotifyOffchainFundsReceivedHandlerTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/rpc/NotifyOffchainFundsReceivedHandlerTest.kt @@ -859,7 +859,9 @@ class NotifyOffchainFundsReceivedHandlerTest { @CsvSource(value = ["deposit", "deposit-exchange"]) @ParameterizedTest - fun test_handle_sep6_ok_withExternalTxIdAndWithoutFundsReceivedAt(kind: String) { + fun test_handle_sep6_ok_withExternalTxIdAndWithoutFundsReceivedAt_custodyIntegrationEnabled( + kind: String + ) { val request = NotifyOffchainFundsReceivedRequest.builder() .transactionId(TX_ID) @@ -870,13 +872,15 @@ class NotifyOffchainFundsReceivedHandlerTest { txn6.kind = kind txn6.requestAssetCode = FIAT_USD_CODE val sep6TxnCapture = slot() + val sep6CustodyTxnCapture = slot() val anchorEventCapture = slot() every { txn6Store.findByTransactionId(TX_ID) } returns txn6 every { txn24Store.findByTransactionId(any()) } returns null every { txn31Store.findByTransactionId(any()) } returns null every { txn6Store.save(capture(sep6TxnCapture)) } returns null - every { custodyConfig.isCustodyIntegrationEnabled } returns false + every { custodyConfig.isCustodyIntegrationEnabled } returns true + every { custodyService.createTransaction(capture(sep6CustodyTxnCapture)) } just Runs every { eventSession.publish(capture(anchorEventCapture)) } just Runs every { metricsService.counter(PLATFORM_RPC_TRANSACTION, "SEP", "sep6") } returns sepTransactionCounter @@ -904,6 +908,12 @@ class NotifyOffchainFundsReceivedHandlerTest { JSONCompareMode.STRICT ) + JSONAssert.assertEquals( + gson.toJson(expectedSep6Txn), + gson.toJson(sep6CustodyTxnCapture.captured), + JSONCompareMode.STRICT + ) + val expectedResponse = GetTransactionResponse() expectedResponse.sep = SEP_6 expectedResponse.kind = Kind.from(kind) diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/rpc/NotifyTrustSetHandlerTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/rpc/NotifyTrustSetHandlerTest.kt index e9d8e44e5a..d9a97ed8ba 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/rpc/NotifyTrustSetHandlerTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/rpc/NotifyTrustSetHandlerTest.kt @@ -9,6 +9,8 @@ import kotlin.test.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource import org.skyscreamer.jsonassert.JSONAssert import org.skyscreamer.jsonassert.JSONCompareMode import org.stellar.anchor.api.event.AnchorEvent @@ -16,13 +18,15 @@ import org.stellar.anchor.api.event.AnchorEvent.Type.TRANSACTION_STATUS_CHANGED import org.stellar.anchor.api.exception.rpc.InvalidParamsException import org.stellar.anchor.api.exception.rpc.InvalidRequestException import org.stellar.anchor.api.platform.GetTransactionResponse +import org.stellar.anchor.api.platform.PlatformTransactionData import org.stellar.anchor.api.platform.PlatformTransactionData.Kind.DEPOSIT import org.stellar.anchor.api.platform.PlatformTransactionData.Kind.WITHDRAWAL -import org.stellar.anchor.api.platform.PlatformTransactionData.Sep.SEP_24 -import org.stellar.anchor.api.platform.PlatformTransactionData.Sep.SEP_38 +import org.stellar.anchor.api.platform.PlatformTransactionData.Sep.* import org.stellar.anchor.api.rpc.method.NotifyTrustSetRequest import org.stellar.anchor.api.sep.SepTransactionStatus.* import org.stellar.anchor.api.shared.Amount +import org.stellar.anchor.api.shared.Customers +import org.stellar.anchor.api.shared.StellarId import org.stellar.anchor.asset.AssetService import org.stellar.anchor.custody.CustodyService import org.stellar.anchor.event.EventService @@ -31,6 +35,7 @@ import org.stellar.anchor.event.EventService.Session import org.stellar.anchor.metrics.MetricsService import org.stellar.anchor.platform.config.PropertyCustodyConfig import org.stellar.anchor.platform.data.JdbcSep24Transaction +import org.stellar.anchor.platform.data.JdbcSep6Transaction import org.stellar.anchor.platform.service.AnchorMetrics.PLATFORM_RPC_TRANSACTION import org.stellar.anchor.platform.validator.RequestValidator import org.stellar.anchor.sep24.Sep24TransactionStore @@ -96,6 +101,7 @@ class NotifyTrustSetHandlerTest { txn24.kind = DEPOSIT.kind val spyTxn24 = spyk(txn24) + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns spyTxn24 every { txn31Store.findByTransactionId(any()) } returns null every { spyTxn24.protocol } returns SEP_38.sep.toString() @@ -106,81 +112,89 @@ class NotifyTrustSetHandlerTest { ex.message ) + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn24Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 0) { sepTransactionCounter.increment() } } @Test - fun test_handle_unsupportedStatus() { + fun test_handle_invalidRequest() { val request = NotifyTrustSetRequest.builder().transactionId(TX_ID).build() val txn24 = JdbcSep24Transaction() - txn24.status = PENDING_ANCHOR.toString() + txn24.status = PENDING_TRUST.toString() txn24.kind = DEPOSIT.kind + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns txn24 every { txn31Store.findByTransactionId(any()) } returns null + every { requestValidator.validate(request) } throws + InvalidParamsException(VALIDATION_ERROR_MESSAGE) - val ex = assertThrows { handler.handle(request) } - assertEquals( - "RPC method[notify_trust_set] is not supported. Status[pending_anchor], kind[deposit], protocol[24], funds received[false]", - ex.message - ) + val ex = assertThrows { handler.handle(request) } + assertEquals(VALIDATION_ERROR_MESSAGE, ex.message?.trimIndent()) + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn24Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 0) { sepTransactionCounter.increment() } } @Test - fun test_handle_unsupportedKind() { + fun test_handle_sep24_unsupportedStatus() { val request = NotifyTrustSetRequest.builder().transactionId(TX_ID).build() val txn24 = JdbcSep24Transaction() txn24.status = PENDING_ANCHOR.toString() - txn24.kind = WITHDRAWAL.kind + txn24.kind = DEPOSIT.kind + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns txn24 every { txn31Store.findByTransactionId(any()) } returns null val ex = assertThrows { handler.handle(request) } assertEquals( - "RPC method[notify_trust_set] is not supported. Status[pending_anchor], kind[withdrawal], protocol[24], funds received[false]", + "RPC method[notify_trust_set] is not supported. Status[pending_anchor], kind[deposit], protocol[24], funds received[false]", ex.message ) + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn24Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 0) { sepTransactionCounter.increment() } } @Test - fun test_handle_invalidRequest() { + fun test_handle_sep24_unsupportedKind() { val request = NotifyTrustSetRequest.builder().transactionId(TX_ID).build() val txn24 = JdbcSep24Transaction() - txn24.status = PENDING_TRUST.toString() - txn24.kind = DEPOSIT.kind + txn24.status = PENDING_ANCHOR.toString() + txn24.kind = WITHDRAWAL.kind + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns txn24 every { txn31Store.findByTransactionId(any()) } returns null - every { requestValidator.validate(request) } throws - InvalidParamsException(VALIDATION_ERROR_MESSAGE) - val ex = assertThrows { handler.handle(request) } - assertEquals(VALIDATION_ERROR_MESSAGE, ex.message?.trimIndent()) + val ex = assertThrows { handler.handle(request) } + assertEquals( + "RPC method[notify_trust_set] is not supported. Status[pending_anchor], kind[withdrawal], protocol[24], funds received[false]", + ex.message + ) + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn24Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 0) { sepTransactionCounter.increment() } } @Test - fun test_handle_ok_custodyIntegrationDisabled() { + fun test_handle_sep24_ok_custodyIntegrationDisabled() { val request = NotifyTrustSetRequest.builder().transactionId(TX_ID).build() val txn24 = JdbcSep24Transaction() txn24.status = PENDING_TRUST.toString() txn24.kind = DEPOSIT.kind val sep24TxnCapture = slot() + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns txn24 every { txn31Store.findByTransactionId(any()) } returns null every { txn24Store.save(capture(sep24TxnCapture)) } returns null @@ -192,6 +206,7 @@ class NotifyTrustSetHandlerTest { val response = handler.handle(request) val endDate = Instant.now() + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 1) { sepTransactionCounter.increment() } @@ -224,7 +239,7 @@ class NotifyTrustSetHandlerTest { } @Test - fun test_handle_ok_custodyIntegrationEnabled_success() { + fun test_handle_sep24_ok_custodyIntegrationEnabled_success() { val request = NotifyTrustSetRequest.builder().transactionId(TX_ID).success(true).build() val txn24 = JdbcSep24Transaction() txn24.id = TX_ID @@ -232,6 +247,7 @@ class NotifyTrustSetHandlerTest { txn24.kind = DEPOSIT.kind val sep24TxnCapture = slot() + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns txn24 every { txn31Store.findByTransactionId(any()) } returns null every { txn24Store.save(capture(sep24TxnCapture)) } returns null @@ -244,6 +260,7 @@ class NotifyTrustSetHandlerTest { val endDate = Instant.now() verify(exactly = 1) { custodyService.createTransactionPayment(TX_ID, null) } + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 1) { sepTransactionCounter.increment() } @@ -278,13 +295,14 @@ class NotifyTrustSetHandlerTest { } @Test - fun test_handle_ok_custodyIntegrationEnabled_fail() { + fun test_handle_sep24_ok_custodyIntegrationEnabled_fail() { val request = NotifyTrustSetRequest.builder().transactionId(TX_ID).success(false).build() val txn24 = JdbcSep24Transaction() txn24.status = PENDING_TRUST.toString() txn24.kind = DEPOSIT.kind val sep24TxnCapture = slot() + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns txn24 every { txn31Store.findByTransactionId(any()) } returns null every { txn24Store.save(capture(sep24TxnCapture)) } returns null @@ -297,6 +315,7 @@ class NotifyTrustSetHandlerTest { val endDate = Instant.now() verify(exactly = 0) { custodyService.createTransactionPayment(any(), any()) } + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 1) { sepTransactionCounter.increment() } @@ -329,7 +348,7 @@ class NotifyTrustSetHandlerTest { } @Test - fun test_handle_ok() { + fun test_handle_sep24_ok() { val request = NotifyTrustSetRequest.builder().transactionId(TX_ID).build() val txn24 = JdbcSep24Transaction() txn24.status = PENDING_TRUST.toString() @@ -337,6 +356,7 @@ class NotifyTrustSetHandlerTest { val sep24TxnCapture = slot() val anchorEventCapture = slot() + every { txn6Store.findByTransactionId(any()) } returns null every { txn24Store.findByTransactionId(TX_ID) } returns txn24 every { txn31Store.findByTransactionId(any()) } returns null every { txn24Store.save(capture(sep24TxnCapture)) } returns null @@ -349,6 +369,7 @@ class NotifyTrustSetHandlerTest { val response = handler.handle(request) val endDate = Instant.now() + verify(exactly = 0) { txn6Store.save(any()) } verify(exactly = 0) { txn31Store.save(any()) } verify(exactly = 1) { sepTransactionCounter.increment() } @@ -393,4 +414,289 @@ class NotifyTrustSetHandlerTest { assertTrue(expectedSep24Txn.updatedAt >= startDate) assertTrue(expectedSep24Txn.updatedAt <= endDate) } + + @CsvSource(value = ["deposit", "deposit-exchange"]) + @ParameterizedTest + fun test_handle_sep6_unsupportedStatus(kind: String) { + val request = NotifyTrustSetRequest.builder().transactionId(TX_ID).build() + val txn6 = JdbcSep6Transaction() + txn6.status = PENDING_ANCHOR.toString() + txn6.kind = kind + + every { txn6Store.findByTransactionId(TX_ID) } returns txn6 + every { txn24Store.findByTransactionId(any()) } returns null + every { txn31Store.findByTransactionId(any()) } returns null + + val ex = assertThrows { handler.handle(request) } + assertEquals( + "RPC method[notify_trust_set] is not supported. Status[pending_anchor], kind[$kind], protocol[6], funds received[false]", + ex.message + ) + + verify(exactly = 0) { txn6Store.save(any()) } + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 0) { sepTransactionCounter.increment() } + } + + @CsvSource(value = ["withdrawal", "withdrawal-exchange"]) + @ParameterizedTest + fun test_handle_sep6_unsupportedKind(kind: String) { + val request = NotifyTrustSetRequest.builder().transactionId(TX_ID).build() + val txn6 = JdbcSep6Transaction() + txn6.status = PENDING_ANCHOR.toString() + txn6.kind = kind + + every { txn6Store.findByTransactionId(TX_ID) } returns txn6 + every { txn24Store.findByTransactionId(any()) } returns null + every { txn31Store.findByTransactionId(any()) } returns null + + val ex = assertThrows { handler.handle(request) } + assertEquals( + "RPC method[notify_trust_set] is not supported. Status[pending_anchor], kind[$kind], protocol[6], funds received[false]", + ex.message + ) + + verify(exactly = 0) { txn6Store.save(any()) } + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 0) { sepTransactionCounter.increment() } + } + + @CsvSource(value = ["deposit", "deposit-exchange"]) + @ParameterizedTest + fun test_handle_sep6_ok_custodyIntegrationDisabled(kind: String) { + val request = NotifyTrustSetRequest.builder().transactionId(TX_ID).build() + val txn6 = JdbcSep6Transaction() + txn6.status = PENDING_TRUST.toString() + txn6.kind = kind + val sep6TxnCapture = slot() + + every { txn6Store.findByTransactionId(TX_ID) } returns txn6 + every { txn24Store.findByTransactionId(any()) } returns null + every { txn31Store.findByTransactionId(any()) } returns null + every { txn6Store.save(capture(sep6TxnCapture)) } returns null + every { custodyConfig.isCustodyIntegrationEnabled } returns false + every { metricsService.counter(PLATFORM_RPC_TRANSACTION, "SEP", "sep6") } returns + sepTransactionCounter + + val startDate = Instant.now() + val response = handler.handle(request) + val endDate = Instant.now() + + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 1) { sepTransactionCounter.increment() } + + val expectedSep6Txn = JdbcSep6Transaction() + expectedSep6Txn.kind = kind + expectedSep6Txn.status = PENDING_ANCHOR.toString() + expectedSep6Txn.updatedAt = sep6TxnCapture.captured.updatedAt + + JSONAssert.assertEquals( + gson.toJson(expectedSep6Txn), + gson.toJson(sep6TxnCapture.captured), + JSONCompareMode.STRICT + ) + + val expectedResponse = GetTransactionResponse() + expectedResponse.sep = SEP_6 + expectedResponse.kind = PlatformTransactionData.Kind.from(kind) + expectedResponse.status = PENDING_ANCHOR + expectedResponse.amountExpected = Amount(null, "") + expectedResponse.updatedAt = sep6TxnCapture.captured.updatedAt + expectedResponse.customers = Customers(StellarId(null, null, null), StellarId(null, null, null)) + + JSONAssert.assertEquals( + gson.toJson(expectedResponse), + gson.toJson(response), + JSONCompareMode.STRICT + ) + + assertTrue(sep6TxnCapture.captured.updatedAt >= startDate) + assertTrue(sep6TxnCapture.captured.updatedAt <= endDate) + } + + @CsvSource(value = ["deposit", "deposit-exchange"]) + @ParameterizedTest + fun test_handle_sep6_ok_custodyIntegrationEnabled_success(kind: String) { + val request = NotifyTrustSetRequest.builder().transactionId(TX_ID).success(true).build() + val txn6 = JdbcSep6Transaction() + txn6.id = TX_ID + txn6.status = PENDING_TRUST.toString() + txn6.kind = kind + val sep6TxnCapture = slot() + + every { txn6Store.findByTransactionId(TX_ID) } returns txn6 + every { txn24Store.findByTransactionId(any()) } returns null + every { txn31Store.findByTransactionId(any()) } returns null + every { txn6Store.save(capture(sep6TxnCapture)) } returns null + every { custodyConfig.isCustodyIntegrationEnabled } returns true + every { metricsService.counter(PLATFORM_RPC_TRANSACTION, "SEP", "sep6") } returns + sepTransactionCounter + + val startDate = Instant.now() + val response = handler.handle(request) + val endDate = Instant.now() + + verify(exactly = 1) { custodyService.createTransactionPayment(TX_ID, null) } + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 1) { sepTransactionCounter.increment() } + + val expectedSep6Txn = JdbcSep6Transaction() + expectedSep6Txn.id = TX_ID + expectedSep6Txn.kind = kind + expectedSep6Txn.status = PENDING_STELLAR.toString() + expectedSep6Txn.updatedAt = sep6TxnCapture.captured.updatedAt + + JSONAssert.assertEquals( + gson.toJson(expectedSep6Txn), + gson.toJson(sep6TxnCapture.captured), + JSONCompareMode.STRICT + ) + + val expectedResponse = GetTransactionResponse() + expectedResponse.id = TX_ID + expectedResponse.sep = SEP_6 + expectedResponse.kind = PlatformTransactionData.Kind.from(kind) + expectedResponse.status = PENDING_STELLAR + expectedResponse.amountExpected = Amount(null, "") + expectedResponse.updatedAt = sep6TxnCapture.captured.updatedAt + expectedResponse.customers = Customers(StellarId(null, null, null), StellarId(null, null, null)) + + JSONAssert.assertEquals( + gson.toJson(expectedResponse), + gson.toJson(response), + JSONCompareMode.STRICT + ) + + assertTrue(expectedSep6Txn.updatedAt >= startDate) + assertTrue(expectedSep6Txn.updatedAt <= endDate) + } + + @CsvSource(value = ["deposit", "deposit-exchange"]) + @ParameterizedTest + fun test_handle_sep6_ok_custodyIntegrationEnabled_fail(kind: String) { + val request = NotifyTrustSetRequest.builder().transactionId(TX_ID).success(false).build() + val txn6 = JdbcSep6Transaction() + txn6.status = PENDING_TRUST.toString() + txn6.kind = kind + val sep6TxnCapture = slot() + + every { txn6Store.findByTransactionId(TX_ID) } returns txn6 + every { txn24Store.findByTransactionId(any()) } returns null + every { txn31Store.findByTransactionId(any()) } returns null + every { txn6Store.save(capture(sep6TxnCapture)) } returns null + every { custodyConfig.isCustodyIntegrationEnabled } returns true + every { metricsService.counter(PLATFORM_RPC_TRANSACTION, "SEP", "sep6") } returns + sepTransactionCounter + + val startDate = Instant.now() + val response = handler.handle(request) + val endDate = Instant.now() + + verify(exactly = 0) { custodyService.createTransactionPayment(any(), any()) } + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 1) { sepTransactionCounter.increment() } + + val expectedSep6Txn = JdbcSep6Transaction() + expectedSep6Txn.kind = kind + expectedSep6Txn.status = PENDING_ANCHOR.toString() + expectedSep6Txn.updatedAt = sep6TxnCapture.captured.updatedAt + + JSONAssert.assertEquals( + gson.toJson(expectedSep6Txn), + gson.toJson(sep6TxnCapture.captured), + JSONCompareMode.STRICT + ) + + val expectedResponse = GetTransactionResponse() + expectedResponse.sep = SEP_6 + expectedResponse.kind = PlatformTransactionData.Kind.from(kind) + expectedResponse.status = PENDING_ANCHOR + expectedResponse.amountExpected = Amount(null, "") + expectedResponse.updatedAt = sep6TxnCapture.captured.updatedAt + expectedResponse.customers = Customers(StellarId(null, null, null), StellarId(null, null, null)) + + JSONAssert.assertEquals( + gson.toJson(expectedResponse), + gson.toJson(response), + JSONCompareMode.STRICT + ) + + assertTrue(expectedSep6Txn.updatedAt >= startDate) + assertTrue(expectedSep6Txn.updatedAt <= endDate) + } + + @CsvSource(value = ["deposit", "deposit-exchange"]) + @ParameterizedTest + fun test_handle_sep6_ok(kind: String) { + val request = NotifyTrustSetRequest.builder().transactionId(TX_ID).build() + val txn6 = JdbcSep6Transaction() + txn6.status = PENDING_TRUST.toString() + txn6.kind = kind + val sep6TxnCapture = slot() + val anchorEventCapture = slot() + + every { txn6Store.findByTransactionId(TX_ID) } returns txn6 + every { txn24Store.findByTransactionId(any()) } returns null + every { txn31Store.findByTransactionId(any()) } returns null + every { txn6Store.save(capture(sep6TxnCapture)) } returns null + every { custodyConfig.isCustodyIntegrationEnabled } returns false + every { eventSession.publish(capture(anchorEventCapture)) } just Runs + every { metricsService.counter(PLATFORM_RPC_TRANSACTION, "SEP", "sep6") } returns + sepTransactionCounter + + val startDate = Instant.now() + val response = handler.handle(request) + val endDate = Instant.now() + + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 1) { sepTransactionCounter.increment() } + + val expectedSep6Txn = JdbcSep6Transaction() + expectedSep6Txn.kind = kind + expectedSep6Txn.status = PENDING_ANCHOR.toString() + expectedSep6Txn.updatedAt = sep6TxnCapture.captured.updatedAt + + JSONAssert.assertEquals( + gson.toJson(expectedSep6Txn), + gson.toJson(sep6TxnCapture.captured), + JSONCompareMode.STRICT + ) + + val expectedResponse = GetTransactionResponse() + expectedResponse.sep = SEP_6 + expectedResponse.kind = PlatformTransactionData.Kind.from(kind) + expectedResponse.status = PENDING_ANCHOR + expectedResponse.amountExpected = Amount(null, "") + expectedResponse.updatedAt = sep6TxnCapture.captured.updatedAt + expectedResponse.customers = Customers(StellarId(null, null, null), StellarId(null, null, null)) + + JSONAssert.assertEquals( + gson.toJson(expectedResponse), + gson.toJson(response), + JSONCompareMode.STRICT + ) + + val expectedEvent = + AnchorEvent.builder() + .id(anchorEventCapture.captured.id) + .sep(SEP_6.sep.toString()) + .type(TRANSACTION_STATUS_CHANGED) + .transaction(expectedResponse) + .build() + + JSONAssert.assertEquals( + gson.toJson(expectedEvent), + gson.toJson(anchorEventCapture.captured), + JSONCompareMode.STRICT + ) + + assertTrue(expectedSep6Txn.updatedAt >= startDate) + assertTrue(expectedSep6Txn.updatedAt <= endDate) + } } diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/rpc/RequestOnchainFundsHandlerTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/rpc/RequestOnchainFundsHandlerTest.kt index 38eb4d19c8..6b62dea738 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/rpc/RequestOnchainFundsHandlerTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/rpc/RequestOnchainFundsHandlerTest.kt @@ -46,10 +46,13 @@ import org.stellar.anchor.platform.data.JdbcSep6Transaction import org.stellar.anchor.platform.service.AnchorMetrics import org.stellar.anchor.platform.service.Sep24DepositInfoNoneGenerator import org.stellar.anchor.platform.service.Sep24DepositInfoSelfGenerator +import org.stellar.anchor.platform.service.Sep6DepositInfoNoneGenerator +import org.stellar.anchor.platform.service.Sep6DepositInfoSelfGenerator import org.stellar.anchor.platform.validator.RequestValidator import org.stellar.anchor.sep24.Sep24Transaction import org.stellar.anchor.sep24.Sep24TransactionStore import org.stellar.anchor.sep31.Sep31TransactionStore +import org.stellar.anchor.sep6.Sep6Transaction import org.stellar.anchor.sep6.Sep6TransactionStore import org.stellar.anchor.util.GsonUtils @@ -89,6 +92,8 @@ class RequestOnchainFundsHandlerTest { @MockK(relaxed = true) private lateinit var custodyService: CustodyService + @MockK(relaxed = true) private lateinit var sep6DepositInfoGenerator: Sep6DepositInfoNoneGenerator + @MockK(relaxed = true) private lateinit var sep24DepositInfoGenerator: Sep24DepositInfoNoneGenerator @@ -116,6 +121,7 @@ class RequestOnchainFundsHandlerTest { assetService, custodyService, custodyConfig, + sep6DepositInfoGenerator, sep24DepositInfoGenerator, eventService, metricsService @@ -664,6 +670,7 @@ class RequestOnchainFundsHandlerTest { assetService, custodyService, custodyConfig, + sep6DepositInfoGenerator, sep24DepositInfoGenerator, eventService, metricsService @@ -1092,6 +1099,7 @@ class RequestOnchainFundsHandlerTest { assetService, custodyService, custodyConfig, + sep6DepositInfoGenerator, sep24DepositInfoGenerator, eventService, metricsService @@ -1221,12 +1229,16 @@ class RequestOnchainFundsHandlerTest { txn6.requestAssetCode = STELLAR_USDC_CODE txn6.requestAssetIssuer = STELLAR_USDC_ISSUER val sep6TxnCapture = slot() + val sep6CustodyTxnCapture = slot() val anchorEventCapture = slot() every { txn6Store.findByTransactionId(TX_ID) } returns txn6 every { txn24Store.findByTransactionId(any()) } returns null every { txn31Store.findByTransactionId(any()) } returns null every { txn6Store.save(capture(sep6TxnCapture)) } returns null + every { custodyConfig.isCustodyIntegrationEnabled } returns true + every { custodyConfig.type } returns NONE + every { custodyService.createTransaction(capture(sep6CustodyTxnCapture)) } just Runs every { eventSession.publish(capture(anchorEventCapture)) } just Runs every { metricsService.counter(AnchorMetrics.PLATFORM_RPC_TRANSACTION, "SEP", "sep6") } returns sepTransactionCounter @@ -1255,7 +1267,6 @@ class RequestOnchainFundsHandlerTest { expectedSep6Txn.amountExpected = "1" expectedSep6Txn.memo = TEXT_MEMO expectedSep6Txn.memoType = TEXT_MEMO_TYPE - expectedSep6Txn.toAccount = DESTINATION_ACCOUNT expectedSep6Txn.withdrawAnchorAccount = DESTINATION_ACCOUNT JSONAssert.assertEquals( @@ -1264,6 +1275,12 @@ class RequestOnchainFundsHandlerTest { JSONCompareMode.STRICT ) + JSONAssert.assertEquals( + gson.toJson(expectedSep6Txn), + gson.toJson(sep6CustodyTxnCapture.captured), + JSONCompareMode.STRICT + ) + val expectedResponse = GetTransactionResponse() expectedResponse.sep = SEP_6 expectedResponse.kind = PlatformTransactionData.Kind.from(kind) @@ -1275,7 +1292,122 @@ class RequestOnchainFundsHandlerTest { expectedResponse.updatedAt = sep6TxnCapture.captured.updatedAt expectedResponse.memo = TEXT_MEMO expectedResponse.memoType = TEXT_MEMO_TYPE - expectedResponse.destinationAccount = DESTINATION_ACCOUNT + expectedResponse.customers = Customers(StellarId(null, null, null), StellarId(null, null, null)) + + JSONAssert.assertEquals( + gson.toJson(expectedResponse), + gson.toJson(response), + JSONCompareMode.STRICT + ) + + val expectedEvent = + AnchorEvent.builder() + .id(anchorEventCapture.captured.id) + .sep(SEP_6.sep.toString()) + .type(TRANSACTION_STATUS_CHANGED) + .transaction(expectedResponse) + .build() + + JSONAssert.assertEquals( + gson.toJson(expectedEvent), + gson.toJson(anchorEventCapture.captured), + JSONCompareMode.STRICT + ) + + assertTrue(sep6TxnCapture.captured.updatedAt >= startDate) + assertTrue(sep6TxnCapture.captured.updatedAt <= endDate) + } + + @CsvSource(value = ["withdrawal", "withdrawal-exchange"]) + @ParameterizedTest + fun test_handle_sep6_ok_autogeneratedMemo(kind: String) { + val sep6DepositInfoGenerator: Sep6DepositInfoSelfGenerator = mockk() + this.handler = + RequestOnchainFundsHandler( + txn6Store, + txn24Store, + txn31Store, + requestValidator, + assetService, + custodyService, + custodyConfig, + sep6DepositInfoGenerator, + sep24DepositInfoGenerator, + eventService, + metricsService + ) + + val request = + RequestOnchainFundsRequest.builder() + .transactionId(TX_ID) + .amountIn(AmountAssetRequest("1", STELLAR_USDC)) + .amountOut(AmountAssetRequest("0.9", FIAT_USD)) + .amountFee(AmountAssetRequest("0.1", STELLAR_USDC)) + .amountExpected(AmountRequest("1")) + .build() + val txn6 = JdbcSep6Transaction() + txn6.status = INCOMPLETE.toString() + txn6.kind = kind + txn6.requestAssetCode = STELLAR_USDC_CODE + txn6.requestAssetIssuer = STELLAR_USDC_ISSUER + val sep6TxnCapture = slot() + val anchorEventCapture = slot() + val depositInfo = SepDepositInfo(DESTINATION_ACCOUNT_2, TEXT_MEMO_2, TEXT_MEMO_TYPE) + + every { txn6Store.findByTransactionId(TX_ID) } returns txn6 + every { txn24Store.findByTransactionId(any()) } returns null + every { txn31Store.findByTransactionId(any()) } returns null + every { txn6Store.save(capture(sep6TxnCapture)) } returns null + every { custodyConfig.isCustodyIntegrationEnabled } returns false + every { custodyConfig.type } returns NONE + every { sep6DepositInfoGenerator.generate(ofType(Sep6Transaction::class)) } returns depositInfo + every { eventSession.publish(capture(anchorEventCapture)) } just Runs + every { metricsService.counter(AnchorMetrics.PLATFORM_RPC_TRANSACTION, "SEP", "sep6") } returns + sepTransactionCounter + + val startDate = Instant.now() + val response = handler.handle(request) + val endDate = Instant.now() + + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 0) { custodyService.createTransaction(ofType(Sep6Transaction::class)) } + verify(exactly = 1) { sepTransactionCounter.increment() } + + val expectedSep6Txn = JdbcSep6Transaction() + expectedSep6Txn.kind = kind + expectedSep6Txn.status = PENDING_USR_TRANSFER_START.toString() + expectedSep6Txn.updatedAt = sep6TxnCapture.captured.updatedAt + expectedSep6Txn.requestAssetCode = STELLAR_USDC_CODE + expectedSep6Txn.requestAssetIssuer = STELLAR_USDC_ISSUER + expectedSep6Txn.amountIn = "1" + expectedSep6Txn.amountInAsset = STELLAR_USDC + expectedSep6Txn.amountOut = "0.9" + expectedSep6Txn.amountOutAsset = FIAT_USD + expectedSep6Txn.amountFee = "0.1" + expectedSep6Txn.amountFeeAsset = STELLAR_USDC + expectedSep6Txn.amountExpected = "1" + expectedSep6Txn.memo = TEXT_MEMO_2 + expectedSep6Txn.memoType = TEXT_MEMO_TYPE + expectedSep6Txn.withdrawAnchorAccount = DESTINATION_ACCOUNT_2 + + JSONAssert.assertEquals( + gson.toJson(expectedSep6Txn), + gson.toJson(sep6TxnCapture.captured), + JSONCompareMode.STRICT + ) + + val expectedResponse = GetTransactionResponse() + expectedResponse.sep = SEP_6 + expectedResponse.kind = PlatformTransactionData.Kind.from(kind) + expectedResponse.status = PENDING_USR_TRANSFER_START + expectedResponse.amountIn = Amount("1", STELLAR_USDC) + expectedResponse.amountOut = Amount("0.9", FIAT_USD) + expectedResponse.amountFee = Amount("0.1", STELLAR_USDC) + expectedResponse.amountExpected = Amount("1", STELLAR_USDC) + expectedResponse.updatedAt = sep6TxnCapture.captured.updatedAt + expectedResponse.memo = TEXT_MEMO_2 + expectedResponse.memoType = TEXT_MEMO_TYPE expectedResponse.customers = Customers(StellarId(null, null, null), StellarId(null, null, null)) JSONAssert.assertEquals( @@ -1354,7 +1486,6 @@ class RequestOnchainFundsHandlerTest { expectedSep6Txn.amountExpected = "1" expectedSep6Txn.memo = TEXT_MEMO expectedSep6Txn.memoType = TEXT_MEMO_TYPE - expectedSep6Txn.toAccount = DESTINATION_ACCOUNT expectedSep6Txn.withdrawAnchorAccount = DESTINATION_ACCOUNT JSONAssert.assertEquals( @@ -1374,7 +1505,6 @@ class RequestOnchainFundsHandlerTest { expectedResponse.updatedAt = sep6TxnCapture.captured.updatedAt expectedResponse.memo = TEXT_MEMO expectedResponse.memoType = TEXT_MEMO_TYPE - expectedResponse.destinationAccount = DESTINATION_ACCOUNT expectedResponse.customers = Customers(StellarId(null, null, null), StellarId(null, null, null)) JSONAssert.assertEquals( @@ -1458,7 +1588,6 @@ class RequestOnchainFundsHandlerTest { expectedSep6Txn.amountExpected = "1" expectedSep6Txn.memo = TEXT_MEMO expectedSep6Txn.memoType = TEXT_MEMO_TYPE - expectedSep6Txn.toAccount = DESTINATION_ACCOUNT expectedSep6Txn.withdrawAnchorAccount = DESTINATION_ACCOUNT JSONAssert.assertEquals( @@ -1478,7 +1607,6 @@ class RequestOnchainFundsHandlerTest { expectedResponse.updatedAt = sep6TxnCapture.captured.updatedAt expectedResponse.memo = TEXT_MEMO expectedResponse.memoType = TEXT_MEMO_TYPE - expectedResponse.destinationAccount = DESTINATION_ACCOUNT expectedResponse.customers = Customers(StellarId(null, null, null), StellarId(null, null, null)) JSONAssert.assertEquals( @@ -1504,4 +1632,54 @@ class RequestOnchainFundsHandlerTest { assertTrue(sep6TxnCapture.captured.updatedAt >= startDate) assertTrue(sep6TxnCapture.captured.updatedAt <= endDate) } + + @CsvSource(value = ["withdrawal", "withdrawal-exchange"]) + @ParameterizedTest + fun test_handle_sep6_notNoneGenerator(kind: String) { + val sep6DepositInfoGenerator: Sep6DepositInfoSelfGenerator = mockk() + this.handler = + RequestOnchainFundsHandler( + txn6Store, + txn24Store, + txn31Store, + requestValidator, + assetService, + custodyService, + custodyConfig, + sep6DepositInfoGenerator, + sep24DepositInfoGenerator, + eventService, + metricsService + ) + + val request = + RequestOnchainFundsRequest.builder() + .transactionId(TX_ID) + .memo(TEXT_MEMO) + .memoType(TEXT_MEMO_TYPE) + .amountIn(AmountAssetRequest("1", STELLAR_USDC)) + .amountOut(AmountAssetRequest("1", FIAT_USD)) + .amountFee(AmountAssetRequest("1", STELLAR_USDC)) + .build() + val txn6 = JdbcSep6Transaction() + txn6.status = INCOMPLETE.toString() + txn6.kind = kind + val sep6TxnCapture = slot() + + every { txn6Store.findByTransactionId(TX_ID) } returns txn6 + every { txn24Store.findByTransactionId(any()) } returns null + every { txn31Store.findByTransactionId(any()) } returns null + every { txn24Store.save(capture(sep6TxnCapture)) } returns null + + val ex = assertThrows { handler.handle(request) } + assertEquals( + "Anchor is not configured to accept memo, memo_type and destination_account", + ex.message + ) + + verify(exactly = 0) { txn6Store.save(any()) } + verify(exactly = 0) { txn24Store.save(any()) } + verify(exactly = 0) { txn31Store.save(any()) } + verify(exactly = 0) { sepTransactionCounter.increment() } + } } diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/service/CustodyServiceTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/service/CustodyServiceTest.kt index cf53c2ffcc..0fe4a59846 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/service/CustodyServiceTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/service/CustodyServiceTest.kt @@ -1,17 +1,14 @@ package org.stellar.anchor.platform.service -import io.mockk.MockKAnnotations -import io.mockk.Runs -import io.mockk.every +import io.mockk.* import io.mockk.impl.annotations.MockK -import io.mockk.just -import io.mockk.slot -import io.mockk.verify import java.util.* import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource import org.skyscreamer.jsonassert.JSONAssert import org.skyscreamer.jsonassert.JSONCompareMode import org.stellar.anchor.api.custody.CreateCustodyTransactionRequest @@ -26,6 +23,7 @@ import org.stellar.anchor.custody.CustodyService import org.stellar.anchor.platform.apiclient.CustodyApiClient import org.stellar.anchor.platform.data.JdbcSep24Transaction import org.stellar.anchor.platform.data.JdbcSep31Transaction +import org.stellar.anchor.platform.data.JdbcSep6Transaction import org.stellar.anchor.util.GsonUtils class CustodyServiceTest { @@ -45,6 +43,46 @@ class CustodyServiceTest { custodyService = CustodyServiceImpl(Optional.of(custodyApiClient)) } + @CsvSource(value = ["deposit", "deposit-exchange"]) + @ParameterizedTest + fun test_createTransaction_sep6Deposit(kind: String) { + val txn = + gson.fromJson(sep6DepositEntity, JdbcSep6Transaction::class.java).apply { this.kind = kind } + val requestCapture = slot() + + every { custodyApiClient.createTransaction(capture(requestCapture)) } just Runs + + custodyService.createTransaction(txn) + + JSONAssert.assertEquals( + sep6DepositRequest.replace("testKind", kind), + gson.toJson(requestCapture.captured), + JSONCompareMode.STRICT + ) + } + + @CsvSource(value = ["withdrawal", "withdrawal-exchange"]) + @ParameterizedTest + fun test_createTransaction_sep6Withdrawal(kind: String) { + val txn = + gson.fromJson(sep6WithdrawalEntity, JdbcSep6Transaction::class.java).apply { + this.kind = kind + } + val requestCapture = slot() + + every { custodyApiClient.createTransaction(capture(requestCapture)) } just Runs + + custodyService.createTransaction(txn) + + println(gson.toJson(requestCapture.captured)) + + JSONAssert.assertEquals( + sep6WithdrawalRequest.replace("testKind", kind), + gson.toJson(requestCapture.captured), + JSONCompareMode.STRICT + ) + } + @Test fun test_createTransaction_sep24Deposit() { val txn = gson.fromJson(sep24DepositEntity, JdbcSep24Transaction::class.java) @@ -188,6 +226,110 @@ class CustodyServiceTest { Assertions.assertEquals("Forbidden", exception.rawMessage) } + private val sep6DepositEntity = + """ + { + "id" : "testId", + "stellar_transaction_id": "testStellarTransactionId", + "external_transaction_id": "testExternalTransactionId", + "status": "pending_anchor", + "kind": "deposit", + "started_at": "2022-04-18T14:00:00.000Z", + "completed_at": "2022-04-18T14:00:00.000Z", + "updated_at": "2022-04-18T14:00:00.000Z", + "transfer_received_at": "2022-04-18T14:00:00.000Z", + "type": "SWIFT", + "requestAssetCode": "testRequestAssetCode", + "requestAssetIssuer": "testRequestAssetIssuer", + "amount_in": "testAmountIn", + "amount_in_asset": "testAmountInAsset", + "amount_out": "testAmountOut", + "amount_out_asset": "testAmountOutAsset", + "amount_fee": "testAmountFee", + "amount_fee_asset": "testAmountFeeAsset", + "amount_expected": "testAmountExpected", + "sep10_account": "testSep10Account", + "sep10_account_memo": "testSep10AccountMemo", + "from_account": "testFromAccount", + "to_account": "testToAccount", + "memo": "testMemo", + "memo_type": "testMemoType", + "quote_id": "testQuoteId", + "message": "testMessage", + "refundMemo": "testRefundMemo", + "refundMemoType": "testRefundMemoType" + } + """ + .trimIndent() + + private val sep6DepositRequest = + """ + { + "id": "testId", + "memo": "testMemo", + "memoType": "testMemoType", + "protocol": "6", + "toAccount": "testToAccount", + "amount": "testAmountOut", + "asset": "testAmountOutAsset", + "kind": "testKind" + } + """ + .trimIndent() + + private val sep6WithdrawalEntity = + """ + { + "id": "testId", + "stellar_transaction_id": "testStellarTransactionId", + "external_transaction_id": "testExternalTransactionId", + "status": "pending_anchor", + "kind": "withdrawal", + "started_at": "2022-04-18T14:00:00.000Z", + "completed_at": "2022-04-18T14:00:00.000Z", + "updated_at": "2022-04-18T14:00:00.000Z", + "transfer_received_at": "2022-04-18T14:00:00.000Z", + "type": "bank_account", + "requestAssetCode": "testRequestAssetCode", + "requestAssetIssuer": "testRequestAssetIssuer", + "amount_in": "testAmountIn", + "amount_in_asset": "testAmountInAsset", + "amount_out": "testAmountOut", + "amount_out_asset": "testAmountOutAsset", + "amount_fee": "testAmountFee", + "amount_fee_asset": "testAmountFeeAsset", + "amount_expected": "testAmountExpected", + "sep10_account": "testSep10Account", + "sep10_account_memo": "testSep10AccountMemo", + "withdraw_anchor_account": "testWithdrawAnchorAccount", + "from_account": "testFromAccount", + "to_account": "testToAccount", + "memo": "testMemo", + "memo_type": "testMemoType", + "quote_id": "testQuoteId", + "message": "testMessage", + "refundMemo": "testRefundMemo", + "refundMemoType": "testRefundMemoType" + } + """ + .trimIndent() + + private val sep6WithdrawalRequest = + """ + { + "id": "testId", + "memo": "testMemo", + "memoType": "testMemoType", + "protocol": "6", + "fromAccount": "testFromAccount", + "toAccount": "testWithdrawAnchorAccount", + "amount": "testAmountExpected", + "asset": "testAmountInAsset", + "kind": "testKind" + } + """ + .trimIndent() + private val sep24DepositEntity = """ { diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/service/Sep6DepositInfoCustodyGeneratorTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/service/Sep6DepositInfoCustodyGeneratorTest.kt new file mode 100644 index 0000000000..d241f1ab41 --- /dev/null +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/service/Sep6DepositInfoCustodyGeneratorTest.kt @@ -0,0 +1,44 @@ +package org.stellar.anchor.platform.service + +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import kotlin.test.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.stellar.anchor.api.custody.GenerateDepositAddressResponse +import org.stellar.anchor.api.shared.SepDepositInfo +import org.stellar.anchor.platform.apiclient.CustodyApiClient +import org.stellar.anchor.platform.data.JdbcSep6Transaction + +class Sep6DepositInfoCustodyGeneratorTest { + companion object { + private const val ADDRESS = "testAccount" + private const val MEMO = "MTIzZTQ1NjctZTg5Yi0xMmQzLWE0NTYtNDI2NjE0MTc=" + private const val MEMO_TYPE = "hash" + private const val ASSET_ID = "USDC" + } + + @MockK(relaxed = true) lateinit var custodyApiClient: CustodyApiClient + + private lateinit var generator: Sep6DepositInfoCustodyGenerator + + @BeforeEach + fun setup() { + MockKAnnotations.init(this, relaxUnitFun = true) + generator = Sep6DepositInfoCustodyGenerator(custodyApiClient) + } + + @Test + fun test_sep6_custodyGenerator_success() { + val txn = JdbcSep6Transaction() + txn.amountInAsset = ASSET_ID + + every { custodyApiClient.generateDepositAddress(ASSET_ID) } returns + GenerateDepositAddressResponse(ADDRESS, MEMO, MEMO_TYPE) + + val result = generator.generate(txn) + val expected = SepDepositInfo(ADDRESS, MEMO, MEMO_TYPE) + assertEquals(expected, result) + } +} diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/service/Sep6DepositInfoSelfGeneratorTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/service/Sep6DepositInfoSelfGeneratorTest.kt new file mode 100644 index 0000000000..6edb815058 --- /dev/null +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/service/Sep6DepositInfoSelfGeneratorTest.kt @@ -0,0 +1,49 @@ +package org.stellar.anchor.platform.service + +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.mockk +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.stellar.anchor.api.sep.AssetInfo +import org.stellar.anchor.api.shared.SepDepositInfo +import org.stellar.anchor.asset.AssetService +import org.stellar.anchor.platform.data.JdbcSep6Transaction + +class Sep6DepositInfoSelfGeneratorTest { + + companion object { + private val TXN_ID = "testId" + private const val ASSET_CODE = "USDC" + private const val ASSET_ISSUER = "testIssuer" + private const val DISTRIBUTION_ACCOUNT = "testAccount" + } + + @MockK(relaxed = true) lateinit var assetService: AssetService + + private lateinit var generator: Sep6DepositInfoSelfGenerator + + @BeforeEach + fun setup() { + MockKAnnotations.init(this, relaxUnitFun = true) + val asset = mockk() + every { asset.distributionAccount } returns DISTRIBUTION_ACCOUNT + every { assetService.getAsset(ASSET_CODE, ASSET_ISSUER) } returns asset + generator = Sep6DepositInfoSelfGenerator(assetService) + } + + @Test + fun test_sep6_custodyGenerator_success() { + val txn = JdbcSep6Transaction() + txn.id = TXN_ID + txn.requestAssetCode = ASSET_CODE + txn.requestAssetIssuer = ASSET_ISSUER + + val result = generator.generate(txn) + val expected = + SepDepositInfo(DISTRIBUTION_ACCOUNT, "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDB0ZXN0SWQ=", "hash") + assertEquals(expected, result) + } +} diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/service/TransactionServiceTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/service/TransactionServiceTest.kt index aae5fae60d..ab55f9a89e 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/service/TransactionServiceTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/service/TransactionServiceTest.kt @@ -1,13 +1,15 @@ package org.stellar.anchor.platform.service -import io.mockk.* +import io.mockk.MockKAnnotations +import io.mockk.every import io.mockk.impl.annotations.MockK -import java.util.* +import io.mockk.verify import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource import org.junit.jupiter.params.provider.EnumSource import org.junit.jupiter.params.provider.NullSource import org.junit.jupiter.params.provider.ValueSource @@ -22,6 +24,8 @@ import org.stellar.anchor.api.platform.PlatformTransactionData import org.stellar.anchor.api.sep.SepTransactionStatus import org.stellar.anchor.api.sep.sep38.RateFee import org.stellar.anchor.api.shared.Amount +import org.stellar.anchor.api.shared.RefundPayment +import org.stellar.anchor.api.shared.Refunds import org.stellar.anchor.asset.AssetService import org.stellar.anchor.asset.DefaultAssetService import org.stellar.anchor.config.CustodyConfig @@ -35,6 +39,8 @@ import org.stellar.anchor.sep24.Sep24Transaction import org.stellar.anchor.sep24.Sep24TransactionStore import org.stellar.anchor.sep31.Sep31TransactionStore import org.stellar.anchor.sep38.Sep38QuoteStore +import org.stellar.anchor.sep6.Sep6DepositInfoGenerator +import org.stellar.anchor.sep6.Sep6Transaction import org.stellar.anchor.sep6.Sep6TransactionStore import org.stellar.anchor.util.GsonUtils @@ -57,6 +63,7 @@ class TransactionServiceTest { @MockK(relaxed = true) private lateinit var assetService: AssetService @MockK(relaxed = true) private lateinit var eventService: EventService @MockK(relaxed = true) private lateinit var eventSession: Session + @MockK(relaxed = true) private lateinit var sep6DepositInfoGenerator: Sep6DepositInfoGenerator @MockK(relaxed = true) private lateinit var sep24DepositInfoGenerator: Sep24DepositInfoGenerator @MockK(relaxed = true) private lateinit var custodyService: CustodyService @MockK(relaxed = true) private lateinit var custodyConfig: CustodyConfig @@ -75,6 +82,7 @@ class TransactionServiceTest { sep38QuoteStore, assetService, eventService, + sep6DepositInfoGenerator, sep24DepositInfoGenerator, custodyService, custodyConfig @@ -143,6 +151,27 @@ class TransactionServiceTest { ) } + @Test + fun `test get SEP6 transaction`() { + // Mock the store + every { sep31TransactionStore.findByTransactionId(any()) } returns null + every { sep24TransactionStore.findByTransactionId(any()) } returns null + every { sep6TransactionStore.newInstance() } returns JdbcSep6Transaction() + every { sep6TransactionStore.newRefunds() } returns Refunds() + every { sep6TransactionStore.newRefundPayment() } answers { RefundPayment() } + + val mockSep6Transaction = gson.fromJson(jsonSep6Transaction, JdbcSep6Transaction::class.java) + + every { sep6TransactionStore.findByTransactionId(TEST_TXN_ID) } returns mockSep6Transaction + val gotGetTransactionResponse = transactionService.findTransaction(TEST_TXN_ID) + + JSONAssert.assertEquals( + wantedGetSep6TransactionResponse, + gson.toJson(gotGetTransactionResponse), + LENIENT + ) + } + @Test fun test_validateAsset_failure() { // fails if amount_in.amount is null @@ -207,6 +236,7 @@ class TransactionServiceTest { sep38QuoteStore, assetService, eventService, + sep6DepositInfoGenerator, sep24DepositInfoGenerator, custodyService, custodyConfig @@ -380,6 +410,138 @@ class TransactionServiceTest { verify(exactly = 1) { eventSession.publish(any()) } } + @CsvSource(value = ["deposit", "deposit-exchange"]) + @ParameterizedTest + fun test_patchTransaction_sep6DepositPendingUserTransferStart(kind: String) { + val txId = "testTxId" + val tx = JdbcSep6Transaction() + tx.status = SepTransactionStatus.INCOMPLETE.toString() + tx.kind = kind + val data = PlatformTransactionData() + data.id = txId + data.memo = "12345" + data.memoType = "id" + data.status = SepTransactionStatus.PENDING_USR_TRANSFER_START + val request = + PatchTransactionsRequest.builder().records(listOf(PatchTransactionRequest(data))).build() + + every { sep31TransactionStore.findByTransactionId(any()) } returns null + every { sep6TransactionStore.findByTransactionId(any()) } returns tx + every { sep24TransactionStore.findByTransactionId(any()) } returns null + + transactionService.patchTransactions(request) + + verify(exactly = 0) { custodyService.createTransaction(ofType(Sep24Transaction::class)) } + verify(exactly = 1) { sep6TransactionStore.save(any()) } + verify(exactly = 1) { eventSession.publish(any()) } + } + + @CsvSource(value = ["withdrawal", "withdrawal-exchange"]) + @ParameterizedTest + fun test_patchTransaction_sep6WithdrawalPendingAnchor(kind: String) { + val txId = "testTxId" + val tx = JdbcSep6Transaction() + tx.status = SepTransactionStatus.INCOMPLETE.toString() + tx.kind = kind + val data = PlatformTransactionData() + data.id = txId + data.memo = "12345" + data.memoType = "id" + data.status = SepTransactionStatus.PENDING_ANCHOR + val request = + PatchTransactionsRequest.builder().records(listOf(PatchTransactionRequest(data))).build() + + every { sep31TransactionStore.findByTransactionId(any()) } returns null + every { sep6TransactionStore.findByTransactionId(any()) } returns tx + every { sep24TransactionStore.findByTransactionId(any()) } returns null + + transactionService.patchTransactions(request) + + verify(exactly = 0) { custodyService.createTransaction(ofType(Sep24Transaction::class)) } + verify(exactly = 1) { sep6TransactionStore.save(any()) } + verify(exactly = 1) { eventSession.publish(any()) } + } + + @CsvSource(value = ["deposit", "deposit-exchange"]) + @ParameterizedTest + fun test_patchTransaction_sep6DepositPendingAnchor(kind: String) { + val txId = "testTxId" + val tx = JdbcSep6Transaction() + tx.status = SepTransactionStatus.INCOMPLETE.toString() + tx.kind = kind + val data = PlatformTransactionData() + data.id = txId + data.memo = "12345" + data.memoType = "id" + data.status = SepTransactionStatus.PENDING_ANCHOR + val request = + PatchTransactionsRequest.builder().records(listOf(PatchTransactionRequest(data))).build() + + every { sep31TransactionStore.findByTransactionId(any()) } returns null + every { sep6TransactionStore.findByTransactionId(any()) } returns tx + every { sep24TransactionStore.findByTransactionId(any()) } returns null + every { custodyConfig.isCustodyIntegrationEnabled } returns true + + transactionService.patchTransactions(request) + + verify(exactly = 1) { custodyService.createTransaction(ofType(Sep6Transaction::class)) } + verify(exactly = 1) { sep6TransactionStore.save(any()) } + verify(exactly = 1) { eventSession.publish(any()) } + } + + @CsvSource(value = ["withdrawal", "withdrawal-exchange"]) + @ParameterizedTest + fun test_patchTransaction_sep6WithdrawalPendingUserTransferStart(kind: String) { + val txId = "testTxId" + val tx = JdbcSep6Transaction() + tx.status = SepTransactionStatus.INCOMPLETE.toString() + tx.kind = kind + val data = PlatformTransactionData() + data.id = txId + data.memo = "12345" + data.memoType = "id" + data.status = SepTransactionStatus.PENDING_USR_TRANSFER_START + val request = + PatchTransactionsRequest.builder().records(listOf(PatchTransactionRequest(data))).build() + + every { sep31TransactionStore.findByTransactionId(any()) } returns null + every { sep6TransactionStore.findByTransactionId(any()) } returns tx + every { sep24TransactionStore.findByTransactionId(any()) } returns null + every { custodyConfig.isCustodyIntegrationEnabled } returns true + + transactionService.patchTransactions(request) + + verify(exactly = 1) { custodyService.createTransaction(ofType(Sep6Transaction::class)) } + verify(exactly = 1) { sep6TransactionStore.save(any()) } + verify(exactly = 1) { eventSession.publish(any()) } + } + + @CsvSource(value = ["withdrawal", "withdrawal-exchange"]) + @ParameterizedTest + fun test_patchTransaction_sep6WithdrawalPendingUserTransferStart_statusNotChanged(kind: String) { + val txId = "testTxId" + val tx = JdbcSep6Transaction() + tx.status = SepTransactionStatus.PENDING_USR_TRANSFER_START.toString() + tx.kind = kind + val data = PlatformTransactionData() + data.id = txId + data.memo = "12345" + data.memoType = "id" + data.status = SepTransactionStatus.PENDING_USR_TRANSFER_START + val request = + PatchTransactionsRequest.builder().records(listOf(PatchTransactionRequest(data))).build() + + every { sep31TransactionStore.findByTransactionId(any()) } returns null + every { sep6TransactionStore.findByTransactionId(any()) } returns tx + every { sep24TransactionStore.findByTransactionId(any()) } returns null + + transactionService.patchTransactions(request) + + verify(exactly = 0) { custodyService.createTransaction(ofType(Sep24Transaction::class)) } + verify(exactly = 1) { sep6TransactionStore.save(any()) } + verify(exactly = 1) { eventSession.publish(any()) } + } + @Test fun test_updateSep31Transaction() { val quoteId = "my-quote-id" @@ -488,6 +650,7 @@ class TransactionServiceTest { sep38QuoteStore, assetService, eventService, + sep6DepositInfoGenerator, sep24DepositInfoGenerator, custodyService, custodyConfig @@ -538,6 +701,46 @@ class TransactionServiceTest { assertTrue(testSep31Transaction.updatedAt > testSep31Transaction.startedAt) } + private val jsonSep6Transaction = + """ + { + "id": "069364b1-f9f1-464f-8da2-5c36f9aad1a6", + "kind": "deposit", + "status": "completed", + "amount_in": "1", + "amount_in_asset": "iso4217:USD", + "amount_out": "1", + "amount_out_asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP", + "amount_fee": "0", + "amount_fee_asset": "iso4217:USD", + "to": "GDJLBYYKMCXNVVNABOE66NYXQGIA5AC5D223Z2KF6ZEYK4UBCA7FKLTG", + "started_at": "2023-10-31T21:16:29.764842Z", + "updated_at": "2023-10-31T21:16:44.652018Z", + "completed_at": "2023-10-31T21:16:44.652008Z", + "stellar_transaction_id": "a8b7f7ba67a5c63975512aa113c5a177e675c5e195a2e15920b39f5a5a91f306", + "message": "Funds sent to user", + "required_customer_info_updates": [ + "id_type", + "id_country_code", + "id_issue_date", + "id_expiration_date", + "id_number", + "address" + ], + "instructions": { + "organization.bank_number": { + "value": "121122676", + "description": "US Bank routing number" + }, + "organization.bank_account_number": { + "value": "13719713158835300", + "description": "US Bank account number" + } + } + } + """ + .trimIndent() + private val jsonSep24Transaction = """ { @@ -860,6 +1063,47 @@ class TransactionServiceTest { """ .trimIndent() + private val wantedGetSep6TransactionResponse = + """ + { + "id": "069364b1-f9f1-464f-8da2-5c36f9aad1a6", + "sep": "6", + "kind": "deposit", + "status": "completed", + "amount_expected": { "asset": "" }, + "amount_in": { "amount": "1", "asset": "iso4217:USD" }, + "amount_out": { + "amount": "1", + "asset": "stellar:USDC:GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP" + }, + "amount_fee": { "amount": "0", "asset": "iso4217:USD" }, + "started_at": "2023-10-31T21:16:29.764842Z", + "updated_at": "2023-10-31T21:16:44.652018Z", + "completed_at": "2023-10-31T21:16:44.652008Z", + "message": "Funds sent to user", + "customers": { "sender": {}, "receiver": {} }, + "required_customer_info_updates": [ + "id_type", + "id_country_code", + "id_issue_date", + "id_expiration_date", + "id_number", + "address" + ], + "instructions": { + "organization.bank_number": { + "value": "121122676", + "description": "US Bank routing number" + }, + "organization.bank_account_number": { + "value": "13719713158835300", + "description": "US Bank account number" + } + } + } + """ + .trimIndent() + @Test fun `patch transaction with bad body`() { var patchTransactionsRequest = PatchTransactionsRequest.builder().records(null).build() diff --git a/service-runner/src/main/resources/profiles/default-custody-rpc/config.env b/service-runner/src/main/resources/profiles/default-custody-rpc/config.env index e480fa6e71..5ce0f9026d 100644 --- a/service-runner/src/main/resources/profiles/default-custody-rpc/config.env +++ b/service-runner/src/main/resources/profiles/default-custody-rpc/config.env @@ -42,6 +42,7 @@ sep38.enabled=true sep24.enabled=true sep24.interactive_url.base_url=http://localhost:8091/sep24/interactive sep24.more_info_url.base_url=http://localhost:8091/sep24/transaction/more_info +sep6.deposit_info_generator_type=custody sep24.deposit_info_generator_type=custody sep31.deposit_info_generator_type=custody custody.type=fireblocks diff --git a/service-runner/src/main/resources/profiles/default-custody/config.env b/service-runner/src/main/resources/profiles/default-custody/config.env index e480fa6e71..5ce0f9026d 100644 --- a/service-runner/src/main/resources/profiles/default-custody/config.env +++ b/service-runner/src/main/resources/profiles/default-custody/config.env @@ -42,6 +42,7 @@ sep38.enabled=true sep24.enabled=true sep24.interactive_url.base_url=http://localhost:8091/sep24/interactive sep24.more_info_url.base_url=http://localhost:8091/sep24/transaction/more_info +sep6.deposit_info_generator_type=custody sep24.deposit_info_generator_type=custody sep31.deposit_info_generator_type=custody custody.type=fireblocks