This commit is contained in:
woodser 2021-05-04 20:20:01 -04:00
commit 8a38081c04
2800 changed files with 344130 additions and 0 deletions

View file

@ -0,0 +1,817 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package bisq.cli;
import bisq.proto.grpc.OfferInfo;
import io.grpc.StatusRuntimeException;
import joptsimple.OptionParser;
import joptsimple.OptionSet;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.util.Date;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import static bisq.cli.CurrencyFormat.formatMarketPrice;
import static bisq.cli.CurrencyFormat.formatTxFeeRateInfo;
import static bisq.cli.CurrencyFormat.toSatoshis;
import static bisq.cli.CurrencyFormat.toSecurityDepositAsPct;
import static bisq.cli.Method.*;
import static bisq.cli.TableFormat.*;
import static bisq.cli.opts.OptLabel.*;
import static java.lang.String.format;
import static java.lang.System.err;
import static java.lang.System.exit;
import static java.lang.System.out;
import static java.util.Collections.singletonList;
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;
import bisq.cli.opts.GetBTCMarketPriceOptionParser;
import bisq.cli.opts.GetBalanceOptionParser;
import bisq.cli.opts.GetOfferOptionParser;
import bisq.cli.opts.GetOffersOptionParser;
import bisq.cli.opts.GetPaymentAcctFormOptionParser;
import bisq.cli.opts.GetTradeOptionParser;
import bisq.cli.opts.GetTransactionOptionParser;
import bisq.cli.opts.RegisterDisputeAgentOptionParser;
import bisq.cli.opts.RemoveWalletPasswordOptionParser;
import bisq.cli.opts.SendBsqOptionParser;
import bisq.cli.opts.SendBtcOptionParser;
import bisq.cli.opts.SetTxFeeRateOptionParser;
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;
/**
* A command-line client for the Bisq gRPC API.
*/
@Slf4j
public class CliMain {
public static void main(String[] args) {
try {
run(args);
} catch (Throwable t) {
err.println("Error: " + t.getMessage());
exit(1);
}
}
public static void run(String[] args) {
var parser = new OptionParser();
var helpOpt = parser.accepts(OPT_HELP, "Print this help text")
.forHelp();
var hostOpt = parser.accepts(OPT_HOST, "rpc server hostname or ip")
.withRequiredArg()
.defaultsTo("localhost");
var portOpt = parser.accepts(OPT_PORT, "rpc server port")
.withRequiredArg()
.ofType(Integer.class)
.defaultsTo(9998);
var passwordOpt = parser.accepts(OPT_PASSWORD, "rpc server password")
.withRequiredArg();
// Parse the CLI opts host, port, password, method name, and help. The help opt
// may indicate the user is asking for method level help, and will be excluded
// from the parsed options if a method opt is present in String[] args.
OptionSet options = parser.parse(new ArgumentList(args).getCLIArguments());
@SuppressWarnings("unchecked")
var nonOptionArgs = (List<String>) options.nonOptionArguments();
// If neither the help opt nor a method name is present, print CLI level help
// to stderr and throw an exception.
if (!options.has(helpOpt) && nonOptionArgs.isEmpty()) {
printHelp(parser, err);
throw new IllegalArgumentException("no method specified");
}
// If the help opt is present, but not a method name, print CLI level help
// to stdout.
if (options.has(helpOpt) && nonOptionArgs.isEmpty()) {
printHelp(parser, out);
return;
}
var host = options.valueOf(hostOpt);
var port = options.valueOf(portOpt);
var password = options.valueOf(passwordOpt);
if (password == null)
throw new IllegalArgumentException("missing required 'password' option");
var methodName = nonOptionArgs.get(0);
Method method;
try {
method = getMethodFromCmd(methodName);
} catch (IllegalArgumentException ex) {
throw new IllegalArgumentException(format("'%s' is not a supported method", methodName));
}
GrpcClient client = new GrpcClient(host, port, password);
try {
switch (method) {
case getversion: {
if (new SimpleMethodOptionParser(args).parse().isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var version = client.getVersion();
out.println(version);
return;
}
case getbalance: {
var opts = new GetBalanceOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var currencyCode = opts.getCurrencyCode();
var balances = client.getBalances(currencyCode);
switch (currencyCode.toUpperCase()) {
case "BSQ":
out.println(formatBsqBalanceInfoTbl(balances.getBsq()));
break;
case "BTC":
out.println(formatBtcBalanceInfoTbl(balances.getBtc()));
break;
case "":
default:
out.println(formatBalancesTbls(balances));
break;
}
return;
}
case getaddressbalance: {
var opts = new GetAddressBalanceOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var address = opts.getAddress();
var addressBalance = client.getAddressBalance(address);
out.println(formatAddressBalanceTbl(singletonList(addressBalance)));
return;
}
case getbtcprice: {
var opts = new GetBTCMarketPriceOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var currencyCode = opts.getCurrencyCode();
var price = client.getBtcPrice(currencyCode);
out.println(formatMarketPrice(price));
return;
}
case getfundingaddresses: {
if (new SimpleMethodOptionParser(args).parse().isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var fundingAddresses = client.getFundingAddresses();
out.println(formatAddressBalanceTbl(fundingAddresses));
return;
}
case getunusedbsqaddress: {
if (new SimpleMethodOptionParser(args).parse().isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var address = client.getUnusedBsqAddress();
out.println(address);
return;
}
case sendbsq: {
var opts = new SendBsqOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var address = opts.getAddress();
var amount = opts.getAmount();
verifyStringIsValidDecimal(OPT_AMOUNT, amount);
var txFeeRate = opts.getFeeRate();
if (!txFeeRate.isEmpty())
verifyStringIsValidLong(OPT_TX_FEE_RATE, txFeeRate);
var txInfo = client.sendBsq(address, amount, txFeeRate);
out.printf("%s bsq sent to %s in tx %s%n",
amount,
address,
txInfo.getTxId());
return;
}
case sendbtc: {
var opts = new SendBtcOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var address = opts.getAddress();
var amount = opts.getAmount();
verifyStringIsValidDecimal(OPT_AMOUNT, amount);
var txFeeRate = opts.getFeeRate();
if (!txFeeRate.isEmpty())
verifyStringIsValidLong(OPT_TX_FEE_RATE, txFeeRate);
var memo = opts.getMemo();
var txInfo = client.sendBtc(address, amount, txFeeRate, memo);
out.printf("%s btc sent to %s in tx %s%n",
amount,
address,
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 %s sent to address %s%n",
amount,
bsqWasSent ? "has been" : "has not been",
address);
return;
}
case gettxfeerate: {
if (new SimpleMethodOptionParser(args).parse().isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var txFeeRate = client.getTxFeeRate();
out.println(formatTxFeeRateInfo(txFeeRate));
return;
}
case settxfeerate: {
var opts = new SetTxFeeRateOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var txFeeRate = client.setTxFeeRate(toLong(opts.getFeeRate()));
out.println(formatTxFeeRateInfo(txFeeRate));
return;
}
case unsettxfeerate: {
if (new SimpleMethodOptionParser(args).parse().isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var txFeeRate = client.unsetTxFeeRate();
out.println(formatTxFeeRateInfo(txFeeRate));
return;
}
case gettransaction: {
var opts = new GetTransactionOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var txId = opts.getTxId();
var tx = client.getTransaction(txId);
out.println(TransactionFormat.format(tx));
return;
}
case createoffer: {
var opts = new CreateOfferOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var paymentAcctId = opts.getPaymentAccountId();
var direction = opts.getDirection();
var currencyCode = opts.getCurrencyCode();
var amount = toSatoshis(opts.getAmount());
var minAmount = toSatoshis(opts.getMinAmount());
var useMarketBasedPrice = opts.isUsingMktPriceMargin();
var fixedPrice = opts.getFixedPrice();
var marketPriceMargin = opts.getMktPriceMarginAsBigDecimal();
var securityDeposit = toSecurityDepositAsPct(opts.getSecurityDeposit());
var makerFeeCurrencyCode = opts.getMakerFeeCurrencyCode();
var offer = client.createOffer(direction,
currencyCode,
amount,
minAmount,
useMarketBasedPrice,
fixedPrice,
marketPriceMargin.doubleValue(),
securityDeposit,
paymentAcctId,
makerFeeCurrencyCode);
out.println(formatOfferTable(singletonList(offer), currencyCode));
return;
}
case canceloffer: {
var opts = new CancelOfferOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var offerId = opts.getOfferId();
client.cancelOffer(offerId);
out.println("offer canceled and removed from offer book");
return;
}
case getoffer: {
var opts = new GetOfferOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var offerId = opts.getOfferId();
var offer = client.getOffer(offerId);
out.println(formatOfferTable(singletonList(offer), offer.getCounterCurrencyCode()));
return;
}
case getmyoffer: {
var opts = new GetOfferOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var offerId = opts.getOfferId();
var offer = client.getMyOffer(offerId);
out.println(formatOfferTable(singletonList(offer), offer.getCounterCurrencyCode()));
return;
}
case getoffers: {
var opts = new GetOffersOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var direction = opts.getDirection();
var currencyCode = opts.getCurrencyCode();
List<OfferInfo> offers = client.getOffers(direction, currencyCode);
if (offers.isEmpty())
out.printf("no %s %s offers found%n", direction, currencyCode);
else
out.println(formatOfferTable(offers, currencyCode));
return;
}
case getmyoffers: {
var opts = new GetOffersOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var direction = opts.getDirection();
var currencyCode = opts.getCurrencyCode();
List<OfferInfo> offers = client.getMyOffers(direction, currencyCode);
if (offers.isEmpty())
out.printf("no %s %s offers found%n", direction, currencyCode);
else
out.println(formatOfferTable(offers, currencyCode));
return;
}
case takeoffer: {
var opts = new TakeOfferOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var offerId = opts.getOfferId();
var paymentAccountId = opts.getPaymentAccountId();
var takerFeeCurrencyCode = opts.getTakerFeeCurrencyCode();
var trade = client.takeOffer(offerId, paymentAccountId, takerFeeCurrencyCode);
out.printf("trade %s successfully taken%n", trade.getTradeId());
return;
}
case gettrade: {
// TODO make short-id a valid argument?
var opts = new GetTradeOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var tradeId = opts.getTradeId();
var showContract = opts.getShowContract();
var trade = client.getTrade(tradeId);
if (showContract)
out.println(trade.getContractAsJson());
else
out.println(TradeFormat.format(trade));
return;
}
case confirmpaymentstarted: {
var opts = new GetTradeOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var tradeId = opts.getTradeId();
client.confirmPaymentStarted(tradeId);
out.printf("trade %s payment started message sent%n", tradeId);
return;
}
case confirmpaymentreceived: {
var opts = new GetTradeOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var tradeId = opts.getTradeId();
client.confirmPaymentReceived(tradeId);
out.printf("trade %s payment received message sent%n", tradeId);
return;
}
case keepfunds: {
var opts = new GetTradeOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var tradeId = opts.getTradeId();
client.keepFunds(tradeId);
out.printf("funds from trade %s saved in bisq wallet%n", tradeId);
return;
}
case withdrawfunds: {
var opts = new WithdrawFundsOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var tradeId = opts.getTradeId();
var address = opts.getAddress();
// Multi-word memos must be double quoted.
var memo = opts.getMemo();
client.withdrawFunds(tradeId, address, memo);
out.printf("trade %s funds sent to btc address %s%n", tradeId, address);
return;
}
case getpaymentmethods: {
if (new SimpleMethodOptionParser(args).parse().isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var paymentMethods = client.getPaymentMethods();
paymentMethods.forEach(p -> out.println(p.getId()));
return;
}
case getpaymentacctform: {
var opts = new GetPaymentAcctFormOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var paymentMethodId = opts.getPaymentMethodId();
String jsonString = client.getPaymentAcctFormAsJson(paymentMethodId);
File jsonFile = saveFileToDisk(paymentMethodId.toLowerCase(),
".json",
jsonString);
out.printf("payment account form %s%nsaved to %s%n",
jsonString, jsonFile.getAbsolutePath());
out.println("Edit the file, and use as the argument to a 'createpaymentacct' command.");
return;
}
case createpaymentacct: {
var opts = new CreatePaymentAcctOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var paymentAccountForm = opts.getPaymentAcctForm();
String jsonString;
try {
jsonString = new String(Files.readAllBytes(paymentAccountForm));
} catch (IOException e) {
throw new IllegalStateException(
format("could not read %s", paymentAccountForm));
}
var paymentAccount = client.createPaymentAccount(jsonString);
out.println("payment account saved");
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 isTradeInstant = opts.getIsTradeInstant();
var paymentAccount = client.createCryptoCurrencyPaymentAccount(accountName,
currencyCode,
address,
isTradeInstant);
out.println("payment account saved");
out.println(formatPaymentAcctTbl(singletonList(paymentAccount)));
return;
}
case getpaymentaccts: {
if (new SimpleMethodOptionParser(args).parse().isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var paymentAccounts = client.getPaymentAccounts();
if (paymentAccounts.size() > 0)
out.println(formatPaymentAcctTbl(paymentAccounts));
else
out.println("no payment accounts are saved");
return;
}
case lockwallet: {
if (new SimpleMethodOptionParser(args).parse().isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
client.lockWallet();
out.println("wallet locked");
return;
}
case unlockwallet: {
var opts = new UnlockWalletOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var walletPassword = opts.getPassword();
var timeout = opts.getUnlockTimeout();
client.unlockWallet(walletPassword, timeout);
out.println("wallet unlocked");
return;
}
case removewalletpassword: {
var opts = new RemoveWalletPasswordOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var walletPassword = opts.getPassword();
client.removeWalletPassword(walletPassword);
out.println("wallet decrypted");
return;
}
case setwalletpassword: {
var opts = new SetWalletPasswordOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var walletPassword = opts.getPassword();
var newWalletPassword = opts.getNewPassword();
client.setWalletPassword(walletPassword, newWalletPassword);
out.println("wallet encrypted" + (!newWalletPassword.isEmpty() ? " with new password" : ""));
return;
}
case registerdisputeagent: {
var opts = new RegisterDisputeAgentOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var disputeAgentType = opts.getDisputeAgentType();
var registrationKey = opts.getRegistrationKey();
client.registerDisputeAgent(disputeAgentType, registrationKey);
out.println(disputeAgentType + " registered");
return;
}
case stop: {
if (new SimpleMethodOptionParser(args).parse().isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
client.stopServer();
out.println("server shutdown signal received");
return;
}
default: {
throw new RuntimeException(format("unhandled method '%s'", method));
}
}
} catch (StatusRuntimeException ex) {
// Remove the leading gRPC status code (e.g. "UNKNOWN: ") from the message
String message = ex.getMessage().replaceFirst("^[A-Z_]+: ", "");
if (message.equals("io exception"))
throw new RuntimeException(message + ", server may not be running", ex);
else
throw new RuntimeException(message, ex);
}
}
private static Method getMethodFromCmd(String methodName) {
// TODO if we use const type for enum we need add some mapping. Even if we don't
// change now it is handy to have flexibility in case we change internal code
// and don't want to break user commands.
return Method.valueOf(methodName.toLowerCase());
}
@SuppressWarnings("SameParameterValue")
private static void verifyStringIsValidDecimal(String optionLabel, String optionValue) {
try {
Double.parseDouble(optionValue);
} catch (NumberFormatException ex) {
throw new IllegalArgumentException(format("--%s=%s, '%s' is not a number",
optionLabel,
optionValue,
optionValue));
}
}
@SuppressWarnings("SameParameterValue")
private static void verifyStringIsValidLong(String optionLabel, String optionValue) {
try {
Long.parseLong(optionValue);
} catch (NumberFormatException ex) {
throw new IllegalArgumentException(format("--%s=%s, '%s' is not a number",
optionLabel,
optionValue,
optionValue));
}
}
private static long toLong(String param) {
try {
return Long.parseLong(param);
} catch (NumberFormatException ex) {
throw new IllegalArgumentException(format("'%s' is not a number", param));
}
}
private static File saveFileToDisk(String prefix,
@SuppressWarnings("SameParameterValue") String suffix,
String text) {
String timestamp = Long.toUnsignedString(new Date().getTime());
String relativeFileName = prefix + "_" + timestamp + suffix;
try {
Path path = Paths.get(relativeFileName);
if (!Files.exists(path)) {
try (PrintWriter out = new PrintWriter(path.toString())) {
out.println(text);
}
return path.toAbsolutePath().toFile();
} else {
throw new IllegalStateException(format("could not overwrite existing file '%s'", relativeFileName));
}
} catch (FileNotFoundException e) {
throw new IllegalStateException(format("could not create file '%s'", relativeFileName));
}
}
private static void printHelp(OptionParser parser, @SuppressWarnings("SameParameterValue") PrintStream stream) {
try {
stream.println("Bisq RPC Client");
stream.println();
stream.println("Usage: bisq-cli [options] <method> [params]");
stream.println();
parser.printHelpOn(stream);
stream.println();
String rowFormat = "%-25s%-52s%s%n";
stream.format(rowFormat, "Method", "Params", "Description");
stream.format(rowFormat, "------", "------", "------------");
stream.format(rowFormat, getversion.name(), "", "Get server version");
stream.println();
stream.format(rowFormat, getbalance.name(), "[--currency-code=<bsq|btc>]", "Get server wallet balances");
stream.println();
stream.format(rowFormat, getaddressbalance.name(), "--address=<btc-address>", "Get server wallet address balance");
stream.println();
stream.format(rowFormat, getbtcprice.name(), "--currency-code=<currency-code>", "Get current market btc price");
stream.println();
stream.format(rowFormat, getfundingaddresses.name(), "", "Get BTC funding addresses");
stream.println();
stream.format(rowFormat, getunusedbsqaddress.name(), "", "Get unused BSQ address");
stream.println();
stream.format(rowFormat, sendbsq.name(), "--address=<bsq-address> --amount=<bsq-amount> \\", "Send BSQ");
stream.format(rowFormat, "", "[--tx-fee-rate=<sats/byte>]", "");
stream.println();
stream.format(rowFormat, sendbtc.name(), "--address=<btc-address> --amount=<btc-amount> \\", "Send BTC");
stream.format(rowFormat, "", "[--tx-fee-rate=<sats/byte>]", "");
stream.format(rowFormat, "", "[--memo=<\"memo\">]", "");
stream.println();
stream.format(rowFormat, verifybsqsenttoaddress.name(), "--address=<bsq-address> --amount=<bsq-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=<sats/byte>", "Set custom tx fee rate in sats/byte");
stream.println();
stream.format(rowFormat, unsettxfeerate.name(), "", "Unset custom tx fee rate");
stream.println();
stream.format(rowFormat, gettransaction.name(), "--transaction-id=<transaction-id>", "Get transaction with id");
stream.println();
stream.format(rowFormat, createoffer.name(), "--payment-account=<payment-account-id> \\", "Create and place an offer");
stream.format(rowFormat, "", "--direction=<buy|sell> \\", "");
stream.format(rowFormat, "", "--currency-code=<currency-code> \\", "");
stream.format(rowFormat, "", "--amount=<btc-amount> \\", "");
stream.format(rowFormat, "", "[--min-amount=<min-btc-amount>] \\", "");
stream.format(rowFormat, "", "--fixed-price=<price> | --market-price=margin=<percent> \\", "");
stream.format(rowFormat, "", "--security-deposit=<percent> \\", "");
stream.format(rowFormat, "", "[--fee-currency=<bsq|btc>]", "");
stream.println();
stream.format(rowFormat, canceloffer.name(), "--offer-id=<offer-id>", "Cancel offer with id");
stream.println();
stream.format(rowFormat, getoffer.name(), "--offer-id=<offer-id>", "Get current offer with id");
stream.println();
stream.format(rowFormat, getmyoffer.name(), "--offer-id=<offer-id>", "Get my current offer with id");
stream.println();
stream.format(rowFormat, getoffers.name(), "--direction=<buy|sell> \\", "Get current offers");
stream.format(rowFormat, "", "--currency-code=<currency-code>", "");
stream.println();
stream.format(rowFormat, getmyoffers.name(), "--direction=<buy|sell> \\", "Get my current offers");
stream.format(rowFormat, "", "--currency-code=<currency-code>", "");
stream.println();
stream.format(rowFormat, takeoffer.name(), "--offer-id=<offer-id> \\", "Take offer with id");
stream.format(rowFormat, "", "--payment-account=<payment-account-id>", "");
stream.format(rowFormat, "", "[--fee-currency=<btc|bsq>]", "");
stream.println();
stream.format(rowFormat, gettrade.name(), "--trade-id=<trade-id> \\", "Get trade summary or full contract");
stream.format(rowFormat, "", "[--show-contract=<true|false>]", "");
stream.println();
stream.format(rowFormat, confirmpaymentstarted.name(), "--trade-id=<trade-id>", "Confirm payment started");
stream.println();
stream.format(rowFormat, confirmpaymentreceived.name(), "--trade-id=<trade-id>", "Confirm payment received");
stream.println();
stream.format(rowFormat, keepfunds.name(), "--trade-id=<trade-id>", "Keep received funds in Bisq wallet");
stream.println();
stream.format(rowFormat, withdrawfunds.name(), "--trade-id=<trade-id> --address=<btc-address> \\",
"Withdraw received funds to external wallet address");
stream.format(rowFormat, "", "[--memo=<\"memo\">]", "");
stream.println();
stream.format(rowFormat, getpaymentmethods.name(), "", "Get list of supported payment account method ids");
stream.println();
stream.format(rowFormat, getpaymentacctform.name(), "--payment-method-id=<payment-method-id>", "Get a new payment account form");
stream.println();
stream.format(rowFormat, createpaymentacct.name(), "--payment-account-form=<path>", "Create a new payment account");
stream.println();
stream.format(rowFormat, createcryptopaymentacct.name(), "--account-name=<name> \\", "Create a new cryptocurrency payment account");
stream.format(rowFormat, "", "--currency-code=<bsq> \\", "");
stream.format(rowFormat, "", "--address=<bsq-address>", "");
stream.format(rowFormat, "", "--trade-instant=<true|false>", "");
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");
stream.println();
stream.format(rowFormat, unlockwallet.name(), "--wallet-password=<password> --timeout=<seconds>",
"Store wallet password in memory for timeout seconds");
stream.println();
stream.format(rowFormat, setwalletpassword.name(), "--wallet-password=<password> \\",
"Encrypt wallet with password, or set new password on encrypted wallet");
stream.format(rowFormat, "", "[--new-wallet-password=<new-password>]", "");
stream.println();
stream.format(rowFormat, stop.name(), "", "Shut down the server");
stream.println();
stream.println("Method Help Usage: bisq-cli [options] <method> --help");
stream.println();
} catch (IOException ex) {
ex.printStackTrace(stream);
}
}
}

View file

@ -0,0 +1,79 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package bisq.cli;
import static com.google.common.base.Strings.padEnd;
import static com.google.common.base.Strings.padStart;
class ColumnHeaderConstants {
// For inserting 2 spaces between column headers.
static final String COL_HEADER_DELIMITER = " ";
// Table column header format specs, right padded with two spaces. In some cases
// such as COL_HEADER_CREATION_DATE, COL_HEADER_VOLUME and COL_HEADER_UUID, the
// 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 = "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";
static final String COL_HEADER_RESERVED_BALANCE = "Reserved Balance";
static final String COL_HEADER_TOTAL_AVAILABLE_BALANCE = "Total Available Balance";
static final String COL_HEADER_LOCKED_BALANCE = "Locked Balance";
static final String COL_HEADER_LOCKED_FOR_VOTING_BALANCE = "Locked For Voting Balance";
static final String COL_HEADER_LOCKUP_BONDS_BALANCE = "Lockup Bonds Balance";
static final String COL_HEADER_UNLOCKING_BONDS_BALANCE = "Unlocking Bonds Balance";
static final String COL_HEADER_UNVERIFIED_BALANCE = "Unverified Balance";
static final String COL_HEADER_CONFIRMATIONS = "Confirmations";
static final String COL_HEADER_IS_USED_ADDRESS = "Is Used";
static final String COL_HEADER_CREATION_DATE = padEnd("Creation Date (UTC)", 20, ' ');
static final String COL_HEADER_CURRENCY = "Currency";
static final String COL_HEADER_DIRECTION = "Buy/Sell";
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_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";
static final String COL_HEADER_TRADE_PAYMENT_SENT = padEnd("%-3s Sent", 8, ' ');
static final String COL_HEADER_TRADE_PAYMENT_RECEIVED = padEnd("%-3s Received", 12, ' ');
static final String COL_HEADER_TRADE_PAYOUT_PUBLISHED = "Payout Published";
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 = 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)";
static final String COL_HEADER_TX_OUTPUT_SUM = "Tx Outputs (BTC)";
static final String COL_HEADER_TX_FEE = "Tx Fee (BTC)";
static final String COL_HEADER_TX_SIZE = "Tx Size (Bytes)";
static final String COL_HEADER_TX_IS_CONFIRMED = "Is Confirmed";
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, ' ');
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<String> getSupportedCryptoCurrencies() {
final List<String> result = new ArrayList<>();
result.add("BSQ");
result.sort(String::compareTo);
return result;
}
}

View file

@ -0,0 +1,155 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package bisq.cli;
import bisq.proto.grpc.TxFeeRateInfo;
import com.google.common.annotations.VisibleForTesting;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.math.BigDecimal;
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(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");
@SuppressWarnings("BigDecimalMethodWithoutRoundingCalled")
public static String formatSatoshis(long sats) {
return BTC_FORMAT.format(BigDecimal.valueOf(sats).divide(SATOSHI_DIVISOR));
}
@SuppressWarnings("BigDecimalMethodWithoutRoundingCalled")
public static String formatBsq(long sats) {
return BSQ_FORMAT.format(BigDecimal.valueOf(sats).divide(BSQ_SATOSHI_DIVISOR));
}
public static String formatBsqAmount(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, min network rate: %s sats/byte",
formatFeeSatoshis(txFeeRateInfo.getCustomTxFeeRate()),
formatFeeSatoshis(txFeeRateInfo.getFeeServiceRate()),
formatFeeSatoshis(txFeeRateInfo.getMinFeeServiceRate()));
else
return format("tx fee rate: %s sats/byte, min tx fee rate: %s sats/byte",
formatFeeSatoshis(txFeeRateInfo.getFeeServiceRate()),
formatFeeSatoshis(txFeeRateInfo.getMinFeeServiceRate()));
}
public static String formatAmountRange(long minAmount, long amount) {
return minAmount != amount
? formatSatoshis(minAmount) + " - " + formatSatoshis(amount)
: formatSatoshis(amount);
}
public static String formatVolumeRange(long minVolume, long volume) {
return minVolume != volume
? formatOfferVolume(minVolume) + " - " + formatOfferVolume(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 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 formatCryptoCurrencyPrice(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(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) {
if (btc.startsWith("-"))
throw new IllegalArgumentException(format("'%s' is not a positive number", btc));
try {
return new BigDecimal(btc).multiply(SATOSHI_DIVISOR).longValue();
} catch (NumberFormatException e) {
throw new IllegalArgumentException(format("'%s' is not a number", btc));
}
}
public static double toSecurityDepositAsPct(String securityDepositInput) {
try {
return new BigDecimal(securityDepositInput)
.multiply(SECURITY_DEPOSIT_MULTIPLICAND).doubleValue();
} catch (NumberFormatException e) {
throw new IllegalArgumentException(format("'%s' is not a number", securityDepositInput));
}
}
public static String formatFeeSatoshis(long sats) {
return BTC_TX_FEE_FORMAT.format(BigDecimal.valueOf(sats));
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<OfferInfo> 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<OfferInfo, String> 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<String, String> 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();
}
}

View file

@ -0,0 +1,516 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package bisq.cli;
import bisq.proto.grpc.AddressBalanceInfo;
import bisq.proto.grpc.BalancesInfo;
import bisq.proto.grpc.BsqBalanceInfo;
import bisq.proto.grpc.BtcBalanceInfo;
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;
import bisq.proto.grpc.GetMyOffersRequest;
import bisq.proto.grpc.GetOfferRequest;
import bisq.proto.grpc.GetOffersRequest;
import bisq.proto.grpc.GetPaymentAccountFormRequest;
import bisq.proto.grpc.GetPaymentAccountsRequest;
import bisq.proto.grpc.GetPaymentMethodsRequest;
import bisq.proto.grpc.GetTradeRequest;
import bisq.proto.grpc.GetTransactionRequest;
import bisq.proto.grpc.GetTxFeeRateRequest;
import bisq.proto.grpc.GetUnusedBsqAddressRequest;
import bisq.proto.grpc.GetVersionRequest;
import bisq.proto.grpc.KeepFundsRequest;
import bisq.proto.grpc.LockWalletRequest;
import bisq.proto.grpc.MarketPriceRequest;
import bisq.proto.grpc.OfferInfo;
import bisq.proto.grpc.RegisterDisputeAgentRequest;
import bisq.proto.grpc.RemoveWalletPasswordRequest;
import bisq.proto.grpc.SendBsqRequest;
import bisq.proto.grpc.SendBtcRequest;
import bisq.proto.grpc.SetTxFeeRatePreferenceRequest;
import bisq.proto.grpc.SetWalletPasswordRequest;
import bisq.proto.grpc.StopRequest;
import bisq.proto.grpc.TakeOfferReply;
import bisq.proto.grpc.TakeOfferRequest;
import bisq.proto.grpc.TradeInfo;
import bisq.proto.grpc.TxFeeRateInfo;
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;
import protobuf.PaymentMethod;
import java.util.ArrayList;
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;
import static protobuf.OfferPayload.Direction.SELL;
@SuppressWarnings("ResultOfMethodCallIgnored")
@Slf4j
public final class GrpcClient {
private final GrpcStubs grpcStubs;
public GrpcClient(String apiHost, int apiPort, String apiPassword) {
this.grpcStubs = new GrpcStubs(apiHost, apiPort, apiPassword);
}
public String getVersion() {
var request = GetVersionRequest.newBuilder().build();
return grpcStubs.versionService.getVersion(request).getVersion();
}
public BalancesInfo getBalances() {
return getBalances("");
}
public BsqBalanceInfo getBsqBalances() {
return getBalances("BSQ").getBsq();
}
public BtcBalanceInfo getBtcBalances() {
return getBalances("BTC").getBtc();
}
public BalancesInfo getBalances(String currencyCode) {
var request = GetBalancesRequest.newBuilder()
.setCurrencyCode(currencyCode)
.build();
return grpcStubs.walletsService.getBalances(request).getBalances();
}
public AddressBalanceInfo getAddressBalance(String address) {
var request = GetAddressBalanceRequest.newBuilder()
.setAddress(address).build();
return grpcStubs.walletsService.getAddressBalance(request).getAddressBalanceInfo();
}
public double getBtcPrice(String currencyCode) {
var request = MarketPriceRequest.newBuilder()
.setCurrencyCode(currencyCode)
.build();
return grpcStubs.priceService.getMarketPrice(request).getPrice();
}
public List<AddressBalanceInfo> getFundingAddresses() {
var request = GetFundingAddressesRequest.newBuilder().build();
return grpcStubs.walletsService.getFundingAddresses(request).getAddressBalanceInfoList();
}
public String getUnusedBsqAddress() {
var request = GetUnusedBsqAddressRequest.newBuilder().build();
return grpcStubs.walletsService.getUnusedBsqAddress(request).getAddress();
}
public String getUnusedBtcAddress() {
var request = GetFundingAddressesRequest.newBuilder().build();
var addressBalances = grpcStubs.walletsService.getFundingAddresses(request)
.getAddressBalanceInfoList();
//noinspection OptionalGetWithoutIsPresent
return addressBalances.stream()
.filter(AddressBalanceInfo::getIsAddressUnused)
.findFirst()
.get()
.getAddress();
}
public TxInfo sendBsq(String address, String amount, String txFeeRate) {
var request = SendBsqRequest.newBuilder()
.setAddress(address)
.setAmount(amount)
.setTxFeeRate(txFeeRate)
.build();
return grpcStubs.walletsService.sendBsq(request).getTxInfo();
}
public TxInfo sendBtc(String address, String amount, String txFeeRate, String memo) {
var request = SendBtcRequest.newBuilder()
.setAddress(address)
.setAmount(amount)
.setTxFeeRate(txFeeRate)
.setMemo(memo)
.build();
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();
}
public TxFeeRateInfo setTxFeeRate(long txFeeRate) {
var request = SetTxFeeRatePreferenceRequest.newBuilder()
.setTxFeeRatePreference(txFeeRate)
.build();
return grpcStubs.walletsService.setTxFeeRatePreference(request).getTxFeeRateInfo();
}
public TxFeeRateInfo unsetTxFeeRate() {
var request = UnsetTxFeeRatePreferenceRequest.newBuilder().build();
return grpcStubs.walletsService.unsetTxFeeRatePreference(request).getTxFeeRateInfo();
}
public TxInfo getTransaction(String txId) {
var request = GetTransactionRequest.newBuilder()
.setTxId(txId)
.build();
return grpcStubs.walletsService.getTransaction(request).getTxInfo();
}
public OfferInfo createFixedPricedOffer(String direction,
String currencyCode,
long amount,
long minAmount,
String fixedPrice,
double securityDeposit,
String paymentAcctId,
String makerFeeCurrencyCode) {
return createOffer(direction,
currencyCode,
amount,
minAmount,
false,
fixedPrice,
0.00,
securityDeposit,
paymentAcctId,
makerFeeCurrencyCode);
}
public OfferInfo createMarketBasedPricedOffer(String direction,
String currencyCode,
long amount,
long minAmount,
double marketPriceMargin,
double securityDeposit,
String paymentAcctId,
String makerFeeCurrencyCode) {
return createOffer(direction,
currencyCode,
amount,
minAmount,
true,
"0",
marketPriceMargin,
securityDeposit,
paymentAcctId,
makerFeeCurrencyCode);
}
public OfferInfo createOffer(String direction,
String currencyCode,
long amount,
long minAmount,
boolean useMarketBasedPrice,
String fixedPrice,
double marketPriceMargin,
double securityDeposit,
String paymentAcctId,
String makerFeeCurrencyCode) {
var request = CreateOfferRequest.newBuilder()
.setDirection(direction)
.setCurrencyCode(currencyCode)
.setAmount(amount)
.setMinAmount(minAmount)
.setUseMarketBasedPrice(useMarketBasedPrice)
.setPrice(fixedPrice)
.setMarketPriceMargin(marketPriceMargin)
.setBuyerSecurityDeposit(securityDeposit)
.setPaymentAccountId(paymentAcctId)
.setMakerFeeCurrencyCode(makerFeeCurrencyCode)
.build();
return grpcStubs.offersService.createOffer(request).getOffer();
}
public void cancelOffer(String offerId) {
var request = CancelOfferRequest.newBuilder()
.setId(offerId)
.build();
grpcStubs.offersService.cancelOffer(request);
}
public OfferInfo getOffer(String offerId) {
var request = GetOfferRequest.newBuilder()
.setId(offerId)
.build();
return grpcStubs.offersService.getOffer(request).getOffer();
}
public OfferInfo getMyOffer(String offerId) {
var request = GetMyOfferRequest.newBuilder()
.setId(offerId)
.build();
return grpcStubs.offersService.getMyOffer(request).getOffer();
}
public List<OfferInfo> getOffers(String direction, String currencyCode) {
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<OfferInfo> getCryptoCurrencyOffers(String direction, String currencyCode) {
return getOffers(direction, "BTC").stream()
.filter(o -> o.getBaseCurrencyCode().equalsIgnoreCase(currencyCode))
.collect(toList());
}
public List<OfferInfo> getOffersSortedByDate(String currencyCode) {
ArrayList<OfferInfo> offers = new ArrayList<>();
offers.addAll(getOffers(BUY.name(), currencyCode));
offers.addAll(getOffers(SELL.name(), currencyCode));
return sortOffersByDate(offers);
}
public List<OfferInfo> getOffersSortedByDate(String direction, String currencyCode) {
var offers = getOffers(direction, currencyCode);
return offers.isEmpty() ? offers : sortOffersByDate(offers);
}
public List<OfferInfo> getBsqOffersSortedByDate() {
ArrayList<OfferInfo> offers = new ArrayList<>();
offers.addAll(getCryptoCurrencyOffers(BUY.name(), "BSQ"));
offers.addAll(getCryptoCurrencyOffers(SELL.name(), "BSQ"));
return sortOffersByDate(offers);
}
public List<OfferInfo> getMyOffers(String direction, String currencyCode) {
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<OfferInfo> getMyCryptoCurrencyOffers(String direction, String currencyCode) {
return getMyOffers(direction, "BTC").stream()
.filter(o -> o.getBaseCurrencyCode().equalsIgnoreCase(currencyCode))
.collect(toList());
}
public List<OfferInfo> getMyOffersSortedByDate(String direction, String currencyCode) {
var offers = getMyOffers(direction, currencyCode);
return offers.isEmpty() ? offers : sortOffersByDate(offers);
}
public List<OfferInfo> getMyOffersSortedByDate(String currencyCode) {
ArrayList<OfferInfo> offers = new ArrayList<>();
offers.addAll(getMyOffers(BUY.name(), currencyCode));
offers.addAll(getMyOffers(SELL.name(), currencyCode));
return sortOffersByDate(offers);
}
public List<OfferInfo> getMyBsqOffersSortedByDate() {
ArrayList<OfferInfo> offers = new ArrayList<>();
offers.addAll(getMyCryptoCurrencyOffers(BUY.name(), "BSQ"));
offers.addAll(getMyCryptoCurrencyOffers(SELL.name(), "BSQ"));
return sortOffersByDate(offers);
}
public OfferInfo getMostRecentOffer(String direction, String currencyCode) {
List<OfferInfo> offers = getOffersSortedByDate(direction, currencyCode);
return offers.isEmpty() ? null : offers.get(offers.size() - 1);
}
public List<OfferInfo> sortOffersByDate(List<OfferInfo> offerInfoList) {
return offerInfoList.stream()
.sorted(comparing(OfferInfo::getDate))
.collect(toList());
}
public TakeOfferReply getTakeOfferReply(String offerId, String paymentAccountId, String takerFeeCurrencyCode) {
var request = TakeOfferRequest.newBuilder()
.setOfferId(offerId)
.setPaymentAccountId(paymentAccountId)
.setTakerFeeCurrencyCode(takerFeeCurrencyCode)
.build();
return grpcStubs.tradesService.takeOffer(request);
}
public TradeInfo takeOffer(String offerId, String paymentAccountId, String takerFeeCurrencyCode) {
var reply = getTakeOfferReply(offerId, paymentAccountId, takerFeeCurrencyCode);
if (reply.hasTrade())
return reply.getTrade();
else
throw new IllegalStateException(reply.getFailureReason().getDescription());
}
public TradeInfo getTrade(String tradeId) {
var request = GetTradeRequest.newBuilder()
.setTradeId(tradeId)
.build();
return grpcStubs.tradesService.getTrade(request).getTrade();
}
public void confirmPaymentStarted(String tradeId) {
var request = ConfirmPaymentStartedRequest.newBuilder()
.setTradeId(tradeId)
.build();
grpcStubs.tradesService.confirmPaymentStarted(request);
}
public void confirmPaymentReceived(String tradeId) {
var request = ConfirmPaymentReceivedRequest.newBuilder()
.setTradeId(tradeId)
.build();
grpcStubs.tradesService.confirmPaymentReceived(request);
}
public void keepFunds(String tradeId) {
var request = KeepFundsRequest.newBuilder()
.setTradeId(tradeId)
.build();
grpcStubs.tradesService.keepFunds(request);
}
public void withdrawFunds(String tradeId, String address, String memo) {
var request = WithdrawFundsRequest.newBuilder()
.setTradeId(tradeId)
.setAddress(address)
.setMemo(memo)
.build();
grpcStubs.tradesService.withdrawFunds(request);
}
public List<PaymentMethod> getPaymentMethods() {
var request = GetPaymentMethodsRequest.newBuilder().build();
return grpcStubs.paymentAccountsService.getPaymentMethods(request).getPaymentMethodsList();
}
public String getPaymentAcctFormAsJson(String paymentMethodId) {
var request = GetPaymentAccountFormRequest.newBuilder()
.setPaymentMethodId(paymentMethodId)
.build();
return grpcStubs.paymentAccountsService.getPaymentAccountForm(request).getPaymentAccountFormJson();
}
public PaymentAccount createPaymentAccount(String json) {
var request = CreatePaymentAccountRequest.newBuilder()
.setPaymentAccountForm(json)
.build();
return grpcStubs.paymentAccountsService.createPaymentAccount(request).getPaymentAccount();
}
public List<PaymentAccount> getPaymentAccounts() {
var request = GetPaymentAccountsRequest.newBuilder().build();
return grpcStubs.paymentAccountsService.getPaymentAccounts(request).getPaymentAccountsList();
}
public PaymentAccount createCryptoCurrencyPaymentAccount(String accountName,
String currencyCode,
String address,
boolean tradeInstant) {
var request = CreateCryptoCurrencyPaymentAccountRequest.newBuilder()
.setAccountName(accountName)
.setCurrencyCode(currencyCode)
.setAddress(address)
.setTradeInstant(tradeInstant)
.build();
return grpcStubs.paymentAccountsService.createCryptoCurrencyPaymentAccount(request).getPaymentAccount();
}
public List<PaymentMethod> getCryptoPaymentMethods() {
var request = GetCryptoCurrencyPaymentMethodsRequest.newBuilder().build();
return grpcStubs.paymentAccountsService.getCryptoCurrencyPaymentMethods(request).getPaymentMethodsList();
}
public void lockWallet() {
var request = LockWalletRequest.newBuilder().build();
grpcStubs.walletsService.lockWallet(request);
}
public void unlockWallet(String walletPassword, long timeout) {
var request = UnlockWalletRequest.newBuilder()
.setPassword(walletPassword)
.setTimeout(timeout).build();
grpcStubs.walletsService.unlockWallet(request);
}
public void removeWalletPassword(String walletPassword) {
var request = RemoveWalletPasswordRequest.newBuilder()
.setPassword(walletPassword).build();
grpcStubs.walletsService.removeWalletPassword(request);
}
public void setWalletPassword(String walletPassword) {
var request = SetWalletPasswordRequest.newBuilder()
.setPassword(walletPassword).build();
grpcStubs.walletsService.setWalletPassword(request);
}
public void setWalletPassword(String oldWalletPassword, String newWalletPassword) {
var request = SetWalletPasswordRequest.newBuilder()
.setPassword(oldWalletPassword)
.setNewPassword(newWalletPassword).build();
grpcStubs.walletsService.setWalletPassword(request);
}
public void registerDisputeAgent(String disputeAgentType, String registrationKey) {
var request = RegisterDisputeAgentRequest.newBuilder()
.setDisputeAgentType(disputeAgentType).setRegistrationKey(registrationKey).build();
grpcStubs.disputeAgentsService.registerDisputeAgent(request);
}
public void stopServer() {
var request = StopRequest.newBuilder().build();
grpcStubs.shutdownService.stop(request);
}
public String getMethodHelp(Method method) {
var request = GetMethodHelpRequest.newBuilder().setMethodName(method.name()).build();
return grpcStubs.helpService.getMethodHelp(request).getMethodHelp();
}
}

View file

@ -0,0 +1,69 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package bisq.cli;
import bisq.proto.grpc.DisputeAgentsGrpc;
import bisq.proto.grpc.GetVersionGrpc;
import bisq.proto.grpc.HelpGrpc;
import bisq.proto.grpc.OffersGrpc;
import bisq.proto.grpc.PaymentAccountsGrpc;
import bisq.proto.grpc.PriceGrpc;
import bisq.proto.grpc.ShutdownServerGrpc;
import bisq.proto.grpc.TradesGrpc;
import bisq.proto.grpc.WalletsGrpc;
import io.grpc.CallCredentials;
import io.grpc.ManagedChannelBuilder;
import static java.util.concurrent.TimeUnit.SECONDS;
public final class GrpcStubs {
public final DisputeAgentsGrpc.DisputeAgentsBlockingStub disputeAgentsService;
public final HelpGrpc.HelpBlockingStub helpService;
public final GetVersionGrpc.GetVersionBlockingStub versionService;
public final OffersGrpc.OffersBlockingStub offersService;
public final PaymentAccountsGrpc.PaymentAccountsBlockingStub paymentAccountsService;
public final PriceGrpc.PriceBlockingStub priceService;
public final ShutdownServerGrpc.ShutdownServerBlockingStub shutdownService;
public final TradesGrpc.TradesBlockingStub tradesService;
public final WalletsGrpc.WalletsBlockingStub walletsService;
public GrpcStubs(String apiHost, int apiPort, String apiPassword) {
CallCredentials credentials = new PasswordCallCredentials(apiPassword);
var channel = ManagedChannelBuilder.forAddress(apiHost, apiPort).usePlaintext().build();
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
channel.shutdown().awaitTermination(1, SECONDS);
} catch (InterruptedException ex) {
throw new IllegalStateException(ex);
}
}));
this.disputeAgentsService = DisputeAgentsGrpc.newBlockingStub(channel).withCallCredentials(credentials);
this.helpService = HelpGrpc.newBlockingStub(channel).withCallCredentials(credentials);
this.versionService = GetVersionGrpc.newBlockingStub(channel).withCallCredentials(credentials);
this.offersService = OffersGrpc.newBlockingStub(channel).withCallCredentials(credentials);
this.paymentAccountsService = PaymentAccountsGrpc.newBlockingStub(channel).withCallCredentials(credentials);
this.priceService = PriceGrpc.newBlockingStub(channel).withCallCredentials(credentials);
this.shutdownService = ShutdownServerGrpc.newBlockingStub(channel).withCallCredentials(credentials);
this.tradesService = TradesGrpc.newBlockingStub(channel).withCallCredentials(credentials);
this.walletsService = WalletsGrpc.newBlockingStub(channel).withCallCredentials(credentials);
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
package bisq.cli;
/**
* Currently supported api methods.
*/
public enum Method {
canceloffer,
confirmpaymentreceived,
confirmpaymentstarted,
createoffer,
createpaymentacct,
createcryptopaymentacct,
getaddressbalance,
getbalance,
getbtcprice,
getfundingaddresses,
getmyoffer,
getmyoffers,
getoffer,
getoffers,
getpaymentacctform,
getpaymentaccts,
getpaymentmethods,
gettrade,
gettransaction,
gettxfeerate,
getunusedbsqaddress,
getversion,
keepfunds,
lockwallet,
registerdisputeagent,
removewalletpassword,
sendbsq,
sendbtc,
verifybsqsenttoaddress,
settxfeerate,
setwalletpassword,
takeoffer,
unlockwallet,
unsettxfeerate,
withdrawfunds,
stop
}

View file

@ -0,0 +1,62 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package bisq.cli;
import io.grpc.CallCredentials;
import io.grpc.Metadata;
import io.grpc.Metadata.Key;
import java.util.concurrent.Executor;
import static io.grpc.Metadata.ASCII_STRING_MARSHALLER;
import static io.grpc.Status.UNAUTHENTICATED;
import static java.lang.String.format;
/**
* Sets the {@value PASSWORD_KEY} rpc call header to a given value.
*/
class PasswordCallCredentials extends CallCredentials {
public static final String PASSWORD_KEY = "password";
private final String passwordValue;
public PasswordCallCredentials(String passwordValue) {
if (passwordValue == null)
throw new IllegalArgumentException(format("'%s' value must not be null", PASSWORD_KEY));
this.passwordValue = passwordValue;
}
@Override
public void applyRequestMetadata(RequestInfo requestInfo, Executor appExecutor, MetadataApplier metadataApplier) {
appExecutor.execute(() -> {
try {
var headers = new Metadata();
var passwordKey = Key.of(PASSWORD_KEY, ASCII_STRING_MARSHALLER);
headers.put(passwordKey, passwordValue);
metadataApplier.apply(headers);
} catch (Throwable ex) {
metadataApplier.fail(UNAUTHENTICATED.withCause(ex));
}
});
}
@Override
public void thisUsesUnstableApi() {
}
}

View file

@ -0,0 +1,276 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package bisq.cli;
import bisq.proto.grpc.AddressBalanceInfo;
import bisq.proto.grpc.BalancesInfo;
import bisq.proto.grpc.BsqBalanceInfo;
import bisq.proto.grpc.BtcBalanceInfo;
import bisq.proto.grpc.OfferInfo;
import protobuf.PaymentAccount;
import com.google.common.annotations.VisibleForTesting;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import java.util.stream.Collectors;
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;
import static java.util.TimeZone.getTimeZone;
@VisibleForTesting
public class TableFormat {
static final TimeZone TZ_UTC = getTimeZone("UTC");
static final SimpleDateFormat DATE_FORMAT_ISO_8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
public static String formatAddressBalanceTbl(List<AddressBalanceInfo> addressBalanceInfo) {
String headerFormatString = COL_HEADER_ADDRESS + COL_HEADER_DELIMITER
+ COL_HEADER_AVAILABLE_BALANCE + COL_HEADER_DELIMITER
+ COL_HEADER_CONFIRMATIONS + COL_HEADER_DELIMITER
+ COL_HEADER_IS_USED_ADDRESS + COL_HEADER_DELIMITER + "\n";
String headerLine = format(headerFormatString, "BTC");
String colDataFormat = "%-" + COL_HEADER_ADDRESS.length() + "s" // lt justify
+ " %" + (COL_HEADER_AVAILABLE_BALANCE.length() - 1) + "s" // rt justify
+ " %" + COL_HEADER_CONFIRMATIONS.length() + "d" // rt justify
+ " %-" + COL_HEADER_IS_USED_ADDRESS.length() + "s"; // lt justify
return headerLine
+ addressBalanceInfo.stream()
.map(info -> format(colDataFormat,
info.getAddress(),
formatSatoshis(info.getBalance()),
info.getNumConfirmations(),
info.getIsAddressUnused() ? "NO" : "YES"))
.collect(Collectors.joining("\n"));
}
public static String formatBalancesTbls(BalancesInfo balancesInfo) {
return "BTC" + "\n"
+ formatBtcBalanceInfoTbl(balancesInfo.getBtc()) + "\n"
+ "BSQ" + "\n"
+ formatBsqBalanceInfoTbl(balancesInfo.getBsq());
}
public static String formatBsqBalanceInfoTbl(BsqBalanceInfo bsqBalanceInfo) {
String headerLine = COL_HEADER_AVAILABLE_CONFIRMED_BALANCE + COL_HEADER_DELIMITER
+ COL_HEADER_UNVERIFIED_BALANCE + COL_HEADER_DELIMITER
+ COL_HEADER_UNCONFIRMED_CHANGE_BALANCE + COL_HEADER_DELIMITER
+ COL_HEADER_LOCKED_FOR_VOTING_BALANCE + COL_HEADER_DELIMITER
+ COL_HEADER_LOCKUP_BONDS_BALANCE + COL_HEADER_DELIMITER
+ COL_HEADER_UNLOCKING_BONDS_BALANCE + COL_HEADER_DELIMITER + "\n";
String colDataFormat = "%" + COL_HEADER_AVAILABLE_CONFIRMED_BALANCE.length() + "s" // rt justify
+ " %" + (COL_HEADER_UNVERIFIED_BALANCE.length() + 1) + "s" // rt justify
+ " %" + (COL_HEADER_UNCONFIRMED_CHANGE_BALANCE.length() + 1) + "s" // rt justify
+ " %" + (COL_HEADER_LOCKED_FOR_VOTING_BALANCE.length() + 1) + "s" // rt justify
+ " %" + (COL_HEADER_LOCKUP_BONDS_BALANCE.length() + 1) + "s" // rt justify
+ " %" + (COL_HEADER_UNLOCKING_BONDS_BALANCE.length() + 1) + "s"; // rt justify
return headerLine + format(colDataFormat,
formatBsq(bsqBalanceInfo.getAvailableConfirmedBalance()),
formatBsq(bsqBalanceInfo.getUnverifiedBalance()),
formatBsq(bsqBalanceInfo.getUnconfirmedChangeBalance()),
formatBsq(bsqBalanceInfo.getLockedForVotingBalance()),
formatBsq(bsqBalanceInfo.getLockupBondsBalance()),
formatBsq(bsqBalanceInfo.getUnlockingBondsBalance()));
}
public static String formatBtcBalanceInfoTbl(BtcBalanceInfo btcBalanceInfo) {
String headerLine = COL_HEADER_AVAILABLE_BALANCE + COL_HEADER_DELIMITER
+ COL_HEADER_RESERVED_BALANCE + COL_HEADER_DELIMITER
+ COL_HEADER_TOTAL_AVAILABLE_BALANCE + COL_HEADER_DELIMITER
+ COL_HEADER_LOCKED_BALANCE + COL_HEADER_DELIMITER + "\n";
String colDataFormat = "%" + COL_HEADER_AVAILABLE_BALANCE.length() + "s" // rt justify
+ " %" + (COL_HEADER_RESERVED_BALANCE.length() + 1) + "s" // rt justify
+ " %" + (COL_HEADER_TOTAL_AVAILABLE_BALANCE.length() + 1) + "s" // rt justify
+ " %" + (COL_HEADER_LOCKED_BALANCE.length() + 1) + "s"; // rt justify
return headerLine + format(colDataFormat,
formatSatoshis(btcBalanceInfo.getAvailableBalance()),
formatSatoshis(btcBalanceInfo.getReservedBalance()),
formatSatoshis(btcBalanceInfo.getTotalAvailableBalance()),
formatSatoshis(btcBalanceInfo.getLockedBalance()));
}
public static String formatPaymentAcctTbl(List<PaymentAccount> paymentAccounts) {
// Some column values might be longer than header, so we need to calculate them.
int nameColWidth = getLongestColumnSize(
COL_HEADER_NAME.length(),
paymentAccounts.stream().map(PaymentAccount::getAccountName)
.collect(Collectors.toList()));
int paymentMethodColWidth = getLongestColumnSize(
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
+ 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<OfferInfo> 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<OfferInfo> 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 -> 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,
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
+ offers.stream()
.map(o -> format(colDataFormat,
o.getDirection(),
formatPrice(o.getPrice()),
formatAmountRange(o.getMinAmount(), o.getAmount()),
formatVolumeRange(o.getMinVolume(), o.getVolume()),
o.getPaymentMethodShortName(),
formatTimestamp(o.getDate()),
o.getId()))
.collect(Collectors.joining("\n"));
}
private static String formatCryptoCurrencyOfferTable(List<OfferInfo> offers, String cryptoCurrencyCode) {
// Some column values might be longer than header, so we need to calculate them.
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_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
+ offers.stream()
.map(o -> format(colDataFormat,
directionFormat.apply(o),
formatCryptoCurrencyPrice(o.getPrice()),
formatAmountRange(o.getMinAmount(), o.getAmount()),
formatCryptoCurrencyVolumeRange(o.getMinVolume(), o.getVolume()),
o.getPaymentMethodShortName(),
formatTimestamp(o.getDate()),
o.getId()))
.collect(Collectors.joining("\n"));
}
private static int getLongestPaymentMethodColWidth(List<OfferInfo> offers) {
return getLongestColumnSize(
COL_HEADER_PAYMENT_METHOD.length(),
offers.stream()
.map(OfferInfo::getPaymentMethodShortName)
.collect(Collectors.toList()));
}
private static int getLongestAmountColWidth(List<OfferInfo> offers) {
return getLongestColumnSize(
COL_HEADER_AMOUNT.length(),
offers.stream()
.map(o -> formatAmountRange(o.getMinAmount(), o.getAmount()))
.collect(Collectors.toList()));
}
private static int getLongestVolumeColWidth(List<OfferInfo> 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<OfferInfo> 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<String> strings) {
int longest = max(strings, comparing(String::length)).length();
return Math.max(longest, headerLength);
}
private static String formatTimestamp(long timestamp) {
DATE_FORMAT_ISO_8601.setTimeZone(TZ_UTC);
return DATE_FORMAT_ISO_8601.format(new Date(timestamp));
}
}

View file

@ -0,0 +1,221 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package bisq.cli;
import bisq.proto.grpc.ContractInfo;
import bisq.proto.grpc.TradeInfo;
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.*;
import static com.google.common.base.Strings.padEnd;
@VisibleForTesting
public class TradeFormat {
private static final String YES = "YES";
private static final String NO = "NO";
// TODO add String format(List<TradeInfo> trades)
@VisibleForTesting
public static String format(TradeInfo tradeInfo) {
// Some column values might be longer than header, so we need to calculate them.
int shortIdColWidth = Math.max(COL_HEADER_TRADE_SHORT_ID.length(), tradeInfo.getShortId().length());
int roleColWidth = Math.max(COL_HEADER_TRADE_ROLE.length(), tradeInfo.getRole().length());
// We only show taker fee under its header when user is the taker.
boolean isTaker = tradeInfo.getRole().toLowerCase().contains("taker");
Supplier<String> makerFeeHeader = () -> !isTaker ?
COL_HEADER_TRADE_MAKER_FEE + COL_HEADER_DELIMITER
: "";
Supplier<String> makerFeeHeaderSpec = () -> !isTaker ?
"%" + (COL_HEADER_TRADE_MAKER_FEE.length() + 2) + "s"
: "";
Supplier<String> takerFeeHeader = () -> isTaker ?
COL_HEADER_TRADE_TAKER_FEE + COL_HEADER_DELIMITER
: "";
Supplier<String> takerFeeHeaderSpec = () -> isTaker ?
"%" + (COL_HEADER_TRADE_TAKER_FEE.length() + 2) + "s"
: "";
boolean showBsqBuyerAddress = shouldShowBsqBuyerAddress(tradeInfo, isTaker);
Supplier<String> bsqBuyerAddressHeader = () -> showBsqBuyerAddress ? COL_HEADER_TRADE_BSQ_BUYER_ADDRESS : "";
Supplier<String> 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
+ padEnd(COL_HEADER_TRADE_AMOUNT, 12, ' ') + COL_HEADER_DELIMITER
+ padEnd(COL_HEADER_TRADE_TX_FEE, 12, ' ') + COL_HEADER_DELIMITER
+ 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
+ COL_HEADER_TRADE_PAYMENT_SENT + COL_HEADER_DELIMITER
+ 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();
String baseCurrencyCode = tradeInfo.getOffer().getBaseCurrencyCode();
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, isTaker),
/* COL_HEADER_TRADE_BUYER_COST */ 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
+ 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
+ " %-" + (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() + 2) + "s"
+ bsqBuyerAddressHeaderSpec.get();
return headerLine + formatTradeData(colDataFormat, tradeInfo, isTaker, showBsqBuyerAddress);
}
private static String formatTradeData(String format,
TradeInfo tradeInfo,
boolean isTaker,
boolean showBsqBuyerAddress) {
return String.format(format,
tradeInfo.getShortId(),
tradeInfo.getRole(),
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,
bsqReceiveAddress.apply(tradeInfo, showBsqBuyerAddress));
}
private static final Function<TradeInfo, String> priceHeader = (t) ->
t.getOffer().getBaseCurrencyCode().equals("BTC")
? COL_HEADER_PRICE
: COL_HEADER_PRICE_OF_ALTCOIN;
private static final Function<TradeInfo, String> priceHeaderCurrencyCode = (t) ->
t.getOffer().getBaseCurrencyCode().equals("BTC")
? t.getOffer().getCounterCurrencyCode()
: t.getOffer().getBaseCurrencyCode();
private static final BiFunction<TradeInfo, Boolean, String> makerTakerFeeHeaderCurrencyCode = (t, isTaker) -> {
if (isTaker) {
return t.getIsCurrencyForTakerFeeBtc() ? "BTC" : "BSQ";
} else {
return t.getOffer().getIsCurrencyForMakerFeeBtc() ? "BTC" : "BSQ";
}
};
private static final Function<TradeInfo, String> paymentStatusHeaderCurrencyCode = (t) ->
t.getOffer().getBaseCurrencyCode().equals("BTC")
? t.getOffer().getCounterCurrencyCode()
: t.getOffer().getBaseCurrencyCode();
private static final Function<TradeInfo, String> priceFormat = (t) ->
t.getOffer().getBaseCurrencyCode().equals("BTC")
? formatPrice(t.getTradePrice())
: formatCryptoCurrencyPrice(t.getOffer().getPrice());
private static final Function<TradeInfo, String> amountFormat = (t) ->
t.getOffer().getBaseCurrencyCode().equals("BTC")
? formatSatoshis(t.getTradeAmountAsLong())
: formatCryptoCurrencyOfferVolume(t.getOffer().getVolume());
private static final BiFunction<TradeInfo, Boolean, String> makerTakerMinerTxFeeFormat = (t, isTaker) -> {
if (isTaker) {
return formatSatoshis(t.getTxFeeAsLong());
} else {
return formatSatoshis(t.getOffer().getTxFee());
}
};
private static final BiFunction<TradeInfo, Boolean, String> makerTakerFeeFormat = (t, isTaker) -> {
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<TradeInfo, String> tradeCostFormat = (t) ->
t.getOffer().getBaseCurrencyCode().equals("BTC")
? formatOfferVolume(t.getOffer().getVolume())
: formatSatoshis(t.getTradeAmountAsLong());
private static final BiFunction<TradeInfo, Boolean, String> 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 shouldShowBsqBuyerAddress(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;
}
}
}
}

View file

@ -0,0 +1,59 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package bisq.cli;
import bisq.proto.grpc.TxInfo;
import com.google.common.annotations.VisibleForTesting;
import static bisq.cli.ColumnHeaderConstants.*;
import static bisq.cli.CurrencyFormat.formatSatoshis;
import static com.google.common.base.Strings.padEnd;
@VisibleForTesting
public class TransactionFormat {
public static String format(TxInfo txInfo) {
String headerLine = padEnd(COL_HEADER_TX_ID, txInfo.getTxId().length(), ' ') + COL_HEADER_DELIMITER
+ COL_HEADER_TX_IS_CONFIRMED + COL_HEADER_DELIMITER
+ COL_HEADER_TX_INPUT_SUM + COL_HEADER_DELIMITER
+ COL_HEADER_TX_OUTPUT_SUM + COL_HEADER_DELIMITER
+ COL_HEADER_TX_FEE + COL_HEADER_DELIMITER
+ COL_HEADER_TX_SIZE + COL_HEADER_DELIMITER
+ (txInfo.getMemo().isEmpty() ? "" : COL_HEADER_TX_MEMO + COL_HEADER_DELIMITER)
+ "\n";
String colDataFormat = "%-" + txInfo.getTxId().length() + "s"
+ " %" + COL_HEADER_TX_IS_CONFIRMED.length() + "s"
+ " %" + COL_HEADER_TX_INPUT_SUM.length() + "s"
+ " %" + COL_HEADER_TX_OUTPUT_SUM.length() + "s"
+ " %" + COL_HEADER_TX_FEE.length() + "s"
+ " %" + COL_HEADER_TX_SIZE.length() + "s"
+ " %s";
return headerLine
+ String.format(colDataFormat,
txInfo.getTxId(),
txInfo.getIsPending() ? "NO" : "YES", // pending=true means not confirmed
formatSatoshis(txInfo.getInputSum()),
formatSatoshis(txInfo.getOutputSum()),
formatSatoshis(txInfo.getFee()),
txInfo.getSize(),
txInfo.getMemo().isEmpty() ? "" : txInfo.getMemo());
}
}

View file

@ -0,0 +1,78 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package bisq.cli.opts;
import joptsimple.OptionException;
import joptsimple.OptionParser;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import java.util.List;
import java.util.function.Function;
import lombok.Getter;
import static bisq.cli.opts.OptLabel.OPT_HELP;
abstract class AbstractMethodOptionParser implements MethodOpts {
// The full command line args passed to CliMain.main(String[] args).
// CLI and Method level arguments are derived from args by an ArgumentList(args).
protected final String[] args;
protected final OptionParser parser = new OptionParser();
// The help option for a specific api method, e.g., takeoffer -help.
protected final OptionSpec<Void> helpOpt = parser.accepts(OPT_HELP, "Print method help").forHelp();
@Getter
protected OptionSet options;
@Getter
protected List<String> nonOptionArguments;
protected AbstractMethodOptionParser(String[] args) {
this.args = args;
}
public AbstractMethodOptionParser parse() {
try {
options = parser.parse(new ArgumentList(args).getMethodArguments());
//noinspection unchecked
nonOptionArguments = (List<String>) options.nonOptionArguments();
return this;
} catch (OptionException ex) {
throw new IllegalArgumentException(cliExceptionMessageStyle.apply(ex), ex);
}
}
public boolean isForHelp() {
return options.has(helpOpt);
}
private final Function<OptionException, String> cliExceptionMessageStyle = (ex) -> {
if (ex.getMessage() == null)
return null;
var optionToken = "option ";
var cliMessage = ex.getMessage().toLowerCase();
if (cliMessage.startsWith(optionToken) && cliMessage.length() > optionToken.length()) {
cliMessage = cliMessage.substring(cliMessage.indexOf(" ") + 1);
}
return cliMessage;
};
}

View file

@ -0,0 +1,124 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package bisq.cli.opts;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
/**
* Wrapper for an array of command line arguments.
*
* Used to extract CLI connection and authentication arguments, or method arguments
* before parsing CLI or method opts
*
*/
public class ArgumentList {
private final Predicate<String> isCliOpt = (o) ->
o.startsWith("--password") || o.startsWith("-password")
|| o.startsWith("--port") || o.startsWith("-port")
|| o.startsWith("--host") || o.startsWith("-host");
// The method name is the only positional opt in a command (easy to identify).
// If the positional argument does not match a Method, or there are more than one
// positional arguments, the joptsimple parser or CLI will fail as expected.
private final Predicate<String> isMethodNameOpt = (o) -> !o.startsWith("-");
private final Predicate<String> isHelpOpt = (o) -> o.startsWith("--help") || o.startsWith("-help");
private final String[] arguments;
private int currentIndex;
public ArgumentList(String... arguments) {
this.arguments = arguments.clone();
}
/**
* Returns only the CLI connection & authentication, and method name args
* (--password, --host, --port, --help, method name) contained in the original
* String[] args; excludes the method specific arguments.
*
* If String[] args contains both a method name (the only positional opt) and a help
* argument (--help, -help), it is assumed the user wants method help, not CLI help,
* and the help argument is not included in the returned String[].
*/
public String[] getCLIArguments() {
currentIndex = 0;
Optional<String> methodNameArgument = Optional.empty();
Optional<String> helpArgument = Optional.empty();
List<String> prunedArguments = new ArrayList<>();
while (hasMore()) {
String arg = peek();
if (isMethodNameOpt.test(arg)) {
methodNameArgument = Optional.of(arg);
prunedArguments.add(arg);
}
if (isCliOpt.test(arg))
prunedArguments.add(arg);
if (isHelpOpt.test(arg))
helpArgument = Optional.of(arg);
next();
}
// Include the saved CLI help argument if the positional method name argument
// was not found.
if (!methodNameArgument.isPresent() && helpArgument.isPresent())
prunedArguments.add(helpArgument.get());
return prunedArguments.toArray(new String[0]);
}
/**
* Returns only the method args contained in the original String[] args; excludes the
* CLI connection & authentication opts (--password, --host, --port), plus the
* positional method name arg.
*/
public String[] getMethodArguments() {
List<String> prunedArguments = new ArrayList<>();
currentIndex = 0;
while (hasMore()) {
String arg = peek();
if (!isCliOpt.test(arg) && !isMethodNameOpt.test(arg)) {
prunedArguments.add(arg);
}
next();
}
return prunedArguments.toArray(new String[0]);
}
boolean hasMore() {
return currentIndex < arguments.length;
}
@SuppressWarnings("UnusedReturnValue")
String next() {
return arguments[currentIndex++];
}
String peek() {
return arguments[currentIndex];
}
}

View file

@ -0,0 +1,50 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package bisq.cli.opts;
import joptsimple.OptionSpec;
import static bisq.cli.opts.OptLabel.OPT_OFFER_ID;
public class CancelOfferOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> offerIdOpt = parser.accepts(OPT_OFFER_ID, "id of offer to cancel")
.withRequiredArg();
public CancelOfferOptionParser(String[] args) {
super(args);
}
public CancelOfferOptionParser parse() {
super.parse();
// Short circuit opt validation if user just wants help.
if (options.has(helpOpt))
return this;
if (!options.has(offerIdOpt) || options.valueOf(offerIdOpt).isEmpty())
throw new IllegalArgumentException("no offer id specified");
return this;
}
public String getOfferId() {
return options.valueOf(offerIdOpt);
}
}

View file

@ -0,0 +1,85 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
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;
import static bisq.cli.opts.OptLabel.OPT_TRADE_INSTANT;
public class CreateCryptoCurrencyPaymentAcctOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> accountNameOpt = parser.accepts(OPT_ACCOUNT_NAME, "crypto currency account name")
.withRequiredArg();
final OptionSpec<String> currencyCodeOpt = parser.accepts(OPT_CURRENCY_CODE, "crypto currency code (bsq only)")
.withRequiredArg();
final OptionSpec<String> addressOpt = parser.accepts(OPT_ADDRESS, "bsq address")
.withRequiredArg();
final OptionSpec<Boolean> tradeInstantOpt = parser.accepts(OPT_TRADE_INSTANT, "create trade instant account")
.withOptionalArg()
.ofType(boolean.class)
.defaultsTo(Boolean.FALSE);
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);
}
public boolean getIsTradeInstant() {
return options.valueOf(tradeInstantOpt);
}
}

View file

@ -0,0 +1,144 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package bisq.cli.opts;
import joptsimple.OptionSpec;
import java.math.BigDecimal;
import static bisq.cli.opts.OptLabel.*;
import static joptsimple.internal.Strings.EMPTY;
public class CreateOfferOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> paymentAccountIdOpt = parser.accepts(OPT_PAYMENT_ACCOUNT,
"id of payment account used for offer")
.withRequiredArg()
.defaultsTo(EMPTY);
final OptionSpec<String> directionOpt = parser.accepts(OPT_DIRECTION, "offer direction (buy|sell)")
.withRequiredArg();
final OptionSpec<String> currencyCodeOpt = parser.accepts(OPT_CURRENCY_CODE, "currency code (eur|usd|...)")
.withRequiredArg();
final OptionSpec<String> amountOpt = parser.accepts(OPT_AMOUNT, "amount of btc to buy or sell")
.withRequiredArg();
final OptionSpec<String> minAmountOpt = parser.accepts(OPT_MIN_AMOUNT, "minimum amount of btc to buy or sell")
.withOptionalArg();
final OptionSpec<String> mktPriceMarginOpt = parser.accepts(OPT_MKT_PRICE_MARGIN, "market btc price margin (%)")
.withOptionalArg()
.defaultsTo("0.00");
final OptionSpec<String> fixedPriceOpt = parser.accepts(OPT_FIXED_PRICE, "fixed btc price")
.withOptionalArg()
.defaultsTo("0");
final OptionSpec<String> securityDepositOpt = parser.accepts(OPT_SECURITY_DEPOSIT, "maker security deposit (%)")
.withRequiredArg();
final OptionSpec<String> makerFeeCurrencyCodeOpt = parser.accepts(OPT_FEE_CURRENCY, "maker fee currency code (bsq|btc)")
.withOptionalArg()
.defaultsTo("btc");
public CreateOfferOptionParser(String[] args) {
super(args);
}
public CreateOfferOptionParser parse() {
super.parse();
// Short circuit opt validation if user just wants help.
if (options.has(helpOpt))
return this;
if (!options.has(paymentAccountIdOpt) || options.valueOf(paymentAccountIdOpt).isEmpty())
throw new IllegalArgumentException("no payment account id specified");
if (!options.has(directionOpt) || options.valueOf(directionOpt).isEmpty())
throw new IllegalArgumentException("no direction (buy|sell) specified");
if (!options.has(currencyCodeOpt) || options.valueOf(currencyCodeOpt).isEmpty())
throw new IllegalArgumentException("no currency code specified");
if (!options.has(amountOpt) || options.valueOf(amountOpt).isEmpty())
throw new IllegalArgumentException("no btc amount specified");
if (!options.has(mktPriceMarginOpt) && !options.has(fixedPriceOpt))
throw new IllegalArgumentException("no market price margin or fixed price specified");
if (options.has(mktPriceMarginOpt) && options.valueOf(mktPriceMarginOpt).isEmpty())
throw new IllegalArgumentException("no market price margin specified");
if (options.has(fixedPriceOpt) && options.valueOf(fixedPriceOpt).isEmpty())
throw new IllegalArgumentException("no fixed price specified");
if (!options.has(securityDepositOpt) || options.valueOf(securityDepositOpt).isEmpty())
throw new IllegalArgumentException("no security deposit specified");
return this;
}
public String getPaymentAccountId() {
return options.valueOf(paymentAccountIdOpt);
}
public String getDirection() {
return options.valueOf(directionOpt);
}
public String getCurrencyCode() {
return options.valueOf(currencyCodeOpt);
}
public String getAmount() {
return options.valueOf(amountOpt);
}
public String getMinAmount() {
return options.has(minAmountOpt) ? options.valueOf(minAmountOpt) : getAmount();
}
public boolean isUsingMktPriceMargin() {
return options.has(mktPriceMarginOpt);
}
@SuppressWarnings("unused")
public String getMktPriceMargin() {
return isUsingMktPriceMargin() ? options.valueOf(mktPriceMarginOpt) : "0.00";
}
public BigDecimal getMktPriceMarginAsBigDecimal() {
return isUsingMktPriceMargin() ? new BigDecimal(options.valueOf(mktPriceMarginOpt)) : BigDecimal.ZERO;
}
public String getFixedPrice() {
return options.has(fixedPriceOpt) ? options.valueOf(fixedPriceOpt) : "0.00";
}
public String getSecurityDeposit() {
return options.valueOf(securityDepositOpt);
}
public String getMakerFeeCurrencyCode() {
return options.has(makerFeeCurrencyCodeOpt) ? options.valueOf(makerFeeCurrencyCodeOpt) : "btc";
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
package bisq.cli.opts;
import joptsimple.OptionSpec;
import java.nio.file.Path;
import java.nio.file.Paths;
import static bisq.cli.opts.OptLabel.OPT_PAYMENT_ACCOUNT_FORM;
import static java.lang.String.format;
public class CreatePaymentAcctOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> paymentAcctFormPathOpt = parser.accepts(OPT_PAYMENT_ACCOUNT_FORM,
"path to json payment account form")
.withRequiredArg();
public CreatePaymentAcctOptionParser(String[] args) {
super(args);
}
public CreatePaymentAcctOptionParser parse() {
super.parse();
// Short circuit opt validation if user just wants help.
if (options.has(helpOpt))
return this;
if (!options.has(paymentAcctFormPathOpt) || options.valueOf(paymentAcctFormPathOpt).isEmpty())
throw new IllegalArgumentException("no path to json payment account form specified");
Path path = Paths.get(options.valueOf(paymentAcctFormPathOpt));
if (!path.toFile().exists())
throw new IllegalStateException(
format("json payment account form '%s' could not be found",
path));
return this;
}
public Path getPaymentAcctForm() {
return Paths.get(options.valueOf(paymentAcctFormPathOpt));
}
}

View file

@ -0,0 +1,50 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package bisq.cli.opts;
import joptsimple.OptionSpec;
import static bisq.cli.opts.OptLabel.OPT_ADDRESS;
public class GetAddressBalanceOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> addressOpt = parser.accepts(OPT_ADDRESS, "wallet btc address")
.withRequiredArg();
public GetAddressBalanceOptionParser(String[] args) {
super(args);
}
public GetAddressBalanceOptionParser 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 address specified");
return this;
}
public String getAddress() {
return options.valueOf(addressOpt);
}
}

View file

@ -0,0 +1,50 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package bisq.cli.opts;
import joptsimple.OptionSpec;
import static bisq.cli.opts.OptLabel.OPT_CURRENCY_CODE;
public class GetBTCMarketPriceOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> currencyCodeOpt = parser.accepts(OPT_CURRENCY_CODE, "currency-code")
.withRequiredArg();
public GetBTCMarketPriceOptionParser(String[] args) {
super(args);
}
public GetBTCMarketPriceOptionParser parse() {
super.parse();
// Short circuit opt validation if user just wants help.
if (options.has(helpOpt))
return this;
if (!options.has(currencyCodeOpt) || options.valueOf(currencyCodeOpt).isEmpty())
throw new IllegalArgumentException("no currency code specified");
return this;
}
public String getCurrencyCode() {
return options.valueOf(currencyCodeOpt);
}
}

View file

@ -0,0 +1,43 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package bisq.cli.opts;
import joptsimple.OptionSpec;
import static bisq.cli.opts.OptLabel.OPT_CURRENCY_CODE;
import static joptsimple.internal.Strings.EMPTY;
public class GetBalanceOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> currencyCodeOpt = parser.accepts(OPT_CURRENCY_CODE, "wallet currency code (bsq|btc)")
.withOptionalArg()
.defaultsTo(EMPTY);
public GetBalanceOptionParser(String[] args) {
super(args);
}
public GetBalanceOptionParser parse() {
return (GetBalanceOptionParser) super.parse();
}
public String getCurrencyCode() {
return options.has(currencyCodeOpt) ? options.valueOf(currencyCodeOpt) : "";
}
}

View file

@ -0,0 +1,50 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package bisq.cli.opts;
import joptsimple.OptionSpec;
import static bisq.cli.opts.OptLabel.OPT_OFFER_ID;
public class GetOfferOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> offerIdOpt = parser.accepts(OPT_OFFER_ID, "id of offer to get")
.withRequiredArg();
public GetOfferOptionParser(String[] args) {
super(args);
}
public GetOfferOptionParser parse() {
super.parse();
// Short circuit opt validation if user just wants help.
if (options.has(helpOpt))
return this;
if (!options.has(offerIdOpt) || options.valueOf(offerIdOpt).isEmpty())
throw new IllegalArgumentException("no offer id specified");
return this;
}
public String getOfferId() {
return options.valueOf(offerIdOpt);
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
package bisq.cli.opts;
import joptsimple.OptionSpec;
import static bisq.cli.opts.OptLabel.OPT_CURRENCY_CODE;
import static bisq.cli.opts.OptLabel.OPT_DIRECTION;
public class GetOffersOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> directionOpt = parser.accepts(OPT_DIRECTION, "offer direction (buy|sell)")
.withRequiredArg();
final OptionSpec<String> currencyCodeOpt = parser.accepts(OPT_CURRENCY_CODE, "currency code (eur|usd|...)")
.withRequiredArg();
public GetOffersOptionParser(String[] args) {
super(args);
}
public GetOffersOptionParser parse() {
super.parse();
// Short circuit opt validation if user just wants help.
if (options.has(helpOpt))
return this;
if (!options.has(directionOpt) || options.valueOf(directionOpt).isEmpty())
throw new IllegalArgumentException("no direction (buy|sell) specified");
if (!options.has(currencyCodeOpt) || options.valueOf(currencyCodeOpt).isEmpty())
throw new IllegalArgumentException("no currency code specified");
return this;
}
public String getDirection() {
return options.valueOf(directionOpt);
}
public String getCurrencyCode() {
return options.valueOf(currencyCodeOpt);
}
}

View file

@ -0,0 +1,51 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package bisq.cli.opts;
import joptsimple.OptionSpec;
import static bisq.cli.opts.OptLabel.OPT_PAYMENT_METHOD_ID;
public class GetPaymentAcctFormOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> paymentMethodIdOpt = parser.accepts(OPT_PAYMENT_METHOD_ID,
"id of payment method type used by a payment account")
.withRequiredArg();
public GetPaymentAcctFormOptionParser(String[] args) {
super(args);
}
public GetPaymentAcctFormOptionParser parse() {
super.parse();
// Short circuit opt validation if user just wants help.
if (options.has(helpOpt))
return this;
if (!options.has(paymentMethodIdOpt) || options.valueOf(paymentMethodIdOpt).isEmpty())
throw new IllegalArgumentException("no payment method id specified");
return this;
}
public String getPaymentMethodId() {
return options.valueOf(paymentMethodIdOpt);
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
package bisq.cli.opts;
import joptsimple.OptionSpec;
import static bisq.cli.opts.OptLabel.OPT_SHOW_CONTRACT;
import static bisq.cli.opts.OptLabel.OPT_TRADE_ID;
public class GetTradeOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> tradeIdOpt = parser.accepts(OPT_TRADE_ID, "id of trade")
.withRequiredArg();
final OptionSpec<Boolean> showContractOpt = parser.accepts(OPT_SHOW_CONTRACT, "show trade's json contract")
.withOptionalArg()
.ofType(boolean.class)
.defaultsTo(Boolean.FALSE);
public GetTradeOptionParser(String[] args) {
super(args);
}
public GetTradeOptionParser parse() {
super.parse();
// Short circuit opt validation if user just wants help.
if (options.has(helpOpt))
return this;
if (!options.has(tradeIdOpt) || options.valueOf(tradeIdOpt).isEmpty())
throw new IllegalArgumentException("no trade id specified");
return this;
}
public String getTradeId() {
return options.valueOf(tradeIdOpt);
}
public boolean getShowContract() {
return options.has(showContractOpt) ? options.valueOf(showContractOpt) : false;
}
}

View file

@ -0,0 +1,50 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package bisq.cli.opts;
import joptsimple.OptionSpec;
import static bisq.cli.opts.OptLabel.OPT_TRANSACTION_ID;
public class GetTransactionOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> txIdOpt = parser.accepts(OPT_TRANSACTION_ID, "id of transaction")
.withRequiredArg();
public GetTransactionOptionParser(String[] args) {
super(args);
}
public GetTransactionOptionParser parse() {
super.parse();
// Short circuit opt validation if user just wants help.
if (options.has(helpOpt))
return this;
if (!options.has(txIdOpt) || options.valueOf(txIdOpt).isEmpty())
throw new IllegalArgumentException("no tx id specified");
return this;
}
public String getTxId() {
return options.valueOf(txIdOpt);
}
}

View file

@ -0,0 +1,25 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package bisq.cli.opts;
public interface MethodOpts {
MethodOpts parse();
boolean isForHelp();
}

View file

@ -0,0 +1,53 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package bisq.cli.opts;
/**
* 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";
public final static String OPT_DIRECTION = "direction";
public final static String OPT_DISPUTE_AGENT_TYPE = "dispute-agent-type";
public final static String OPT_FEE_CURRENCY = "fee-currency";
public final static String OPT_FIXED_PRICE = "fixed-price";
public final static String OPT_HELP = "help";
public final static String OPT_HOST = "host";
public final static String OPT_MEMO = "memo";
public final static String OPT_MKT_PRICE_MARGIN = "market-price-margin";
public final static String OPT_MIN_AMOUNT = "min-amount";
public final static String OPT_OFFER_ID = "offer-id";
public final static String OPT_PASSWORD = "password";
public final static String OPT_PAYMENT_ACCOUNT = "payment-account";
public final static String OPT_PAYMENT_ACCOUNT_FORM = "payment-account-form";
public final static String OPT_PAYMENT_METHOD_ID = "payment-method-id";
public final static String OPT_PORT = "port";
public final static String OPT_REGISTRATION_KEY = "registration-key";
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";
public final static String OPT_WALLET_PASSWORD = "wallet-password";
public final static String OPT_NEW_WALLET_PASSWORD = "new-wallet-password";
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
package bisq.cli.opts;
import joptsimple.OptionSpec;
import static bisq.cli.opts.OptLabel.OPT_DISPUTE_AGENT_TYPE;
import static bisq.cli.opts.OptLabel.OPT_REGISTRATION_KEY;
public class RegisterDisputeAgentOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> disputeAgentTypeOpt = parser.accepts(OPT_DISPUTE_AGENT_TYPE, "dispute agent type")
.withRequiredArg();
final OptionSpec<String> registrationKeyOpt = parser.accepts(OPT_REGISTRATION_KEY, "registration key")
.withRequiredArg();
public RegisterDisputeAgentOptionParser(String[] args) {
super(args);
}
public RegisterDisputeAgentOptionParser parse() {
super.parse();
// Short circuit opt validation if user just wants help.
if (options.has(helpOpt))
return this;
if (!options.has(disputeAgentTypeOpt) || options.valueOf(disputeAgentTypeOpt).isEmpty())
throw new IllegalArgumentException("no dispute agent type specified");
if (!options.has(registrationKeyOpt) || options.valueOf(registrationKeyOpt).isEmpty())
throw new IllegalArgumentException("no registration key specified");
return this;
}
public String getDisputeAgentType() {
return options.valueOf(disputeAgentTypeOpt);
}
public String getRegistrationKey() {
return options.valueOf(registrationKeyOpt);
}
}

View file

@ -0,0 +1,50 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package bisq.cli.opts;
import joptsimple.OptionSpec;
import static bisq.cli.opts.OptLabel.OPT_WALLET_PASSWORD;
public class RemoveWalletPasswordOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> passwordOpt = parser.accepts(OPT_WALLET_PASSWORD, "bisq wallet password")
.withRequiredArg();
public RemoveWalletPasswordOptionParser(String[] args) {
super(args);
}
public RemoveWalletPasswordOptionParser parse() {
super.parse();
// Short circuit opt validation if user just wants help.
if (options.has(helpOpt))
return this;
if (!options.has(passwordOpt) || options.valueOf(passwordOpt).isEmpty())
throw new IllegalArgumentException("no password specified");
return this;
}
public String getPassword() {
return options.valueOf(passwordOpt);
}
}

View file

@ -0,0 +1,71 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package bisq.cli.opts;
import joptsimple.OptionSpec;
import static bisq.cli.opts.OptLabel.OPT_ADDRESS;
import static bisq.cli.opts.OptLabel.OPT_AMOUNT;
import static bisq.cli.opts.OptLabel.OPT_TX_FEE_RATE;
import static joptsimple.internal.Strings.EMPTY;
public class SendBsqOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> addressOpt = parser.accepts(OPT_ADDRESS, "destination bsq address")
.withRequiredArg();
final OptionSpec<String> amountOpt = parser.accepts(OPT_AMOUNT, "amount of bsq to send")
.withRequiredArg();
final OptionSpec<String> feeRateOpt = parser.accepts(OPT_TX_FEE_RATE, "optional tx fee rate (sats/byte)")
.withOptionalArg()
.defaultsTo(EMPTY);
public SendBsqOptionParser(String[] args) {
super(args);
}
public SendBsqOptionParser 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);
}
public String getFeeRate() {
return options.has(feeRateOpt) ? options.valueOf(feeRateOpt) : "";
}
}

View file

@ -0,0 +1,80 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package bisq.cli.opts;
import joptsimple.OptionSpec;
import static bisq.cli.opts.OptLabel.OPT_ADDRESS;
import static bisq.cli.opts.OptLabel.OPT_AMOUNT;
import static bisq.cli.opts.OptLabel.OPT_MEMO;
import static bisq.cli.opts.OptLabel.OPT_TX_FEE_RATE;
import static joptsimple.internal.Strings.EMPTY;
public class SendBtcOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> addressOpt = parser.accepts(OPT_ADDRESS, "destination btc address")
.withRequiredArg();
final OptionSpec<String> amountOpt = parser.accepts(OPT_AMOUNT, "amount of btc to send")
.withRequiredArg();
final OptionSpec<String> feeRateOpt = parser.accepts(OPT_TX_FEE_RATE, "optional tx fee rate (sats/byte)")
.withOptionalArg()
.defaultsTo(EMPTY);
final OptionSpec<String> memoOpt = parser.accepts(OPT_MEMO, "optional tx memo")
.withOptionalArg()
.defaultsTo(EMPTY);
public SendBtcOptionParser(String[] args) {
super(args);
}
public SendBtcOptionParser 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 btc address specified");
if (!options.has(amountOpt) || options.valueOf(amountOpt).isEmpty())
throw new IllegalArgumentException("no btc amount specified");
return this;
}
public String getAddress() {
return options.valueOf(addressOpt);
}
public String getAmount() {
return options.valueOf(amountOpt);
}
public String getFeeRate() {
return options.has(feeRateOpt) ? options.valueOf(feeRateOpt) : "";
}
public String getMemo() {
return options.has(memoOpt) ? options.valueOf(memoOpt) : "";
}
}

View file

@ -0,0 +1,51 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package bisq.cli.opts;
import joptsimple.OptionSpec;
import static bisq.cli.opts.OptLabel.OPT_TX_FEE_RATE;
public class SetTxFeeRateOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> feeRateOpt = parser.accepts(OPT_TX_FEE_RATE,
"tx fee rate preference (sats/byte)")
.withRequiredArg();
public SetTxFeeRateOptionParser(String[] args) {
super(args);
}
public SetTxFeeRateOptionParser parse() {
super.parse();
// Short circuit opt validation if user just wants help.
if (options.has(helpOpt))
return this;
if (!options.has(feeRateOpt) || options.valueOf(feeRateOpt).isEmpty())
throw new IllegalArgumentException("no tx fee rate specified");
return this;
}
public String getFeeRate() {
return options.valueOf(feeRateOpt);
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
package bisq.cli.opts;
import joptsimple.OptionSpec;
import static bisq.cli.opts.OptLabel.OPT_NEW_WALLET_PASSWORD;
import static bisq.cli.opts.OptLabel.OPT_WALLET_PASSWORD;
import static joptsimple.internal.Strings.EMPTY;
public class SetWalletPasswordOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> passwordOpt = parser.accepts(OPT_WALLET_PASSWORD, "bisq wallet password")
.withRequiredArg();
final OptionSpec<String> newPasswordOpt = parser.accepts(OPT_NEW_WALLET_PASSWORD, "new bisq wallet password")
.withOptionalArg()
.defaultsTo(EMPTY);
public SetWalletPasswordOptionParser(String[] args) {
super(args);
}
public SetWalletPasswordOptionParser parse() {
super.parse();
// Short circuit opt validation if user just wants help.
if (options.has(helpOpt))
return this;
if (!options.has(passwordOpt) || options.valueOf(passwordOpt).isEmpty())
throw new IllegalArgumentException("no password specified");
return this;
}
public String getPassword() {
return options.valueOf(passwordOpt);
}
public String getNewPassword() {
return options.has(newPasswordOpt) ? options.valueOf(newPasswordOpt) : "";
}
}

View file

@ -0,0 +1,30 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package bisq.cli.opts;
public class SimpleMethodOptionParser extends AbstractMethodOptionParser implements MethodOpts {
public SimpleMethodOptionParser(String[] args) {
super(args);
}
public SimpleMethodOptionParser parse() {
return (SimpleMethodOptionParser) super.parse();
}
}

View file

@ -0,0 +1,70 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package bisq.cli.opts;
import joptsimple.OptionSpec;
import static bisq.cli.opts.OptLabel.OPT_FEE_CURRENCY;
import static bisq.cli.opts.OptLabel.OPT_OFFER_ID;
import static bisq.cli.opts.OptLabel.OPT_PAYMENT_ACCOUNT;
public class TakeOfferOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> offerIdOpt = parser.accepts(OPT_OFFER_ID, "id of offer to take")
.withRequiredArg();
final OptionSpec<String> paymentAccountIdOpt = parser.accepts(OPT_PAYMENT_ACCOUNT, "id of payment account used for trade")
.withRequiredArg();
final OptionSpec<String> takerFeeCurrencyCodeOpt = parser.accepts(OPT_FEE_CURRENCY, "taker fee currency code (bsq|btc)")
.withOptionalArg()
.defaultsTo("btc");
public TakeOfferOptionParser(String[] args) {
super(args);
}
public TakeOfferOptionParser parse() {
super.parse();
// Short circuit opt validation if user just wants help.
if (options.has(helpOpt))
return this;
if (!options.has(offerIdOpt) || options.valueOf(offerIdOpt).isEmpty())
throw new IllegalArgumentException("no offer id specified");
if (!options.has(paymentAccountIdOpt) || options.valueOf(paymentAccountIdOpt).isEmpty())
throw new IllegalArgumentException("no payment account id specified");
return this;
}
public String getOfferId() {
return options.valueOf(offerIdOpt);
}
public String getPaymentAccountId() {
return options.valueOf(paymentAccountIdOpt);
}
public String getTakerFeeCurrencyCode() {
return options.has(takerFeeCurrencyCodeOpt) ? options.valueOf(takerFeeCurrencyCodeOpt) : "btc";
}
}

View file

@ -0,0 +1,63 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package bisq.cli.opts;
import joptsimple.OptionSpec;
import static bisq.cli.opts.OptLabel.OPT_TIMEOUT;
import static bisq.cli.opts.OptLabel.OPT_WALLET_PASSWORD;
public class UnlockWalletOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> passwordOpt = parser.accepts(OPT_WALLET_PASSWORD, "bisq wallet password")
.withRequiredArg();
final OptionSpec<Long> unlockTimeoutOpt = parser.accepts(OPT_TIMEOUT, "wallet unlock timeout (s)")
.withRequiredArg()
.ofType(long.class)
.defaultsTo(0L);
public UnlockWalletOptionParser(String[] args) {
super(args);
}
public UnlockWalletOptionParser parse() {
super.parse();
// Short circuit opt validation if user just wants help.
if (options.has(helpOpt))
return this;
if (!options.has(passwordOpt) || options.valueOf(passwordOpt).isEmpty())
throw new IllegalArgumentException("no password specified");
if (!options.has(unlockTimeoutOpt) || options.valueOf(unlockTimeoutOpt) <= 0)
throw new IllegalArgumentException("no unlock timeout specified");
return this;
}
public String getPassword() {
return options.valueOf(passwordOpt);
}
public long getUnlockTimeout() {
return options.valueOf(unlockTimeoutOpt);
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<String> addressOpt = parser.accepts(OPT_ADDRESS, "receiving bsq address")
.withRequiredArg();
final OptionSpec<String> 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);
}
}

View file

@ -0,0 +1,71 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package bisq.cli.opts;
import joptsimple.OptionSpec;
import static bisq.cli.opts.OptLabel.OPT_ADDRESS;
import static bisq.cli.opts.OptLabel.OPT_MEMO;
import static bisq.cli.opts.OptLabel.OPT_TRADE_ID;
import static joptsimple.internal.Strings.EMPTY;
public class WithdrawFundsOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> tradeIdOpt = parser.accepts(OPT_TRADE_ID, "id of trade")
.withRequiredArg();
final OptionSpec<String> addressOpt = parser.accepts(OPT_ADDRESS, "destination btc address")
.withRequiredArg();
final OptionSpec<String> memoOpt = parser.accepts(OPT_MEMO, "optional tx memo")
.withOptionalArg()
.defaultsTo(EMPTY);
public WithdrawFundsOptionParser(String[] args) {
super(args);
}
public WithdrawFundsOptionParser parse() {
super.parse();
// Short circuit opt validation if user just wants help.
if (options.has(helpOpt))
return this;
if (!options.has(tradeIdOpt) || options.valueOf(tradeIdOpt).isEmpty())
throw new IllegalArgumentException("no trade id specified");
if (!options.has(addressOpt) || options.valueOf(addressOpt).isEmpty())
throw new IllegalArgumentException("no destination address specified");
return this;
}
public String getTradeId() {
return options.valueOf(tradeIdOpt);
}
public String getAddress() {
return options.valueOf(addressOpt);
}
public String getMemo() {
return options.has(memoOpt) ? options.valueOf(memoOpt) : "";
}
}

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="CONSOLE_APPENDER" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%highlight(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{30}: %msg %xEx%n)</pattern>
</encoder>
</appender>
<root level="TRACE">
<appender-ref ref="CONSOLE_APPENDER"/>
</root>
<logger name="io.grpc.netty" level="WARN"/>
</configuration>

View file

@ -0,0 +1,39 @@
package bisq.cli;
import static java.lang.System.out;
/**
Smoke tests for getoffers method. Useful for examining the format of the console output.
Prerequisites:
- Run `./bisq-daemon --apiPassword=xyz --appDataDir=$TESTDIR`
This can be run on mainnet.
*/
public class GetOffersSmokeTest {
public static void main(String[] args) {
out.println(">>> getoffers buy usd");
CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=buy", "--currency-code=usd"});
out.println(">>> getoffers sell usd");
CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=sell", "--currency-code=usd"});
out.println(">>> getoffers buy eur");
CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=buy", "--currency-code=eur"});
out.println(">>> getoffers sell eur");
CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=sell", "--currency-code=eur"});
out.println(">>> getoffers buy gbp");
CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=buy", "--currency-code=gbp"});
out.println(">>> getoffers sell gbp");
CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=sell", "--currency-code=gbp"});
out.println(">>> getoffers buy brl");
CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=buy", "--currency-code=brl"});
out.println(">>> getoffers sell brl");
CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=sell", "--currency-code=brl"});
}
}

View file

@ -0,0 +1,267 @@
package bisq.cli.opt;
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.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import bisq.cli.opts.CancelOfferOptionParser;
import bisq.cli.opts.CreateCryptoCurrencyPaymentAcctOptionParser;
import bisq.cli.opts.CreateOfferOptionParser;
import bisq.cli.opts.CreatePaymentAcctOptionParser;
public class OptionParsersTest {
private static final String PASSWORD_OPT = "--" + OPT_PASSWORD + "=" + "xyz";
// canceloffer opt parser tests
@Test
public void testCancelOfferWithMissingOfferIdOptShouldThrowException() {
String[] args = new String[]{
PASSWORD_OPT,
canceloffer.name()
};
Throwable exception = assertThrows(RuntimeException.class, () ->
new CancelOfferOptionParser(args).parse());
assertEquals("no offer id specified", exception.getMessage());
}
@Test
public void testCancelOfferWithEmptyOfferIdOptShouldThrowException() {
String[] args = new String[]{
PASSWORD_OPT,
canceloffer.name(),
"--" + OPT_OFFER_ID + "=" // missing opt value
};
Throwable exception = assertThrows(RuntimeException.class, () ->
new CancelOfferOptionParser(args).parse());
assertEquals("no offer id specified", exception.getMessage());
}
@Test
public void testCancelOfferWithMissingOfferIdValueShouldThrowException() {
String[] args = new String[]{
PASSWORD_OPT,
canceloffer.name(),
"--" + OPT_OFFER_ID // missing equals sign & opt value
};
Throwable exception = assertThrows(RuntimeException.class, () ->
new CancelOfferOptionParser(args).parse());
assertEquals("offer-id requires an argument", exception.getMessage());
}
@Test
public void testValidCancelOfferOpts() {
String[] args = new String[]{
PASSWORD_OPT,
canceloffer.name(),
"--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID"
};
new CancelOfferOptionParser(args).parse();
}
// createoffer opt parser tests
@Test
public void testCreateOfferOptParserWithMissingPaymentAccountIdOptShouldThrowException() {
String[] args = new String[]{
PASSWORD_OPT,
createoffer.name()
};
Throwable exception = assertThrows(RuntimeException.class, () ->
new CreateOfferOptionParser(args).parse());
assertEquals("no payment account id specified", exception.getMessage());
}
@Test
public void testCreateOfferOptParserWithEmptyPaymentAccountIdOptShouldThrowException() {
String[] args = new String[]{
PASSWORD_OPT,
createoffer.name(),
"--" + OPT_PAYMENT_ACCOUNT
};
Throwable exception = assertThrows(RuntimeException.class, () ->
new CreateOfferOptionParser(args).parse());
assertEquals("payment-account requires an argument", exception.getMessage());
}
@Test
public void testCreateOfferOptParserWithMissingDirectionOptShouldThrowException() {
String[] args = new String[]{
PASSWORD_OPT,
createoffer.name(),
"--" + OPT_PAYMENT_ACCOUNT + "=" + "abc-payment-acct-id-123"
};
Throwable exception = assertThrows(RuntimeException.class, () ->
new CreateOfferOptionParser(args).parse());
assertEquals("no direction (buy|sell) specified", exception.getMessage());
}
@Test
public void testCreateOfferOptParserWithMissingDirectionOptValueShouldThrowException() {
String[] args = new String[]{
PASSWORD_OPT,
createoffer.name(),
"--" + OPT_PAYMENT_ACCOUNT + "=" + "abc-payment-acct-id-123",
"--" + OPT_DIRECTION + "=" + ""
};
Throwable exception = assertThrows(RuntimeException.class, () ->
new CreateOfferOptionParser(args).parse());
assertEquals("no direction (buy|sell) specified", exception.getMessage());
}
@Test
public void testValidCreateOfferOpts() {
String[] args = new String[]{
PASSWORD_OPT,
createoffer.name(),
"--" + OPT_PAYMENT_ACCOUNT + "=" + "abc-payment-acct-id-123",
"--" + OPT_DIRECTION + "=" + "BUY",
"--" + OPT_CURRENCY_CODE + "=" + "EUR",
"--" + OPT_AMOUNT + "=" + "0.125",
"--" + OPT_MKT_PRICE_MARGIN + "=" + "0.0",
"--" + OPT_SECURITY_DEPOSIT + "=" + "25.0"
};
CreateOfferOptionParser parser = new CreateOfferOptionParser(args).parse();
assertEquals("abc-payment-acct-id-123", parser.getPaymentAccountId());
assertEquals("BUY", parser.getDirection());
assertEquals("EUR", parser.getCurrencyCode());
assertEquals("0.125", parser.getAmount());
assertEquals("0.0", parser.getMktPriceMargin());
assertEquals("25.0", parser.getSecurityDeposit());
}
// createpaymentacct opt parser tests
@Test
public void testCreatePaymentAcctOptParserWithMissingPaymentFormOptShouldThrowException() {
String[] args = new String[]{
PASSWORD_OPT,
createpaymentacct.name()
// OPT_PAYMENT_ACCOUNT_FORM
};
Throwable exception = assertThrows(RuntimeException.class, () ->
new CreatePaymentAcctOptionParser(args).parse());
assertEquals("no path to json payment account form specified", exception.getMessage());
}
@Test
public void testCreatePaymentAcctOptParserWithMissingPaymentFormOptValueShouldThrowException() {
String[] args = new String[]{
PASSWORD_OPT,
createpaymentacct.name(),
"--" + OPT_PAYMENT_ACCOUNT_FORM + "="
};
Throwable exception = assertThrows(RuntimeException.class, () ->
new CreatePaymentAcctOptionParser(args).parse());
assertEquals("no path to json payment account form specified", exception.getMessage());
}
@Test
public void testCreatePaymentAcctOptParserWithInvalidPaymentFormOptValueShouldThrowException() {
String[] args = new String[]{
PASSWORD_OPT,
createpaymentacct.name(),
"--" + OPT_PAYMENT_ACCOUNT_FORM + "=" + "/tmp/milkyway/solarsystem/mars"
};
Throwable exception = assertThrows(RuntimeException.class, () ->
new CreatePaymentAcctOptionParser(args).parse());
if (System.getProperty("os.name").toLowerCase().indexOf("win") >= 0)
assertEquals("json payment account form '\\tmp\\milkyway\\solarsystem\\mars' could not be found",
exception.getMessage());
else
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());
}
}