diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index b85858fa12..dbff135b30 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -76,6 +76,7 @@ import bisq.cli.opts.SetWalletPasswordOptionParser; import bisq.cli.opts.SimpleMethodOptionParser; import bisq.cli.opts.TakeOfferOptionParser; import bisq.cli.opts.UnlockWalletOptionParser; +import bisq.cli.opts.VerifyBsqSentToAddressOptionParser; import bisq.cli.opts.WithdrawFundsOptionParser; /** @@ -264,6 +265,23 @@ public class CliMain { txInfo.getTxId()); return; } + case verifybsqsenttoaddress: { + var opts = new VerifyBsqSentToAddressOptionParser(args).parse(); + if (opts.isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var address = opts.getAddress(); + var amount = opts.getAmount(); + verifyStringIsValidDecimal(OPT_AMOUNT, amount); + + var bsqWasSent = client.verifyBsqSentToAddress(address, amount); + out.printf("%s bsq has %s been sent to address %s%n", + amount, + bsqWasSent ? "" : "not", + address); + return; + } case gettxfeerate: { if (new SimpleMethodOptionParser(args).parse().isForHelp()) { out.println(client.getMethodHelp(method)); @@ -511,7 +529,7 @@ public class CliMain { jsonString = new String(Files.readAllBytes(paymentAccountForm)); } catch (IOException e) { throw new IllegalStateException( - format("could not read %s", paymentAccountForm.toString())); + format("could not read %s", paymentAccountForm)); } var paymentAccount = client.createPaymentAccount(jsonString); out.println("payment account saved"); @@ -717,6 +735,9 @@ public class CliMain { stream.format(rowFormat, "", "[--tx-fee-rate=]", ""); stream.format(rowFormat, "", "[--memo=<\"memo\">]", ""); stream.println(); + stream.format(rowFormat, verifybsqsenttoaddress.name(), "--address= --amount=", + "Verify amount was sent to BSQ wallet address"); + stream.println(); stream.format(rowFormat, gettxfeerate.name(), "", "Get current tx fee rate in sats/byte"); stream.println(); stream.format(rowFormat, settxfeerate.name(), "--tx-fee-rate=", "Set custom tx fee rate in sats/byte"); diff --git a/cli/src/main/java/bisq/cli/CryptoCurrencyUtil.java b/cli/src/main/java/bisq/cli/CryptoCurrencyUtil.java new file mode 100644 index 0000000000..cb503d7c07 --- /dev/null +++ b/cli/src/main/java/bisq/cli/CryptoCurrencyUtil.java @@ -0,0 +1,35 @@ +/* + * 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 java.util.ArrayList; +import java.util.List; + +class CryptoCurrencyUtil { + + public static boolean isSupportedCryptoCurrency(String currencyCode) { + return getSupportedCryptoCurrencies().contains(currencyCode.toUpperCase()); + } + + public static List getSupportedCryptoCurrencies() { + final List result = new ArrayList<>(); + result.add("BSQ"); + result.sort(String::compareTo); + return result; + } +} diff --git a/cli/src/main/java/bisq/cli/GrpcClient.java b/cli/src/main/java/bisq/cli/GrpcClient.java index b238c17e5f..92784e8829 100644 --- a/cli/src/main/java/bisq/cli/GrpcClient.java +++ b/cli/src/main/java/bisq/cli/GrpcClient.java @@ -73,6 +73,7 @@ import java.util.List; import lombok.extern.slf4j.Slf4j; +import static bisq.cli.CryptoCurrencyUtil.isSupportedCryptoCurrency; import static java.util.Comparator.comparing; import static java.util.stream.Collectors.toList; import static protobuf.OfferPayload.Direction.BUY; @@ -286,16 +287,20 @@ public final class GrpcClient { } public List getOffers(String direction, String currencyCode) { - var request = GetOffersRequest.newBuilder() - .setDirection(direction) - .setCurrencyCode(currencyCode) - .build(); - return grpcStubs.offersService.getOffers(request).getOffersList(); + if (isSupportedCryptoCurrency(currencyCode)) { + return getCryptoCurrencyOffers(direction, currencyCode); + } else { + var request = GetOffersRequest.newBuilder() + .setDirection(direction) + .setCurrencyCode(currencyCode) + .build(); + return grpcStubs.offersService.getOffers(request).getOffersList(); + } } - public List getBsqOffers(String direction) { + public List getCryptoCurrencyOffers(String direction, String currencyCode) { return getOffers(direction, "BTC").stream() - .filter(o -> o.getBaseCurrencyCode().equals("BSQ")) + .filter(o -> o.getBaseCurrencyCode().equalsIgnoreCase(currencyCode)) .collect(toList()); } @@ -313,22 +318,26 @@ public final class GrpcClient { public List getBsqOffersSortedByDate() { ArrayList offers = new ArrayList<>(); - offers.addAll(getBsqOffers(BUY.name())); - offers.addAll(getBsqOffers(SELL.name())); + offers.addAll(getCryptoCurrencyOffers(BUY.name(), "BSQ")); + offers.addAll(getCryptoCurrencyOffers(SELL.name(), "BSQ")); return sortOffersByDate(offers); } public List getMyOffers(String direction, String currencyCode) { - var request = GetMyOffersRequest.newBuilder() - .setDirection(direction) - .setCurrencyCode(currencyCode) - .build(); - return grpcStubs.offersService.getMyOffers(request).getOffersList(); + if (isSupportedCryptoCurrency(currencyCode)) { + return getMyCryptoCurrencyOffers(direction, currencyCode); + } else { + var request = GetMyOffersRequest.newBuilder() + .setDirection(direction) + .setCurrencyCode(currencyCode) + .build(); + return grpcStubs.offersService.getMyOffers(request).getOffersList(); + } } - public List getMyBsqOffers(String direction) { + public List getMyCryptoCurrencyOffers(String direction, String currencyCode) { return getMyOffers(direction, "BTC").stream() - .filter(o -> o.getBaseCurrencyCode().equals("BSQ")) + .filter(o -> o.getBaseCurrencyCode().equalsIgnoreCase(currencyCode)) .collect(toList()); } @@ -346,8 +355,8 @@ public final class GrpcClient { public List getMyBsqOffersSortedByDate() { ArrayList offers = new ArrayList<>(); - offers.addAll(getMyBsqOffers(BUY.name())); - offers.addAll(getMyBsqOffers(SELL.name())); + offers.addAll(getMyCryptoCurrencyOffers(BUY.name(), "BSQ")); + offers.addAll(getMyCryptoCurrencyOffers(SELL.name(), "BSQ")); return sortOffersByDate(offers); } diff --git a/cli/src/main/java/bisq/cli/Method.java b/cli/src/main/java/bisq/cli/Method.java index 908321a090..cf8b1d7df5 100644 --- a/cli/src/main/java/bisq/cli/Method.java +++ b/cli/src/main/java/bisq/cli/Method.java @@ -49,6 +49,7 @@ public enum Method { removewalletpassword, sendbsq, sendbtc, + verifybsqsenttoaddress, settxfeerate, setwalletpassword, takeoffer, diff --git a/cli/src/main/java/bisq/cli/opts/CreatePaymentAcctOptionParser.java b/cli/src/main/java/bisq/cli/opts/CreatePaymentAcctOptionParser.java index 907f7ca0ed..fe91924bb4 100644 --- a/cli/src/main/java/bisq/cli/opts/CreatePaymentAcctOptionParser.java +++ b/cli/src/main/java/bisq/cli/opts/CreatePaymentAcctOptionParser.java @@ -50,7 +50,7 @@ public class CreatePaymentAcctOptionParser extends AbstractMethodOptionParser im if (!path.toFile().exists()) throw new IllegalStateException( format("json payment account form '%s' could not be found", - path.toString())); + path)); return this; } diff --git a/cli/src/main/java/bisq/cli/opts/VerifyBsqSentToAddressOptionParser.java b/cli/src/main/java/bisq/cli/opts/VerifyBsqSentToAddressOptionParser.java new file mode 100644 index 0000000000..c2d5ea2b35 --- /dev/null +++ b/cli/src/main/java/bisq/cli/opts/VerifyBsqSentToAddressOptionParser.java @@ -0,0 +1,61 @@ +/* + * 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_ADDRESS; +import static bisq.cli.opts.OptLabel.OPT_AMOUNT; + +public class VerifyBsqSentToAddressOptionParser extends AbstractMethodOptionParser implements MethodOpts { + + final OptionSpec addressOpt = parser.accepts(OPT_ADDRESS, "receiving bsq address") + .withRequiredArg(); + + final OptionSpec amountOpt = parser.accepts(OPT_AMOUNT, "amount of bsq received") + .withRequiredArg(); + + public VerifyBsqSentToAddressOptionParser(String[] args) { + super(args); + } + + public VerifyBsqSentToAddressOptionParser parse() { + super.parse(); + + // Short circuit opt validation if user just wants help. + if (options.has(helpOpt)) + return this; + + if (!options.has(addressOpt) || options.valueOf(addressOpt).isEmpty()) + throw new IllegalArgumentException("no bsq address specified"); + + if (!options.has(amountOpt) || options.valueOf(amountOpt).isEmpty()) + throw new IllegalArgumentException("no bsq amount specified"); + + return this; + } + + public String getAddress() { + return options.valueOf(addressOpt); + } + + public String getAmount() { + return options.valueOf(amountOpt); + } +} diff --git a/core/src/main/resources/help/verifybsqsenttoaddress-help.txt b/core/src/main/resources/help/verifybsqsenttoaddress-help.txt new file mode 100644 index 0000000000..47cb09d790 --- /dev/null +++ b/core/src/main/resources/help/verifybsqsenttoaddress-help.txt @@ -0,0 +1,39 @@ +verifybsqsenttoaddress + +NAME +---- +verifybsqsenttoaddress - verify BSQ sent to wallet address + +SYNOPSIS +-------- +verifybsqsenttoaddress + --address= + --amount= + +DESCRIPTION +----------- +Verify an exact amount of BSQ was sent to a specific Bisq wallet's BSQ address. +Receipt of BSQ to a BSQ (altcoin) payment account address should always be verified +before a BSQ seller sends a confirmpaymentreceived message for a BSQ/BTC trade. + +Warning: The verification result should be considered a false positive if a BSQ wallet +address has received the same amount of BSQ in more than one transaction. A way to +avoid this problem is to use different BSQ payment accounts for different trades, so +the payment account receiving address will vary from trade to trade. Another way is to +slightly vary your offer amounts and BSQ prices (if you are the maker), to make sure the +received BSQ amounts vary from trade to trade. Doing all of the above further reduces +the chance of a false positive. Another step is to check your BSQ wallet balance when +you verify BSQ has been received to an address. + +OPTIONS +------- +--address + The receiving BSQ address. + +--amount + The amount of BSQ received. + +EXAMPLES +-------- +Verify 500.00 BSQ was sent to address Bn3PCQgRwhkrGnaMp1RYwt9tFwL51YELqne: +$ ./bisq-cli --password=xyz --port=9998 verifybsqsenttoaddress --address=Bn3PCQgRwhkrGnaMp1RYwt9tFwL51YELqne --amount=500.00