From c3f5669cf88f13ebaae76927976b667ddbab1d2a Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 25 Mar 2021 18:36:26 -0300 Subject: [PATCH 01/10] Add api method createcryptopaymentacct This change supports creation of BSQ BLOCKCHAIN payment method accounts. - Added proto message defs to grpc.proto. - Added grpc server boilerplate to GrpcPaymentAccountsService. - Added server impl to CoreApi, CorePaymentAccountsService. - Added createcryptopaymentacct-help.txt. - Added CLI side support for new api method. - Added opt parsing unit tests to OptionParsersTest. This is the 1st PR in a series, with the goal of supporting the BTC/BSQ trading pair. Support for other crypto currency payment accounts will be added later. --- cli/src/main/java/bisq/cli/CliMain.java | 19 ++++ cli/src/main/java/bisq/cli/GrpcClient.java | 49 +++++++++- cli/src/main/java/bisq/cli/Method.java | 1 + .../main/java/bisq/cli/opts/ArgumentList.java | 1 + ...CryptoCurrencyPaymentAcctOptionParser.java | 75 ++++++++++++++++ .../main/java/bisq/cli/opts/MethodOpts.java | 1 - cli/src/main/java/bisq/cli/opts/OptLabel.java | 1 + .../java/bisq/cli/opt/OptionParsersTest.java | 89 ++++++++++++++++++- core/src/main/java/bisq/core/api/CoreApi.java | 8 ++ .../core/api/CorePaymentAccountsService.java | 50 ++++++++++- .../bisq/core/api/CoreWalletsService.java | 20 ++--- .../help/createcryptopaymentacct-help.txt | 39 ++++++++ .../grpc/GrpcPaymentAccountsService.java | 37 ++++++++ proto/src/main/proto/grpc.proto | 21 +++++ 14 files changed, 393 insertions(+), 18 deletions(-) create mode 100644 cli/src/main/java/bisq/cli/opts/CreateCryptoCurrencyPaymentAcctOptionParser.java create mode 100644 core/src/main/resources/help/createcryptopaymentacct-help.txt diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index 5e6e913711c..37bda937eb8 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -56,6 +56,7 @@ import bisq.cli.opts.ArgumentList; import bisq.cli.opts.CancelOfferOptionParser; +import bisq.cli.opts.CreateCryptoCurrencyPaymentAcctOptionParser; import bisq.cli.opts.CreateOfferOptionParser; import bisq.cli.opts.CreatePaymentAcctOptionParser; import bisq.cli.opts.GetAddressBalanceOptionParser; @@ -517,6 +518,22 @@ public static void run(String[] args) { out.println(formatPaymentAcctTbl(singletonList(paymentAccount))); return; } + case createcryptopaymentacct: { + var opts = new CreateCryptoCurrencyPaymentAcctOptionParser(args).parse(); + if (opts.isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var accountName = opts.getAccountName(); + var currencyCode = opts.getCurrencyCode(); + var address = opts.getAddress(); + var paymentAccount = client.createCryptoCurrencyPaymentAccount(accountName, + currencyCode, + address); + out.println("payment account saved"); + out.println(formatPaymentAcctTbl(singletonList(paymentAccount))); + return; + } case getpaymentaccts: { if (new SimpleMethodOptionParser(args).parse().isForHelp()) { out.println(client.getMethodHelp(method)); @@ -748,6 +765,8 @@ private static void printHelp(OptionParser parser, @SuppressWarnings("SameParame stream.println(); stream.format(rowFormat, createpaymentacct.name(), "--payment-account-form=", "Create a new payment account"); stream.println(); + stream.format(rowFormat, createcryptopaymentacct.name(), "--TODO=", "Create a new cryptocurrency payment account"); + stream.println(); stream.format(rowFormat, getpaymentaccts.name(), "", "Get user payment accounts"); stream.println(); stream.format(rowFormat, lockwallet.name(), "", "Remove wallet password from memory, locking the wallet"); diff --git a/cli/src/main/java/bisq/cli/GrpcClient.java b/cli/src/main/java/bisq/cli/GrpcClient.java index ea20c6aef88..5d3dde4eccb 100644 --- a/cli/src/main/java/bisq/cli/GrpcClient.java +++ b/cli/src/main/java/bisq/cli/GrpcClient.java @@ -24,10 +24,12 @@ import bisq.proto.grpc.CancelOfferRequest; import bisq.proto.grpc.ConfirmPaymentReceivedRequest; import bisq.proto.grpc.ConfirmPaymentStartedRequest; +import bisq.proto.grpc.CreateCryptoCurrencyPaymentAccountRequest; import bisq.proto.grpc.CreateOfferRequest; import bisq.proto.grpc.CreatePaymentAccountRequest; import bisq.proto.grpc.GetAddressBalanceRequest; import bisq.proto.grpc.GetBalancesRequest; +import bisq.proto.grpc.GetCryptoCurrencyPaymentMethodsRequest; import bisq.proto.grpc.GetFundingAddressesRequest; import bisq.proto.grpc.GetMethodHelpRequest; import bisq.proto.grpc.GetMyOfferRequest; @@ -67,11 +69,11 @@ import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import static java.util.Comparator.comparing; +import static java.util.stream.Collectors.toList; import static protobuf.OfferPayload.Direction.BUY; import static protobuf.OfferPayload.Direction.SELL; @@ -228,7 +230,6 @@ public OfferInfo createMarketBasedPricedOffer(String direction, makerFeeCurrencyCode); } - // TODO make private, move to bottom of class public OfferInfo createOffer(String direction, String currencyCode, long amount, @@ -283,6 +284,12 @@ public List getOffers(String direction, String currencyCode) { return grpcStubs.offersService.getOffers(request).getOffersList(); } + public List getBsqOffers(String direction) { + return getOffers(direction, "BTC").stream() + .filter(o -> o.getBaseCurrencyCode().equals("BSQ")) + .collect(toList()); + } + public List getOffersSortedByDate(String currencyCode) { ArrayList offers = new ArrayList<>(); offers.addAll(getOffers(BUY.name(), currencyCode)); @@ -295,6 +302,13 @@ public List getOffersSortedByDate(String direction, String currencyCo return offers.isEmpty() ? offers : sortOffersByDate(offers); } + public List getBsqOffersSortedByDate() { + ArrayList offers = new ArrayList<>(); + offers.addAll(getBsqOffers(BUY.name())); + offers.addAll(getBsqOffers(SELL.name())); + return sortOffersByDate(offers); + } + public List getMyOffers(String direction, String currencyCode) { var request = GetMyOffersRequest.newBuilder() .setDirection(direction) @@ -303,6 +317,12 @@ public List getMyOffers(String direction, String currencyCode) { return grpcStubs.offersService.getMyOffers(request).getOffersList(); } + public List getMyBsqOffers(String direction) { + return getMyOffers(direction, "BTC").stream() + .filter(o -> o.getBaseCurrencyCode().equals("BSQ")) + .collect(toList()); + } + public List getMyOffersSortedByDate(String direction, String currencyCode) { var offers = getMyOffers(direction, currencyCode); return offers.isEmpty() ? offers : sortOffersByDate(offers); @@ -315,6 +335,13 @@ public List getMyOffersSortedByDate(String currencyCode) { return sortOffersByDate(offers); } + public List getMyBsqOffersSortedByDate() { + ArrayList offers = new ArrayList<>(); + offers.addAll(getMyBsqOffers(BUY.name())); + offers.addAll(getMyBsqOffers(SELL.name())); + return sortOffersByDate(offers); + } + public OfferInfo getMostRecentOffer(String direction, String currencyCode) { List offers = getOffersSortedByDate(direction, currencyCode); return offers.isEmpty() ? null : offers.get(offers.size() - 1); @@ -323,7 +350,7 @@ public OfferInfo getMostRecentOffer(String direction, String currencyCode) { public List sortOffersByDate(List offerInfoList) { return offerInfoList.stream() .sorted(comparing(OfferInfo::getDate)) - .collect(Collectors.toList()); + .collect(toList()); } public TakeOfferReply getTakeOfferReply(String offerId, String paymentAccountId, String takerFeeCurrencyCode) { @@ -404,6 +431,22 @@ public List getPaymentAccounts() { return grpcStubs.paymentAccountsService.getPaymentAccounts(request).getPaymentAccountsList(); } + public PaymentAccount createCryptoCurrencyPaymentAccount(String accountName, + String currencyCode, + String address) { + var request = CreateCryptoCurrencyPaymentAccountRequest.newBuilder() + .setAccountName(accountName) + .setCurrencyCode(currencyCode) + .setAddress(address) + .build(); + return grpcStubs.paymentAccountsService.createCryptoCurrencyPaymentAccount(request).getPaymentAccount(); + } + + public List getCryptoPaymentMethods() { + var request = GetCryptoCurrencyPaymentMethodsRequest.newBuilder().build(); + return grpcStubs.paymentAccountsService.getCryptoCurrencyPaymentMethods(request).getPaymentMethodsList(); + } + public void lockWallet() { var request = LockWalletRequest.newBuilder().build(); grpcStubs.walletsService.lockWallet(request); diff --git a/cli/src/main/java/bisq/cli/Method.java b/cli/src/main/java/bisq/cli/Method.java index 67b582f13c6..908321a0902 100644 --- a/cli/src/main/java/bisq/cli/Method.java +++ b/cli/src/main/java/bisq/cli/Method.java @@ -26,6 +26,7 @@ public enum Method { confirmpaymentstarted, createoffer, createpaymentacct, + createcryptopaymentacct, getaddressbalance, getbalance, getbtcprice, diff --git a/cli/src/main/java/bisq/cli/opts/ArgumentList.java b/cli/src/main/java/bisq/cli/opts/ArgumentList.java index 3b52fb34a90..b416946d646 100644 --- a/cli/src/main/java/bisq/cli/opts/ArgumentList.java +++ b/cli/src/main/java/bisq/cli/opts/ArgumentList.java @@ -113,6 +113,7 @@ boolean hasMore() { return currentIndex < arguments.length; } + @SuppressWarnings("UnusedReturnValue") String next() { return arguments[currentIndex++]; } diff --git a/cli/src/main/java/bisq/cli/opts/CreateCryptoCurrencyPaymentAcctOptionParser.java b/cli/src/main/java/bisq/cli/opts/CreateCryptoCurrencyPaymentAcctOptionParser.java new file mode 100644 index 00000000000..90542e1e0b7 --- /dev/null +++ b/cli/src/main/java/bisq/cli/opts/CreateCryptoCurrencyPaymentAcctOptionParser.java @@ -0,0 +1,75 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.cli.opts; + + +import joptsimple.OptionSpec; + +import static bisq.cli.opts.OptLabel.OPT_ACCOUNT_NAME; +import static bisq.cli.opts.OptLabel.OPT_ADDRESS; +import static bisq.cli.opts.OptLabel.OPT_CURRENCY_CODE; + +public class CreateCryptoCurrencyPaymentAcctOptionParser extends AbstractMethodOptionParser implements MethodOpts { + + final OptionSpec accountNameOpt = parser.accepts(OPT_ACCOUNT_NAME, "crypto currency account name") + .withRequiredArg(); + + final OptionSpec currencyCodeOpt = parser.accepts(OPT_CURRENCY_CODE, "crypto currency code (bsq only)") + .withRequiredArg(); + + final OptionSpec addressOpt = parser.accepts(OPT_ADDRESS, "bsq address") + .withRequiredArg(); + + public CreateCryptoCurrencyPaymentAcctOptionParser(String[] args) { + super(args); + } + + public CreateCryptoCurrencyPaymentAcctOptionParser parse() { + super.parse(); + + // Short circuit opt validation if user just wants help. + if (options.has(helpOpt)) + return this; + + if (!options.has(accountNameOpt) || options.valueOf(accountNameOpt).isEmpty()) + throw new IllegalArgumentException("no payment account name specified"); + + if (!options.has(currencyCodeOpt) || options.valueOf(currencyCodeOpt).isEmpty()) + throw new IllegalArgumentException("no currency code specified"); + + if (!options.valueOf(currencyCodeOpt).equalsIgnoreCase("bsq")) + throw new IllegalArgumentException("api only supports bsq crypto currency payment accounts"); + + if (!options.has(addressOpt) || options.valueOf(addressOpt).isEmpty()) + throw new IllegalArgumentException("no bsq address specified"); + + return this; + } + + public String getAccountName() { + return options.valueOf(accountNameOpt); + } + + public String getCurrencyCode() { + return options.valueOf(currencyCodeOpt); + } + + public String getAddress() { + return options.valueOf(addressOpt); + } +} diff --git a/cli/src/main/java/bisq/cli/opts/MethodOpts.java b/cli/src/main/java/bisq/cli/opts/MethodOpts.java index da639857522..9f6c2d1a3e4 100644 --- a/cli/src/main/java/bisq/cli/opts/MethodOpts.java +++ b/cli/src/main/java/bisq/cli/opts/MethodOpts.java @@ -22,5 +22,4 @@ public interface MethodOpts { MethodOpts parse(); boolean isForHelp(); - } diff --git a/cli/src/main/java/bisq/cli/opts/OptLabel.java b/cli/src/main/java/bisq/cli/opts/OptLabel.java index 5cbd068ce53..fcabd0820e4 100644 --- a/cli/src/main/java/bisq/cli/opts/OptLabel.java +++ b/cli/src/main/java/bisq/cli/opts/OptLabel.java @@ -21,6 +21,7 @@ * CLI opt label definitions. */ public class OptLabel { + public final static String OPT_ACCOUNT_NAME = "account-name"; public final static String OPT_ADDRESS = "address"; public final static String OPT_AMOUNT = "amount"; public final static String OPT_CURRENCY_CODE = "currency-code"; diff --git a/cli/src/test/java/bisq/cli/opt/OptionParsersTest.java b/cli/src/test/java/bisq/cli/opt/OptionParsersTest.java index 1f939198d69..d5cfe5d708f 100644 --- a/cli/src/test/java/bisq/cli/opt/OptionParsersTest.java +++ b/cli/src/test/java/bisq/cli/opt/OptionParsersTest.java @@ -3,6 +3,7 @@ import org.junit.jupiter.api.Test; import static bisq.cli.Method.canceloffer; +import static bisq.cli.Method.createcryptopaymentacct; import static bisq.cli.Method.createoffer; import static bisq.cli.Method.createpaymentacct; import static bisq.cli.opts.OptLabel.*; @@ -12,6 +13,7 @@ import bisq.cli.opts.CancelOfferOptionParser; +import bisq.cli.opts.CreateCryptoCurrencyPaymentAcctOptionParser; import bisq.cli.opts.CreateOfferOptionParser; import bisq.cli.opts.CreatePaymentAcctOptionParser; @@ -20,7 +22,7 @@ public class OptionParsersTest { private static final String PASSWORD_OPT = "--" + OPT_PASSWORD + "=" + "xyz"; - // CancelOffer opt parsing tests + // canceloffer opt parser tests @Test public void testCancelOfferWithMissingOfferIdOptShouldThrowException() { @@ -67,7 +69,7 @@ public void testValidCancelOfferOpts() { new CancelOfferOptionParser(args).parse(); } - // CreateOffer opt parsing tests + // createoffer opt parser tests @Test public void testCreateOfferOptParserWithMissingPaymentAccountIdOptShouldThrowException() { @@ -139,7 +141,7 @@ public void testValidCreateOfferOpts() { assertEquals("25.0", parser.getSecurityDeposit()); } - // CreatePaymentAcct opt parser tests + // createpaymentacct opt parser tests @Test public void testCreatePaymentAcctOptParserWithMissingPaymentFormOptShouldThrowException() { @@ -177,4 +179,85 @@ public void testCreatePaymentAcctOptParserWithInvalidPaymentFormOptValueShouldTh assertEquals("json payment account form '/tmp/milkyway/solarsystem/mars' could not be found", exception.getMessage()); } + + // createcryptopaymentacct parser tests + + @Test + public void testCreateCryptoCurrencyPaymentAcctOptionParserWithMissingAcctNameOptShouldThrowException() { + String[] args = new String[]{ + PASSWORD_OPT, + createcryptopaymentacct.name() + }; + Throwable exception = assertThrows(RuntimeException.class, () -> + new CreateCryptoCurrencyPaymentAcctOptionParser(args).parse()); + assertEquals("no payment account name specified", exception.getMessage()); + } + + @Test + public void testCreateCryptoCurrencyPaymentAcctOptionParserWithEmptyAcctNameOptShouldThrowException() { + String[] args = new String[]{ + PASSWORD_OPT, + createcryptopaymentacct.name(), + "--" + OPT_ACCOUNT_NAME + }; + Throwable exception = assertThrows(RuntimeException.class, () -> + new CreateCryptoCurrencyPaymentAcctOptionParser(args).parse()); + assertEquals("account-name requires an argument", exception.getMessage()); + } + + @Test + public void testCreateCryptoCurrencyPaymentAcctOptionParserWithMissingCurrencyCodeOptShouldThrowException() { + String[] args = new String[]{ + PASSWORD_OPT, + createcryptopaymentacct.name(), + "--" + OPT_ACCOUNT_NAME + "=" + "bsq payment account" + }; + Throwable exception = assertThrows(RuntimeException.class, () -> + new CreateCryptoCurrencyPaymentAcctOptionParser(args).parse()); + assertEquals("no currency code specified", exception.getMessage()); + } + + @Test + public void testCreateCryptoCurrencyPaymentAcctOptionParserWithInvalidCurrencyCodeOptShouldThrowException() { + String[] args = new String[]{ + PASSWORD_OPT, + createcryptopaymentacct.name(), + "--" + OPT_ACCOUNT_NAME + "=" + "bsq payment account", + "--" + OPT_CURRENCY_CODE + "=" + "xmr" + }; + Throwable exception = assertThrows(RuntimeException.class, () -> + new CreateCryptoCurrencyPaymentAcctOptionParser(args).parse()); + assertEquals("api only supports bsq crypto currency payment accounts", exception.getMessage()); + } + + @Test + public void testCreateCryptoCurrencyPaymentAcctOptionParserWithMissingAddressOptShouldThrowException() { + String[] args = new String[]{ + PASSWORD_OPT, + createcryptopaymentacct.name(), + "--" + OPT_ACCOUNT_NAME + "=" + "bsq payment account", + "--" + OPT_CURRENCY_CODE + "=" + "bsq" + }; + Throwable exception = assertThrows(RuntimeException.class, () -> + new CreateCryptoCurrencyPaymentAcctOptionParser(args).parse()); + assertEquals("no bsq address specified", exception.getMessage()); + } + + @Test + public void testCreateCryptoCurrencyPaymentAcctOptionParser() { + var acctName = "bsq payment account"; + var currencyCode = "bsq"; + var address = "B1nXyZ"; // address is validated on server + String[] args = new String[]{ + PASSWORD_OPT, + createcryptopaymentacct.name(), + "--" + OPT_ACCOUNT_NAME + "=" + acctName, + "--" + OPT_CURRENCY_CODE + "=" + currencyCode, + "--" + OPT_ADDRESS + "=" + address + }; + var parser = new CreateCryptoCurrencyPaymentAcctOptionParser(args).parse(); + assertEquals(acctName, parser.getAccountName()); + assertEquals(currencyCode, parser.getCurrencyCode()); + assertEquals(address, parser.getAddress()); + } } diff --git a/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java index 8861620adbc..6db1c667ed9 100644 --- a/core/src/main/java/bisq/core/api/CoreApi.java +++ b/core/src/main/java/bisq/core/api/CoreApi.java @@ -210,6 +210,14 @@ public String getPaymentAccountForm(String paymentMethodId) { return paymentAccountsService.getPaymentAccountFormAsString(paymentMethodId); } + public PaymentAccount createCryptoCurrencyPaymentAccount(String accountName, String currencyCode, String address) { + return paymentAccountsService.createCryptoCurrencyPaymentAccount(accountName, currencyCode, address); + } + + public List getCryptoCurrencyPaymentMethods() { + return paymentAccountsService.getCryptoCurrencyPaymentMethods(); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Prices /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java b/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java index b915b3ab4be..a316b76ed07 100644 --- a/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java +++ b/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java @@ -19,7 +19,10 @@ import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.api.model.PaymentAccountForm; +import bisq.core.locale.CryptoCurrency; +import bisq.core.payment.CryptoCurrencyAccount; import bisq.core.payment.PaymentAccount; +import bisq.core.payment.PaymentAccountFactory; import bisq.core.payment.payload.PaymentMethod; import bisq.core.user.User; @@ -30,30 +33,37 @@ import java.util.Comparator; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; +import static bisq.core.locale.CurrencyUtil.getCryptoCurrency; import static java.lang.String.format; @Singleton @Slf4j class CorePaymentAccountsService { + private final CoreWalletsService coreWalletsService; private final AccountAgeWitnessService accountAgeWitnessService; private final PaymentAccountForm paymentAccountForm; private final User user; @Inject - public CorePaymentAccountsService(AccountAgeWitnessService accountAgeWitnessService, + public CorePaymentAccountsService(CoreWalletsService coreWalletsService, + AccountAgeWitnessService accountAgeWitnessService, PaymentAccountForm paymentAccountForm, User user) { + this.coreWalletsService = coreWalletsService; this.accountAgeWitnessService = accountAgeWitnessService; this.paymentAccountForm = paymentAccountForm; this.user = user; } + // Fiat Currency Accounts + PaymentAccount createPaymentAccount(String jsonString) { PaymentAccount paymentAccount = paymentAccountForm.toPaymentAccount(jsonString); verifyPaymentAccountHasRequiredFields(paymentAccount); @@ -86,6 +96,44 @@ File getPaymentAccountForm(String paymentMethodId) { return paymentAccountForm.getPaymentAccountForm(paymentMethodId); } + // Crypto Currency Accounts + + PaymentAccount createCryptoCurrencyPaymentAccount(String accountName, + String currencyCode, + String address) { + String bsqCode = currencyCode.toUpperCase(); + if (!bsqCode.equals("BSQ")) + throw new IllegalArgumentException("api does not currently support " + currencyCode + " accounts"); + + // Validate the BSQ address string but ignore the return value. + coreWalletsService.getValidBsqLegacyAddress(address); + + CryptoCurrencyAccount cryptoCurrencyAccount = + (CryptoCurrencyAccount) PaymentAccountFactory.getPaymentAccount(PaymentMethod.BLOCK_CHAINS); + cryptoCurrencyAccount.init(); + cryptoCurrencyAccount.setAccountName(accountName); + cryptoCurrencyAccount.setAddress(address); + Optional cryptoCurrency = getCryptoCurrency(bsqCode); + cryptoCurrency.ifPresent(cryptoCurrencyAccount::setSingleTradeCurrency); + user.addPaymentAccount(cryptoCurrencyAccount); + accountAgeWitnessService.publishMyAccountAgeWitness(cryptoCurrencyAccount.getPaymentAccountPayload()); + log.info("Saved crypto payment account with id {} and payment method {}.", + cryptoCurrencyAccount.getId(), + cryptoCurrencyAccount.getPaymentAccountPayload().getPaymentMethodId()); + return cryptoCurrencyAccount; + } + + // TODO Support all alt coin payment methods supported by UI. + // The getCryptoCurrencyPaymentMethods method below will be + // callable from the CLI when more are supported. + + List getCryptoCurrencyPaymentMethods() { + return PaymentMethod.getPaymentMethods().stream() + .filter(PaymentMethod::isAsset) + .sorted(Comparator.comparing(PaymentMethod::getId)) + .collect(Collectors.toList()); + } + private void verifyPaymentAccountHasRequiredFields(PaymentAccount paymentAccount) { // Do checks here to make sure required fields are populated. if (paymentAccount.isTransferwiseAccount() && paymentAccount.getTradeCurrencies().isEmpty()) diff --git a/core/src/main/java/bisq/core/api/CoreWalletsService.java b/core/src/main/java/bisq/core/api/CoreWalletsService.java index 3438f640d8e..d6aa8a240a3 100644 --- a/core/src/main/java/bisq/core/api/CoreWalletsService.java +++ b/core/src/main/java/bisq/core/api/CoreWalletsService.java @@ -511,6 +511,16 @@ void verifyApplicationIsFullyInitialized() { throw new IllegalStateException("server is not fully initialized"); } + // Returns a LegacyAddress for the string, or a RuntimeException if invalid. + LegacyAddress getValidBsqLegacyAddress(String address) { + try { + return bsqFormatter.getAddressFromBsqAddress(address); + } catch (Throwable t) { + log.error("", t); + throw new IllegalStateException(format("%s is not a valid bsq address", address)); + } + } + // Throws a RuntimeException if wallet currency code is not BSQ or BTC. private void verifyWalletCurrencyCodeIsValid(String currencyCode) { if (currencyCode == null || currencyCode.isEmpty()) @@ -575,16 +585,6 @@ private BtcBalanceInfo getBtcBalances() { lockedBalance.value); } - // Returns a LegacyAddress for the string, or a RuntimeException if invalid. - private LegacyAddress getValidBsqLegacyAddress(String address) { - try { - return bsqFormatter.getAddressFromBsqAddress(address); - } catch (Throwable t) { - log.error("", t); - throw new IllegalStateException(format("%s is not a valid bsq address", address)); - } - } - // Returns a Coin for the transfer amount string, or a RuntimeException if invalid. private Coin getValidTransferAmount(String amount, CoinFormatter coinFormatter) { Coin amountAsCoin = parseToCoin(amount, coinFormatter); diff --git a/core/src/main/resources/help/createcryptopaymentacct-help.txt b/core/src/main/resources/help/createcryptopaymentacct-help.txt new file mode 100644 index 00000000000..c1724550e1c --- /dev/null +++ b/core/src/main/resources/help/createcryptopaymentacct-help.txt @@ -0,0 +1,39 @@ +createcryptopaymentacct + +NAME +---- +createcryptopaymentacct - create a cryptocurrency payment account + +SYNOPSIS +-------- +createcryptopaymentacct + --account-name= + --currency-code= + --address= + +DESCRIPTION +----------- +Creates a cryptocurrency (altcoin) trading account for buying and selling BTC. + +OPTIONS +------- +--account-name + The name of the cryptocurrency payment account. + +--currency-code + The three letter code for the cryptocurrency used to buy or sell BTC, e.g., BSQ. + +--address + A valid BSQ wallet address. + +EXAMPLES +-------- +To create a new BSQ payment account, find an unused BSQ wallet address: +$ ./bisq-cli --password=xyz --port=9998 getunusedbsqaddress + +With the returned BSQ address, e.g., Bn3PCQgRwhkrGnaMp1RYwt9tFwL51YELqne create the cryptocurrency payment account: +$ ./bisq-cli --password=xyz --port=9998 createcryptopaymentacct \ + --account-name="My BSQ Account" \ + --currency-code=BSQ \ + --address=Bn3PCQgRwhkrGnaMp1RYwt9tFwL51YELqne + diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java index e1df2b16cb1..af801004841 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java @@ -21,8 +21,12 @@ import bisq.core.payment.PaymentAccount; import bisq.core.payment.payload.PaymentMethod; +import bisq.proto.grpc.CreateCryptoCurrencyPaymentAccountReply; +import bisq.proto.grpc.CreateCryptoCurrencyPaymentAccountRequest; import bisq.proto.grpc.CreatePaymentAccountReply; import bisq.proto.grpc.CreatePaymentAccountRequest; +import bisq.proto.grpc.GetCryptoCurrencyPaymentMethodsReply; +import bisq.proto.grpc.GetCryptoCurrencyPaymentMethodsRequest; import bisq.proto.grpc.GetPaymentAccountFormReply; import bisq.proto.grpc.GetPaymentAccountFormRequest; import bisq.proto.grpc.GetPaymentAccountsReply; @@ -125,6 +129,39 @@ public void getPaymentAccountForm(GetPaymentAccountFormRequest req, } } + @Override + public void createCryptoCurrencyPaymentAccount(CreateCryptoCurrencyPaymentAccountRequest req, + StreamObserver responseObserver) { + try { + PaymentAccount paymentAccount = coreApi.createCryptoCurrencyPaymentAccount(req.getAccountName(), + req.getCurrencyCode(), + req.getAddress()); + var reply = CreateCryptoCurrencyPaymentAccountReply.newBuilder() + .setPaymentAccount(paymentAccount.toProtoMessage()) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + @Override + public void getCryptoCurrencyPaymentMethods(GetCryptoCurrencyPaymentMethodsRequest req, + StreamObserver responseObserver) { + try { + var paymentMethods = coreApi.getCryptoCurrencyPaymentMethods().stream() + .map(PaymentMethod::toProtoMessage) + .collect(Collectors.toList()); + var reply = GetCryptoCurrencyPaymentMethodsReply.newBuilder() + .addAllPaymentMethods(paymentMethods).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + final ServerInterceptor[] interceptors() { Optional rateMeteringInterceptor = rateMeteringInterceptor(); return rateMeteringInterceptor.map(serverInterceptor -> diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index 8a819ff83ba..3fd783ada38 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -170,6 +170,10 @@ service PaymentAccounts { } rpc GetPaymentAccountForm (GetPaymentAccountFormRequest) returns (GetPaymentAccountFormReply) { } + rpc CreateCryptoCurrencyPaymentAccount (CreateCryptoCurrencyPaymentAccountRequest) returns (CreateCryptoCurrencyPaymentAccountReply) { + } + rpc GetCryptoCurrencyPaymentMethods (GetCryptoCurrencyPaymentMethodsRequest) returns (GetCryptoCurrencyPaymentMethodsReply) { + } } message CreatePaymentAccountRequest { @@ -202,6 +206,23 @@ message GetPaymentAccountFormReply { string paymentAccountFormJson = 1; } +message CreateCryptoCurrencyPaymentAccountRequest { + string accountName = 1; + string currencyCode = 2; + string address = 3; +} + +message CreateCryptoCurrencyPaymentAccountReply { + PaymentAccount paymentAccount = 1; +} + +message GetCryptoCurrencyPaymentMethodsRequest { +} + +message GetCryptoCurrencyPaymentMethodsReply { + repeated PaymentMethod paymentMethods = 1; +} + /////////////////////////////////////////////////////////////////////////////////////////// // Price /////////////////////////////////////////////////////////////////////////////////////////// From 27b090005dfc601352fd366bb2bbab27b520a32d Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 25 Mar 2021 18:46:41 -0300 Subject: [PATCH 02/10] Add cli side help for createcryptopaymentacct --- cli/src/main/java/bisq/cli/CliMain.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index 37bda937eb8..4b6568712b4 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -693,7 +693,7 @@ private static void printHelp(OptionParser parser, @SuppressWarnings("SameParame stream.println(); parser.printHelpOn(stream); stream.println(); - String rowFormat = "%-24s%-52s%s%n"; + String rowFormat = "%-25s%-52s%s%n"; stream.format(rowFormat, "Method", "Params", "Description"); stream.format(rowFormat, "------", "------", "------------"); stream.format(rowFormat, getversion.name(), "", "Get server version"); @@ -765,7 +765,9 @@ private static void printHelp(OptionParser parser, @SuppressWarnings("SameParame stream.println(); stream.format(rowFormat, createpaymentacct.name(), "--payment-account-form=", "Create a new payment account"); stream.println(); - stream.format(rowFormat, createcryptopaymentacct.name(), "--TODO=", "Create a new cryptocurrency payment account"); + stream.format(rowFormat, createcryptopaymentacct.name(), "--account-name= \\", "Create a new cryptocurrency payment account"); + stream.format(rowFormat, "", "--currency-code= \\", ""); + stream.format(rowFormat, "", "--address=", ""); stream.println(); stream.format(rowFormat, getpaymentaccts.name(), "", "Get user payment accounts"); stream.println(); From 9e035e5542f94b5298bc302b43bbc09460c319dd Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 25 Mar 2021 19:23:06 -0300 Subject: [PATCH 03/10] Provide more offer & contract detail available to CLI. This change adds offer and trade contract detail to the API's Offer and Trade protos, and improves CLI output formatting. - Appended missing fields to OfferInfo proto message: uint64 sellerSecurityDeposit = 20; string offerFeePaymentTxId = 21; uint64 txFee = 22; uint64 makerFee = 23; - Added new api proto messages ContractInfo and PaymentAccountPayloadInfo. Lighterweight protos are needed because core Trade/Contract classes are not visible to CLI. - Appended ContractInfo field to api proto message TradeInfo. - Added proto / model converters for ContractInfo and PaymentAccountPayloadInfo, and adjusted OfferInfo & TradeInfo. - Improved CLI output formatting. Added more trade detail to CLI's gettrade output, and prepared to support BTC/BSQ trading pair. Note a reviewer is advised to look at the CLI outout formatting class files instead getting bogged down in the many commit changes. --- .../java/bisq/cli/ColumnHeaderConstants.java | 9 +- .../main/java/bisq/cli/CurrencyFormat.java | 46 ++++- .../main/java/bisq/cli/DirectionFormat.java | 60 +++++++ cli/src/main/java/bisq/cli/TableFormat.java | 166 +++++++++++++----- cli/src/main/java/bisq/cli/TradeFormat.java | 145 +++++++++------ .../bisq/core/api/model/ContractInfo.java | 126 +++++++++++++ .../java/bisq/core/api/model/OfferInfo.java | 52 +++++- .../core/api/model/PaymentAccountForm.java | 10 +- .../api/model/PaymentAccountPayloadInfo.java | 76 ++++++++ .../java/bisq/core/api/model/TradeInfo.java | 63 ++++++- .../main/java/bisq/core/api/model/TxInfo.java | 22 +-- proto/src/main/proto/grpc.proto | 26 +++ 12 files changed, 673 insertions(+), 128 deletions(-) create mode 100644 cli/src/main/java/bisq/cli/DirectionFormat.java create mode 100644 core/src/main/java/bisq/core/api/model/ContractInfo.java create mode 100644 core/src/main/java/bisq/core/api/model/PaymentAccountPayloadInfo.java diff --git a/cli/src/main/java/bisq/cli/ColumnHeaderConstants.java b/cli/src/main/java/bisq/cli/ColumnHeaderConstants.java index 79aff0d3f9d..0ad303dd64d 100644 --- a/cli/src/main/java/bisq/cli/ColumnHeaderConstants.java +++ b/cli/src/main/java/bisq/cli/ColumnHeaderConstants.java @@ -30,7 +30,7 @@ class ColumnHeaderConstants { // expected max data string length is accounted for. In others, column header // lengths are expected to be greater than any column value length. static final String COL_HEADER_ADDRESS = padEnd("%-3s Address", 52, ' '); - static final String COL_HEADER_AMOUNT = padEnd("BTC(min - max)", 24, ' '); + static final String COL_HEADER_AMOUNT = "BTC(min - max)"; static final String COL_HEADER_AVAILABLE_BALANCE = "Available Balance"; static final String COL_HEADER_AVAILABLE_CONFIRMED_BALANCE = "Available Confirmed Balance"; static final String COL_HEADER_UNCONFIRMED_CHANGE_BALANCE = "Unconfirmed Change Balance"; @@ -49,6 +49,7 @@ class ColumnHeaderConstants { static final String COL_HEADER_NAME = "Name"; static final String COL_HEADER_PAYMENT_METHOD = "Payment Method"; static final String COL_HEADER_PRICE = "Price in %-3s for 1 BTC"; + static final String COL_HEADER_PRICE_OF_ALTCOIN = "Price in BTC for 1 %-3s"; static final String COL_HEADER_TRADE_AMOUNT = padStart("Amount(%-3s)", 12, ' '); static final String COL_HEADER_TRADE_BUYER_COST = padEnd("Buyer Cost(%-3s)", 15, ' '); static final String COL_HEADER_TRADE_DEPOSIT_CONFIRMED = "Deposit Confirmed"; @@ -59,8 +60,9 @@ class ColumnHeaderConstants { static final String COL_HEADER_TRADE_WITHDRAWN = "Withdrawn"; static final String COL_HEADER_TRADE_ROLE = "My Role"; static final String COL_HEADER_TRADE_SHORT_ID = "ID"; - static final String COL_HEADER_TRADE_TX_FEE = "Tx Fee(%-3s)"; - static final String COL_HEADER_TRADE_TAKER_FEE = "Taker Fee(%-3s)"; + static final String COL_HEADER_TRADE_TX_FEE = padEnd("Tx Fee(BTC)", 12, ' '); + static final String COL_HEADER_TRADE_MAKER_FEE = padEnd("Maker Fee(%-3s)", 12, ' '); // "Maker Fee(%-3s)"; + static final String COL_HEADER_TRADE_TAKER_FEE = padEnd("Taker Fee(%-3s)", 12, ' '); // "Taker Fee(%-3s)"; static final String COL_HEADER_TX_ID = "Tx ID"; static final String COL_HEADER_TX_INPUT_SUM = "Tx Inputs (BTC)"; @@ -71,5 +73,6 @@ class ColumnHeaderConstants { static final String COL_HEADER_TX_MEMO = "Memo"; static final String COL_HEADER_VOLUME = padEnd("%-3s(min - max)", 15, ' '); + static final String COL_HEADER_UUID = padEnd("ID", 52, ' '); } diff --git a/cli/src/main/java/bisq/cli/CurrencyFormat.java b/cli/src/main/java/bisq/cli/CurrencyFormat.java index afa97b34300..9bada381a12 100644 --- a/cli/src/main/java/bisq/cli/CurrencyFormat.java +++ b/cli/src/main/java/bisq/cli/CurrencyFormat.java @@ -25,23 +25,25 @@ import java.text.NumberFormat; import java.math.BigDecimal; -import java.math.RoundingMode; import java.util.Locale; import static java.lang.String.format; +import static java.math.RoundingMode.HALF_UP; +import static java.math.RoundingMode.UNNECESSARY; @VisibleForTesting public class CurrencyFormat { private static final NumberFormat NUMBER_FORMAT = NumberFormat.getInstance(Locale.US); - static final BigDecimal SATOSHI_DIVISOR = new BigDecimal(100000000); + static final BigDecimal SATOSHI_DIVISOR = new BigDecimal(100_000_000); static final DecimalFormat BTC_FORMAT = new DecimalFormat("###,##0.00000000"); static final DecimalFormat BTC_TX_FEE_FORMAT = new DecimalFormat("###,###,##0"); static final BigDecimal BSQ_SATOSHI_DIVISOR = new BigDecimal(100); static final DecimalFormat BSQ_FORMAT = new DecimalFormat("###,###,###,##0.00"); + static final DecimalFormat SEND_BSQ_FORMAT = new DecimalFormat("###########0.00"); static final BigDecimal SECURITY_DEPOSIT_MULTIPLICAND = new BigDecimal("0.01"); @@ -55,6 +57,14 @@ public static String formatBsq(long sats) { return BSQ_FORMAT.format(BigDecimal.valueOf(sats).divide(BSQ_SATOSHI_DIVISOR)); } + public static String formatBsqSendAmount(long bsqSats) { + // BSQ sats = trade.getOffer().getVolume() + NUMBER_FORMAT.setMinimumFractionDigits(2); + NUMBER_FORMAT.setMaximumFractionDigits(2); + NUMBER_FORMAT.setRoundingMode(HALF_UP); + return SEND_BSQ_FORMAT.format((double) bsqSats / SATOSHI_DIVISOR.doubleValue()); + } + public static String formatTxFeeRateInfo(TxFeeRateInfo txFeeRateInfo) { if (txFeeRateInfo.getUseCustomTxFeeRate()) return format("custom tx fee rate: %s sats/byte, network rate: %s sats/byte", @@ -77,22 +87,44 @@ public static String formatVolumeRange(long minVolume, long volume) { : formatOfferVolume(volume); } + public static String formatCryptoCurrencyVolumeRange(long minVolume, long volume) { + return minVolume != volume + ? formatCryptoCurrencyOfferVolume(minVolume) + " - " + formatCryptoCurrencyOfferVolume(volume) + : formatCryptoCurrencyOfferVolume(volume); + } + public static String formatMarketPrice(double price) { NUMBER_FORMAT.setMinimumFractionDigits(4); + NUMBER_FORMAT.setMaximumFractionDigits(4); return NUMBER_FORMAT.format(price); } public static String formatOfferPrice(long price) { - NUMBER_FORMAT.setMaximumFractionDigits(4); NUMBER_FORMAT.setMinimumFractionDigits(4); - NUMBER_FORMAT.setRoundingMode(RoundingMode.UNNECESSARY); - return NUMBER_FORMAT.format((double) price / 10000); + NUMBER_FORMAT.setMaximumFractionDigits(4); + NUMBER_FORMAT.setRoundingMode(UNNECESSARY); + return NUMBER_FORMAT.format((double) price / 10_000); + } + + public static String formatCryptoCurrencyOfferPrice(long price) { + NUMBER_FORMAT.setMinimumFractionDigits(8); + NUMBER_FORMAT.setMaximumFractionDigits(8); + NUMBER_FORMAT.setRoundingMode(UNNECESSARY); + return NUMBER_FORMAT.format((double) price / SATOSHI_DIVISOR.doubleValue()); } public static String formatOfferVolume(long volume) { + NUMBER_FORMAT.setMinimumFractionDigits(0); NUMBER_FORMAT.setMaximumFractionDigits(0); - NUMBER_FORMAT.setRoundingMode(RoundingMode.UNNECESSARY); - return NUMBER_FORMAT.format((double) volume / 10000); + NUMBER_FORMAT.setRoundingMode(HALF_UP); + return NUMBER_FORMAT.format((double) volume / 10_000); + } + + public static String formatCryptoCurrencyOfferVolume(long volume) { + NUMBER_FORMAT.setMinimumFractionDigits(2); + NUMBER_FORMAT.setMaximumFractionDigits(2); + NUMBER_FORMAT.setRoundingMode(HALF_UP); + return NUMBER_FORMAT.format((double) volume / SATOSHI_DIVISOR.doubleValue()); } public static long toSatoshis(String btc) { diff --git a/cli/src/main/java/bisq/cli/DirectionFormat.java b/cli/src/main/java/bisq/cli/DirectionFormat.java new file mode 100644 index 00000000000..ac0e5b6c556 --- /dev/null +++ b/cli/src/main/java/bisq/cli/DirectionFormat.java @@ -0,0 +1,60 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.cli; + +import bisq.proto.grpc.OfferInfo; + +import java.util.List; +import java.util.function.Function; + +import static bisq.cli.ColumnHeaderConstants.COL_HEADER_DIRECTION; +import static java.lang.String.format; +import static protobuf.OfferPayload.Direction.BUY; +import static protobuf.OfferPayload.Direction.SELL; + +class DirectionFormat { + + static int getLongestDirectionColWidth(List offers) { + if (offers.isEmpty() || offers.get(0).getBaseCurrencyCode().equals("BTC")) + return COL_HEADER_DIRECTION.length(); + else + return 18; // .e.g., "Sell BSQ (Buy BTC)".length() + } + + static final Function directionFormat = (offer) -> { + String baseCurrencyCode = offer.getBaseCurrencyCode(); + boolean isCryptoCurrencyOffer = !baseCurrencyCode.equals("BTC"); + if (!isCryptoCurrencyOffer) { + return baseCurrencyCode; + } else { + // Return "Sell BSQ (Buy BTC)", or "Buy BSQ (Sell BTC)". + String direction = offer.getDirection(); + String mirroredDirection = getMirroredDirection(direction); + Function mixedCase = (word) -> word.charAt(0) + word.substring(1).toLowerCase(); + return format("%s %s (%s %s)", + mixedCase.apply(mirroredDirection), + baseCurrencyCode, + mixedCase.apply(direction), + offer.getCounterCurrencyCode()); + } + }; + + static String getMirroredDirection(String directionAsString) { + return directionAsString.equalsIgnoreCase(BUY.name()) ? SELL.name() : BUY.name(); + } +} diff --git a/cli/src/main/java/bisq/cli/TableFormat.java b/cli/src/main/java/bisq/cli/TableFormat.java index 112de0e7d74..fbc99459375 100644 --- a/cli/src/main/java/bisq/cli/TableFormat.java +++ b/cli/src/main/java/bisq/cli/TableFormat.java @@ -36,7 +36,10 @@ import static bisq.cli.ColumnHeaderConstants.*; import static bisq.cli.CurrencyFormat.*; +import static bisq.cli.DirectionFormat.directionFormat; +import static bisq.cli.DirectionFormat.getLongestDirectionColWidth; import static com.google.common.base.Strings.padEnd; +import static com.google.common.base.Strings.padStart; import static java.lang.String.format; import static java.util.Collections.max; import static java.util.Comparator.comparing; @@ -114,31 +117,69 @@ public static String formatBtcBalanceInfoTbl(BtcBalanceInfo btcBalanceInfo) { formatSatoshis(btcBalanceInfo.getLockedBalance())); } - public static String formatOfferTable(List offerInfo, String fiatCurrency) { + public static String formatPaymentAcctTbl(List paymentAccounts) { // Some column values might be longer than header, so we need to calculate them. - int paymentMethodColWidth = getLengthOfLongestColumn( + int nameColWidth = getLongestColumnSize( + COL_HEADER_NAME.length(), + paymentAccounts.stream().map(PaymentAccount::getAccountName) + .collect(Collectors.toList())); + int paymentMethodColWidth = getLongestColumnSize( COL_HEADER_PAYMENT_METHOD.length(), - offerInfo.stream() - .map(OfferInfo::getPaymentMethodShortName) + paymentAccounts.stream().map(a -> a.getPaymentMethod().getId()) .collect(Collectors.toList())); + String headerLine = padEnd(COL_HEADER_NAME, nameColWidth, ' ') + COL_HEADER_DELIMITER + + COL_HEADER_CURRENCY + COL_HEADER_DELIMITER + + padEnd(COL_HEADER_PAYMENT_METHOD, paymentMethodColWidth, ' ') + COL_HEADER_DELIMITER + + COL_HEADER_UUID + COL_HEADER_DELIMITER + "\n"; + String colDataFormat = "%-" + nameColWidth + "s" // left justify + + " %-" + COL_HEADER_CURRENCY.length() + "s" // left justify + + " %-" + paymentMethodColWidth + "s" // left justify + + " %-" + COL_HEADER_UUID.length() + "s"; // left justify + return headerLine + + paymentAccounts.stream() + .map(a -> format(colDataFormat, + a.getAccountName(), + a.getSelectedTradeCurrency().getCode(), + a.getPaymentMethod().getId(), + a.getId())) + .collect(Collectors.joining("\n")); + } + + public static String formatOfferTable(List offers, String currencyCode) { + if (offers == null || offers.isEmpty()) + throw new IllegalArgumentException(format("%s offers argument is empty", currencyCode.toLowerCase())); + + String baseCurrencyCode = offers.get(0).getBaseCurrencyCode(); + return baseCurrencyCode.equalsIgnoreCase("BTC") + ? formatFiatOfferTable(offers, currencyCode) + : formatCryptoCurrencyOfferTable(offers, baseCurrencyCode); + } + + private static String formatFiatOfferTable(List offers, String fiatCurrencyCode) { + // Some column values might be longer than header, so we need to calculate them. + int amountColWith = getLongestAmountColWidth(offers); + int volumeColWidth = getLongestVolumeColWidth(offers); + int paymentMethodColWidth = getLongestPaymentMethodColWidth(offers); String headersFormat = COL_HEADER_DIRECTION + COL_HEADER_DELIMITER - + COL_HEADER_PRICE + COL_HEADER_DELIMITER // includes %s -> fiatCurrency - + COL_HEADER_AMOUNT + COL_HEADER_DELIMITER - + COL_HEADER_VOLUME + COL_HEADER_DELIMITER // includes %s -> fiatCurrency + + COL_HEADER_PRICE + COL_HEADER_DELIMITER // includes %s -> fiatCurrencyCode + + padStart(COL_HEADER_AMOUNT, amountColWith, ' ') + COL_HEADER_DELIMITER + // COL_HEADER_VOLUME includes %s -> fiatCurrencyCode + + padStart(COL_HEADER_VOLUME, volumeColWidth, ' ') + COL_HEADER_DELIMITER + padEnd(COL_HEADER_PAYMENT_METHOD, paymentMethodColWidth, ' ') + COL_HEADER_DELIMITER + COL_HEADER_CREATION_DATE + COL_HEADER_DELIMITER + COL_HEADER_UUID.trim() + "%n"; - String headerLine = format(headersFormat, fiatCurrency.toUpperCase(), fiatCurrency.toUpperCase()); - - String colDataFormat = "%-" + (COL_HEADER_DIRECTION.length() + COL_HEADER_DELIMITER.length()) + "s" // left - + "%" + (COL_HEADER_PRICE.length() - 1) + "s" // rt justify to end of hdr - + " %-" + (COL_HEADER_AMOUNT.length() - 1) + "s" // left justify - + " %" + COL_HEADER_VOLUME.length() + "s" // right justify - + " %-" + paymentMethodColWidth + "s" // left justify - + " %-" + (COL_HEADER_CREATION_DATE.length()) + "s" // left justify + String headerLine = format(headersFormat, + fiatCurrencyCode.toUpperCase(), + fiatCurrencyCode.toUpperCase()); + String colDataFormat = "%-" + (COL_HEADER_DIRECTION.length() + COL_HEADER_DELIMITER.length()) + "s" + + "%" + (COL_HEADER_PRICE.length() - 1) + "s" + + " %" + amountColWith + "s" + + " %" + (volumeColWidth - 1) + "s" + + " %-" + paymentMethodColWidth + "s" + + " %-" + (COL_HEADER_CREATION_DATE.length()) + "s" + " %-" + COL_HEADER_UUID.length() + "s"; return headerLine - + offerInfo.stream() + + offers.stream() .map(o -> format(colDataFormat, o.getDirection(), formatOfferPrice(o.getPrice()), @@ -150,37 +191,80 @@ public static String formatOfferTable(List offerInfo, String fiatCurr .collect(Collectors.joining("\n")); } - public static String formatPaymentAcctTbl(List paymentAccounts) { + private static String formatCryptoCurrencyOfferTable(List offers, String cryptoCurrencyCode) { // Some column values might be longer than header, so we need to calculate them. - int nameColWidth = getLengthOfLongestColumn( - COL_HEADER_NAME.length(), - paymentAccounts.stream().map(PaymentAccount::getAccountName) - .collect(Collectors.toList())); - int paymentMethodColWidth = getLengthOfLongestColumn( - COL_HEADER_PAYMENT_METHOD.length(), - paymentAccounts.stream().map(a -> a.getPaymentMethod().getId()) - .collect(Collectors.toList())); - - String headerLine = padEnd(COL_HEADER_NAME, nameColWidth, ' ') + COL_HEADER_DELIMITER - + COL_HEADER_CURRENCY + COL_HEADER_DELIMITER + int directionColWidth = getLongestDirectionColWidth(offers); + int amountColWith = getLongestAmountColWidth(offers); + int volumeColWidth = getLongestCryptoCurrencyVolumeColWidth(offers); + int paymentMethodColWidth = getLongestPaymentMethodColWidth(offers); + // TODO use memoize function to avoid duplicate the formatting done above? + String headersFormat = padEnd(COL_HEADER_DIRECTION, directionColWidth, ' ') + COL_HEADER_DELIMITER + + COL_HEADER_PRICE_OF_ALTCOIN + COL_HEADER_DELIMITER // includes %s -> cryptoCurrencyCode + + padStart(COL_HEADER_AMOUNT, amountColWith, ' ') + COL_HEADER_DELIMITER + // COL_HEADER_VOLUME includes %s -> cryptoCurrencyCode + + padStart(COL_HEADER_VOLUME, volumeColWidth, ' ') + COL_HEADER_DELIMITER + padEnd(COL_HEADER_PAYMENT_METHOD, paymentMethodColWidth, ' ') + COL_HEADER_DELIMITER - + COL_HEADER_UUID + COL_HEADER_DELIMITER + "\n"; - String colDataFormat = "%-" + nameColWidth + "s" // left justify - + " %-" + COL_HEADER_CURRENCY.length() + "s" // left justify - + " %-" + paymentMethodColWidth + "s" // left justify - + " %-" + COL_HEADER_UUID.length() + "s"; // left justify + + COL_HEADER_CREATION_DATE + COL_HEADER_DELIMITER + + COL_HEADER_UUID.trim() + "%n"; + String headerLine = format(headersFormat, + cryptoCurrencyCode.toUpperCase(), + cryptoCurrencyCode.toUpperCase()); + String colDataFormat = "%-" + directionColWidth + "s" + + "%" + (COL_HEADER_PRICE_OF_ALTCOIN.length() + 1) + "s" + + " %" + amountColWith + "s" + + " %" + (volumeColWidth - 1) + "s" + + " %-" + paymentMethodColWidth + "s" + + " %-" + (COL_HEADER_CREATION_DATE.length()) + "s" + + " %-" + COL_HEADER_UUID.length() + "s"; return headerLine - + paymentAccounts.stream() - .map(a -> format(colDataFormat, - a.getAccountName(), - a.getSelectedTradeCurrency().getCode(), - a.getPaymentMethod().getId(), - a.getId())) + + offers.stream() + .map(o -> format(colDataFormat, + directionFormat.apply(o), + formatCryptoCurrencyOfferPrice(o.getPrice()), + formatAmountRange(o.getMinAmount(), o.getAmount()), + formatCryptoCurrencyVolumeRange(o.getMinVolume(), o.getVolume()), + o.getPaymentMethodShortName(), + formatTimestamp(o.getDate()), + o.getId())) .collect(Collectors.joining("\n")); } - // Return length of the longest string value, or the header.len, whichever is greater. - private static int getLengthOfLongestColumn(int headerLength, List strings) { + private static int getLongestPaymentMethodColWidth(List offers) { + return getLongestColumnSize( + COL_HEADER_PAYMENT_METHOD.length(), + offers.stream() + .map(OfferInfo::getPaymentMethodShortName) + .collect(Collectors.toList())); + } + + private static int getLongestAmountColWidth(List offers) { + return getLongestColumnSize( + COL_HEADER_AMOUNT.length(), + offers.stream() + .map(o -> formatAmountRange(o.getMinAmount(), o.getAmount())) + .collect(Collectors.toList())); + } + + private static int getLongestVolumeColWidth(List offers) { + // Pad this col width by 1 space. + return 1 + getLongestColumnSize( + COL_HEADER_VOLUME.length(), + offers.stream() + .map(o -> formatVolumeRange(o.getMinVolume(), o.getVolume())) + .collect(Collectors.toList())); + } + + private static int getLongestCryptoCurrencyVolumeColWidth(List offers) { + // Pad this col width by 1 space. + return 1 + getLongestColumnSize( + COL_HEADER_VOLUME.length(), + offers.stream() + .map(o -> formatCryptoCurrencyVolumeRange(o.getMinVolume(), o.getVolume())) + .collect(Collectors.toList())); + } + + // Return size of the longest string value, or the header.len, whichever is greater. + private static int getLongestColumnSize(int headerLength, List strings) { int longest = max(strings, comparing(String::length)).length(); return Math.max(longest, headerLength); } diff --git a/cli/src/main/java/bisq/cli/TradeFormat.java b/cli/src/main/java/bisq/cli/TradeFormat.java index 668f9603552..4ca1a20d1ab 100644 --- a/cli/src/main/java/bisq/cli/TradeFormat.java +++ b/cli/src/main/java/bisq/cli/TradeFormat.java @@ -21,17 +21,20 @@ import com.google.common.annotations.VisibleForTesting; +import java.util.function.BiFunction; +import java.util.function.Function; import java.util.function.Supplier; import static bisq.cli.ColumnHeaderConstants.*; -import static bisq.cli.CurrencyFormat.formatOfferPrice; -import static bisq.cli.CurrencyFormat.formatOfferVolume; -import static bisq.cli.CurrencyFormat.formatSatoshis; +import static bisq.cli.CurrencyFormat.*; import static com.google.common.base.Strings.padEnd; @VisibleForTesting public class TradeFormat { + private static final String YES = "YES"; + private static final String NO = "NO"; + @VisibleForTesting public static String format(TradeInfo tradeInfo) { // Some column values might be longer than header, so we need to calculate them. @@ -40,19 +43,27 @@ public static String format(TradeInfo tradeInfo) { // We only show taker fee under its header when user is the taker. boolean isTaker = tradeInfo.getRole().toLowerCase().contains("taker"); - Supplier takerFeeHeaderFormat = () -> isTaker ? - padEnd(COL_HEADER_TRADE_TAKER_FEE, 12, ' ') + COL_HEADER_DELIMITER + Supplier makerFeeHeader = () -> !isTaker ? + COL_HEADER_TRADE_MAKER_FEE + COL_HEADER_DELIMITER + : ""; + Supplier makerFeeHeaderSpec = () -> !isTaker ? + "%" + (COL_HEADER_TRADE_MAKER_FEE.length() + 2) + "s" : ""; Supplier takerFeeHeader = () -> isTaker ? - "%" + (COL_HEADER_TRADE_TAKER_FEE.length() + 1) + "s" + COL_HEADER_TRADE_TAKER_FEE + COL_HEADER_DELIMITER + : ""; + Supplier takerFeeHeaderSpec = () -> isTaker ? + "%" + (COL_HEADER_TRADE_TAKER_FEE.length() + 2) + "s" : ""; String headersFormat = padEnd(COL_HEADER_TRADE_SHORT_ID, shortIdColWidth, ' ') + COL_HEADER_DELIMITER + padEnd(COL_HEADER_TRADE_ROLE, roleColWidth, ' ') + COL_HEADER_DELIMITER - + COL_HEADER_PRICE + COL_HEADER_DELIMITER // includes %s -> currencyCode + + priceHeader.apply(tradeInfo) + COL_HEADER_DELIMITER // includes %s -> currencyCode + padEnd(COL_HEADER_TRADE_AMOUNT, 12, ' ') + COL_HEADER_DELIMITER + padEnd(COL_HEADER_TRADE_TX_FEE, 12, ' ') + COL_HEADER_DELIMITER - + takerFeeHeaderFormat.get() + + makerFeeHeader.get() + // maker or taker fee header, not both + + takerFeeHeader.get() + COL_HEADER_TRADE_DEPOSIT_PUBLISHED + COL_HEADER_DELIMITER + COL_HEADER_TRADE_DEPOSIT_CONFIRMED + COL_HEADER_DELIMITER + COL_HEADER_TRADE_BUYER_COST + COL_HEADER_DELIMITER @@ -65,30 +76,22 @@ public static String format(TradeInfo tradeInfo) { String counterCurrencyCode = tradeInfo.getOffer().getCounterCurrencyCode(); String baseCurrencyCode = tradeInfo.getOffer().getBaseCurrencyCode(); - // The taker's output contains an extra taker tx fee column. - String headerLine = isTaker - ? String.format(headersFormat, - /* COL_HEADER_PRICE */ counterCurrencyCode, - /* COL_HEADER_TRADE_AMOUNT */ baseCurrencyCode, - /* COL_HEADER_TRADE_TX_FEE */ baseCurrencyCode, - /* COL_HEADER_TRADE_TAKER_FEE */ baseCurrencyCode, - /* COL_HEADER_TRADE_BUYER_COST */ counterCurrencyCode, - /* COL_HEADER_TRADE_PAYMENT_SENT */ counterCurrencyCode, - /* COL_HEADER_TRADE_PAYMENT_RECEIVED */ counterCurrencyCode) - : String.format(headersFormat, - /* COL_HEADER_PRICE */ counterCurrencyCode, + String headerLine = String.format(headersFormat, + /* COL_HEADER_PRICE */ priceHeaderCurrencyCode.apply(tradeInfo), /* COL_HEADER_TRADE_AMOUNT */ baseCurrencyCode, - /* COL_HEADER_TRADE_TX_FEE */ baseCurrencyCode, + /* COL_HEADER_TRADE_(M||T)AKER_FEE */ makerTakerFeeHeaderCurrencyCode.apply(tradeInfo), /* COL_HEADER_TRADE_BUYER_COST */ counterCurrencyCode, - /* COL_HEADER_TRADE_PAYMENT_SENT */ counterCurrencyCode, - /* COL_HEADER_TRADE_PAYMENT_RECEIVED */ counterCurrencyCode); + /* COL_HEADER_TRADE_PAYMENT_SENT */ paymentStatusHeaderCurrencyCode.apply(tradeInfo), + /* COL_HEADER_TRADE_PAYMENT_RECEIVED */ paymentStatusHeaderCurrencyCode.apply(tradeInfo)); String colDataFormat = "%-" + shortIdColWidth + "s" // lt justify + " %-" + (roleColWidth + COL_HEADER_DELIMITER.length()) + "s" // left + "%" + (COL_HEADER_PRICE.length() - 1) + "s" // rt justify + "%" + (COL_HEADER_TRADE_AMOUNT.length() + 1) + "s" // rt justify + "%" + (COL_HEADER_TRADE_TX_FEE.length() + 1) + "s" // rt justify - + takerFeeHeader.get() // rt justify + + makerFeeHeaderSpec.get() // rt justify + // OR (one of them is an empty string) + + takerFeeHeaderSpec.get() // rt justify + " %-" + COL_HEADER_TRADE_DEPOSIT_PUBLISHED.length() + "s" // lt justify + " %-" + COL_HEADER_TRADE_DEPOSIT_CONFIRMED.length() + "s" // lt justify + "%" + (COL_HEADER_TRADE_BUYER_COST.length() + 1) + "s" // rt justify @@ -97,42 +100,74 @@ public static String format(TradeInfo tradeInfo) { + " %-" + COL_HEADER_TRADE_PAYOUT_PUBLISHED.length() + "s" // lt justify + " %-" + COL_HEADER_TRADE_WITHDRAWN.length() + "s"; // lt justify - return headerLine + - (isTaker - ? formatTradeForTaker(colDataFormat, tradeInfo) - : formatTradeForMaker(colDataFormat, tradeInfo)); + return headerLine + formatTradeData(colDataFormat, tradeInfo, isTaker); } - private static String formatTradeForMaker(String format, TradeInfo tradeInfo) { + private static String formatTradeData(String format, + TradeInfo tradeInfo, + boolean isTaker) { return String.format(format, tradeInfo.getShortId(), tradeInfo.getRole(), - formatOfferPrice(tradeInfo.getTradePrice()), - formatSatoshis(tradeInfo.getTradeAmountAsLong()), - formatSatoshis(tradeInfo.getTxFeeAsLong()), - tradeInfo.getIsDepositPublished() ? "YES" : "NO", - tradeInfo.getIsDepositConfirmed() ? "YES" : "NO", - formatOfferVolume(tradeInfo.getOffer().getVolume()), - tradeInfo.getIsFiatSent() ? "YES" : "NO", - tradeInfo.getIsFiatReceived() ? "YES" : "NO", - tradeInfo.getIsPayoutPublished() ? "YES" : "NO", - tradeInfo.getIsWithdrawn() ? "YES" : "NO"); + priceFormat.apply(tradeInfo), + amountFormat.apply(tradeInfo), + makerTakerMinerTxFeeFormat.apply(tradeInfo, isTaker), + makerTakerFeeFormat.apply(tradeInfo, isTaker), + tradeInfo.getIsDepositPublished() ? YES : NO, + tradeInfo.getIsDepositConfirmed() ? YES : NO, + tradeCostFormat.apply(tradeInfo), + tradeInfo.getIsFiatSent() ? YES : NO, + tradeInfo.getIsFiatReceived() ? YES : NO, + tradeInfo.getIsPayoutPublished() ? YES : NO, + tradeInfo.getIsWithdrawn() ? YES : NO); } - private static String formatTradeForTaker(String format, TradeInfo tradeInfo) { - return String.format(format, - tradeInfo.getShortId(), - tradeInfo.getRole(), - formatOfferPrice(tradeInfo.getTradePrice()), - formatSatoshis(tradeInfo.getTradeAmountAsLong()), - formatSatoshis(tradeInfo.getTxFeeAsLong()), - formatSatoshis(tradeInfo.getTakerFeeAsLong()), - tradeInfo.getIsDepositPublished() ? "YES" : "NO", - tradeInfo.getIsDepositConfirmed() ? "YES" : "NO", - formatOfferVolume(tradeInfo.getOffer().getVolume()), - tradeInfo.getIsFiatSent() ? "YES" : "NO", - tradeInfo.getIsFiatReceived() ? "YES" : "NO", - tradeInfo.getIsPayoutPublished() ? "YES" : "NO", - tradeInfo.getIsWithdrawn() ? "YES" : "NO"); - } + private static final Function priceHeader = (t) -> + t.getOffer().getBaseCurrencyCode().equals("BTC") + ? COL_HEADER_PRICE + : COL_HEADER_PRICE_OF_ALTCOIN; + + private static final Function priceHeaderCurrencyCode = (t) -> + t.getOffer().getBaseCurrencyCode().equals("BTC") + ? t.getOffer().getCounterCurrencyCode() + : t.getOffer().getBaseCurrencyCode(); + + private static final Function makerTakerFeeHeaderCurrencyCode = (t) -> + t.getIsCurrencyForTakerFeeBtc() ? "BTC" : "BSQ"; + + private static final Function paymentStatusHeaderCurrencyCode = (t) -> + t.getOffer().getBaseCurrencyCode().equals("BTC") + ? t.getOffer().getCounterCurrencyCode() + : t.getOffer().getBaseCurrencyCode(); + + private static final Function priceFormat = (t) -> + t.getOffer().getBaseCurrencyCode().equals("BTC") + ? formatOfferPrice(t.getTradePrice()) + : formatCryptoCurrencyOfferPrice(t.getOffer().getPrice()); + + private static final Function amountFormat = (t) -> + t.getOffer().getBaseCurrencyCode().equals("BTC") + ? formatSatoshis(t.getTradeAmountAsLong()) + : formatCryptoCurrencyOfferVolume(t.getOffer().getVolume()); + + private static final BiFunction makerTakerMinerTxFeeFormat = (t, isTaker) -> { + if (isTaker) { + return formatSatoshis(t.getTxFeeAsLong()); + } else { + return formatSatoshis(t.getOffer().getTxFee()); + } + }; + + private static final BiFunction makerTakerFeeFormat = (t, isTaker) -> { + if (isTaker) + return t.getIsCurrencyForTakerFeeBtc() ? formatSatoshis(t.getTakerFeeAsLong()) : formatBsq(t.getTakerFeeAsLong()); + else + return t.getIsCurrencyForTakerFeeBtc() ? formatSatoshis(t.getOffer().getMakerFee()) : formatBsq(t.getOffer().getMakerFee()); + + }; + + private static final Function tradeCostFormat = (t) -> + t.getOffer().getBaseCurrencyCode().equals("BTC") + ? formatOfferVolume(t.getOffer().getVolume()) + : formatSatoshis(t.getTradeAmountAsLong()); } diff --git a/core/src/main/java/bisq/core/api/model/ContractInfo.java b/core/src/main/java/bisq/core/api/model/ContractInfo.java new file mode 100644 index 00000000000..404335c9c7f --- /dev/null +++ b/core/src/main/java/bisq/core/api/model/ContractInfo.java @@ -0,0 +1,126 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.api.model; + +import bisq.common.Payload; + +import java.util.function.Supplier; + +import lombok.Getter; + +import static bisq.core.api.model.PaymentAccountPayloadInfo.emptyPaymentAccountPayload; + +/** + * A lightweight Trade Contract constructed from a trade's json contract. + * Many fields in the core Contract are ignored, but can be added as needed. + */ +@Getter +public class ContractInfo implements Payload { + + private final String buyerNodeAddress; + private final String sellerNodeAddress; + private final String mediatorNodeAddress; + private final String refundAgentNodeAddress; + private final boolean isBuyerMakerAndSellerTaker; + private final String makerAccountId; + private final String takerAccountId; + private final PaymentAccountPayloadInfo makerPaymentAccountPayload; + private final PaymentAccountPayloadInfo takerPaymentAccountPayload; + private final String makerPayoutAddressString; + private final String takerPayoutAddressString; + private final long lockTime; + + public ContractInfo(String buyerNodeAddress, + String sellerNodeAddress, + String mediatorNodeAddress, + String refundAgentNodeAddress, + boolean isBuyerMakerAndSellerTaker, + String makerAccountId, + String takerAccountId, + PaymentAccountPayloadInfo makerPaymentAccountPayload, + PaymentAccountPayloadInfo takerPaymentAccountPayload, + String makerPayoutAddressString, + String takerPayoutAddressString, + long lockTime) { + this.buyerNodeAddress = buyerNodeAddress; + this.sellerNodeAddress = sellerNodeAddress; + this.mediatorNodeAddress = mediatorNodeAddress; + this.refundAgentNodeAddress = refundAgentNodeAddress; + this.isBuyerMakerAndSellerTaker = isBuyerMakerAndSellerTaker; + this.makerAccountId = makerAccountId; + this.takerAccountId = takerAccountId; + this.makerPaymentAccountPayload = makerPaymentAccountPayload; + this.takerPaymentAccountPayload = takerPaymentAccountPayload; + this.makerPayoutAddressString = makerPayoutAddressString; + this.takerPayoutAddressString = takerPayoutAddressString; + this.lockTime = lockTime; + } + + + // For transmitting TradeInfo messages when no contract is available. + public static Supplier emptyContract = () -> + new ContractInfo("", + "", + "", + "", + false, + "", + "", + emptyPaymentAccountPayload.get(), + emptyPaymentAccountPayload.get(), + "", + "", + 0); + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + public static ContractInfo fromProto(bisq.proto.grpc.ContractInfo proto) { + return new ContractInfo(proto.getBuyerNodeAddress(), + proto.getSellerNodeAddress(), + proto.getMediatorNodeAddress(), + proto.getRefundAgentNodeAddress(), + proto.getIsBuyerMakerAndSellerTaker(), + proto.getMakerAccountId(), + proto.getTakerAccountId(), + PaymentAccountPayloadInfo.fromProto(proto.getMakerPaymentAccountPayload()), + PaymentAccountPayloadInfo.fromProto(proto.getTakerPaymentAccountPayload()), + proto.getMakerPayoutAddressString(), + proto.getTakerPayoutAddressString(), + proto.getLockTime()); + } + + @Override + public bisq.proto.grpc.ContractInfo toProtoMessage() { + return bisq.proto.grpc.ContractInfo.newBuilder() + .setBuyerNodeAddress(buyerNodeAddress) + .setSellerNodeAddress(sellerNodeAddress) + .setMediatorNodeAddress(mediatorNodeAddress) + .setRefundAgentNodeAddress(refundAgentNodeAddress) + .setIsBuyerMakerAndSellerTaker(isBuyerMakerAndSellerTaker) + .setMakerAccountId(makerAccountId) + .setTakerAccountId(takerAccountId) + .setMakerPaymentAccountPayload(makerPaymentAccountPayload.toProtoMessage()) + .setTakerPaymentAccountPayload(takerPaymentAccountPayload.toProtoMessage()) + .setMakerPayoutAddressString(makerPayoutAddressString) + .setTakerPayoutAddressString(takerPayoutAddressString) + .setLockTime(lockTime) + .build(); + } +} diff --git a/core/src/main/java/bisq/core/api/model/OfferInfo.java b/core/src/main/java/bisq/core/api/model/OfferInfo.java index 2314cb43086..f8501f7df1f 100644 --- a/core/src/main/java/bisq/core/api/model/OfferInfo.java +++ b/core/src/main/java/bisq/core/api/model/OfferInfo.java @@ -45,7 +45,11 @@ public class OfferInfo implements Payload { private final long minAmount; private final long volume; private final long minVolume; + private final long txFee; + private final long makerFee; + private final String offerFeePaymentTxId; private final long buyerSecurityDeposit; + private final long sellerSecurityDeposit; private final long triggerPrice; private final boolean isCurrencyForMakerFeeBtc; private final String paymentAccountId; @@ -58,6 +62,7 @@ public class OfferInfo implements Payload { private final long date; private final String state; + public OfferInfo(OfferInfoBuilder builder) { this.id = builder.id; this.direction = builder.direction; @@ -68,7 +73,11 @@ public OfferInfo(OfferInfoBuilder builder) { this.minAmount = builder.minAmount; this.volume = builder.volume; this.minVolume = builder.minVolume; + this.txFee = builder.txFee; + this.makerFee = builder.makerFee; + this.offerFeePaymentTxId = builder.offerFeePaymentTxId; this.buyerSecurityDeposit = builder.buyerSecurityDeposit; + this.sellerSecurityDeposit = builder.sellerSecurityDeposit; this.triggerPrice = builder.triggerPrice; this.isCurrencyForMakerFeeBtc = builder.isCurrencyForMakerFeeBtc; this.paymentAccountId = builder.paymentAccountId; @@ -78,6 +87,7 @@ public OfferInfo(OfferInfoBuilder builder) { this.counterCurrencyCode = builder.counterCurrencyCode; this.date = builder.date; this.state = builder.state; + } public static OfferInfo toOfferInfo(Offer offer) { @@ -90,8 +100,8 @@ public static OfferInfo toOfferInfo(Offer offer, long triggerPrice) { return getOfferInfoBuilder(offer).withTriggerPrice(triggerPrice).build(); } - private static OfferInfo.OfferInfoBuilder getOfferInfoBuilder(Offer offer) { - return new OfferInfo.OfferInfoBuilder() + private static OfferInfoBuilder getOfferInfoBuilder(Offer offer) { + return new OfferInfoBuilder() .withId(offer.getId()) .withDirection(offer.getDirection().name()) .withPrice(Objects.requireNonNull(offer.getPrice()).getValue()) @@ -101,7 +111,11 @@ private static OfferInfo.OfferInfoBuilder getOfferInfoBuilder(Offer offer) { .withMinAmount(offer.getMinAmount().value) .withVolume(Objects.requireNonNull(offer.getVolume()).getValue()) .withMinVolume(Objects.requireNonNull(offer.getMinVolume()).getValue()) + .withMakerFee(offer.getMakerFee().value) + .withTxFee(offer.getTxFee().value) + .withOfferFeePaymentTxId(offer.getOfferFeePaymentTxId()) .withBuyerSecurityDeposit(offer.getBuyerSecurityDeposit().value) + .withSellerSecurityDeposit(offer.getSellerSecurityDeposit().value) .withIsCurrencyForMakerFeeBtc(offer.isCurrencyForMakerFeeBtc()) .withPaymentAccountId(offer.getMakerPaymentAccountId()) .withPaymentMethodId(offer.getPaymentMethod().getId()) @@ -128,7 +142,11 @@ public bisq.proto.grpc.OfferInfo toProtoMessage() { .setMinAmount(minAmount) .setVolume(volume) .setMinVolume(minVolume) + .setMakerFee(makerFee) + .setTxFee(txFee) + .setOfferFeePaymentTxId(offerFeePaymentTxId) .setBuyerSecurityDeposit(buyerSecurityDeposit) + .setSellerSecurityDeposit(sellerSecurityDeposit) .setTriggerPrice(triggerPrice) .setIsCurrencyForMakerFeeBtc(isCurrencyForMakerFeeBtc) .setPaymentAccountId(paymentAccountId) @@ -143,7 +161,7 @@ public bisq.proto.grpc.OfferInfo toProtoMessage() { @SuppressWarnings("unused") public static OfferInfo fromProto(bisq.proto.grpc.OfferInfo proto) { - return new OfferInfo.OfferInfoBuilder() + return new OfferInfoBuilder() .withId(proto.getId()) .withDirection(proto.getDirection()) .withPrice(proto.getPrice()) @@ -153,7 +171,11 @@ public static OfferInfo fromProto(bisq.proto.grpc.OfferInfo proto) { .withMinAmount(proto.getMinAmount()) .withVolume(proto.getVolume()) .withMinVolume(proto.getMinVolume()) + .withMakerFee(proto.getMakerFee()) + .withTxFee(proto.getTxFee()) + .withOfferFeePaymentTxId(proto.getOfferFeePaymentTxId()) .withBuyerSecurityDeposit(proto.getBuyerSecurityDeposit()) + .withSellerSecurityDeposit(proto.getSellerSecurityDeposit()) .withTriggerPrice(proto.getTriggerPrice()) .withIsCurrencyForMakerFeeBtc(proto.getIsCurrencyForMakerFeeBtc()) .withPaymentAccountId(proto.getPaymentAccountId()) @@ -182,7 +204,11 @@ public static class OfferInfoBuilder { private long minAmount; private long volume; private long minVolume; + private long txFee; + private long makerFee; + private String offerFeePaymentTxId; private long buyerSecurityDeposit; + private long sellerSecurityDeposit; private long triggerPrice; private boolean isCurrencyForMakerFeeBtc; private String paymentAccountId; @@ -238,11 +264,31 @@ public OfferInfoBuilder withMinVolume(long minVolume) { return this; } + public OfferInfoBuilder withTxFee(long txFee) { + this.txFee = txFee; + return this; + } + + public OfferInfoBuilder withMakerFee(long makerFee) { + this.makerFee = makerFee; + return this; + } + + public OfferInfoBuilder withOfferFeePaymentTxId(String offerFeePaymentTxId) { + this.offerFeePaymentTxId = offerFeePaymentTxId; + return this; + } + public OfferInfoBuilder withBuyerSecurityDeposit(long buyerSecurityDeposit) { this.buyerSecurityDeposit = buyerSecurityDeposit; return this; } + public OfferInfoBuilder withSellerSecurityDeposit(long sellerSecurityDeposit) { + this.sellerSecurityDeposit = sellerSecurityDeposit; + return this; + } + public OfferInfoBuilder withTriggerPrice(long triggerPrice) { this.triggerPrice = triggerPrice; return this; diff --git a/core/src/main/java/bisq/core/api/model/PaymentAccountForm.java b/core/src/main/java/bisq/core/api/model/PaymentAccountForm.java index 777e48987d5..eb3a6cd3bac 100644 --- a/core/src/main/java/bisq/core/api/model/PaymentAccountForm.java +++ b/core/src/main/java/bisq/core/api/model/PaymentAccountForm.java @@ -57,7 +57,7 @@ /** *

* An instance of this class can write new payment account forms (editable json files), - * and de-serialize edited json files into {@link bisq.core.payment.PaymentAccount} + * and de-serialize edited json files into {@link PaymentAccount} * instances. *

*

@@ -66,8 +66,8 @@ *

*
*

- * (1) Ask for a hal cash account form: Pass a {@link bisq.core.payment.payload.PaymentMethod#HAL_CASH_ID} - * to {@link bisq.core.api.model.PaymentAccountForm#getPaymentAccountForm(String)} to + * (1) Ask for a hal cash account form: Pass a {@link PaymentMethod#HAL_CASH_ID} + * to {@link PaymentAccountForm#getPaymentAccountForm(String)} to * get the json Hal Cash payment account form: *

  * {
@@ -98,8 +98,8 @@
  * 
*

* (3) De-serialize the edited json account form: Pass the edited json file to - * {@link bisq.core.api.model.PaymentAccountForm#toPaymentAccount(File)}, or - * a json string to {@link bisq.core.api.model.PaymentAccountForm#toPaymentAccount(String)} + * {@link PaymentAccountForm#toPaymentAccount(File)}, or + * a json string to {@link PaymentAccountForm#toPaymentAccount(String)} * and get a {@link bisq.core.payment.HalCashAccount} instance. *
  * PaymentAccount(
diff --git a/core/src/main/java/bisq/core/api/model/PaymentAccountPayloadInfo.java b/core/src/main/java/bisq/core/api/model/PaymentAccountPayloadInfo.java
new file mode 100644
index 00000000000..9af1277ce33
--- /dev/null
+++ b/core/src/main/java/bisq/core/api/model/PaymentAccountPayloadInfo.java
@@ -0,0 +1,76 @@
+/*
+ * This file is part of Bisq.
+ *
+ * Bisq is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bisq is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bisq. If not, see .
+ */
+
+package bisq.core.api.model;
+
+import bisq.core.payment.payload.CryptoCurrencyAccountPayload;
+import bisq.core.payment.payload.PaymentAccountPayload;
+
+import bisq.common.Payload;
+
+import java.util.function.Supplier;
+
+import lombok.Getter;
+
+import javax.annotation.Nullable;
+
+@Getter
+public class PaymentAccountPayloadInfo implements Payload {
+
+    private final String id;
+    private final String paymentMethodId;
+    @Nullable
+    private final String address;
+
+    public PaymentAccountPayloadInfo(String id,
+                                     String paymentMethodId,
+                                     @Nullable String address) {
+        this.id = id;
+        this.paymentMethodId = paymentMethodId;
+        this.address = address;
+    }
+
+    public static PaymentAccountPayloadInfo toPaymentAccountPayloadInfo(PaymentAccountPayload paymentAccountPayload) {
+        String address = paymentAccountPayload instanceof CryptoCurrencyAccountPayload
+                ? ((CryptoCurrencyAccountPayload) paymentAccountPayload).getAddress()
+                : "";
+        return new PaymentAccountPayloadInfo(paymentAccountPayload.getId(),
+                paymentAccountPayload.getPaymentMethodId(),
+                address);
+    }
+
+    // For transmitting TradeInfo messages when no contract & payloads are available.
+    public static Supplier emptyPaymentAccountPayload = () ->
+            new PaymentAccountPayloadInfo("", "", "");
+
+    ///////////////////////////////////////////////////////////////////////////////////////////
+    // PROTO BUFFER
+    ///////////////////////////////////////////////////////////////////////////////////////////
+
+    public static PaymentAccountPayloadInfo fromProto(bisq.proto.grpc.PaymentAccountPayloadInfo proto) {
+        return new PaymentAccountPayloadInfo(proto.getId(), proto.getPaymentMethodId(), proto.getAddress());
+    }
+
+    @Override
+    public bisq.proto.grpc.PaymentAccountPayloadInfo toProtoMessage() {
+        return bisq.proto.grpc.PaymentAccountPayloadInfo.newBuilder()
+                .setId(id)
+                .setPaymentMethodId(paymentMethodId)
+                .setAddress(address != null ? address : "")
+                .build();
+    }
+}
diff --git a/core/src/main/java/bisq/core/api/model/TradeInfo.java b/core/src/main/java/bisq/core/api/model/TradeInfo.java
index 1a717a7672e..078e5ee4d9c 100644
--- a/core/src/main/java/bisq/core/api/model/TradeInfo.java
+++ b/core/src/main/java/bisq/core/api/model/TradeInfo.java
@@ -17,6 +17,7 @@
 
 package bisq.core.api.model;
 
+import bisq.core.trade.Contract;
 import bisq.core.trade.Trade;
 
 import bisq.common.Payload;
@@ -27,6 +28,7 @@
 import lombok.Getter;
 
 import static bisq.core.api.model.OfferInfo.toOfferInfo;
+import static bisq.core.api.model.PaymentAccountPayloadInfo.toPaymentAccountPayloadInfo;
 
 @EqualsAndHashCode
 @Getter
@@ -60,6 +62,7 @@ public class TradeInfo implements Payload {
     private final boolean isPayoutPublished;
     private final boolean isWithdrawn;
     private final String contractAsJson;
+    private final ContractInfo contract;
 
     public TradeInfo(TradeInfoBuilder builder) {
         this.offer = builder.offer;
@@ -86,6 +89,7 @@ public TradeInfo(TradeInfoBuilder builder) {
         this.isPayoutPublished = builder.isPayoutPublished;
         this.isWithdrawn = builder.isWithdrawn;
         this.contractAsJson = builder.contractAsJson;
+        this.contract = builder.contract;
     }
 
     public static TradeInfo toTradeInfo(Trade trade) {
@@ -93,7 +97,26 @@ public static TradeInfo toTradeInfo(Trade trade) {
     }
 
     public static TradeInfo toTradeInfo(Trade trade, String role) {
-        return new TradeInfo.TradeInfoBuilder()
+        ContractInfo contractInfo;
+        if (trade.getContract() != null) {
+            Contract contract = trade.getContract();
+            contractInfo = new ContractInfo(contract.getBuyerPayoutAddressString(),
+                    contract.getSellerPayoutAddressString(),
+                    contract.getMediatorNodeAddress().getFullAddress(),
+                    contract.getRefundAgentNodeAddress().getFullAddress(),
+                    contract.isBuyerMakerAndSellerTaker(),
+                    contract.getMakerAccountId(),
+                    contract.getTakerAccountId(),
+                    toPaymentAccountPayloadInfo(contract.getMakerPaymentAccountPayload()),
+                    toPaymentAccountPayloadInfo(contract.getTakerPaymentAccountPayload()),
+                    contract.getMakerPayoutAddressString(),
+                    contract.getTakerPayoutAddressString(),
+                    contract.getLockTime());
+        } else {
+            contractInfo = ContractInfo.emptyContract.get();
+        }
+
+        return new TradeInfoBuilder()
                 .withOffer(toOfferInfo(trade.getOffer()))
                 .withTradeId(trade.getId())
                 .withShortId(trade.getShortId())
@@ -120,6 +143,7 @@ public static TradeInfo toTradeInfo(Trade trade, String role) {
                 .withIsPayoutPublished(trade.isPayoutPublished())
                 .withIsWithdrawn(trade.isWithdrawn())
                 .withContractAsJson(trade.getContractAsJson())
+                .withContract(contractInfo)
                 .build();
     }
 
@@ -154,12 +178,38 @@ public bisq.proto.grpc.TradeInfo toProtoMessage() {
                 .setIsPayoutPublished(isPayoutPublished)
                 .setIsWithdrawn(isWithdrawn)
                 .setContractAsJson(contractAsJson == null ? "" : contractAsJson)
+                .setContract(contract.toProtoMessage())
                 .build();
     }
 
-    @SuppressWarnings({"unused", "SameReturnValue"})
     public static TradeInfo fromProto(bisq.proto.grpc.TradeInfo proto) {
-        return null;  // TODO
+        return new TradeInfoBuilder()
+                .withOffer(OfferInfo.fromProto(proto.getOffer()))
+                .withTradeId(proto.getTradeId())
+                .withShortId(proto.getShortId())
+                .withDate(proto.getDate())
+                .withRole(proto.getRole())
+                .withIsCurrencyForTakerFeeBtc(proto.getIsCurrencyForTakerFeeBtc())
+                .withTxFeeAsLong(proto.getTxFeeAsLong())
+                .withTakerFeeAsLong(proto.getTakerFeeAsLong())
+                .withTakerFeeTxId(proto.getTakerFeeTxId())
+                .withDepositTxId(proto.getDepositTxId())
+                .withPayoutTxId(proto.getPayoutTxId())
+                .withTradeAmountAsLong(proto.getTradeAmountAsLong())
+                .withTradePrice(proto.getTradePrice())
+                .withTradePeriodState(proto.getTradePeriodState())
+                .withState(proto.getState())
+                .withPhase(proto.getPhase())
+                .withTradingPeerNodeAddress(proto.getTradingPeerNodeAddress())
+                .withIsDepositPublished(proto.getIsDepositPublished())
+                .withIsDepositConfirmed(proto.getIsDepositConfirmed())
+                .withIsFiatSent(proto.getIsFiatSent())
+                .withIsFiatReceived(proto.getIsFiatReceived())
+                .withIsPayoutPublished(proto.getIsPayoutPublished())
+                .withIsWithdrawn(proto.getIsWithdrawn())
+                .withContractAsJson(proto.getContractAsJson())
+                .withContract((ContractInfo.fromProto(proto.getContract())))
+                .build();
     }
 
     /*
@@ -193,6 +243,7 @@ public static class TradeInfoBuilder {
         private boolean isPayoutPublished;
         private boolean isWithdrawn;
         private String contractAsJson;
+        private ContractInfo contract;
 
         public TradeInfoBuilder withOffer(OfferInfo offer) {
             this.offer = offer;
@@ -314,6 +365,11 @@ public TradeInfoBuilder withContractAsJson(String contractAsJson) {
             return this;
         }
 
+        public TradeInfoBuilder withContract(ContractInfo contract) {
+            this.contract = contract;
+            return this;
+        }
+
         public TradeInfo build() {
             return new TradeInfo(this);
         }
@@ -346,6 +402,7 @@ public String toString() {
                 ", isWithdrawn=" + isWithdrawn + "\n" +
                 ", offer=" + offer + "\n" +
                 ", contractAsJson=" + contractAsJson + "\n" +
+                ", contract=" + contract + "\n" +
                 '}';
     }
 }
diff --git a/core/src/main/java/bisq/core/api/model/TxInfo.java b/core/src/main/java/bisq/core/api/model/TxInfo.java
index f62174f406f..3bd937c96e5 100644
--- a/core/src/main/java/bisq/core/api/model/TxInfo.java
+++ b/core/src/main/java/bisq/core/api/model/TxInfo.java
@@ -46,7 +46,7 @@ public class TxInfo implements Payload {
     private final boolean isPending;
     private final String memo;
 
-    public TxInfo(TxInfo.TxInfoBuilder builder) {
+    public TxInfo(TxInfoBuilder builder) {
         this.txId = builder.txId;
         this.inputSum = builder.inputSum;
         this.outputSum = builder.outputSum;
@@ -61,7 +61,7 @@ public static TxInfo toTxInfo(Transaction transaction) {
             throw new IllegalStateException("server created a null transaction");
 
         if (transaction.getFee() != null)
-            return new TxInfo.TxInfoBuilder()
+            return new TxInfoBuilder()
                     .withTxId(transaction.getTxId().toString())
                     .withInputSum(transaction.getInputSum().value)
                     .withOutputSum(transaction.getOutputSum().value)
@@ -71,7 +71,7 @@ public static TxInfo toTxInfo(Transaction transaction) {
                     .withMemo(transaction.getMemo())
                     .build();
         else
-            return new TxInfo.TxInfoBuilder()
+            return new TxInfoBuilder()
                     .withTxId(transaction.getTxId().toString())
                     .withInputSum(transaction.getInputSum().value)
                     .withOutputSum(transaction.getOutputSum().value)
@@ -101,7 +101,7 @@ public bisq.proto.grpc.TxInfo toProtoMessage() {
 
     @SuppressWarnings("unused")
     public static TxInfo fromProto(bisq.proto.grpc.TxInfo proto) {
-        return new TxInfo.TxInfoBuilder()
+        return new TxInfoBuilder()
                 .withTxId(proto.getTxId())
                 .withInputSum(proto.getInputSum())
                 .withOutputSum(proto.getOutputSum())
@@ -121,37 +121,37 @@ public static class TxInfoBuilder {
         private boolean isPending;
         private String memo;
 
-        public TxInfo.TxInfoBuilder withTxId(String txId) {
+        public TxInfoBuilder withTxId(String txId) {
             this.txId = txId;
             return this;
         }
 
-        public TxInfo.TxInfoBuilder withInputSum(long inputSum) {
+        public TxInfoBuilder withInputSum(long inputSum) {
             this.inputSum = inputSum;
             return this;
         }
 
-        public TxInfo.TxInfoBuilder withOutputSum(long outputSum) {
+        public TxInfoBuilder withOutputSum(long outputSum) {
             this.outputSum = outputSum;
             return this;
         }
 
-        public TxInfo.TxInfoBuilder withFee(long fee) {
+        public TxInfoBuilder withFee(long fee) {
             this.fee = fee;
             return this;
         }
 
-        public TxInfo.TxInfoBuilder withSize(int size) {
+        public TxInfoBuilder withSize(int size) {
             this.size = size;
             return this;
         }
 
-        public TxInfo.TxInfoBuilder withIsPending(boolean isPending) {
+        public TxInfoBuilder withIsPending(boolean isPending) {
             this.isPending = isPending;
             return this;
         }
 
-        public TxInfo.TxInfoBuilder withMemo(String memo) {
+        public TxInfoBuilder withMemo(String memo) {
             this.memo = memo;
             return this;
         }
diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto
index 3fd783ada38..76c5b1f6b7c 100644
--- a/proto/src/main/proto/grpc.proto
+++ b/proto/src/main/proto/grpc.proto
@@ -155,6 +155,10 @@ message OfferInfo {
     string counterCurrencyCode = 17;
     uint64 date = 18;
     string state = 19;
+    uint64 sellerSecurityDeposit = 20;
+    string offerFeePaymentTxId = 21;
+    uint64 txFee = 22;
+    uint64 makerFee = 23;
 }
 
 ///////////////////////////////////////////////////////////////////////////////////////////
@@ -365,6 +369,28 @@ message TradeInfo {
     bool isPayoutPublished = 22;
     bool isWithdrawn = 23;
     string contractAsJson = 24;
+    ContractInfo contract = 25;
+}
+
+message ContractInfo {
+    string buyerNodeAddress = 1;
+    string sellerNodeAddress = 2;
+    string mediatorNodeAddress = 3;
+    string refundAgentNodeAddress = 4;
+    bool isBuyerMakerAndSellerTaker = 5;
+    string makerAccountId = 6;
+    string takerAccountId = 7;
+    PaymentAccountPayloadInfo makerPaymentAccountPayload = 8;
+    PaymentAccountPayloadInfo takerPaymentAccountPayload = 9;
+    string makerPayoutAddressString = 10;
+    string takerPayoutAddressString = 11;
+    uint64 lockTime = 12;
+}
+
+message PaymentAccountPayloadInfo {
+    string id = 1;
+    string paymentMethodId = 2;
+    string address = 3;
 }
 
 ///////////////////////////////////////////////////////////////////////////////////////////

From 780be1a93cf78f7ab947320843d0c23227d25a6b Mon Sep 17 00:00:00 2001
From: ghubstan <36207203+ghubstan@users.noreply.github.com>
Date: Fri, 26 Mar 2021 10:58:47 -0300
Subject: [PATCH 04/10] Adjust maker gettrade output checks to new column

The number of gettrade output columns are the same for makers and takers.
---
 apitest/scripts/trade-simulation-utils.sh | 54 +++++------------------
 1 file changed, 11 insertions(+), 43 deletions(-)

diff --git a/apitest/scripts/trade-simulation-utils.sh b/apitest/scripts/trade-simulation-utils.sh
index c9f03ac80f5..1953302a80e 100755
--- a/apitest/scripts/trade-simulation-utils.sh
+++ b/apitest/scripts/trade-simulation-utils.sh
@@ -238,65 +238,35 @@ gettradedetail() {
 
 istradedepositpublished() {
     TRADE_DETAIL="$1"
-    MAKER_OR_TAKER="$2"
-    if [ "$MAKER_OR_TAKER" = "MAKER" ]
-    then
-        ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $9}')
-    else
-        ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $10}')
-    fi
+    ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $10}')
     commandalert $? "Could not parse istradedepositpublished from trade detail."
     echo "$ANSWER"
 }
 
 istradedepositconfirmed() {
     TRADE_DETAIL="$1"
-    MAKER_OR_TAKER="$2"
-    if [ "$MAKER_OR_TAKER" = "MAKER" ]
-    then
-        ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $10}')
-    else
-        ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $11}')
-    fi
+    ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $11}')
     commandalert $? "Could not parse istradedepositconfirmed from trade detail."
     echo "$ANSWER"
 }
 
 istradepaymentsent() {
     TRADE_DETAIL="$1"
-    MAKER_OR_TAKER="$2"
-    if [ "$MAKER_OR_TAKER" = "MAKER" ]
-    then
-        ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $12}')
-    else
-        ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $13}')
-    fi
+    ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $13}')
     commandalert $? "Could not parse istradepaymentsent from trade detail."
     echo "$ANSWER"
 }
 
 istradepaymentreceived() {
     TRADE_DETAIL="$1"
-    MAKER_OR_TAKER="$2"
-    if [ "$MAKER_OR_TAKER" = "MAKER" ]
-    then
-        ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $13}')
-    else
-        ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $14}')
-    fi
+    ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $14}')
     commandalert $? "Could not parse istradepaymentreceived from trade detail."
     echo "$ANSWER"
 }
 
 istradepayoutpublished() {
     TRADE_DETAIL="$1"
-    MAKER_OR_TAKER="$2"
-    if [ "$MAKER_OR_TAKER" = "MAKER" ]
-    then
-        ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $14}')
-    else
-        ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $15}')
-    fi
+    ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $15}')
     commandalert $? "Could not parse istradepayoutpublished from trade detail."
     echo "$ANSWER"
 }
@@ -321,7 +291,7 @@ waitfortradedepositpublished() {
         TRADE_DETAIL=$(gettradedetail "$GETTRADE_CMD_OUTPUT")
         exitoncommandalert $?
 
-        IS_TRADE_DEPOSIT_PUBLISHED=$(istradedepositpublished "$TRADE_DETAIL" "TAKER")
+        IS_TRADE_DEPOSIT_PUBLISHED=$(istradedepositpublished "$TRADE_DETAIL")
         exitoncommandalert $?
 
         printdate "BOB $BOB_ROLE:  Has taker's trade deposit been published?  $IS_TRADE_DEPOSIT_PUBLISHED"
@@ -356,7 +326,7 @@ waitfortradedepositconfirmed() {
         TRADE_DETAIL=$(gettradedetail "$GETTRADE_CMD_OUTPUT")
         exitoncommandalert $?
 
-        IS_TRADE_DEPOSIT_CONFIRMED=$(istradedepositconfirmed "$TRADE_DETAIL" "TAKER")
+        IS_TRADE_DEPOSIT_CONFIRMED=$(istradedepositconfirmed "$TRADE_DETAIL")
         exitoncommandalert $?
         printdate "BOB $BOB_ROLE:  Has taker's trade deposit been confirmed?  $IS_TRADE_DEPOSIT_CONFIRMED"
         printbreak
@@ -379,7 +349,6 @@ waitfortradepaymentsent() {
     PORT="$1"
     SELLER="$2"
     OFFER_ID="$3"
-    MAKER_OR_TAKER="$4"
     DONE=0
     while : ; do
         if [ "$DONE" -ne 0 ]; then
@@ -397,7 +366,7 @@ waitfortradepaymentsent() {
         TRADE_DETAIL=$(gettradedetail "$GETTRADE_CMD_OUTPUT")
         exitoncommandalert $?
 
-        IS_TRADE_PAYMENT_SENT=$(istradepaymentsent "$TRADE_DETAIL" "$MAKER_OR_TAKER")
+        IS_TRADE_PAYMENT_SENT=$(istradepaymentsent "$TRADE_DETAIL")
         exitoncommandalert $?
         printdate "$SELLER:  Has buyer's fiat payment been initiated?  $IS_TRADE_PAYMENT_SENT"
         if [ "$IS_TRADE_PAYMENT_SENT" = "YES" ]
@@ -416,7 +385,6 @@ waitfortradepaymentreceived() {
     PORT="$1"
     SELLER="$2"
     OFFER_ID="$3"
-    MAKER_OR_TAKER="$4"
     DONE=0
     while : ; do
         if [ "$DONE" -ne 0 ]; then
@@ -437,7 +405,7 @@ waitfortradepaymentreceived() {
         # When the seller receives a 'payment sent' message, it is assumed funds (fiat) have already been deposited.
         # In a real trade, there is usually a delay between receipt of a 'payment sent' message, and the funds deposit,
         # but we do not need to simulate that in this regtest script.
-        IS_TRADE_PAYMENT_SENT=$(istradepaymentreceived "$TRADE_DETAIL" "$MAKER_OR_TAKER")
+        IS_TRADE_PAYMENT_SENT=$(istradepaymentreceived "$TRADE_DETAIL")
         exitoncommandalert $?
         printdate "$SELLER:  Has buyer's payment been transferred to seller's fiat account?  $IS_TRADE_PAYMENT_SENT"
         if [ "$IS_TRADE_PAYMENT_SENT" = "YES" ]
@@ -544,10 +512,10 @@ executetrade() {
     if [ "$DIRECTION" = "BUY" ]
     then
         # Bob waits for payment, polling status in taker specific trade detail.
-        waitfortradepaymentsent "$PAYEE_PORT" "$PAYEE" "$OFFER_ID" "TAKER"
+        waitfortradepaymentsent "$PAYEE_PORT" "$PAYEE" "$OFFER_ID"
     else
         # Alice waits for payment, polling status in maker specific trade detail.
-        waitfortradepaymentsent "$PAYEE_PORT" "$PAYEE" "$OFFER_ID" "MAKER"
+        waitfortradepaymentsent "$PAYEE_PORT" "$PAYEE" "$OFFER_ID"
     fi
 
 

From 152f4591eb0d870c373bbacec9542aa1e8b60c66 Mon Sep 17 00:00:00 2001
From: ghubstan <36207203+ghubstan@users.noreply.github.com>
Date: Fri, 26 Mar 2021 11:04:51 -0300
Subject: [PATCH 05/10] Remove unused parameter

---
 apitest/scripts/trade-simulation-utils.sh | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/apitest/scripts/trade-simulation-utils.sh b/apitest/scripts/trade-simulation-utils.sh
index 1953302a80e..3ed7d3488b8 100755
--- a/apitest/scripts/trade-simulation-utils.sh
+++ b/apitest/scripts/trade-simulation-utils.sh
@@ -525,10 +525,10 @@ executetrade() {
     if [ "$DIRECTION" = "BUY" ]
     then
         # Alice waits for payment rcvd confirm from Bob, polling status in maker specific trade detail.
-        waitfortradepaymentreceived "$PAYER_PORT" "$PAYER" "$OFFER_ID" "MAKER"
+        waitfortradepaymentreceived "$PAYER_PORT" "$PAYER" "$OFFER_ID"
     else
         # Bob waits for payment rcvd confirm from Alice, polling status in taker specific trade detail.
-        waitfortradepaymentreceived "$PAYER_PORT" "$PAYER" "$OFFER_ID" "TAKER"
+        waitfortradepaymentreceived "$PAYER_PORT" "$PAYER" "$OFFER_ID"
     fi
 
     # Generate some btc blocks

From aad998cf3a0574f3bec7c52d03e4403c51610198 Mon Sep 17 00:00:00 2001
From: ghubstan <36207203+ghubstan@users.noreply.github.com>
Date: Sun, 28 Mar 2021 16:42:55 -0300
Subject: [PATCH 06/10] Fix maker/taker fee format bug, rename methods

---
 .../main/java/bisq/cli/CurrencyFormat.java    |  4 +--
 cli/src/main/java/bisq/cli/TableFormat.java   |  4 +--
 cli/src/main/java/bisq/cli/TradeFormat.java   | 31 +++++++++++++------
 3 files changed, 25 insertions(+), 14 deletions(-)

diff --git a/cli/src/main/java/bisq/cli/CurrencyFormat.java b/cli/src/main/java/bisq/cli/CurrencyFormat.java
index 9bada381a12..97d16fc6bdc 100644
--- a/cli/src/main/java/bisq/cli/CurrencyFormat.java
+++ b/cli/src/main/java/bisq/cli/CurrencyFormat.java
@@ -99,14 +99,14 @@ public static String formatMarketPrice(double price) {
         return NUMBER_FORMAT.format(price);
     }
 
-    public static String formatOfferPrice(long price) {
+    public static String formatPrice(long price) {
         NUMBER_FORMAT.setMinimumFractionDigits(4);
         NUMBER_FORMAT.setMaximumFractionDigits(4);
         NUMBER_FORMAT.setRoundingMode(UNNECESSARY);
         return NUMBER_FORMAT.format((double) price / 10_000);
     }
 
-    public static String formatCryptoCurrencyOfferPrice(long price) {
+    public static String formatCryptoCurrencyPrice(long price) {
         NUMBER_FORMAT.setMinimumFractionDigits(8);
         NUMBER_FORMAT.setMaximumFractionDigits(8);
         NUMBER_FORMAT.setRoundingMode(UNNECESSARY);
diff --git a/cli/src/main/java/bisq/cli/TableFormat.java b/cli/src/main/java/bisq/cli/TableFormat.java
index fbc99459375..5c123184e94 100644
--- a/cli/src/main/java/bisq/cli/TableFormat.java
+++ b/cli/src/main/java/bisq/cli/TableFormat.java
@@ -182,7 +182,7 @@ private static String formatFiatOfferTable(List offers, String fiatCu
                 + offers.stream()
                 .map(o -> format(colDataFormat,
                         o.getDirection(),
-                        formatOfferPrice(o.getPrice()),
+                        formatPrice(o.getPrice()),
                         formatAmountRange(o.getMinAmount(), o.getAmount()),
                         formatVolumeRange(o.getMinVolume(), o.getVolume()),
                         o.getPaymentMethodShortName(),
@@ -220,7 +220,7 @@ private static String formatCryptoCurrencyOfferTable(List offers, Str
                 + offers.stream()
                 .map(o -> format(colDataFormat,
                         directionFormat.apply(o),
-                        formatCryptoCurrencyOfferPrice(o.getPrice()),
+                        formatCryptoCurrencyPrice(o.getPrice()),
                         formatAmountRange(o.getMinAmount(), o.getAmount()),
                         formatCryptoCurrencyVolumeRange(o.getMinVolume(), o.getVolume()),
                         o.getPaymentMethodShortName(),
diff --git a/cli/src/main/java/bisq/cli/TradeFormat.java b/cli/src/main/java/bisq/cli/TradeFormat.java
index 4ca1a20d1ab..04816eca247 100644
--- a/cli/src/main/java/bisq/cli/TradeFormat.java
+++ b/cli/src/main/java/bisq/cli/TradeFormat.java
@@ -35,6 +35,8 @@ public class TradeFormat {
     private static final String YES = "YES";
     private static final String NO = "NO";
 
+    // TODO add String format(List trades)
+
     @VisibleForTesting
     public static String format(TradeInfo tradeInfo) {
         // Some column values might be longer than header, so we need to calculate them.
@@ -79,7 +81,7 @@ public static String format(TradeInfo tradeInfo) {
         String headerLine = String.format(headersFormat,
                 /* COL_HEADER_PRICE */ priceHeaderCurrencyCode.apply(tradeInfo),
                 /* COL_HEADER_TRADE_AMOUNT */ baseCurrencyCode,
-                /* COL_HEADER_TRADE_(M||T)AKER_FEE */ makerTakerFeeHeaderCurrencyCode.apply(tradeInfo),
+                /* COL_HEADER_TRADE_(M||T)AKER_FEE */ makerTakerFeeHeaderCurrencyCode.apply(tradeInfo, isTaker),
                 /* COL_HEADER_TRADE_BUYER_COST */ counterCurrencyCode,
                 /* COL_HEADER_TRADE_PAYMENT_SENT */ paymentStatusHeaderCurrencyCode.apply(tradeInfo),
                 /* COL_HEADER_TRADE_PAYMENT_RECEIVED */  paymentStatusHeaderCurrencyCode.apply(tradeInfo));
@@ -132,8 +134,13 @@ private static String formatTradeData(String format,
                     ? t.getOffer().getCounterCurrencyCode()
                     : t.getOffer().getBaseCurrencyCode();
 
-    private static final Function makerTakerFeeHeaderCurrencyCode = (t) ->
-            t.getIsCurrencyForTakerFeeBtc() ? "BTC" : "BSQ";
+    private static final BiFunction makerTakerFeeHeaderCurrencyCode = (t, isTaker) -> {
+        if (isTaker) {
+            return t.getIsCurrencyForTakerFeeBtc() ? "BTC" : "BSQ";
+        } else {
+            return t.getOffer().getIsCurrencyForMakerFeeBtc() ? "BTC" : "BSQ";
+        }
+    };
 
     private static final Function paymentStatusHeaderCurrencyCode = (t) ->
             t.getOffer().getBaseCurrencyCode().equals("BTC")
@@ -142,8 +149,8 @@ private static String formatTradeData(String format,
 
     private static final Function priceFormat = (t) ->
             t.getOffer().getBaseCurrencyCode().equals("BTC")
-                    ? formatOfferPrice(t.getTradePrice())
-                    : formatCryptoCurrencyOfferPrice(t.getOffer().getPrice());
+                    ? formatPrice(t.getTradePrice())
+                    : formatCryptoCurrencyPrice(t.getOffer().getPrice());
 
     private static final Function amountFormat = (t) ->
             t.getOffer().getBaseCurrencyCode().equals("BTC")
@@ -159,11 +166,15 @@ private static String formatTradeData(String format,
     };
 
     private static final BiFunction makerTakerFeeFormat = (t, isTaker) -> {
-        if (isTaker)
-            return t.getIsCurrencyForTakerFeeBtc() ? formatSatoshis(t.getTakerFeeAsLong()) : formatBsq(t.getTakerFeeAsLong());
-        else
-            return t.getIsCurrencyForTakerFeeBtc() ? formatSatoshis(t.getOffer().getMakerFee()) : formatBsq(t.getOffer().getMakerFee());
-
+        if (isTaker) {
+            return t.getIsCurrencyForTakerFeeBtc()
+                    ? formatSatoshis(t.getTakerFeeAsLong())
+                    : formatBsq(t.getTakerFeeAsLong());
+        } else {
+            return t.getOffer().getIsCurrencyForMakerFeeBtc()
+                    ? formatSatoshis(t.getOffer().getMakerFee())
+                    : formatBsq(t.getOffer().getMakerFee());
+        }
     };
 
     private static final Function tradeCostFormat = (t) ->

From 58c885efc193db456def73c04a570d97e724b6d9 Mon Sep 17 00:00:00 2001
From: ghubstan <36207203+ghubstan@users.noreply.github.com>
Date: Thu, 1 Apr 2021 15:43:48 -0300
Subject: [PATCH 07/10] Add support for creating instant altcoin payment
 accounts in api

- Added bool tradeInstant field to proto message def.
- Adjusted core createcryptopaymentacct impl to new tradeInstant request param.
- Adjusted cli side createcryptopaymentacct impl to new tradeInstant request param.
- Fixed CliMain's takeoffer help text (was missing the --payment-account opt).
---
 cli/src/main/java/bisq/cli/CliMain.java       |  9 +++--
 cli/src/main/java/bisq/cli/GrpcClient.java    |  4 ++-
 ...CryptoCurrencyPaymentAcctOptionParser.java | 10 ++++++
 cli/src/main/java/bisq/cli/opts/OptLabel.java |  1 +
 core/src/main/java/bisq/core/api/CoreApi.java | 10 ++++--
 .../core/api/CorePaymentAccountsService.java  |  9 +++--
 .../help/createcryptopaymentacct-help.txt     | 36 +++++++++++--------
 .../grpc/GrpcPaymentAccountsService.java      |  3 +-
 proto/src/main/proto/grpc.proto               |  1 +
 9 files changed, 60 insertions(+), 23 deletions(-)

diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java
index 4b6568712b4..b85858fa120 100644
--- a/cli/src/main/java/bisq/cli/CliMain.java
+++ b/cli/src/main/java/bisq/cli/CliMain.java
@@ -527,9 +527,11 @@ public static void run(String[] args) {
                     var accountName = opts.getAccountName();
                     var currencyCode = opts.getCurrencyCode();
                     var address = opts.getAddress();
+                    var isTradeInstant = opts.getIsTradeInstant();
                     var paymentAccount = client.createCryptoCurrencyPaymentAccount(accountName,
                             currencyCode,
-                            address);
+                            address,
+                            isTradeInstant);
                     out.println("payment account saved");
                     out.println(formatPaymentAcctTbl(singletonList(paymentAccount)));
                     return;
@@ -744,7 +746,9 @@ private static void printHelp(OptionParser parser, @SuppressWarnings("SameParame
             stream.format(rowFormat, getmyoffers.name(), "--direction= \\", "Get my current offers");
             stream.format(rowFormat, "", "--currency-code=", "");
             stream.println();
-            stream.format(rowFormat, takeoffer.name(), "--offer-id= [--fee-currency=]", "Take offer with id");
+            stream.format(rowFormat, takeoffer.name(), "--offer-id= \\", "Take offer with id");
+            stream.format(rowFormat, "", "--payment-account=", "");
+            stream.format(rowFormat, "", "[--fee-currency=]", "");
             stream.println();
             stream.format(rowFormat, gettrade.name(), "--trade-id= \\", "Get trade summary or full contract");
             stream.format(rowFormat, "", "[--show-contract=]", "");
@@ -768,6 +772,7 @@ private static void printHelp(OptionParser parser, @SuppressWarnings("SameParame
             stream.format(rowFormat, createcryptopaymentacct.name(), "--account-name= \\", "Create a new cryptocurrency payment account");
             stream.format(rowFormat, "", "--currency-code= \\", "");
             stream.format(rowFormat, "", "--address=", "");
+            stream.format(rowFormat, "", "--trade-instant=", "");
             stream.println();
             stream.format(rowFormat, getpaymentaccts.name(), "", "Get user payment accounts");
             stream.println();
diff --git a/cli/src/main/java/bisq/cli/GrpcClient.java b/cli/src/main/java/bisq/cli/GrpcClient.java
index 5d3dde4eccb..e9e87f0ed65 100644
--- a/cli/src/main/java/bisq/cli/GrpcClient.java
+++ b/cli/src/main/java/bisq/cli/GrpcClient.java
@@ -433,11 +433,13 @@ public List getPaymentAccounts() {
 
     public PaymentAccount createCryptoCurrencyPaymentAccount(String accountName,
                                                              String currencyCode,
-                                                             String address) {
+                                                             String address,
+                                                             boolean tradeInstant) {
         var request = CreateCryptoCurrencyPaymentAccountRequest.newBuilder()
                 .setAccountName(accountName)
                 .setCurrencyCode(currencyCode)
                 .setAddress(address)
+                .setTradeInstant(tradeInstant)
                 .build();
         return grpcStubs.paymentAccountsService.createCryptoCurrencyPaymentAccount(request).getPaymentAccount();
     }
diff --git a/cli/src/main/java/bisq/cli/opts/CreateCryptoCurrencyPaymentAcctOptionParser.java b/cli/src/main/java/bisq/cli/opts/CreateCryptoCurrencyPaymentAcctOptionParser.java
index 90542e1e0b7..a37a9f109bb 100644
--- a/cli/src/main/java/bisq/cli/opts/CreateCryptoCurrencyPaymentAcctOptionParser.java
+++ b/cli/src/main/java/bisq/cli/opts/CreateCryptoCurrencyPaymentAcctOptionParser.java
@@ -23,6 +23,7 @@
 import static bisq.cli.opts.OptLabel.OPT_ACCOUNT_NAME;
 import static bisq.cli.opts.OptLabel.OPT_ADDRESS;
 import static bisq.cli.opts.OptLabel.OPT_CURRENCY_CODE;
+import static bisq.cli.opts.OptLabel.OPT_TRADE_INSTANT;
 
 public class CreateCryptoCurrencyPaymentAcctOptionParser extends AbstractMethodOptionParser implements MethodOpts {
 
@@ -35,6 +36,11 @@ public class CreateCryptoCurrencyPaymentAcctOptionParser extends AbstractMethodO
     final OptionSpec addressOpt = parser.accepts(OPT_ADDRESS, "bsq address")
             .withRequiredArg();
 
+    final OptionSpec tradeInstantOpt = parser.accepts(OPT_TRADE_INSTANT, "create trade instant account")
+            .withOptionalArg()
+            .ofType(boolean.class)
+            .defaultsTo(Boolean.FALSE);
+
     public CreateCryptoCurrencyPaymentAcctOptionParser(String[] args) {
         super(args);
     }
@@ -72,4 +78,8 @@ public String getCurrencyCode() {
     public String getAddress() {
         return options.valueOf(addressOpt);
     }
+
+    public boolean getIsTradeInstant() {
+        return options.valueOf(tradeInstantOpt);
+    }
 }
diff --git a/cli/src/main/java/bisq/cli/opts/OptLabel.java b/cli/src/main/java/bisq/cli/opts/OptLabel.java
index fcabd0820e4..084c230aae3 100644
--- a/cli/src/main/java/bisq/cli/opts/OptLabel.java
+++ b/cli/src/main/java/bisq/cli/opts/OptLabel.java
@@ -44,6 +44,7 @@ public class OptLabel {
     public final static String OPT_SECURITY_DEPOSIT = "security-deposit";
     public final static String OPT_SHOW_CONTRACT = "show-contract";
     public final static String OPT_TRADE_ID = "trade-id";
+    public final static String OPT_TRADE_INSTANT = "trade-instant";
     public final static String OPT_TIMEOUT = "timeout";
     public final static String OPT_TRANSACTION_ID = "transaction-id";
     public final static String OPT_TX_FEE_RATE = "tx-fee-rate";
diff --git a/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java
index 6db1c667ed9..24ead3f1aba 100644
--- a/core/src/main/java/bisq/core/api/CoreApi.java
+++ b/core/src/main/java/bisq/core/api/CoreApi.java
@@ -210,8 +210,14 @@ public String getPaymentAccountForm(String paymentMethodId) {
         return paymentAccountsService.getPaymentAccountFormAsString(paymentMethodId);
     }
 
-    public PaymentAccount createCryptoCurrencyPaymentAccount(String accountName, String currencyCode, String address) {
-        return paymentAccountsService.createCryptoCurrencyPaymentAccount(accountName, currencyCode, address);
+    public PaymentAccount createCryptoCurrencyPaymentAccount(String accountName,
+                                                             String currencyCode,
+                                                             String address,
+                                                             boolean tradeInstant) {
+        return paymentAccountsService.createCryptoCurrencyPaymentAccount(accountName,
+                currencyCode,
+                address,
+                tradeInstant);
     }
 
     public List getCryptoCurrencyPaymentMethods() {
diff --git a/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java b/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java
index a316b76ed07..0843e20ab76 100644
--- a/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java
+++ b/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java
@@ -21,6 +21,7 @@
 import bisq.core.api.model.PaymentAccountForm;
 import bisq.core.locale.CryptoCurrency;
 import bisq.core.payment.CryptoCurrencyAccount;
+import bisq.core.payment.InstantCryptoCurrencyAccount;
 import bisq.core.payment.PaymentAccount;
 import bisq.core.payment.PaymentAccountFactory;
 import bisq.core.payment.payload.PaymentMethod;
@@ -100,7 +101,8 @@ File getPaymentAccountForm(String paymentMethodId) {
 
     PaymentAccount createCryptoCurrencyPaymentAccount(String accountName,
                                                       String currencyCode,
-                                                      String address) {
+                                                      String address,
+                                                      boolean tradeInstant) {
         String bsqCode = currencyCode.toUpperCase();
         if (!bsqCode.equals("BSQ"))
             throw new IllegalArgumentException("api does not currently support " + currencyCode + " accounts");
@@ -108,8 +110,9 @@ PaymentAccount createCryptoCurrencyPaymentAccount(String accountName,
         // Validate the BSQ address string but ignore the return value.
         coreWalletsService.getValidBsqLegacyAddress(address);
 
-        CryptoCurrencyAccount cryptoCurrencyAccount =
-                (CryptoCurrencyAccount) PaymentAccountFactory.getPaymentAccount(PaymentMethod.BLOCK_CHAINS);
+        var cryptoCurrencyAccount = tradeInstant
+                ? (InstantCryptoCurrencyAccount) PaymentAccountFactory.getPaymentAccount(PaymentMethod.BLOCK_CHAINS_INSTANT)
+                : (CryptoCurrencyAccount) PaymentAccountFactory.getPaymentAccount(PaymentMethod.BLOCK_CHAINS);
         cryptoCurrencyAccount.init();
         cryptoCurrencyAccount.setAccountName(accountName);
         cryptoCurrencyAccount.setAddress(address);
diff --git a/core/src/main/resources/help/createcryptopaymentacct-help.txt b/core/src/main/resources/help/createcryptopaymentacct-help.txt
index c1724550e1c..83fa2e9f633 100644
--- a/core/src/main/resources/help/createcryptopaymentacct-help.txt
+++ b/core/src/main/resources/help/createcryptopaymentacct-help.txt
@@ -7,33 +7,41 @@ createcryptopaymentacct - create a cryptocurrency payment account
 SYNOPSIS
 --------
 createcryptopaymentacct
-		--account-name=
+		--account-name=
 		--currency-code=
-		--address=
+		--address=
+		[--trade-instant=]
 
 DESCRIPTION
 -----------
-Creates a cryptocurrency (altcoin) trading account for buying and selling BTC.
+Create an cryptocurrency (altcoin) payment account.  Only BSQ payment accounts are currently supported.
 
 OPTIONS
 -------
 --account-name
-		The name of the cryptocurrency payment account.
+		The name of the cryptocurrency payment account used to create and take altcoin offers.
 
 --currency-code
-		The three letter code for the cryptocurrency used to buy or sell BTC, e.g., BSQ.
+		The three letter code for the altcoin, e.g., BSQ.
 
 --address
-		A valid BSQ wallet address.
+		The altcoin address to be used receive cryptocurrency payment when selling BTC.
+
+--trade-instant
+		True for creating an instant cryptocurrency payment account, false otherwise.
+		Default is false.
 
 EXAMPLES
 --------
-To create a new BSQ payment account, find an unused BSQ wallet address:
-$ ./bisq-cli --password=xyz --port=9998 getunusedbsqaddress
-
-With the returned BSQ address, e.g., Bn3PCQgRwhkrGnaMp1RYwt9tFwL51YELqne create the cryptocurrency payment account:
-$ ./bisq-cli --password=xyz --port=9998 createcryptopaymentacct \
-    --account-name="My BSQ Account" \
-    --currency-code=BSQ \
-    --address=Bn3PCQgRwhkrGnaMp1RYwt9tFwL51YELqne
 
+To create a BSQ Altcoin payment account:
+$ ./bisq-cli --password=xyz --port=9998 createcryptopaymentacct --account-name="My BSQ Account" \
+    --currency-code=bsq \
+    --address=Bn3PCQgRwhkrGnaMp1RYwt9tFwL51YELqne \
+    --trade-instant=false
+
+To create a BSQ Instant Altcoin payment account:
+$ ./bisq-cli --password=xyz --port=9998 createcryptopaymentacct --account-name="My Instant BSQ Account" \
+    --currency-code=bsq \
+    --address=Bn3PCQgRwhkrGnaMp1RYwt9tFwL51YELqne \
+    --trade-instant=true
diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java
index af801004841..2c1b5501b28 100644
--- a/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java
+++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java
@@ -135,7 +135,8 @@ public void createCryptoCurrencyPaymentAccount(CreateCryptoCurrencyPaymentAccoun
         try {
             PaymentAccount paymentAccount = coreApi.createCryptoCurrencyPaymentAccount(req.getAccountName(),
                     req.getCurrencyCode(),
-                    req.getAddress());
+                    req.getAddress(),
+                    req.getTradeInstant());
             var reply = CreateCryptoCurrencyPaymentAccountReply.newBuilder()
                     .setPaymentAccount(paymentAccount.toProtoMessage())
                     .build();
diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto
index 3fd783ada38..432547edc01 100644
--- a/proto/src/main/proto/grpc.proto
+++ b/proto/src/main/proto/grpc.proto
@@ -210,6 +210,7 @@ message CreateCryptoCurrencyPaymentAccountRequest {
     string accountName = 1;
     string currencyCode = 2;
     string address = 3;
+     bool tradeInstant = 4;
 }
 
 message CreateCryptoCurrencyPaymentAccountReply {

From 4fbdc32ba49cca800e2745fcdd5757a2cc9a44f1 Mon Sep 17 00:00:00 2001
From: ghubstan <36207203+ghubstan@users.noreply.github.com>
Date: Thu, 1 Apr 2021 15:45:53 -0300
Subject: [PATCH 08/10] Remove space char

---
 proto/src/main/proto/grpc.proto | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto
index 432547edc01..2d686506654 100644
--- a/proto/src/main/proto/grpc.proto
+++ b/proto/src/main/proto/grpc.proto
@@ -210,7 +210,7 @@ message CreateCryptoCurrencyPaymentAccountRequest {
     string accountName = 1;
     string currencyCode = 2;
     string address = 3;
-     bool tradeInstant = 4;
+    bool tradeInstant = 4;
 }
 
 message CreateCryptoCurrencyPaymentAccountReply {

From 6bde12ba405ebe63a85e67d4d2f537274dd8056d Mon Sep 17 00:00:00 2001
From: ghubstan <36207203+ghubstan@users.noreply.github.com>
Date: Thu, 1 Apr 2021 16:40:08 -0300
Subject: [PATCH 09/10] Improve takeoffer output and failure reason messaging

- Added AvailabilityResultWithDescription proto for better takeoffer failure msgs.
- Added VerifyBsqSentToAddress impl to api, but don't expose to CLI yet.
- Show BSQ Buyer Address in gettrade output (changed cli output formatting classes).
- Fixed api.model.PaymentAccountPayloadInfo altcoin instant acct support bug
---
 .../java/bisq/cli/ColumnHeaderConstants.java  |  1 +
 .../main/java/bisq/cli/CurrencyFormat.java    |  2 +-
 cli/src/main/java/bisq/cli/GrpcClient.java    | 11 ++++-
 cli/src/main/java/bisq/cli/TradeFormat.java   | 45 +++++++++++++++++--
 .../bisq/core/api/CoreWalletsService.java     | 42 +++++++++++++++++
 .../api/model/PaymentAccountPayloadInfo.java  | 13 ++++--
 .../daemon/grpc/GrpcErrorMessageHandler.java  | 32 +++++++++----
 proto/src/main/proto/grpc.proto               | 20 +++++++--
 8 files changed, 145 insertions(+), 21 deletions(-)

diff --git a/cli/src/main/java/bisq/cli/ColumnHeaderConstants.java b/cli/src/main/java/bisq/cli/ColumnHeaderConstants.java
index 0ad303dd64d..775221b5ed5 100644
--- a/cli/src/main/java/bisq/cli/ColumnHeaderConstants.java
+++ b/cli/src/main/java/bisq/cli/ColumnHeaderConstants.java
@@ -51,6 +51,7 @@ class ColumnHeaderConstants {
     static final String COL_HEADER_PRICE = "Price in %-3s for 1 BTC";
     static final String COL_HEADER_PRICE_OF_ALTCOIN = "Price in BTC for 1 %-3s";
     static final String COL_HEADER_TRADE_AMOUNT = padStart("Amount(%-3s)", 12, ' ');
+    static final String COL_HEADER_TRADE_BSQ_BUYER_ADDRESS = "BSQ Buyer Address";
     static final String COL_HEADER_TRADE_BUYER_COST = padEnd("Buyer Cost(%-3s)", 15, ' ');
     static final String COL_HEADER_TRADE_DEPOSIT_CONFIRMED = "Deposit Confirmed";
     static final String COL_HEADER_TRADE_DEPOSIT_PUBLISHED = "Deposit Published";
diff --git a/cli/src/main/java/bisq/cli/CurrencyFormat.java b/cli/src/main/java/bisq/cli/CurrencyFormat.java
index 97d16fc6bdc..baeeb775f95 100644
--- a/cli/src/main/java/bisq/cli/CurrencyFormat.java
+++ b/cli/src/main/java/bisq/cli/CurrencyFormat.java
@@ -57,7 +57,7 @@ public static String formatBsq(long sats) {
         return BSQ_FORMAT.format(BigDecimal.valueOf(sats).divide(BSQ_SATOSHI_DIVISOR));
     }
 
-    public static String formatBsqSendAmount(long bsqSats) {
+    public static String formatBsqAmount(long bsqSats) {
         // BSQ sats = trade.getOffer().getVolume()
         NUMBER_FORMAT.setMinimumFractionDigits(2);
         NUMBER_FORMAT.setMaximumFractionDigits(2);
diff --git a/cli/src/main/java/bisq/cli/GrpcClient.java b/cli/src/main/java/bisq/cli/GrpcClient.java
index e9e87f0ed65..b238c17e5f1 100644
--- a/cli/src/main/java/bisq/cli/GrpcClient.java
+++ b/cli/src/main/java/bisq/cli/GrpcClient.java
@@ -62,6 +62,7 @@
 import bisq.proto.grpc.TxInfo;
 import bisq.proto.grpc.UnlockWalletRequest;
 import bisq.proto.grpc.UnsetTxFeeRatePreferenceRequest;
+import bisq.proto.grpc.VerifyBsqSentToAddressRequest;
 import bisq.proto.grpc.WithdrawFundsRequest;
 
 import protobuf.PaymentAccount;
@@ -166,6 +167,14 @@ public TxInfo sendBtc(String address, String amount, String txFeeRate, String me
         return grpcStubs.walletsService.sendBtc(request).getTxInfo();
     }
 
+    public boolean verifyBsqSentToAddress(String address, String amount) {
+        var request = VerifyBsqSentToAddressRequest.newBuilder()
+                .setAddress(address)
+                .setAmount(amount)
+                .build();
+        return grpcStubs.walletsService.verifyBsqSentToAddress(request).getIsAmountReceived();
+    }
+
     public TxFeeRateInfo getTxFeeRate() {
         var request = GetTxFeeRateRequest.newBuilder().build();
         return grpcStubs.walletsService.getTxFeeRate(request).getTxFeeRateInfo();
@@ -367,7 +376,7 @@ public TradeInfo takeOffer(String offerId, String paymentAccountId, String taker
         if (reply.hasTrade())
             return reply.getTrade();
         else
-            throw new IllegalStateException(reply.getAvailabilityResultDescription());
+            throw new IllegalStateException(reply.getFailureReason().getDescription());
     }
 
     public TradeInfo getTrade(String tradeId) {
diff --git a/cli/src/main/java/bisq/cli/TradeFormat.java b/cli/src/main/java/bisq/cli/TradeFormat.java
index 04816eca247..d11f8847be4 100644
--- a/cli/src/main/java/bisq/cli/TradeFormat.java
+++ b/cli/src/main/java/bisq/cli/TradeFormat.java
@@ -17,6 +17,7 @@
 
 package bisq.cli;
 
+import bisq.proto.grpc.ContractInfo;
 import bisq.proto.grpc.TradeInfo;
 
 import com.google.common.annotations.VisibleForTesting;
@@ -58,6 +59,10 @@ public static String format(TradeInfo tradeInfo) {
                 "%" + (COL_HEADER_TRADE_TAKER_FEE.length() + 2) + "s"
                 : "";
 
+        boolean showBsqBuyerAddress = shouldShowBqsBuyerAddress(tradeInfo, isTaker);
+        Supplier bsqBuyerAddressHeader = () -> showBsqBuyerAddress ? COL_HEADER_TRADE_BSQ_BUYER_ADDRESS : "";
+        Supplier bsqBuyerAddressHeaderSpec = () -> showBsqBuyerAddress ? "%s" : "";
+
         String headersFormat = padEnd(COL_HEADER_TRADE_SHORT_ID, shortIdColWidth, ' ') + COL_HEADER_DELIMITER
                 + padEnd(COL_HEADER_TRADE_ROLE, roleColWidth, ' ') + COL_HEADER_DELIMITER
                 + priceHeader.apply(tradeInfo) + COL_HEADER_DELIMITER   // includes %s -> currencyCode
@@ -73,6 +78,7 @@ public static String format(TradeInfo tradeInfo) {
                 + COL_HEADER_TRADE_PAYMENT_RECEIVED + COL_HEADER_DELIMITER
                 + COL_HEADER_TRADE_PAYOUT_PUBLISHED + COL_HEADER_DELIMITER
                 + COL_HEADER_TRADE_WITHDRAWN + COL_HEADER_DELIMITER
+                + bsqBuyerAddressHeader.get()
                 + "%n";
 
         String counterCurrencyCode = tradeInfo.getOffer().getCounterCurrencyCode();
@@ -100,14 +106,16 @@ public static String format(TradeInfo tradeInfo) {
                 + "  %-" + (COL_HEADER_TRADE_PAYMENT_SENT.length() - 1) + "s" // left
                 + "  %-" + (COL_HEADER_TRADE_PAYMENT_RECEIVED.length() - 1) + "s" // left
                 + "  %-" + COL_HEADER_TRADE_PAYOUT_PUBLISHED.length() + "s" // lt justify
-                + "  %-" + COL_HEADER_TRADE_WITHDRAWN.length() + "s";       // lt justify
+                + "  %-" + (COL_HEADER_TRADE_WITHDRAWN.length() + 2) + "s"
+                + bsqBuyerAddressHeaderSpec.get();
 
-        return headerLine + formatTradeData(colDataFormat, tradeInfo, isTaker);
+        return headerLine + formatTradeData(colDataFormat, tradeInfo, isTaker, showBsqBuyerAddress);
     }
 
     private static String formatTradeData(String format,
                                           TradeInfo tradeInfo,
-                                          boolean isTaker) {
+                                          boolean isTaker,
+                                          boolean showBsqBuyerAddress) {
         return String.format(format,
                 tradeInfo.getShortId(),
                 tradeInfo.getRole(),
@@ -121,7 +129,8 @@ private static String formatTradeData(String format,
                 tradeInfo.getIsFiatSent() ? YES : NO,
                 tradeInfo.getIsFiatReceived() ? YES : NO,
                 tradeInfo.getIsPayoutPublished() ? YES : NO,
-                tradeInfo.getIsWithdrawn() ? YES : NO);
+                tradeInfo.getIsWithdrawn() ? YES : NO,
+                bsqReceiveAddress.apply(tradeInfo, showBsqBuyerAddress));
     }
 
     private static final Function priceHeader = (t) ->
@@ -181,4 +190,32 @@ private static String formatTradeData(String format,
             t.getOffer().getBaseCurrencyCode().equals("BTC")
                     ? formatOfferVolume(t.getOffer().getVolume())
                     : formatSatoshis(t.getTradeAmountAsLong());
+
+    private static final BiFunction bsqReceiveAddress = (t, showBsqBuyerAddress) -> {
+        if (showBsqBuyerAddress) {
+            ContractInfo contract = t.getContract();
+            boolean isBuyerMakerAndSellerTaker = contract.getIsBuyerMakerAndSellerTaker();
+            return isBuyerMakerAndSellerTaker  // (is BTC buyer / maker)
+                    ? contract.getTakerPaymentAccountPayload().getAddress()
+                    : contract.getMakerPaymentAccountPayload().getAddress();
+        } else {
+            return "";
+        }
+    };
+
+    private static boolean shouldShowBqsBuyerAddress(TradeInfo tradeInfo, boolean isTaker) {
+        if (tradeInfo.getOffer().getBaseCurrencyCode().equals("BTC")) {
+            return false;
+        } else {
+            ContractInfo contract = tradeInfo.getContract();
+            // Do not forget buyer and seller refer to BTC buyer and seller, not BSQ
+            // buyer and seller.  If you are buying BSQ, you are the (BTC) seller.
+            boolean isBuyerMakerAndSellerTaker = contract.getIsBuyerMakerAndSellerTaker();
+            if (isTaker) {
+                return !isBuyerMakerAndSellerTaker;
+            } else {
+                return isBuyerMakerAndSellerTaker;
+            }
+        }
+    }
 }
diff --git a/core/src/main/java/bisq/core/api/CoreWalletsService.java b/core/src/main/java/bisq/core/api/CoreWalletsService.java
index d6aa8a240a3..4af8c3dd12d 100644
--- a/core/src/main/java/bisq/core/api/CoreWalletsService.java
+++ b/core/src/main/java/bisq/core/api/CoreWalletsService.java
@@ -52,8 +52,10 @@
 import org.bitcoinj.core.Coin;
 import org.bitcoinj.core.InsufficientMoneyException;
 import org.bitcoinj.core.LegacyAddress;
+import org.bitcoinj.core.NetworkParameters;
 import org.bitcoinj.core.Transaction;
 import org.bitcoinj.core.TransactionConfidence;
+import org.bitcoinj.core.TransactionOutput;
 import org.bitcoinj.crypto.KeyCrypterScrypt;
 
 import javax.inject.Inject;
@@ -75,6 +77,7 @@
 import java.util.Optional;
 import java.util.Set;
 import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
 import lombok.extern.slf4j.Slf4j;
@@ -145,6 +148,10 @@ KeyParameter getKey() {
         return tempAesKey;
     }
 
+    NetworkParameters getNetworkParameters() {
+        return btcWalletService.getWallet().getContext().getParams();
+    }
+
     BalancesInfo getBalances(String currencyCode) {
         verifyWalletCurrencyCodeIsValid(currencyCode);
         verifyWalletsAreAvailable();
@@ -305,6 +312,41 @@ void sendBtc(String address,
         }
     }
 
+    boolean verifyBsqSentToAddress(String address, String amount) {
+        Address receiverAddress = getValidBsqLegacyAddress(address);
+        NetworkParameters networkParameters = getNetworkParameters();
+        Predicate isTxOutputAddressMatch = (txOut) ->
+                txOut.getScriptPubKey().getToAddress(networkParameters).equals(receiverAddress);
+        Coin coinValue = parseToCoin(amount, bsqFormatter);
+        Predicate isTxOutputValueMatch = (txOut) ->
+                txOut.getValue().longValue() == coinValue.longValue();
+        List spendableBsqTxOutputs = bsqWalletService.getSpendableBsqTransactionOutputs();
+
+        log.info("Searching {} spendable tx outputs for matching address {} and value {}:",
+                spendableBsqTxOutputs.size(),
+                address,
+                coinValue.toPlainString());
+        long numMatches = 0;
+        for (TransactionOutput txOut : spendableBsqTxOutputs) {
+            if (isTxOutputAddressMatch.test(txOut) && isTxOutputValueMatch.test(txOut)) {
+                log.info("\t\tTx {} output has matching address {} and value {}.",
+                        txOut.getParentTransaction().getTxId(),
+                        address,
+                        txOut.getValue().toPlainString());
+                numMatches++;
+            }
+        }
+        if (numMatches > 1) {
+            log.warn("{} tx outputs matched address {} and value {}, could be a"
+                            + " false positive BSQ payment verification result.",
+                    numMatches,
+                    address,
+                    coinValue.toPlainString());
+
+        }
+        return numMatches > 0;
+    }
+
     void getTxFeeRate(ResultHandler resultHandler) {
         try {
             @SuppressWarnings({"unchecked", "Convert2MethodRef"})
diff --git a/core/src/main/java/bisq/core/api/model/PaymentAccountPayloadInfo.java b/core/src/main/java/bisq/core/api/model/PaymentAccountPayloadInfo.java
index 9af1277ce33..8bce2e96db8 100644
--- a/core/src/main/java/bisq/core/api/model/PaymentAccountPayloadInfo.java
+++ b/core/src/main/java/bisq/core/api/model/PaymentAccountPayloadInfo.java
@@ -18,10 +18,12 @@
 package bisq.core.api.model;
 
 import bisq.core.payment.payload.CryptoCurrencyAccountPayload;
+import bisq.core.payment.payload.InstantCryptoCurrencyPayload;
 import bisq.core.payment.payload.PaymentAccountPayload;
 
 import bisq.common.Payload;
 
+import java.util.Optional;
 import java.util.function.Supplier;
 
 import lombok.Getter;
@@ -45,12 +47,15 @@ public PaymentAccountPayloadInfo(String id,
     }
 
     public static PaymentAccountPayloadInfo toPaymentAccountPayloadInfo(PaymentAccountPayload paymentAccountPayload) {
-        String address = paymentAccountPayload instanceof CryptoCurrencyAccountPayload
-                ? ((CryptoCurrencyAccountPayload) paymentAccountPayload).getAddress()
-                : "";
+        Optional address = Optional.empty();
+        if (paymentAccountPayload instanceof CryptoCurrencyAccountPayload)
+            address = Optional.of(((CryptoCurrencyAccountPayload) paymentAccountPayload).getAddress());
+        else if (paymentAccountPayload instanceof InstantCryptoCurrencyPayload)
+            address = Optional.of(((InstantCryptoCurrencyPayload) paymentAccountPayload).getAddress());
+
         return new PaymentAccountPayloadInfo(paymentAccountPayload.getId(),
                 paymentAccountPayload.getPaymentMethodId(),
-                address);
+                address.orElse(""));
     }
 
     // For transmitting TradeInfo messages when no contract & payloads are available.
diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcErrorMessageHandler.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcErrorMessageHandler.java
index 435284252b5..4c139e17094 100644
--- a/daemon/src/main/java/bisq/daemon/grpc/GrpcErrorMessageHandler.java
+++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcErrorMessageHandler.java
@@ -19,6 +19,7 @@
 
 import bisq.common.handlers.ErrorMessageHandler;
 
+import bisq.proto.grpc.AvailabilityResultWithDescription;
 import bisq.proto.grpc.TakeOfferReply;
 
 import protobuf.AvailabilityResult;
@@ -77,7 +78,7 @@ public void handleErrorMessage(String errorMessage) {
             this.isErrorHandled = true;
             log.error(errorMessage);
 
-            if (isTakeOfferError()) {
+            if (takeOfferWasCalled()) {
                 handleTakeOfferError(errorMessage);
             } else {
                 exceptionHandler.handleErrorMessage(log,
@@ -88,14 +89,20 @@ public void handleErrorMessage(String errorMessage) {
     }
 
     private void handleTakeOfferError(String errorMessage) {
-        // Send the AvailabilityResult to the client instead of throwing an exception.
-        // The client should look at the grpc reply object's AvailabilityResult
-        // field if reply.hasTrade = false, and use it give the user a human readable msg.
+        // If the errorMessage originated from a UI purposed TaskRunner, it should
+        // contain an AvailabilityResult enum name.  If it does, derive the
+        // AvailabilityResult enum from the errorMessage, wrap it in a new
+        // AvailabilityResultWithDescription enum, then send the
+        // AvailabilityResultWithDescription to the client instead of throwing
+        // an exception.  The client should use the grpc reply object's
+        // AvailabilityResultWithDescription field if reply.hasTrade = false, and the
+        // client can decide to throw an exception with the client friendly error
+        // description, or take some other action based on the AvailabilityResult enum.
+        // (Some offer availability problems are not fatal, and retries are appropriate.)
         try {
-            AvailabilityResult availabilityResultProto = getAvailabilityResult(errorMessage);
+            var failureReason = getAvailabilityResultWithDescription(errorMessage);
             var reply = TakeOfferReply.newBuilder()
-                    .setAvailabilityResult(availabilityResultProto)
-                    .setAvailabilityResultDescription(getAvailabilityResultDescription(availabilityResultProto))
+                    .setFailureReason(failureReason)
                     .build();
             @SuppressWarnings("unchecked")
             var takeOfferResponseObserver = (StreamObserver) responseObserver;
@@ -109,6 +116,15 @@ private void handleTakeOfferError(String errorMessage) {
         }
     }
 
+    private AvailabilityResultWithDescription getAvailabilityResultWithDescription(String errorMessage) {
+        AvailabilityResult proto = getAvailabilityResult(errorMessage);
+        String description = getAvailabilityResultDescription(proto);
+        return AvailabilityResultWithDescription.newBuilder()
+                .setAvailabilityResult(proto)
+                .setDescription(description)
+                .build();
+    }
+
     private AvailabilityResult getAvailabilityResult(String errorMessage) {
         return stream(AvailabilityResult.values())
                 .filter((e) -> errorMessage.toUpperCase().contains(e.name()))
@@ -121,7 +137,7 @@ private String getAvailabilityResultDescription(AvailabilityResult proto) {
         return bisq.core.offer.AvailabilityResult.fromProto(proto).description();
     }
 
-    private boolean isTakeOfferError() {
+    private boolean takeOfferWasCalled() {
         return fullMethodName.equals(getTakeOfferMethod().getFullMethodName());
     }
 }
diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto
index 836d2e731b2..14a3208c352 100644
--- a/proto/src/main/proto/grpc.proto
+++ b/proto/src/main/proto/grpc.proto
@@ -161,6 +161,11 @@ message OfferInfo {
     uint64 makerFee = 23;
 }
 
+message AvailabilityResultWithDescription {
+    AvailabilityResult availabilityResult = 1;
+    string description = 2;
+}
+
 ///////////////////////////////////////////////////////////////////////////////////////////
 // PaymentAccounts
 ///////////////////////////////////////////////////////////////////////////////////////////
@@ -303,8 +308,7 @@ message TakeOfferRequest {
 
 message TakeOfferReply {
     TradeInfo trade = 1;
-    AvailabilityResult availabilityResult = 2;
-    string availabilityResultDescription = 3;
+    AvailabilityResultWithDescription failureReason = 2;
 }
 
 message ConfirmPaymentStartedRequest {
@@ -430,6 +434,8 @@ service Wallets {
     }
     rpc SendBtc (SendBtcRequest) returns (SendBtcReply) {
     }
+    rpc VerifyBsqSentToAddress (VerifyBsqSentToAddressRequest) returns (VerifyBsqSentToAddressReply) {
+    }
     rpc GetTxFeeRate (GetTxFeeRateRequest) returns (GetTxFeeRateReply) {
     }
     rpc SetTxFeeRatePreference (SetTxFeeRatePreferenceRequest) returns (SetTxFeeRatePreferenceReply) {
@@ -494,6 +500,15 @@ message SendBtcReply {
     TxInfo txInfo = 1;
 }
 
+message VerifyBsqSentToAddressRequest {
+    string address = 1;
+    string amount = 2;
+}
+
+message VerifyBsqSentToAddressReply {
+    bool isAmountReceived = 1;
+}
+
 message GetTxFeeRateRequest {
 }
 
@@ -606,4 +621,3 @@ message GetVersionRequest {
 message GetVersionReply {
     string version = 1;
 }
-

From bddc273bb27ba73f93ed9e9cea4defa598c9f28c Mon Sep 17 00:00:00 2001
From: ghubstan <36207203+ghubstan@users.noreply.github.com>
Date: Fri, 2 Apr 2021 12:58:46 -0300
Subject: [PATCH 10/10] Fix spelling

---
 cli/src/main/java/bisq/cli/TradeFormat.java | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/cli/src/main/java/bisq/cli/TradeFormat.java b/cli/src/main/java/bisq/cli/TradeFormat.java
index d11f8847be4..dbf8dbf4b86 100644
--- a/cli/src/main/java/bisq/cli/TradeFormat.java
+++ b/cli/src/main/java/bisq/cli/TradeFormat.java
@@ -59,7 +59,7 @@ public static String format(TradeInfo tradeInfo) {
                 "%" + (COL_HEADER_TRADE_TAKER_FEE.length() + 2) + "s"
                 : "";
 
-        boolean showBsqBuyerAddress = shouldShowBqsBuyerAddress(tradeInfo, isTaker);
+        boolean showBsqBuyerAddress = shouldShowBsqBuyerAddress(tradeInfo, isTaker);
         Supplier bsqBuyerAddressHeader = () -> showBsqBuyerAddress ? COL_HEADER_TRADE_BSQ_BUYER_ADDRESS : "";
         Supplier bsqBuyerAddressHeaderSpec = () -> showBsqBuyerAddress ? "%s" : "";
 
@@ -203,7 +203,7 @@ private static String formatTradeData(String format,
         }
     };
 
-    private static boolean shouldShowBqsBuyerAddress(TradeInfo tradeInfo, boolean isTaker) {
+    private static boolean shouldShowBsqBuyerAddress(TradeInfo tradeInfo, boolean isTaker) {
         if (tradeInfo.getOffer().getBaseCurrencyCode().equals("BTC")) {
             return false;
         } else {