Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Let API user set currencies in some payment account json forms #5685

Merged
merged 12 commits into from
Sep 17, 2021
Merged
16 changes: 14 additions & 2 deletions common/src/main/java/bisq/common/util/Utilities.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,11 @@
import java.io.File;
import java.io.IOException;

import java.util.Arrays;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
Expand Down Expand Up @@ -337,12 +339,12 @@ public static boolean isCorrectOSArchitecture() {

public static void openURI(URI uri) throws IOException {
if (!DesktopUtil.browse(uri))
throw new IOException("Failed to open URI: " + uri.toString());
throw new IOException("Failed to open URI: " + uri);
}

public static void openFile(File file) throws IOException {
if (!DesktopUtil.open(file))
throw new IOException("Failed to open file: " + file.toString());
throw new IOException("Failed to open file: " + file);
}

public static String getDownloadOfHomeDir() {
Expand Down Expand Up @@ -480,6 +482,16 @@ public static String toTruncatedString(Object message, int maxLength, boolean re

}

public static List<String> toListOfWrappedStrings(String s, int wrapLength) {
StringBuilder sb = new StringBuilder(s);
int i = 0;
while (i + wrapLength < sb.length() && (i = sb.lastIndexOf(" ", i + wrapLength)) != -1) {
sb.replace(i, i + 1, "\n");
}
String[] splitLine = sb.toString().split("\n");
return Arrays.asList(splitLine);
}

public static String getRandomPrefix(int minLength, int maxLength) {
int length = minLength + new Random().nextInt(maxLength - minLength + 1);
String result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,12 @@ List<PaymentMethod> getCryptoCurrencyPaymentMethods() {
}

private void verifyPaymentAccountHasRequiredFields(PaymentAccount paymentAccount) {
// Do checks here to make sure required fields are populated.
if (paymentAccount.isTransferwiseAccount() && paymentAccount.getTradeCurrencies().isEmpty())
if (paymentAccount.canSupportMultipleCurrencies() && paymentAccount.getTradeCurrencies().isEmpty())
throw new IllegalArgumentException(format("no trade currencies defined for %s payment account",
paymentAccount.getPaymentMethod().getDisplayString().toLowerCase()));

if (!paymentAccount.canSupportMultipleCurrencies() && paymentAccount.getSingleTradeCurrency() == null)
throw new IllegalArgumentException(format("no trade currency defined for %s payment account",
paymentAccount.getPaymentMethod().getDisplayString().toLowerCase()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,11 @@ public class PaymentAccountForm {
"maxTradePeriod",
"paymentAccountPayload",
"paymentMethod",
"paymentMethodId", // This field will be included, but handled differently.
"selectedTradeCurrency",
"tradeCurrencies", // This field may be included, but handled differently.
"paymentMethodId", // Will be included, but handled differently.
"selectedTradeCurrency", // May be included, but handled differently.
"tradeCurrencies", // May be included, but handled differently.
"HOLDER_NAME",
"SALT" // This field will be included, but handled differently.
"SALT" // Will be included, but handled differently.
};

/**
Expand Down
141 changes: 108 additions & 33 deletions core/src/main/java/bisq/core/api/model/PaymentAccountTypeAdapter.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,8 @@
import static bisq.common.util.ReflectionUtils.*;
import static bisq.common.util.Utilities.decodeFromHex;
import static bisq.core.locale.CountryUtil.findCountryByCode;
import static bisq.core.locale.CurrencyUtil.getAllTransferwiseCurrencies;
import static bisq.core.locale.CurrencyUtil.getCurrencyByCountryCode;
import static bisq.core.locale.CurrencyUtil.getTradeCurrencies;
import static bisq.core.locale.CurrencyUtil.getTradeCurrenciesInList;
import static bisq.core.locale.CurrencyUtil.*;
import static bisq.core.payment.payload.PaymentMethod.*;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.lang.String.format;
import static java.util.Arrays.stream;
Expand Down Expand Up @@ -110,13 +108,7 @@ public void write(JsonWriter out, PaymentAccount account) throws IOException {
// We're not serializing a real payment account instance here.
out.beginObject();

// All json forms start with immutable _COMMENTS_ and paymentMethodId fields.
out.name("_COMMENTS_");
out.beginArray();
for (String s : JSON_COMMENTS) {
out.value(s);
}
out.endArray();
writeComments(out, account);

out.name("paymentMethodId");
out.value(account.getPaymentMethod().getId());
Expand All @@ -131,9 +123,32 @@ public void write(JsonWriter out, PaymentAccount account) throws IOException {
out.endObject();
}

private void writeComments(JsonWriter out, PaymentAccount account) throws IOException {
// All json forms start with immutable _COMMENTS_ and paymentMethodId fields.
out.name("_COMMENTS_");
out.beginArray();
for (String s : JSON_COMMENTS) {
out.value(s);
}
/*
if (account.isSwiftAccount()) {
// Add extra comments for more complex swift account form.
List<String> wrappedSwiftComments = Res.getWrappedAsList("payment.swift.info", 110);
for (String line : wrappedSwiftComments) {
out.value(line);
}
out.value("See https://bisq.wiki/SWIFT");
}
*/
out.endArray();
}


private void writeInnerMutableFields(JsonWriter out, PaymentAccount account) {
if (account.isTransferwiseAccount())
if (account.canSupportMultipleCurrencies()) {
writeTradeCurrenciesField(out, account);
writeSelectedTradeCurrencyField(out, account);
}

fieldSettersMap.forEach((field, value) -> {
try {
Expand Down Expand Up @@ -170,7 +185,7 @@ private void writeTradeCurrenciesField(JsonWriter out, PaymentAccount account) {
String fieldName = "tradeCurrencies";
log.debug("Append form with non-settable field: {}", fieldName);
out.name(fieldName);
out.value("comma delimited currency code list, e.g., gbp,eur");
out.value("comma delimited currency code list, e.g., gbp,eur,jpy,usd");
} catch (Exception ex) {
String errMsg = format("cannot create a new %s json form",
account.getClass().getSimpleName());
Expand All @@ -179,6 +194,22 @@ private void writeTradeCurrenciesField(JsonWriter out, PaymentAccount account) {
}
}

// PaymentAccounts that support multiple 'tradeCurrencies' need to define a
// 'selectedTradeCurrency' field (not simply defaulting to first in list).
// Write this field to the form.
private void writeSelectedTradeCurrencyField(JsonWriter out, PaymentAccount account) {
try {
String fieldName = "selectedTradeCurrency";
log.debug("Append form with settable field: {}", fieldName);
out.name(fieldName);
out.value("primary trading currency code, e.g., eur");
} catch (Exception ex) {
String errMsg = format("cannot create a new %s json form",
account.getClass().getSimpleName());
log.error(StringUtils.capitalize(errMsg) + ".", ex);
throw new IllegalStateException("programmer error: " + errMsg);
}
}

@Override
public PaymentAccount read(JsonReader in) throws IOException {
Expand All @@ -187,12 +218,17 @@ public PaymentAccount read(JsonReader in) throws IOException {
while (in.hasNext()) {
String currentFieldName = in.nextName();

// The tradeCurrency field is common to all payment account types,
// The tradeCurrencies field is common to all payment account types,
// but has no setter.
if (didReadTradeCurrenciesField(in, account, currentFieldName))
continue;

// Some of the fields are common to all payment account types.
// The selectedTradeCurrency field is common to all payment account types,
// but is @Nullable, and may not need to be explicitly defined by user.
if (didReadSelectedTradeCurrencyField(in, account, currentFieldName))
continue;

// Some fields are common to all payment account types.
if (didReadCommonField(in, account, currentFieldName))
continue;

Expand Down Expand Up @@ -318,30 +354,22 @@ private boolean didReadTradeCurrenciesField(JsonReader in,
PaymentAccount account,
String fieldName) {
// The PaymentAccount.tradeCurrencies field is a special case because it has
// no setter, and we add currencies to the List here. Normally, it is an
// excluded field, TransferwiseAccount excepted.
// no setter, so we add currencies to the List here if the payment account
// supports multiple trade currencies.
if (fieldName.equals("tradeCurrencies")) {
String fieldValue = nextStringOrNull(in);
List<String> currencyCodes = commaDelimitedCodesToList.apply(fieldValue);

Optional<List<TradeCurrency>> tradeCurrencies;
if (account.isTransferwiseAccount())
tradeCurrencies = getTradeCurrenciesInList(currencyCodes, getAllTransferwiseCurrencies());
else
tradeCurrencies = getTradeCurrencies(currencyCodes);

Optional<List<TradeCurrency>> tradeCurrencies = getReconciledTradeCurrencies(currencyCodes, account);
if (tradeCurrencies.isPresent()) {
for (TradeCurrency tradeCurrency : tradeCurrencies.get()) {
account.addCurrency(tradeCurrency);
}
// For api users, define a selected currency.
account.setSelectedTradeCurrency(account.getTradeCurrency().orElse(null));
} else {
// Log a warning. We should not throw an exception here because the
// gson library will not pass it up to the calling Bisq class as it
// would be defined here. Do a check in a calling class to make sure
// the tradeCurrencies field is populated in the PaymentAccount
// object, if it is required for the payment account method.
// gson library will not pass it up to the calling Bisq object exactly as
// it would be defined here (causing confusion). Do a check in a calling
// class to make sure the tradeCurrencies field is populated in the
// PaymentAccount object, if it is required for the payment account method.
log.warn("No trade currencies were found in the {} account form.",
account.getPaymentMethod().getDisplayString());
}
Expand All @@ -350,14 +378,61 @@ private boolean didReadTradeCurrenciesField(JsonReader in,
return false;
}

private Optional<List<TradeCurrency>> getReconciledTradeCurrencies(List<String> currencyCodes,
PaymentAccount account) {
if (account.hasPaymentMethodWithId(ADVANCED_CASH_ID))
return getTradeCurrenciesInList(currencyCodes, getAllAdvancedCashCurrencies());
else if (account.hasPaymentMethodWithId(AMAZON_GIFT_CARD_ID))
return getTradeCurrenciesInList(currencyCodes, getAllAmazonGiftCardCurrencies());
else if (account.hasPaymentMethodWithId(CAPITUAL_ID))
return getTradeCurrenciesInList(currencyCodes, getAllCapitualCurrencies());
else if (account.hasPaymentMethodWithId(MONEY_GRAM_ID))
return getTradeCurrenciesInList(currencyCodes, getAllMoneyGramCurrencies());
else if (account.hasPaymentMethodWithId(PAXUM_ID))
return getTradeCurrenciesInList(currencyCodes, getAllPaxumCurrencies());
else if (account.hasPaymentMethodWithId(PAYSERA_ID))
return getTradeCurrenciesInList(currencyCodes, getAllPayseraCurrencies());
else if (account.hasPaymentMethodWithId(REVOLUT_ID))
return getTradeCurrenciesInList(currencyCodes, getAllRevolutCurrencies());
/*else if (account.hasPaymentMethodWithId(SWIFT_ID))
return getTradeCurrenciesInList(currencyCodes, new ArrayList<>(getAllSortedFiatCurrencies()));*/
else if (account.hasPaymentMethodWithId(TRANSFERWISE_ID))
return getTradeCurrenciesInList(currencyCodes, getAllTransferwiseCurrencies());
else if (account.hasPaymentMethodWithId(UPHOLD_ID))
return getTradeCurrenciesInList(currencyCodes, getAllUpholdCurrencies());
else
return Optional.empty();
}

private boolean didReadSelectedTradeCurrencyField(JsonReader in,
PaymentAccount account,
String fieldName) {
if (fieldName.equals("selectedTradeCurrency")) {
ghubstan marked this conversation as resolved.
Show resolved Hide resolved
String fieldValue = nextStringOrNull(in);
if (fieldValue != null && !fieldValue.isEmpty()) {
Optional<TradeCurrency> tradeCurrency = getTradeCurrency(fieldValue.toUpperCase());
if (tradeCurrency.isPresent()) {
account.setSelectedTradeCurrency(tradeCurrency.get());
} else {
sqrrm marked this conversation as resolved.
Show resolved Hide resolved
// Log an error. We should not throw an exception here because the
// gson library will not pass it up to the calling Bisq object exactly as
// it would be defined here (causing confusion).
log.error("{} is not a valid trade currency code.", fieldValue);
}
}
return true;
}
return false;
}

private boolean didReadCommonField(JsonReader in,
PaymentAccount account,
String fieldName) throws IOException {
switch (fieldName) {
case "_COMMENTS_":
case "paymentMethodId":
// Skip over the the comments and paymentMethodId, which is already
// set on the PaymentAccount instance.
// Skip over comments and paymentMethodId field, which
// are already set on the PaymentAccount instance.
in.skipValue();
return true;
case "accountName":
Expand Down Expand Up @@ -388,7 +463,7 @@ private boolean didReadCountryField(JsonReader in, PaymentAccount account, Strin
((CountryBasedPaymentAccount) account).setCountry(country.get());
FiatCurrency fiatCurrency = getCurrencyByCountryCode(checkNotNull(countryCode));
account.setSingleTradeCurrency(fiatCurrency);
} else if (account.isMoneyGramAccount()) {
} else if (account.hasPaymentMethodWithId(MONEY_GRAM_ID)) {
((MoneyGramAccount) account).setCountry(country.get());
} else {
String errMsg = format("cannot set the country on a %s",
Expand Down
28 changes: 25 additions & 3 deletions core/src/main/java/bisq/core/locale/Res.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
import java.io.InputStream;
import java.io.InputStreamReader;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.PropertyResourceBundle;
Expand All @@ -42,6 +44,9 @@

import org.jetbrains.annotations.NotNull;

import static bisq.common.util.Utilities.toListOfWrappedStrings;
import static java.nio.charset.StandardCharsets.UTF_8;

@Slf4j
public class Res {
public static void setup() {
Expand Down Expand Up @@ -125,13 +130,30 @@ public static String get(String key) {
return key;
}
}

public static List<String> getWrappedAsList(String key, int wrapLength) {
String[] raw = get(key).split("\n");
List<String> wrapped = new ArrayList<>();
for (String s : raw) {
List<String> list = toListOfWrappedStrings(s, wrapLength);
for (String line : list) {
if (!line.isEmpty())
wrapped.add(line);
}
}
return wrapped;
}
}

// Adds UTF8 support for property files
class UTF8Control extends ResourceBundle.Control {

public ResourceBundle newBundle(String baseName, @NotNull Locale locale, @NotNull String format, ClassLoader loader, boolean reload)
throws IllegalAccessException, InstantiationException, IOException {
public ResourceBundle newBundle(String baseName,
@NotNull Locale locale,
@NotNull String format,
ClassLoader loader,
boolean reload)
throws IOException {
// Below is a copy of the default implementation.
final String bundleName = toBundleName(baseName, locale);
final String resourceName = toResourceName(bundleName, "properties");
Expand All @@ -152,7 +174,7 @@ public ResourceBundle newBundle(String baseName, @NotNull Locale locale, @NotNul
if (stream != null) {
try {
// Only this line is changed to make it read properties files as UTF-8.
bundle = new PropertyResourceBundle(new InputStreamReader(stream, "UTF-8"));
bundle = new PropertyResourceBundle(new InputStreamReader(stream, UTF_8));
} finally {
stream.close();
}
Expand Down
4 changes: 3 additions & 1 deletion core/src/main/java/bisq/core/offer/CreateOfferService.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@

import lombok.extern.slf4j.Slf4j;

import static bisq.core.payment.payload.PaymentMethod.HAL_CASH_ID;

@Slf4j
@Singleton
public class CreateOfferService {
Expand Down Expand Up @@ -133,7 +135,7 @@ public Offer createAndGetOffer(String offerId,
NodeAddress makerAddress = p2PService.getAddress();
boolean useMarketBasedPriceValue = useMarketBasedPrice &&
isMarketPriceAvailable(currencyCode) &&
!paymentAccount.isHalCashAccount();
!paymentAccount.hasPaymentMethodWithId(HAL_CASH_ID);

long priceAsLong = price != null && !useMarketBasedPriceValue ? price.getValue() : 0L;
double marketPriceMarginParam = useMarketBasedPriceValue ? marketPriceMargin : 0;
Expand Down
Loading