Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 3 additions & 8 deletions core/src/main/java/org/stellar/anchor/sep24/Sep24Service.java
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,9 @@ public InteractiveTransactionResponse withdraw(
withdrawRequest.put("client_domain", token.getClientDomain());
else withdrawRequest.remove("client_domain");

Memo memo = makeMemo(withdrawRequest.get("memo"), withdrawRequest.get("memo_type"));
// The memo of a withdrawal request is validated but not stored: the memo the user pays with is
// the one the anchor assigns in request_onchain_funds, never one the client chose.
makeMemo(withdrawRequest.get("memo"), withdrawRequest.get("memo_type"));
Memo refundMemo =
makeMemo(withdrawRequest.get("refund_memo"), withdrawRequest.get("refund_memo_type"));
String txnId = UUID.randomUUID().toString();
Expand All @@ -205,13 +207,6 @@ public InteractiveTransactionResponse withdraw(
.clientName(token.getClientName())
.requestClientIpAddress(withdrawRequest.get("clientIpAddress"));

if (memo != null) {
debug("transaction memo detected.", memo);
debug("Set the transaction memo.", memo);
builder.memo(memo.toString());
builder.memoType(memoTypeString(memoType(memo)));
}

if (refundMemo != null) {
debug("refund memo detected.", refundMemo);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,16 @@ List<Sep24Transaction> findTransactions(
* @return list of transactions
*/
List<? extends Sep24Transaction> findTransactions(TransactionsParams params);

/**
* Find the withdrawals that wait for a payment to the given account with the given memo, in the
* given status. Used to tell whether a memo is already assigned and to route an incoming payment.
*
* @param withdrawAnchorAccount The account the user pays.
* @param memo The memo the user pays with.
* @param status The status the withdrawal must be in.
* @return The matching transactions, empty if none.
*/
List<? extends Sep24Transaction> findAllByWithdrawAnchorAccountAndMemoAndStatus(
String withdrawAnchorAccount, String memo, String status);
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,11 @@ List<? extends Sep31Transaction> findByTransactionIds(@NonNull Collection<String
* @return list of transactions
*/
List<? extends Sep31Transaction> findTransactions(TransactionsParams params);

/**
* Find the transactions that wait for a payment to the given account with the given memo, in the
* given status. Used to tell whether a memo is already assigned and to route an incoming payment.
*/
List<? extends Sep31Transaction> findAllByToAccountAndMemoAndStatus(
String toAccount, String memo, String status);
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,11 @@ List<Sep6Transaction> findTransactions(
Sep6Transaction save(Sep6Transaction sep6Transaction) throws SepException;

List<? extends Sep6Transaction> findTransactions(TransactionsParams params) throws SepException;

/**
* Find the withdrawals that wait for a payment to the given account with the given memo, in the
* given status. Used to tell whether a memo is already assigned and to route an incoming payment.
*/
List<? extends Sep6Transaction> findAllByWithdrawAnchorAccountAndMemoAndStatus(
String withdrawAnchorAccount, String memo, String status);
}
68 changes: 68 additions & 0 deletions core/src/test/kotlin/org/stellar/anchor/sep24/Sep24ServiceTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import org.stellar.anchor.TestConstants.Companion.TEST_TRANSACTION_ID_1
import org.stellar.anchor.TestHelper
import org.stellar.anchor.api.asset.StellarAssetInfo
import org.stellar.anchor.api.exception.*
import org.stellar.anchor.api.sep.sep24.DepositTransactionResponse
import org.stellar.anchor.api.sep.sep24.GetTransactionRequest
import org.stellar.anchor.api.sep.sep24.GetTransactionsRequest
import org.stellar.anchor.api.shared.FeeDescription
Expand Down Expand Up @@ -341,6 +342,73 @@ internal class Sep24ServiceTest {
verify { Log.infoF("Sep24.deposit. account={}, memo={}", shortenedAccount, "***") }
}

@Test
fun `test withdraw ignores the client memo and stores none`() {
// A valid memo of each type the request accepts: none of them reaches the stored row.
val memos =
listOf(
"77713371337" to "id",
"client memo" to "text",
"MTIzZTQ1NjctZTg5Yi0xMmQzLWE0NTYtNDI2NjE0MTc=" to "hash",
)
for ((memo, memoType) in memos) {
val slotTxn = slot<Sep24Transaction>()
every { txnStore.save(capture(slotTxn)) } returns null
val request = createTestTransactionRequest()
request["memo"] = memo
request["memo_type"] = memoType

sep24Service.withdraw(createTestWebAuthJwt(), request)

assertNull(slotTxn.captured.memo, "memo of type $memoType")
assertNull(slotTxn.captured.memoType, "memo_type of type $memoType")
val response = Sep24Helper.fromWithdrawTxn(slotTxn.captured, moreInfoUrlConstructor, "en")
assertNull(response.withdrawMemo, "withdraw_memo of type $memoType")
assertNull(response.withdrawMemoType, "withdraw_memo_type of type $memoType")
}
verify(exactly = memos.size) { txnStore.save(any()) }
}

@Test
fun `test deposit still stores the client memo as the deposit memo`() {
val slotTxn = slot<Sep24Transaction>()
every { txnStore.save(capture(slotTxn)) } returns null
val request = createTestTransactionRequest()
request["memo"] = "77713371337"
request["memo_type"] = "id"

sep24Service.deposit(createTestWebAuthJwt(), request)

assertEquals("77713371337", slotTxn.captured.memo)
assertEquals("id", slotTxn.captured.memoType)
val response = Sep24Helper.fromDepositTxn(slotTxn.captured, moreInfoUrlConstructor, "en")
val deposit = response as DepositTransactionResponse
assertEquals("77713371337", deposit.depositMemo)
assertEquals("id", deposit.depositMemoType)
}

@Test
fun `test withdraw still rejects an invalid memo with 400`() {
val missingType = createTestTransactionRequest()
missingType["memo"] = "77713371337"
val missingTypeEx =
assertThrows<SepValidationException> {
sep24Service.withdraw(createTestWebAuthJwt(), missingType)
}
assertEquals("memo_type is required if memo is specified", missingTypeEx.message)

val notANumber = createTestTransactionRequest()
notANumber["memo"] = "not-a-number"
notANumber["memo_type"] = "id"
val notANumberEx =
assertThrows<SepValidationException> {
sep24Service.withdraw(createTestWebAuthJwt(), notANumber)
}
assertEquals("Invalid memo not-a-number of type: MEMO_ID", notANumberEx.message)

verify(exactly = 0) { txnStore.save(any()) }
}

@Test
fun `test withdraw with quote_id`() {
val slotTxn = slot<Sep24Transaction>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import org.stellar.anchor.api.event.AnchorEvent.Type.TRANSACTION_CREATED
import org.stellar.anchor.api.event.AnchorEvent.Type.TRANSACTION_STATUS_CHANGED
import org.stellar.anchor.api.sep.SepTransactionStatus
import org.stellar.anchor.api.sep.sep24.Sep24GetTransactionResponse
import org.stellar.anchor.api.sep.sep24.WithdrawTransactionResponse
import org.stellar.anchor.auth.JwtService
import org.stellar.anchor.auth.Sep24InteractiveUrlJwt
import org.stellar.anchor.client.Sep24Client
Expand Down Expand Up @@ -338,10 +339,14 @@ open class Sep24End2EndTests : IntegrationTestBase(TestConfig()) {
wallet.sep24,
)

// Submit transfer transaction
// Submit transfer transaction. The memo is assigned by the anchor; the memo the wallet asked
// for is ignored, so the wallet pays the withdraw_memo read back from the transaction.
val withdrawTxn = wallet.sep24.getTransaction(response.id, "USDC")
val withdrawal = withdrawTxn.transaction as WithdrawTransactionResponse
assertNotNull(withdrawal.withdrawMemo)
assertNotEquals(memo, withdrawal.withdrawMemo)
transactionWithRetry {
wallet.send(withdrawTxn.transaction.to, Asset.create(USDC.id), "1", memo)
wallet.send(withdrawTxn.transaction.to, Asset.create(USDC.id), "1", withdrawal.withdrawMemo)
}

// Wait for the status to change to COMPLETED
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public Sep24Transaction findByExternalTransactionId(String externalTransactionId
return txnRepo.findOneByExternalTransactionId(externalTransactionId);
}

@Override
public List<JdbcSep24Transaction> findAllByWithdrawAnchorAccountAndMemoAndStatus(
String toAccount, String memo, String status) {
return txnRepo.findAllByWithdrawAnchorAccountAndMemoAndStatus(toAccount, memo, status);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ public Sep31Transaction save(Sep31Transaction transaction) throws SepException {
return transactionRepo.save((JdbcSep31Transaction) transaction);
}

@Override
public List<JdbcSep31Transaction> findAllByToAccountAndMemoAndStatus(
String toAccount, String memo, String status) {
return transactionRepo.findAllByToAccountAndStellarMemoAndStatus(toAccount, memo, status);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ public List<? extends Sep6Transaction> findTransactions(TransactionsParams param
return transactionRepo.findAllTransactions(params, JdbcSep6Transaction.class);
}

@Override
public List<JdbcSep6Transaction> findAllByWithdrawAnchorAccountAndMemoAndStatus(
String withdrawAnchorAccount, String memo, String status) {
return transactionRepo.findAllByWithdrawAnchorAccountAndMemoAndStatus(
Expand Down
Loading
Loading