Skip to content

Commit

Permalink
payment-circle: polish responses and code (stellar#88)
Browse files Browse the repository at this point in the history
### What

Code polishes and small fixes. Changes details:
* Add `CircleWallet.defaultCapabilities()` and `CircleWallet.merchantAccountCapabilities()` to avoid hardcoding the capabilities every time.
* Update Account default `balances` and `unsettledBalances` values to null, so the JSON response works better when we don't have the balances. Otherwise, returning an empty list passes the impression the balances are empty when that might not be the case.
* Fix paginated responses cursors.
* Updated docs on how to get the bank deposit instructions.
* Solve warnings
* Update `CirclePaymentServiceTest` to reduce code repetition and standardize method names.
* Add all Circle asset names to the `CircleAsset` class, so we don't need to hardcode the string names.
  • Loading branch information
marcelosalloum committed Mar 2, 2022
1 parent c3fe0c9 commit 5489326
Show file tree
Hide file tree
Showing 11 changed files with 482 additions and 504 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ public class Account {
*/
@NonNull public Account.Capabilities capabilities;

@NonNull public List<Balance> balances = new ArrayList<>();
@Nullable public List<Balance> balances;

/**
* The list of not-yet-available balances that are expected to settle shortly. These balances
* could be cancelled or returned, in which cases they may never become available in the user
* account.
*/
@NonNull public List<Balance> unsettledBalances = new ArrayList<>();
@Nullable public List<Balance> unsettledBalances;

public Account(
@NonNull PaymentNetwork paymentNetwork,
Expand Down
10 changes: 5 additions & 5 deletions docs/payment-circle.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ Usage:
// CircleWallet->CircleWallet
Account source = new Account(PaymentNetwork.CIRCLE, "1000066041", Account.Capabilities(PaymentNetwork.CIRCLE, PaymentNetwork.STELLAR));
Account destination = new Account(PaymentNetwork.CIRCLE, "1000067536", Account.Capabilities(PaymentNetwork.CIRCLE, PaymentNetwork.STELLAR));
String currencyName = "circle:USD";
String currencyName = CircleAsset.circleUSD();
Payment payment = service.sendPayment(source, destination, currencyName, BigDecimal.valueOf(0.91)).block();
System.out.println(payment);

Expand All @@ -172,7 +172,7 @@ System.out.println(payment);
Account source = new Account(PaymentNetwork.CIRCLE, "1000066041", Account.Capabilities(PaymentNetwork.CIRCLE, PaymentNetwork.STELLAR));
// The bank wire account should have been created in advance directly in Circle
Account destination = new Account(PaymentNetwork.BANK_WIRE, "6c87da10-feb8-484f-822c-2083ed762d25", "[email protected]", Account.Capabilities());
String currencyName = "iso4217:USD";
String currencyName = CircleAsset.fiatUSD();
Payment payment = service.sendPayment(source, destination, currencyName, BigDecimal.valueOf(0.91)).block();
System.out.println(payment);
```
Expand All @@ -186,17 +186,17 @@ Usage:

```java
// Deposit requirements to receive CircleWallet<-CircleWallet payments
DepositRequirements config = DepositRequirements("1000066041", null, PaymentNetwork.CIRCLE, "circle:USD");
DepositRequirements config = DepositRequirements("1000066041", PaymentNetwork.CIRCLE, CircleAsset.circleUSD());
DepositInstructions instructions = service.getDepositInstructions(config).block();
System.out.println(instructions);

// Deposit requirements to receive CircleWallet<-Stellar payments
DepositRequirements config = DepositRequirements("1000066041", null, PaymentNetwork.STELLAR, "circle:USD");
DepositRequirements config = DepositRequirements("1000066041", PaymentNetwork.STELLAR, CircleAsset.circleUSD());
DepositInstructions instructions = service.getDepositInstructions(config).block();
System.out.println(instructions);

// Deposit requirements to receive CircleWallet<-BankWire payments
DepositRequirements config = DepositRequirements("1000066041", null, PaymentNetwork.BANK_WIRE, "circle:USD");
DepositRequirements config = DepositRequirements("1000066041", null, PaymentNetwork.BANK_WIRE, "a4e76642-81c5-47ca-9229-ebd64efd74a7", CircleAsset.circleUSD());
DepositInstructions instructions = service.getDepositInstructions(config).block();
System.out.println(instructions);
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.stellar.anchor.paymentservice.circle.model.*;
import org.stellar.anchor.paymentservice.circle.model.request.CircleSendTransactionRequest;
import org.stellar.anchor.paymentservice.circle.model.response.*;
import org.stellar.anchor.paymentservice.circle.util.CircleAsset;
import org.stellar.anchor.paymentservice.circle.util.NettyHttpClient;
import org.stellar.sdk.Network;
import reactor.core.publisher.Mono;
Expand Down Expand Up @@ -335,7 +336,7 @@ public Mono<PaymentHistory> getAccountPaymentHistory(
String afterTransfer = null, afterPayout = null, afterPayment = null;
if (beforeCursor != null) {
String[] beforeCursors = beforeCursor.split(":");
if (beforeCursors.length < 2) {
if (beforeCursors.length < 3) {
throw new HttpException(400, "invalid before cursor");
}
beforeTransfer = beforeCursors[0];
Expand All @@ -344,7 +345,7 @@ public Mono<PaymentHistory> getAccountPaymentHistory(
}
if (afterCursor != null) {
String[] afterCursors = afterCursor.split(":");
if (afterCursors.length < 2) {
if (afterCursors.length < 3) {
throw new HttpException(400, "invalid after cursor");
}
afterTransfer = afterCursors[0];
Expand All @@ -361,31 +362,30 @@ public Mono<PaymentHistory> getAccountPaymentHistory(
.map(
args -> {
String distributionAccId = args.getT1();
Account account =
new Account(
PaymentNetwork.CIRCLE,
accountID,
new Account.Capabilities(PaymentNetwork.CIRCLE, PaymentNetwork.STELLAR));
account.capabilities.set(
PaymentNetwork.BANK_WIRE, distributionAccId.equals(account.id));
boolean isMerchantAccount = distributionAccId.equals(accountID);
Account.Capabilities capabilities =
isMerchantAccount
? CircleWallet.merchantAccountCapabilities()
: CircleWallet.defaultCapabilities();
Account account = new Account(PaymentNetwork.CIRCLE, accountID, capabilities);

PaymentHistory transfersHistory =
args.getT2().toPaymentHistory(pageSize, account, distributionAccId);
PaymentHistory payoutsHistory = args.getT3().toPaymentHistory(pageSize, account);
PaymentHistory paymentsHistory = args.getT4().toPaymentHistory(pageSize, account);
PaymentHistory result = new PaymentHistory(account);

String befTransfer = transfersHistory.getBeforeCursor();
String befPayout = payoutsHistory.getBeforeCursor();
String befPayment = paymentsHistory.getBeforeCursor();
if (befTransfer != null || befPayout != null || befPayment != null) {
String befTransfer = Objects.toString(transfersHistory.getBeforeCursor(), "");
String befPayout = Objects.toString(payoutsHistory.getBeforeCursor(), "");
String befPayment = Objects.toString(paymentsHistory.getBeforeCursor(), "");
if (!befTransfer.isEmpty() || !befPayout.isEmpty() || !befPayment.isEmpty()) {
result.setBeforeCursor(befTransfer + ":" + befPayout + ":" + befPayment);
}

String aftTransfer = transfersHistory.getAfterCursor();
String aftPayout = payoutsHistory.getAfterCursor();
String aftPayment = paymentsHistory.getAfterCursor();
if (aftTransfer != null || aftPayout != null || aftPayment != null) {
String aftTransfer = Objects.toString(transfersHistory.getAfterCursor(), "");
String aftPayout = Objects.toString(payoutsHistory.getAfterCursor(), "");
String aftPayment = Objects.toString(paymentsHistory.getAfterCursor(), "");
if (!aftTransfer.isEmpty() || !aftPayout.isEmpty() || !aftPayment.isEmpty()) {
result.setAfterCursor(aftTransfer + ":" + aftPayout + ":" + aftPayment);
}

Expand Down Expand Up @@ -444,6 +444,13 @@ private void validateSendPaymentInput(
throw new HttpException(
400, "the currency to be sent must contain the destination network schema");
}
if (!CircleAsset.isSupported(currencyName, stellarNetwork)) {
throw new HttpException(
400,
String.format(
"the only supported currencies are %s, %s and %s.",
"circle:USD", "iso4217:USD", CircleAsset.stellarUSDC(stellarNetwork)));
}
}

/**
Expand Down Expand Up @@ -546,14 +553,17 @@ private void updatePaymentWireCapability(Payment payment, String distributionAcc
Boolean isSourceWireEnabled =
sourceAcc.paymentNetwork.equals(PaymentNetwork.BANK_WIRE)
|| distributionAccountId.equals(sourceAcc.id);
sourceAcc.capabilities.set(PaymentNetwork.BANK_WIRE, isSourceWireEnabled);
sourceAcc.capabilities.getReceive().put(PaymentNetwork.BANK_WIRE, isSourceWireEnabled);

// fill destination account level
Account destinationAcc = payment.getDestinationAccount();
Boolean isDestinationWireEnabled =
destinationAcc.paymentNetwork.equals(PaymentNetwork.BANK_WIRE)
|| distributionAccountId.equals(destinationAcc.id);
destinationAcc.capabilities.set(PaymentNetwork.BANK_WIRE, isDestinationWireEnabled);
destinationAcc
.capabilities
.getReceive()
.put(PaymentNetwork.BANK_WIRE, isDestinationWireEnabled);
}

/**
Expand All @@ -577,10 +587,7 @@ public Mono<Payment> sendPayment(
// validate input
validateSendPaymentInput(sourceAccount, destinationAccount, currencyName);

String rawCurrencyName =
currencyName.replace(destinationAccount.paymentNetwork.getCurrencyPrefix() + ":", "");
CircleBalance circleBalance =
new CircleBalance(rawCurrencyName, amount.toString(), stellarNetwork);
CircleBalance circleBalance = new CircleBalance("USD", amount.toString(), stellarNetwork);

switch (destinationAccount.paymentNetwork) {
case CIRCLE:
Expand Down Expand Up @@ -675,7 +682,7 @@ private void validateDepositRequirements(@NonNull DepositRequirements config)
throw new HttpException(400, "beneficiary account id cannot be empty");
}

if (!"circle:USD".equals(config.getBeneficiaryCurrencyName())) {
if (!CircleAsset.circleUSD().equals(config.getBeneficiaryCurrencyName())) {
throw new HttpException(
400, "the only receiving currency in a circle account is \"circle:USD\"");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ public CircleBalance(@NonNull String currency, @NonNull String amount) {

public Balance toBalance(@NonNull PaymentNetwork destinationPaymentNetwork) {
String currencyName = destinationPaymentNetwork.getCurrencyPrefix() + ":" + currency;
if (currencyName.equals("stellar:USD"))
currencyName = destinationPaymentNetwork.getCurrencyPrefix() + ":" + stellarUSDC();
if (currencyName.equals("stellar:USD")) currencyName = stellarUSDC();

return new Balance(amount, currencyName);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,14 @@ public CircleBlockchainAddress(

public DepositInstructions toDepositInstructions(
String beneficiaryAccountId, Network stellarNetwork) {
String intermediaryCurrencyName =
PaymentNetwork.STELLAR.getCurrencyPrefix() + ":" + CircleAsset.stellarUSDC(stellarNetwork);
return new DepositInstructions(
beneficiaryAccountId,
null,
PaymentNetwork.CIRCLE,
address,
addressTag,
PaymentNetwork.STELLAR,
intermediaryCurrencyName,
CircleAsset.stellarUSDC(stellarNetwork),
null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,13 @@ public Account toAccount(@Nullable String distributionAccountId) {
new Account.Capabilities(PaymentNetwork.STELLAR));

case WALLET:
Account account =
new Account(
PaymentNetwork.CIRCLE,
id,
new Account.Capabilities(PaymentNetwork.CIRCLE, PaymentNetwork.STELLAR));
boolean isWireEnabled = distributionAccountId != null && distributionAccountId.equals(id);
account.capabilities.set(PaymentNetwork.BANK_WIRE, isWireEnabled);
return account;
boolean isMerchantAccount =
distributionAccountId != null && distributionAccountId.equals(id);
Account.Capabilities capabilities =
isMerchantAccount
? CircleWallet.merchantAccountCapabilities()
: CircleWallet.defaultCapabilities();
return new Account(PaymentNetwork.CIRCLE, id, capabilities);

case WIRE:
return new Account(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package org.stellar.anchor.paymentservice.circle.model;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import lombok.Data;
import org.stellar.anchor.paymentservice.Account;
import org.stellar.anchor.paymentservice.DepositInstructions;
import org.stellar.anchor.paymentservice.PaymentNetwork;
import org.stellar.anchor.paymentservice.circle.util.CircleAsset;

@Data
public class CircleWallet {
Expand All @@ -15,25 +17,33 @@ public class CircleWallet {
String description;
List<CircleBalance> balances;

public CircleWallet() {}

public CircleWallet(String walletId) {
this.walletId = walletId;
}

public Account.Capabilities getCapabilities() {
return "merchant".equals(type) ? merchantAccountCapabilities() : defaultCapabilities();
}

public static Account.Capabilities defaultCapabilities() {
Account.Capabilities capabilities =
new Account.Capabilities(PaymentNetwork.CIRCLE, PaymentNetwork.STELLAR);
capabilities.set(PaymentNetwork.BANK_WIRE, "merchant".equals(type));
capabilities.getSend().put(PaymentNetwork.BANK_WIRE, true);
return capabilities;
}

public static Account.Capabilities merchantAccountCapabilities() {
return new Account.Capabilities(
PaymentNetwork.CIRCLE, PaymentNetwork.STELLAR, PaymentNetwork.BANK_WIRE);
}

public Account toAccount() {
Account account = new Account(PaymentNetwork.CIRCLE, walletId, description, getCapabilities());
account.setBalances(
balances.stream()
.map(circleBalance -> circleBalance.toBalance(PaymentNetwork.CIRCLE))
.collect(Collectors.toList()));
account.setUnsettledBalances(new ArrayList<>());
return account;
}

Expand All @@ -45,7 +55,7 @@ public DepositInstructions toDepositInstructions() {
walletId,
null,
PaymentNetwork.CIRCLE,
"circle:USD",
CircleAsset.circleUSD(),
null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import lombok.Data;
import org.stellar.anchor.paymentservice.DepositInstructions;
import org.stellar.anchor.paymentservice.PaymentNetwork;
import org.stellar.anchor.paymentservice.circle.util.CircleAsset;
import org.stellar.sdk.responses.GsonSingleton;
import shadow.com.google.common.reflect.TypeToken;
import shadow.com.google.gson.Gson;
Expand Down Expand Up @@ -45,7 +46,7 @@ public DepositInstructions toDepositInstructions(String beneficiaryAccountId) {
trackingRef,
null,
PaymentNetwork.BANK_WIRE,
"iso4217:USD",
CircleAsset.fiatUSD(),
originalResponse);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ public static CircleSendTransactionRequest forTransfer(
CircleSendTransactionRequest req = new CircleSendTransactionRequest();
req.source = source;
req.destination = destination;
if (amount.stellarUSDC().equals(amount.getCurrency())) amount.setCurrency("USD");
req.amount = amount;
req.idempotencyKey = idempotencyKey;
return req;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,29 @@
package org.stellar.anchor.paymentservice.circle.util;

import java.util.List;
import org.stellar.sdk.Network;
import reactor.util.annotation.NonNull;

public class CircleAsset {
@NonNull
public static String stellarUSDC(Network stellarNetwork) {
if (stellarNetwork == org.stellar.sdk.Network.PUBLIC)
return "USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN";
return "stellar:USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN";

return "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5";
return "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5";
}

public static String circleUSD() {
return "circle:USD";
}

public static String fiatUSD() {
return "iso4217:USD";
}

public static boolean isSupported(String currencyName, Network stellarNetwork) {
return List.of(
CircleAsset.circleUSD(), CircleAsset.fiatUSD(), CircleAsset.stellarUSDC(stellarNetwork))
.contains(currencyName);
}
}
Loading

0 comments on commit 5489326

Please sign in to comment.