Skip to content

Commit

Permalink
[ANCHOR-508] SEP-6: Custody integration (#1179)
Browse files Browse the repository at this point in the history
### 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](stellar/stellar-docs#253)

### Known limitations

N/A
  • Loading branch information
philipliu authored Nov 2, 2023
1 parent e436286 commit a758fa7
Show file tree
Hide file tree
Showing 43 changed files with 2,206 additions and 350 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/sub_gradle_test_and_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
8 changes: 8 additions & 0 deletions core/src/main/java/org/stellar/anchor/config/Sep6Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public interface Sep6Config {

Features getFeatures();

DepositInfoGeneratorType getDepositInfoGeneratorType();

@Getter
@Setter
@AllArgsConstructor
Expand All @@ -22,4 +24,10 @@ class Features {
@SerializedName("claimable_balances")
boolean claimableBalances;
}

enum DepositInfoGeneratorType {
SELF,
CUSTODY,
NONE
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
22 changes: 2 additions & 20 deletions core/src/main/java/org/stellar/anchor/sep6/Sep6Service.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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());

Expand All @@ -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(
Expand Down Expand Up @@ -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());
Expand All @@ -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)
Expand Down
7 changes: 0 additions & 7 deletions core/src/main/java/org/stellar/anchor/util/MemoHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
Expand Down Expand Up @@ -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()));
}
}
32 changes: 29 additions & 3 deletions core/src/main/java/org/stellar/anchor/util/TransactionHelper.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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())
Expand Down
16 changes: 0 additions & 16 deletions core/src/test/kotlin/org/stellar/anchor/sep6/Sep6ServiceTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -388,10 +388,8 @@ class Sep6ServiceTestData {
val withdrawResJson =
"""
{
"account_id": "GA7FYRB5VREZKOBIIKHG5AVTPFGWUBPOBF7LTYG4GTMFVIOOD2DWAL7I",
"memo_type": "hash"
}
"""
"""
.trimIndent()

val withdrawTxnJson =
Expand All @@ -411,9 +409,7 @@ class Sep6ServiceTestData {
"amountExpected": "100",
"sep10Account": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO",
"sep10AccountMemo": "123",
"withdrawAnchorAccount": "GA7FYRB5VREZKOBIIKHG5AVTPFGWUBPOBF7LTYG4GTMFVIOOD2DWAL7I",
"fromAccount": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO",
"memoType": "hash",
"refundMemo": "some text",
"refundMemoType": "text"
}
Expand Down Expand Up @@ -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": {
Expand Down Expand Up @@ -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"
}
Expand All @@ -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": {
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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": {
Expand Down Expand Up @@ -604,9 +593,7 @@ class Sep6ServiceTestData {
"amountExpected": "100",
"sep10Account": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO",
"sep10AccountMemo": "123",
"withdrawAnchorAccount": "GA7FYRB5VREZKOBIIKHG5AVTPFGWUBPOBF7LTYG4GTMFVIOOD2DWAL7I",
"fromAccount": "GBLGJA4TUN5XOGTV6WO2BWYUI2OZR5GYQ5PDPCRMQ5XEPJOYWB2X4CJO",
"memoType": "hash",
"refundMemo": "some text",
"refundMemoType": "text"
}
Expand Down Expand Up @@ -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": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
Loading

0 comments on commit a758fa7

Please sign in to comment.