hasWalletPasswordOpt = (args) ->
Arrays.stream(args).anyMatch(a -> a.contains("--wallet-password"));
/**
* Return a wallet password read from stdin. If read from a command terminal, input will not be echoed.
* If run in a virtual terminal (IDE console), the input will be echoed.
*
* @param prompt password prompt
* @return String the password
*/
public static String readWalletPassword(String prompt) {
String walletPassword;
var console = console();
// System.console() returns null if you do not launch your java application with a real console.
if (console == null) {
// Have to read it in a less secure way in the IDE's virtual console.
out.println(prompt);
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
try {
walletPassword = reader.readLine();
} catch (IOException ex) {
throw new RuntimeException(ex);
}
} else {
char[] pwdChars = console.readPassword(prompt);
walletPassword = new String(pwdChars);
}
return walletPassword;
}
/**
* Return the given String[] args with an additional --wallet-password="be careful" option appended to it.
*
* @param args program arguments
* @param walletPassword wallet password
* @return String[] appended program arguments
*/
public static String[] appendWalletPasswordOpt(String[] args, String walletPassword) {
String[] walletPasswordOpt = new String[]{"--wallet-password=" + walletPassword};
String[] newOpts = new String[args.length + 1];
arraycopy(args, 0, newOpts, 0, args.length);
arraycopy(walletPasswordOpt, 0, newOpts, args.length, walletPasswordOpt.length);
return newOpts;
}
/**
* Returns a validated address:port specification as a String.
*
* @param addressString The address:port pair being validated.
* @return String
*/
public static String getValidatedPeerAddress(String addressString) {
String[] hostnameAndPort = addressString.split(":");
String hostname;
int port;
try {
if (hostnameAndPort.length < 2) {
throw new IllegalStateException(format("Invalid preferredTradingPeers configuration:%n"
+ "\t\t%s%n\tEach address much include a port, i.e, host:port.",
addressString));
}
hostname = hostnameAndPort[0].trim();
port = Integer.parseInt(hostnameAndPort[1].trim());
} catch (Exception ex) {
throw new IllegalStateException(format("Invalid preferredTradingPeers configuration:%n"
+ "\t\t%s%n\tMultiple addresses must be separated by commas.",
addressString),
ex);
}
return hostname + ":" + port;
}
/**
* Return given Map transformed into a String representing a table with two columns: label and value.
*
* The map argument should contain only scalar values or short strings as values
* (not lists or maps), or you will get ugly results.
*/
public static final BiFunction, String> toTable = (title, map) -> {
var mapElements = map.entrySet();
Supplier longestLabel = () -> {
int[] len = {0}; // Make implicitly final to modify in map element iteration.
mapElements.forEach((e) -> {
var labelLen = e.getKey().length();
len[0] = Math.max(labelLen, len[0]);
});
return len[0];
};
int labelWidth = longestLabel.get() + 2;
Supplier resultsTable = () -> {
var numRows = mapElements.size();
var rows = new StringBuilder();
int[] rowNum = {0}; // Make implicitly final to modify in map element iteration.
mapElements.forEach((e) -> {
var label = e.getKey();
String value;
if (e.getValue() instanceof Boolean) {
value = ((Boolean) e.getValue()) ? "YES" : "NO";
} else {
value = e.getValue().toString();
}
var rowFormatSpec = (label.startsWith("\t"))
? "%-" + labelWidth + "s" + " " + "%s"
: "%-" + (labelWidth + 3) + "s" + " " + "%s";
var row = format(rowFormatSpec, label, value);
rows.append("\t").append(row);
if (++rowNum[0] < numRows) {
rows.append("\n");
}
});
return rows.toString();
};
return title + "\n" +
resultsTable.get();
};
/**
* Print offer summary to stdout.
*
* @param offer printed offer
*/
public static void printOfferSummary(OfferInfo offer) {
requireNonNull(offer, "OfferInfo offer param cannot be null.");
new TableBuilder(OFFER_TBL, offer).build().print(out);
}
/**
* Print list of offer summaries to stdout
*
* @param offers printed offer list
*/
public static void printOffersSummary(List offers) {
requireNonNull(offers, "List offers param cannot be null.");
if (offers.isEmpty()) {
log.info("No offers to print.");
} else {
new TableBuilder(OFFER_TBL, offers).build().print(out);
}
}
/**
* Print trade summary to stdout.
*
* @param trade printed trade
*/
public static void printTradeSummary(TradeInfo trade) {
requireNonNull(trade, "TradeInfo trade param cannot be null.");
new TableBuilder(TRADE_DETAIL_TBL, trade).build().print(out);
}
/**
* Print list of trade summaries to stdout.
*
* @param category category OPEN | CLOSED | FAILED
* @param trades list of trades
*/
public static void printTradesSummary(GetTradesRequest.Category category, List trades) {
requireNonNull(trades, "List trades param cannot be null.");
if (trades.isEmpty()) {
log.info("No trades to print.");
} else {
switch (category) {
case CLOSED -> new TableBuilder(CLOSED_TRADES_TBL, trades).build().print(out);
case FAILED -> new TableBuilder(FAILED_TRADES_TBL, trades).build().print(out);
default -> new TableBuilder(OPEN_TRADES_TBL, trades).build().print(out);
}
}
}
/**
* Prints PaymentAccount summary to stdout.
*
* @param paymentAccount the printed PaymentAccount
*/
public static void printPaymentAccountSummary(PaymentAccount paymentAccount) {
requireNonNull(paymentAccount, "PaymentAccount paymentAccount param cannot be null.");
new TableBuilder(PAYMENT_ACCOUNT_TBL, paymentAccount).build().print(out);
}
/**
* Log a CLI confirmpaymentstarted command for a simulated trading peer.
*
* @param log calling bot's logger
* @param tradingPeerApiPassword trading peer's CLI --password param value
* @param tradingPeerApiPort trading peer's CLI --port param value
* @param tradeId trade's unique identifier (cannot be short-id)
*/
public static void printCliPaymentStartedCommand(Logger log,
String tradingPeerApiPassword,
int tradingPeerApiPort,
String currencyCode,
String tradeId) {
log.warn(BANNER);
log.warn("BTC buyer must manually confirm {} payment has been sent"
+ " with a confirmpaymentstarted CLI command:",
currencyCode);
log.warn("./bisq-cli --password={} --port={} confirmpaymentstarted --trade-id={}",
tradingPeerApiPassword,
tradingPeerApiPort,
tradeId);
log.warn(BANNER);
}
/**
* Log a CLI confirmpaymentreceived command for a simulated trading peer.
*
* @param log calling bot's logger
* @param tradingPeerApiPassword trading peer's CLI --password param value
* @param tradingPeerApiPort trading peer's CLI --port param value
* @param tradeId trade's unique identifier (cannot be short-id)
*/
public static void printCliPaymentReceivedConfirmationCommand(Logger log,
String tradingPeerApiPassword,
int tradingPeerApiPort,
String currencyCode,
String tradeId) {
log.warn(BANNER);
log.warn("BTC seller must manually confirm {} payment was received"
+ " with a confirmpaymentreceived CLI command:",
currencyCode);
log.warn("./bisq-cli --password={} --port={} confirmpaymentreceived --trade-id={}",
tradingPeerApiPassword,
tradingPeerApiPort,
tradeId);
log.warn(BANNER);
}
/**
* Log a CLI closetrade command for a simulated trading peer.
*
* @param log calling bot's logger
* @param tradingPeerApiPassword trading peer's CLI --password param value
* @param tradingPeerApiPort trading peer's CLI --port param value
* @param tradeId trade's unique identifier (cannot be short-id)
*/
public static void printCliCloseTradeCommand(Logger log,
String tradingPeerApiPassword,
int tradingPeerApiPort,
String tradeId) {
log.warn(BANNER);
log.warn("Trading peer must manually close trade with a closetrade CLI command:");
log.warn("./bisq-cli --password={} --port={} closetrade --trade-id={}",
tradingPeerApiPassword,
tradingPeerApiPort,
tradeId);
log.warn(BANNER);
}
/**
* Log a CLI gettrades --category=closed command for a simulated trading peer.
*
* @param log calling bot's logger
* @param tradingPeerApiPassword trading peer's CLI --password param value
* @param tradingPeerApiPort trading peer's CLI --port param value
*/
public static void printCliGetClosedTradesCommand(Logger log,
String tradingPeerApiPassword,
int tradingPeerApiPort) {
log.warn(BANNER);
log.warn("Trading peer can view completed trade history with a gettrades CLI command:");
log.warn("./bisq-cli --password={} --port={} gettrades --category=closed",
tradingPeerApiPassword,
tradingPeerApiPort);
log.warn(BANNER);
}
/**
* Log a CLI gettrade command for a simulated trading peer.
*
* @param log calling bot's logger
* @param tradingPeerApiPassword trading peer's CLI --password param value
* @param tradingPeerApiPort trading peer's CLI --port param value
* @param tradeId trade's unique identifier (cannot be short-id)
*/
public static void printCliGetTradeCommand(Logger log,
String tradingPeerApiPassword,
int tradingPeerApiPort,
String tradeId) {
log.warn(BANNER);
log.warn("Trading peer can view a trade with a gettrade CLI command:");
log.warn("./bisq-cli --password={} --port={} gettrade --trade-id={}",
tradingPeerApiPassword,
tradingPeerApiPort,
tradeId);
log.warn(BANNER);
}
/**
* Log 1 or more CLI commands for a simulated trading peer.
* Commands need to be separated by newlines to be legible.
*
* @param log calling bot's logger
* @param description description of CLI commands
* @param commands CLI commands separated by newlines.
*/
public static void printCliCommands(Logger log,
String description,
String commands) {
log.warn(BANNER);
log.warn(description);
log.warn(commands);
log.warn(BANNER);
}
/**
* Run a bash script to count down the given number of seconds, printing each character of output from stdout.
*
* Can only be run if the system's bash command language interpreter can be found.
*
* @param seconds to count down
*/
public static void showCountdown(int seconds) {
getBashPath().ifPresentOrElse((bashPath) -> {
var bashScript = format(
"for i in {%d..1}; do echo -ne \"Waking up in $i seconds...\\r\" && sleep 1; done; echo -ne \"\\r\"", seconds);
try {
BotUtils.runBashCommand(bashScript);
} catch (IOException ex) {
throw new RuntimeException("Error running bash script.", ex);
} catch (InterruptedException ignored) {
// ignored
}
}, () -> {
throw new UnsupportedOperationException("Bash command language interpreter not found.");
});
}
/**
* Execute a bash system command, print process' stdout during the command's execution,
* and return its status code (0 or 1).
*
* @param bashCommand the system bash command
* @return int system command status code
* @throws IOException if an I/O error occurs
* @throws InterruptedException if the current thread is interrupted by another thread while it is waiting,
* then the wait is ended and an InterruptedException is thrown.
* @throws UnsupportedOperationException if the command language interpreter could not be found on the system, or
* if the operating system does not support the creation of processes.
*/
@SuppressWarnings("UnusedReturnValue")
public static int runBashCommand(String bashCommand) throws IOException, InterruptedException {
var bashPath = getBashPath();
if (bashPath.isPresent()) {
List cmdOptions = new ArrayList<>() {{
//noinspection OptionalGetWithoutIsPresent
add(bashPath.get());
add("-c");
add(bashCommand);
}};
Process process = new ProcessBuilder(cmdOptions).start();
try (InputStreamReader isr = new InputStreamReader(process.getInputStream())) {
int c;
while ((c = isr.read()) >= 0) {
out.print((char) c);
out.flush();
}
}
return process.waitFor();
} else {
throw new UnsupportedOperationException("Bash util not found on this " + getOSName() + " system.");
}
}
/**
* Return an Optional for the absolute path of the system's bash utility,
* if it exists at one of two locations: "/bin/bash", or "/usr/bin/bash".
*
* @return Optional
*/
public static Optional getBashPath() {
if (isUnix()) {
var f1 = new File("/bin/bash");
var f2 = new File("/usr/bin/bash");
if (f1.exists() && f1.canExecute()) {
return Optional.of(f1.getAbsolutePath());
} else if (f2.exists() && f2.canExecute()) {
return Optional.of(f2.getAbsolutePath());
} else {
return Optional.empty();
}
} else {
return Optional.empty();
}
}
/**
* Return true if OS is any flavor of Linux.
*
* @return true if OS is any flavor of Linux
*/
public static boolean isUnix() {
return isOSX() || isLinux() || getOSName().contains("freebsd");
}
/**
* Return true if OS is Windows.
*
* @return true if OS is Windows
*/
public static boolean isWindows() {
return getOSName().contains("win");
}
/**
* Return true if running on a virtualized OS within Qubes.
*
* @return true if running on a virtualized OS within Qubes
*/
public static boolean isQubesOS() {
// For Linux qubes, "os.version" looks like "4.19.132-1.pvops.qubes.x86_64"
// The presence of the "qubes" substring indicates this Linux is running as a qube
// This is the case for all 3 virtualization modes (PV, PVH, HVM)
// In addition, this works for both simple AppVMs, as well as for StandaloneVMs
// TODO This might not work for detecting Qubes virtualization for other OSes
// like Windows
return getOSVersion().contains("qubes");
}
/**
* Return true if OS is Mac.
*
* @return true if OS is Mac
*/
public static boolean isOSX() {
return getOSName().contains("mac") || getOSName().contains("darwin");
}
/**
* Return true if OS is Linux.
*
* @return true if OS is Linux
*/
public static boolean isLinux() {
return getOSName().contains("linux");
}
/**
* Return true if OS is Debian Linux.
*
* @return true if OS is Debian Linux
*/
public static boolean isDebianLinux() {
return isLinux() && new File("/etc/debian_version").isFile();
}
/**
* Return true if OS is Redhat Linux.
*
* @return true if OS is Redhat Linux
*/
public static boolean isRedHatLinux() {
return isLinux() && new File("/etc/redhat-release").isFile();
}
/**
* Returns the OS name in lower case.
*
* @return OS name
*/
public static String getOSName() {
return System.getProperty("os.name").toLowerCase(Locale.US);
}
/**
* Returns the OS version in lower case.
*
* @return OS version
*/
public static String getOSVersion() {
return System.getProperty("os.version").toLowerCase(Locale.US);
}
}