cmdOptions) {
if (cmdOptions.isEmpty())
throw new IllegalStateException("No command params specified.");
if (cmdOptions.contains("sudo"))
throw new IllegalStateException("'sudo' commands are prohibited.");
log.trace("System cmd options {}", cmdOptions);
this.cmdOptions = cmdOptions;
}
// Execute a system command and return its status code (0 or 1).
// The system command's output (stderr or stdout) can be accessed from accessors.
public int exec() throws IOException, InterruptedException {
return exec(true);
}
// Execute a system command and return its status code (0 or 1).
// The system command's output (stderr or stdout) can be accessed from accessors
// if the waitOnErrStream flag is true, else the method will not wait on (join)
// the error stream handler thread.
public int exec(boolean waitOnErrStream) throws IOException, InterruptedException {
Process process = new ProcessBuilder(cmdOptions).start();
// I'm currently doing these on a separate line here in case i need to set them to null
// to get the threads to stop.
// see http://java.sun.com/j2se/1.5.0/docs/guide/misc/threadPrimitiveDeprecation.html
InputStream inputStream = process.getInputStream();
InputStream errorStream = process.getErrorStream();
// These need to run as java threads to get the standard output and error from the command.
// the inputstream handler gets a reference to our stdOutput in case we need to write
// something to it.
inputStreamHandler = new ThreadedStreamHandler(inputStream);
errorStreamHandler = new ThreadedStreamHandler(errorStream);
inputStreamHandler.start();
errorStreamHandler.start();
int exitStatus = process.waitFor();
inputStreamHandler.interrupt();
errorStreamHandler.interrupt();
inputStreamHandler.join();
if (waitOnErrStream)
errorStreamHandler.join();
return exitStatus;
}
// Get the standard error from an executed system command.
public StringBuilder getStandardErrorFromCommand() {
return errorStreamHandler.getOutputBuffer();
}
// Get the standard output from an executed system command.
public StringBuilder getStandardOutputFromCommand() {
return inputStreamHandler.getOutputBuffer();
}
}
================================================
FILE: apitest/src/main/java/haveno/apitest/linux/ThreadedStreamHandler.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.apitest.linux;
import lombok.extern.slf4j.Slf4j;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
/**
* This class is intended to be used with the SystemCommandExecutor
* class to let users execute system commands from Java applications.
*
* This class is based on work that was shared in a JavaWorld article
* named "When System.exec() won't". That article is available at this
* url:
*
* http://www.javaworld.com/javaworld/jw-12-2000/jw-1229-traps.html
*
* Documentation for this class is available at this URL:
*
* http://devdaily.com/java/java-processbuilder-process-system-exec
*
*
* Copyright 2010 alvin j. alexander, devdaily.com.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program 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 Lesser Public License for more details.
* You should have received a copy of the GNU Lesser Public License
* along with this program. If not, see .
*
* Please ee the following page for the LGPL license:
* http://www.gnu.org/licenses/lgpl.txt
*
*/
@Slf4j
class ThreadedStreamHandler extends Thread {
final InputStream inputStream;
final StringBuilder outputBuffer = new StringBuilder();
ThreadedStreamHandler(InputStream inputStream) {
this.inputStream = inputStream;
}
public void run() {
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) {
String line;
while ((line = bufferedReader.readLine()) != null)
outputBuffer.append(line).append("\n");
} catch (Throwable t) {
t.printStackTrace();
}
}
@SuppressWarnings("unused")
private void doSleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException ignored) {
// empty
}
}
public StringBuilder getOutputBuffer() {
return outputBuffer;
}
}
================================================
FILE: apitest/src/main/resources/apitest.properties
================================================
================================================
FILE: apitest/src/main/resources/blocknotify
================================================
#!/bin/bash
# Regtest ports start with 512*
# To avoid pesky bitcoind io errors, do not specify ports Haveno is not listening to.
# SeedNode listens on port 5120
echo $1 | nc -w 1 127.0.0.1 5120
# Arb Node listens on port 5121
echo $1 | nc -w 1 127.0.0.1 5121
# Alice Node listens on port 5122
echo $1 | nc -w 1 127.0.0.1 5122
# Bob Node listens on port 5123
echo $1 | nc -w 1 127.0.0.1 5123
# Some other node listens on port 5124, etc.
# echo $1 | nc -w 1 127.0.0.1 5124
================================================
FILE: apitest/src/main/resources/haveno.properties
================================================
# Haveno core properties file loaded by Haveno instances started by the test harness.
# Normally, it would be left empty, but it is useful for ad-hoc testing with
# Haveno Config options not configurable in test harness-specific apitest.properties
# file. This is where you might define Haveno options such as:
# dumpBlockchainData=true
# dumpStatistics=true
================================================
FILE: apitest/src/main/resources/logback.xml
================================================
%highlight(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{30}: %msg %xEx%n)
================================================
FILE: apitest/src/test/java/haveno/apitest/ApiTestCase.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.apitest;
import haveno.apitest.config.ApiTestConfig;
import haveno.apitest.method.BitcoinCliHelper;
import haveno.cli.GrpcClient;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.TestInfo;
import javax.annotation.Nullable;
import java.io.IOException;
import java.time.Duration;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import static com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly;
import static haveno.apitest.config.ApiTestRateMeterInterceptorConfig.getTestRateMeterInterceptorConfig;
import static haveno.apitest.config.HavenoAppConfig.alicedaemon;
import static haveno.apitest.config.HavenoAppConfig.arbdaemon;
import static haveno.apitest.config.HavenoAppConfig.bobdaemon;
import static java.net.InetAddress.getLoopbackAddress;
import static java.util.Arrays.stream;
/**
* Base class for all test types: 'method', 'scenario' and 'e2e'.
*
* During scaffold setup, various combinations of bitcoind and haveno instances
* can be started in the background before test cases are run. Currently, this test
* harness supports only the "Haveno DAO development environment running against a
* local Bitcoin regtest network" as described in
* dev-setup.md
* and dao-setup.md.
*
* Those documents contain information about the configurations used by this test harness:
* bitcoin-core's bitcoin.conf and blocknotify values, haveno instance options, the DAO genesis
* transaction id, initial BTC balances for Bob & Alice accounts, and Bob and
* Alice's default payment accounts.
*
* During a build, the
* dao-setup.zip
* file is downloaded and extracted if necessary. In each test case's @BeforeClass
* method, the DAO setup files are re-installed into the run time's data directories
* (each test case runs on a refreshed DAO/regtest environment setup).
*
* Initial Alice balances & accounts: 10.0 BTC, USD PerfectMoney dummy
*
* Initial Bob balances & accounts: 10.0 BTC, USD PerfectMoney dummy
*/
@Slf4j
public class ApiTestCase {
protected static Scaffold scaffold;
protected static ApiTestConfig config;
protected static BitcoinCliHelper bitcoinCli;
@Nullable
protected static GrpcClient arbClient;
@Nullable
protected static GrpcClient aliceClient;
@Nullable
protected static GrpcClient bobClient;
public static void setUpScaffold(Enum>... supportingApps)
throws InterruptedException, ExecutionException, IOException {
String[] params = new String[]{
"--supportingApps", stream(supportingApps).map(Enum::name).collect(Collectors.joining(",")),
"--callRateMeteringConfigPath", getTestRateMeterInterceptorConfig().getAbsolutePath(),
"--enableHavenoDebugging", "false"
};
setUpScaffold(params);
}
public static void setUpScaffold(String[] params)
throws InterruptedException, ExecutionException, IOException {
// Test cases needing to pass more than just an ApiTestConfig
// --supportingApps option will use this setup method, but the
// --supportingApps option will need to be passed too, with its comma
// delimited app list value, e.g., "bitcoind,seednode,arbdaemon".
scaffold = new Scaffold(params).setUp();
config = scaffold.config;
bitcoinCli = new BitcoinCliHelper((config));
createGrpcClients();
}
public static void tearDownScaffold() {
scaffold.tearDown();
}
protected static void createGrpcClients() {
if (config.supportingApps.contains(alicedaemon.name())) {
aliceClient = new GrpcClient(getLoopbackAddress().getHostAddress(),
alicedaemon.apiPort,
config.apiPassword);
}
if (config.supportingApps.contains(bobdaemon.name())) {
bobClient = new GrpcClient(getLoopbackAddress().getHostAddress(),
bobdaemon.apiPort,
config.apiPassword);
}
if (config.supportingApps.contains(arbdaemon.name())) {
arbClient = new GrpcClient(getLoopbackAddress().getHostAddress(),
arbdaemon.apiPort,
config.apiPassword);
}
}
protected static void genBtcBlocksThenWait(int numBlocks, long wait) {
bitcoinCli.generateBlocks(numBlocks);
sleep(wait);
}
protected static void sleep(long ms) {
sleepUninterruptibly(Duration.ofMillis(ms));
}
protected final String testName(TestInfo testInfo) {
return testInfo.getTestMethod().isPresent()
? testInfo.getTestMethod().get().getName()
: "unknown test name";
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/method/BitcoinCliHelper.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.apitest.method;
import haveno.apitest.config.ApiTestConfig;
import haveno.apitest.linux.BitcoinCli;
import java.io.IOException;
import static java.lang.String.format;
import static org.junit.jupiter.api.Assertions.fail;
public final class BitcoinCliHelper {
private final ApiTestConfig config;
public BitcoinCliHelper(ApiTestConfig config) {
this.config = config;
}
// Convenience methods for making bitcoin-cli calls.
public String getNewBtcAddress() {
try {
BitcoinCli newAddress = new BitcoinCli(config, "getnewaddress").run();
if (newAddress.isError())
fail(format("Could not generate new bitcoin address:%n%s", newAddress.getErrorMessage()));
return newAddress.getOutput();
} catch (IOException | InterruptedException ex) {
fail(ex);
return null;
}
}
public String[] generateToAddress(int blocks, String address) {
try {
String generateToAddressCmd = format("generatetoaddress %d \"%s\"", blocks, address);
BitcoinCli generateToAddress = new BitcoinCli(config, generateToAddressCmd).run();
if (generateToAddress.isError())
fail(format("Could not generate bitcoin block(s):%n%s", generateToAddress.getErrorMessage()));
return generateToAddress.getOutputValueAsStringArray();
} catch (IOException | InterruptedException ex) {
fail(ex);
return null;
}
}
public void generateBlocks(int blocks) {
generateToAddress(blocks, getNewBtcAddress());
}
public String sendToAddress(String address, String amount) {
// sendtoaddress "address" amount \
// ( "comment" "comment_to" subtractfeefromamount \
// replaceable conf_target "estimate_mode" )
// returns a transaction id
try {
String sendToAddressCmd = format("sendtoaddress \"%s\" %s \"\" \"\" false",
address, amount);
BitcoinCli sendToAddress = new BitcoinCli(config, sendToAddressCmd).run();
if (sendToAddress.isError())
fail(format("Could not send BTC to address:%n%s", sendToAddress.getErrorMessage()));
return sendToAddress.getOutput();
} catch (IOException | InterruptedException ex) {
fail(ex);
return null;
}
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/method/CallRateMeteringInterceptorTest.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.apitest.method;
import io.grpc.StatusRuntimeException;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import static haveno.apitest.Scaffold.BitcoinCoreApp.bitcoind;
import static haveno.apitest.config.HavenoAppConfig.alicedaemon;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@Disabled
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class CallRateMeteringInterceptorTest extends MethodTest {
private static final GetVersionTest getVersionTest = new GetVersionTest();
@BeforeAll
public static void setUp() {
startSupportingApps(false,
false,
bitcoind, alicedaemon);
}
@BeforeEach
public void sleep200Milliseconds() {
sleep(200);
}
@Test
@Order(1)
public void testGetVersionCall1IsAllowed() {
getVersionTest.testGetVersion();
}
@Test
@Order(2)
public void testGetVersionCall2ShouldThrowException() {
Throwable exception = assertThrows(StatusRuntimeException.class, getVersionTest::testGetVersion);
assertEquals("PERMISSION_DENIED: the maximum allowed number of getversion calls (1/second) has been exceeded",
exception.getMessage());
}
@Test
@Order(3)
public void testGetVersionCall3ShouldThrowException() {
Throwable exception = assertThrows(StatusRuntimeException.class, getVersionTest::testGetVersion);
assertEquals("PERMISSION_DENIED: the maximum allowed number of getversion calls (1/second) has been exceeded",
exception.getMessage());
}
@Test
@Order(4)
public void testGetVersionCall4IsAllowed() {
sleep(1100); // Let the server's rate meter reset the call count.
getVersionTest.testGetVersion();
}
@AfterAll
public static void tearDown() {
tearDownScaffold();
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/method/GetMethodHelpTest.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.apitest.method;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import static haveno.apitest.config.HavenoAppConfig.alicedaemon;
import static haveno.cli.Method.createoffer;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
@Disabled
@Slf4j
@TestMethodOrder(OrderAnnotation.class)
public class GetMethodHelpTest extends MethodTest {
@BeforeAll
public static void setUp() {
try {
setUpScaffold(alicedaemon);
} catch (Exception ex) {
fail(ex);
}
}
@Test
@Order(1)
public void testGetCreateOfferHelp() {
var help = aliceClient.getMethodHelp(createoffer);
assertNotNull(help);
}
@AfterAll
public static void tearDown() {
tearDownScaffold();
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/method/GetVersionTest.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.apitest.method;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import static haveno.apitest.config.HavenoAppConfig.alicedaemon;
import static haveno.common.app.Version.VERSION;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
@Disabled
@Slf4j
@TestMethodOrder(OrderAnnotation.class)
public class GetVersionTest extends MethodTest {
@BeforeAll
public static void setUp() {
try {
setUpScaffold(alicedaemon);
} catch (Exception ex) {
fail(ex);
}
}
@Test
@Order(1)
public void testGetVersion() {
var version = aliceClient.getVersion();
assertEquals(VERSION, version);
}
@AfterAll
public static void tearDown() {
tearDownScaffold();
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/method/MethodTest.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.apitest.method;
import haveno.apitest.ApiTestCase;
import haveno.apitest.linux.BashCommand;
import haveno.cli.GrpcClient;
import haveno.cli.table.builder.TableBuilder;
import haveno.common.util.Utilities;
import haveno.core.api.model.PaymentAccountForm;
import haveno.core.payment.F2FAccount;
import haveno.core.payment.NationalBankAccount;
import haveno.core.proto.CoreProtoResolver;
import haveno.proto.grpc.BalancesInfo;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import org.slf4j.Logger;
import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.math.BigDecimal;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static haveno.apitest.config.ApiTestConfig.BTC;
import static haveno.apitest.config.ApiTestRateMeterInterceptorConfig.getTestRateMeterInterceptorConfig;
import static haveno.cli.table.builder.TableType.BTC_BALANCE_TBL;
import static haveno.core.xmr.wallet.Restrictions.getDefaultSecurityDepositPct;
import static java.lang.String.format;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Arrays.stream;
import static org.junit.jupiter.api.Assertions.fail;
public class MethodTest extends ApiTestCase {
protected static final CoreProtoResolver CORE_PROTO_RESOLVER = new CoreProtoResolver();
private static final Function[], String> toNameList = (enums) ->
stream(enums).map(Enum::name).collect(Collectors.joining(","));
public static void startSupportingApps(File callRateMeteringConfigFile,
boolean generateBtcBlock,
boolean startSupportingAppsInDebugMode,
Enum>... supportingApps) {
try {
setUpScaffold(new String[]{
"--supportingApps", toNameList.apply(supportingApps),
"--callRateMeteringConfigPath", callRateMeteringConfigFile.getAbsolutePath(),
"--enableHavenoDebugging", startSupportingAppsInDebugMode ? "true" : "false"
});
doPostStartup(generateBtcBlock);
} catch (Exception ex) {
fail(ex);
}
}
public static void startSupportingApps(boolean generateBtcBlock,
boolean startSupportingAppsInDebugMode,
Enum>... supportingApps) {
try {
// Disable call rate metering where there is no callRateMeteringConfigFile.
File callRateMeteringConfigFile = getTestRateMeterInterceptorConfig();
setUpScaffold(new String[]{
"--supportingApps", toNameList.apply(supportingApps),
"--callRateMeteringConfigPath", callRateMeteringConfigFile.getAbsolutePath(),
"--enableHavenoDebugging", startSupportingAppsInDebugMode ? "true" : "false"
});
doPostStartup(generateBtcBlock);
} catch (Exception ex) {
fail(ex);
}
}
protected static void doPostStartup(boolean generateBtcBlock) {
// Generate 1 regtest block for alice's and/or bob's wallet to
// show 10 BTC balance, and allow time for daemons parse the new block.
if (generateBtcBlock)
genBtcBlocksThenWait(1, 1500);
}
protected final File getPaymentAccountForm(GrpcClient grpcClient, String paymentMethodId) {
// We take seemingly unnecessary steps to get a File object, but the point is to
// test the API, and we do not directly ask haveno.core.api.model.PaymentAccountForm
// for an empty json form (file).
String jsonString = grpcClient.getPaymentAcctFormAsJson(paymentMethodId);
// Write the json string to a file here in the test case.
File jsonFile = PaymentAccountForm.getTmpJsonFile(paymentMethodId);
try (PrintWriter out = new PrintWriter(jsonFile, UTF_8)) {
out.println(jsonString);
} catch (IOException ex) {
fail("Could not create tmp payment account form.", ex);
}
return jsonFile;
}
protected haveno.core.payment.PaymentAccount createDummyF2FAccount(GrpcClient grpcClient,
String countryCode) {
String f2fAccountJsonString = "{\n" +
" \"_COMMENTS_\": \"This is a dummy account.\",\n" +
" \"paymentMethodId\": \"F2F\",\n" +
" \"accountName\": \"Dummy " + countryCode.toUpperCase() + " F2F Account\",\n" +
" \"city\": \"Anytown\",\n" +
" \"contact\": \"Morse Code\",\n" +
" \"country\": \"" + countryCode.toUpperCase() + "\",\n" +
" \"extraInfo\": \"Salt Lick #213\"\n" +
"}\n";
F2FAccount f2FAccount = (F2FAccount) createPaymentAccount(grpcClient, f2fAccountJsonString);
return f2FAccount;
}
protected haveno.core.payment.PaymentAccount createDummyBRLAccount(GrpcClient grpcClient,
String holderName,
String nationalAccountId,
String holderTaxId) {
String nationalBankAccountJsonString = "{\n" +
" \"_COMMENTS_\": [ \"Dummy Account\" ],\n" +
" \"paymentMethodId\": \"NATIONAL_BANK\",\n" +
" \"accountName\": \"Banco do Brasil\",\n" +
" \"country\": \"BR\",\n" +
" \"bankName\": \"Banco do Brasil\",\n" +
" \"branchId\": \"456789-10\",\n" +
" \"holderName\": \"" + holderName + "\",\n" +
" \"accountNr\": \"456789-87\",\n" +
" \"nationalAccountId\": \"" + nationalAccountId + "\",\n" +
" \"holderTaxId\": \"" + holderTaxId + "\"\n" +
"}\n";
NationalBankAccount nationalBankAccount =
(NationalBankAccount) createPaymentAccount(grpcClient, nationalBankAccountJsonString);
return nationalBankAccount;
}
protected final haveno.core.payment.PaymentAccount createPaymentAccount(GrpcClient grpcClient, String jsonString) {
// Normally, we do asserts on the protos from the gRPC service, but in this
// case we need a haveno.core.payment.PaymentAccount so it can be cast to its
// sub-type.
var paymentAccount = grpcClient.createPaymentAccount(jsonString);
return haveno.core.payment.PaymentAccount.fromProto(paymentAccount, CORE_PROTO_RESOLVER);
}
public static final Supplier defaultSecurityDepositPct = () -> {
var defaultPct = BigDecimal.valueOf(getDefaultSecurityDepositPct());
if (defaultPct.precision() != 2)
throw new IllegalStateException(format(
"Unexpected decimal precision, expected 2 but actual is %d%n."
+ "Check for changes to Restrictions.getDefaultBuyerSecurityDepositAsPercent()",
defaultPct.precision()));
return defaultPct.movePointRight(2).doubleValue();
};
public static String formatBalancesTbls(BalancesInfo allBalances) {
StringBuilder balances = new StringBuilder(BTC).append("\n");
balances.append(new TableBuilder(BTC_BALANCE_TBL, allBalances.getBtc()).build());
balances.append("\n");
return balances.toString();
}
protected static String encodeToHex(String s) {
return Utilities.bytesAsHexString(s.getBytes(UTF_8));
}
protected static Status.Code getStatusRuntimeExceptionStatusCode(Exception grpcException) {
if (grpcException instanceof StatusRuntimeException)
return ((StatusRuntimeException) grpcException).getStatus().getCode();
else
throw new IllegalArgumentException(
format("Expected a io.grpc.StatusRuntimeException argument, but got a %s",
grpcException.getClass().getName()));
}
protected void verifyNoLoggedNodeExceptions() {
var loggedExceptions = getNodeExceptionMessages();
if (loggedExceptions != null) {
String err = format("Exception(s) found in daemon log(s):%n%s", loggedExceptions);
fail(err);
}
}
protected void printNodeExceptionMessages(Logger log) {
var loggedExceptions = getNodeExceptionMessages();
if (loggedExceptions != null)
log.error("Exception(s) found in daemon log(s):\n{}", loggedExceptions);
}
@Nullable
protected static String getNodeExceptionMessages() {
var nodeLogsSpec = config.rootAppDataDir.getAbsolutePath() + "/haveno-BTC_REGTEST_*_dao/haveno.log";
var grep = "grep Exception " + nodeLogsSpec;
var bashCommand = new BashCommand(grep);
try {
bashCommand.run();
} catch (IOException | InterruptedException ex) {
fail("Bash command execution error: " + ex);
}
if (bashCommand.getError() == null)
return bashCommand.getOutput();
else
throw new IllegalStateException("Bash command execution error: " + bashCommand.getError());
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/method/RegisterDisputeAgentsTest.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.apitest.method;
import io.grpc.StatusRuntimeException;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import static haveno.apitest.Scaffold.BitcoinCoreApp.bitcoind;
import static haveno.apitest.config.ApiTestConfig.ARBITRATOR;
import static haveno.apitest.config.ApiTestConfig.MEDIATOR;
import static haveno.apitest.config.ApiTestConfig.REFUND_AGENT;
import static haveno.apitest.config.HavenoAppConfig.arbdaemon;
import static haveno.apitest.config.HavenoAppConfig.seednode;
import static haveno.common.app.DevEnv.DEV_PRIVILEGE_PRIV_KEY;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
@SuppressWarnings("ResultOfMethodCallIgnored")
@Disabled
@Slf4j
@TestMethodOrder(OrderAnnotation.class)
public class RegisterDisputeAgentsTest extends MethodTest {
@BeforeAll
public static void setUp() {
try {
setUpScaffold(bitcoind, seednode, arbdaemon);
} catch (Exception ex) {
fail(ex);
}
}
@Test
@Order(1)
public void testRegisterArbitratorShouldThrowException() {
Throwable exception = assertThrows(StatusRuntimeException.class, () ->
arbClient.registerDisputeAgent(ARBITRATOR, DEV_PRIVILEGE_PRIV_KEY));
assertEquals("UNIMPLEMENTED: arbitrators must be registered in a Haveno UI",
exception.getMessage());
}
@Test
@Order(2)
public void testInvalidDisputeAgentTypeArgShouldThrowException() {
Throwable exception = assertThrows(StatusRuntimeException.class, () ->
arbClient.registerDisputeAgent("badagent", DEV_PRIVILEGE_PRIV_KEY));
assertEquals("INVALID_ARGUMENT: unknown dispute agent type 'badagent'",
exception.getMessage());
}
@Test
@Order(3)
public void testInvalidRegistrationKeyArgShouldThrowException() {
Throwable exception = assertThrows(StatusRuntimeException.class, () ->
arbClient.registerDisputeAgent(REFUND_AGENT, "invalid" + DEV_PRIVILEGE_PRIV_KEY));
assertEquals("INVALID_ARGUMENT: invalid registration key",
exception.getMessage());
}
@Test
@Order(4)
public void testRegisterMediator() {
arbClient.registerDisputeAgent(MEDIATOR, DEV_PRIVILEGE_PRIV_KEY);
}
@Test
@Order(5)
public void testRegisterRefundAgent() {
arbClient.registerDisputeAgent(REFUND_AGENT, DEV_PRIVILEGE_PRIV_KEY);
}
@AfterAll
public static void tearDown() {
tearDownScaffold();
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/method/offer/AbstractOfferTest.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.apitest.method.offer;
import haveno.apitest.method.MethodTest;
import haveno.cli.CliMain;
import haveno.cli.table.builder.TableBuilder;
import haveno.proto.grpc.OfferInfo;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import protobuf.PaymentAccount;
import java.math.BigDecimal;
import java.math.MathContext;
import java.util.List;
import java.util.function.Function;
import static haveno.apitest.Scaffold.BitcoinCoreApp.bitcoind;
import static haveno.apitest.config.ApiTestConfig.XMR;
import static haveno.apitest.config.HavenoAppConfig.alicedaemon;
import static haveno.apitest.config.HavenoAppConfig.arbdaemon;
import static haveno.apitest.config.HavenoAppConfig.bobdaemon;
import static haveno.apitest.config.HavenoAppConfig.seednode;
import static haveno.cli.table.builder.TableType.OFFER_TBL;
import static java.lang.String.format;
import static java.lang.System.out;
@Slf4j
public abstract class AbstractOfferTest extends MethodTest {
protected static final int ACTIVATE_OFFER = 1;
protected static final int DEACTIVATE_OFFER = 0;
protected static final String NO_TRIGGER_PRICE = "0";
@Setter
protected static boolean isLongRunningTest;
protected static PaymentAccount alicesBtcAcct;
protected static PaymentAccount bobsBtcAcct;
protected static PaymentAccount alicesXmrAcct;
protected static PaymentAccount bobsXmrAcct;
@BeforeAll
public static void setUp() {
setUp(false);
}
public static void setUp(boolean startSupportingAppsInDebugMode) {
startSupportingApps(true,
startSupportingAppsInDebugMode,
bitcoind,
seednode,
arbdaemon,
alicedaemon,
bobdaemon);
initPaymentAccounts();
}
protected static final Function toOfferTable = (offer) ->
new TableBuilder(OFFER_TBL, offer).build().toString();
protected static final Function, String> toOffersTable = (offers) ->
new TableBuilder(OFFER_TBL, offers).build().toString();
protected static String calcPriceAsString(double base, double delta, int precision) {
var mathContext = new MathContext(precision);
var priceAsBigDecimal = new BigDecimal(Double.toString(base), mathContext)
.add(new BigDecimal(Double.toString(delta), mathContext))
.round(mathContext);
return format("%." + precision + "f", priceAsBigDecimal.doubleValue());
}
@SuppressWarnings("ConstantConditions")
public static void initPaymentAccounts() {
alicesBtcAcct = aliceClient.getPaymentAccount("BTC");
bobsBtcAcct = bobClient.getPaymentAccount("BTC");
}
@SuppressWarnings("ConstantConditions")
public static void createXmrPaymentAccounts() {
alicesXmrAcct = aliceClient.createCryptoCurrencyPaymentAccount("Alice's XMR Account",
XMR,
"44G4jWmSvTEfifSUZzTDnJVLPvYATmq9XhhtDqUof1BGCLceG82EQsVYG9Q9GN4bJcjbAJEc1JD1m5G7iK4UPZqACubV4Mq",
false);
log.trace("Alices XMR Account: {}", alicesXmrAcct);
bobsXmrAcct = bobClient.createCryptoCurrencyPaymentAccount("Bob's XMR Account",
XMR,
"4BDRhdSBKZqAXs3PuNTbMtaXBNqFj5idC2yMVnQj8Rm61AyKY8AxLTt9vGRJ8pwcG4EtpyD8YpGqdZWCZ2VZj6yVBN2RVKs",
false);
log.trace("Bob's XMR Account: {}", bobsXmrAcct);
}
@AfterAll
public static void tearDown() {
tearDownScaffold();
}
protected static void runCliGetOffer(String offerId) {
out.println("Alice's CLI 'getmyoffer' response:");
CliMain.main(new String[]{"--password=xyz", "--port=9998", "getmyoffer", "--offer-id=" + offerId});
out.println("Bob's CLI 'getoffer' response:");
CliMain.main(new String[]{"--password=xyz", "--port=9999", "getoffer", "--offer-id=" + offerId});
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/method/offer/CancelOfferTest.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.apitest.method.offer;
import haveno.core.payment.PaymentAccount;
import haveno.proto.grpc.OfferInfo;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import java.util.List;
import java.util.function.Consumer;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static protobuf.OfferDirection.BUY;
@Disabled
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class CancelOfferTest extends AbstractOfferTest {
private static final String DIRECTION = BUY.name();
private static final String CURRENCY_CODE = "cad";
private static final int MAX_OFFERS = 3;
private final Consumer createOfferToCancel = (paymentAccountId) -> {
aliceClient.createMarketBasedPricedOffer(DIRECTION,
CURRENCY_CODE,
10000000L,
10000000L,
0.00,
defaultSecurityDepositPct.get(),
paymentAccountId,
NO_TRIGGER_PRICE);
};
@Test
@Order(1)
public void testCancelOffer() {
PaymentAccount cadAccount = createDummyF2FAccount(aliceClient, "CA");
// Create some offers.
for (int i = 1; i <= MAX_OFFERS; i++) {
createOfferToCancel.accept(cadAccount.getId());
// Wait for Alice's AddToOfferBook task.
// Wait times vary; my logs show >= 2 second delay.
sleep(2500);
}
List offers = aliceClient.getMyOffersSortedByDate(DIRECTION, CURRENCY_CODE);
assertEquals(MAX_OFFERS, offers.size());
// Cancel the offers, checking the open offer count after each offer removal.
for (int i = 1; i <= MAX_OFFERS; i++) {
aliceClient.cancelOffer(offers.remove(0).getId());
offers = aliceClient.getMyOffersSortedByDate(DIRECTION, CURRENCY_CODE);
assertEquals(MAX_OFFERS - i, offers.size());
}
sleep(1000); // wait for offer removal
offers = aliceClient.getMyOffersSortedByDate(DIRECTION, CURRENCY_CODE);
assertEquals(0, offers.size());
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/method/offer/CreateOfferUsingFixedPriceTest.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.apitest.method.offer;
import haveno.core.payment.PaymentAccount;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import static haveno.apitest.config.ApiTestConfig.EUR;
import static haveno.apitest.config.ApiTestConfig.USD;
import static haveno.apitest.config.ApiTestConfig.XMR;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static protobuf.OfferDirection.BUY;
import static protobuf.OfferDirection.SELL;
@Disabled
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
@Test
@Order(1)
public void testCreateAUDBTCBuyOfferUsingFixedPrice16000() {
PaymentAccount audAccount = createDummyF2FAccount(aliceClient, "AU");
var newOffer = aliceClient.createFixedPricedOffer(BUY.name(),
"aud",
10_000_000L,
10_000_000L,
"36000",
defaultSecurityDepositPct.get(),
audAccount.getId());
log.debug("Offer #1:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer());
assertFalse(newOffer.getIsActivated());
String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId);
assertEquals(BUY.name(), newOffer.getDirection());
assertFalse(newOffer.getUseMarketBasedPrice());
assertEquals("36000.0000", newOffer.getPrice());
assertEquals(10_000_000, newOffer.getAmount());
assertEquals(10_000_000, newOffer.getMinAmount());
assertEquals("3600", newOffer.getVolume());
assertEquals("3600", newOffer.getMinVolume());
assertEquals(.15, newOffer.getBuyerSecurityDepositPct());
assertEquals(.15, newOffer.getSellerSecurityDepositPct());
assertEquals(audAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode());
assertEquals("AUD", newOffer.getCounterCurrencyCode());
newOffer = aliceClient.getOffer(newOfferId);
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsActivated());
assertEquals(newOfferId, newOffer.getId());
assertEquals(BUY.name(), newOffer.getDirection());
assertFalse(newOffer.getUseMarketBasedPrice());
assertEquals("36000.0000", newOffer.getPrice());
assertEquals(10_000_000, newOffer.getAmount());
assertEquals(10_000_000, newOffer.getMinAmount());
assertEquals("3600", newOffer.getVolume());
assertEquals("3600", newOffer.getMinVolume());
assertEquals(.15, newOffer.getBuyerSecurityDepositPct());
assertEquals(.15, newOffer.getSellerSecurityDepositPct());
assertEquals(audAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode());
assertEquals("AUD", newOffer.getCounterCurrencyCode());
}
@Test
@Order(2)
public void testCreateUSDBTCBuyOfferUsingFixedPrice100001234() {
PaymentAccount usdAccount = createDummyF2FAccount(aliceClient, "US");
var newOffer = aliceClient.createFixedPricedOffer(BUY.name(),
"usd",
10_000_000L,
10_000_000L,
"30000.1234",
defaultSecurityDepositPct.get(),
usdAccount.getId());
log.debug("Offer #2:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer());
assertFalse(newOffer.getIsActivated());
String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId);
assertEquals(BUY.name(), newOffer.getDirection());
assertFalse(newOffer.getUseMarketBasedPrice());
assertEquals("30000.1234", newOffer.getPrice());
assertEquals(10_000_000, newOffer.getAmount());
assertEquals(10_000_000, newOffer.getMinAmount());
assertEquals("3000", newOffer.getVolume());
assertEquals("3000", newOffer.getMinVolume());
assertEquals(.15, newOffer.getBuyerSecurityDepositPct());
assertEquals(.15, newOffer.getSellerSecurityDepositPct());
assertEquals(usdAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode());
assertEquals(USD, newOffer.getCounterCurrencyCode());
newOffer = aliceClient.getOffer(newOfferId);
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsActivated());
assertEquals(newOfferId, newOffer.getId());
assertEquals(BUY.name(), newOffer.getDirection());
assertFalse(newOffer.getUseMarketBasedPrice());
assertEquals("30000.1234", newOffer.getPrice());
assertEquals(10_000_000, newOffer.getAmount());
assertEquals(10_000_000, newOffer.getMinAmount());
assertEquals("3000", newOffer.getVolume());
assertEquals("3000", newOffer.getMinVolume());
assertEquals(.15, newOffer.getBuyerSecurityDepositPct());
assertEquals(.15, newOffer.getSellerSecurityDepositPct());
assertEquals(usdAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode());
assertEquals(USD, newOffer.getCounterCurrencyCode());
}
@Test
@Order(3)
public void testCreateEURBTCSellOfferUsingFixedPrice95001234() {
PaymentAccount eurAccount = createDummyF2FAccount(aliceClient, "FR");
var newOffer = aliceClient.createFixedPricedOffer(SELL.name(),
"eur",
10_000_000L,
5_000_000L,
"29500.1234",
defaultSecurityDepositPct.get(),
eurAccount.getId());
log.debug("Offer #3:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer());
assertFalse(newOffer.getIsActivated());
String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId);
assertEquals(SELL.name(), newOffer.getDirection());
assertFalse(newOffer.getUseMarketBasedPrice());
assertEquals("29500.1234", newOffer.getPrice());
assertEquals(10_000_000, newOffer.getAmount());
assertEquals(5_000_000, newOffer.getMinAmount());
assertEquals("2950", newOffer.getVolume());
assertEquals("1475", newOffer.getMinVolume());
assertEquals(.15, newOffer.getBuyerSecurityDepositPct());
assertEquals(.15, newOffer.getSellerSecurityDepositPct());
assertEquals(eurAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode());
assertEquals(EUR, newOffer.getCounterCurrencyCode());
newOffer = aliceClient.getOffer(newOfferId);
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsActivated());
assertEquals(newOfferId, newOffer.getId());
assertEquals(SELL.name(), newOffer.getDirection());
assertFalse(newOffer.getUseMarketBasedPrice());
assertEquals("29500.1234", newOffer.getPrice());
assertEquals(10_000_000, newOffer.getAmount());
assertEquals(5_000_000, newOffer.getMinAmount());
assertEquals("2950", newOffer.getVolume());
assertEquals("1475", newOffer.getMinVolume());
assertEquals(.15, newOffer.getBuyerSecurityDepositPct());
assertEquals(.15, newOffer.getSellerSecurityDepositPct());
assertEquals(eurAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode());
assertEquals(EUR, newOffer.getCounterCurrencyCode());
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.apitest.method.offer;
import haveno.core.payment.PaymentAccount;
import haveno.proto.grpc.OfferInfo;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import static haveno.apitest.config.ApiTestConfig.BTC;
import static haveno.apitest.config.ApiTestConfig.USD;
import static haveno.common.util.MathUtils.roundDouble;
import static haveno.common.util.MathUtils.scaleDownByPowerOf10;
import static haveno.common.util.MathUtils.scaleUpByPowerOf10;
import static java.lang.Math.abs;
import static java.lang.String.format;
import static java.math.RoundingMode.HALF_UP;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static protobuf.OfferDirection.BUY;
import static protobuf.OfferDirection.SELL;
@SuppressWarnings("ConstantConditions")
@Disabled
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
private static final DecimalFormat PCT_FORMAT = new DecimalFormat("##0.00");
private static final double MKT_PRICE_MARGIN_ERROR_TOLERANCE = 0.0050; // 0.50%
private static final double MKT_PRICE_MARGIN_WARNING_TOLERANCE = 0.0001; // 0.01%
private static final String MAKER_FEE_CURRENCY_CODE = BTC;
@Test
@Order(1)
public void testCreateUSDBTCBuyOffer5PctPriceMargin() {
PaymentAccount usdAccount = createDummyF2FAccount(aliceClient, "US");
double priceMarginPctInput = 5.00d;
var newOffer = aliceClient.createMarketBasedPricedOffer(BUY.name(),
"usd",
10_000_000L,
10_000_000L,
priceMarginPctInput,
defaultSecurityDepositPct.get(),
usdAccount.getId(),
NO_TRIGGER_PRICE);
log.debug("Offer #1:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer());
assertFalse(newOffer.getIsActivated());
String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId);
assertEquals(BUY.name(), newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice());
assertEquals(priceMarginPctInput, newOffer.getMarketPriceMarginPct());
assertEquals(10_000_000, newOffer.getAmount());
assertEquals(10_000_000, newOffer.getMinAmount());
assertEquals(.15, newOffer.getBuyerSecurityDepositPct());
assertEquals(.15, newOffer.getSellerSecurityDepositPct());
assertEquals(usdAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(BTC, newOffer.getBaseCurrencyCode());
assertEquals(USD, newOffer.getCounterCurrencyCode());
newOffer = aliceClient.getOffer(newOfferId);
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsActivated());
assertEquals(newOfferId, newOffer.getId());
assertEquals(BUY.name(), newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice());
assertEquals(priceMarginPctInput, newOffer.getMarketPriceMarginPct());
assertEquals(10_000_000, newOffer.getAmount());
assertEquals(10_000_000, newOffer.getMinAmount());
assertEquals(.15, newOffer.getBuyerSecurityDepositPct());
assertEquals(.15, newOffer.getSellerSecurityDepositPct());
assertEquals(usdAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(BTC, newOffer.getBaseCurrencyCode());
assertEquals(USD, newOffer.getCounterCurrencyCode());
assertCalculatedPriceIsCorrect(newOffer, priceMarginPctInput);
}
@Test
@Order(2)
public void testCreateNZDBTCBuyOfferMinus2PctPriceMargin() {
PaymentAccount nzdAccount = createDummyF2FAccount(aliceClient, "NZ");
double priceMarginPctInput = -2.00d; // -2%
var newOffer = aliceClient.createMarketBasedPricedOffer(BUY.name(),
"nzd",
10_000_000L,
10_000_000L,
priceMarginPctInput,
defaultSecurityDepositPct.get(),
nzdAccount.getId(),
NO_TRIGGER_PRICE);
log.debug("Offer #2:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer());
assertFalse(newOffer.getIsActivated());
String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId);
assertEquals(BUY.name(), newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice());
assertEquals(priceMarginPctInput, newOffer.getMarketPriceMarginPct());
assertEquals(10_000_000, newOffer.getAmount());
assertEquals(10_000_000, newOffer.getMinAmount());
assertEquals(.15, newOffer.getBuyerSecurityDepositPct());
assertEquals(.15, newOffer.getSellerSecurityDepositPct());
assertEquals(nzdAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(BTC, newOffer.getBaseCurrencyCode());
assertEquals("NZD", newOffer.getCounterCurrencyCode());
newOffer = aliceClient.getOffer(newOfferId);
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsActivated());
assertEquals(newOfferId, newOffer.getId());
assertEquals(BUY.name(), newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice());
assertEquals(priceMarginPctInput, newOffer.getMarketPriceMarginPct());
assertEquals(10_000_000, newOffer.getAmount());
assertEquals(10_000_000, newOffer.getMinAmount());
assertEquals(.15, newOffer.getBuyerSecurityDepositPct());
assertEquals(.15, newOffer.getSellerSecurityDepositPct());
assertEquals(nzdAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(BTC, newOffer.getBaseCurrencyCode());
assertEquals("NZD", newOffer.getCounterCurrencyCode());
assertCalculatedPriceIsCorrect(newOffer, priceMarginPctInput);
}
@Test
@Order(3)
public void testCreateGBPBTCSellOfferMinus1Point5PctPriceMargin() {
PaymentAccount gbpAccount = createDummyF2FAccount(aliceClient, "GB");
double priceMarginPctInput = -1.5;
var newOffer = aliceClient.createMarketBasedPricedOffer(SELL.name(),
"gbp",
10_000_000L,
5_000_000L,
priceMarginPctInput,
defaultSecurityDepositPct.get(),
gbpAccount.getId(),
NO_TRIGGER_PRICE);
log.debug("Offer #3:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer());
assertFalse(newOffer.getIsActivated());
String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId);
assertEquals(SELL.name(), newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice());
assertEquals(priceMarginPctInput, newOffer.getMarketPriceMarginPct());
assertEquals(10_000_000, newOffer.getAmount());
assertEquals(5_000_000, newOffer.getMinAmount());
assertEquals(.15, newOffer.getBuyerSecurityDepositPct());
assertEquals(.15, newOffer.getSellerSecurityDepositPct());
assertEquals(gbpAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(BTC, newOffer.getBaseCurrencyCode());
assertEquals("GBP", newOffer.getCounterCurrencyCode());
newOffer = aliceClient.getOffer(newOfferId);
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsActivated());
assertEquals(newOfferId, newOffer.getId());
assertEquals(SELL.name(), newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice());
assertEquals(priceMarginPctInput, newOffer.getMarketPriceMarginPct());
assertEquals(10_000_000, newOffer.getAmount());
assertEquals(5_000_000, newOffer.getMinAmount());
assertEquals(.15, newOffer.getBuyerSecurityDepositPct());
assertEquals(.15, newOffer.getSellerSecurityDepositPct());
assertEquals(gbpAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(BTC, newOffer.getBaseCurrencyCode());
assertEquals("GBP", newOffer.getCounterCurrencyCode());
assertCalculatedPriceIsCorrect(newOffer, priceMarginPctInput);
}
@Test
@Order(4)
public void testCreateBRLBTCSellOffer6Point55PctPriceMargin() {
PaymentAccount brlAccount = createDummyF2FAccount(aliceClient, "BR");
double priceMarginPctInput = 6.55;
var newOffer = aliceClient.createMarketBasedPricedOffer(SELL.name(),
"brl",
10_000_000L,
5_000_000L,
priceMarginPctInput,
defaultSecurityDepositPct.get(),
brlAccount.getId(),
NO_TRIGGER_PRICE);
log.debug("Offer #4:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer());
assertFalse(newOffer.getIsActivated());
String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId);
assertEquals(SELL.name(), newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice());
assertEquals(priceMarginPctInput, newOffer.getMarketPriceMarginPct());
assertEquals(10_000_000, newOffer.getAmount());
assertEquals(5_000_000, newOffer.getMinAmount());
assertEquals(.15, newOffer.getBuyerSecurityDepositPct());
assertEquals(.15, newOffer.getSellerSecurityDepositPct());
assertEquals(brlAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(BTC, newOffer.getBaseCurrencyCode());
assertEquals("BRL", newOffer.getCounterCurrencyCode());
newOffer = aliceClient.getOffer(newOfferId);
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsActivated());
assertEquals(newOfferId, newOffer.getId());
assertEquals(SELL.name(), newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice());
assertEquals(priceMarginPctInput, newOffer.getMarketPriceMarginPct());
assertEquals(10_000_000, newOffer.getAmount());
assertEquals(5_000_000, newOffer.getMinAmount());
assertEquals(.15, newOffer.getBuyerSecurityDepositPct());
assertEquals(.15, newOffer.getSellerSecurityDepositPct());
assertEquals(brlAccount.getId(), newOffer.getPaymentAccountId());
assertEquals(BTC, newOffer.getBaseCurrencyCode());
assertEquals("BRL", newOffer.getCounterCurrencyCode());
assertCalculatedPriceIsCorrect(newOffer, priceMarginPctInput);
}
@Test
@Order(5)
public void testCreateUSDBTCBuyOfferWithTriggerPrice() {
PaymentAccount usdAccount = createDummyF2FAccount(aliceClient, "US");
double mktPriceAsDouble = aliceClient.getBtcPrice("usd");
String triggerPrice = calcPriceAsString(mktPriceAsDouble, Double.parseDouble("1000.9999"), 4);
var newOffer = aliceClient.createMarketBasedPricedOffer(BUY.name(),
"usd",
10_000_000L,
5_000_000L,
0.0,
defaultSecurityDepositPct.get(),
usdAccount.getId(),
triggerPrice);
assertTrue(newOffer.getIsMyOffer());
assertFalse(newOffer.getIsActivated());
genBtcBlocksThenWait(1, 4000); // give time to add to offer book
newOffer = aliceClient.getOffer(newOffer.getId());
log.debug("Offer #5:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsActivated());
assertEquals(triggerPrice, newOffer.getTriggerPrice());
}
private void assertCalculatedPriceIsCorrect(OfferInfo offer, double priceMarginPctInput) {
assertTrue(() -> {
String counterCurrencyCode = offer.getCounterCurrencyCode();
double mktPrice = aliceClient.getBtcPrice(counterCurrencyCode);
double priceAsDouble = Double.parseDouble(offer.getPrice());
double expectedDiffPct = scaleDownByPowerOf10(priceMarginPctInput, 2);
double actualDiffPct = offer.getDirection().equals(BUY.name())
? getPercentageDifference(priceAsDouble, mktPrice)
: getPercentageDifference(mktPrice, priceAsDouble);
double pctDiffDelta = abs(expectedDiffPct) - abs(actualDiffPct);
return isCalculatedPriceWithinErrorTolerance(pctDiffDelta,
expectedDiffPct,
actualDiffPct,
mktPrice,
priceAsDouble,
offer);
});
}
private double getPercentageDifference(double price1, double price2) {
return BigDecimal.valueOf(roundDouble((1 - (price1 / price2)), 5))
.setScale(4, HALF_UP)
.doubleValue();
}
private boolean isCalculatedPriceWithinErrorTolerance(double delta,
double expectedDiffPct,
double actualDiffPct,
double mktPrice,
double scaledOfferPrice,
OfferInfo offer) {
if (abs(delta) > MKT_PRICE_MARGIN_ERROR_TOLERANCE) {
logCalculatedPricePoppedErrorTolerance(expectedDiffPct,
actualDiffPct,
mktPrice,
scaledOfferPrice);
log.error(offer.toString());
return false;
}
if (abs(delta) >= MKT_PRICE_MARGIN_WARNING_TOLERANCE) {
logCalculatedPricePoppedWarningTolerance(expectedDiffPct,
actualDiffPct,
mktPrice,
scaledOfferPrice);
log.trace(offer.toString());
}
return true;
}
private void logCalculatedPricePoppedWarningTolerance(double expectedDiffPct,
double actualDiffPct,
double mktPrice,
double scaledOfferPrice) {
log.warn(format("Calculated price %.4f & mkt price %.4f differ by ~ %s%s,"
+ " not by %s%s, outside the %s%s warning tolerance,"
+ " but within the %s%s error tolerance.",
scaledOfferPrice, mktPrice,
PCT_FORMAT.format(scaleUpByPowerOf10(actualDiffPct, 2)), "%",
PCT_FORMAT.format(scaleUpByPowerOf10(expectedDiffPct, 2)), "%",
PCT_FORMAT.format(scaleUpByPowerOf10(MKT_PRICE_MARGIN_WARNING_TOLERANCE, 2)), "%",
PCT_FORMAT.format(scaleUpByPowerOf10(MKT_PRICE_MARGIN_ERROR_TOLERANCE, 2)), "%"));
}
private void logCalculatedPricePoppedErrorTolerance(double expectedDiffPct,
double actualDiffPct,
double mktPrice,
double scaledOfferPrice) {
log.error(format("Calculated price %.4f & mkt price %.4f differ by ~ %s%s,"
+ " not by %s%s, outside the %s%s error tolerance.",
scaledOfferPrice, mktPrice,
PCT_FORMAT.format(scaleUpByPowerOf10(actualDiffPct, 2)), "%",
PCT_FORMAT.format(scaleUpByPowerOf10(expectedDiffPct, 2)), "%",
PCT_FORMAT.format(scaleUpByPowerOf10(MKT_PRICE_MARGIN_ERROR_TOLERANCE, 2)), "%"));
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/method/offer/CreateXMROffersTest.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.apitest.method.offer;
import haveno.proto.grpc.OfferInfo;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import java.util.List;
import static haveno.apitest.config.ApiTestConfig.BTC;
import static haveno.apitest.config.ApiTestConfig.XMR;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static protobuf.OfferDirection.BUY;
import static protobuf.OfferDirection.SELL;
@SuppressWarnings("ConstantConditions")
@Disabled
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class CreateXMROffersTest extends AbstractOfferTest {
private static final String MAKER_FEE_CURRENCY_CODE = BTC;
@BeforeAll
public static void setUp() {
AbstractOfferTest.setUp();
createXmrPaymentAccounts();
}
@Test
@Order(1)
public void testCreateFixedPriceBuy1BTCFor200KXMROffer() {
// Remember alt coin trades are BTC trades. When placing an offer, you are
// offering to buy or sell BTC, not ETH, XMR, etc. In this test case,
// Alice places an offer to BUY BTC.
var newOffer = aliceClient.createFixedPricedOffer(BUY.name(),
XMR,
100_000_000L,
75_000_000L,
"0.005", // FIXED PRICE IN BTC FOR 1 XMR
defaultSecurityDepositPct.get(),
alicesXmrAcct.getId());
log.debug("Sell XMR (Buy BTC) offer:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer());
assertFalse(newOffer.getIsActivated());
String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId);
assertEquals(BUY.name(), newOffer.getDirection());
assertFalse(newOffer.getUseMarketBasedPrice());
assertEquals("0.00500000", newOffer.getPrice());
assertEquals(100_000_000L, newOffer.getAmount());
assertEquals(75_000_000L, newOffer.getMinAmount());
assertEquals(.15, newOffer.getBuyerSecurityDepositPct());
assertEquals(.15, newOffer.getSellerSecurityDepositPct());
assertEquals(alicesXmrAcct.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode());
assertEquals(BTC, newOffer.getCounterCurrencyCode());
genBtcBlockAndWaitForOfferPreparation();
newOffer = aliceClient.getOffer(newOfferId);
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsActivated());
assertEquals(newOfferId, newOffer.getId());
assertEquals(BUY.name(), newOffer.getDirection());
assertFalse(newOffer.getUseMarketBasedPrice());
assertEquals("0.00500000", newOffer.getPrice());
assertEquals(100_000_000L, newOffer.getAmount());
assertEquals(75_000_000L, newOffer.getMinAmount());
assertEquals(.15, newOffer.getBuyerSecurityDepositPct());
assertEquals(.15, newOffer.getSellerSecurityDepositPct());
assertEquals(alicesXmrAcct.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode());
assertEquals(BTC, newOffer.getCounterCurrencyCode());
}
@Test
@Order(2)
public void testCreateFixedPriceSell1BTCFor200KXMROffer() {
// Alice places an offer to SELL BTC for XMR.
var newOffer = aliceClient.createFixedPricedOffer(SELL.name(),
XMR,
100_000_000L,
50_000_000L,
"0.005", // FIXED PRICE IN BTC (satoshis) FOR 1 XMR
defaultSecurityDepositPct.get(),
alicesXmrAcct.getId());
log.debug("Buy XMR (Sell BTC) offer:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer());
assertFalse(newOffer.getIsActivated());
String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId);
assertEquals(SELL.name(), newOffer.getDirection());
assertFalse(newOffer.getUseMarketBasedPrice());
assertEquals("0.00500000", newOffer.getPrice());
assertEquals(100_000_000L, newOffer.getAmount());
assertEquals(50_000_000L, newOffer.getMinAmount());
assertEquals(.15, newOffer.getBuyerSecurityDepositPct());
assertEquals(.15, newOffer.getSellerSecurityDepositPct());
assertEquals(alicesXmrAcct.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode());
assertEquals(BTC, newOffer.getCounterCurrencyCode());
genBtcBlockAndWaitForOfferPreparation();
newOffer = aliceClient.getOffer(newOfferId);
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsActivated());
assertEquals(newOfferId, newOffer.getId());
assertEquals(SELL.name(), newOffer.getDirection());
assertFalse(newOffer.getUseMarketBasedPrice());
assertEquals("0.00500000", newOffer.getPrice());
assertEquals(100_000_000L, newOffer.getAmount());
assertEquals(50_000_000L, newOffer.getMinAmount());
assertEquals(.15, newOffer.getBuyerSecurityDepositPct());
assertEquals(.15, newOffer.getSellerSecurityDepositPct());
assertEquals(alicesXmrAcct.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode());
assertEquals(BTC, newOffer.getCounterCurrencyCode());
}
@Test
@Order(3)
public void testCreatePriceMarginBasedBuy1BTCOfferWithTriggerPrice() {
double priceMarginPctInput = 1.00;
double mktPriceAsDouble = aliceClient.getBtcPrice(XMR);
String triggerPrice = calcPriceAsString(mktPriceAsDouble, Double.parseDouble("-0.001"), 8);
var newOffer = aliceClient.createMarketBasedPricedOffer(BUY.name(),
XMR,
100_000_000L,
75_000_000L,
priceMarginPctInput,
defaultSecurityDepositPct.get(),
alicesXmrAcct.getId(),
triggerPrice);
log.debug("Pending Sell XMR (Buy BTC) offer:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer());
assertFalse(newOffer.getIsActivated());
String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId);
assertEquals(BUY.name(), newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice());
// There is no trigger price while offer is pending.
assertEquals(NO_TRIGGER_PRICE, newOffer.getTriggerPrice());
assertEquals(100_000_000L, newOffer.getAmount());
assertEquals(75_000_000L, newOffer.getMinAmount());
assertEquals(.15, newOffer.getBuyerSecurityDepositPct());
assertEquals(.15, newOffer.getSellerSecurityDepositPct());
assertEquals(alicesXmrAcct.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode());
assertEquals(BTC, newOffer.getCounterCurrencyCode());
genBtcBlockAndWaitForOfferPreparation();
newOffer = aliceClient.getOffer(newOfferId);
log.debug("Available Sell XMR (Buy BTC) offer:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsActivated());
assertEquals(newOfferId, newOffer.getId());
assertEquals(BUY.name(), newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice());
// The trigger price should exist on the prepared offer.
assertEquals(triggerPrice, newOffer.getTriggerPrice());
assertEquals(100_000_000L, newOffer.getAmount());
assertEquals(75_000_000L, newOffer.getMinAmount());
assertEquals(.15, newOffer.getBuyerSecurityDepositPct());
assertEquals(.15, newOffer.getSellerSecurityDepositPct());
assertEquals(alicesXmrAcct.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode());
assertEquals(BTC, newOffer.getCounterCurrencyCode());
}
@Test
@Order(4)
public void testCreatePriceMarginBasedSell1BTCOffer() {
// Alice places an offer to SELL BTC for XMR.
double priceMarginPctInput = 0.50;
var newOffer = aliceClient.createMarketBasedPricedOffer(SELL.name(),
XMR,
100_000_000L,
50_000_000L,
priceMarginPctInput,
defaultSecurityDepositPct.get(),
alicesXmrAcct.getId(),
NO_TRIGGER_PRICE);
log.debug("Buy XMR (Sell BTC) offer:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer());
assertFalse(newOffer.getIsActivated());
String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId);
assertEquals(SELL.name(), newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice());
assertEquals(100_000_000L, newOffer.getAmount());
assertEquals(50_000_000L, newOffer.getMinAmount());
assertEquals(.15, newOffer.getBuyerSecurityDepositPct());
assertEquals(.15, newOffer.getSellerSecurityDepositPct());
assertEquals(alicesXmrAcct.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode());
assertEquals(BTC, newOffer.getCounterCurrencyCode());
genBtcBlockAndWaitForOfferPreparation();
newOffer = aliceClient.getOffer(newOfferId);
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsActivated());
assertEquals(newOfferId, newOffer.getId());
assertEquals(SELL.name(), newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice());
assertEquals(100_000_000L, newOffer.getAmount());
assertEquals(50_000_000L, newOffer.getMinAmount());
assertEquals(.15, newOffer.getBuyerSecurityDepositPct());
assertEquals(.15, newOffer.getSellerSecurityDepositPct());
assertEquals(alicesXmrAcct.getId(), newOffer.getPaymentAccountId());
assertEquals(XMR, newOffer.getBaseCurrencyCode());
assertEquals(BTC, newOffer.getCounterCurrencyCode());
}
@Test
@Order(5)
public void testGetAllMyXMROffers() {
List offers = aliceClient.getMyOffersSortedByDate(XMR);
log.debug("All of Alice's XMR offers:\n{}", toOffersTable.apply(offers));
assertEquals(4, offers.size());
log.debug("Alice's balances\n{}", formatBalancesTbls(aliceClient.getBalances()));
}
@Test
@Order(6)
public void testGetAvailableXMROffers() {
List offers = bobClient.getOffersSortedByDate(XMR);
log.debug("All of Bob's available XMR offers:\n{}", toOffersTable.apply(offers));
assertEquals(4, offers.size());
log.debug("Bob's balances\n{}", formatBalancesTbls(bobClient.getBalances()));
}
private void genBtcBlockAndWaitForOfferPreparation() {
genBtcBlocksThenWait(1, 5000);
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/method/offer/ValidateCreateOfferTest.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.apitest.method.offer;
import haveno.core.payment.PaymentAccount;
import io.grpc.StatusRuntimeException;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import static java.lang.String.format;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static protobuf.OfferDirection.BUY;
@Disabled
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class ValidateCreateOfferTest extends AbstractOfferTest {
@Test
@Order(1)
public void testAmtTooLargeShouldThrowException() {
PaymentAccount usdAccount = createDummyF2FAccount(aliceClient, "US");
@SuppressWarnings("ResultOfMethodCallIgnored")
Throwable exception = assertThrows(StatusRuntimeException.class, () ->
aliceClient.createFixedPricedOffer(BUY.name(),
"usd",
100000000000L, // exceeds amount limit
100000000000L,
"10000.0000",
defaultSecurityDepositPct.get(),
usdAccount.getId()));
assertEquals("UNKNOWN: An error occurred at task: ValidateOffer", exception.getMessage());
}
@Test
@Order(2)
public void testNoMatchingEURPaymentAccountShouldThrowException() {
PaymentAccount chfAccount = createDummyF2FAccount(aliceClient, "ch");
@SuppressWarnings("ResultOfMethodCallIgnored")
Throwable exception = assertThrows(StatusRuntimeException.class, () ->
aliceClient.createFixedPricedOffer(BUY.name(),
"eur",
10000000L,
10000000L,
"40000.0000",
defaultSecurityDepositPct.get(),
chfAccount.getId()));
String expectedError = format("UNKNOWN: cannot create EUR offer with payment account %s", chfAccount.getId());
assertEquals(expectedError, exception.getMessage());
}
@Test
@Order(2)
public void testNoMatchingCADPaymentAccountShouldThrowException() {
PaymentAccount audAccount = createDummyF2FAccount(aliceClient, "au");
@SuppressWarnings("ResultOfMethodCallIgnored")
Throwable exception = assertThrows(StatusRuntimeException.class, () ->
aliceClient.createFixedPricedOffer(BUY.name(),
"cad",
10000000L,
10000000L,
"63000.0000",
defaultSecurityDepositPct.get(),
audAccount.getId()));
String expectedError = format("UNKNOWN: cannot create CAD offer with payment account %s", audAccount.getId());
assertEquals(expectedError, exception.getMessage());
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/method/payment/AbstractPaymentAccountTest.java
================================================
package haveno.apitest.method.payment;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.stream.JsonWriter;
import haveno.apitest.method.MethodTest;
import haveno.cli.GrpcClient;
import haveno.core.api.model.PaymentAccountForm;
import haveno.core.locale.TraditionalCurrency;
import haveno.core.locale.Res;
import haveno.core.locale.TradeCurrency;
import haveno.core.payment.PaymentAccount;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestInfo;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import static java.lang.String.format;
import static java.lang.System.getProperty;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
@Slf4j
public class AbstractPaymentAccountTest extends MethodTest {
static final String PROPERTY_NAME_JSON_COMMENTS = "_COMMENTS_";
static final List PROPERTY_VALUE_JSON_COMMENTS = new ArrayList<>() {{
add("Do not manually edit the paymentMethodId field.");
add("Edit the salt field only if you are recreating a payment"
+ " account on a new installation and wish to preserve the account age.");
}};
static final String PROPERTY_NAME_PAYMENT_METHOD_ID = "paymentMethodId";
static final String PROPERTY_NAME_ACCOUNT_ID = "accountId";
static final String PROPERTY_NAME_ACCOUNT_NAME = "accountName";
static final String PROPERTY_NAME_ACCOUNT_NR = "accountNr";
static final String PROPERTY_NAME_ACCOUNT_TYPE = "accountType";
static final String PROPERTY_NAME_ANSWER = "answer";
static final String PROPERTY_NAME_BANK_ACCOUNT_NAME = "bankAccountName";
static final String PROPERTY_NAME_BANK_ACCOUNT_NUMBER = "bankAccountNumber";
static final String PROPERTY_NAME_BANK_ACCOUNT_TYPE = "bankAccountType";
static final String PROPERTY_NAME_BANK_ADDRESS = "bankAddress";
static final String PROPERTY_NAME_BANK_BRANCH = "bankBranch";
static final String PROPERTY_NAME_BANK_BRANCH_CODE = "bankBranchCode";
static final String PROPERTY_NAME_BANK_BRANCH_NAME = "bankBranchName";
static final String PROPERTY_NAME_BANK_CODE = "bankCode";
static final String PROPERTY_NAME_BANK_COUNTRY_CODE = "bankCountryCode";
@SuppressWarnings("unused")
static final String PROPERTY_NAME_BANK_ID = "bankId";
static final String PROPERTY_NAME_BANK_NAME = "bankName";
static final String PROPERTY_NAME_BANK_SWIFT_CODE = "bankSwiftCode";
static final String PROPERTY_NAME_BRANCH_ID = "branchId";
static final String PROPERTY_NAME_BIC = "bic";
static final String PROPERTY_NAME_BENEFICIARY_NAME = "beneficiaryName";
static final String PROPERTY_NAME_BENEFICIARY_ACCOUNT_NR = "beneficiaryAccountNr";
static final String PROPERTY_NAME_BENEFICIARY_ADDRESS = "beneficiaryAddress";
static final String PROPERTY_NAME_BENEFICIARY_CITY = "beneficiaryCity";
static final String PROPERTY_NAME_BENEFICIARY_PHONE = "beneficiaryPhone";
static final String PROPERTY_NAME_COUNTRY = "country";
static final String PROPERTY_NAME_CITY = "city";
static final String PROPERTY_NAME_CONTACT = "contact";
static final String PROPERTY_NAME_EMAIL = "email";
static final String PROPERTY_NAME_EMAIL_OR_MOBILE_NR = "emailOrMobileNr";
static final String PROPERTY_NAME_EXTRA_INFO = "extraInfo";
static final String PROPERTY_NAME_HOLDER_EMAIL = "holderEmail";
static final String PROPERTY_NAME_HOLDER_NAME = "holderName";
static final String PROPERTY_NAME_HOLDER_TAX_ID = "holderTaxId";
static final String PROPERTY_NAME_IBAN = "iban";
static final String PROPERTY_NAME_INTERMEDIARY_ADDRESS = "intermediaryAddress";
static final String PROPERTY_NAME_INTERMEDIARY_BRANCH = "intermediaryBranch";
static final String PROPERTY_NAME_INTERMEDIARY_COUNTRY_CODE = "intermediaryCountryCode";
static final String PROPERTY_NAME_INTERMEDIARY_NAME = "intermediaryName";
static final String PROPERTY_NAME_INTERMEDIARY_SWIFT_CODE = "intermediarySwiftCode";
static final String PROPERTY_NAME_MOBILE_NR = "mobileNr";
static final String PROPERTY_NAME_NATIONAL_ACCOUNT_ID = "nationalAccountId";
static final String PROPERTY_NAME_PAY_ID = "payid";
static final String PROPERTY_NAME_POSTAL_ADDRESS = "postalAddress";
static final String PROPERTY_NAME_PROMPT_PAY_ID = "promptPayId";
static final String PROPERTY_NAME_QUESTION = "question";
static final String PROPERTY_NAME_REQUIREMENTS = "requirements";
static final String PROPERTY_NAME_SALT = "salt";
static final String PROPERTY_NAME_SELECTED_TRADE_CURRENCY = "selectedTradeCurrency";
static final String PROPERTY_NAME_SORT_CODE = "sortCode";
static final String PROPERTY_NAME_SPECIAL_INSTRUCTIONS = "specialInstructions";
static final String PROPERTY_NAME_STATE = "state";
static final String PROPERTY_NAME_TRADE_CURRENCIES = "tradeCurrencies";
static final String PROPERTY_NAME_USERNAME = "userName";
static final Gson GSON = new GsonBuilder()
.setPrettyPrinting()
.serializeNulls()
.create();
static final Map COMPLETED_FORM_MAP = new HashMap<>();
@BeforeEach
public void setup() {
Res.setup();
}
protected final File getEmptyForm(TestInfo testInfo, String paymentMethodId) {
// This would normally be done in @BeforeEach, but these test cases might be
// called from a single 'scenario' test case, and the @BeforeEach -> clear()
// would be skipped.
COMPLETED_FORM_MAP.clear();
File emptyForm = getPaymentAccountForm(aliceClient, paymentMethodId);
// A shortcut over the API:
// File emptyForm = PAYMENT_ACCOUNT_FORM.getPaymentAccountForm(paymentMethodId);
log.debug("{} Empty form saved to {}",
testName(testInfo),
PaymentAccountForm.getClickableURI(emptyForm));
emptyForm.deleteOnExit();
return emptyForm;
}
protected final void verifyEmptyForm(File jsonForm, String paymentMethodId, String... fields) {
@SuppressWarnings("unchecked")
Map emptyForm = (Map) GSON.fromJson(
PaymentAccountForm.toJsonString(jsonForm),
Object.class);
assertNotNull(emptyForm);
if (paymentMethodId.equals("SWIFT_ID")) {
assertEquals(getSwiftFormComments(), emptyForm.get(PROPERTY_NAME_JSON_COMMENTS));
} else {
assertEquals(PROPERTY_VALUE_JSON_COMMENTS, emptyForm.get(PROPERTY_NAME_JSON_COMMENTS));
}
assertEquals(paymentMethodId, emptyForm.get(PROPERTY_NAME_PAYMENT_METHOD_ID));
assertEquals("your accountname", emptyForm.get(PROPERTY_NAME_ACCOUNT_NAME));
for (String field : fields) {
if (field.equals("country"))
assertEquals("your two letter country code", emptyForm.get(field));
else
assertEquals("your " + field.toLowerCase(), emptyForm.get(field));
}
}
protected final void verifyCommonFormEntries(PaymentAccount paymentAccount) {
// All PaymentAccount subclasses have paymentMethodId and an accountName fields.
assertNotNull(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_PAYMENT_METHOD_ID), paymentAccount.getPaymentMethod().getId());
assertTrue(paymentAccount.getCreationDate().getTime() > 0);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NAME), paymentAccount.getAccountName());
}
protected final void verifyAccountSingleTradeCurrency(String expectedCurrencyCode, PaymentAccount paymentAccount) {
assertNotNull(paymentAccount.getSingleTradeCurrency());
assertEquals(expectedCurrencyCode, paymentAccount.getSingleTradeCurrency().getCode());
}
protected final void verifyAccountTradeCurrencies(Collection expectedTraditionalCurrencies,
PaymentAccount paymentAccount) {
assertNotNull(paymentAccount.getTradeCurrencies());
List expectedTradeCurrencies = new ArrayList<>() {{
addAll(expectedTraditionalCurrencies);
}};
assertArrayEquals(expectedTradeCurrencies.toArray(), paymentAccount.getTradeCurrencies().toArray());
}
protected final void verifyAccountTradeCurrencies(List expectedTradeCurrencies,
PaymentAccount paymentAccount) {
assertNotNull(paymentAccount.getTradeCurrencies());
assertArrayEquals(expectedTradeCurrencies.toArray(), paymentAccount.getTradeCurrencies().toArray());
}
protected final void verifyUserPayloadHasPaymentAccountWithId(GrpcClient grpcClient,
String paymentAccountId) {
Optional paymentAccount = grpcClient.getPaymentAccounts()
.stream()
.filter(a -> a.getId().equals(paymentAccountId))
.findFirst();
assertTrue(paymentAccount.isPresent());
}
protected final String getCompletedFormAsJsonString(List comments) {
File completedForm = fillPaymentAccountForm(comments);
String jsonString = PaymentAccountForm.toJsonString(completedForm);
log.debug("Completed form: {}", jsonString);
return jsonString;
}
protected final String getCompletedFormAsJsonString() {
File completedForm = fillPaymentAccountForm(PROPERTY_VALUE_JSON_COMMENTS);
String jsonString = PaymentAccountForm.toJsonString(completedForm);
log.debug("Completed form: {}", jsonString);
return jsonString;
}
protected final String getCommaDelimitedTraditionalCurrencyCodes(Collection traditionalCurrencies) {
return traditionalCurrencies.stream()
.sorted(Comparator.comparing(TradeCurrency::getCode))
.map(c -> c.getCode())
.collect(Collectors.joining(","));
}
protected final List getSwiftFormComments() {
List comments = new ArrayList<>();
comments.addAll(PROPERTY_VALUE_JSON_COMMENTS);
List wrappedSwiftComments = Res.getWrappedAsList("payment.swift.info.account", 110);
comments.addAll(wrappedSwiftComments);
return comments;
}
private File fillPaymentAccountForm(List comments) {
File tmpJsonForm = null;
try {
tmpJsonForm = File.createTempFile("temp_acct_form_",
".json",
Paths.get(getProperty("java.io.tmpdir")).toFile());
JsonWriter writer = new JsonWriter(new OutputStreamWriter(new FileOutputStream(tmpJsonForm), UTF_8));
writer.beginObject();
writer.name(PROPERTY_NAME_JSON_COMMENTS);
writer.beginArray();
for (String s : comments) {
writer.value(s);
}
writer.endArray();
for (Map.Entry entry : COMPLETED_FORM_MAP.entrySet()) {
String k = entry.getKey();
Object v = entry.getValue();
writer.name(k);
writer.value(v.toString());
}
writer.endObject();
writer.close();
} catch (IOException ex) {
log.error("", ex);
fail(format("Could not write json file from form entries %s", COMPLETED_FORM_MAP));
}
tmpJsonForm.deleteOnExit();
return tmpJsonForm;
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/method/payment/CreatePaymentAccountTest.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.apitest.method.payment;
import haveno.cli.table.builder.TableBuilder;
import haveno.core.locale.TraditionalCurrency;
import haveno.core.locale.TradeCurrency;
import haveno.core.payment.AdvancedCashAccount;
import haveno.core.payment.AliPayAccount;
import haveno.core.payment.AustraliaPayidAccount;
import haveno.core.payment.CapitualAccount;
import haveno.core.payment.CashDepositAccount;
import haveno.core.payment.ZelleAccount;
import haveno.core.payment.F2FAccount;
import haveno.core.payment.FasterPaymentsAccount;
import haveno.core.payment.HalCashAccount;
import haveno.core.payment.InteracETransferAccount;
import haveno.core.payment.JapanBankAccount;
import haveno.core.payment.MoneyBeamAccount;
import haveno.core.payment.MoneyGramAccount;
import haveno.core.payment.NationalBankAccount;
import haveno.core.payment.PaxumAccount;
import haveno.core.payment.PaymentAccount;
import haveno.core.payment.PayseraAccount;
import haveno.core.payment.PerfectMoneyAccount;
import haveno.core.payment.PopmoneyAccount;
import haveno.core.payment.PromptPayAccount;
import haveno.core.payment.RevolutAccount;
import haveno.core.payment.SameBankAccount;
import haveno.core.payment.SepaAccount;
import haveno.core.payment.SepaInstantAccount;
import haveno.core.payment.SpecificBanksAccount;
import haveno.core.payment.SwiftAccount;
import haveno.core.payment.SwishAccount;
import haveno.core.payment.TransferwiseAccount;
import haveno.core.payment.USPostalMoneyOrderAccount;
import haveno.core.payment.UpholdAccount;
import haveno.core.payment.WeChatPayAccount;
import haveno.core.payment.WesternUnionAccount;
import haveno.core.payment.payload.BankAccountPayload;
import haveno.core.payment.payload.CashDepositAccountPayload;
import haveno.core.payment.payload.SameBankAccountPayload;
import haveno.core.payment.payload.SpecificBanksAccountPayload;
import haveno.core.payment.payload.SwiftAccountPayload;
import io.grpc.StatusRuntimeException;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestMethodOrder;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import static haveno.apitest.Scaffold.BitcoinCoreApp.bitcoind;
import static haveno.apitest.config.ApiTestConfig.EUR;
import static haveno.apitest.config.ApiTestConfig.USD;
import static haveno.apitest.config.HavenoAppConfig.alicedaemon;
import static haveno.cli.table.builder.TableType.PAYMENT_ACCOUNT_TBL;
import static haveno.core.locale.CurrencyUtil.getAllSortedTraditionalCurrencies;
import static haveno.core.locale.CurrencyUtil.getTradeCurrency;
import static haveno.core.payment.payload.PaymentMethod.ADVANCED_CASH_ID;
import static haveno.core.payment.payload.PaymentMethod.ALI_PAY_ID;
import static haveno.core.payment.payload.PaymentMethod.AUSTRALIA_PAYID_ID;
import static haveno.core.payment.payload.PaymentMethod.CAPITUAL_ID;
import static haveno.core.payment.payload.PaymentMethod.CASH_DEPOSIT_ID;
import static haveno.core.payment.payload.PaymentMethod.ZELLE_ID;
import static haveno.core.payment.payload.PaymentMethod.F2F_ID;
import static haveno.core.payment.payload.PaymentMethod.FASTER_PAYMENTS_ID;
import static haveno.core.payment.payload.PaymentMethod.HAL_CASH_ID;
import static haveno.core.payment.payload.PaymentMethod.INTERAC_E_TRANSFER_ID;
import static haveno.core.payment.payload.PaymentMethod.JAPAN_BANK_ID;
import static haveno.core.payment.payload.PaymentMethod.MONEY_BEAM_ID;
import static haveno.core.payment.payload.PaymentMethod.MONEY_GRAM_ID;
import static haveno.core.payment.payload.PaymentMethod.NATIONAL_BANK_ID;
import static haveno.core.payment.payload.PaymentMethod.PAXUM_ID;
import static haveno.core.payment.payload.PaymentMethod.PAYSERA_ID;
import static haveno.core.payment.payload.PaymentMethod.PERFECT_MONEY_ID;
import static haveno.core.payment.payload.PaymentMethod.POPMONEY_ID;
import static haveno.core.payment.payload.PaymentMethod.PROMPT_PAY_ID;
import static haveno.core.payment.payload.PaymentMethod.REVOLUT_ID;
import static haveno.core.payment.payload.PaymentMethod.SAME_BANK_ID;
import static haveno.core.payment.payload.PaymentMethod.SEPA_ID;
import static haveno.core.payment.payload.PaymentMethod.SEPA_INSTANT_ID;
import static haveno.core.payment.payload.PaymentMethod.SPECIFIC_BANKS_ID;
import static haveno.core.payment.payload.PaymentMethod.SWIFT_ID;
import static haveno.core.payment.payload.PaymentMethod.SWISH_ID;
import static haveno.core.payment.payload.PaymentMethod.TRANSFERWISE_ID;
import static haveno.core.payment.payload.PaymentMethod.UPHOLD_ID;
import static haveno.core.payment.payload.PaymentMethod.US_POSTAL_MONEY_ORDER_ID;
import static haveno.core.payment.payload.PaymentMethod.WECHAT_PAY_ID;
import static haveno.core.payment.payload.PaymentMethod.WESTERN_UNION_ID;
import static java.util.Comparator.comparing;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
@SuppressWarnings({"OptionalGetWithoutIsPresent", "ConstantConditions"})
@Disabled
@Slf4j
@TestMethodOrder(OrderAnnotation.class)
public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
@BeforeAll
public static void setUp() {
try {
setUpScaffold(bitcoind, alicedaemon);
} catch (Exception ex) {
fail(ex);
}
}
@Test
public void testCreateAdvancedCashAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, ADVANCED_CASH_ID);
verifyEmptyForm(emptyForm,
ADVANCED_CASH_ID,
PROPERTY_NAME_ACCOUNT_NR);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, ADVANCED_CASH_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Advanced Cash Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "0000 1111 2222");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, AdvancedCashAccount.SUPPORTED_CURRENCIES
.stream()
.map(TradeCurrency::getCode)
.collect(Collectors.joining(",")));
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SELECTED_TRADE_CURRENCY, "RUB");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Advanced Cash Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
AdvancedCashAccount paymentAccount = (AdvancedCashAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountTradeCurrencies(AdvancedCashAccount.SUPPORTED_CURRENCIES, paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SELECTED_TRADE_CURRENCY),
paymentAccount.getSelectedTradeCurrency().getCode());
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
print(paymentAccount);
}
@Test
public void testCreateAliPayAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, ALI_PAY_ID);
verifyEmptyForm(emptyForm,
ALI_PAY_ID,
PROPERTY_NAME_ACCOUNT_NR);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, ALI_PAY_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Ali Pay Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "2222 3333 4444");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
AliPayAccount paymentAccount = (AliPayAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("CNY", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr());
print(paymentAccount);
}
@Test
public void testCreateAustraliaPayidAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, AUSTRALIA_PAYID_ID);
verifyEmptyForm(emptyForm,
AUSTRALIA_PAYID_ID,
PROPERTY_NAME_BANK_ACCOUNT_NAME);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, AUSTRALIA_PAYID_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Australia Pay ID Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAY_ID, "123 456 789");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_ACCOUNT_NAME, "Credit Union Australia");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Australia Pay ID Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
AustraliaPayidAccount paymentAccount = (AustraliaPayidAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("AUD", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_PAY_ID), paymentAccount.getPayid());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_ACCOUNT_NAME), paymentAccount.getBankAccountName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
print(paymentAccount);
}
@Test
public void testCreateCapitualAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, CAPITUAL_ID);
verifyEmptyForm(emptyForm,
CAPITUAL_ID,
PROPERTY_NAME_ACCOUNT_NR);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, CAPITUAL_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Capitual Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "1111 2222 3333-4");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, CapitualAccount.SUPPORTED_CURRENCIES
.stream()
.map(TradeCurrency::getCode)
.collect(Collectors.joining(",")));
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SELECTED_TRADE_CURRENCY, "BRL");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Capitual Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
CapitualAccount paymentAccount = (CapitualAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountTradeCurrencies(CapitualAccount.SUPPORTED_CURRENCIES, paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SELECTED_TRADE_CURRENCY),
paymentAccount.getSelectedTradeCurrency().getCode());
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
print(paymentAccount);
}
@Test
public void testCreateCashDepositAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, CASH_DEPOSIT_ID);
verifyEmptyForm(emptyForm,
CASH_DEPOSIT_ID,
PROPERTY_NAME_ACCOUNT_NR,
PROPERTY_NAME_ACCOUNT_TYPE,
PROPERTY_NAME_BANK_ID,
PROPERTY_NAME_BANK_NAME,
PROPERTY_NAME_BRANCH_ID,
PROPERTY_NAME_COUNTRY,
PROPERTY_NAME_HOLDER_EMAIL,
PROPERTY_NAME_HOLDER_NAME,
PROPERTY_NAME_HOLDER_TAX_ID,
PROPERTY_NAME_NATIONAL_ACCOUNT_ID,
PROPERTY_NAME_REQUIREMENTS);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, CASH_DEPOSIT_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Cash Deposit Account");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "4444 5555 6666");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_TYPE, "Checking");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_ID, "0001");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_NAME, "BoF");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BRANCH_ID, "99-8888-7654");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "FR");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_EMAIL, "jean@johnson.info");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jean Johnson");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_TAX_ID, "123456789");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_NATIONAL_ACCOUNT_ID, "123456789");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_REQUIREMENTS, "Requirements...");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
CashDepositAccount paymentAccount = (CashDepositAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency(EUR, paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
Objects.requireNonNull(paymentAccount.getCountry()).code);
CashDepositAccountPayload payload = (CashDepositAccountPayload) paymentAccount.getPaymentAccountPayload();
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), payload.getAccountNr());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_TYPE), payload.getAccountType());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_ID), payload.getBankId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), payload.getBankName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BRANCH_ID), payload.getBranchId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_EMAIL), payload.getHolderEmail());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), payload.getHolderName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_TAX_ID), payload.getHolderTaxId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_NATIONAL_ACCOUNT_ID), payload.getNationalAccountId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_REQUIREMENTS), payload.getRequirements());
print(paymentAccount);
}
@Test
public void testCreateBrazilNationalBankAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, NATIONAL_BANK_ID);
verifyEmptyForm(emptyForm,
NATIONAL_BANK_ID,
PROPERTY_NAME_ACCOUNT_NR,
PROPERTY_NAME_ACCOUNT_TYPE,
PROPERTY_NAME_BANK_NAME,
PROPERTY_NAME_BRANCH_ID,
PROPERTY_NAME_COUNTRY,
PROPERTY_NAME_HOLDER_NAME,
PROPERTY_NAME_HOLDER_TAX_ID,
PROPERTY_NAME_NATIONAL_ACCOUNT_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, NATIONAL_BANK_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Banco do Brasil");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "456789-87");
// No BankId is required for BR.
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_NAME, "Banco do Brasil");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BRANCH_ID, "456789-10");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "BR");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Joao da Silva");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_TAX_ID, "123456789");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_NATIONAL_ACCOUNT_ID, "123456789");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Banco do Brasil Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
NationalBankAccount paymentAccount = (NationalBankAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("BRL", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
Objects.requireNonNull(paymentAccount.getCountry()).code);
BankAccountPayload payload = (BankAccountPayload) paymentAccount.getPaymentAccountPayload();
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), payload.getAccountNr());
// When no BankId is required, getBankId() returns bankName.
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), payload.getBankId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), payload.getBankName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BRANCH_ID), payload.getBranchId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), payload.getHolderName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_TAX_ID), payload.getHolderTaxId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_NATIONAL_ACCOUNT_ID), payload.getNationalAccountId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
print(paymentAccount);
}
@Test
public void testCreateZelleAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, ZELLE_ID);
verifyEmptyForm(emptyForm,
ZELLE_ID,
PROPERTY_NAME_EMAIL_OR_MOBILE_NR,
PROPERTY_NAME_HOLDER_NAME);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, ZELLE_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "USD Zelle Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL_OR_MOBILE_NR, "jane@doe.com");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jane Doe");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Zelle Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
ZelleAccount paymentAccount = (ZelleAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency(USD, paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL_OR_MOBILE_NR), paymentAccount.getEmailOrMobileNr());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
print(paymentAccount);
}
@Test
public void testCreateF2FAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, F2F_ID);
verifyEmptyForm(emptyForm,
F2F_ID,
PROPERTY_NAME_COUNTRY,
PROPERTY_NAME_CITY,
PROPERTY_NAME_CONTACT,
PROPERTY_NAME_EXTRA_INFO);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, F2F_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Conta Cara a Cara");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "BR");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_CITY, "Rio de Janeiro");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_CONTACT, "Freddy Beira Mar");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_EXTRA_INFO, "So fim de semana");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
F2FAccount paymentAccount = (F2FAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("BRL", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
Objects.requireNonNull(paymentAccount.getCountry()).code);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_CITY), paymentAccount.getCity());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_CONTACT), paymentAccount.getContact());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EXTRA_INFO), paymentAccount.getExtraInfo());
print(paymentAccount);
}
@Test
public void testCreateFasterPaymentsAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, FASTER_PAYMENTS_ID);
verifyEmptyForm(emptyForm,
FASTER_PAYMENTS_ID,
PROPERTY_NAME_ACCOUNT_NR,
PROPERTY_NAME_SORT_CODE);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, FASTER_PAYMENTS_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Faster Payments Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "9999 8888 7777");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SORT_CODE, "3127");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Faster Payments Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
FasterPaymentsAccount paymentAccount = (FasterPaymentsAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("GBP", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SORT_CODE), paymentAccount.getSortCode());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
print(paymentAccount);
}
@Test
public void testCreateHalCashAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, HAL_CASH_ID);
verifyEmptyForm(emptyForm,
HAL_CASH_ID,
PROPERTY_NAME_MOBILE_NR);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, HAL_CASH_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Hal Cash Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_MOBILE_NR, "798 123 456");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
HalCashAccount paymentAccount = (HalCashAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency(EUR, paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_MOBILE_NR), paymentAccount.getMobileNr());
print(paymentAccount);
}
@Test
public void testCreateInteracETransferAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, INTERAC_E_TRANSFER_ID);
verifyEmptyForm(emptyForm,
INTERAC_E_TRANSFER_ID,
PROPERTY_NAME_HOLDER_NAME,
PROPERTY_NAME_EMAIL,
PROPERTY_NAME_QUESTION,
PROPERTY_NAME_ANSWER);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, INTERAC_E_TRANSFER_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Interac Transfer Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "John Doe");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "john@doe.info");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_QUESTION, "What is my dog's name?");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ANSWER, "Fido");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Interac Transfer Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
InteracETransferAccount paymentAccount = (InteracETransferAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("CAD", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_QUESTION), paymentAccount.getQuestion());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ANSWER), paymentAccount.getAnswer());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
print(paymentAccount);
}
@Test
public void testCreateJapanBankAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, JAPAN_BANK_ID);
verifyEmptyForm(emptyForm,
JAPAN_BANK_ID,
PROPERTY_NAME_BANK_NAME,
PROPERTY_NAME_BANK_CODE,
PROPERTY_NAME_BANK_BRANCH_CODE,
PROPERTY_NAME_BANK_BRANCH_NAME,
PROPERTY_NAME_BANK_ACCOUNT_NAME,
PROPERTY_NAME_BANK_ACCOUNT_TYPE,
PROPERTY_NAME_BANK_ACCOUNT_NUMBER);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, JAPAN_BANK_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Fukuoka Account");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_NAME, "Bank of Kyoto");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_CODE, "FKBKJPJT");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_BRANCH_CODE, "8100-8727");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_BRANCH_NAME, "Fukuoka Branch");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_ACCOUNT_NAME, "Fukuoka Account");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_ACCOUNT_TYPE, "Yen Account");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_ACCOUNT_NUMBER, "8100-8727-0000");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
JapanBankAccount paymentAccount = (JapanBankAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("JPY", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_CODE), paymentAccount.getBankCode());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), paymentAccount.getBankName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_BRANCH_CODE), paymentAccount.getBankBranchCode());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_BRANCH_NAME), paymentAccount.getBankBranchName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_ACCOUNT_NAME), paymentAccount.getBankAccountName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_ACCOUNT_TYPE), paymentAccount.getBankAccountType());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_ACCOUNT_NUMBER), paymentAccount.getBankAccountNumber());
print(paymentAccount);
}
@Test
public void testCreateMoneyBeamAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, MONEY_BEAM_ID);
verifyEmptyForm(emptyForm,
MONEY_BEAM_ID,
PROPERTY_NAME_ACCOUNT_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, MONEY_BEAM_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Money Beam Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_ID, "MB 0000 1111");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Money Beam Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
MoneyBeamAccount paymentAccount = (MoneyBeamAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency(EUR, paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_ID), paymentAccount.getAccountId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
print(paymentAccount);
}
@Test
public void testCreateMoneyGramAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, MONEY_GRAM_ID);
verifyEmptyForm(emptyForm,
MONEY_GRAM_ID,
PROPERTY_NAME_HOLDER_NAME,
PROPERTY_NAME_EMAIL,
PROPERTY_NAME_COUNTRY,
PROPERTY_NAME_STATE);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, MONEY_GRAM_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Money Gram Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, MoneyGramAccount.SUPPORTED_CURRENCIES
.stream()
.map(TradeCurrency::getCode)
.collect(Collectors.joining(",")));
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SELECTED_TRADE_CURRENCY, "INR");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "John Doe");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "john@doe.info");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "US");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_STATE, "NY");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
MoneyGramAccount paymentAccount = (MoneyGramAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountTradeCurrencies(MoneyGramAccount.SUPPORTED_CURRENCIES, paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SELECTED_TRADE_CURRENCY),
paymentAccount.getSelectedTradeCurrency().getCode());
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getFullName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
Objects.requireNonNull(paymentAccount.getCountry()).code);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_STATE), paymentAccount.getState());
print(paymentAccount);
}
@Test
public void testCreatePerfectMoneyAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, PERFECT_MONEY_ID);
verifyEmptyForm(emptyForm,
PERFECT_MONEY_ID,
PROPERTY_NAME_ACCOUNT_NR);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, PERFECT_MONEY_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Perfect Money Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "PM 0000 1111");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Perfect Money Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
PerfectMoneyAccount paymentAccount = (PerfectMoneyAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency(USD, paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
print(paymentAccount);
}
@Test
public void testCreatePaxumAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, PAXUM_ID);
verifyEmptyForm(emptyForm,
PAXUM_ID,
PROPERTY_NAME_EMAIL);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, PAXUM_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Paxum Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, PaxumAccount.SUPPORTED_CURRENCIES
.stream()
.map(TradeCurrency::getCode)
.collect(Collectors.joining(",")));
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SELECTED_TRADE_CURRENCY, "SEK");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "jane@doe.net");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
PaxumAccount paymentAccount = (PaxumAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountTradeCurrencies(PaxumAccount.SUPPORTED_CURRENCIES, paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SELECTED_TRADE_CURRENCY),
paymentAccount.getSelectedTradeCurrency().getCode());
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail());
print(paymentAccount);
}
@Test
public void testCreatePayseraAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, PAYSERA_ID);
verifyEmptyForm(emptyForm,
PAYSERA_ID,
PROPERTY_NAME_EMAIL);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, PAYSERA_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Paysera Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, PayseraAccount.SUPPORTED_CURRENCIES
.stream()
.map(TradeCurrency::getCode)
.collect(Collectors.joining(",")));
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SELECTED_TRADE_CURRENCY, "ZAR");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "jane@doe.net");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
PayseraAccount paymentAccount = (PayseraAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountTradeCurrencies(PayseraAccount.SUPPORTED_CURRENCIES, paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SELECTED_TRADE_CURRENCY),
paymentAccount.getSelectedTradeCurrency().getCode());
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail());
print(paymentAccount);
}
@Test
public void testCreatePopmoneyAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, POPMONEY_ID);
verifyEmptyForm(emptyForm,
POPMONEY_ID,
PROPERTY_NAME_ACCOUNT_ID,
PROPERTY_NAME_HOLDER_NAME);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, POPMONEY_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Pop Money Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_ID, "POPMONEY 0000 1111");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jane Doe");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
PopmoneyAccount paymentAccount = (PopmoneyAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency(USD, paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_ID), paymentAccount.getAccountId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
print(paymentAccount);
}
@Test
public void testCreatePromptPayAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, PROMPT_PAY_ID);
verifyEmptyForm(emptyForm,
PROMPT_PAY_ID,
PROPERTY_NAME_PROMPT_PAY_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, PROMPT_PAY_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Prompt Pay Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PROMPT_PAY_ID, "PP 0000 1111");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Prompt Pay Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
PromptPayAccount paymentAccount = (PromptPayAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("THB", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_PROMPT_PAY_ID), paymentAccount.getPromptPayId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
print(paymentAccount);
}
@Test
public void testCreateRevolutAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, REVOLUT_ID);
verifyEmptyForm(emptyForm,
REVOLUT_ID,
PROPERTY_NAME_USERNAME);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, REVOLUT_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Revolut Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, RevolutAccount.SUPPORTED_CURRENCIES
.stream()
.map(TradeCurrency::getCode)
.collect(Collectors.joining(",")));
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SELECTED_TRADE_CURRENCY, "QAR");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_USERNAME, "revolut123");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
RevolutAccount paymentAccount = (RevolutAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountTradeCurrencies(RevolutAccount.SUPPORTED_CURRENCIES, paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SELECTED_TRADE_CURRENCY),
paymentAccount.getSelectedTradeCurrency().getCode());
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_USERNAME), paymentAccount.getUsername());
print(paymentAccount);
}
@Test
public void testCreateSameBankAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, SAME_BANK_ID);
verifyEmptyForm(emptyForm,
SAME_BANK_ID,
PROPERTY_NAME_ACCOUNT_NR,
PROPERTY_NAME_ACCOUNT_TYPE,
PROPERTY_NAME_BANK_NAME,
PROPERTY_NAME_BRANCH_ID,
PROPERTY_NAME_COUNTRY,
PROPERTY_NAME_HOLDER_NAME,
PROPERTY_NAME_HOLDER_TAX_ID,
PROPERTY_NAME_NATIONAL_ACCOUNT_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, SAME_BANK_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Same Bank Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "000 1 4567");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_TYPE, "Checking");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_NAME, "HSBC");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BRANCH_ID, "111");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "GB");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jane Doe");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_TAX_ID, "123456789");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_NATIONAL_ACCOUNT_ID, "123456789");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Same Bank Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
SameBankAccount paymentAccount = (SameBankAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("GBP", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
Objects.requireNonNull(paymentAccount.getCountry()).code);
SameBankAccountPayload payload = (SameBankAccountPayload) paymentAccount.getPaymentAccountPayload();
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), payload.getAccountNr());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_TYPE), payload.getAccountType());
// The bankId == bankName because bank id is not required in the UK.
assertEquals(payload.getBankId(), payload.getBankName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), payload.getBankName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BRANCH_ID), payload.getBranchId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), payload.getHolderName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_TAX_ID), payload.getHolderTaxId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_NATIONAL_ACCOUNT_ID), payload.getNationalAccountId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
print(paymentAccount);
}
@Test
public void testCreateSepaInstantAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, SEPA_INSTANT_ID);
verifyEmptyForm(emptyForm,
SEPA_INSTANT_ID,
PROPERTY_NAME_COUNTRY,
PROPERTY_NAME_HOLDER_NAME,
PROPERTY_NAME_IBAN,
PROPERTY_NAME_BIC);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, SEPA_INSTANT_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Conta Sepa Instant");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "PT");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jose da Silva");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_IBAN, "909-909");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BIC, "909");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
SepaInstantAccount paymentAccount = (SepaInstantAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
Objects.requireNonNull(paymentAccount.getCountry()).code);
verifyAccountSingleTradeCurrency(EUR, paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_IBAN), paymentAccount.getIban());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BIC), paymentAccount.getBic());
// bankId == bic
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BIC), paymentAccount.getBankId());
print(paymentAccount);
}
@Test
public void testCreateSepaAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, SEPA_ID);
verifyEmptyForm(emptyForm,
SEPA_ID,
PROPERTY_NAME_COUNTRY,
PROPERTY_NAME_HOLDER_NAME,
PROPERTY_NAME_IBAN,
PROPERTY_NAME_BIC);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, SEPA_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Conta Sepa");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "PT");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jose da Silva");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_IBAN, "909-909");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BIC, "909");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Conta Sepa Salt"));
String jsonString = getCompletedFormAsJsonString();
SepaAccount paymentAccount = (SepaAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
Objects.requireNonNull(paymentAccount.getCountry()).code);
verifyAccountSingleTradeCurrency(EUR, paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_IBAN), paymentAccount.getIban());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BIC), paymentAccount.getBic());
// bankId == bic
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BIC), paymentAccount.getBankId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
print(paymentAccount);
}
@Test
public void testCreateSpecificBanksAccount(TestInfo testInfo) {
// TODO Supporting set of accepted banks may require some refactoring
// of the SpecificBanksAccount and SpecificBanksAccountPayload classes, i.e.,
// public void setAcceptedBanks(String... bankNames) { ... }
File emptyForm = getEmptyForm(testInfo, SPECIFIC_BANKS_ID);
verifyEmptyForm(emptyForm,
SPECIFIC_BANKS_ID,
PROPERTY_NAME_ACCOUNT_NR,
PROPERTY_NAME_ACCOUNT_TYPE,
PROPERTY_NAME_BANK_NAME,
PROPERTY_NAME_BRANCH_ID,
PROPERTY_NAME_COUNTRY,
PROPERTY_NAME_HOLDER_NAME,
PROPERTY_NAME_HOLDER_TAX_ID,
PROPERTY_NAME_NATIONAL_ACCOUNT_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, SPECIFIC_BANKS_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Specific Banks Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "000 1 4567");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_TYPE, "Checking");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_NAME, "HSBC");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BRANCH_ID, "111");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "GB");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jane Doe");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_TAX_ID, "123456789");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_NATIONAL_ACCOUNT_ID, "123456789");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
SpecificBanksAccount paymentAccount = (SpecificBanksAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("GBP", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
Objects.requireNonNull(paymentAccount.getCountry()).code);
SpecificBanksAccountPayload payload = (SpecificBanksAccountPayload) paymentAccount.getPaymentAccountPayload();
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), payload.getAccountNr());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_TYPE), payload.getAccountType());
// The bankId == bankName because bank id is not required in the UK.
assertEquals(payload.getBankId(), payload.getBankName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), payload.getBankName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BRANCH_ID), payload.getBranchId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), payload.getHolderName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_TAX_ID), payload.getHolderTaxId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_NATIONAL_ACCOUNT_ID), payload.getNationalAccountId());
print(paymentAccount);
}
@Test
public void testCreateSwiftAccount(TestInfo testInfo) {
// https://www.theswiftcodes.com
File emptyForm = getEmptyForm(testInfo, SWIFT_ID);
verifyEmptyForm(emptyForm,
SWIFT_ID,
PROPERTY_NAME_BANK_SWIFT_CODE);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, SWIFT_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "IT Swift Acct w/ DE Intermediary");
Collection swiftCurrenciesSortedByCode = getAllSortedTraditionalCurrencies(comparing(TradeCurrency::getCode));
String allTraditionalCodes = getCommaDelimitedTraditionalCurrencyCodes(swiftCurrenciesSortedByCode);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, allTraditionalCodes);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SELECTED_TRADE_CURRENCY, EUR);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_SWIFT_CODE, "PASCITMMFIR");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_COUNTRY_CODE, "IT");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_NAME, "BANCA MONTE DEI PASCHI DI SIENA S.P.A.");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_BRANCH, "SUCC. DI FIRENZE");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_ADDRESS, "Via dei Pecori, 8, 50123 Firenze FI, Italy");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BENEFICIARY_NAME, "Vito de' Medici");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BENEFICIARY_ACCOUNT_NR, "0000 1111 2222 3333");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BENEFICIARY_ADDRESS, "Via dei Pecori, 1, 50123 Firenze FI, Italy");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BENEFICIARY_CITY, "Firenze");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BENEFICIARY_PHONE, "+39 055 222222");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SPECIAL_INSTRUCTIONS, "N/A");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_INTERMEDIARY_SWIFT_CODE, "DEUTDEFFXXX");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_INTERMEDIARY_COUNTRY_CODE, "DE");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_INTERMEDIARY_NAME, "Kosmo Krump");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_INTERMEDIARY_ADDRESS, "TAUNUSANLAGE 12, FRANKFURT AM MAIN, 60262");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_INTERMEDIARY_BRANCH, "Deutsche Bank Frankfurt F");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Swift Acct Salt"));
String jsonString = getCompletedFormAsJsonString(getSwiftFormComments());
SwiftAccount paymentAccount = (SwiftAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountTradeCurrencies(swiftCurrenciesSortedByCode, paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SELECTED_TRADE_CURRENCY),
paymentAccount.getSelectedTradeCurrency().getCode());
verifyCommonFormEntries(paymentAccount);
SwiftAccountPayload payload = (SwiftAccountPayload) paymentAccount.getPaymentAccountPayload();
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_SWIFT_CODE), payload.getBankSwiftCode());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_COUNTRY_CODE), payload.getBankCountryCode());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), payload.getBankName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_BRANCH), payload.getBankBranch());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_ADDRESS), payload.getBankAddress());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BENEFICIARY_NAME), payload.getBeneficiaryName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BENEFICIARY_ACCOUNT_NR), payload.getBeneficiaryAccountNr());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BENEFICIARY_ADDRESS), payload.getBeneficiaryAddress());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BENEFICIARY_CITY), payload.getBeneficiaryCity());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BENEFICIARY_PHONE), payload.getBeneficiaryPhone());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SPECIAL_INSTRUCTIONS), payload.getSpecialInstructions());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_INTERMEDIARY_SWIFT_CODE), payload.getIntermediarySwiftCode());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_INTERMEDIARY_COUNTRY_CODE), payload.getIntermediaryCountryCode());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_INTERMEDIARY_NAME), payload.getIntermediaryName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_INTERMEDIARY_BRANCH), payload.getIntermediaryBranch());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_INTERMEDIARY_ADDRESS), payload.getIntermediaryAddress());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
print(paymentAccount);
}
@Test
public void testCreateSwishAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, SWISH_ID);
verifyEmptyForm(emptyForm,
SWISH_ID,
PROPERTY_NAME_MOBILE_NR,
PROPERTY_NAME_HOLDER_NAME);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, SWISH_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Swish Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_MOBILE_NR, "+46 7 6060 0101");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Swish Acct Holder");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Swish Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
SwishAccount paymentAccount = (SwishAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("SEK", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_MOBILE_NR), paymentAccount.getMobileNr());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
print(paymentAccount);
}
@Test
public void testCreateTransferwiseAccountWith1TradeCurrency(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, TRANSFERWISE_ID);
verifyEmptyForm(emptyForm,
TRANSFERWISE_ID,
PROPERTY_NAME_EMAIL);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, TRANSFERWISE_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Transferwise Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, "NZD");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SELECTED_TRADE_CURRENCY, "NZD");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "jane@doe.info");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
TransferwiseAccount paymentAccount = (TransferwiseAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
assertEquals(1, paymentAccount.getTradeCurrencies().size());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SELECTED_TRADE_CURRENCY),
paymentAccount.getSelectedTradeCurrency().getCode());
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail());
print(paymentAccount);
}
@Test
public void testCreateTransferwiseAccountWith10TradeCurrencies(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, TRANSFERWISE_ID);
verifyEmptyForm(emptyForm,
TRANSFERWISE_ID,
PROPERTY_NAME_EMAIL);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, TRANSFERWISE_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Transferwise Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, "ARS,CAD,HRK,CZK,EUR,HKD,IDR,JPY,CHF,NZD");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SELECTED_TRADE_CURRENCY, "CHF");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "jane@doe.info");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
TransferwiseAccount paymentAccount = (TransferwiseAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
assertEquals(10, paymentAccount.getTradeCurrencies().size());
List expectedTradeCurrencies = new ArrayList<>() {{
add(getTradeCurrency("ARS").get()); // 1st in list = selected ccy
add(getTradeCurrency("CAD").get());
add(getTradeCurrency("CZK").get());
add(getTradeCurrency(EUR).get());
add(getTradeCurrency("HKD").get());
add(getTradeCurrency("IDR").get());
add(getTradeCurrency("JPY").get());
add(getTradeCurrency("CHF").get());
add(getTradeCurrency("NZD").get());
}};
verifyAccountTradeCurrencies(expectedTradeCurrencies, paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SELECTED_TRADE_CURRENCY),
paymentAccount.getSelectedTradeCurrency().getCode());
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail());
print(paymentAccount);
}
@Test
public void testCreateTransferwiseAccountWithSupportedTradeCurrencies(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, TRANSFERWISE_ID);
verifyEmptyForm(emptyForm,
TRANSFERWISE_ID,
PROPERTY_NAME_EMAIL);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, TRANSFERWISE_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Transferwise Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, TransferwiseAccount.SUPPORTED_CURRENCIES
.stream()
.map(TradeCurrency::getCode)
.collect(Collectors.joining(",")));
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SELECTED_TRADE_CURRENCY, "AUD");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "jane@doe.info");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
TransferwiseAccount paymentAccount = (TransferwiseAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountTradeCurrencies(TransferwiseAccount.SUPPORTED_CURRENCIES, paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SELECTED_TRADE_CURRENCY),
paymentAccount.getSelectedTradeCurrency().getCode());
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail());
print(paymentAccount);
}
@Test
public void testCreateTransferwiseAccountWithInvalidBrlTradeCurrencyShouldThrowException(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, TRANSFERWISE_ID);
verifyEmptyForm(emptyForm,
TRANSFERWISE_ID,
PROPERTY_NAME_EMAIL);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, TRANSFERWISE_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Transferwise Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, "eur, hkd, idr, jpy, chf, nzd, brl, gbp");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "jane@doe.info");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
Throwable exception = assertThrows(StatusRuntimeException.class, () ->
createPaymentAccount(aliceClient, jsonString));
assertEquals("INVALID_ARGUMENT: BRL is not a member of valid currencies list",
exception.getMessage());
}
@Test
public void testCreateTransferwiseAccountWithoutTradeCurrenciesShouldThrowException(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, TRANSFERWISE_ID);
verifyEmptyForm(emptyForm,
TRANSFERWISE_ID,
PROPERTY_NAME_EMAIL);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, TRANSFERWISE_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Transferwise Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, "");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "jane@doe.info");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
Throwable exception = assertThrows(StatusRuntimeException.class, () ->
createPaymentAccount(aliceClient, jsonString));
assertEquals("INVALID_ARGUMENT: no trade currency defined for transferwise payment account",
exception.getMessage());
}
@Test
public void testCreateUpholdAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, UPHOLD_ID);
verifyEmptyForm(emptyForm,
UPHOLD_ID,
PROPERTY_NAME_ACCOUNT_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, UPHOLD_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Uphold Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_ID, "UA 9876");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, UpholdAccount.SUPPORTED_CURRENCIES
.stream()
.map(TradeCurrency::getCode)
.collect(Collectors.joining(",")));
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SELECTED_TRADE_CURRENCY, "MXN");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Uphold Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
UpholdAccount paymentAccount = (UpholdAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountTradeCurrencies(UpholdAccount.SUPPORTED_CURRENCIES, paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SELECTED_TRADE_CURRENCY),
paymentAccount.getSelectedTradeCurrency().getCode());
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_ID), paymentAccount.getAccountId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
print(paymentAccount);
}
@Test
public void testCreateUSPostalMoneyOrderAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, US_POSTAL_MONEY_ORDER_ID);
verifyEmptyForm(emptyForm,
US_POSTAL_MONEY_ORDER_ID,
PROPERTY_NAME_HOLDER_NAME,
PROPERTY_NAME_POSTAL_ADDRESS);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, US_POSTAL_MONEY_ORDER_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Bubba's Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Bubba");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_POSTAL_ADDRESS, "000 Westwood Terrace Austin, TX 78700");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
USPostalMoneyOrderAccount paymentAccount = (USPostalMoneyOrderAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency(USD, paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_POSTAL_ADDRESS), paymentAccount.getPostalAddress());
print(paymentAccount);
}
@Test
public void testCreateWeChatPayAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, WECHAT_PAY_ID);
verifyEmptyForm(emptyForm,
WECHAT_PAY_ID,
PROPERTY_NAME_ACCOUNT_NR);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, WECHAT_PAY_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "WeChat Pay Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "WC 1234");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored WeChat Pay Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
WeChatPayAccount paymentAccount = (WeChatPayAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("CNY", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
print(paymentAccount);
}
@Test
public void testCreateWesternUnionAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, WESTERN_UNION_ID);
verifyEmptyForm(emptyForm,
WESTERN_UNION_ID,
PROPERTY_NAME_HOLDER_NAME,
PROPERTY_NAME_CITY,
PROPERTY_NAME_STATE,
PROPERTY_NAME_COUNTRY,
PROPERTY_NAME_EMAIL);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, WESTERN_UNION_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Western Union Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jane Doe");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_CITY, "Fargo");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_STATE, "North Dakota");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "US");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "jane@doe.info");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
WesternUnionAccount paymentAccount = (WesternUnionAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency(USD, paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getFullName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_CITY), paymentAccount.getCity());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_STATE), paymentAccount.getState());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
Objects.requireNonNull(paymentAccount.getCountry()).code);
print(paymentAccount);
}
@AfterAll
public static void tearDown() {
tearDownScaffold();
}
private void print(PaymentAccount paymentAccount) {
if (log.isDebugEnabled()) {
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
log.debug("\n{}", new TableBuilder(PAYMENT_ACCOUNT_TBL, paymentAccount.toProtoMessage()).build());
}
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/method/payment/GetPaymentMethodsTest.java
================================================
package haveno.apitest.method.payment;
import haveno.apitest.method.MethodTest;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import protobuf.PaymentMethod;
import java.util.List;
import java.util.stream.Collectors;
import static haveno.apitest.Scaffold.BitcoinCoreApp.bitcoind;
import static haveno.apitest.config.HavenoAppConfig.alicedaemon;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
@Disabled
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class GetPaymentMethodsTest extends MethodTest {
@BeforeAll
public static void setUp() {
try {
setUpScaffold(bitcoind, alicedaemon);
} catch (Exception ex) {
fail(ex);
}
}
@Test
@Order(1)
public void testGetPaymentMethods() {
List paymentMethodIds = aliceClient.getPaymentMethods()
.stream()
.map(PaymentMethod::getId)
.collect(Collectors.toList());
assertTrue(paymentMethodIds.size() >= 20);
}
@AfterAll
public static void tearDown() {
tearDownScaffold();
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/method/trade/AbstractTradeTest.java
================================================
package haveno.apitest.method.trade;
import haveno.apitest.method.offer.AbstractOfferTest;
import haveno.cli.CliMain;
import haveno.cli.GrpcClient;
import haveno.cli.table.builder.TableBuilder;
import haveno.proto.grpc.TradeInfo;
import lombok.Getter;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.TestInfo;
import org.slf4j.Logger;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import static haveno.cli.table.builder.TableType.TRADE_DETAIL_TBL;
import static haveno.core.trade.Trade.Phase.DEPOSITS_UNLOCKED;
import static haveno.core.trade.Trade.Phase.PAYMENT_SENT;
import static haveno.core.trade.Trade.State.BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG;
import static haveno.core.trade.Trade.State.BUYER_SENT_PAYMENT_SENT_MSG;
import static haveno.core.trade.Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN;
import static java.lang.String.format;
import static java.lang.System.out;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.fail;
public class AbstractTradeTest extends AbstractOfferTest {
public static final ExpectedProtocolStatus EXPECTED_PROTOCOL_STATUS = new ExpectedProtocolStatus();
// A Trade ID cache for use in @Test sequences.
@Getter
protected static String tradeId;
protected final Supplier maxTradeStateAndPhaseChecks = () -> isLongRunningTest ? 10 : 2;
protected final Function toTradeDetailTable = (trade) ->
new TableBuilder(TRADE_DETAIL_TBL, trade).build().toString();
protected final Function toUserName = (client) -> client.equals(aliceClient) ? "Alice" : "Bob";
@BeforeAll
public static void initStaticFixtures() {
EXPECTED_PROTOCOL_STATUS.init();
}
protected final TradeInfo takeAlicesOffer(String offerId,
String paymentAccountId) {
return takeAlicesOffer(offerId,
paymentAccountId,
true);
}
protected final TradeInfo takeAlicesOffer(String offerId,
String paymentAccountId,
boolean generateBtcBlock) {
@SuppressWarnings("ConstantConditions")
var trade = bobClient.takeOffer(offerId,
paymentAccountId);
assertNotNull(trade);
assertEquals(offerId, trade.getTradeId());
// Cache the trade id for the other tests.
tradeId = trade.getTradeId();
if (generateBtcBlock)
genBtcBlocksThenWait(1, 6_000);
return trade;
}
protected final void waitForDepositUnlocked(Logger log,
TestInfo testInfo,
GrpcClient grpcClient,
String tradeId) {
Predicate isTradeInDepositUnlockedStateAndPhase = (t) ->
t.getState().equals(DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN.name())
&& t.getPhase().equals(DEPOSITS_UNLOCKED.name());
String userName = toUserName.apply(grpcClient);
for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) {
TradeInfo trade = grpcClient.getTrade(tradeId);
if (!isTradeInDepositUnlockedStateAndPhase.test(trade)) {
log.warn("{} still waiting on trade {} tx {}: DEPOSIT_UNLOCKED_IN_BLOCK_CHAIN, attempt # {}",
userName,
trade.getShortId(),
trade.getMakerDepositTxId(),
trade.getTakerDepositTxId(),
i);
genBtcBlocksThenWait(1, 4_000);
} else {
EXPECTED_PROTOCOL_STATUS.setState(DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN)
.setPhase(DEPOSITS_UNLOCKED)
.setDepositPublished(true)
.setDepositConfirmed(true);
verifyExpectedProtocolStatus(trade);
logTrade(log,
testInfo,
userName + "'s view after deposit is confirmed",
trade);
break;
}
}
}
protected final void verifyTakerDepositConfirmed(TradeInfo trade) {
if (!trade.getIsDepositsUnlocked()) {
fail(format("INVALID_PHASE for trade %s in STATE=%s PHASE=%s, deposit tx never unlocked.",
trade.getShortId(),
trade.getState(),
trade.getPhase()));
}
}
protected final void waitForBuyerSeesPaymentInitiatedMessage(Logger log,
TestInfo testInfo,
GrpcClient grpcClient,
String tradeId) {
String userName = toUserName.apply(grpcClient);
for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) {
TradeInfo trade = grpcClient.getTrade(tradeId);
if (!trade.getIsPaymentSent()) {
log.warn("{} still waiting for trade {} {}, attempt # {}",
userName,
trade.getShortId(),
BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG,
i);
sleep(5_000);
} else {
// Do not check trade.getOffer().getState() here because
// it might be AVAILABLE, not OFFER_FEE_RESERVED.
EXPECTED_PROTOCOL_STATUS.setState(BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG)
.setPhase(PAYMENT_SENT)
.setPaymentSentMessageSent(true);
verifyExpectedProtocolStatus(trade);
logTrade(log, testInfo, userName + "'s view after confirming trade payment sent", trade);
break;
}
}
}
protected final void waitForSellerSeesPaymentInitiatedMessage(Logger log,
TestInfo testInfo,
GrpcClient grpcClient,
String tradeId) {
Predicate isTradeInPaymentReceiptConfirmedStateAndPhase = (t) ->
t.getState().equals(BUYER_SENT_PAYMENT_SENT_MSG.name()) &&
t.getPhase().equals(PAYMENT_SENT.name());
String userName = toUserName.apply(grpcClient);
for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) {
TradeInfo trade = grpcClient.getTrade(tradeId);
if (!isTradeInPaymentReceiptConfirmedStateAndPhase.test(trade)) {
log.warn("INVALID_PHASE for {}'s trade {} in STATE={} PHASE={}, cannot confirm payment received yet.",
userName,
trade.getShortId(),
trade.getState(),
trade.getPhase());
sleep(10_000);
} else {
break;
}
}
TradeInfo trade = grpcClient.getTrade(tradeId);
if (!isTradeInPaymentReceiptConfirmedStateAndPhase.test(trade)) {
fail(format("INVALID_PHASE for %s's trade %s in STATE=%s PHASE=%s, cannot confirm payment received.",
userName,
trade.getShortId(),
trade.getState(),
trade.getPhase()));
}
}
protected final void verifyExpectedProtocolStatus(TradeInfo trade) {
assertNotNull(trade);
assertEquals(EXPECTED_PROTOCOL_STATUS.state.name(), trade.getState());
assertEquals(EXPECTED_PROTOCOL_STATUS.phase.name(), trade.getPhase());
if (!isLongRunningTest)
assertEquals(EXPECTED_PROTOCOL_STATUS.isDepositPublished, trade.getIsDepositsPublished());
assertEquals(EXPECTED_PROTOCOL_STATUS.isDepositConfirmed, trade.getIsDepositsUnlocked());
assertEquals(EXPECTED_PROTOCOL_STATUS.isPaymentSentMessageSent, trade.getIsPaymentSent());
assertEquals(EXPECTED_PROTOCOL_STATUS.isPaymentReceivedMessageSent, trade.getIsPaymentReceived());
assertEquals(EXPECTED_PROTOCOL_STATUS.isPayoutPublished, trade.getIsPayoutPublished());
assertEquals(EXPECTED_PROTOCOL_STATUS.isCompleted, trade.getIsCompleted());
}
protected final void logBalances(Logger log, TestInfo testInfo) {
var alicesBalances = aliceClient.getBalances();
log.debug("{} Alice's Current Balances:\n{}",
testName(testInfo),
formatBalancesTbls(alicesBalances));
var bobsBalances = bobClient.getBalances();
log.debug("{} Bob's Current Balances:\n{}",
testName(testInfo),
formatBalancesTbls(bobsBalances));
}
protected final void logTrade(Logger log,
TestInfo testInfo,
String description,
TradeInfo trade) {
if (log.isDebugEnabled()) {
log.debug(format("%s %s%n%s",
testName(testInfo),
description,
new TableBuilder(TRADE_DETAIL_TBL, trade).build()));
}
}
protected static void runCliGetTrade(String tradeId) {
out.println("Alice's CLI 'gettrade' response:");
CliMain.main(new String[]{"--password=xyz", "--port=9998", "gettrade", "--trade-id=" + tradeId});
out.println("Bob's CLI 'gettrade' response:");
CliMain.main(new String[]{"--password=xyz", "--port=9999", "gettrade", "--trade-id=" + tradeId});
}
protected static void runCliGetOpenTrades() {
out.println("Alice's CLI 'gettrades --category=open' response:");
CliMain.main(new String[]{"--password=xyz", "--port=9998", "gettrades", "--category=open"});
out.println("Bob's CLI 'gettrades --category=open' response:");
CliMain.main(new String[]{"--password=xyz", "--port=9999", "gettrades", "--category=open"});
}
protected static void runCliGetClosedTrades() {
out.println("Alice's CLI 'gettrades --category=closed' response:");
CliMain.main(new String[]{"--password=xyz", "--port=9998", "gettrades", "--category=closed"});
out.println("Bob's CLI 'gettrades --category=closed' response:");
CliMain.main(new String[]{"--password=xyz", "--port=9999", "gettrades", "--category=closed"});
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/method/trade/ExpectedProtocolStatus.java
================================================
package haveno.apitest.method.trade;
import haveno.core.trade.Trade;
/**
* A test fixture encapsulating expected trade protocol status.
* Status flags should be cleared via init() before starting a new trade protocol.
*/
public class ExpectedProtocolStatus {
Trade.State state;
Trade.Phase phase;
boolean isDepositPublished;
boolean isDepositConfirmed;
boolean isPaymentSentMessageSent;
boolean isPaymentReceivedMessageSent;
boolean isPayoutPublished;
boolean isCompleted;
public ExpectedProtocolStatus setState(Trade.State state) {
this.state = state;
return this;
}
public ExpectedProtocolStatus setPhase(Trade.Phase phase) {
this.phase = phase;
return this;
}
public ExpectedProtocolStatus setDepositPublished(boolean depositPublished) {
isDepositPublished = depositPublished;
return this;
}
public ExpectedProtocolStatus setDepositConfirmed(boolean depositConfirmed) {
isDepositConfirmed = depositConfirmed;
return this;
}
public ExpectedProtocolStatus setPaymentSentMessageSent(boolean paymentSentMessageSent) {
isPaymentSentMessageSent = paymentSentMessageSent;
return this;
}
public ExpectedProtocolStatus setPaymentReceivedMessageSent(boolean paymentReceivedMessageSent) {
isPaymentReceivedMessageSent = paymentReceivedMessageSent;
return this;
}
public ExpectedProtocolStatus setPayoutPublished(boolean payoutPublished) {
isPayoutPublished = payoutPublished;
return this;
}
public ExpectedProtocolStatus setCompleted(boolean completed) {
isCompleted = completed;
return this;
}
public void init() {
state = null;
phase = null;
isDepositPublished = false;
isDepositConfirmed = false;
isPaymentSentMessageSent = false;
isPaymentReceivedMessageSent = false;
isPayoutPublished = false;
isCompleted = false;
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/method/trade/TakeBuyBTCOfferTest.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.apitest.method.trade;
import haveno.core.payment.PaymentAccount;
import io.grpc.StatusRuntimeException;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestMethodOrder;
import static haveno.apitest.config.ApiTestConfig.USD;
import static haveno.core.trade.Trade.Phase.PAYMENT_RECEIVED;
import static haveno.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
import static protobuf.OfferDirection.BUY;
import static protobuf.OpenOffer.State.AVAILABLE;
@Disabled
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class TakeBuyBTCOfferTest extends AbstractTradeTest {
// Alice is maker/buyer, Bob is taker/seller.
@Test
@Order(1)
public void testTakeAlicesBuyOffer(final TestInfo testInfo) {
try {
PaymentAccount alicesUsdAccount = createDummyF2FAccount(aliceClient, "US");
var alicesOffer = aliceClient.createMarketBasedPricedOffer(BUY.name(),
USD,
12_500_000L,
12_500_000L, // min-amount = amount
0.00,
defaultSecurityDepositPct.get(),
alicesUsdAccount.getId(),
NO_TRIGGER_PRICE);
var offerId = alicesOffer.getId();
// Wait for Alice's AddToOfferBook task.
// Wait times vary; my logs show >= 2-second delay.
sleep(3_000); // TODO loop instead of hard code a wait time
var alicesUsdOffers = aliceClient.getMyOffersSortedByDate(BUY.name(), USD);
assertEquals(1, alicesUsdOffers.size());
PaymentAccount bobsUsdAccount = createDummyF2FAccount(bobClient, "US");
var trade = takeAlicesOffer(offerId,
bobsUsdAccount.getId(),
false);
sleep(2_500); // Allow available offer to be removed from offer book.
alicesUsdOffers = aliceClient.getMyOffersSortedByDate(BUY.name(), USD);
assertEquals(0, alicesUsdOffers.size());
genBtcBlocksThenWait(1, 2_500);
waitForDepositUnlocked(log, testInfo, bobClient, trade.getTradeId());
trade = bobClient.getTrade(tradeId);
verifyTakerDepositConfirmed(trade);
logTrade(log, testInfo, "Alice's Maker/Buyer View", aliceClient.getTrade(tradeId));
logTrade(log, testInfo, "Bob's Taker/Seller View", bobClient.getTrade(tradeId));
} catch (StatusRuntimeException e) {
fail(e);
}
}
@Test
@Order(2)
public void testAlicesConfirmPaymentSent(final TestInfo testInfo) {
try {
var trade = aliceClient.getTrade(tradeId);
waitForDepositUnlocked(log, testInfo, aliceClient, trade.getTradeId());
aliceClient.confirmPaymentSent(trade.getTradeId());
sleep(6_000);
waitForBuyerSeesPaymentInitiatedMessage(log, testInfo, aliceClient, tradeId);
} catch (StatusRuntimeException e) {
fail(e);
}
}
@Test
@Order(3)
public void testBobsConfirmPaymentReceived(final TestInfo testInfo) {
try {
waitForSellerSeesPaymentInitiatedMessage(log, testInfo, bobClient, tradeId);
var trade = bobClient.getTrade(tradeId);
bobClient.confirmPaymentReceived(trade.getTradeId());
sleep(3_000);
trade = bobClient.getTrade(tradeId);
// Note: offer.state == available
assertEquals(AVAILABLE.name(), trade.getOffer().getState());
EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG)
.setPhase(PAYMENT_RECEIVED)
.setPayoutPublished(true)
.setPaymentReceivedMessageSent(true);
verifyExpectedProtocolStatus(trade);
logTrade(log, testInfo, "Bob's view after confirming fiat payment received", trade);
} catch (StatusRuntimeException e) {
fail(e);
}
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/method/trade/TakeBuyBTCOfferWithNationalBankAcctTest.java
================================================
/*
* 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 .
*/
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.apitest.method.trade;
import haveno.core.payment.PaymentAccount;
import haveno.core.payment.payload.NationalBankAccountPayload;
import io.grpc.StatusRuntimeException;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestMethodOrder;
import static haveno.core.trade.Trade.Phase.PAYMENT_RECEIVED;
import static haveno.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static protobuf.Offer.State.OFFER_FEE_RESERVED;
import static protobuf.OfferDirection.BUY;
import static protobuf.OpenOffer.State.AVAILABLE;
/**
* Test case verifies trade can be made with national bank payment method,
* and json contracts exclude bank acct details until deposit tx is confirmed.
*/
@SuppressWarnings("ConstantConditions")
@Disabled
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class TakeBuyBTCOfferWithNationalBankAcctTest extends AbstractTradeTest {
// Alice is maker/buyer, Bob is taker/seller.
private static final String BRL = "BRL";
private static PaymentAccount alicesPaymentAccount;
private static PaymentAccount bobsPaymentAccount;
@BeforeAll
public static void setUp() {
setUp(false);
}
@Test
@Order(1)
public void testTakeAlicesBuyOffer(final TestInfo testInfo) {
try {
alicesPaymentAccount = createDummyBRLAccount(aliceClient,
"Alicia da Silva",
String.valueOf(System.currentTimeMillis()),
"123.456.789-01");
bobsPaymentAccount = createDummyBRLAccount(bobClient,
"Roberto da Silva",
String.valueOf(System.currentTimeMillis()),
"123.456.789-02");
var alicesOffer = aliceClient.createMarketBasedPricedOffer(BUY.name(),
BRL,
1_000_000L,
1_000_000L, // min-amount = amount
0.00,
defaultSecurityDepositPct.get(),
alicesPaymentAccount.getId(),
NO_TRIGGER_PRICE);
var offerId = alicesOffer.getId();
// Wait for Alice's AddToOfferBook task.
// Wait times vary; my logs show >= 2 second delay.
sleep(3_000); // TODO loop instead of hard code wait time
var alicesOffers = aliceClient.getMyOffersSortedByDate(BUY.name(), BRL);
assertEquals(1, alicesOffers.size());
var trade = takeAlicesOffer(offerId,
bobsPaymentAccount.getId(),
false);
// Before generating a blk and confirming deposit tx, make sure there
// are no bank acct details in the either side's contract.
while (true) {
try {
var alicesContract = aliceClient.getTrade(trade.getTradeId()).getContractAsJson();
var bobsContract = bobClient.getTrade(trade.getTradeId()).getContractAsJson();
verifyJsonContractExcludesBankAccountDetails(alicesContract, alicesPaymentAccount);
verifyJsonContractExcludesBankAccountDetails(alicesContract, bobsPaymentAccount);
verifyJsonContractExcludesBankAccountDetails(bobsContract, alicesPaymentAccount);
verifyJsonContractExcludesBankAccountDetails(bobsContract, bobsPaymentAccount);
break;
} catch (StatusRuntimeException ex) {
if (ex.getMessage() == null) {
String message = ex.getMessage().replaceFirst("^[A-Z_]+: ", "");
if (message.contains("trade") && message.contains("not found")) {
fail(ex);
}
} else {
sleep(500);
}
}
}
genBtcBlocksThenWait(1, 4000);
alicesOffers = aliceClient.getMyOffersSortedByDate(BUY.name(), BRL);
assertEquals(0, alicesOffers.size());
genBtcBlocksThenWait(1, 2_500);
waitForDepositUnlocked(log, testInfo, bobClient, trade.getTradeId());
trade = bobClient.getTrade(tradeId);
verifyTakerDepositConfirmed(trade);
logTrade(log, testInfo, "Alice's Maker/Buyer View", aliceClient.getTrade(tradeId));
logTrade(log, testInfo, "Bob's Taker/Seller View", bobClient.getTrade(tradeId));
} catch (StatusRuntimeException e) {
fail(e);
}
}
@Test
@Order(2)
public void testBankAcctDetailsIncludedInContracts(final TestInfo testInfo) {
assertNotNull(alicesPaymentAccount);
assertNotNull(bobsPaymentAccount);
var alicesTrade = aliceClient.getTrade(tradeId);
assertNotEquals(null, alicesTrade.getContract().getMakerPaymentAccountPayload());
assertNotEquals(null, alicesTrade.getContract().getTakerPaymentAccountPayload());
var alicesContractJson = alicesTrade.getContractAsJson();
verifyJsonContractIncludesBankAccountDetails(alicesContractJson, alicesPaymentAccount);
verifyJsonContractIncludesBankAccountDetails(alicesContractJson, bobsPaymentAccount);
var bobsTrade = bobClient.getTrade(tradeId);
assertNotEquals(null, bobsTrade.getContract().getMakerPaymentAccountPayload());
assertNotEquals(null, bobsTrade.getContract().getTakerPaymentAccountPayload());
var bobsContractJson = bobsTrade.getContractAsJson();
verifyJsonContractIncludesBankAccountDetails(bobsContractJson, alicesPaymentAccount);
verifyJsonContractIncludesBankAccountDetails(bobsContractJson, bobsPaymentAccount);
}
@Test
@Order(3)
public void testAlicesConfirmPaymentSent(final TestInfo testInfo) {
try {
var trade = aliceClient.getTrade(tradeId);
waitForDepositUnlocked(log, testInfo, aliceClient, trade.getTradeId());
aliceClient.confirmPaymentSent(trade.getTradeId());
sleep(6_000);
waitForBuyerSeesPaymentInitiatedMessage(log, testInfo, aliceClient, tradeId);
trade = aliceClient.getTrade(tradeId);
assertEquals(OFFER_FEE_RESERVED.name(), trade.getOffer().getState());
logTrade(log, testInfo, "Alice's Maker/Buyer View (Payment Sent)", aliceClient.getTrade(tradeId));
logTrade(log, testInfo, "Bob's Taker/Seller View (Payment Sent)", bobClient.getTrade(tradeId));
} catch (StatusRuntimeException e) {
fail(e);
}
}
@Test
@Order(4)
public void testBobsConfirmPaymentReceived(final TestInfo testInfo) {
try {
waitForSellerSeesPaymentInitiatedMessage(log, testInfo, bobClient, tradeId);
var trade = bobClient.getTrade(tradeId);
bobClient.confirmPaymentReceived(trade.getTradeId());
sleep(3_000);
trade = bobClient.getTrade(tradeId);
// Note: offer.state == available
assertEquals(AVAILABLE.name(), trade.getOffer().getState());
EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG)
.setPhase(PAYMENT_RECEIVED)
.setPayoutPublished(true)
.setPaymentReceivedMessageSent(true);
verifyExpectedProtocolStatus(trade);
logTrade(log, testInfo, "Bob's view after confirming fiat payment received", trade);
} catch (StatusRuntimeException e) {
fail(e);
}
}
private void verifyJsonContractExcludesBankAccountDetails(String jsonContract,
PaymentAccount paymentAccount) {
NationalBankAccountPayload nationalBankAccountPayload =
(NationalBankAccountPayload) paymentAccount.getPaymentAccountPayload();
// The client cannot know exactly when payment acct payloads are added to a contract,
// so auto-failing here results in a flaky test.
// assertFalse(jsonContract.contains(nationalBankAccountPayload.getNationalAccountId()));
// assertFalse(jsonContract.contains(nationalBankAccountPayload.getBranchId()));
// assertFalse(jsonContract.contains(nationalBankAccountPayload.getAccountNr()));
// assertFalse(jsonContract.contains(nationalBankAccountPayload.getHolderName()));
// assertFalse(jsonContract.contains(nationalBankAccountPayload.getHolderTaxId()));
// Log warning if bank acct details are found in json contract.
if (jsonContract.contains(nationalBankAccountPayload.getNationalAccountId()))
log.warn("Could not check json contract soon enough; it contains national bank acct id");
if (jsonContract.contains(nationalBankAccountPayload.getBranchId()))
log.warn("Could not check json contract soon enough; it contains natl bank branch id");
if (jsonContract.contains(nationalBankAccountPayload.getAccountNr()))
log.warn("Could not check json contract soon enough; it contains natl bank acct #");
if (jsonContract.contains(nationalBankAccountPayload.getHolderName()))
log.warn("Could not check json contract soon enough; it contains natl bank acct holder name");
if (jsonContract.contains(nationalBankAccountPayload.getHolderTaxId()))
log.warn("Could not check json contract soon enough; it contains natl bank acct holder tax id");
}
private void verifyJsonContractIncludesBankAccountDetails(String jsonContract,
PaymentAccount paymentAccount) {
NationalBankAccountPayload nationalBankAccountPayload =
(NationalBankAccountPayload) paymentAccount.getPaymentAccountPayload();
assertTrue(jsonContract.contains(nationalBankAccountPayload.getNationalAccountId()));
assertTrue(jsonContract.contains(nationalBankAccountPayload.getBranchId()));
assertTrue(jsonContract.contains(nationalBankAccountPayload.getAccountNr()));
assertTrue(jsonContract.contains(nationalBankAccountPayload.getHolderName()));
assertTrue(jsonContract.contains(nationalBankAccountPayload.getHolderTaxId()));
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/method/trade/TakeBuyXMROfferTest.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.apitest.method.trade;
import haveno.apitest.method.offer.AbstractOfferTest;
import haveno.cli.table.builder.TableBuilder;
import io.grpc.StatusRuntimeException;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestMethodOrder;
import static haveno.apitest.config.ApiTestConfig.XMR;
import static haveno.cli.table.builder.TableType.OFFER_TBL;
import static haveno.core.trade.Trade.Phase.PAYMENT_RECEIVED;
import static haveno.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
import static protobuf.Offer.State.OFFER_FEE_RESERVED;
import static protobuf.OfferDirection.SELL;
@Disabled
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class TakeBuyXMROfferTest extends AbstractTradeTest {
// Alice is maker / xmr buyer (btc seller), Bob is taker / xmr seller (btc buyer).
@BeforeAll
public static void setUp() {
AbstractOfferTest.setUp();
createXmrPaymentAccounts();
EXPECTED_PROTOCOL_STATUS.init();
}
@Test
@Order(1)
public void testTakeAlicesSellBTCForXMROffer(final TestInfo testInfo) {
try {
// Alice is going to BUY XMR, but the Offer direction = SELL because it is a
// BTC trade; Alice will SELL BTC for XMR. Bob will send Alice XMR.
// Confused me, but just need to remember there are only BTC offers.
var btcTradeDirection = SELL.name();
var alicesOffer = aliceClient.createFixedPricedOffer(btcTradeDirection,
XMR,
15_000_000L,
7_500_000L,
"0.00455500", // FIXED PRICE IN BTC (satoshis) FOR 1 XMR
defaultSecurityDepositPct.get(),
alicesXmrAcct.getId());
log.debug("Alice's BUY XMR (SELL BTC) Offer:\n{}", new TableBuilder(OFFER_TBL, alicesOffer).build());
genBtcBlocksThenWait(1, 5000);
var offerId = alicesOffer.getId();
var alicesXmrOffers = aliceClient.getMyOffers(btcTradeDirection, XMR);
assertEquals(1, alicesXmrOffers.size());
var trade = takeAlicesOffer(offerId, bobsXmrAcct.getId());
alicesXmrOffers = aliceClient.getMyOffersSortedByDate(XMR);
assertEquals(0, alicesXmrOffers.size());
genBtcBlocksThenWait(1, 2_500);
waitForDepositUnlocked(log, testInfo, bobClient, trade.getTradeId());
trade = bobClient.getTrade(tradeId);
verifyTakerDepositConfirmed(trade);
logTrade(log, testInfo, "Alice's Maker/Buyer View", aliceClient.getTrade(tradeId));
logTrade(log, testInfo, "Bob's Taker/Seller View", bobClient.getTrade(tradeId));
} catch (StatusRuntimeException e) {
fail(e);
}
}
@Test
@Order(2)
public void testBobsConfirmPaymentSent(final TestInfo testInfo) {
try {
var trade = bobClient.getTrade(tradeId);
verifyTakerDepositConfirmed(trade);
log.debug("Bob sends XMR payment to Alice for trade {}", trade.getTradeId());
bobClient.confirmPaymentSent(trade.getTradeId());
sleep(3500);
waitForBuyerSeesPaymentInitiatedMessage(log, testInfo, bobClient, tradeId);
logTrade(log, testInfo, "Alice's Maker/Buyer View (Payment Sent)", aliceClient.getTrade(tradeId));
logTrade(log, testInfo, "Bob's Taker/Seller View (Payment Sent)", bobClient.getTrade(tradeId));
} catch (StatusRuntimeException e) {
fail(e);
}
}
@Test
@Order(3)
public void testAlicesConfirmPaymentReceived(final TestInfo testInfo) {
try {
waitForSellerSeesPaymentInitiatedMessage(log, testInfo, aliceClient, tradeId);
sleep(2_000);
var trade = aliceClient.getTrade(tradeId);
// If we were trading BSQ, Alice would verify payment has been sent to her
// Haveno wallet, but we can do no such checks for XMR payments.
// All XMR transfers are done outside Haveno.
log.debug("Alice verifies XMR payment was received from Bob, for trade {}", trade.getTradeId());
aliceClient.confirmPaymentReceived(trade.getTradeId());
sleep(3_000);
trade = aliceClient.getTrade(tradeId);
assertEquals(OFFER_FEE_RESERVED.name(), trade.getOffer().getState());
EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG)
.setPhase(PAYMENT_RECEIVED)
.setPayoutPublished(true)
.setPaymentReceivedMessageSent(true);
verifyExpectedProtocolStatus(trade);
logTrade(log, testInfo, "Alice's Maker/Buyer View (Payment Received)", aliceClient.getTrade(tradeId));
logTrade(log, testInfo, "Bob's Taker/Seller View (Payment Received)", bobClient.getTrade(tradeId));
} catch (StatusRuntimeException e) {
fail(e);
}
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/method/trade/TakeSellBTCOfferTest.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.apitest.method.trade;
import haveno.core.payment.PaymentAccount;
import io.grpc.StatusRuntimeException;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestMethodOrder;
import static haveno.apitest.config.ApiTestConfig.BTC;
import static haveno.apitest.config.ApiTestConfig.USD;
import static haveno.core.trade.Trade.Phase.PAYMENT_RECEIVED;
import static haveno.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
import static protobuf.Offer.State.OFFER_FEE_RESERVED;
import static protobuf.OfferDirection.SELL;
@Disabled
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class TakeSellBTCOfferTest extends AbstractTradeTest {
// Alice is maker/seller, Bob is taker/buyer.
// Maker and Taker fees are in BTC.
private static final String TRADE_FEE_CURRENCY_CODE = BTC;
private static final String WITHDRAWAL_TX_MEMO = "Bob's trade withdrawal";
@Test
@Order(1)
public void testTakeAlicesSellOffer(final TestInfo testInfo) {
try {
PaymentAccount alicesUsdAccount = createDummyF2FAccount(aliceClient, "US");
var alicesOffer = aliceClient.createMarketBasedPricedOffer(SELL.name(),
USD,
12_500_000L,
12_500_000L, // min-amount = amount
0.00,
defaultSecurityDepositPct.get(),
alicesUsdAccount.getId(),
NO_TRIGGER_PRICE);
var offerId = alicesOffer.getId();
// Wait for Alice's AddToOfferBook task.
// Wait times vary; my logs show >= 2-second delay, but taking sell offers
// seems to require more time to prepare.
sleep(3_000); // TODO loop instead of hard code a wait time
var alicesUsdOffers = aliceClient.getMyOffersSortedByDate(SELL.name(), USD);
assertEquals(1, alicesUsdOffers.size());
PaymentAccount bobsUsdAccount = createDummyF2FAccount(bobClient, "US");
var trade = takeAlicesOffer(offerId,
bobsUsdAccount.getId(),
false);
sleep(2_500); // Allow available offer to be removed from offer book.
var takeableUsdOffers = bobClient.getOffersSortedByDate(SELL.name(), USD);
assertEquals(0, takeableUsdOffers.size());
genBtcBlocksThenWait(1, 2_500);
waitForDepositUnlocked(log, testInfo, bobClient, trade.getTradeId());
trade = bobClient.getTrade(tradeId);
verifyTakerDepositConfirmed(trade);
logTrade(log, testInfo, "Alice's Maker/Buyer View", aliceClient.getTrade(tradeId));
logTrade(log, testInfo, "Bob's Taker/Seller View", bobClient.getTrade(tradeId));
} catch (StatusRuntimeException e) {
fail(e);
}
}
@Test
@Order(2)
public void testBobsConfirmPaymentSent(final TestInfo testInfo) {
try {
var trade = bobClient.getTrade(tradeId);
verifyTakerDepositConfirmed(trade);
bobClient.confirmPaymentSent(tradeId);
sleep(6_000);
waitForBuyerSeesPaymentInitiatedMessage(log, testInfo, bobClient, tradeId);
} catch (StatusRuntimeException e) {
fail(e);
}
}
@Test
@Order(3)
public void testAlicesConfirmPaymentReceived(final TestInfo testInfo) {
try {
waitForSellerSeesPaymentInitiatedMessage(log, testInfo, aliceClient, tradeId);
var trade = aliceClient.getTrade(tradeId);
aliceClient.confirmPaymentReceived(trade.getTradeId());
sleep(3_000);
trade = aliceClient.getTrade(tradeId);
assertEquals(OFFER_FEE_RESERVED.name(), trade.getOffer().getState());
EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG)
.setPhase(PAYMENT_RECEIVED)
.setPayoutPublished(true)
.setPaymentReceivedMessageSent(true);
verifyExpectedProtocolStatus(trade);
logTrade(log, testInfo, "Alice's view after confirming fiat payment received", trade);
} catch (StatusRuntimeException e) {
fail(e);
}
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/method/trade/TakeSellXMROfferTest.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.apitest.method.trade;
import haveno.apitest.method.offer.AbstractOfferTest;
import haveno.cli.table.builder.TableBuilder;
import io.grpc.StatusRuntimeException;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestMethodOrder;
import static haveno.apitest.config.ApiTestConfig.BTC;
import static haveno.apitest.config.ApiTestConfig.XMR;
import static haveno.cli.table.builder.TableType.OFFER_TBL;
import static haveno.core.trade.Trade.Phase.PAYMENT_RECEIVED;
import static haveno.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
import static protobuf.OfferDirection.BUY;
@Disabled
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class TakeSellXMROfferTest extends AbstractTradeTest {
// Alice is maker / xmr seller (btc buyer), Bob is taker / xmr buyer (btc seller).
// Maker and Taker fees are in BTC.
private static final String TRADE_FEE_CURRENCY_CODE = BTC;
private static final String WITHDRAWAL_TX_MEMO = "Bob's trade withdrawal";
@BeforeAll
public static void setUp() {
AbstractOfferTest.setUp();
createXmrPaymentAccounts();
EXPECTED_PROTOCOL_STATUS.init();
}
@Test
@Order(1)
public void testTakeAlicesBuyBTCForXMROffer(final TestInfo testInfo) {
try {
// Alice is going to SELL XMR, but the Offer direction = BUY because it is a
// BTC trade; Alice will BUY BTC for XMR. Alice will send Bob XMR.
// Confused me, but just need to remember there are only BTC offers.
var btcTradeDirection = BUY.name();
double priceMarginPctInput = 1.50;
var alicesOffer = aliceClient.createMarketBasedPricedOffer(btcTradeDirection,
XMR,
20_000_000L,
10_500_000L,
priceMarginPctInput,
defaultSecurityDepositPct.get(),
alicesXmrAcct.getId(),
NO_TRIGGER_PRICE);
log.debug("Alice's SELL XMR (BUY BTC) Offer:\n{}", new TableBuilder(OFFER_TBL, alicesOffer).build());
genBtcBlocksThenWait(1, 4000);
var offerId = alicesOffer.getId();
var alicesXmrOffers = aliceClient.getMyOffers(btcTradeDirection, XMR);
assertEquals(1, alicesXmrOffers.size());
var trade = takeAlicesOffer(offerId, bobsXmrAcct.getId());
alicesXmrOffers = aliceClient.getMyOffersSortedByDate(XMR);
assertEquals(0, alicesXmrOffers.size());
genBtcBlocksThenWait(1, 2_500);
waitForDepositUnlocked(log, testInfo, bobClient, trade.getTradeId());
trade = bobClient.getTrade(tradeId);
verifyTakerDepositConfirmed(trade);
logTrade(log, testInfo, "Alice's Maker/Seller View", aliceClient.getTrade(tradeId));
logTrade(log, testInfo, "Bob's Taker/Buyer View", bobClient.getTrade(tradeId));
} catch (StatusRuntimeException e) {
fail(e);
}
}
@Test
@Order(2)
public void testAlicesConfirmPaymentSent(final TestInfo testInfo) {
try {
var trade = aliceClient.getTrade(tradeId);
waitForDepositUnlocked(log, testInfo, aliceClient, trade.getTradeId());
log.debug("Alice sends XMR payment to Bob for trade {}", trade.getTradeId());
aliceClient.confirmPaymentSent(trade.getTradeId());
sleep(3500);
waitForBuyerSeesPaymentInitiatedMessage(log, testInfo, aliceClient, tradeId);
logTrade(log, testInfo, "Alice's Maker/Seller View (Payment Sent)", aliceClient.getTrade(tradeId));
logTrade(log, testInfo, "Bob's Taker/Buyer View (Payment Sent)", bobClient.getTrade(tradeId));
} catch (StatusRuntimeException e) {
fail(e);
}
}
@Test
@Order(3)
public void testBobsConfirmPaymentReceived(final TestInfo testInfo) {
try {
waitForSellerSeesPaymentInitiatedMessage(log, testInfo, bobClient, tradeId);
var trade = bobClient.getTrade(tradeId);
sleep(2_000);
// If we were trading BTC, Bob would verify payment has been sent to his
// Haveno wallet, but we can do no such checks for XMR payments.
// All XMR transfers are done outside Haveno.
log.debug("Bob verifies XMR payment was received from Alice, for trade {}", trade.getTradeId());
bobClient.confirmPaymentReceived(trade.getTradeId());
sleep(3_000);
trade = bobClient.getTrade(tradeId);
// Warning: trade.getOffer().getState() might be AVAILABLE, not OFFER_FEE_RESERVED.
EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG)
.setPhase(PAYMENT_RECEIVED)
.setPayoutPublished(true)
.setPaymentReceivedMessageSent(true);
verifyExpectedProtocolStatus(trade);
logTrade(log, testInfo, "Alice's Maker/Seller View (Payment Received)", aliceClient.getTrade(tradeId));
logTrade(log, testInfo, "Bob's Taker/Buyer View (Payment Received)", bobClient.getTrade(tradeId));
} catch (StatusRuntimeException e) {
fail(e);
}
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/method/wallet/BtcWalletTest.java
================================================
package haveno.apitest.method.wallet;
import haveno.apitest.method.MethodTest;
import haveno.cli.table.builder.TableBuilder;
import haveno.proto.grpc.BtcBalanceInfo;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestMethodOrder;
import static haveno.apitest.Scaffold.BitcoinCoreApp.bitcoind;
import static haveno.apitest.config.HavenoAppConfig.alicedaemon;
import static haveno.apitest.config.HavenoAppConfig.bobdaemon;
import static haveno.apitest.config.HavenoAppConfig.seednode;
import static haveno.apitest.method.wallet.WalletTestUtil.INITIAL_BTC_BALANCES;
import static haveno.apitest.method.wallet.WalletTestUtil.verifyBtcBalances;
import static haveno.cli.table.builder.TableType.ADDRESS_BALANCE_TBL;
import static haveno.cli.table.builder.TableType.BTC_BALANCE_TBL;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
@Disabled
@Slf4j
@TestMethodOrder(OrderAnnotation.class)
public class BtcWalletTest extends MethodTest {
private static final String TX_MEMO = "tx memo";
@BeforeAll
public static void setUp() {
startSupportingApps(false,
true,
bitcoind,
seednode,
alicedaemon,
bobdaemon);
}
@Test
@Order(1)
public void testInitialBtcBalances(final TestInfo testInfo) {
// Bob & Alice's regtest Haveno wallets were initialized with 10 BTC.
BtcBalanceInfo alicesBalances = aliceClient.getBtcBalances();
log.debug("{} Alice's BTC Balances:\n{}",
testName(testInfo),
new TableBuilder(BTC_BALANCE_TBL, alicesBalances).build());
BtcBalanceInfo bobsBalances = bobClient.getBtcBalances();
log.debug("{} Bob's BTC Balances:\n{}",
testName(testInfo),
new TableBuilder(BTC_BALANCE_TBL, bobsBalances).build());
assertEquals(INITIAL_BTC_BALANCES.getAvailableBalance(), alicesBalances.getAvailableBalance());
assertEquals(INITIAL_BTC_BALANCES.getAvailableBalance(), bobsBalances.getAvailableBalance());
}
@Test
@Order(2)
public void testFundAlicesBtcWallet(final TestInfo testInfo) {
String newAddress = aliceClient.getUnusedBtcAddress();
bitcoinCli.sendToAddress(newAddress, "2.5");
genBtcBlocksThenWait(1, 1000);
BtcBalanceInfo btcBalanceInfo = aliceClient.getBtcBalances();
// New balance is 12.5 BTC
assertEquals(1250000000, btcBalanceInfo.getAvailableBalance());
log.debug("{} -> Alice's Funded Address Balance -> \n{}",
testName(testInfo),
new TableBuilder(ADDRESS_BALANCE_TBL,
aliceClient.getAddressBalance(newAddress)));
// New balance is 12.5 BTC
btcBalanceInfo = aliceClient.getBtcBalances();
haveno.core.api.model.BtcBalanceInfo alicesExpectedBalances =
haveno.core.api.model.BtcBalanceInfo.valueOf(1250000000,
0,
1250000000,
0);
verifyBtcBalances(alicesExpectedBalances, btcBalanceInfo);
log.debug("{} -> Alice's BTC Balances After Sending 2.5 BTC -> \n{}",
testName(testInfo),
new TableBuilder(BTC_BALANCE_TBL, btcBalanceInfo).build());
}
@AfterAll
public static void tearDown() {
tearDownScaffold();
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/method/wallet/WalletProtectionTest.java
================================================
package haveno.apitest.method.wallet;
import haveno.apitest.method.MethodTest;
import io.grpc.StatusRuntimeException;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import static haveno.apitest.config.HavenoAppConfig.alicedaemon;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
@SuppressWarnings("ResultOfMethodCallIgnored")
@Disabled
@Slf4j
@TestMethodOrder(OrderAnnotation.class)
public class WalletProtectionTest extends MethodTest {
@BeforeAll
public static void setUp() {
try {
setUpScaffold(alicedaemon);
MILLISECONDS.sleep(2000);
} catch (Exception ex) {
fail(ex);
}
}
@Test
@Order(1)
public void testSetWalletPassword() {
aliceClient.setWalletPassword("first-password");
}
@Test
@Order(2)
public void testGetBalanceOnEncryptedWalletShouldThrowException() {
Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getBtcBalances());
assertEquals("FAILED_PRECONDITION: wallet is locked", exception.getMessage());
}
@Test
@Order(3)
public void testUnlockWalletFor4Seconds() {
aliceClient.unlockWallet("first-password", 4);
aliceClient.getBtcBalances(); // should not throw 'wallet locked' exception
sleep(4500); // let unlock timeout expire
Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getBtcBalances());
assertEquals("FAILED_PRECONDITION: wallet is locked", exception.getMessage());
}
@Test
@Order(4)
public void testGetBalanceAfterUnlockTimeExpiryShouldThrowException() {
aliceClient.unlockWallet("first-password", 3);
sleep(4000); // let unlock timeout expire
Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getBtcBalances());
assertEquals("FAILED_PRECONDITION: wallet is locked", exception.getMessage());
}
@Test
@Order(5)
public void testLockWalletBeforeUnlockTimeoutExpiry() {
aliceClient.unlockWallet("first-password", 60);
aliceClient.lockWallet();
Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getBtcBalances());
assertEquals("FAILED_PRECONDITION: wallet is locked", exception.getMessage());
}
@Test
@Order(6)
public void testLockWalletWhenWalletAlreadyLockedShouldThrowException() {
Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.lockWallet());
assertEquals("ALREADY_EXISTS: wallet is already locked", exception.getMessage());
}
@Test
@Order(7)
public void testUnlockWalletTimeoutOverride() {
aliceClient.unlockWallet("first-password", 2);
sleep(500); // override unlock timeout after 0.5s
aliceClient.unlockWallet("first-password", 6);
sleep(5000);
aliceClient.getBtcBalances(); // getbalance 5s after overriding timeout to 6s
}
@Test
@Order(8)
public void testSetNewWalletPassword() {
aliceClient.setWalletPassword("first-password", "second-password");
sleep(2500); // allow time for wallet save
aliceClient.unlockWallet("second-password", 2);
aliceClient.getBtcBalances();
}
@Test
@Order(9)
public void testSetNewWalletPasswordWithIncorrectNewPasswordShouldThrowException() {
Throwable exception = assertThrows(StatusRuntimeException.class, () ->
aliceClient.setWalletPassword("bad old password", "irrelevant"));
assertEquals("INVALID_ARGUMENT: incorrect old password", exception.getMessage());
}
@Test
@Order(10)
public void testRemoveNewWalletPassword() {
aliceClient.removeWalletPassword("second-password");
aliceClient.getBtcBalances(); // should not throw 'wallet locked' exception
}
@AfterAll
public static void tearDown() {
tearDownScaffold();
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/method/wallet/WalletTestUtil.java
================================================
package haveno.apitest.method.wallet;
import haveno.proto.grpc.BtcBalanceInfo;
import lombok.extern.slf4j.Slf4j;
import static org.junit.jupiter.api.Assertions.assertEquals;
@Slf4j
public class WalletTestUtil {
// All api tests depend on the DAO / regtest environment, and Bob & Alice's wallets
// are initialized with 10 BTC during the scaffolding setup.
public static final haveno.core.api.model.BtcBalanceInfo INITIAL_BTC_BALANCES =
haveno.core.api.model.BtcBalanceInfo.valueOf(1000000000,
0,
1000000000,
0);
public static void verifyBtcBalances(haveno.core.api.model.BtcBalanceInfo expected,
BtcBalanceInfo actual) {
assertEquals(expected.getAvailableBalance(), actual.getAvailableBalance());
assertEquals(expected.getReservedBalance(), actual.getReservedBalance());
assertEquals(expected.getTotalAvailableBalance(), actual.getTotalAvailableBalance());
assertEquals(expected.getLockedBalance(), actual.getLockedBalance());
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/scenario/LongRunningOfferDeactivationTest.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.apitest.scenario;
import haveno.apitest.method.offer.AbstractOfferTest;
import haveno.core.payment.PaymentAccount;
import haveno.proto.grpc.OfferInfo;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.condition.EnabledIf;
import static java.lang.System.getenv;
import static org.junit.jupiter.api.Assertions.fail;
import static protobuf.OfferDirection.BUY;
import static protobuf.OfferDirection.SELL;
/**
* Used to verify trigger based, automatic offer deactivation works.
* Disabled by default.
* Set ENV or IDE-ENV LONG_RUNNING_OFFER_DEACTIVATION_TEST_ENABLED=true to run.
*/
@EnabledIf("envLongRunningTestEnabled")
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class LongRunningOfferDeactivationTest extends AbstractOfferTest {
private static final int MAX_ITERATIONS = 500;
@Test
@Order(1)
public void testSellOfferAutoDisable(final TestInfo testInfo) {
PaymentAccount paymentAcct = createDummyF2FAccount(aliceClient, "US");
double mktPriceAsDouble = aliceClient.getBtcPrice("USD");
String triggerPrice = calcPriceAsString(mktPriceAsDouble, -50.0000, 4);
log.info("Current USD mkt price = {} Trigger Price = {}", mktPriceAsDouble, triggerPrice);
OfferInfo offer = aliceClient.createMarketBasedPricedOffer(SELL.name(),
"USD",
1_000_000,
1_000_000,
0.00,
defaultSecurityDepositPct.get(),
paymentAcct.getId(),
triggerPrice);
log.info("SELL offer {} created with margin based price {}.",
offer.getId(),
offer.getPrice());
genBtcBlocksThenWait(1, 2500); // Wait for offer book entry.
offer = aliceClient.getOffer(offer.getId()); // Offer has trigger price now.
log.info("SELL offer should be automatically disabled when mkt price falls below {}.", offer.getTriggerPrice());
int numIterations = 0;
while (++numIterations < MAX_ITERATIONS) {
offer = aliceClient.getOffer(offer.getId());
var mktPrice = aliceClient.getBtcPrice("USD");
if (offer.getIsActivated()) {
log.info("Offer still enabled at mkt price {} > {} trigger price",
mktPrice,
offer.getTriggerPrice());
sleep(1000 * 60); // 60s
} else {
log.info("Successful test completion after offer disabled at mkt price {} < {} trigger price.",
mktPrice,
offer.getTriggerPrice());
break;
}
if (numIterations == MAX_ITERATIONS)
fail("Offer never disabled");
genBtcBlocksThenWait(1, 0);
}
}
@Test
@Order(2)
public void testBuyOfferAutoDisable(final TestInfo testInfo) {
PaymentAccount paymentAcct = createDummyF2FAccount(aliceClient, "US");
double mktPriceAsDouble = aliceClient.getBtcPrice("USD");
String triggerPrice = calcPriceAsString(mktPriceAsDouble, 50.0000, 4);
log.info("Current USD mkt price = {} Trigger Price = {}", mktPriceAsDouble, triggerPrice);
OfferInfo offer = aliceClient.createMarketBasedPricedOffer(BUY.name(),
"USD",
1_000_000,
1_000_000,
0.00,
defaultSecurityDepositPct.get(),
paymentAcct.getId(),
triggerPrice);
log.info("BUY offer {} created with margin based price {}.",
offer.getId(),
offer.getPrice());
genBtcBlocksThenWait(1, 2500); // Wait for offer book entry.
offer = aliceClient.getOffer(offer.getId()); // Offer has trigger price now.
log.info("BUY offer should be automatically disabled when mkt price rises above {}.",
offer.getTriggerPrice());
int numIterations = 0;
while (++numIterations < MAX_ITERATIONS) {
offer = aliceClient.getOffer(offer.getId());
var mktPrice = aliceClient.getBtcPrice("USD");
if (offer.getIsActivated()) {
log.info("Offer still enabled at mkt price {} < {} trigger price",
mktPrice,
offer.getTriggerPrice());
sleep(1000 * 60); // 60s
} else {
log.info("Successful test completion after offer disabled at mkt price {} > {} trigger price.",
mktPrice,
offer.getTriggerPrice());
break;
}
if (numIterations == MAX_ITERATIONS)
fail("Offer never disabled");
genBtcBlocksThenWait(1, 0);
}
}
protected static boolean envLongRunningTestEnabled() {
String envName = "LONG_RUNNING_OFFER_DEACTIVATION_TEST_ENABLED";
String envX = getenv(envName);
if (envX != null) {
log.info("Enabled, found {}.", envName);
return true;
} else {
log.info("Skipped, no environment variable {} defined.", envName);
log.info("To enable on Mac OS or Linux:"
+ "\tIf running in terminal, export LONG_RUNNING_OFFER_DEACTIVATION_TEST_ENABLED=true in bash shell."
+ "\tIf running in Intellij, set LONG_RUNNING_OFFER_DEACTIVATION_TEST_ENABLED=true in launcher's Environment variables field.");
return false;
}
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/scenario/LongRunningTradesTest.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.apitest.scenario;
import haveno.apitest.method.trade.AbstractTradeTest;
import haveno.apitest.method.trade.TakeBuyBTCOfferTest;
import haveno.apitest.method.trade.TakeSellBTCOfferTest;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.condition.EnabledIf;
import static java.lang.System.getenv;
@EnabledIf("envLongRunningTestEnabled")
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class LongRunningTradesTest extends AbstractTradeTest {
@Test
@Order(1)
public void TradeLoop(final TestInfo testInfo) {
int numTrades = 0;
while (numTrades < 50) {
log.info("*******************************************************************");
log.info("Trade # {}", ++numTrades);
log.info("*******************************************************************");
EXPECTED_PROTOCOL_STATUS.init();
testTakeBuyBTCOffer(testInfo);
genBtcBlocksThenWait(1, 1000 * 15);
log.info("*******************************************************************");
log.info("Trade # {}", ++numTrades);
log.info("*******************************************************************");
EXPECTED_PROTOCOL_STATUS.init();
testTakeSellBTCOffer(testInfo);
genBtcBlocksThenWait(1, 1000 * 15);
}
}
public void testTakeBuyBTCOffer(final TestInfo testInfo) {
TakeBuyBTCOfferTest test = new TakeBuyBTCOfferTest();
setLongRunningTest(true);
test.testTakeAlicesBuyOffer(testInfo);
test.testAlicesConfirmPaymentSent(testInfo);
test.testBobsConfirmPaymentReceived(testInfo);
}
public void testTakeSellBTCOffer(final TestInfo testInfo) {
TakeSellBTCOfferTest test = new TakeSellBTCOfferTest();
setLongRunningTest(true);
test.testTakeAlicesSellOffer(testInfo);
test.testBobsConfirmPaymentSent(testInfo);
test.testAlicesConfirmPaymentReceived(testInfo);
}
protected static boolean envLongRunningTestEnabled() {
String envName = "LONG_RUNNING_TRADES_TEST_ENABLED";
String envX = getenv(envName);
if (envX != null) {
log.info("Enabled, found {}.", envName);
return true;
} else {
log.info("Skipped, no environment variable {} defined.", envName);
log.info("To enable on Mac OS or Linux:"
+ "\tIf running in terminal, export LONG_RUNNING_TRADES_TEST_ENABLED=true in bash shell."
+ "\tIf running in Intellij, set LONG_RUNNING_TRADES_TEST_ENABLED=true in launcher's Environment variables field.");
return false;
}
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/scenario/OfferTest.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.apitest.scenario;
import haveno.apitest.method.offer.AbstractOfferTest;
import haveno.apitest.method.offer.CancelOfferTest;
import haveno.apitest.method.offer.CreateOfferUsingFixedPriceTest;
import haveno.apitest.method.offer.CreateOfferUsingMarketPriceMarginTest;
import haveno.apitest.method.offer.CreateXMROffersTest;
import haveno.apitest.method.offer.ValidateCreateOfferTest;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class OfferTest extends AbstractOfferTest {
@BeforeAll
public static void setUp() {
setUp(false); // Use setUp(true) for running API daemons in remote debug mode.
}
@Test
@Order(1)
public void testCreateOfferValidation() {
ValidateCreateOfferTest test = new ValidateCreateOfferTest();
test.testAmtTooLargeShouldThrowException();
test.testNoMatchingEURPaymentAccountShouldThrowException();
test.testNoMatchingCADPaymentAccountShouldThrowException();
}
@Test
@Order(2)
public void testCancelOffer() {
CancelOfferTest test = new CancelOfferTest();
test.testCancelOffer();
}
@Test
@Order(3)
public void testCreateOfferUsingFixedPrice() {
CreateOfferUsingFixedPriceTest test = new CreateOfferUsingFixedPriceTest();
test.testCreateAUDBTCBuyOfferUsingFixedPrice16000();
test.testCreateUSDBTCBuyOfferUsingFixedPrice100001234();
test.testCreateEURBTCSellOfferUsingFixedPrice95001234();
}
@Test
@Order(4)
public void testCreateOfferUsingMarketPriceMarginPct() {
CreateOfferUsingMarketPriceMarginTest test = new CreateOfferUsingMarketPriceMarginTest();
test.testCreateUSDBTCBuyOffer5PctPriceMargin();
test.testCreateNZDBTCBuyOfferMinus2PctPriceMargin();
test.testCreateGBPBTCSellOfferMinus1Point5PctPriceMargin();
test.testCreateBRLBTCSellOffer6Point55PctPriceMargin();
test.testCreateUSDBTCBuyOfferWithTriggerPrice();
}
@Test
@Order(6)
public void testCreateXMROffers() {
CreateXMROffersTest test = new CreateXMROffersTest();
CreateXMROffersTest.createXmrPaymentAccounts();
test.testCreateFixedPriceBuy1BTCFor200KXMROffer();
test.testCreateFixedPriceSell1BTCFor200KXMROffer();
test.testCreatePriceMarginBasedBuy1BTCOfferWithTriggerPrice();
test.testCreatePriceMarginBasedSell1BTCOffer();
test.testGetAllMyXMROffers();
test.testGetAvailableXMROffers();
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/scenario/PaymentAccountTest.java
================================================
package haveno.apitest.scenario;
import haveno.apitest.method.payment.AbstractPaymentAccountTest;
import haveno.apitest.method.payment.CreatePaymentAccountTest;
import haveno.apitest.method.payment.GetPaymentMethodsTest;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestMethodOrder;
import static haveno.apitest.Scaffold.BitcoinCoreApp.bitcoind;
import static haveno.apitest.config.HavenoAppConfig.alicedaemon;
import static haveno.apitest.config.HavenoAppConfig.seednode;
import static org.junit.jupiter.api.Assertions.fail;
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class PaymentAccountTest extends AbstractPaymentAccountTest {
@BeforeAll
public static void setUp() {
try {
setUpScaffold(bitcoind, seednode, alicedaemon);
} catch (Exception ex) {
fail(ex);
}
}
@Test
@Order(1)
public void testGetPaymentMethods() {
GetPaymentMethodsTest test = new GetPaymentMethodsTest();
test.testGetPaymentMethods();
}
@Test
@Order(2)
public void testCreatePaymentAccount(TestInfo testInfo) {
CreatePaymentAccountTest test = new CreatePaymentAccountTest();
test.testCreateAdvancedCashAccount(testInfo);
test.testCreateAliPayAccount(testInfo);
test.testCreateAustraliaPayidAccount(testInfo);
test.testCreateCapitualAccount(testInfo);
test.testCreateCashDepositAccount(testInfo);
test.testCreateBrazilNationalBankAccount(testInfo);
test.testCreateZelleAccount(testInfo);
test.testCreateF2FAccount(testInfo);
test.testCreateFasterPaymentsAccount(testInfo);
test.testCreateHalCashAccount(testInfo);
test.testCreateInteracETransferAccount(testInfo);
test.testCreateJapanBankAccount(testInfo);
test.testCreateMoneyBeamAccount(testInfo);
test.testCreateMoneyGramAccount(testInfo);
test.testCreatePerfectMoneyAccount(testInfo);
test.testCreatePaxumAccount(testInfo);
test.testCreatePayseraAccount(testInfo);
test.testCreatePopmoneyAccount(testInfo);
test.testCreatePromptPayAccount(testInfo);
test.testCreateRevolutAccount(testInfo);
test.testCreateSameBankAccount(testInfo);
test.testCreateSepaInstantAccount(testInfo);
test.testCreateSepaAccount(testInfo);
test.testCreateSpecificBanksAccount(testInfo);
test.testCreateSwiftAccount(testInfo);
test.testCreateSwishAccount(testInfo);
test.testCreateTransferwiseAccountWith1TradeCurrency(testInfo);
test.testCreateTransferwiseAccountWith10TradeCurrencies(testInfo);
test.testCreateTransferwiseAccountWithSupportedTradeCurrencies(testInfo);
test.testCreateTransferwiseAccountWithInvalidBrlTradeCurrencyShouldThrowException(testInfo);
test.testCreateTransferwiseAccountWithoutTradeCurrenciesShouldThrowException(testInfo);
test.testCreateUpholdAccount(testInfo);
test.testCreateUSPostalMoneyOrderAccount(testInfo);
test.testCreateWeChatPayAccount(testInfo);
test.testCreateWesternUnionAccount(testInfo);
}
@AfterAll
public static void tearDown() {
tearDownScaffold();
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/scenario/ScriptedBotTest.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.apitest.scenario;
import haveno.apitest.config.ApiTestConfig;
import haveno.apitest.method.BitcoinCliHelper;
import haveno.apitest.scenario.bot.AbstractBotTest;
import haveno.apitest.scenario.bot.BotClient;
import haveno.apitest.scenario.bot.RobotBob;
import haveno.apitest.scenario.bot.script.BashScriptGenerator;
import haveno.apitest.scenario.bot.shutdown.ManualBotShutdownException;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.condition.EnabledIf;
import static haveno.apitest.Scaffold.BitcoinCoreApp.bitcoind;
import static haveno.apitest.config.HavenoAppConfig.alicedaemon;
import static haveno.apitest.config.HavenoAppConfig.arbdaemon;
import static haveno.apitest.config.HavenoAppConfig.bobdaemon;
import static haveno.apitest.config.HavenoAppConfig.seednode;
import static haveno.apitest.scenario.bot.shutdown.ManualShutdown.startShutdownTimer;
import static org.junit.jupiter.api.Assertions.fail;
// The test case is enabled if AbstractBotTest#botScriptExists() returns true.
@EnabledIf("botScriptExists")
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class ScriptedBotTest extends AbstractBotTest {
private RobotBob robotBob;
@BeforeAll
public static void startTestHarness() {
botScript = deserializeBotScript();
if (botScript.isUseTestHarness()) {
startSupportingApps(true,
true,
bitcoind,
seednode,
arbdaemon,
alicedaemon,
bobdaemon);
} else {
// We need just enough configurations to make sure Bob and testers use
// the right apiPassword, to create a bitcoin-cli helper, and RobotBob's
// gRPC stubs. But the user will have to register dispute agents before
// an offer can be taken.
config = new ApiTestConfig("--apiPassword", "xyz");
bitcoinCli = new BitcoinCliHelper(config);
log.warn("Don't forget to register dispute agents before trying to trade with me.");
}
botClient = new BotClient(bobClient);
}
@BeforeEach
public void initRobotBob() {
try {
BashScriptGenerator bashScriptGenerator = getBashScriptGenerator();
robotBob = new RobotBob(botClient, botScript, bitcoinCli, bashScriptGenerator);
} catch (Exception ex) {
fail(ex);
}
}
@Test
@Order(1)
public void runRobotBob() {
try {
startShutdownTimer();
robotBob.run();
} catch (ManualBotShutdownException ex) {
// This exception is thrown if a /tmp/bottest-shutdown file was found.
// You can also kill -15
// of worker.org.gradle.process.internal.worker.GradleWorkerMain 'Gradle Test Executor #'
//
// This will cleanly shut everything down as well, but you will see a
// Process 'Gradle Test Executor #' finished with non-zero exit value 143 error,
// which you may think is a test failure.
log.warn("{} Shutting down test case before test completion;"
+ " this is not a test failure.",
ex.getMessage());
} catch (Throwable throwable) {
fail(throwable);
}
}
@AfterAll
public static void tearDown() {
if (botScript.isUseTestHarness())
tearDownScaffold();
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/scenario/StartupTest.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.apitest.scenario;
import haveno.apitest.method.CallRateMeteringInterceptorTest;
import haveno.apitest.method.GetMethodHelpTest;
import haveno.apitest.method.GetVersionTest;
import haveno.apitest.method.MethodTest;
import haveno.apitest.method.RegisterDisputeAgentsTest;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import java.io.File;
import java.io.IOException;
import static haveno.apitest.Scaffold.BitcoinCoreApp.bitcoind;
import static haveno.apitest.config.ApiTestRateMeterInterceptorConfig.getTestRateMeterInterceptorConfig;
import static haveno.apitest.config.HavenoAppConfig.alicedaemon;
import static haveno.apitest.config.HavenoAppConfig.arbdaemon;
import static haveno.apitest.config.HavenoAppConfig.seednode;
import static haveno.common.file.FileUtil.deleteFileIfExists;
import static org.junit.jupiter.api.Assertions.fail;
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class StartupTest extends MethodTest {
private static File callRateMeteringConfigFile;
@BeforeAll
public static void setUp() {
try {
callRateMeteringConfigFile = getTestRateMeterInterceptorConfig();
startSupportingApps(callRateMeteringConfigFile,
false,
false,
bitcoind, seednode, arbdaemon, alicedaemon);
} catch (Exception ex) {
fail(ex);
}
}
@Test
@Order(1)
public void testCallRateMeteringInterceptor() {
CallRateMeteringInterceptorTest test = new CallRateMeteringInterceptorTest();
test.testGetVersionCall1IsAllowed();
test.sleep200Milliseconds();
test.testGetVersionCall2ShouldThrowException();
test.sleep200Milliseconds();
test.testGetVersionCall3ShouldThrowException();
test.sleep200Milliseconds();
test.testGetVersionCall4IsAllowed();
sleep(1000); // Wait 1 second before calling getversion in next test.
}
@Test
@Order(2)
public void testGetVersion() {
GetVersionTest test = new GetVersionTest();
test.testGetVersion();
}
@Test
@Order(3)
public void testRegisterDisputeAgents() {
RegisterDisputeAgentsTest test = new RegisterDisputeAgentsTest();
test.testRegisterArbitratorShouldThrowException();
test.testInvalidDisputeAgentTypeArgShouldThrowException();
test.testInvalidRegistrationKeyArgShouldThrowException();
test.testRegisterMediator();
test.testRegisterRefundAgent();
}
@Test
@Order(4)
public void testGetCreateOfferHelp() {
GetMethodHelpTest test = new GetMethodHelpTest();
test.testGetCreateOfferHelp();
}
@AfterAll
public static void tearDown() {
try {
deleteFileIfExists(callRateMeteringConfigFile);
} catch (IOException ex) {
log.error(ex.getMessage());
}
tearDownScaffold();
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/scenario/TradeTest.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.apitest.scenario;
import haveno.apitest.method.trade.AbstractTradeTest;
import haveno.apitest.method.trade.TakeBuyBTCOfferTest;
import haveno.apitest.method.trade.TakeBuyBTCOfferWithNationalBankAcctTest;
import haveno.apitest.method.trade.TakeBuyXMROfferTest;
import haveno.apitest.method.trade.TakeSellBTCOfferTest;
import haveno.apitest.method.trade.TakeSellXMROfferTest;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestMethodOrder;
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class TradeTest extends AbstractTradeTest {
@BeforeEach
public void init() {
EXPECTED_PROTOCOL_STATUS.init();
}
@Test
@Order(1)
public void testTakeBuyBTCOffer(final TestInfo testInfo) {
TakeBuyBTCOfferTest test = new TakeBuyBTCOfferTest();
test.testTakeAlicesBuyOffer(testInfo);
test.testAlicesConfirmPaymentSent(testInfo);
test.testBobsConfirmPaymentReceived(testInfo);
}
@Test
@Order(2)
public void testTakeSellBTCOffer(final TestInfo testInfo) {
TakeSellBTCOfferTest test = new TakeSellBTCOfferTest();
test.testTakeAlicesSellOffer(testInfo);
test.testBobsConfirmPaymentSent(testInfo);
test.testAlicesConfirmPaymentReceived(testInfo);
}
@Test
@Order(4)
public void testTakeBuyBTCOfferWithNationalBankAcct(final TestInfo testInfo) {
TakeBuyBTCOfferWithNationalBankAcctTest test = new TakeBuyBTCOfferWithNationalBankAcctTest();
test.testTakeAlicesBuyOffer(testInfo);
test.testBankAcctDetailsIncludedInContracts(testInfo);
test.testAlicesConfirmPaymentSent(testInfo);
test.testBobsConfirmPaymentReceived(testInfo);
}
@Test
@Order(6)
public void testTakeBuyXMROffer(final TestInfo testInfo) {
TakeBuyXMROfferTest test = new TakeBuyXMROfferTest();
TakeBuyXMROfferTest.createXmrPaymentAccounts();
test.testTakeAlicesSellBTCForXMROffer(testInfo);
test.testBobsConfirmPaymentSent(testInfo);
test.testAlicesConfirmPaymentReceived(testInfo);
}
@Test
@Order(7)
public void testTakeSellXMROffer(final TestInfo testInfo) {
TakeSellXMROfferTest test = new TakeSellXMROfferTest();
TakeBuyXMROfferTest.createXmrPaymentAccounts();
test.testTakeAlicesBuyBTCForXMROffer(testInfo);
test.testAlicesConfirmPaymentSent(testInfo);
test.testBobsConfirmPaymentReceived(testInfo);
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/scenario/WalletTest.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.apitest.scenario;
import haveno.apitest.method.MethodTest;
import haveno.apitest.method.wallet.BtcWalletTest;
import haveno.apitest.method.wallet.WalletProtectionTest;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestMethodOrder;
import static haveno.apitest.Scaffold.BitcoinCoreApp.bitcoind;
import static haveno.apitest.config.HavenoAppConfig.alicedaemon;
import static haveno.apitest.config.HavenoAppConfig.arbdaemon;
import static haveno.apitest.config.HavenoAppConfig.bobdaemon;
import static haveno.apitest.config.HavenoAppConfig.seednode;
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class WalletTest extends MethodTest {
// Batching all wallet tests in this test case reduces scaffold setup
// time. Here, we create a method WalletProtectionTest instance and run each
// test in declared order.
@BeforeAll
public static void setUp() {
startSupportingApps(true,
false,
bitcoind,
seednode,
arbdaemon,
alicedaemon,
bobdaemon);
}
@Test
@Order(1)
public void testBtcWalletFunding(final TestInfo testInfo) {
BtcWalletTest btcWalletTest = new BtcWalletTest();
btcWalletTest.testInitialBtcBalances(testInfo);
btcWalletTest.testFundAlicesBtcWallet(testInfo);
}
@Test
@Order(3)
public void testWalletProtection() {
WalletProtectionTest walletProtectionTest = new WalletProtectionTest();
walletProtectionTest.testSetWalletPassword();
walletProtectionTest.testGetBalanceOnEncryptedWalletShouldThrowException();
walletProtectionTest.testUnlockWalletFor4Seconds();
walletProtectionTest.testGetBalanceAfterUnlockTimeExpiryShouldThrowException();
walletProtectionTest.testLockWalletBeforeUnlockTimeoutExpiry();
walletProtectionTest.testLockWalletWhenWalletAlreadyLockedShouldThrowException();
walletProtectionTest.testUnlockWalletTimeoutOverride();
walletProtectionTest.testSetNewWalletPassword();
walletProtectionTest.testSetNewWalletPasswordWithIncorrectNewPasswordShouldThrowException();
walletProtectionTest.testRemoveNewWalletPassword();
}
@AfterAll
public static void tearDown() {
tearDownScaffold();
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/scenario/bot/AbstractBotTest.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.apitest.scenario.bot;
import com.google.gson.GsonBuilder;
import haveno.apitest.method.MethodTest;
import haveno.apitest.scenario.bot.script.BashScriptGenerator;
import haveno.apitest.scenario.bot.script.BotScript;
import haveno.core.locale.Country;
import lombok.extern.slf4j.Slf4j;
import protobuf.PaymentAccount;
import java.io.File;
import java.io.IOException;
import java.nio.file.Paths;
import static haveno.core.locale.CountryUtil.findCountryByCode;
import static haveno.core.payment.payload.PaymentMethod.ZELLE_ID;
import static haveno.core.payment.payload.PaymentMethod.getPaymentMethod;
import static java.lang.String.format;
import static java.lang.System.getProperty;
import static java.nio.file.Files.readAllBytes;
@Slf4j
public abstract class AbstractBotTest extends MethodTest {
protected static final String BOT_SCRIPT_NAME = "bot-script.json";
protected static BotScript botScript;
protected static BotClient botClient;
protected BashScriptGenerator getBashScriptGenerator() {
if (botScript.isUseTestHarness()) {
PaymentAccount alicesAccount = createAlicesPaymentAccount();
botScript.setPaymentAccountIdForCliScripts(alicesAccount.getId());
}
return new BashScriptGenerator(config.apiPassword,
botScript.getApiPortForCliScripts(),
botScript.getPaymentAccountIdForCliScripts(),
botScript.isPrintCliScripts());
}
private PaymentAccount createAlicesPaymentAccount() {
BotPaymentAccountGenerator accountGenerator =
new BotPaymentAccountGenerator(new BotClient(aliceClient));
String paymentMethodId = botScript.getBotPaymentMethodId();
if (paymentMethodId != null) {
if (paymentMethodId.equals(ZELLE_ID)) {
// Only Zelle test accts are supported now.
return accountGenerator.createZellePaymentAccount(
"Alice's Zelle Account",
"Alice");
} else {
throw new UnsupportedOperationException(
format("This test harness bot does not work with %s payment accounts yet.",
getPaymentMethod(paymentMethodId).getDisplayString()));
}
} else {
String countryCode = botScript.getCountryCode();
Country country = findCountryByCode(countryCode).orElseThrow(() ->
new IllegalArgumentException(countryCode + " is not a valid iso country code."));
return accountGenerator.createF2FPaymentAccount(country,
"Alice's " + country.name + " F2F Account");
}
}
protected static BotScript deserializeBotScript() {
try {
File botScriptFile = new File(getProperty("java.io.tmpdir"), BOT_SCRIPT_NAME);
String json = new String(readAllBytes(Paths.get(botScriptFile.getPath())));
return new GsonBuilder().setPrettyPrinting().create().fromJson(json, BotScript.class);
} catch (IOException ex) {
throw new IllegalStateException("Error reading script bot file contents.", ex);
}
}
@SuppressWarnings("unused") // This is used by the jupiter framework.
protected static boolean botScriptExists() {
File botScriptFile = new File(getProperty("java.io.tmpdir"), BOT_SCRIPT_NAME);
if (botScriptFile.exists()) {
botScriptFile.deleteOnExit();
log.info("Enabled, found {}.", botScriptFile.getPath());
return true;
} else {
log.info("Skipped, no bot script.\n\tTo generate a bot-script.json file, see BotScriptGenerator.");
return false;
}
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/scenario/bot/Bot.java
================================================
package haveno.apitest.scenario.bot;
import haveno.apitest.method.BitcoinCliHelper;
import haveno.apitest.scenario.bot.script.BashScriptGenerator;
import haveno.apitest.scenario.bot.script.BotScript;
import haveno.core.locale.Country;
import lombok.extern.slf4j.Slf4j;
import protobuf.PaymentAccount;
import static haveno.core.locale.CountryUtil.findCountryByCode;
import static haveno.core.payment.payload.PaymentMethod.ZELLE_ID;
import static haveno.core.payment.payload.PaymentMethod.getPaymentMethod;
import static java.lang.String.format;
import static java.util.concurrent.TimeUnit.MINUTES;
@Slf4j
public
class Bot {
static final String MAKE = "MAKE";
static final String TAKE = "TAKE";
protected final BotClient botClient;
protected final BitcoinCliHelper bitcoinCli;
protected final BashScriptGenerator bashScriptGenerator;
protected final String[] actions;
protected final long protocolStepTimeLimitInMs;
protected final boolean stayAlive;
protected final boolean isUsingTestHarness;
protected final PaymentAccount paymentAccount;
public Bot(BotClient botClient,
BotScript botScript,
BitcoinCliHelper bitcoinCli,
BashScriptGenerator bashScriptGenerator) {
this.botClient = botClient;
this.bitcoinCli = bitcoinCli;
this.bashScriptGenerator = bashScriptGenerator;
this.actions = botScript.getActions();
this.protocolStepTimeLimitInMs = MINUTES.toMillis(botScript.getProtocolStepTimeLimitInMinutes());
this.stayAlive = botScript.isStayAlive();
this.isUsingTestHarness = botScript.isUseTestHarness();
if (isUsingTestHarness)
this.paymentAccount = createBotPaymentAccount(botScript);
else
this.paymentAccount = botClient.getPaymentAccount(botScript.getPaymentAccountIdForBot());
}
private PaymentAccount createBotPaymentAccount(BotScript botScript) {
BotPaymentAccountGenerator accountGenerator = new BotPaymentAccountGenerator(botClient);
String paymentMethodId = botScript.getBotPaymentMethodId();
if (paymentMethodId != null) {
if (paymentMethodId.equals(ZELLE_ID)) {
return accountGenerator.createZellePaymentAccount("Bob's Zelle Account",
"Bob");
} else {
throw new UnsupportedOperationException(
format("This bot test does not work with %s payment accounts yet.",
getPaymentMethod(paymentMethodId).getDisplayString()));
}
} else {
Country country = findCountry(botScript.getCountryCode());
return accountGenerator.createF2FPaymentAccount(country, country.name + " F2F Account");
}
}
private Country findCountry(String countryCode) {
return findCountryByCode(countryCode).orElseThrow(() ->
new IllegalArgumentException(countryCode + " is not a valid iso country code."));
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/scenario/bot/BotClient.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.apitest.scenario.bot;
import haveno.cli.GrpcClient;
import haveno.proto.grpc.BalancesInfo;
import haveno.proto.grpc.GetPaymentAccountsRequest;
import haveno.proto.grpc.OfferInfo;
import haveno.proto.grpc.TradeInfo;
import lombok.extern.slf4j.Slf4j;
import protobuf.PaymentAccount;
import java.text.DecimalFormat;
import java.util.List;
import java.util.function.BiPredicate;
import static org.apache.commons.lang3.StringUtils.capitalize;
/**
* Convenience GrpcClient wrapper for bots using gRPC services.
*/
@SuppressWarnings({"unused"})
@Slf4j
public class BotClient {
private static final DecimalFormat FIXED_PRICE_FMT = new DecimalFormat("###########0");
private final GrpcClient grpcClient;
public BotClient(GrpcClient grpcClient) {
this.grpcClient = grpcClient;
}
/**
* Returns current balance information.
* @return BalancesInfo
*/
public BalancesInfo getBalance() {
return grpcClient.getBalances();
}
/**
* Return the most recent BTC market price for the given currencyCode.
* @param currencyCode
* @return double
*/
public double getCurrentBTCMarketPrice(String currencyCode) {
return grpcClient.getBtcPrice(currencyCode);
}
/**
* Return the most recent BTC market price for the given currencyCode as an integer string.
* @param currencyCode
* @return String
*/
public String getCurrentBTCMarketPriceAsIntegerString(String currencyCode) {
return FIXED_PRICE_FMT.format(getCurrentBTCMarketPrice(currencyCode));
}
/**
* Return all BUY and SELL offers for the given currencyCode.
* @param currencyCode
* @return List
*/
public List getOffers(String currencyCode) {
var buyOffers = getBuyOffers(currencyCode);
if (buyOffers.size() > 0) {
return buyOffers;
} else {
return getSellOffers(currencyCode);
}
}
/**
* Return BUY offers for the given currencyCode.
* @param currencyCode
* @return List
*/
public List getBuyOffers(String currencyCode) {
return grpcClient.getOffers("BUY", currencyCode);
}
/**
* Return SELL offers for the given currencyCode.
* @param currencyCode
* @return List
*/
public List getSellOffers(String currencyCode) {
return grpcClient.getOffers("SELL", currencyCode);
}
/**
* Create and return a new Offer using a market based price.
* @param paymentAccount
* @param direction
* @param currencyCode
* @param amountInSatoshis
* @param minAmountInSatoshis
* @param priceMarginAsPercent
* @param securityDepositAsPercent
* @param feeCurrency
* @param triggerPrice
* @return OfferInfo
*/
public OfferInfo createOfferAtMarketBasedPrice(PaymentAccount paymentAccount,
String direction,
String currencyCode,
long amountInSatoshis,
long minAmountInSatoshis,
double priceMarginAsPercent,
double securityDepositAsPercent,
String triggerPrice) {
return grpcClient.createMarketBasedPricedOffer(direction,
currencyCode,
amountInSatoshis,
minAmountInSatoshis,
priceMarginAsPercent,
securityDepositAsPercent,
paymentAccount.getId(),
triggerPrice);
}
/**
* Create and return a new Offer using a fixed price.
* @param paymentAccount
* @param direction
* @param currencyCode
* @param amountInSatoshis
* @param minAmountInSatoshis
* @param fixedOfferPriceAsString
* @param securityDepositAsPercent
* @param feeCurrency
* @return OfferInfo
*/
public OfferInfo createOfferAtFixedPrice(PaymentAccount paymentAccount,
String direction,
String currencyCode,
long amountInSatoshis,
long minAmountInSatoshis,
String fixedOfferPriceAsString,
double securityDepositAsPercent) {
return grpcClient.createFixedPricedOffer(direction,
currencyCode,
amountInSatoshis,
minAmountInSatoshis,
fixedOfferPriceAsString,
securityDepositAsPercent,
paymentAccount.getId());
}
public TradeInfo takeOffer(String offerId, PaymentAccount paymentAccount) {
return grpcClient.takeOffer(offerId, paymentAccount.getId());
}
/**
* Returns a persisted Trade with the given tradeId, or throws an exception.
* @param tradeId
* @return TradeInfo
*/
public TradeInfo getTrade(String tradeId) {
return grpcClient.getTrade(tradeId);
}
/**
* Predicate returns true if the given exception indicates the trade with the given
* tradeId exists, but the trade's contract has not been fully prepared.
*/
public final BiPredicate tradeContractIsNotReady = (exception, tradeId) -> {
if (exception.getMessage().contains("no contract was found")) {
log.warn("Trade {} exists but is not fully prepared: {}.",
tradeId,
toCleanGrpcExceptionMessage(exception));
return true;
} else {
return false;
}
};
/**
* Returns a trade's contract as a Json string, or null if the trade exists
* but the contract is not ready.
* @param tradeId
* @return String
*/
public String getTradeContract(String tradeId) {
try {
var trade = grpcClient.getTrade(tradeId);
return trade.getContractAsJson();
} catch (Exception ex) {
if (tradeContractIsNotReady.test(ex, tradeId))
return null;
else
throw ex;
}
}
/**
* Returns true if the trade's taker deposit fee transaction has been published.
* @param tradeId a valid trade id
* @return boolean
*/
public boolean isTakerDepositFeeTxPublished(String tradeId) {
return grpcClient.getTrade(tradeId).getIsPayoutPublished();
}
/**
* Returns true if the trade's taker deposit fee transaction has been confirmed.
* @param tradeId a valid trade id
* @return boolean
*/
public boolean isTakerDepositFeeTxConfirmed(String tradeId) {
return grpcClient.getTrade(tradeId).getIsDepositsUnlocked();
}
/**
* Returns true if the trade's 'start payment' message has been sent by the buyer.
* @param tradeId a valid trade id
* @return boolean
*/
public boolean isTradePaymentSentSent(String tradeId) {
return grpcClient.getTrade(tradeId).getIsPaymentSent();
}
/**
* Returns true if the trade's 'payment received' message has been sent by the seller.
* @param tradeId a valid trade id
* @return boolean
*/
public boolean isTradePaymentReceivedConfirmationSent(String tradeId) {
return grpcClient.getTrade(tradeId).getIsPaymentReceived();
}
/**
* Returns true if the trade's payout transaction has been published.
* @param tradeId a valid trade id
* @return boolean
*/
public boolean isTradePayoutTxPublished(String tradeId) {
return grpcClient.getTrade(tradeId).getIsPayoutPublished();
}
/**
* Sends a 'confirm payment started message' for a trade with the given tradeId,
* or throws an exception.
* @param tradeId
*/
public void sendConfirmPaymentSentMessage(String tradeId) {
grpcClient.confirmPaymentSent(tradeId);
}
/**
* Sends a 'confirm payment received message' for a trade with the given tradeId,
* or throws an exception.
* @param tradeId
*/
public void sendConfirmPaymentReceivedMessage(String tradeId) {
grpcClient.confirmPaymentReceived(tradeId);
}
/**
* Create and save a new PaymentAccount with details in the given json.
* @param json
* @return PaymentAccount
*/
public PaymentAccount createNewPaymentAccount(String json) {
return grpcClient.createPaymentAccount(json);
}
/**
* Returns a persisted PaymentAccount with the given paymentAccountId, or throws
* an exception.
* @param paymentAccountId The id of the PaymentAccount being looked up.
* @return PaymentAccount
*/
public PaymentAccount getPaymentAccount(String paymentAccountId) {
return grpcClient.getPaymentAccounts().stream()
.filter(a -> (a.getId().equals(paymentAccountId)))
.findFirst()
.orElseThrow(() ->
new PaymentAccountNotFoundException("Could not find a payment account with id "
+ paymentAccountId + "."));
}
/**
* Returns a persisted PaymentAccount with the given accountName, or throws
* an exception.
* @param accountName
* @return PaymentAccount
*/
public PaymentAccount getPaymentAccountWithName(String accountName) {
var req = GetPaymentAccountsRequest.newBuilder().build();
return grpcClient.getPaymentAccounts().stream()
.filter(a -> (a.getAccountName().equals(accountName)))
.findFirst()
.orElseThrow(() ->
new PaymentAccountNotFoundException("Could not find a payment account with name "
+ accountName + "."));
}
public String toCleanGrpcExceptionMessage(Exception ex) {
return capitalize(ex.getMessage().replaceFirst("^[A-Z_]+: ", ""));
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/scenario/bot/BotPaymentAccountGenerator.java
================================================
package haveno.apitest.scenario.bot;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import haveno.core.api.model.PaymentAccountForm;
import haveno.core.locale.Country;
import lombok.extern.slf4j.Slf4j;
import protobuf.PaymentAccount;
import java.io.File;
import java.util.Map;
import static haveno.core.payment.payload.PaymentMethod.ZELLE_ID;
import static haveno.core.payment.payload.PaymentMethod.F2F_ID;
@Slf4j
public class BotPaymentAccountGenerator {
private final Gson gson = new GsonBuilder().setPrettyPrinting().serializeNulls().create();
private final BotClient botClient;
public BotPaymentAccountGenerator(BotClient botClient) {
this.botClient = botClient;
}
public PaymentAccount createF2FPaymentAccount(Country country, String accountName) {
try {
return botClient.getPaymentAccountWithName(accountName);
} catch (PaymentAccountNotFoundException ignored) {
// Ignore not found exception, create a new account.
}
Map p = getPaymentAccountFormMap(F2F_ID);
p.put("accountName", accountName);
p.put("city", country.name + " City");
p.put("country", country.code);
p.put("contact", "By Semaphore");
p.put("extraInfo", "");
// Convert the map back to a json string and create the payment account over gRPC.
return botClient.createNewPaymentAccount(gson.toJson(p));
}
public PaymentAccount createZellePaymentAccount(String accountName, String holderName) {
try {
return botClient.getPaymentAccountWithName(accountName);
} catch (PaymentAccountNotFoundException ignored) {
// Ignore not found exception, create a new account.
}
Map p = getPaymentAccountFormMap(ZELLE_ID);
p.put("accountName", accountName);
p.put("emailOrMobileNr", holderName + "@zelle.com");
p.put("holderName", holderName);
return botClient.createNewPaymentAccount(gson.toJson(p));
}
private Map getPaymentAccountFormMap(String paymentMethodId) {
File jsonFormTemplate = PaymentAccountForm.getPaymentAccountForm(paymentMethodId);
jsonFormTemplate.deleteOnExit();
String jsonString = PaymentAccountForm.toJsonString(jsonFormTemplate);
//noinspection unchecked
return (Map) gson.fromJson(jsonString, Object.class);
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/scenario/bot/InvalidRandomOfferException.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.apitest.scenario.bot;
import haveno.common.HavenoException;
@SuppressWarnings("unused")
public class InvalidRandomOfferException extends HavenoException {
public InvalidRandomOfferException(Throwable cause) {
super(cause);
}
public InvalidRandomOfferException(String format, Object... args) {
super(format, args);
}
public InvalidRandomOfferException(Throwable cause, String format, Object... args) {
super(cause, format, args);
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/scenario/bot/PaymentAccountNotFoundException.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.apitest.scenario.bot;
import haveno.common.HavenoException;
@SuppressWarnings("unused")
public class PaymentAccountNotFoundException extends HavenoException {
public PaymentAccountNotFoundException(Throwable cause) {
super(cause);
}
public PaymentAccountNotFoundException(String format, Object... args) {
super(format, args);
}
public PaymentAccountNotFoundException(Throwable cause, String format, Object... args) {
super(cause, format, args);
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/scenario/bot/RandomOffer.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.apitest.scenario.bot;
import haveno.proto.grpc.OfferInfo;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import protobuf.PaymentAccount;
import java.math.BigDecimal;
import java.security.SecureRandom;
import java.text.DecimalFormat;
import java.util.Objects;
import java.util.function.Supplier;
import static haveno.apitest.method.offer.AbstractOfferTest.defaultSecurityDepositPct;
import static haveno.cli.CurrencyFormat.formatInternalFiatPrice;
import static haveno.cli.CurrencyFormat.formatSatoshis;
import static haveno.common.util.MathUtils.scaleDownByPowerOf10;
import static haveno.core.payment.payload.PaymentMethod.F2F_ID;
import static java.lang.String.format;
import static java.math.RoundingMode.HALF_UP;
@Slf4j
public class RandomOffer {
private static final SecureRandom RANDOM = new SecureRandom();
private static final DecimalFormat FIXED_PRICE_FMT = new DecimalFormat("###########0");
@SuppressWarnings("FieldCanBeLocal")
// If not an F2F account, keep amount <= 0.01 BTC to avoid hitting unsigned
// acct trading limit.
private final Supplier nextAmount = () ->
this.getPaymentAccount().getPaymentMethod().getId().equals(F2F_ID)
? (long) (10000000 + RANDOM.nextInt(2500000))
: (long) (750000 + RANDOM.nextInt(250000));
@SuppressWarnings("FieldCanBeLocal")
private final Supplier nextMinAmount = () -> {
boolean useMinAmount = RANDOM.nextBoolean();
if (useMinAmount) {
return this.getPaymentAccount().getPaymentMethod().getId().equals(F2F_ID)
? this.getAmount() - 5000000L
: this.getAmount() - 50000L;
} else {
return this.getAmount();
}
};
@SuppressWarnings("FieldCanBeLocal")
private final Supplier nextPriceMargin = () -> {
boolean useZeroMargin = RANDOM.nextBoolean();
if (useZeroMargin) {
return 0.00;
} else {
BigDecimal min = BigDecimal.valueOf(-5.0).setScale(2, HALF_UP);
BigDecimal max = BigDecimal.valueOf(5.0).setScale(2, HALF_UP);
BigDecimal randomBigDecimal = min.add(BigDecimal.valueOf(RANDOM.nextDouble()).multiply(max.subtract(min)));
return randomBigDecimal.setScale(2, HALF_UP).doubleValue();
}
};
private final BotClient botClient;
@Getter
private final PaymentAccount paymentAccount;
@Getter
private final String direction;
@Getter
private final String currencyCode;
@Getter
private final long amount;
@Getter
private final long minAmount;
@Getter
private final boolean useMarketBasedPrice;
@Getter
private final double priceMargin;
@Getter
private String fixedOfferPrice = "0";
@Getter
private OfferInfo offer;
@Getter
private String id;
public RandomOffer(BotClient botClient, PaymentAccount paymentAccount) {
this.botClient = botClient;
this.paymentAccount = paymentAccount;
this.direction = RANDOM.nextBoolean() ? "BUY" : "SELL";
this.currencyCode = Objects.requireNonNull(paymentAccount.getSelectedTradeCurrency()).getCode();
this.amount = nextAmount.get();
this.minAmount = nextMinAmount.get();
this.useMarketBasedPrice = RANDOM.nextBoolean();
this.priceMargin = nextPriceMargin.get();
}
public RandomOffer create() throws InvalidRandomOfferException {
try {
printDescription();
if (useMarketBasedPrice) {
this.offer = botClient.createOfferAtMarketBasedPrice(paymentAccount,
direction,
currencyCode,
amount,
minAmount,
priceMargin,
defaultSecurityDepositPct.get(),
"0" /*no trigger price*/);
} else {
this.offer = botClient.createOfferAtFixedPrice(paymentAccount,
direction,
currencyCode,
amount,
minAmount,
fixedOfferPrice,
defaultSecurityDepositPct.get());
}
this.id = offer.getId();
return this;
} catch (Exception ex) {
String error = format("Could not create valid %s offer for %s BTC: %s",
currencyCode,
formatSatoshis(amount),
ex.getMessage());
throw new InvalidRandomOfferException(error, ex);
}
}
private void printDescription() {
double currentMarketPrice = botClient.getCurrentBTCMarketPrice(currencyCode);
// Calculate a fixed price based on the random mkt price margin, even if we don't use it.
double differenceFromMarketPrice = currentMarketPrice * scaleDownByPowerOf10(priceMargin, 2);
double fixedOfferPriceAsDouble = direction.equals("BUY")
? currentMarketPrice - differenceFromMarketPrice
: currentMarketPrice + differenceFromMarketPrice;
this.fixedOfferPrice = FIXED_PRICE_FMT.format(fixedOfferPriceAsDouble);
String description = format("Creating new %s %s / %s offer for amount = %s BTC, min-amount = %s BTC.",
useMarketBasedPrice ? "mkt-based-price" : "fixed-priced",
direction,
currencyCode,
formatSatoshis(amount),
formatSatoshis(minAmount));
log.info(description);
if (useMarketBasedPrice) {
log.info("Offer Price Margin = {}%", priceMargin);
log.info("Expected Offer Price = {} {}", formatInternalFiatPrice(Double.parseDouble(fixedOfferPrice)), currencyCode);
} else {
log.info("Fixed Offer Price = {} {}", fixedOfferPrice, currencyCode);
}
log.info("Current Market Price = {} {}", formatInternalFiatPrice(currentMarketPrice), currencyCode);
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/scenario/bot/RobotBob.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.apitest.scenario.bot;
import haveno.apitest.method.BitcoinCliHelper;
import haveno.apitest.scenario.bot.protocol.BotProtocol;
import haveno.apitest.scenario.bot.protocol.MakerBotProtocol;
import haveno.apitest.scenario.bot.protocol.TakerBotProtocol;
import haveno.apitest.scenario.bot.script.BashScriptGenerator;
import haveno.apitest.scenario.bot.script.BotScript;
import haveno.apitest.scenario.bot.shutdown.ManualBotShutdownException;
import haveno.cli.table.builder.TableBuilder;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import static haveno.apitest.scenario.bot.protocol.ProtocolStep.DONE;
import static haveno.apitest.scenario.bot.shutdown.ManualShutdown.isShutdownCalled;
import static haveno.cli.table.builder.TableType.BTC_BALANCE_TBL;
import static java.util.concurrent.TimeUnit.SECONDS;
@Slf4j
public
class RobotBob extends Bot {
@Getter
private int numTrades;
public RobotBob(BotClient botClient,
BotScript botScript,
BitcoinCliHelper bitcoinCli,
BashScriptGenerator bashScriptGenerator) {
super(botClient, botScript, bitcoinCli, bashScriptGenerator);
}
public void run() {
for (String action : actions) {
checkActionIsValid(action);
BotProtocol botProtocol;
if (action.equalsIgnoreCase(MAKE)) {
botProtocol = new MakerBotProtocol(botClient,
paymentAccount,
protocolStepTimeLimitInMs,
bitcoinCli,
bashScriptGenerator);
} else {
botProtocol = new TakerBotProtocol(botClient,
paymentAccount,
protocolStepTimeLimitInMs,
bitcoinCli,
bashScriptGenerator);
}
botProtocol.run();
if (!botProtocol.getCurrentProtocolStep().equals(DONE)) {
throw new IllegalStateException(botProtocol.getClass().getSimpleName() + " failed to complete.");
}
StringBuilder balancesBuilder = new StringBuilder();
balancesBuilder.append("BTC").append("\n");
balancesBuilder.append(new TableBuilder(BTC_BALANCE_TBL, botClient.getBalance().getBtc()).build().toString()).append("\n");
log.info("Completed {} successful trade{}. Current Balance:\n{}",
++numTrades,
numTrades == 1 ? "" : "s",
balancesBuilder);
if (numTrades < actions.length) {
try {
SECONDS.sleep(20);
} catch (InterruptedException ignored) {
// empty
}
}
} // end of actions loop
if (stayAlive)
waitForManualShutdown();
else
warnCLIUserBeforeShutdown();
}
private void checkActionIsValid(String action) {
if (!action.equalsIgnoreCase(MAKE) && !action.equalsIgnoreCase(TAKE))
throw new IllegalStateException(action + " is not a valid bot action; must be 'make' or 'take'");
}
private void waitForManualShutdown() {
String harnessOrCase = isUsingTestHarness ? "harness" : "case";
log.info("All script actions have been completed, but the test {} will stay alive"
+ " until a /tmp/bottest-shutdown file is detected.",
harnessOrCase);
log.info("When ready to shutdown the test {}, run '$ touch /tmp/bottest-shutdown'.",
harnessOrCase);
if (!isUsingTestHarness) {
log.warn("You will have to manually shutdown the bitcoind and Haveno nodes"
+ " running outside of the test harness.");
}
try {
while (!isShutdownCalled()) {
SECONDS.sleep(10);
}
log.warn("Manual shutdown signal received.");
} catch (ManualBotShutdownException ex) {
log.warn(ex.getMessage());
} catch (InterruptedException ignored) {
// empty
}
}
private void warnCLIUserBeforeShutdown() {
if (isUsingTestHarness) {
long delayInSeconds = 30;
log.warn("All script actions have been completed. You have {} seconds to complete any"
+ " remaining tasks before the test harness shuts down.",
delayInSeconds);
try {
SECONDS.sleep(delayInSeconds);
} catch (InterruptedException ignored) {
// empty
}
} else {
log.info("Shutting down test case");
}
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/scenario/bot/protocol/BotProtocol.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.apitest.scenario.bot.protocol;
import haveno.apitest.method.BitcoinCliHelper;
import haveno.apitest.scenario.bot.BotClient;
import haveno.apitest.scenario.bot.script.BashScriptGenerator;
import haveno.apitest.scenario.bot.shutdown.ManualBotShutdownException;
import haveno.cli.table.builder.TableBuilder;
import haveno.proto.grpc.TradeInfo;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import protobuf.PaymentAccount;
import java.io.File;
import java.security.SecureRandom;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static haveno.apitest.scenario.bot.protocol.ProtocolStep.SEND_PAYMENT_RECEIVED_CONFIRMATION_MESSAGE;
import static haveno.apitest.scenario.bot.protocol.ProtocolStep.SEND_PAYMENT_SENT_MESSAGE;
import static haveno.apitest.scenario.bot.protocol.ProtocolStep.START;
import static haveno.apitest.scenario.bot.protocol.ProtocolStep.WAIT_FOR_PAYMENT_RECEIVED_CONFIRMATION_MESSAGE;
import static haveno.apitest.scenario.bot.protocol.ProtocolStep.WAIT_FOR_PAYMENT_SENT_MESSAGE;
import static haveno.apitest.scenario.bot.protocol.ProtocolStep.WAIT_FOR_PAYOUT_TX;
import static haveno.apitest.scenario.bot.protocol.ProtocolStep.WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED;
import static haveno.apitest.scenario.bot.protocol.ProtocolStep.WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED;
import static haveno.apitest.scenario.bot.shutdown.ManualShutdown.checkIfShutdownCalled;
import static haveno.cli.table.builder.TableType.TRADE_DETAIL_TBL;
import static java.lang.String.format;
import static java.lang.System.currentTimeMillis;
import static java.util.Arrays.stream;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
@Slf4j
public abstract class BotProtocol {
static final SecureRandom RANDOM = new SecureRandom();
static final String BUY = "BUY";
static final String SELL = "SELL";
protected final Supplier randomDelay = () -> (long) (2000 + RANDOM.nextInt(5000));
protected final AtomicLong protocolStepStartTime = new AtomicLong(0);
protected final Consumer initProtocolStep = (step) -> {
currentProtocolStep = step;
printBotProtocolStep();
protocolStepStartTime.set(currentTimeMillis());
};
@Getter
protected ProtocolStep currentProtocolStep;
@Getter // Functions within 'this' need the @Getter.
protected final BotClient botClient;
protected final PaymentAccount paymentAccount;
protected final String currencyCode;
protected final long protocolStepTimeLimitInMs;
protected final BitcoinCliHelper bitcoinCli;
@Getter
protected final BashScriptGenerator bashScriptGenerator;
public BotProtocol(BotClient botClient,
PaymentAccount paymentAccount,
long protocolStepTimeLimitInMs,
BitcoinCliHelper bitcoinCli,
BashScriptGenerator bashScriptGenerator) {
this.botClient = botClient;
this.paymentAccount = paymentAccount;
this.currencyCode = Objects.requireNonNull(paymentAccount.getSelectedTradeCurrency()).getCode();
this.protocolStepTimeLimitInMs = protocolStepTimeLimitInMs;
this.bitcoinCli = bitcoinCli;
this.bashScriptGenerator = bashScriptGenerator;
this.currentProtocolStep = START;
}
public abstract void run();
protected boolean isWithinProtocolStepTimeLimit() {
return (currentTimeMillis() - protocolStepStartTime.get()) < protocolStepTimeLimitInMs;
}
protected void checkIsStartStep() {
if (currentProtocolStep != START) {
throw new IllegalStateException("First bot protocol step must be " + START.name());
}
}
protected void printBotProtocolStep() {
log.info("Starting protocol step {}. Bot will shutdown if step not completed within {} minutes.",
currentProtocolStep.name(), MILLISECONDS.toMinutes(protocolStepTimeLimitInMs));
if (currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED)) {
log.info("Generate a btc block to trigger taker's deposit fee tx confirmation.");
createGenerateBtcBlockScript();
}
}
protected final Function waitForTakerFeeTxConfirm = (trade) -> {
sleep(5000);
waitForTakerFeeTxPublished(trade.getTradeId());
waitForTakerFeeTxConfirmed(trade.getTradeId());
return trade;
};
protected final Function waitForPaymentSentMessage = (trade) -> {
initProtocolStep.accept(WAIT_FOR_PAYMENT_SENT_MESSAGE);
try {
createPaymentSentScript(trade);
log.info(" Waiting for a 'payment started' message from buyer for trade with id {}.", trade.getTradeId());
while (isWithinProtocolStepTimeLimit()) {
checkIfShutdownCalled("Interrupted before checking if 'payment started' message has been sent.");
try {
var t = this.getBotClient().getTrade(trade.getTradeId());
if (t.getIsPaymentSent()) {
log.info("Buyer has started payment for trade:\n{}",
new TableBuilder(TRADE_DETAIL_TBL, t).build().toString());
return t;
}
} catch (Exception ex) {
throw new IllegalStateException(this.getBotClient().toCleanGrpcExceptionMessage(ex));
}
sleep(randomDelay.get());
} // end while
throw new IllegalStateException("Payment was never sent; we won't wait any longer.");
} catch (ManualBotShutdownException ex) {
throw ex; // not an error, tells bot to shutdown
} catch (Exception ex) {
throw new IllegalStateException("Error while waiting payment sent message.", ex);
}
};
protected final Function sendPaymentSentMessage = (trade) -> {
initProtocolStep.accept(SEND_PAYMENT_SENT_MESSAGE);
checkIfShutdownCalled("Interrupted before sending 'payment started' message.");
this.getBotClient().sendConfirmPaymentSentMessage(trade.getTradeId());
return trade;
};
protected final Function waitForPaymentReceivedConfirmation = (trade) -> {
initProtocolStep.accept(WAIT_FOR_PAYMENT_RECEIVED_CONFIRMATION_MESSAGE);
createPaymentReceivedScript(trade);
try {
log.info("Waiting for a 'payment received confirmation' message from seller for trade with id {}.", trade.getTradeId());
while (isWithinProtocolStepTimeLimit()) {
checkIfShutdownCalled("Interrupted before checking if 'payment received confirmation' message has been sent.");
try {
var t = this.getBotClient().getTrade(trade.getTradeId());
if (t.getIsPaymentReceived()) {
log.info("Seller has received payment for trade:\n{}",
new TableBuilder(TRADE_DETAIL_TBL, t).build().toString());
return t;
}
} catch (Exception ex) {
throw new IllegalStateException(this.getBotClient().toCleanGrpcExceptionMessage(ex));
}
sleep(randomDelay.get());
} // end while
throw new IllegalStateException("Payment was never received; we won't wait any longer.");
} catch (ManualBotShutdownException ex) {
throw ex; // not an error, tells bot to shutdown
} catch (Exception ex) {
throw new IllegalStateException("Error while waiting payment received confirmation message.", ex);
}
};
protected final Function sendPaymentReceivedMessage = (trade) -> {
initProtocolStep.accept(SEND_PAYMENT_RECEIVED_CONFIRMATION_MESSAGE);
checkIfShutdownCalled("Interrupted before sending 'payment received confirmation' message.");
this.getBotClient().sendConfirmPaymentReceivedMessage(trade.getTradeId());
return trade;
};
protected final Function waitForPayoutTx = (trade) -> {
initProtocolStep.accept(WAIT_FOR_PAYOUT_TX);
try {
log.info("Waiting on the 'payout tx published confirmation' for trade with id {}.", trade.getTradeId());
while (isWithinProtocolStepTimeLimit()) {
checkIfShutdownCalled("Interrupted before checking if payout tx has been published.");
try {
var t = this.getBotClient().getTrade(trade.getTradeId());
if (t.getIsPayoutPublished()) {
log.info("Payout tx {} has been published for trade:\n{}",
t.getPayoutTxId(),
new TableBuilder(TRADE_DETAIL_TBL, t).build().toString());
return t;
}
} catch (Exception ex) {
throw new IllegalStateException(this.getBotClient().toCleanGrpcExceptionMessage(ex));
}
sleep(randomDelay.get());
} // end while
throw new IllegalStateException("Payout tx was never published; we won't wait any longer.");
} catch (ManualBotShutdownException ex) {
throw ex; // not an error, tells bot to shutdown
} catch (Exception ex) {
throw new IllegalStateException("Error while waiting for published payout tx.", ex);
}
};
protected void createPaymentSentScript(TradeInfo trade) {
File script = bashScriptGenerator.createPaymentSentScript(trade);
printCliHintAndOrScript(script, "The manual CLI side can send a 'payment started' message");
}
protected void createPaymentReceivedScript(TradeInfo trade) {
File script = bashScriptGenerator.createPaymentReceivedScript(trade);
printCliHintAndOrScript(script, "The manual CLI side can sent a 'payment received confirmation' message");
}
protected void createKeepFundsScript(TradeInfo trade) {
File script = bashScriptGenerator.createKeepFundsScript(trade);
printCliHintAndOrScript(script, "The manual CLI side can close the trade");
}
protected void createGetBalanceScript() {
File script = bashScriptGenerator.createGetBalanceScript();
printCliHintAndOrScript(script, "The manual CLI side can view current balances");
}
protected void createGenerateBtcBlockScript() {
String newBitcoinCoreAddress = bitcoinCli.getNewBtcAddress();
File script = bashScriptGenerator.createGenerateBtcBlockScript(newBitcoinCoreAddress);
printCliHintAndOrScript(script, "The manual CLI side can generate 1 btc block");
}
protected void printCliHintAndOrScript(File script, String hint) {
log.info("{} by running bash script '{}'.", hint, script.getAbsolutePath());
if (this.getBashScriptGenerator().isPrintCliScripts())
this.getBashScriptGenerator().printCliScript(script, log);
sleep(5000); // Allow 5s for CLI user to read the hint.
}
protected void sleep(long ms) {
try {
MILLISECONDS.sleep(ms);
} catch (InterruptedException ignored) {
// empty
}
}
private void waitForTakerFeeTxPublished(String tradeId) {
waitForTakerDepositFee(tradeId, WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED);
}
private void waitForTakerFeeTxConfirmed(String tradeId) {
waitForTakerDepositFee(tradeId, WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED);
}
private void waitForTakerDepositFee(String tradeId, ProtocolStep depositTxProtocolStep) {
initProtocolStep.accept(depositTxProtocolStep);
validateCurrentProtocolStep(WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED, WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED);
try {
log.info(waitingForDepositFeeTxMsg(tradeId));
while (isWithinProtocolStepTimeLimit()) {
checkIfShutdownCalled("Interrupted before checking taker deposit fee tx is published and confirmed.");
try {
var trade = this.getBotClient().getTrade(tradeId);
if (isDepositFeeTxStepComplete.test(trade))
return;
else
sleep(randomDelay.get());
} catch (Exception ex) {
if (this.getBotClient().tradeContractIsNotReady.test(ex, tradeId))
sleep(randomDelay.get());
else
throw new IllegalStateException(this.getBotClient().toCleanGrpcExceptionMessage(ex));
}
} // end while
throw new IllegalStateException(stoppedWaitingForDepositFeeTxMsg(this.getBotClient().getTrade(tradeId).getTakerDepositTxId()));
} catch (ManualBotShutdownException ex) {
throw ex; // not an error, tells bot to shutdown
} catch (Exception ex) {
throw new IllegalStateException("Error while waiting for taker deposit tx to be published or confirmed.", ex);
}
}
private final Predicate isDepositFeeTxStepComplete = (trade) -> {
if (currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED) && trade.getIsDepositsPublished()) {
log.info("Taker deposit fee tx {} has been published.", trade.getTakerDepositTxId());
return true;
} else if (currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED) && trade.getIsDepositsUnlocked()) {
log.info("Taker deposit fee tx {} has been confirmed.", trade.getTakerDepositTxId());
return true;
} else {
return false;
}
};
private void validateCurrentProtocolStep(Enum>... validBotSteps) {
for (Enum> validBotStep : validBotSteps) {
if (currentProtocolStep.equals(validBotStep))
return;
}
throw new IllegalStateException("Unexpected bot step: " + currentProtocolStep.name() + ".\n"
+ "Must be one of "
+ stream(validBotSteps).map((Enum::name)).collect(Collectors.joining(","))
+ ".");
}
private String waitingForDepositFeeTxMsg(String tradeId) {
return format("Waiting for taker deposit fee tx for trade %s to be %s.",
tradeId,
currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED) ? "published" : "confirmed");
}
private String stoppedWaitingForDepositFeeTxMsg(String txId) {
return format("Taker deposit fee tx %s is took too long to be %s; we won't wait any longer.",
txId,
currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED) ? "published" : "confirmed");
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/scenario/bot/protocol/MakerBotProtocol.java
================================================
package haveno.apitest.scenario.bot.protocol;
import haveno.apitest.method.BitcoinCliHelper;
import haveno.apitest.scenario.bot.BotClient;
import haveno.apitest.scenario.bot.RandomOffer;
import haveno.apitest.scenario.bot.script.BashScriptGenerator;
import haveno.apitest.scenario.bot.shutdown.ManualBotShutdownException;
import haveno.cli.table.builder.TableBuilder;
import haveno.proto.grpc.OfferInfo;
import haveno.proto.grpc.TradeInfo;
import lombok.extern.slf4j.Slf4j;
import protobuf.PaymentAccount;
import java.io.File;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
import static haveno.apitest.scenario.bot.protocol.ProtocolStep.DONE;
import static haveno.apitest.scenario.bot.protocol.ProtocolStep.WAIT_FOR_OFFER_TAKER;
import static haveno.apitest.scenario.bot.shutdown.ManualShutdown.checkIfShutdownCalled;
import static haveno.cli.table.builder.TableType.OFFER_TBL;
import static haveno.cli.table.builder.TableType.TRADE_DETAIL_TBL;
@Slf4j
public class MakerBotProtocol extends BotProtocol {
public MakerBotProtocol(BotClient botClient,
PaymentAccount paymentAccount,
long protocolStepTimeLimitInMs,
BitcoinCliHelper bitcoinCli,
BashScriptGenerator bashScriptGenerator) {
super(botClient,
paymentAccount,
protocolStepTimeLimitInMs,
bitcoinCli,
bashScriptGenerator);
}
@Override
public void run() {
checkIsStartStep();
Function, TradeInfo> makeTrade = waitForNewTrade.andThen(waitForTakerFeeTxConfirm);
var trade = makeTrade.apply(randomOffer);
var makerIsBuyer = trade.getOffer().getDirection().equalsIgnoreCase(BUY);
Function completeTraditionalTransaction = makerIsBuyer
? sendPaymentSentMessage.andThen(waitForPaymentReceivedConfirmation)
: waitForPaymentSentMessage.andThen(sendPaymentReceivedMessage);
completeTraditionalTransaction.apply(trade);
currentProtocolStep = DONE;
}
private final Supplier randomOffer = () -> {
checkIfShutdownCalled("Interrupted before creating random offer.");
OfferInfo offer = new RandomOffer(botClient, paymentAccount).create().getOffer();
log.info("Created random {} offer\n{}", currencyCode, new TableBuilder(OFFER_TBL, offer).build());
return offer;
};
private final Function, TradeInfo> waitForNewTrade = (randomOffer) -> {
initProtocolStep.accept(WAIT_FOR_OFFER_TAKER);
OfferInfo offer = randomOffer.get();
createTakeOfferCliScript(offer);
try {
log.info("Impatiently waiting for offer {} to be taken, repeatedly calling gettrade.", offer.getId());
while (isWithinProtocolStepTimeLimit()) {
checkIfShutdownCalled("Interrupted while waiting for offer to be taken.");
try {
var trade = getNewTrade(offer.getId());
if (trade.isPresent())
return trade.get();
else
sleep(randomDelay.get());
} catch (Exception ex) {
throw new IllegalStateException(this.getBotClient().toCleanGrpcExceptionMessage(ex), ex);
}
} // end while
throw new IllegalStateException("Offer was never taken; we won't wait any longer.");
} catch (ManualBotShutdownException ex) {
throw ex; // not an error, tells bot to shutdown
} catch (Exception ex) {
throw new IllegalStateException("Error while waiting for offer to be taken.", ex);
}
};
private Optional getNewTrade(String offerId) {
try {
var trade = botClient.getTrade(offerId);
log.info("Offer {} was taken, new trade:\n{}",
offerId,
new TableBuilder(TRADE_DETAIL_TBL, trade).build().toString());
return Optional.of(trade);
} catch (Exception ex) {
// Get trade will throw a non-fatal gRPC exception if not found.
log.info(this.getBotClient().toCleanGrpcExceptionMessage(ex));
return Optional.empty();
}
}
private void createTakeOfferCliScript(OfferInfo offer) {
File script = bashScriptGenerator.createTakeOfferScript(offer);
printCliHintAndOrScript(script, "The manual CLI side can take the offer");
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/scenario/bot/protocol/ProtocolStep.java
================================================
package haveno.apitest.scenario.bot.protocol;
public enum ProtocolStep {
START,
FIND_OFFER,
TAKE_OFFER,
WAIT_FOR_OFFER_TAKER,
WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED,
WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED,
SEND_PAYMENT_SENT_MESSAGE,
WAIT_FOR_PAYMENT_SENT_MESSAGE,
SEND_PAYMENT_RECEIVED_CONFIRMATION_MESSAGE,
WAIT_FOR_PAYMENT_RECEIVED_CONFIRMATION_MESSAGE,
WAIT_FOR_PAYOUT_TX,
CLOSE_TRADE,
DONE
}
================================================
FILE: apitest/src/test/java/haveno/apitest/scenario/bot/protocol/TakerBotProtocol.java
================================================
package haveno.apitest.scenario.bot.protocol;
import haveno.apitest.method.BitcoinCliHelper;
import haveno.apitest.scenario.bot.BotClient;
import haveno.apitest.scenario.bot.script.BashScriptGenerator;
import haveno.apitest.scenario.bot.shutdown.ManualBotShutdownException;
import haveno.cli.table.builder.TableBuilder;
import haveno.proto.grpc.OfferInfo;
import haveno.proto.grpc.TradeInfo;
import lombok.extern.slf4j.Slf4j;
import protobuf.PaymentAccount;
import java.io.File;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
import static haveno.apitest.scenario.bot.protocol.ProtocolStep.DONE;
import static haveno.apitest.scenario.bot.protocol.ProtocolStep.FIND_OFFER;
import static haveno.apitest.scenario.bot.protocol.ProtocolStep.TAKE_OFFER;
import static haveno.apitest.scenario.bot.shutdown.ManualShutdown.checkIfShutdownCalled;
import static haveno.cli.table.builder.TableType.OFFER_TBL;
import static haveno.core.payment.payload.PaymentMethod.F2F_ID;
@Slf4j
public class TakerBotProtocol extends BotProtocol {
public TakerBotProtocol(BotClient botClient,
PaymentAccount paymentAccount,
long protocolStepTimeLimitInMs,
BitcoinCliHelper bitcoinCli,
BashScriptGenerator bashScriptGenerator) {
super(botClient,
paymentAccount,
protocolStepTimeLimitInMs,
bitcoinCli,
bashScriptGenerator);
}
@Override
public void run() {
checkIsStartStep();
Function takeTrade = takeOffer.andThen(waitForTakerFeeTxConfirm);
var trade = takeTrade.apply(findOffer.get());
var takerIsSeller = trade.getOffer().getDirection().equalsIgnoreCase(BUY);
Function completeTraditionalTransaction = takerIsSeller
? waitForPaymentSentMessage.andThen(sendPaymentReceivedMessage)
: sendPaymentSentMessage.andThen(waitForPaymentReceivedConfirmation);
completeTraditionalTransaction.apply(trade);
currentProtocolStep = DONE;
}
private final Supplier> firstOffer = () -> {
var offers = botClient.getOffers(currencyCode);
if (offers.size() > 0) {
log.info("Offers found:\n{}", new TableBuilder(OFFER_TBL, offers).build());
OfferInfo offer = offers.get(0);
log.info("Will take first offer {}", offer.getId());
return Optional.of(offer);
} else {
log.info("No buy or sell {} offers found.", currencyCode);
return Optional.empty();
}
};
private final Supplier findOffer = () -> {
initProtocolStep.accept(FIND_OFFER);
createMakeOfferScript();
try {
log.info("Impatiently waiting for at least one {} offer to be created, repeatedly calling getoffers.", currencyCode);
while (isWithinProtocolStepTimeLimit()) {
checkIfShutdownCalled("Interrupted while checking offers.");
try {
Optional offer = firstOffer.get();
if (offer.isPresent())
return offer.get();
else
sleep(randomDelay.get());
} catch (Exception ex) {
throw new IllegalStateException(this.getBotClient().toCleanGrpcExceptionMessage(ex), ex);
}
} // end while
throw new IllegalStateException("Offer was never created; we won't wait any longer.");
} catch (ManualBotShutdownException ex) {
throw ex; // not an error, tells bot to shutdown
} catch (Exception ex) {
throw new IllegalStateException("Error while waiting for a new offer.", ex);
}
};
private final Function takeOffer = (offer) -> {
initProtocolStep.accept(TAKE_OFFER);
checkIfShutdownCalled("Interrupted before taking offer.");
return botClient.takeOffer(offer.getId(), paymentAccount);
};
private void createMakeOfferScript() {
String direction = RANDOM.nextBoolean() ? "BUY" : "SELL";
boolean createMarginPricedOffer = RANDOM.nextBoolean();
// If not using an F2F account, don't go over possible 0.01 BTC
// limit if account is not signed.
String amount = paymentAccount.getPaymentMethod().getId().equals(F2F_ID)
? "0.25"
: "0.01";
File script;
if (createMarginPricedOffer) {
script = bashScriptGenerator.createMakeMarginPricedOfferScript(direction,
currencyCode,
amount,
"0.0",
"15.0");
} else {
script = bashScriptGenerator.createMakeFixedPricedOfferScript(direction,
currencyCode,
amount,
botClient.getCurrentBTCMarketPriceAsIntegerString(currencyCode),
"15.0");
}
printCliHintAndOrScript(script, "The manual CLI side can create an offer");
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/scenario/bot/script/BashScriptGenerator.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.apitest.scenario.bot.script;
import com.google.common.io.Files;
import haveno.common.file.FileUtil;
import haveno.proto.grpc.OfferInfo;
import haveno.proto.grpc.TradeInfo;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static com.google.common.io.FileWriteMode.APPEND;
import static java.lang.String.format;
import static java.lang.System.getProperty;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.Files.readAllBytes;
@Slf4j
@Getter
public class BashScriptGenerator {
private final int apiPort;
private final String apiPassword;
private final String paymentAccountId;
private final String cliBase;
private final boolean printCliScripts;
public BashScriptGenerator(String apiPassword,
int apiPort,
String paymentAccountId,
boolean printCliScripts) {
this.apiPassword = apiPassword;
this.apiPort = apiPort;
this.paymentAccountId = paymentAccountId;
this.printCliScripts = printCliScripts;
this.cliBase = format("./haveno-cli --password=%s --port=%d", apiPassword, apiPort);
}
public File createMakeMarginPricedOfferScript(String direction,
String currencyCode,
String amount,
String marketPriceMargin,
String securityDeposit) {
String makeOfferCmd = format("%s createoffer --payment-account=%s "
+ " --direction=%s"
+ " --currency-code=%s"
+ " --amount=%s"
+ " --market-price-margin=%s"
+ " --security-deposit=%s"
+ " --fee-currency=%s",
cliBase,
this.getPaymentAccountId(),
direction,
currencyCode,
amount,
marketPriceMargin,
securityDeposit);
String getOffersCmd = format("%s getmyoffers --direction=%s --currency-code=%s",
cliBase,
direction,
currencyCode);
return createCliScript("createoffer.sh",
makeOfferCmd,
"sleep 2",
getOffersCmd);
}
public File createMakeFixedPricedOfferScript(String direction,
String currencyCode,
String amount,
String fixedPrice,
String securityDeposit) {
String makeOfferCmd = format("%s createoffer --payment-account=%s "
+ " --direction=%s"
+ " --currency-code=%s"
+ " --amount=%s"
+ " --fixed-price=%s"
+ " --security-deposit=%s"
+ " --fee-currency=%s",
cliBase,
this.getPaymentAccountId(),
direction,
currencyCode,
amount,
fixedPrice,
securityDeposit);
String getOffersCmd = format("%s getmyoffers --direction=%s --currency-code=%s",
cliBase,
direction,
currencyCode);
return createCliScript("createoffer.sh",
makeOfferCmd,
"sleep 2",
getOffersCmd);
}
public File createTakeOfferScript(OfferInfo offer) {
String getOffersCmd = format("%s getoffers --direction=%s --currency-code=%s",
cliBase,
offer.getDirection(),
offer.getCounterCurrencyCode());
String takeOfferCmd = format("%s takeoffer --offer-id=%s --payment-account=%s",
cliBase,
offer.getId(),
this.getPaymentAccountId());
String getTradeCmd = format("%s gettrade --trade-id=%s",
cliBase,
offer.getId());
return createCliScript("takeoffer.sh",
getOffersCmd,
takeOfferCmd,
"sleep 5",
getTradeCmd);
}
public File createPaymentSentScript(TradeInfo trade) {
String paymentSentCmd = format("%s confirmpaymentsent --trade-id=%s",
cliBase,
trade.getTradeId());
String getTradeCmd = format("%s gettrade --trade-id=%s", cliBase, trade.getTradeId());
return createCliScript("confirmpaymentsent.sh",
paymentSentCmd,
"sleep 2",
getTradeCmd);
}
public File createPaymentReceivedScript(TradeInfo trade) {
String paymentSentCmd = format("%s confirmpaymentreceived --trade-id=%s",
cliBase,
trade.getTradeId());
String getTradeCmd = format("%s gettrade --trade-id=%s", cliBase, trade.getTradeId());
return createCliScript("confirmpaymentreceived.sh",
paymentSentCmd,
"sleep 2",
getTradeCmd);
}
public File createKeepFundsScript(TradeInfo trade) {
String paymentSentCmd = format("%s closetrade --trade-id=%s", cliBase, trade.getTradeId());
String getTradeCmd = format("%s gettrade --trade-id=%s", cliBase, trade.getTradeId());
String getBalanceCmd = format("%s getbalance", cliBase);
return createCliScript("closetrade.sh",
paymentSentCmd,
"sleep 2",
getTradeCmd,
getBalanceCmd);
}
public File createGetBalanceScript() {
String getBalanceCmd = format("%s getbalance", cliBase);
return createCliScript("getbalance.sh", getBalanceCmd);
}
public File createGenerateBtcBlockScript(String address) {
String bitcoinCliCmd = format("bitcoin-cli -regtest -rpcport=19443 -rpcuser=apitest"
+ " -rpcpassword=apitest generatetoaddress 1 \"%s\"",
address);
return createCliScript("genbtcblk.sh",
bitcoinCliCmd);
}
public File createCliScript(String scriptName, String... commands) {
String filename = getProperty("java.io.tmpdir") + File.separator + scriptName;
File oldScript = new File(filename);
if (oldScript.exists()) {
try {
FileUtil.deleteFileIfExists(oldScript);
} catch (IOException ex) {
throw new IllegalStateException("Unable to delete old script.", ex);
}
}
File script = new File(filename);
try {
List lines = new ArrayList<>();
lines.add("#!/bin/bash");
lines.add("############################################################");
lines.add("# This example CLI script may be overwritten during the test");
lines.add("# run, and will be deleted when the test harness shuts down.");
lines.add("# Make a copy if you want to save it.");
lines.add("############################################################");
lines.add("set -x");
Collections.addAll(lines, commands);
Files.asCharSink(script, UTF_8, APPEND).writeLines(lines);
if (!script.setExecutable(true))
throw new IllegalStateException("Unable to set script owner's execute permission.");
} catch (IOException ex) {
log.error("", ex);
throw new IllegalStateException(ex);
} finally {
script.deleteOnExit();
}
return script;
}
public void printCliScript(File cliScript,
org.slf4j.Logger logger) {
try {
String contents = new String(readAllBytes(Paths.get(cliScript.getPath())));
logger.info("CLI script {}:\n{}", cliScript.getAbsolutePath(), contents);
} catch (IOException ex) {
throw new IllegalStateException("Error reading CLI script contents.", ex);
}
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/scenario/bot/script/BotScript.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.apitest.scenario.bot.script;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import javax.annotation.Nullable;
@Getter
@ToString
public
class BotScript {
// Common, default is true.
private final boolean useTestHarness;
// Used only with test harness. Mutually exclusive, but if both are not null,
// the botPaymentMethodId takes precedence over countryCode.
@Nullable
private final String botPaymentMethodId;
@Nullable
private final String countryCode;
// Used only without test harness.
@Nullable
@Setter
private String paymentAccountIdForBot;
@Nullable
@Setter
private String paymentAccountIdForCliScripts;
// Common, used with or without test harness.
private final int apiPortForCliScripts;
private final String[] actions;
private final long protocolStepTimeLimitInMinutes;
private final boolean printCliScripts;
private final boolean stayAlive;
@SuppressWarnings("NullableProblems")
BotScript(boolean useTestHarness,
String botPaymentMethodId,
String countryCode,
String paymentAccountIdForBot,
String paymentAccountIdForCliScripts,
String[] actions,
int apiPortForCliScripts,
long protocolStepTimeLimitInMinutes,
boolean printCliScripts,
boolean stayAlive) {
this.useTestHarness = useTestHarness;
this.botPaymentMethodId = botPaymentMethodId;
this.countryCode = countryCode != null ? countryCode.toUpperCase() : null;
this.paymentAccountIdForBot = paymentAccountIdForBot;
this.paymentAccountIdForCliScripts = paymentAccountIdForCliScripts;
this.apiPortForCliScripts = apiPortForCliScripts;
this.actions = actions;
this.protocolStepTimeLimitInMinutes = protocolStepTimeLimitInMinutes;
this.printCliScripts = printCliScripts;
this.stayAlive = stayAlive;
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/scenario/bot/script/BotScriptGenerator.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.apitest.scenario.bot.script;
import haveno.common.file.JsonFileManager;
import haveno.core.util.JsonUtil;
import joptsimple.BuiltinHelpFormatter;
import joptsimple.OptionParser;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import static java.lang.System.err;
import static java.lang.System.exit;
import static java.lang.System.getProperty;
import static java.lang.System.out;
@Slf4j
public class BotScriptGenerator {
private final boolean useTestHarness;
@Nullable
private final String countryCode;
@Nullable
private final String botPaymentMethodId;
@Nullable
private final String paymentAccountIdForBot;
@Nullable
private final String paymentAccountIdForCliScripts;
private final int apiPortForCliScripts;
private final String actions;
private final int protocolStepTimeLimitInMinutes;
private final boolean printCliScripts;
private final boolean stayAlive;
public BotScriptGenerator(String[] args) {
OptionParser parser = new OptionParser();
var helpOpt = parser.accepts("help", "Print this help text.")
.forHelp();
OptionSpec useTestHarnessOpt = parser
.accepts("use-testharness", "Use the test harness, or manually start your own nodes.")
.withRequiredArg()
.ofType(Boolean.class)
.defaultsTo(true);
OptionSpec actionsOpt = parser
.accepts("actions", "A comma delimited list with no spaces, e.g., make,take,take,make,...")
.withRequiredArg();
OptionSpec botPaymentMethodIdOpt = parser
.accepts("bot-payment-method",
"The bot's (Bob) payment method id. If using the test harness,"
+ " the id will be used to automatically create a payment account.")
.withRequiredArg();
OptionSpec countryCodeOpt = parser
.accepts("country-code",
"The two letter country-code for an F2F payment account if using the test harness,"
+ " but the bot-payment-method option takes precedence.")
.withRequiredArg();
OptionSpec apiPortForCliScriptsOpt = parser
.accepts("api-port-for-cli-scripts",
"The api port used in bot generated bash/cli scripts.")
.withRequiredArg()
.ofType(Integer.class)
.defaultsTo(9998);
OptionSpec paymentAccountIdForBotOpt = parser
.accepts("payment-account-for-bot",
"The bot side's payment account id, when the test harness is not used,"
+ " and Bob & Alice accounts are not automatically created.")
.withRequiredArg();
OptionSpec paymentAccountIdForCliScriptsOpt = parser
.accepts("payment-account-for-cli-scripts",
"The other side's payment account id, used in generated bash/cli scripts when"
+ " the test harness is not used, and Bob & Alice accounts are not automatically created.")
.withRequiredArg();
OptionSpec protocolStepTimeLimitInMinutesOpt = parser
.accepts("step-time-limit", "Each protocol step's time limit in minutes")
.withRequiredArg()
.ofType(Integer.class)
.defaultsTo(60);
OptionSpec printCliScriptsOpt = parser
.accepts("print-cli-scripts", "Print the generated CLI scripts from bot")
.withRequiredArg()
.ofType(Boolean.class)
.defaultsTo(false);
OptionSpec stayAliveOpt = parser
.accepts("stay-alive", "Leave test harness nodes running after the last action.")
.withRequiredArg()
.ofType(Boolean.class)
.defaultsTo(true);
OptionSet options = parser.parse(args);
if (options.has(helpOpt)) {
printHelp(parser, out);
exit(0);
}
if (!options.has(actionsOpt)) {
printHelp(parser, err);
exit(1);
}
this.useTestHarness = options.has(useTestHarnessOpt) ? options.valueOf(useTestHarnessOpt) : true;
this.actions = options.valueOf(actionsOpt);
this.apiPortForCliScripts = options.has(apiPortForCliScriptsOpt) ? options.valueOf(apiPortForCliScriptsOpt) : 9998;
this.botPaymentMethodId = options.has(botPaymentMethodIdOpt) ? options.valueOf(botPaymentMethodIdOpt) : null;
this.countryCode = options.has(countryCodeOpt) ? options.valueOf(countryCodeOpt) : null;
this.paymentAccountIdForBot = options.has(paymentAccountIdForBotOpt) ? options.valueOf(paymentAccountIdForBotOpt) : null;
this.paymentAccountIdForCliScripts = options.has(paymentAccountIdForCliScriptsOpt) ? options.valueOf(paymentAccountIdForCliScriptsOpt) : null;
this.protocolStepTimeLimitInMinutes = options.valueOf(protocolStepTimeLimitInMinutesOpt);
this.printCliScripts = options.valueOf(printCliScriptsOpt);
this.stayAlive = options.valueOf(stayAliveOpt);
var noPaymentAccountCountryOrMethodForTestHarness = useTestHarness &&
(!options.has(countryCodeOpt) && !options.has(botPaymentMethodIdOpt));
if (noPaymentAccountCountryOrMethodForTestHarness) {
log.error("When running the test harness, payment accounts are automatically generated,");
log.error("and you must provide one of the following options:");
log.error(" \t\t(1) --bot-payment-method= OR");
log.error(" \t\t(2) --country-code=");
log.error("If the bot-payment-method option is not present, the bot will create"
+ " a country based F2F account using the country-code.");
log.error("If both are present, the bot-payment-method will take precedence. "
+ "Currently, only the ZELLE_ID bot-payment-method is supported.");
printHelp(parser, err);
exit(1);
}
var noPaymentAccountIdOrApiPortForCliScripts = !useTestHarness &&
(!options.has(paymentAccountIdForCliScriptsOpt) || !options.has(paymentAccountIdForBotOpt));
if (noPaymentAccountIdOrApiPortForCliScripts) {
log.error("If not running the test harness, payment accounts are not automatically generated,");
log.error("and you must provide three options:");
log.error(" \t\t(1) --api-port-for-cli-scripts=");
log.error(" \t\t(2) --payment-account-for-bot=");
log.error(" \t\t(3) --payment-account-for-cli-scripts=");
log.error("These will be used by the bot and in CLI scripts the bot will generate when creating an offer.");
printHelp(parser, err);
exit(1);
}
}
private void printHelp(OptionParser parser, PrintStream stream) {
try {
String usage = "Examples\n--------\n"
+ examplesUsingTestHarness()
+ examplesNotUsingTestHarness();
stream.println();
parser.formatHelpWith(new HelpFormatter());
parser.printHelpOn(stream);
stream.println();
stream.println(usage);
stream.println();
} catch (IOException ex) {
log.error("", ex);
}
}
private String examplesUsingTestHarness() {
@SuppressWarnings("StringBufferReplaceableByString") StringBuilder builder = new StringBuilder();
builder.append("To generate a bot-script.json file that will start the test harness,");
builder.append(" create F2F accounts for Bob and Alice,");
builder.append(" and take an offer created by Alice's CLI:").append("\n");
builder.append("\tUsage: BotScriptGenerator").append("\n");
builder.append("\t\t").append("--use-testharness=true").append("\n");
builder.append("\t\t").append("--country-code=").append("\n");
builder.append("\t\t").append("--actions=take").append("\n");
builder.append("\n");
builder.append("To generate a bot-script.json file that will start the test harness,");
builder.append(" create Zelle accounts for Bob and Alice,");
builder.append(" and create an offer to be taken by Alice's CLI:").append("\n");
builder.append("\tUsage: BotScriptGenerator").append("\n");
builder.append("\t\t").append("--use-testharness=true").append("\n");
builder.append("\t\t").append("--bot-payment-method=ZELLE").append("\n");
builder.append("\t\t").append("--actions=make").append("\n");
builder.append("\n");
return builder.toString();
}
private String examplesNotUsingTestHarness() {
@SuppressWarnings("StringBufferReplaceableByString") StringBuilder builder = new StringBuilder();
builder.append("To generate a bot-script.json file that will not start the test harness,");
builder.append(" but will create useful bash scripts for the CLI user,");
builder.append(" and make two offers, then take two offers:").append("\n");
builder.append("\tUsage: BotScriptGenerator").append("\n");
builder.append("\t\t").append("--use-testharness=false").append("\n");
builder.append("\t\t").append("--api-port-for-cli-scripts=").append("\n");
builder.append("\t\t").append("--payment-account-for-bot=").append("\n");
builder.append("\t\t").append("--payment-account-for-cli-scripts=").append("\n");
builder.append("\t\t").append("--actions=make,make,take,take").append("\n");
builder.append("\n");
return builder.toString();
}
private String generateBotScriptTemplate() {
return JsonUtil.objectToJson(new BotScript(
useTestHarness,
botPaymentMethodId,
countryCode,
paymentAccountIdForBot,
paymentAccountIdForCliScripts,
actions.split("\\s*,\\s*").clone(),
apiPortForCliScripts,
protocolStepTimeLimitInMinutes,
printCliScripts,
stayAlive));
}
public static void main(String[] args) {
BotScriptGenerator generator = new BotScriptGenerator(args);
String json = generator.generateBotScriptTemplate();
String destDir = getProperty("java.io.tmpdir");
JsonFileManager jsonFileManager = new JsonFileManager(new File(destDir));
jsonFileManager.writeToDisc(json, "bot-script");
JsonFileManager.shutDownAllInstances();
log.info("Saved {}/bot-script.json", destDir);
log.info("bot-script.json contents\n{}", json);
}
// Makes a formatter with a given overall row width of 120 and column separator width of 2.
private static class HelpFormatter extends BuiltinHelpFormatter {
public HelpFormatter() {
super(120, 2);
}
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/scenario/bot/shutdown/ManualBotShutdownException.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.apitest.scenario.bot.shutdown;
import haveno.common.HavenoException;
@SuppressWarnings("unused")
public class ManualBotShutdownException extends HavenoException {
public ManualBotShutdownException(Throwable cause) {
super(cause);
}
public ManualBotShutdownException(String format, Object... args) {
super(format, args);
}
public ManualBotShutdownException(Throwable cause, String format, Object... args) {
super(cause, format, args);
}
}
================================================
FILE: apitest/src/test/java/haveno/apitest/scenario/bot/shutdown/ManualShutdown.java
================================================
package haveno.apitest.scenario.bot.shutdown;
import haveno.common.UserThread;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
import static haveno.common.file.FileUtil.deleteFileIfExists;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
@Slf4j
public class ManualShutdown {
public static final String SHUTDOWN_FILENAME = "/tmp/bottest-shutdown";
private static final AtomicBoolean SHUTDOWN_CALLED = new AtomicBoolean(false);
/**
* Looks for a /tmp/bottest-shutdown file and throws a BotShutdownException if found.
*
* Running '$ touch /tmp/bottest-shutdown' could be used to trigger a scaffold teardown.
*
* This is much easier than manually shutdown down haveno apps & bitcoind.
*/
public static void startShutdownTimer() {
deleteStaleShutdownFile();
UserThread.runPeriodically(() -> {
File shutdownFile = new File(SHUTDOWN_FILENAME);
if (shutdownFile.exists()) {
log.warn("Caught manual shutdown signal: /tmp/bottest-shutdown file exists.");
try {
deleteFileIfExists(shutdownFile);
} catch (IOException ex) {
log.error("", ex);
throw new IllegalStateException(ex);
}
SHUTDOWN_CALLED.set(true);
}
}, 2000, MILLISECONDS);
}
public static boolean isShutdownCalled() {
return SHUTDOWN_CALLED.get();
}
public static void checkIfShutdownCalled(String warning) throws ManualBotShutdownException {
if (isShutdownCalled())
throw new ManualBotShutdownException(warning);
}
private static void deleteStaleShutdownFile() {
try {
deleteFileIfExists(new File(SHUTDOWN_FILENAME));
} catch (IOException ex) {
log.error("", ex);
throw new IllegalStateException(ex);
}
}
}
================================================
FILE: apitest/src/test/resources/logback.xml
================================================
%highlight(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{30}: %msg %xEx%n)
================================================
FILE: assets/src/main/java/haveno/asset/AbstractAsset.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.asset;
import static org.apache.commons.lang3.Validate.notBlank;
import static org.apache.commons.lang3.Validate.notNull;
/**
* Abstract base class for {@link Asset} implementations. Most implementations should not
* extend this class directly, but should rather extend {@link Coin}, {@link Token} or one
* of their subtypes.
*
* @author Chris Beams
* @since 0.7.0
*/
public abstract class AbstractAsset implements Asset {
private final String name;
private final String tickerSymbol;
private final AddressValidator addressValidator;
public AbstractAsset(String name, String tickerSymbol, AddressValidator addressValidator) {
this.name = notBlank(name);
this.tickerSymbol = notBlank(tickerSymbol);
this.addressValidator = notNull(addressValidator);
}
@Override
public final String getName() {
return name;
}
@Override
public final String getTickerSymbol() {
return tickerSymbol;
}
@Override
public final AddressValidationResult validateAddress(String address) {
return addressValidator.validate(address);
}
@Override
public String toString() {
return getClass().getName();
}
}
================================================
FILE: assets/src/main/java/haveno/asset/AddressValidationResult.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.asset;
/**
* Value object representing the result of validating an {@link Asset} address. Various
* factory methods are provided for typical use cases.
*
* @author Chris Beams
* @since 0.7.0
* @see Asset#validateAddress(String)
*/
public class AddressValidationResult {
private static final AddressValidationResult VALID_ADDRESS = new AddressValidationResult(true, "", "");
private final boolean isValid;
private final String message;
private final String i18nKey;
private AddressValidationResult(boolean isValid, String message, String i18nKey) {
this.isValid = isValid;
this.message = message;
this.i18nKey = i18nKey;
}
public boolean isValid() {
return isValid;
}
public String getI18nKey() {
return i18nKey;
}
public String getMessage() {
return message;
}
public static AddressValidationResult validAddress() {
return VALID_ADDRESS;
}
public static AddressValidationResult invalidAddress(Throwable cause) {
return invalidAddress(cause.getMessage());
}
public static AddressValidationResult invalidAddress(String cause) {
return invalidAddress(cause, "validation.crypto.invalidAddress");
}
public static AddressValidationResult invalidAddress(String cause, String i18nKey) {
return new AddressValidationResult(false, cause, i18nKey);
}
public static AddressValidationResult invalidStructure() {
return invalidAddress("", "validation.crypto.wrongStructure");
}
}
================================================
FILE: assets/src/main/java/haveno/asset/AddressValidator.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.asset;
/**
* An {@link Asset} address validation function.
*
* @author Chris Beams
* @since 0.7.0
*/
public interface AddressValidator {
AddressValidationResult validate(String address);
}
================================================
FILE: assets/src/main/java/haveno/asset/Asset.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.asset;
/**
* Interface representing a given ("crypto") asset in its most abstract form, having a
* {@link #getName() name}, eg "Bitcoin", a {@link #getTickerSymbol() ticker symbol},
* eg "BTC", and an address validation function. Together, these properties represent
* the minimum information and functionality required to register and trade an asset on
* the Haveno network.
*
* Implementations typically extend either the {@link Coin} or {@link Token} base
* classes, and must be registered in the {@code META-INF/services/haveno.asset.Asset} file
* in order to be available in the {@link AssetRegistry} at runtime.
*
* @author Chris Beams
* @since 0.7.0
* @see AbstractAsset
* @see Coin
* @see Token
* @see Erc20Token
* @see AssetRegistry
*/
public interface Asset {
String getName();
String getTickerSymbol();
AddressValidationResult validateAddress(String address);
}
================================================
FILE: assets/src/main/java/haveno/asset/AssetRegistry.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.asset;
import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;
import java.util.stream.Stream;
/**
* Provides {@link Stream}-based access to {@link Asset} implementations registered in
* the {@code META-INF/services/haveno.asset.Asset} provider-configuration file.
*
* @author Chris Beams
* @since 0.7.0
* @see ServiceLoader
*/
public class AssetRegistry {
private static final List registeredAssets = new ArrayList<>();
static {
for (Asset asset : ServiceLoader.load(Asset.class)) {
registeredAssets.add(asset);
}
}
public Stream stream() {
return registeredAssets.stream();
}
}
================================================
FILE: assets/src/main/java/haveno/asset/Base58AddressValidator.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.asset;
import org.bitcoinj.core.AddressFormatException;
import org.bitcoinj.core.LegacyAddress;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.params.MainNetParams;
/**
* {@link AddressValidator} for Base58-encoded addresses.
*
* @author Chris Beams
* @since 0.7.0
* @see org.bitcoinj.core.LegacyAddress#fromBase58(NetworkParameters, String)
*/
public class Base58AddressValidator implements AddressValidator {
private final NetworkParameters networkParameters;
public Base58AddressValidator() {
this(MainNetParams.get());
}
public Base58AddressValidator(NetworkParameters networkParameters) {
this.networkParameters = networkParameters;
}
@Override
public AddressValidationResult validate(String address) {
try {
LegacyAddress.fromBase58(networkParameters, address);
} catch (AddressFormatException ex) {
return AddressValidationResult.invalidAddress(ex);
}
return AddressValidationResult.validAddress();
}
}
================================================
FILE: assets/src/main/java/haveno/asset/BitcoinAddressValidator.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.asset;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.AddressFormatException;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.params.MainNetParams;
/**
* {@link AddressValidator} for Bitcoin addresses.
*
* @author Oscar Guindzberg
*/
public class BitcoinAddressValidator implements AddressValidator {
private final NetworkParameters networkParameters;
public BitcoinAddressValidator() {
this(MainNetParams.get());
}
public BitcoinAddressValidator(NetworkParameters networkParameters) {
this.networkParameters = networkParameters;
}
@Override
public AddressValidationResult validate(String address) {
try {
Address.fromString(networkParameters, address);
} catch (AddressFormatException ex) {
return AddressValidationResult.invalidAddress(ex);
}
return AddressValidationResult.validAddress();
}
}
================================================
FILE: assets/src/main/java/haveno/asset/BitcoinCashAddressValidator.java
================================================
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see .
*/
package haveno.asset;
/**
* Validates a Bitcoin Cash address.
*/
public class BitcoinCashAddressValidator extends RegexAddressValidator {
public BitcoinCashAddressValidator() {
super("^([13][a-km-zA-HJ-NP-Z1-9]{25,34})|^((bitcoincash:)?(q|p)[a-z0-9]{41})|^((BITCOINCASH:)?(Q|P)[A-Z0-9]{41})$");
}
public BitcoinCashAddressValidator(String errorMessageI18nKey) {
super("^([13][a-km-zA-HJ-NP-Z1-9]{25,34})|^((bitcoincash:)?(q|p)[a-z0-9]{41})|^((BITCOINCASH:)?(Q|P)[A-Z0-9]{41})$", errorMessageI18nKey);
}
}
================================================
FILE: assets/src/main/java/haveno/asset/CardanoAddressValidator.java
================================================
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see .
*/
package haveno.asset;
/**
* Validates a Shelley-era mainnet Cardano address.
*/
public class CardanoAddressValidator extends RegexAddressValidator {
private static final String CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
private static final int BECH32_CONST = 1;
private static final int BECH32M_CONST = 0x2bc830a3;
private static final int MAX_LEN = 104; // bech32 / bech32m max for Cardano
public CardanoAddressValidator() {
super("^addr1[0-9a-z]{20,98}$");
}
public CardanoAddressValidator(String errorMessageI18nKey) {
super("^addr1[0-9a-z]{20,98}$", errorMessageI18nKey);
}
@Override
public AddressValidationResult validate(String address) {
if (!isValidShelleyMainnet(address)) {
return AddressValidationResult.invalidStructure();
}
return super.validate(address);
}
/**
* Checks if the given address is a valid Shelley-era mainnet Cardano address.
*
* This code is AI-generated and has been tested with a variety of addresses.
*
* @param addr the address to validate
* @return true if the address is valid, false otherwise
*/
private static boolean isValidShelleyMainnet(String addr) {
if (addr == null) return false;
String lower = addr.toLowerCase();
// must start addr1 and not be absurdly long
if (!lower.startsWith("addr1") || lower.length() > MAX_LEN) return false;
int sep = lower.lastIndexOf('1');
if (sep < 1) return false; // no separator or empty HRP
String hrp = lower.substring(0, sep);
if (!"addr".equals(hrp)) return false; // mainnet only
String dataPart = lower.substring(sep + 1);
if (dataPart.length() < 6) return false; // checksum is 6 chars minimum
int[] data = new int[dataPart.length()];
for (int i = 0; i < dataPart.length(); i++) {
int v = CHARSET.indexOf(dataPart.charAt(i));
if (v == -1) return false;
data[i] = v;
}
int[] hrpExp = hrpExpand(hrp);
int[] combined = new int[hrpExp.length + data.length];
System.arraycopy(hrpExp, 0, combined, 0, hrpExp.length);
System.arraycopy(data, 0, combined, hrpExp.length, data.length);
int chk = polymod(combined);
return chk == BECH32_CONST || chk == BECH32M_CONST; // accept either legacy Bech32 (1) or Bech32m (0x2bc830a3)
}
private static int[] hrpExpand(String hrp) {
int[] ret = new int[hrp.length() * 2 + 1];
int idx = 0;
for (char c : hrp.toCharArray()) ret[idx++] = c >> 5;
ret[idx++] = 0;
for (char c : hrp.toCharArray()) ret[idx++] = c & 31;
return ret;
}
private static int polymod(int[] values) {
int chk = 1;
int[] GEN = {0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3};
for (int v : values) {
int b = chk >>> 25;
chk = ((chk & 0x1ffffff) << 5) ^ v;
for (int i = 0; i < 5; i++) {
if (((b >>> i) & 1) != 0) chk ^= GEN[i];
}
}
return chk;
}
}
================================================
FILE: assets/src/main/java/haveno/asset/Coin.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.asset;
/**
* Abstract base class for {@link Asset}s with their own dedicated blockchain, such as
* {@link haveno.asset.coins.Bitcoin}, {@link haveno.asset.coins.Ether}, and {@link haveno.asset.coins.Monero}.
*
* In addition to the usual {@code Asset} properties, a {@code Coin} maintains information
* about which {@link Network} it may be used on. By default, coins are constructed with
* the assumption they are for use on that coin's "main network", or "main blockchain",
* i.e. that they are "real" coins for use in a production environment. In testing
* scenarios, however, a coin may be constructed for use only on "testnet" or "stagenet"
* networks.
*
* @author Chris Beams
* @since 0.7.0
*/
public abstract class Coin extends AbstractAsset {
public enum Network { MAINNET, TESTNET, STAGENET }
private final Network network;
public Coin(String name, String tickerSymbol, AddressValidator addressValidator) {
this(name, tickerSymbol, addressValidator, Network.MAINNET);
}
public Coin(String name, String tickerSymbol, AddressValidator addressValidator, Network network) {
super(name, tickerSymbol, addressValidator);
this.network = network;
}
public Network getNetwork() {
return network;
}
}
================================================
FILE: assets/src/main/java/haveno/asset/CryptoAccountDisclaimer.java
================================================
package haveno.asset;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* When a new PaymentAccount is created for given asset, this annotation tells UI to show user a disclaimer message
* with requirements needed to be fulfilled when conducting trade given payment method.
*
* I.e. in case of Monero user must use official Monero GUI wallet or Monero CLI wallet with certain options enabled,
* user needs to keep tx private key, tx hash, recipient's address, etc.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface CryptoAccountDisclaimer {
/**
* Translation key of the message to show, i.e. "account.crypto.popup.xmr.msg"
* @return translation key
*/
String value();
}
================================================
FILE: assets/src/main/java/haveno/asset/CryptoNoteAddressValidator.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.asset;
/**
* {@link AddressValidator} for Base58-encoded Cryptonote addresses.
*
* @author Xiphon
*/
public class CryptoNoteAddressValidator implements AddressValidator {
private final long[] validPrefixes;
private final boolean validateChecksum;
public CryptoNoteAddressValidator(boolean validateChecksum, long... validPrefixes) {
this.validPrefixes = validPrefixes;
this.validateChecksum = validateChecksum;
}
public CryptoNoteAddressValidator(long... validPrefixes) {
this(true, validPrefixes);
}
@Override
public AddressValidationResult validate(String address) {
try {
long prefix = CryptoNoteUtils.MoneroBase58.decodeAddress(address, this.validateChecksum);
for (long validPrefix : this.validPrefixes) {
if (prefix == validPrefix) {
return AddressValidationResult.validAddress();
}
}
return AddressValidationResult.invalidAddress(String.format("invalid address prefix %x", prefix));
} catch (Exception e) {
return AddressValidationResult.invalidStructure();
}
}
}
================================================
FILE: assets/src/main/java/haveno/asset/CryptoNoteUtils.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.asset;
import org.bitcoinj.core.Utils;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.Map;
public class CryptoNoteUtils {
public static String getRawSpendKeyAndViewKey(String address) throws CryptoNoteUtils.CryptoNoteException {
try {
// See https://monerodocs.org/public-address/standard-address/
byte[] decoded = CryptoNoteUtils.MoneroBase58.decode(address);
// Standard addresses are of length 69 and addresses with integrated payment ID of length 77.
if (decoded.length <= 65) {
throw new CryptoNoteUtils.CryptoNoteException("The address we received is too short. address=" + address);
}
// If the length is not as expected but still can be truncated we log an error and continue.
if (decoded.length != 69 && decoded.length != 77) {
System.out.println("The address we received is not in the expected format. address=" + address);
}
// We remove the network type byte, the checksum (4 bytes) and optionally the payment ID (8 bytes if present)
// So extract the 64 bytes after the first byte, which are the 32 byte public spend key + the 32 byte public view key
byte[] slice = Arrays.copyOfRange(decoded, 1, 65);
return Utils.HEX.encode(slice);
} catch (CryptoNoteUtils.CryptoNoteException e) {
throw new CryptoNoteUtils.CryptoNoteException(e);
}
}
public static class CryptoNoteException extends Exception {
CryptoNoteException(String msg) {
super(msg);
}
public CryptoNoteException(CryptoNoteException exception) {
super(exception);
}
}
static class Keccak {
private static final int BLOCK_SIZE = 136;
private static final int LONGS_PER_BLOCK = BLOCK_SIZE / 8;
private static final int KECCAK_ROUNDS = 24;
private static final long[] KECCAKF_RNDC = {
0x0000000000000001L, 0x0000000000008082L, 0x800000000000808aL,
0x8000000080008000L, 0x000000000000808bL, 0x0000000080000001L,
0x8000000080008081L, 0x8000000000008009L, 0x000000000000008aL,
0x0000000000000088L, 0x0000000080008009L, 0x000000008000000aL,
0x000000008000808bL, 0x800000000000008bL, 0x8000000000008089L,
0x8000000000008003L, 0x8000000000008002L, 0x8000000000000080L,
0x000000000000800aL, 0x800000008000000aL, 0x8000000080008081L,
0x8000000000008080L, 0x0000000080000001L, 0x8000000080008008L
};
private static final int[] KECCAKF_ROTC = {
1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 2, 14,
27, 41, 56, 8, 25, 43, 62, 18, 39, 61, 20, 44
};
private static final int[] KECCAKF_PILN = {
10, 7, 11, 17, 18, 3, 5, 16, 8, 21, 24, 4,
15, 23, 19, 13, 12, 2, 20, 14, 22, 9, 6, 1
};
private static long rotateLeft(long value, int shift) {
return (value << shift) | (value >>> (64 - shift));
}
private static void keccakf(long[] st, int rounds) {
long[] bc = new long[5];
for (int round = 0; round < rounds; ++round) {
for (int i = 0; i < 5; ++i) {
bc[i] = st[i] ^ st[i + 5] ^ st[i + 10] ^ st[i + 15] ^ st[i + 20];
}
for (int i = 0; i < 5; i++) {
long t = bc[(i + 4) % 5] ^ rotateLeft(bc[(i + 1) % 5], 1);
for (int j = 0; j < 25; j += 5) {
st[j + i] ^= t;
}
}
long t = st[1];
for (int i = 0; i < 24; ++i) {
int j = KECCAKF_PILN[i];
bc[0] = st[j];
st[j] = rotateLeft(t, KECCAKF_ROTC[i]);
t = bc[0];
}
for (int j = 0; j < 25; j += 5) {
for (int i = 0; i < 5; i++) {
bc[i] = st[j + i];
}
for (int i = 0; i < 5; i++) {
st[j + i] ^= (~bc[(i + 1) % 5]) & bc[(i + 2) % 5];
}
}
st[0] ^= KECCAKF_RNDC[round];
}
}
static ByteBuffer keccak1600(ByteBuffer input) {
input.order(ByteOrder.LITTLE_ENDIAN);
int fullBlocks = input.remaining() / BLOCK_SIZE;
long[] st = new long[25];
for (int block = 0; block < fullBlocks; ++block) {
for (int index = 0; index < LONGS_PER_BLOCK; ++index) {
st[index] ^= input.getLong();
}
keccakf(st, KECCAK_ROUNDS);
}
ByteBuffer lastBlock = ByteBuffer.allocate(144).order(ByteOrder.LITTLE_ENDIAN);
lastBlock.put(input);
lastBlock.put((byte) 1);
int paddingOffset = BLOCK_SIZE - 1;
lastBlock.put(paddingOffset, (byte) (lastBlock.get(paddingOffset) | 0x80));
lastBlock.rewind();
for (int index = 0; index < LONGS_PER_BLOCK; ++index) {
st[index] ^= lastBlock.getLong();
}
keccakf(st, KECCAK_ROUNDS);
ByteBuffer result = ByteBuffer.allocate(32);
result.slice().order(ByteOrder.LITTLE_ENDIAN).asLongBuffer().put(st, 0, 4);
return result;
}
}
static class MoneroBase58 {
private static final String ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
private static final BigInteger ALPHABET_SIZE = BigInteger.valueOf(ALPHABET.length());
private static final int FULL_DECODED_BLOCK_SIZE = 8;
private static final int FULL_ENCODED_BLOCK_SIZE = 11;
private static final BigInteger UINT64_MAX = new BigInteger("18446744073709551615");
private static final Map DECODED_CHUNK_LENGTH = Map.of(2, 1,
3, 2,
5, 3,
6, 4,
7, 5,
9, 6,
10, 7,
11, 8);
private static void decodeChunk(String input,
int inputOffset,
int inputLength,
byte[] decoded,
int decodedOffset,
int decodedLength) throws CryptoNoteException {
BigInteger result = BigInteger.ZERO;
BigInteger order = BigInteger.ONE;
for (int index = inputOffset + inputLength; index != inputOffset; order = order.multiply(ALPHABET_SIZE)) {
char character = input.charAt(--index);
int digit = ALPHABET.indexOf(character);
if (digit == -1) {
throw new CryptoNoteException("invalid character " + character);
}
result = result.add(order.multiply(BigInteger.valueOf(digit)));
if (result.compareTo(UINT64_MAX) > 0) {
throw new CryptoNoteException("64-bit unsigned integer overflow " + result.toString());
}
}
BigInteger maxCapacity = BigInteger.ONE.shiftLeft(8 * decodedLength);
if (result.compareTo(maxCapacity) >= 0) {
throw new CryptoNoteException("capacity overflow " + result.toString());
}
for (int index = decodedOffset + decodedLength; index != decodedOffset; result = result.shiftRight(8)) {
decoded[--index] = result.byteValue();
}
}
public static byte[] decode(String input) throws CryptoNoteException {
if (input.length() == 0) {
return new byte[0];
}
int chunks = input.length() / FULL_ENCODED_BLOCK_SIZE;
int lastEncodedSize = input.length() % FULL_ENCODED_BLOCK_SIZE;
int lastChunkSize = lastEncodedSize > 0 ? DECODED_CHUNK_LENGTH.get(lastEncodedSize) : 0;
byte[] result = new byte[chunks * FULL_DECODED_BLOCK_SIZE + lastChunkSize];
int inputOffset = 0;
int resultOffset = 0;
for (int chunk = 0; chunk < chunks; ++chunk,
inputOffset += FULL_ENCODED_BLOCK_SIZE,
resultOffset += FULL_DECODED_BLOCK_SIZE) {
decodeChunk(input, inputOffset, FULL_ENCODED_BLOCK_SIZE, result, resultOffset, FULL_DECODED_BLOCK_SIZE);
}
if (lastChunkSize > 0) {
decodeChunk(input, inputOffset, lastEncodedSize, result, resultOffset, lastChunkSize);
}
return result;
}
private static long readVarInt(ByteBuffer buffer) {
long result = 0;
for (int shift = 0; ; shift += 7) {
byte current = buffer.get();
result += (current & 0x7fL) << shift;
if ((current & 0x80L) == 0) {
break;
}
}
return result;
}
static long decodeAddress(String address, boolean validateChecksum) throws CryptoNoteException {
byte[] decoded = decode(address);
int checksumSize = 4;
if (decoded.length < checksumSize) {
throw new CryptoNoteException("invalid length");
}
ByteBuffer decodedAddress = ByteBuffer.wrap(decoded, 0, decoded.length - checksumSize);
long prefix = readVarInt(decodedAddress.slice());
if (!validateChecksum) {
return prefix;
}
ByteBuffer fastHash = Keccak.keccak1600(decodedAddress.slice());
int checksum = fastHash.getInt();
int expected = ByteBuffer.wrap(decoded, decoded.length - checksumSize, checksumSize).getInt();
if (checksum != expected) {
throw new CryptoNoteException(String.format("invalid checksum %08X, expected %08X", checksum, expected));
}
return prefix;
}
}
}
================================================
FILE: assets/src/main/java/haveno/asset/Erc20Token.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.asset;
/**
* Abstract base class for Ethereum-based {@link Token}s that implement the
* ERC-20 Token
* Standard.
*
* @author Chris Beams
* @since 0.7.0
*/
public abstract class Erc20Token extends Token {
public Erc20Token(String name, String tickerSymbol) {
super(name, tickerSymbol, new EtherAddressValidator());
}
}
================================================
FILE: assets/src/main/java/haveno/asset/EtherAddressValidator.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.asset;
/**
* Validates an Ethereum address using the regular expression on record in the
*
* ethereum/web3.js project. Note that this implementation is widely used, not just
* for actual {@link haveno.asset.coins.Ether} address validation, but also for
* {@link Erc20Token} implementations and other Ethereum-based {@link Asset}
* implementations.
*
* @author Chris Beams
* @since 0.7.0
*/
public class EtherAddressValidator extends RegexAddressValidator {
public EtherAddressValidator() {
super("^(0x)?[0-9a-fA-F]{40}$");
}
public EtherAddressValidator(String errorMessageI18nKey) {
super("^(0x)?[0-9a-fA-F]{40}$", errorMessageI18nKey);
}
}
================================================
FILE: assets/src/main/java/haveno/asset/GrinAddressValidator.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.asset;
/**
* We only support the grinbox format as it is currently the only tool which offers a validation options of sender.
* Beside that is the IP:port format very insecure with MITM attacks.
*
* Here is the information from a conversation with the Grinbox developer regarding the Grinbox address format.
*
A Grinbox address is of the format: grinbox://@domain.com:port where everything besides is optional.
If no domain is specified, the default relay grinbox.io will be used.
The is a base58check encoded value (like in Bitcoin). For Grin mainnet, the first 2 bytes will be [1, 11] and
the following 33 bytes should be a valid secp256k1 compressed public key.
Some examples of valid addresses are:
gVvRNiuopubvxPrs1BzJdQjVdFAxmkLzMqiVJzUZ7ubznhdtNTGB
gVvUcSafSTD3YTSqgNf9ojEYWkz3zMZNfsjdpdb9en5mxc6gmja6
gVvk7rLBg3r3qoWYL3VsREnBbooT7nynxx5HtDvUWCJUaNCnddvY
grinbox://gVtWzX5NTLCBkyNV19QVdnLXue13heAVRD36sfkGD6xpqy7k7e4a
gVw9TWimGFXRjoDXWhWxeNQbu84ZpLkvnenkKvA5aJeDo31eM5tC@somerelay.com
grinbox://gVwjSsYW5vvHpK4AunJ5piKhhQTV6V3Jb818Uqs6PdC3SsB36AsA@somerelay.com:1220
Some examples of invalid addresses are:
gVuBJDKcWkhueMfBLAbFwV4ax55YXPeinWXdRME1Zi3eiC6sFNye (invalid checksum)
geWGCMQjxZMHG3EtTaRbR7rH9rE4DsmLfpm1iiZEa7HFKjjkgpf2 (wrong version bytes)
gVvddC2jYAfxTxnikcbTEQKLjhJZpqpBg39tXkwAKnD2Pys2mWiK (invalid public key)
We only add the basic validation without checksum, version byte and pubkey validation as that would require much more
effort. Any Grin developer is welcome to add that though!
*/
public class GrinAddressValidator implements AddressValidator {
// A Grin Wallet URL (address is not the correct term) can be in the form IP:port or a grinbox format.
// The grinbox has the format grinbox://@domain.com:port where everything beside the key is optional.
// Regex for IP validation borrowed from https://stackoverflow.com/questions/53497/regular-expression-that-matches-valid-ipv6-addresses
private static final String PORT = "((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4}))$";
private static final String DOMAIN = "[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\\.[a-zA-Z]{2,}$";
private static final String KEY = "[a-km-zA-HJ-NP-Z1-9]{52}$";
public GrinAddressValidator() {
}
@Override
public AddressValidationResult validate(String address) {
if (address == null || address.length() == 0)
return AddressValidationResult.invalidAddress("Address may not be empty (only Grinbox format is supported)");
// We only support grinbox address
String key;
String domain = null;
String port = null;
address = address.replace("grinbox://", "");
if (address.contains("@")) {
String[] keyAndDomain = address.split("@");
key = keyAndDomain[0];
if (keyAndDomain.length > 1) {
domain = keyAndDomain[1];
if (domain.contains(":")) {
String[] domainAndPort = domain.split(":");
domain = domainAndPort[0];
if (domainAndPort.length > 1)
port = domainAndPort[1];
}
}
} else {
key = address;
}
if (!key.matches("^" + KEY))
return AddressValidationResult.invalidAddress("Invalid key (only Grinbox format is supported)");
if (domain != null && !domain.matches("^" + DOMAIN))
return AddressValidationResult.invalidAddress("Invalid domain (only Grinbox format is supported)");
if (port != null && !port.matches("^" + PORT))
return AddressValidationResult.invalidAddress("Invalid port (only Grinbox format is supported)");
return AddressValidationResult.validAddress();
}
}
================================================
FILE: assets/src/main/java/haveno/asset/I18n.java
================================================
package haveno.asset;
import java.util.ResourceBundle;
public class I18n {
public static ResourceBundle DISPLAY_STRINGS = ResourceBundle.getBundle("i18n.displayStrings-assets");
}
================================================
FILE: assets/src/main/java/haveno/asset/LiquidBitcoinAddressValidator.java
================================================
package haveno.asset;
public class LiquidBitcoinAddressValidator extends RegexAddressValidator {
static private final String REGEX = "^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,87}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,87})$";
public LiquidBitcoinAddressValidator() {
super(REGEX, "validation.crypto.liquidBitcoin.invalidAddress");
}
}
================================================
FILE: assets/src/main/java/haveno/asset/NetworkParametersAdapter.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.asset;
import org.bitcoinj.core.BitcoinSerializer;
import org.bitcoinj.core.Block;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.StoredBlock;
import org.bitcoinj.core.VerificationException;
import org.bitcoinj.store.BlockStore;
import org.bitcoinj.utils.MonetaryFormat;
/**
* Convenient abstract {@link NetworkParameters} base class providing no-op
* implementations of all methods that are not required for address validation
* purposes.
*
* @author Chris Beams
* @since 0.7.0
*/
public abstract class NetworkParametersAdapter extends NetworkParameters {
@Override
public String getPaymentProtocolId() {
return PAYMENT_PROTOCOL_ID_MAINNET;
}
@Override
public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore)
throws VerificationException {
}
@Override
public Coin getMaxMoney() {
return null;
}
@Override
public Coin getMinNonDustOutput() {
return null;
}
@Override
public MonetaryFormat getMonetaryFormat() {
return null;
}
@Override
public String getUriScheme() {
return null;
}
@Override
public boolean hasMaxMoney() {
return false;
}
@Override
public BitcoinSerializer getSerializer(boolean parseRetain) {
return null;
}
@Override
public int getProtocolVersionNum(ProtocolVersion version) {
return 0;
}
}
================================================
FILE: assets/src/main/java/haveno/asset/PrintTool.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.asset;
import java.util.Comparator;
public class PrintTool {
public static void main(String[] args) {
// Prints out all coins in the format used in the FAQ webpage.
// Run that and copy paste the result to the FAQ webpage at new releases.
StringBuilder sb = new StringBuilder();
new AssetRegistry().stream()
.sorted(Comparator.comparing(o -> o.getName().toLowerCase()))
.filter(e -> !e.getTickerSymbol().equals("BTC"))
.map(e -> new Pair(e.getName(), e.getTickerSymbol())) // We want to get rid of duplicated entries for regtest/testnet...
.distinct()
.forEach(e -> sb.append("“")
.append(e.right)
.append("”, “")
.append(e.left)
.append("”")
.append("\n"));
System.out.println(sb.toString());
}
private static class Pair {
final String left;
final String right;
Pair(String left, String right) {
this.left = left;
this.right = right;
}
}
}
================================================
FILE: assets/src/main/java/haveno/asset/RegexAddressValidator.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.asset;
/**
* Validates an {@link Asset} address against a given regular expression.
*
* @author Chris Beams
* @since 0.7.0
*/
public class RegexAddressValidator implements AddressValidator {
private final String regex;
private final String errorMessageI18nKey;
public RegexAddressValidator(String regex) {
this(regex, null);
}
public RegexAddressValidator(String regex, String errorMessageI18nKey) {
this.regex = regex;
this.errorMessageI18nKey = errorMessageI18nKey;
}
@Override
public AddressValidationResult validate(String address) {
if (!address.matches(regex))
if (errorMessageI18nKey == null) return AddressValidationResult.invalidStructure();
else return AddressValidationResult.invalidAddress("", errorMessageI18nKey);
return AddressValidationResult.validAddress();
}
}
================================================
FILE: assets/src/main/java/haveno/asset/RippleAddressValidator.java
================================================
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see .
*/
package haveno.asset;
/**
* Validates a Ripple address using a regular expression.
*/
public class RippleAddressValidator extends RegexAddressValidator {
public RippleAddressValidator() {
super("^r[1-9A-HJ-NP-Za-km-z]{25,34}$");
}
public RippleAddressValidator(String errorMessageI18nKey) {
super("^r[1-9A-HJ-NP-Za-km-z]{25,34}$", errorMessageI18nKey);
}
}
================================================
FILE: assets/src/main/java/haveno/asset/SolanaAddressValidator.java
================================================
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see .
*/
package haveno.asset;
import java.math.BigInteger;
/**
* Validates a Solana address.
*/
public class SolanaAddressValidator implements AddressValidator {
private static final String BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
public SolanaAddressValidator() {
}
@Override
public AddressValidationResult validate(String address) {
if (!isValidSolanaAddress(address)) {
return AddressValidationResult.invalidStructure();
}
return AddressValidationResult.validAddress();
}
/**
* Checks if the given address is a valid Solana address.
*
* This code is AI-generated and has been tested with a variety of addresses.
*
* @param addr the address to validate
* @return true if the address is valid, false otherwise
*/
private static boolean isValidSolanaAddress(String address) {
if (address == null) return false;
if (address.length() < 32 || address.length() > 44) return false; // typical Solana length range
// Check all chars are base58 valid
for (char c : address.toCharArray()) {
if (BASE58_ALPHABET.indexOf(c) == -1) return false;
}
// Decode from base58 and ensure exactly 32 bytes
byte[] decoded = decodeBase58(address);
return decoded != null && decoded.length == 32;
}
private static byte[] decodeBase58(String input) {
BigInteger num = BigInteger.ZERO;
BigInteger base = BigInteger.valueOf(58);
for (char c : input.toCharArray()) {
int digit = BASE58_ALPHABET.indexOf(c);
if (digit < 0) return null; // invalid char
num = num.multiply(base).add(BigInteger.valueOf(digit));
}
// Convert BigInteger to byte array
byte[] bytes = num.toByteArray();
// Remove sign byte if present
if (bytes.length > 1 && bytes[0] == 0) {
byte[] tmp = new byte[bytes.length - 1];
System.arraycopy(bytes, 1, tmp, 0, tmp.length);
bytes = tmp;
}
// Count leading '1's and add leading zero bytes
int leadingZeros = 0;
for (char c : input.toCharArray()) {
if (c == '1') leadingZeros++;
else break;
}
byte[] result = new byte[leadingZeros + bytes.length];
System.arraycopy(bytes, 0, result, leadingZeros, bytes.length);
return result;
}
}
================================================
FILE: assets/src/main/java/haveno/asset/Token.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.asset;
/**
* Abstract base class for {@link Asset}s that do not have their own dedicated blockchain,
* but are rather based on or derived from another blockchain. Contrast with {@link Coin}.
* Note that this is essentially a "marker" base class in the sense that it (currently)
* exposes no additional information or functionality beyond that found in
* {@link AbstractAsset}, but it is nevertheless useful in distinguishing between major
* different {@code Asset} types.
*
* @author Chris Beams
* @since 0.7.0
* @see Erc20Token
*/
public abstract class Token extends AbstractAsset {
public Token(String name, String tickerSymbol, AddressValidator addressValidator) {
super(name, tickerSymbol, addressValidator);
}
}
================================================
FILE: assets/src/main/java/haveno/asset/Trc20Token.java
================================================
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see .
*/
package haveno.asset;
/**
* Abstract base class for Tron-based {@link Token}s that implement the
* TRC-20 Token Standard.
*/
public abstract class Trc20Token extends Token {
public Trc20Token(String name, String tickerSymbol) {
super(name, tickerSymbol, new RegexAddressValidator("T[A-Za-z1-9]{33}"));
}
}
================================================
FILE: assets/src/main/java/haveno/asset/TronAddressValidator.java
================================================
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see .
*/
package haveno.asset;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.util.Arrays;
/**
* Validates a Tron address.
*/
public class TronAddressValidator implements AddressValidator {
private static final String BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
private static final byte MAINNET_PREFIX = 0x41;
public TronAddressValidator() {
}
@Override
public AddressValidationResult validate(String address) {
if (!isValidTronAddress(address)) {
return AddressValidationResult.invalidStructure();
}
return AddressValidationResult.validAddress();
}
/**
* Checks if the given address is a valid Solana address.
*
* This code is AI-generated and has been tested with a variety of addresses.
*
* @param addr the address to validate
* @return true if the address is valid, false otherwise
*/
private static boolean isValidTronAddress(String address) {
if (address == null || address.length() != 34) return false;
byte[] decoded = decodeBase58(address);
if (decoded == null || decoded.length != 25) return false; // 21 bytes data + 4 bytes checksum
// Check checksum
byte[] data = Arrays.copyOfRange(decoded, 0, 21);
byte[] checksum = Arrays.copyOfRange(decoded, 21, 25);
byte[] calculatedChecksum = Arrays.copyOfRange(doubleSHA256(data), 0, 4);
if (!Arrays.equals(checksum, calculatedChecksum)) return false;
// Check mainnet prefix
return data[0] == MAINNET_PREFIX;
}
private static byte[] decodeBase58(String input) {
BigInteger num = BigInteger.ZERO;
BigInteger base = BigInteger.valueOf(58);
for (char c : input.toCharArray()) {
int digit = BASE58_ALPHABET.indexOf(c);
if (digit < 0) return null;
num = num.multiply(base).add(BigInteger.valueOf(digit));
}
// Convert BigInteger to byte array
byte[] bytes = num.toByteArray();
if (bytes.length > 1 && bytes[0] == 0) {
bytes = Arrays.copyOfRange(bytes, 1, bytes.length);
}
// Add leading zero bytes for '1's
int leadingZeros = 0;
for (char c : input.toCharArray()) {
if (c == '1') leadingZeros++;
else break;
}
byte[] result = new byte[leadingZeros + bytes.length];
System.arraycopy(bytes, 0, result, leadingZeros, bytes.length);
return result;
}
private static byte[] doubleSHA256(byte[] data) {
try {
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
return sha256.digest(sha256.digest(data));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
================================================
FILE: assets/src/main/java/haveno/asset/coins/Bitcoin.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.asset.coins;
import haveno.asset.BitcoinAddressValidator;
import haveno.asset.Coin;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.params.MainNetParams;
import org.bitcoinj.params.RegTestParams;
import org.bitcoinj.params.TestNet3Params;
public abstract class Bitcoin extends Coin {
public Bitcoin(Network network, NetworkParameters networkParameters) {
super("Bitcoin", "BTC", new BitcoinAddressValidator(networkParameters), network);
}
public static class Mainnet extends Bitcoin {
public Mainnet() {
super(Network.MAINNET, MainNetParams.get());
}
}
public static class Testnet extends Bitcoin {
public Testnet() {
super(Network.TESTNET, TestNet3Params.get());
}
}
public static class Regtest extends Bitcoin {
public Regtest() {
super(Network.STAGENET, RegTestParams.get());
}
}
}
================================================
FILE: assets/src/main/java/haveno/asset/coins/BitcoinCash.java
================================================
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see .
*/
package haveno.asset.coins;
import haveno.asset.BitcoinCashAddressValidator;
import haveno.asset.Coin;
public class BitcoinCash extends Coin {
public BitcoinCash() {
super("Bitcoin Cash", "BCH", new BitcoinCashAddressValidator());
}
}
================================================
FILE: assets/src/main/java/haveno/asset/coins/Cardano.java
================================================
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see .
*/
package haveno.asset.coins;
import haveno.asset.CardanoAddressValidator;
import haveno.asset.Coin;
public class Cardano extends Coin {
public Cardano() {
super("Cardano", "ADA", new CardanoAddressValidator());
}
}
================================================
FILE: assets/src/main/java/haveno/asset/coins/Dogecoin.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.asset.coins;
import haveno.asset.Base58AddressValidator;
import haveno.asset.Coin;
import haveno.asset.NetworkParametersAdapter;
public class Dogecoin extends Coin {
public Dogecoin() {
super("Dogecoin", "DOGE", new Base58AddressValidator(new DogecoinMainNetParams()), Network.MAINNET);
}
public static class DogecoinMainNetParams extends NetworkParametersAdapter {
public DogecoinMainNetParams() {
this.addressHeader = 30;
this.p2shHeader = 22;
}
}
}
================================================
FILE: assets/src/main/java/haveno/asset/coins/Ether.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.asset.coins;
import haveno.asset.Coin;
import haveno.asset.EtherAddressValidator;
public class Ether extends Coin {
public Ether() {
super("Ether", "ETH", new EtherAddressValidator());
}
}
================================================
FILE: assets/src/main/java/haveno/asset/coins/Litecoin.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.asset.coins;
import haveno.asset.BitcoinAddressValidator;
import haveno.asset.Coin;
import haveno.asset.NetworkParametersAdapter;
public class Litecoin extends Coin {
public Litecoin() {
super("Litecoin", "LTC", new BitcoinAddressValidator(new LitecoinMainNetParams()), Network.MAINNET);
}
public static class LitecoinMainNetParams extends NetworkParametersAdapter {
public LitecoinMainNetParams() {
this.addressHeader = 48;
this.p2shHeader = 50;
this.segwitAddressHrp = "ltc";
}
}
}
================================================
FILE: assets/src/main/java/haveno/asset/coins/Monero.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.asset.coins;
import haveno.asset.CryptoAccountDisclaimer;
import haveno.asset.Coin;
import haveno.asset.CryptoNoteAddressValidator;
@CryptoAccountDisclaimer("account.crypto.popup.xmr.msg")
public class Monero extends Coin {
public Monero() {
super("Monero", "XMR", new CryptoNoteAddressValidator(18, 19, 42));
}
}
================================================
FILE: assets/src/main/java/haveno/asset/coins/Ripple.java
================================================
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see .
*/
package haveno.asset.coins;
import haveno.asset.Coin;
import haveno.asset.RippleAddressValidator;
public class Ripple extends Coin {
public Ripple() {
super("Ripple", "XRP", new RippleAddressValidator());
}
}
================================================
FILE: assets/src/main/java/haveno/asset/coins/Solana.java
================================================
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see .
*/
package haveno.asset.coins;
import haveno.asset.Coin;
import haveno.asset.SolanaAddressValidator;
public class Solana extends Coin {
public Solana() {
super("Solana", "SOL", new SolanaAddressValidator());
}
}
================================================
FILE: assets/src/main/java/haveno/asset/coins/Tron.java
================================================
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see .
*/
package haveno.asset.coins;
import haveno.asset.Coin;
import haveno.asset.TronAddressValidator;
public class Tron extends Coin {
public Tron() {
super("Tron", "TRX", new TronAddressValidator());
}
}
================================================
FILE: assets/src/main/java/haveno/asset/package-info.java
================================================
/*
* 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 .
*/
/**
* Haveno's family of abstractions representing different ("crypto")
* {@link haveno.asset.Asset} types such as {@link haveno.asset.Coin},
* {@link haveno.asset.Token} and {@link haveno.asset.Erc20Token}, as well as concrete
* implementations of each, such as {@link haveno.asset.coins.Bitcoin} itself, cryptos like
* {@link haveno.asset.coins.Litecoin} and {@link haveno.asset.coins.Ether} and tokens like
* {@link haveno.asset.tokens.DaiStablecoinERC20}.
*
* The purpose of this package is to provide everything necessary for registering
* ("listing") new assets and managing / accessing those assets within, e.g. the Haveno
* Desktop UI.
*
* Note that everything within this package is intentionally designed to be simple and
* low-level with no dependencies on any other Haveno packages or components.
*
* @author Chris Beams
* @since 0.7.0
*/
package haveno.asset;
================================================
FILE: assets/src/main/java/haveno/asset/tokens/AugmintEuro.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.asset.tokens;
import haveno.asset.Erc20Token;
public class AugmintEuro extends Erc20Token {
public AugmintEuro() {
super("Augmint Euro", "AEUR");
}
}
================================================
FILE: assets/src/main/java/haveno/asset/tokens/DaiStablecoinERC20.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.asset.tokens;
import haveno.asset.Erc20Token;
public class DaiStablecoinERC20 extends Erc20Token {
public DaiStablecoinERC20() {
super("Dai Stablecoin", "DAI-ERC20");
}
}
================================================
FILE: assets/src/main/java/haveno/asset/tokens/EtherStone.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.asset.tokens;
import haveno.asset.Erc20Token;
public class EtherStone extends Erc20Token {
public EtherStone() {
super("EtherStone", "ETHS");
}
}
================================================
FILE: assets/src/main/java/haveno/asset/tokens/TetherUSDERC20.java
================================================
package haveno.asset.tokens;
import haveno.asset.Erc20Token;
public class TetherUSDERC20 extends Erc20Token {
public TetherUSDERC20() {
// If you add a new USDT variant or want to change this ticker symbol you should also look here:
// core/src/main/java/haveno/core/provider/price/PriceProvider.java:getAll()
super("Tether USD", "USDT-ERC20");
}
}
================================================
FILE: assets/src/main/java/haveno/asset/tokens/TetherUSDTRC20.java
================================================
package haveno.asset.tokens;
import haveno.asset.Trc20Token;
public class TetherUSDTRC20 extends Trc20Token {
public TetherUSDTRC20() {
// If you add a new USDT variant or want to change this ticker symbol you should also look here:
// core/src/main/java/haveno/core/provider/price/PriceProvider.java:getAll()
super("Tether USD", "USDT-TRC20");
}
}
================================================
FILE: assets/src/main/java/haveno/asset/tokens/TrueUSD.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.asset.tokens;
import haveno.asset.Erc20Token;
public class TrueUSD extends Erc20Token {
public TrueUSD() {
super("TrueUSD", "TUSD");
}
}
================================================
FILE: assets/src/main/java/haveno/asset/tokens/USDCoinERC20.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.asset.tokens;
import haveno.asset.Erc20Token;
public class USDCoinERC20 extends Erc20Token {
public USDCoinERC20() {
super("USD Coin", "USDC-ERC20");
}
}
================================================
FILE: assets/src/main/java/haveno/asset/tokens/VectorspaceAI.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.asset.tokens;
import haveno.asset.Erc20Token;
public class VectorspaceAI extends Erc20Token {
public VectorspaceAI() {
super("VectorspaceAI", "VXV");
}
}
================================================
FILE: assets/src/main/resources/META-INF/services/haveno.asset.Asset
================================================
# All assets available for trading on the Haveno network.
# Contents are sorted according to the output of `sort --ignore-case --dictionary-order`.
# See haveno.asset.Asset and haveno.asset.AssetRegistry for further details.
# See https://haveno.exchange/list-asset for complete instructions.
haveno.asset.coins.Bitcoin$Mainnet
haveno.asset.coins.BitcoinCash
haveno.asset.coins.Cardano
haveno.asset.coins.Dogecoin
haveno.asset.coins.Ether
haveno.asset.coins.Litecoin
haveno.asset.coins.Monero
haveno.asset.coins.Ripple
haveno.asset.coins.Solana
haveno.asset.coins.Tron
haveno.asset.tokens.TetherUSDERC20
haveno.asset.tokens.TetherUSDTRC20
haveno.asset.tokens.USDCoinERC20
haveno.asset.tokens.DaiStablecoinERC20
================================================
FILE: assets/src/main/resources/i18n/displayStrings-assets.properties
================================================
# Keep display strings organized by domain
# Naming convention: We use camelCase and dot separated name spaces.
# Use as many sub spaces as required to make the structure clear, but as little as possible.
# E.g.: [main-view].[component].[description]
# In some cases we use enum values or constants to map to display strings
# We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces
# at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose!
# To make longer strings with better readable you can make a line break with \ which does not result in a line break
# in the display but only in the editor.
# Please use in all language files the exact same order of the entries, that way a comparison is easier.
# Please try to keep the length of the translated string similar to English. If it is longer it might break layout or
# get truncated. We will need some adjustments in the UI code to support that but we want to keep effort at the minimum.
account.crypto.popup.validation.XCP=XCP address must start with '1' and must have 34 characters.
account.crypto.popup.validation.DCR=DCR address must start with 'Dk' or 'Ds' or 'De' or 'DS' or 'Dc' or 'Pm' and must have 34 characters.
account.crypto.popup.validation.ETC=ETC address must start with '0x' and made up of letters A to F and numbers which are 40 characters long.
account.crypto.popup.validation.NMC=NMC address must start with 'N' or 'M' and must be 34 characters long.
account.crypto.popup.validation.SF= Siafund address must be made up of letters A to F and numbers which are 76 characters long.
account.crypto.popup.validation.UNO=UNO address must start with 'u' and must have 34 characters.
account.crypto.popup.validation.XZC=XZC address must start with 'a' and must have 34 characters.
================================================
FILE: assets/src/test/java/haveno/asset/AbstractAssetTest.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.asset;
import org.junit.jupiter.api.Test;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
/**
* Abstract base class for all {@link Asset} unit tests. Subclasses must implement the
* {@link #testValidAddresses()} and {@link #testInvalidAddresses()} methods, and are
* expected to use the convenient {@link #assertValidAddress(String)} and
* {@link #assertInvalidAddress(String)} assertions when doing so.
*
* Blank / empty addresses are tested automatically by this base class and are always
* considered invalid.
*
* This base class also serves as a kind of integration test for {@link AssetRegistry}, in
* that all assets tested through subclasses are tested to make sure they are also
* properly registered and available there.
*
* @author Chris Beams
* @author Bernard Labno
* @since 0.7.0
*/
public abstract class AbstractAssetTest {
private final AssetRegistry assetRegistry = new AssetRegistry();
protected final Asset asset;
public AbstractAssetTest(Asset asset) {
this.asset = asset;
}
@Test
public void testPresenceInAssetRegistry() {
assertThat(asset + " is not registered in META-INF/services/" + Asset.class.getName(),
assetRegistry.stream().anyMatch(this::hasSameTickerSymbol), is(true));
}
@Test
public void testBlank() {
assertInvalidAddress("");
}
@Test
public abstract void testValidAddresses();
@Test
public abstract void testInvalidAddresses();
protected void assertValidAddress(String address) {
AddressValidationResult result = asset.validateAddress(address);
assertThat(result.getMessage(), result.isValid(), is(true));
}
protected void assertInvalidAddress(String address) {
assertThat(asset.validateAddress(address).isValid(), is(false));
}
private boolean hasSameTickerSymbol(Asset asset) {
return this.asset.getTickerSymbol().equals(asset.getTickerSymbol());
}
}
================================================
FILE: assets/src/test/java/haveno/asset/coins/BitcoinCashTest.java
================================================
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see .
*/
package haveno.asset.coins;
import haveno.asset.AbstractAssetTest;
import org.junit.jupiter.api.Test;
public class BitcoinCashTest extends AbstractAssetTest {
public BitcoinCashTest() {
super(new BitcoinCash());
}
@Test
public void testValidAddresses() {
assertValidAddress("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhem");
assertValidAddress("3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX");
assertValidAddress("1111111111111111111114oLvT2");
assertValidAddress("1BitcoinEaterAddressDontSendf59kuE");
}
@Test
public void testInvalidAddresses() {
assertInvalidAddress("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhemqq");
assertInvalidAddress("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYheO");
assertInvalidAddress("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhek#");
}
}
================================================
FILE: assets/src/test/java/haveno/asset/coins/BitcoinTest.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.asset.coins;
import haveno.asset.AbstractAssetTest;
import org.junit.jupiter.api.Test;
public class BitcoinTest extends AbstractAssetTest {
public BitcoinTest() {
super(new Bitcoin.Mainnet());
}
@Test
public void testValidAddresses() {
assertValidAddress("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhem");
assertValidAddress("3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX");
assertValidAddress("1111111111111111111114oLvT2");
assertValidAddress("1BitcoinEaterAddressDontSendf59kuE");
assertValidAddress("bc1qj89046x7zv6pm4n00qgqp505nvljnfp6xfznyw");
}
@Test
public void testInvalidAddresses() {
assertInvalidAddress("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhemqq");
assertInvalidAddress("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYheO");
assertInvalidAddress("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhek#");
}
}
================================================
FILE: assets/src/test/java/haveno/asset/coins/CardanoTest.java
================================================
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see .
*/
package haveno.asset.coins;
import haveno.asset.AbstractAssetTest;
import org.junit.jupiter.api.Test;
public class CardanoTest extends AbstractAssetTest {
public CardanoTest() {
super(new Cardano());
}
@Test
public void testValidAddresses() {
assertValidAddress("addr1vpu5vlrf4xkxv2qpwngf6cjhtw542ayty80v8dyr49rf5eg0yu80w");
assertValidAddress("addr1q8gg2r3vf9zggn48g7m8vx62rwf6warcs4k7ej8mdzmqmesj30jz7psduyk6n4n2qrud2xlv9fgj53n6ds3t8cs4fvzs05yzmz");
}
@Test
public void testInvalidAddresses() {
assertInvalidAddress("addr1Q9r4y0gx0m4hd5s2u3pnj7ufc4s0ghqzj7u6czxyfks5cty5k5yq5qp6gmw5v7uqvx2g4kw6zjhx4l6fnhcey9lg9nys6v2mpu");
assertInvalidAddress("addr2q9r4y0gx0m4hd5s2u3pnj7ufc4s0ghqzj7u6czxyfks5cty5k5yq5qp6gmw5v7uqvx2g4kw6zjhx4l6fnhcey9lg9nys6v2mpu");
assertInvalidAddress("addr2vpu5vlrf4xkxv2qpwngf6cjhtw542ayty80v8dyr49rf5eg0yu80w");
assertInvalidAddress("Ae2tdPwUPEYxkYw5GrFyqb4Z9TzXo8f1WnWpPZP1sXrEn1pz2VU3CkJ8aTQ");
}
}
================================================
FILE: assets/src/test/java/haveno/asset/coins/DogecoinTest.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.asset.coins;
import haveno.asset.AbstractAssetTest;
import org.junit.jupiter.api.Test;
public class DogecoinTest extends AbstractAssetTest {
public DogecoinTest() {
super(new Dogecoin());
}
@Test
public void testValidAddresses() {
assertValidAddress("DEa7damK8MsbdCJztidBasZKVsDLJifWfE");
assertValidAddress("DNkkfdUvkCDiywYE98MTVp9nQJTgeZAiFr");
assertValidAddress("DDWUYQ3GfMDj8hkx8cbnAMYkTzzAunAQxg");
}
@Test
public void testInvalidAddresses() {
assertInvalidAddress("1DDWUYQ3GfMDj8hkx8cbnAMYkTzzAunAQxg");
assertInvalidAddress("DDWUYQ3GfMDj8hkx8cbnAMYkTzzAunAQxgs");
assertInvalidAddress("DDWUYQ3GfMDj8hkx8cbnAMYkTzzAunAQxg#");
}
}
================================================
FILE: assets/src/test/java/haveno/asset/coins/LitecoinTest.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.asset.coins;
import haveno.asset.AbstractAssetTest;
import org.junit.jupiter.api.Test;
public class LitecoinTest extends AbstractAssetTest {
public LitecoinTest() {
super(new Litecoin());
}
@Test
public void testValidAddresses() {
assertValidAddress("Lg3PX8wRWmApFCoCMAsPF5P9dPHYQHEWKW");
assertValidAddress("LTuoeY6RBHV3n3cfhXVVTbJbxzxnXs9ofm");
assertValidAddress("LgfapHEPhZbRF9pMd5WPT35hFXcZS1USrW");
assertValidAddress("M8T1B2Z97gVdvmfkQcAtYbEepune1tzGua");
assertValidAddress("ltc1qr07zu594qf63xm7l7x6pu3a2v39m2z6hh5pp4t");
assertValidAddress("ltc1qzvcgmntglcuv4smv3lzj6k8szcvsrmvk0phrr9wfq8w493r096ssm2fgsw");
assertValidAddress("MESruSiB2uC9i7tMU6VMUVom91ohM7Rnbd");
assertValidAddress("ltc1q2a0laq2jg2gntzhfs43qptajd325kkx7hrq9cs");
assertValidAddress("ltc1qd6d54mt8xxcg0xg3l0vh6fymdfvd2tv0vnwyrv");
assertValidAddress("ltc1gmay6ht028aurcm680f8e8wxdup07y2tq46f6z2d4v8rutewqmmcqk29jtm");
assertValidAddress("MTf4tP1TCNBn8dNkyxeBVoPrFCcVzxJvvh");
assertValidAddress("LaRoRBC6utQtY3U2FbHwhmhhDPyxodDeKA");
assertValidAddress("MDMFP9Dx84tyaxiYksjvkG1jymBdqCuHGA");
//assertValidAddress("3MSvaVbVFFLML86rt5eqgA9SvW23upaXdY"); // deprecated
}
@Test
public void testInvalidAddresses() {
assertInvalidAddress("1LgfapHEPhZbRF9pMd5WPT35hFXcZS1USrW");
assertInvalidAddress("LgfapHEPhZbdRF9pMd5WPT35hFXcZS1USrW");
assertInvalidAddress("LgfapHEPhZbRF9pMd5WPT35hFXcZS1USrW#");
assertInvalidAddress("3MSvaVbVFFLML86rt5eqgl9SvW23upaXdY"); // contains lowercase l
assertInvalidAddress("LURw7hYhREXjWHyiXhQNsKInWtPezwNe98"); // contains uppercase I
assertInvalidAddress("LM4ch8ZtAowdiGLSnf92MrMOC9dVmve2hr"); // contains uppercase O
assertInvalidAddress("MArsfeyS7P0HzsqLpAFGC9pFdhuqHgdL2R"); // contains number 0
assertInvalidAddress("ltc1qr6quwn3v2gxpadd0cu040r9385gayk5vdcyl5"); // too short
assertInvalidAddress("ltc1q5det08ke2gpet06wczcdfs2v3hgfqllxw28uln8vxxx82qlue6uswceljma"); // too long
assertInvalidAddress("MADpfTtabZ6pDjms4pMd3ZmnrgyhTCo4N8?time=1708476729&exp=86400"); // additional information
assertInvalidAddress("ltc1q8tk47lvgqu55h4pfast39r3t9360gmll5z9m6z?time=1708476604&exp=600"); // additional information
assertInvalidAddress("ltc1q026xyextkwhmveh7rpf6v6mp5p88vwc25aynxr?time=1708476626"); // additional information
}
}
================================================
FILE: assets/src/test/java/haveno/asset/coins/MoneroTest.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.asset.coins;
import haveno.asset.AbstractAssetTest;
import org.junit.jupiter.api.Test;
public class MoneroTest extends AbstractAssetTest {
public MoneroTest() {
super(new Monero());
}
@Test
public void testValidAddresses() {
assertValidAddress("4BJHitCigGy6giuYsJFP26KGkTKiQDJ6HJP1pan2ir2CCV8Twc2WWmo4fu1NVXt8XLGYAkjo5cJ3yH68Lfz9ZXEUJ9MeqPW");
assertValidAddress("46tM15KsogEW5MiVmBn7waPF8u8ZsB6aHjJk7BAv1wvMKfWhQ2h2so5BCJ9cRakfPt5BFo452oy3K8UK6L2u2v7aJ3Nf7P2");
assertValidAddress("86iQTnEqQ9mXJFvBvbY3KU5do5Jh2NCkpTcZsw3TMZ6oKNJhELvAreZFQ1p8EknRRTKPp2vg9fJvy47Q4ARVChjLMuUAFQJ");
// integrated addresses
assertValidAddress("4LL9oSLmtpccfufTMvppY6JwXNouMBzSkbLYfpAV5Usx3skxNgYeYTRj5UzqtReoS44qo9mtmXCqY45DJ852K5Jv2bYXZKKQePHES9khPK");
assertValidAddress("4GdoN7NCTi8a5gZug7PrwZNKjvHFmKeV11L6pNJPgj5QNEHsN6eeX3DaAQFwZ1ufD4LYCZKArktt113W7QjWvQ7CWD1FFMXoYHeE6M55P9");
assertValidAddress("4GdoN7NCTi8a5gZug7PrwZNKjvHFmKeV11L6pNJPgj5QNEHsN6eeX3DaAQFwZ1ufD4LYCZKArktt113W7QjWvQ7CW82yHFEGvSG3NJRNtH");
}
@Test
public void testInvalidAddresses() {
assertInvalidAddress("");
assertInvalidAddress("4BJHitCigGy6giuYsJFP26KGkTKiQDJ6HJP1pan2ir2CCV8Twc2WWmo4fu1NVXt8XLGYAkjo5cJ3yH68Lfz9ZXEUJ9MeqP");
assertInvalidAddress("4BJHitCigGy6giuYsJFP26KGkTKiQDJ6HJP1pan2ir2CCV8Twc2WWmo4fu1NVXt8XLGYAkjo5cJ3yH68Lfz9ZXEUJ9MeqPWW");
assertInvalidAddress("86iQTnEqQ9mXJFvBvbY3KU5do5Jh2NCkpTcZsw3TMZ6oKNJhELvAreZFQ1p8EknRRTKPp2vg9fJvy47Q4ARVChjLMuUAFQ!");
assertInvalidAddress("76iQTnEqQ9mXJFvBvbY3KU5do5Jh2NCkpTcZsw3TMZ6oKNJhELvAreZFQ1p8EknRRTKPp2vg9fJvy47Q4ARVChjLMuUAFQJ");
}
}
================================================
FILE: assets/src/test/java/haveno/asset/coins/RippleTest.java
================================================
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see .
*/
package haveno.asset.coins;
import haveno.asset.AbstractAssetTest;
import org.junit.jupiter.api.Test;
public class RippleTest extends AbstractAssetTest {
public RippleTest() {
super(new Ripple());
}
@Test
public void testValidAddresses() {
assertValidAddress("r9CxAMAoZAgyVGP8CY9F1arzf9bJg3Y7U8");
assertValidAddress("rsXMbDtCAmzSWajWiii7ffWygAjYVNDxY7");
assertValidAddress("rE3nYkQy121JEVb37JKX8LSH6wUBnNvNo2");
assertValidAddress("rMzucuWFUEE6aM9DC992BqqMgZNPrv4kvi");
assertValidAddress("rJUmAFPWE36cpdbN4DUEAFBLtG2xkEavY8");
}
@Test
public void testInvalidAddresses() {
assertInvalidAddress("RJUmAFPWE36cpdbN4DUEAFBLtG2xkEavY8");
assertInvalidAddress("zJUmAFPWE36cpdbN4DUEAFBLtG2xkEavY8");
assertInvalidAddress("1LgfapHEPhZbRF9pMd5WPT35hFXcZS1USrW");
}
}
================================================
FILE: assets/src/test/java/haveno/asset/coins/SolanaTest.java
================================================
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see .
*/
package haveno.asset.coins;
import haveno.asset.AbstractAssetTest;
import org.junit.jupiter.api.Test;
public class SolanaTest extends AbstractAssetTest {
public SolanaTest() {
super(new Solana());
}
@Test
public void testValidAddresses() {
assertValidAddress("4Nd1mYZbtJbHkj9QwxAXWah8X9M8vZ9H1fsn6uhPW33k");
assertValidAddress("8HoQnePLqPj4M7PUDzfw8e3Ymdwgc7NqAcoH7okh4wz7");
assertValidAddress("H3C5pGrMmD8FrGd9VRtNVbY3tWusJX3A1u33f9bdBpsk");
assertValidAddress("7zVhJcA5s8zfg3UoDUuG4zmnqaVmLqj6L6F6L8WPLnYw");
assertValidAddress("AVHUu155WoNexeNCGce8mrb8hvg8pBgvCJh4vtd3Q1RV");
assertValidAddress("8HoQnePLqPj4M7PUDzfw8e3Ymdwgc7NqAcoH7okh4wz");
}
@Test
public void testInvalidAddresses() {
assertInvalidAddress("4Nd1mYZbtJbHkj9QwxAXWah8X9M8vZ9H1fsn6uhPW33O");
assertInvalidAddress("H3C5pGrMmD8FrGd9VRtNVbY3tWusJX3A1u33f9bdBpskAAA");
assertInvalidAddress("1");
assertInvalidAddress("abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ123456789");
}
}
================================================
FILE: assets/src/test/java/haveno/asset/coins/TetherUSDERC20Test.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.asset.coins;
import haveno.asset.AbstractAssetTest;
import haveno.asset.tokens.TetherUSDERC20;
import org.junit.jupiter.api.Test;
public class TetherUSDERC20Test extends AbstractAssetTest {
public TetherUSDERC20Test() {
super(new TetherUSDERC20());
}
@Test
public void testValidAddresses() {
assertValidAddress("0x2a65Aca4D5fC5B5C859090a6c34d164135398226");
assertValidAddress("2a65Aca4D5fC5B5C859090a6c34d164135398226");
}
@Test
public void testInvalidAddresses() {
assertInvalidAddress("0x2a65Aca4D5fC5B5C859090a6c34d1641353982266");
assertInvalidAddress("0x2a65Aca4D5fC5B5C859090a6c34d16413539822g");
assertInvalidAddress("2a65Aca4D5fC5B5C859090a6c34d16413539822g");
}
}
================================================
FILE: assets/src/test/java/haveno/asset/coins/TetherUSDTRC20Test.java
================================================
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see .
*/
package haveno.asset.coins;
import haveno.asset.AbstractAssetTest;
import haveno.asset.tokens.TetherUSDTRC20;
import org.junit.jupiter.api.Test;
public class TetherUSDTRC20Test extends AbstractAssetTest {
public TetherUSDTRC20Test() {
super(new TetherUSDTRC20());
}
@Test
public void testValidAddresses() {
assertValidAddress("TVnmu3E6DYVL4bpAoZnPNEPVUrgC7eSWaX");
}
@Test
public void testInvalidAddresses() {
assertInvalidAddress("0x2a65Aca4D5fC5B5C859090a6c34d1641353982266");
assertInvalidAddress("0x2a65Aca4D5fC5B5C859090a6c34d16413539822g");
assertInvalidAddress("2a65Aca4D5fC5B5C859090a6c34d16413539822g");
}
}
================================================
FILE: assets/src/test/java/haveno/asset/coins/TronTest.java
================================================
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see .
*/
package haveno.asset.coins;
import haveno.asset.AbstractAssetTest;
import org.junit.jupiter.api.Test;
public class TronTest extends AbstractAssetTest {
public TronTest() {
super(new Tron());
}
@Test
public void testValidAddresses() {
assertValidAddress("TRjE1H8dxypKM1NZRdysbs9wo7huR4bdNz");
assertValidAddress("THdUXD3mZqT5aMnPQMtBSJX9ANGjaeUwQK");
assertValidAddress("THUE6WTLaEGytFyuGJQUcKc3r245UKypoi");
assertValidAddress("TH7vVF9RTMXM9x7ZnPnbNcEph734hpu8cf");
assertValidAddress("TJNtFduS4oebw3jgGKCYmgSpTdyPieb6Ha");
}
@Test
public void testInvalidAddresses() {
assertInvalidAddress("TJRyWwFs9wTFGZg3L8nL62xwP9iK8QdK9R");
assertInvalidAddress("TJRyWwFs9wTFGZg3L8nL62xwP9iK8QdK9X");
assertInvalidAddress("1JRyWwFs9wTFGZg3L8nL62xwP9iK8QdK9R");
assertInvalidAddress("TGzz8gjYiYRqpfmDwnLxfgPuLVNmpCswVo");
}
}
================================================
FILE: build.gradle
================================================
import org.apache.tools.ant.taskdefs.condition.Os
import org.gradle.internal.logging.ConsoleRenderer
buildscript {
repositories {
gradlePluginPortal()
}
dependencies {
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.17'
classpath 'com.google.gradle:osdetector-gradle-plugin:1.7.3'
classpath 'com.github.johnrengelman:shadow:8.1.1'
classpath 'org.springframework.boot:spring-boot-gradle-plugin:3.2.3'
classpath 'com.gradle:gradle-enterprise-gradle-plugin:3.16.1' // added for windows build verification-metadata.xml error
}
}
configure(rootProject) {
// remove the 'haveno-*' scripts and 'lib' dir generated by the 'installDist' task
task clean {
doLast {
delete fileTree(dir: rootProject.projectDir, include: 'haveno-*'), 'lib'
}
}
}
configure(subprojects) {
apply plugin: 'java'
apply plugin: 'com.google.osdetector'
// Apply the jacoco plugin to add support for test coverage
apply plugin: 'jacoco'
apply plugin: 'jacoco-report-aggregation'
apply plugin: 'checkstyle'
sourceCompatibility = JavaVersion.VERSION_21
ext { // in alphabetical order
bcVersion = '1.63'
bitcoinjVersion = '2a80db4'
codecVersion = '1.13'
cowwocVersion = '1.2'
easybindVersion = '1.0.3'
easyVersion = '4.0.1'
findbugsVersion = '3.0.2'
firebaseVersion = '6.2.0'
fontawesomefxVersion = '8.0.0'
fontawesomefxCommonsVersion = '9.1.2'
fontawesomefxMaterialdesignfontVersion = '2.0.26-9.1.2'
grpcVersion = '1.42.1'
gsonVersion = '2.8.5'
guavaVersion = '32.1.1-jre'
guiceVersion = '7.0.0'
moneroJavaVersion = '0.8.43'
httpclient5Version = '5.0'
hamcrestVersion = '2.2'
httpclientVersion = '4.5.12'
httpcoreVersion = '4.4.13'
ioVersion = '2.6'
jacksonVersion = '2.12.1'
javafxVersion = '21.0.9'
javaxAnnotationVersion = '1.2'
jcsvVersion = '1.4.0'
jetbrainsAnnotationsVersion = '13.0'
jfoenixVersion = '9.0.10'
joptVersion = '5.0.4'
jsonsimpleVersion = '1.1.1'
jsonrpc4jVersion = '1.6.0.bisq.1'
jupiterVersion = '5.9.2'
kotlinVersion = '1.3.41'
langVersion = '3.11'
logbackVersion = '1.1.11'
loggingVersion = '1.2'
lombokVersion = '1.18.30'
mockitoVersion = '5.10.0'
netlayerVersion = 'd9c60be46d' // Tor browser version 14.0.7 and tor binary version: 0.4.8.14
protobufVersion = '3.19.1'
protocVersion = protobufVersion
pushyVersion = '0.13.2'
qrgenVersion = '1.3'
slf4jVersion = '1.7.30'
sparkVersion = '2.5.2'
def osName = osdetector.os == 'osx' ? 'mac' : osdetector.os == 'windows' ? 'win' : osdetector.os
def osArch = System.getProperty("os.arch").toLowerCase()
os = (osName == 'mac' && (osArch.contains('aarch64') || osArch.contains('arm'))) ? 'mac-aarch64' : osName
}
configurations {
javafxVerification
}
repositories {
mavenCentral()
//mavenLocal()
maven { url 'https://jitpack.io' }
maven { url 'https://mvnrepository.com' }
}
tasks.withType(JavaCompile) {
options.encoding = 'UTF-8'
}
checkstyle {
toolVersion = '10.8.1'
// https://raw.githubusercontent.com/checkstyle/checkstyle/checkstyle-10.8.1/src/main/resources/google_checks.xml
configFile = rootProject.file("$rootDir/config/checkstyle/checkstyle.xml")
}
tasks.withType(Checkstyle) {
minHeapSize.set('200m')
maxHeapSize.set('1g')
}
jacoco {
toolVersion = "0.8.10"
reportsDirectory = file("$rootDir/build/reports/jacoco")
}
test.finalizedBy {
testCodeCoverageReport {
// tests are required to run before generating the report
reports {
xml.required.set(true)
html.required.set(false)
}
}
}
test {
useJUnitPlatform()
}
}
configure([project(':cli'),
project(':daemon'),
project(':desktop'),
project(':monitor'),
project(':relay'),
project(':seednode'),
project(':statsnode'),
project(':inventory'),
project(':apitest')]) {
apply plugin: 'application'
build.dependsOn installDist
installDist.destinationDir = file('build/app')
distZip.enabled = false
// the 'installDist' and 'startScripts' blocks below configure haveno executables to put
// generated shell scripts in the root project directory, such that users can easily
// discover and invoke e.g. ./haveno-desktop, ./haveno-seednode, etc.
// See https://stackoverflow.com/q/46327736 for details.
installDist {
doLast {
// copy generated shell scripts, e.g. `haveno-desktop` directly to the project
// root directory for discoverability and ease of use
copy {
from "$destinationDir/bin"
into rootProject.projectDir
}
// copy libs required for generated shell script classpaths to 'lib' dir under
// the project root directory
copy {
from "$destinationDir/lib"
into "${rootProject.projectDir}/lib"
}
// edit generated shell scripts such that they expect to be executed in the
// project root dir as opposed to a 'bin' subdirectory
if (osdetector.os == 'windows') {
def windowsScriptFile = file("${rootProject.projectDir}/haveno-${applicationName}.bat")
windowsScriptFile.text = windowsScriptFile.text.replace(
'set APP_HOME=%DIRNAME%..', 'set APP_HOME=%DIRNAME%')
if (applicationName == 'desktop') {
windowsScriptFile.text = windowsScriptFile.text.replace(
'DEFAULT_JVM_OPTS=', 'DEFAULT_JVM_OPTS=-XX:MaxRAM=4g ' +
'--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=ALL-UNNAMED ' +
'--add-opens=javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED ' +
'--add-opens=java.base/java.lang.reflect=ALL-UNNAMED ' +
'--add-opens=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED')
}
}
else {
def unixScriptFile = file("${rootProject.projectDir}/haveno-$applicationName")
unixScriptFile.text = unixScriptFile.text.replace(
'APP_HOME=$( cd "${APP_HOME:-./}.." > /dev/null && pwd -P ) || exit', 'APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit')
if (applicationName == 'desktop') {
unixScriptFile.text = unixScriptFile.text.replace(
'DEFAULT_JVM_OPTS=""', 'DEFAULT_JVM_OPTS="-XX:MaxRAM=4g ' +
'--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=ALL-UNNAMED ' +
'--add-opens=javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED ' +
'--add-opens=java.base/java.lang.reflect=ALL-UNNAMED ' +
'--add-opens=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED"')
}
}
if (applicationName == 'apitest') {
// Pass the logback config file as a system property to avoid chatty
// logback startup due to multiple logback.xml files in the classpath
// (:daemon & :cli).
def script = file("${rootProject.projectDir}/haveno-$applicationName")
script.text = script.text.replace(
'DEFAULT_JVM_OPTS=""', 'DEFAULT_JVM_OPTS="' +
'-Dlogback.configurationFile=apitest/build/resources/main/logback.xml"')
}
if (osdetector.os != 'windows')
delete fileTree(dir: rootProject.projectDir, include: 'haveno-*.bat')
else
delete fileTree(dir: rootProject.projectDir, include: 'haveno-*', exclude: '*.bat')
}
}
startScripts {
// rename scripts from, e.g. `desktop` to `haveno-desktop`
applicationName = "haveno-$applicationName"
}
}
configure(project(':proto')) {
apply plugin: 'com.google.protobuf'
dependencies {
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
compileOnly "javax.annotation:javax.annotation-api:$javaxAnnotationVersion"
compileOnly "org.projectlombok:lombok:$lombokVersion"
implementation "ch.qos.logback:logback-classic:$logbackVersion"
implementation "ch.qos.logback:logback-core:$logbackVersion"
implementation "com.google.guava:guava:$guavaVersion"
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
implementation "org.slf4j:slf4j-api:$slf4jVersion"
implementation("io.grpc:grpc-protobuf:$grpcVersion") {
exclude(module: 'animal-sniffer-annotations')
exclude(module: 'guava')
}
implementation("io.grpc:grpc-stub:$grpcVersion") {
exclude(module: 'animal-sniffer-annotations')
exclude(module: 'guava')
}
}
sourceSets.main.java.srcDirs += [
'build/generated/source/proto/main/grpc',
'build/generated/source/proto/main/java'
]
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:${protocVersion}"
}
plugins {
grpc {
artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}"
}
}
generateProtoTasks {
all()*.plugins { grpc {} }
}
}
}
configure(project(':assets')) {
dependencies {
implementation("com.github.bisq-network:bitcoinj:$bitcoinjVersion") {
exclude(module: 'bcprov-jdk15on')
exclude(module: 'guava')
exclude(module: 'jsr305')
exclude(module: 'okhttp')
exclude(module: 'okio')
exclude(module: 'protobuf-java')
exclude(module: 'slf4j-api')
}
implementation "com.google.guava:guava:$guavaVersion"
implementation "org.apache.commons:commons-lang3:$langVersion"
implementation "org.slf4j:slf4j-api:$slf4jVersion"
testImplementation "org.hamcrest:hamcrest:$hamcrestVersion"
testImplementation "org.junit.jupiter:junit-jupiter-api:$jupiterVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$jupiterVersion"
}
}
configure(project(':common')) {
dependencies {
implementation project(':proto')
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
compileOnly "javax.annotation:javax.annotation-api:$javaxAnnotationVersion"
compileOnly "org.projectlombok:lombok:$lombokVersion"
implementation "ch.qos.logback:logback-classic:$logbackVersion"
implementation "ch.qos.logback:logback-core:$logbackVersion"
implementation("com.github.bisq-network:bitcoinj:$bitcoinjVersion") {
exclude(module: 'jsr305')
exclude(module: 'slf4j-api')
exclude(module: 'guava')
exclude(module: 'protobuf-java')
exclude(module: 'bcprov-jdk15on')
exclude(module: 'okhttp')
exclude(module: 'okio')
}
implementation "com.google.code.findbugs:jsr305:$findbugsVersion"
implementation "com.google.code.gson:gson:$gsonVersion"
implementation "com.google.guava:guava:$guavaVersion"
implementation("com.google.inject:guice:$guiceVersion") {
exclude(module: 'guava')
}
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion"
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
implementation "commons-io:commons-io:$ioVersion"
implementation "net.sf.jopt-simple:jopt-simple:$joptVersion"
implementation "org.apache.commons:commons-lang3:$langVersion"
implementation "org.bouncycastle:bcpg-jdk15on:$bcVersion"
implementation "org.jetbrains:annotations:$jetbrainsAnnotationsVersion"
implementation "org.slf4j:slf4j-api:$slf4jVersion"
testImplementation "org.hamcrest:hamcrest:$hamcrestVersion"
testImplementation "org.junit.jupiter:junit-jupiter-api:$jupiterVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$jupiterVersion"
runtimeOnly("io.grpc:grpc-netty-shaded:$grpcVersion") {
exclude(module: 'guava')
exclude(module: 'animal-sniffer-annotations')
}
// override transitive dependency and use latest version from bisq
implementation(group: 'com.github.bisq-network', name: 'jtorctl') { version { strictly "[b2a172f44edcd8deaa5ed75d936dcbb007f0d774]" } }
implementation "org.openjfx:javafx-base:$javafxVersion:$os"
implementation "org.openjfx:javafx-graphics:$javafxVersion:$os"
}
}
configure(project(':p2p')) {
dependencies {
implementation project(':proto')
implementation project(':common')
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
compileOnly "org.projectlombok:lombok:$lombokVersion"
implementation "com.google.guava:guava:$guavaVersion"
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
implementation "org.fxmisc.easybind:easybind:$easybindVersion"
implementation "org.slf4j:slf4j-api:$slf4jVersion"
implementation "org.apache.commons:commons-lang3:$langVersion"
implementation("com.github.haveno-dex.netlayer:tor.external:$netlayerVersion") {
exclude(module: 'slf4j-api')
}
implementation("com.github.haveno-dex.netlayer:tor.native:$netlayerVersion") {
exclude(module: 'slf4j-api')
}
implementation("com.github.bisq-network:bitcoinj:$bitcoinjVersion") {
exclude(module: 'bcprov-jdk15on')
exclude(module: 'guava')
exclude(module: 'jsr305')
exclude(module: 'okhttp')
exclude(module: 'okio')
exclude(module: 'protobuf-java')
exclude(module: 'slf4j-api')
}
implementation("com.google.inject:guice:$guiceVersion") {
exclude(module: 'guava')
}
implementation("org.apache.httpcomponents:httpclient:$httpclientVersion") {
exclude(module: 'commons-codec')
}
testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion"
testCompileOnly "org.projectlombok:lombok:$lombokVersion"
testImplementation "org.junit.jupiter:junit-jupiter-api:$jupiterVersion"
testImplementation "org.junit.jupiter:junit-jupiter-params:$jupiterVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$jupiterVersion"
testImplementation "ch.qos.logback:logback-classic:$logbackVersion"
testImplementation "ch.qos.logback:logback-core:$logbackVersion"
testImplementation "org.apache.commons:commons-lang3:$langVersion"
testImplementation("org.mockito:mockito-core:$mockitoVersion")
testImplementation("org.mockito:mockito-junit-jupiter:$mockitoVersion")
implementation "org.openjfx:javafx-base:$javafxVersion:$os"
implementation "org.openjfx:javafx-graphics:$javafxVersion:$os"
}
}
configure(project(':core')) {
dependencies {
implementation project(':proto')
implementation project(':assets')
implementation project(':common')
implementation project(':p2p')
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
compileOnly "javax.annotation:javax.annotation-api:$javaxAnnotationVersion"
compileOnly "org.projectlombok:lombok:$lombokVersion"
implementation "ch.qos.logback:logback-classic:$logbackVersion"
implementation "ch.qos.logback:logback-core:$logbackVersion"
implementation "com.fasterxml.jackson.core:jackson-core:$jacksonVersion"
implementation "com.fasterxml.jackson.core:jackson-annotations:$jacksonVersion"
implementation "com.google.code.findbugs:jsr305:$findbugsVersion"
implementation "com.google.code.gson:gson:$gsonVersion"
implementation "com.google.guava:guava:$guavaVersion"
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
implementation "commons-codec:commons-codec:$codecVersion"
implementation "commons-io:commons-io:$ioVersion"
implementation "net.sf.jopt-simple:jopt-simple:$joptVersion"
implementation "org.apache.commons:commons-lang3:$langVersion"
implementation "org.apache.httpcomponents:httpcore:$httpcoreVersion"
implementation "org.fxmisc.easybind:easybind:$easybindVersion"
implementation "org.jetbrains:annotations:$jetbrainsAnnotationsVersion"
implementation "org.slf4j:slf4j-api:$slf4jVersion"
implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") {
exclude(module: 'jackson-annotations')
}
implementation("com.github.haveno-dex.netlayer:tor.native:$netlayerVersion") {
exclude(module: 'slf4j-api')
}
implementation("com.github.haveno-dex.netlayer:tor.external:$netlayerVersion") {
exclude(module: 'slf4j-api')
}
implementation("com.github.bisq-network:bitcoinj:$bitcoinjVersion") {
exclude(module: 'bcprov-jdk15on')
exclude(module: 'guava')
exclude(module: 'jsr305')
exclude(module: 'okhttp')
exclude(module: 'okio')
exclude(module: 'protobuf-java')
exclude(module: 'slf4j-api')
}
implementation("com.github.bisq-network:jsonrpc4j:$jsonrpc4jVersion") {
exclude(module: 'base64')
exclude(module: 'httpcore-nio')
}
implementation("com.google.inject:guice:$guiceVersion") {
exclude(module: 'guava')
}
implementation("org.apache.httpcomponents:httpclient:$httpclientVersion") {
exclude(module: 'commons-codec')
}
testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion"
testCompileOnly "org.projectlombok:lombok:$lombokVersion"
testImplementation "com.natpryce:make-it-easy:$easyVersion"
testImplementation "org.hamcrest:hamcrest:$hamcrestVersion"
testImplementation "org.junit.jupiter:junit-jupiter-api:$jupiterVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$jupiterVersion"
testImplementation "org.mockito:mockito-core:$mockitoVersion"
implementation "org.openjfx:javafx-base:$javafxVersion:$os"
implementation "org.openjfx:javafx-graphics:$javafxVersion:$os"
implementation("io.github.woodser:monero-java:$moneroJavaVersion") {
exclude(module: 'jackson-core')
exclude(module: 'jackson-annotations')
exclude(module: 'jackson-databind')
exclude(module: 'bcprov-jdk15on')
exclude(group: 'org.slf4j', module: 'slf4j-simple')
}
}
test {
systemProperty 'jdk.attach.allowAttachSelf', true
}
task generateKeypairs(type: JavaExec) {
mainClass = 'haveno.core.util.GenerateKeyPairs'
classpath = sourceSets.main.runtimeClasspath
}
task havenoDeps {
doLast {
// get monero binaries download url
Map moneroBinaries = [
'linux-x86_64' : 'https://github.com/haveno-dex/monero/releases/download/release9/monero-bins-haveno-linux-x86_64.tar.gz',
'linux-x86_64-sha256' : 'e1df7c2789472ece060619bcfe1a8a6e792799c4cd963433226e12b265ca0c9f',
'linux-aarch64' : 'https://github.com/haveno-dex/monero/releases/download/release9/monero-bins-haveno-linux-aarch64.tar.gz',
'linux-aarch64-sha256' : 'b197c46b08780c27ccd05d8123207af25211dd59edff0a63d73e93aa88d16892',
'mac' : 'https://github.com/haveno-dex/monero/releases/download/release9/monero-bins-haveno-mac.tar.gz',
'mac-sha256' : '1f39647d686a7a15b72ecff594069d90f047ed2f6df1817165ef1abc5419ec43',
'windows' : 'https://github.com/haveno-dex/monero/releases/download/release9/monero-bins-haveno-windows.zip',
'windows-sha256' : '10f7b38ebc1679ad9d25ac72a930141a082e6e502d7a5b06816ad770fe23f27c'
]
String osKey
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
osKey = 'windows'
} else if (Os.isFamily(Os.FAMILY_MAC)) {
osKey = 'mac'
} else {
String architecture = System.getProperty("os.arch").toLowerCase()
if (architecture.contains('aarch64') || architecture.contains('arm')) {
osKey = 'linux-aarch64'
} else {
osKey = 'linux-x86_64'
}
}
String moneroDownloadUrl = moneroBinaries[osKey]
String moneroSHA256Hash = moneroBinaries[osKey + '-sha256']
String moneroArchiveFileName = moneroDownloadUrl.tokenize('/').last()
String localnetDirName = '.localnet'
File localnetDir = new File(project.rootDir, localnetDirName)
localnetDir.mkdirs()
File moneroArchiveFile = new File(localnetDir, moneroArchiveFileName)
ext.downloadAndVerifyDependencies(moneroDownloadUrl, moneroSHA256Hash, moneroArchiveFile)
// extract if dependencies are missing or if archive was updated
File monerodFile
File moneroRpcFile
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
monerodFile = new File(localnetDir, 'monerod.exe')
moneroRpcFile = new File(localnetDir, 'monero-wallet-rpc.exe')
} else {
monerodFile = new File(localnetDir, 'monerod')
moneroRpcFile = new File(localnetDir, 'monero-wallet-rpc')
}
if (ext.dependencyDownloadedAndVerified || !monerodFile.exists() || !moneroRpcFile.exists()) {
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
ext.extractArchiveZip(moneroArchiveFile, localnetDir)
} else {
ext.extractArchiveTarGz(moneroArchiveFile, localnetDir)
}
}
// add the current platform's monero dependencies into the resources folder for installation
copy {
from "${monerodFile}"
into "${project(':core').projectDir}/src/main/resources/bin"
}
copy {
from "${moneroRpcFile}"
into "${project(':core').projectDir}/src/main/resources/bin"
}
}
ext.extractArchiveTarGz = { File tarGzFile, File destinationDir ->
println "Extracting tar.gz ${tarGzFile}"
// Gradle's tar extraction preserves permissions (crucial for jpackage to function correctly)
copy {
from tarTree(resources.gzip(tarGzFile))
into destinationDir
}
println "Extracted to ${destinationDir}"
}
ext.extractArchiveZip = { File zipFile, File destinationDir ->
println "Extracting zip ${zipFile}..."
ant.unzip(src: zipFile, dest: destinationDir)
println "Extracted to ${destinationDir}"
}
ext.downloadAndVerifyDependencies = { String archiveURL, String archiveSHA256, File destinationArchiveFile ->
ext.dependencyDownloadedAndVerified = false
// if archive exists, check to see if its already up to date
if (destinationArchiveFile.exists()) {
println "Verifying existing archive ${destinationArchiveFile}"
ant.archiveHash = archiveSHA256
ant.checksum(file: destinationArchiveFile, algorithm: 'SHA-256', property: '${archiveHash}', verifyProperty: 'existingHashMatches')
if (ant.properties['existingHashMatches'] != 'true') {
println "Existing archive does not match hash ${archiveSHA256}"
} else {
println "Existing archive matches hash"
return
}
}
// download archives
println "Downloading ${archiveURL}"
ant.get(src: archiveURL, dest: destinationArchiveFile)
println 'Download saved to ' + destinationArchiveFile
// verify checksum
println 'Verifying checksum for downloaded binary ...'
ant.archiveHash = archiveSHA256
ant.checksum(file: destinationArchiveFile, algorithm: 'SHA-256', property: '${archiveHash}', verifyProperty: 'downloadedHashMatches') // use a different verifyProperty name from existing verification or it will always fail
if (ant.properties['downloadedHashMatches'] != 'true') {
ant.fail('Checksum mismatch: Downloaded archive has a different checksum than expected')
}
println 'Checksum verified'
ext.dependencyDownloadedAndVerified = true
}
}
processResources.dependsOn havenoDeps // before both test and build
}
configure(project(':cli')) {
mainClassName = 'haveno.cli.CliMain'
dependencies {
implementation project(':proto')
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
compileOnly "org.projectlombok:lombok:$lombokVersion"
implementation "ch.qos.logback:logback-classic:$logbackVersion"
implementation "ch.qos.logback:logback-core:$logbackVersion"
implementation "com.google.guava:guava:$guavaVersion"
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
implementation "net.sf.jopt-simple:jopt-simple:$joptVersion"
implementation "org.slf4j:slf4j-api:$slf4jVersion"
implementation("io.grpc:grpc-core:$grpcVersion") {
exclude(module: 'animal-sniffer-annotations')
exclude(module: 'guava')
}
implementation("io.grpc:grpc-stub:$grpcVersion") {
exclude(module: 'animal-sniffer-annotations')
exclude(module: 'guava')
}
runtimeOnly("io.grpc:grpc-netty-shaded:$grpcVersion") {
exclude(module: 'animal-sniffer-annotations')
exclude(module: 'guava')
}
testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion"
testCompileOnly "org.projectlombok:lombok:$lombokVersion"
testImplementation "org.bitbucket.cowwoc:diff-match-patch:$cowwocVersion"
testImplementation "org.junit.jupiter:junit-jupiter-api:$jupiterVersion"
testImplementation "org.junit.jupiter:junit-jupiter-params:$jupiterVersion"
testRuntimeOnly "javax.annotation:javax.annotation-api:$javaxAnnotationVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$jupiterVersion"
}
}
configure(project(':desktop')) {
apply plugin: 'com.github.johnrengelman.shadow'
apply from: 'package/package.gradle'
version = '1.2.3-SNAPSHOT'
jar.manifest.attributes(
"Implementation-Title": project.name,
"Implementation-Version": version,
"CFBundleVersion": version)
mainClassName = 'haveno.desktop.app.HavenoAppMain'
tasks.withType(AbstractArchiveTask) {
preserveFileTimestamps = false
reproducibleFileOrder = true
}
sourceSets.main.resources.srcDirs += ['src/main/java'] // to copy fxml and css files
dependencies {
implementation project(':assets')
implementation project(':common')
implementation project(':proto')
implementation project(':p2p')
implementation project(':core')
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
compileOnly "org.projectlombok:lombok:$lombokVersion"
implementation "ch.qos.logback:logback-classic:$logbackVersion"
implementation "ch.qos.logback:logback-core:$logbackVersion"
implementation "com.google.code.gson:gson:$gsonVersion"
implementation "com.google.guava:guava:$guavaVersion"
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
implementation "com.googlecode.jcsv:jcsv:$jcsvVersion"
implementation "com.jfoenix:jfoenix:$jfoenixVersion"
implementation "commons-io:commons-io:$ioVersion"
implementation "de.jensd:fontawesomefx-commons:$fontawesomefxCommonsVersion"
implementation "de.jensd:fontawesomefx-materialdesignfont:$fontawesomefxMaterialdesignfontVersion"
implementation "de.jensd:fontawesomefx:$fontawesomefxVersion"
implementation "net.glxn:qrgen:$qrgenVersion"
implementation "org.apache.commons:commons-lang3:$langVersion"
implementation "org.bouncycastle:bcpg-jdk15on:$bcVersion"
implementation "org.fxmisc.easybind:easybind:$easybindVersion"
implementation "org.jetbrains:annotations:$jetbrainsAnnotationsVersion"
implementation "org.slf4j:slf4j-api:$slf4jVersion"
implementation("com.github.bisq-network:bitcoinj:$bitcoinjVersion") {
exclude(module: 'bcprov-jdk15on')
exclude(module: 'guava')
exclude(module: 'jsr305')
exclude(module: 'okhttp')
exclude(module: 'okio')
exclude(module: 'protobuf-java')
exclude(module: 'slf4j-api')
}
implementation("com.google.inject:guice:$guiceVersion") {
exclude(module: 'guava')
}
testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion"
testCompileOnly "org.projectlombok:lombok:$lombokVersion"
testImplementation "com.natpryce:make-it-easy:$easyVersion"
testImplementation "org.mockito:mockito-core:$mockitoVersion"
testImplementation "org.hamcrest:hamcrest:$hamcrestVersion"
testImplementation "org.junit.jupiter:junit-jupiter-api:$jupiterVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$jupiterVersion"
implementation("io.github.woodser:monero-java:$moneroJavaVersion") {
exclude(module: 'jackson-core')
exclude(module: 'jackson-annotations')
exclude(module: 'jackson-databind')
exclude(module: 'bcprov-jdk15on')
exclude(group: 'org.slf4j', module: 'slf4j-simple')
}
implementation "org.openjfx:javafx-controls:$javafxVersion:$os"
implementation "org.openjfx:javafx-fxml:$javafxVersion:$os"
implementation "org.openjfx:javafx-swing:$javafxVersion:$os"
implementation "org.openjfx:javafx-base:$javafxVersion:$os"
implementation "org.openjfx:javafx-graphics:$javafxVersion:$os"
// verification-only dependencies
javafxVerification "org.openjfx:javafx-controls:$javafxVersion:mac"
javafxVerification "org.openjfx:javafx-controls:$javafxVersion:mac-aarch64"
javafxVerification "org.openjfx:javafx-controls:$javafxVersion:win"
javafxVerification "org.openjfx:javafx-controls:$javafxVersion:linux"
javafxVerification "org.openjfx:javafx-fxml:$javafxVersion:mac"
javafxVerification "org.openjfx:javafx-fxml:$javafxVersion:mac-aarch64"
javafxVerification "org.openjfx:javafx-fxml:$javafxVersion:win"
javafxVerification "org.openjfx:javafx-fxml:$javafxVersion:linux"
javafxVerification "org.openjfx:javafx-swing:$javafxVersion:mac"
javafxVerification "org.openjfx:javafx-swing:$javafxVersion:mac-aarch64"
javafxVerification "org.openjfx:javafx-swing:$javafxVersion:win"
javafxVerification "org.openjfx:javafx-swing:$javafxVersion:linux"
javafxVerification "org.openjfx:javafx-base:$javafxVersion:mac"
javafxVerification "org.openjfx:javafx-base:$javafxVersion:mac-aarch64"
javafxVerification "org.openjfx:javafx-base:$javafxVersion:win"
javafxVerification "org.openjfx:javafx-base:$javafxVersion:linux"
javafxVerification "org.openjfx:javafx-graphics:$javafxVersion:mac"
javafxVerification "org.openjfx:javafx-graphics:$javafxVersion:mac-aarch64"
javafxVerification "org.openjfx:javafx-graphics:$javafxVersion:win"
javafxVerification "org.openjfx:javafx-graphics:$javafxVersion:linux"
}
test {
systemProperty 'jdk.attach.allowAttachSelf', true
}
}
configure(project(':monitor')) {
mainClassName = 'haveno.monitor.Monitor'
dependencies {
implementation project(':assets')
implementation project(':common')
implementation project(':core')
implementation project(':p2p')
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
compileOnly "org.projectlombok:lombok:$lombokVersion"
implementation "ch.qos.logback:logback-classic:$logbackVersion"
implementation "ch.qos.logback:logback-core:$logbackVersion"
implementation "com.google.guava:guava:$guavaVersion"
implementation "org.slf4j:slf4j-api:$slf4jVersion"
implementation("com.github.haveno-dex.netlayer:tor.external:$netlayerVersion") {
exclude(module: 'slf4j-api')
}
implementation("com.github.haveno-dex.netlayer:tor.native:$netlayerVersion") {
exclude(module: 'slf4j-api')
}
implementation("com.google.inject:guice:$guiceVersion") {
exclude(module: 'guava')
}
testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion"
testCompileOnly "org.projectlombok:lombok:$lombokVersion"
testImplementation "org.junit.jupiter:junit-jupiter-api:$jupiterVersion"
testImplementation "org.junit.jupiter:junit-jupiter-params:$jupiterVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$jupiterVersion"
implementation "org.openjfx:javafx-base:$javafxVersion:$os"
implementation "org.openjfx:javafx-graphics:$javafxVersion:$os"
}
}
configure(project(':relay')) {
mainClassName = 'haveno.relay.RelayMain'
dependencies {
implementation project(':common')
implementation "ch.qos.logback:logback-classic:$logbackVersion"
implementation "ch.qos.logback:logback-core:$logbackVersion"
implementation("com.google.firebase:firebase-admin:$firebaseVersion") {
exclude(module: 'commons-logging')
exclude(module: 'grpc-auth')
exclude(module: 'httpclient')
exclude(module: 'httpcore')
}
implementation "com.sparkjava:spark-core:$sparkVersion"
implementation "com.turo:pushy:$pushyVersion"
implementation "commons-codec:commons-codec:$codecVersion"
implementation "io.grpc:grpc-auth:$grpcVersion"
}
}
configure(project(':seednode')) {
apply plugin: 'com.github.johnrengelman.shadow'
mainClassName = 'haveno.seednode.SeedNodeMain'
dependencies {
implementation project(':common')
implementation project(':p2p')
implementation project(':core')
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
compileOnly "org.projectlombok:lombok:$lombokVersion"
implementation "com.google.guava:guava:$guavaVersion"
implementation "org.slf4j:slf4j-api:$slf4jVersion"
implementation("com.google.inject:guice:$guiceVersion") {
exclude(module: 'guava')
}
testImplementation "org.mockito:mockito-core:$mockitoVersion"
testImplementation "org.junit.jupiter:junit-jupiter-api:$jupiterVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$jupiterVersion"
}
}
configure(project(':statsnode')) {
mainClassName = 'haveno.statistics.StatisticsMain'
dependencies {
implementation project(':common')
implementation project(':p2p')
implementation project(':core')
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
compileOnly "org.projectlombok:lombok:$lombokVersion"
implementation "org.slf4j:slf4j-api:$slf4jVersion"
implementation("com.google.inject:guice:$guiceVersion") {
exclude(module: 'guava')
}
}
}
configure(project(':daemon')) {
mainClassName = 'haveno.daemon.app.HavenoDaemonMain'
apply plugin: 'com.github.johnrengelman.shadow'
dependencies {
implementation project(':proto')
implementation project(':common')
implementation project(':p2p')
implementation project(':core')
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
compileOnly "javax.annotation:javax.annotation-api:$javaxAnnotationVersion"
compileOnly "org.projectlombok:lombok:$lombokVersion"
implementation "ch.qos.logback:logback-classic:$logbackVersion"
implementation "ch.qos.logback:logback-core:$logbackVersion"
implementation "com.google.code.gson:gson:$gsonVersion"
implementation "com.google.guava:guava:$guavaVersion"
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
implementation "org.apache.commons:commons-lang3:$langVersion"
implementation "org.jetbrains:annotations:$jetbrainsAnnotationsVersion"
implementation "org.slf4j:slf4j-api:$slf4jVersion"
implementation("com.github.bisq-network:bitcoinj:$bitcoinjVersion") {
exclude(module: 'bcprov-jdk15on')
exclude(module: 'guava')
exclude(module: 'jsr305')
exclude(module: 'okhttp')
exclude(module: 'okio')
exclude(module: 'protobuf-java')
exclude(module: 'slf4j-api')
}
implementation("com.google.inject:guice:$guiceVersion") {
exclude(module: 'guava')
}
implementation("io.grpc:grpc-protobuf:$grpcVersion") {
exclude(module: 'animal-sniffer-annotations')
exclude(module: 'guava')
}
implementation("io.grpc:grpc-stub:$grpcVersion") {
exclude(module: 'animal-sniffer-annotations')
exclude(module: 'guava')
}
runtimeOnly("io.grpc:grpc-netty-shaded:$grpcVersion") {
exclude(module: 'animal-sniffer-annotations')
exclude(module: 'guava')
}
testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion"
testCompileOnly "org.projectlombok:lombok:$lombokVersion"
testImplementation "org.junit.jupiter:junit-jupiter-api:$jupiterVersion"
testImplementation "org.junit.jupiter:junit-jupiter-params:$jupiterVersion"
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$jupiterVersion")
implementation("io.github.woodser:monero-java:$moneroJavaVersion") {
exclude(module: 'jackson-core')
exclude(module: 'jackson-annotations')
exclude(module: 'jackson-databind')
exclude(module: 'bcprov-jdk15on')
exclude(group: 'org.slf4j', module: 'slf4j-simple')
}
implementation "org.openjfx:javafx-base:$javafxVersion:$os"
implementation "org.openjfx:javafx-graphics:$javafxVersion:$os"
}
}
configure(project(':inventory')) {
apply plugin: 'com.github.johnrengelman.shadow'
mainClassName = 'haveno.inventory.InventoryMonitorMain'
dependencies {
implementation project(':common')
implementation project(':p2p')
implementation project(':core')
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
compileOnly "org.projectlombok:lombok:$lombokVersion"
implementation "ch.qos.logback:logback-classic:$logbackVersion"
implementation "ch.qos.logback:logback-core:$logbackVersion"
implementation "com.google.guava:guava:$guavaVersion"
implementation "com.sparkjava:spark-core:$sparkVersion"
implementation "org.jetbrains:annotations:$jetbrainsAnnotationsVersion"
implementation("com.google.inject:guice:$guiceVersion") {
exclude(module: 'guava')
}
}
}
configure(project(':apitest')) {
mainClassName = 'haveno.apitest.ApiTestMain'
// We have to disable the :apitest 'test' task by default because we do not want
// to interfere with normal builds. To run JUnit tests in this subproject:
// Run a normal build and install dao-setup files first, then run:
// 'gradle :apitest:test -DrunApiTests=true'
test.enabled = System.getProperty("runApiTests") == "true"
test {
outputs.upToDateWhen { false } // Don't use previously cached test outputs.
testLogging {
showStackTraces = true // Show full stack traces in the console.
exceptionFormat = "full"
// Show passed & failed tests, and anything printed to stderr by the tests in the console.
// Do not show skipped tests in the console; they are shown in the html report.
events "passed", "failed", "standardError"
}
afterSuite { desc, result ->
if (!desc.parent) {
println("${result.resultType} " +
"[${result.testCount} tests, " +
"${result.successfulTestCount} passed, " +
"${result.failedTestCount} failed, " +
"${result.skippedTestCount} skipped] html report contains skipped test info")
// Show report link if all tests passed in case you want to see more detail, stdout, skipped, etc.
if (result.resultType == TestResult.ResultType.SUCCESS) {
DirectoryReport htmlReport = getReports().getHtml()
String reportUrl = new ConsoleRenderer()
.asClickableFileUrl(htmlReport.getEntryPoint())
println("REPORT " + reportUrl)
}
}
}
}
dependencies {
implementation project(':proto')
implementation project(':common')
implementation project(':core')
implementation project(':seednode')
implementation project(':desktop')
implementation project(':daemon')
implementation project(':cli')
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
compileOnly "javax.annotation:javax.annotation-api:$javaxAnnotationVersion"
compileOnly "org.projectlombok:lombok:$lombokVersion"
implementation "ch.qos.logback:logback-classic:$logbackVersion"
implementation "ch.qos.logback:logback-core:$logbackVersion"
implementation "com.google.code.gson:gson:$gsonVersion"
implementation "com.google.guava:guava:$guavaVersion"
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
implementation "net.sf.jopt-simple:jopt-simple:$joptVersion"
implementation "org.apache.commons:commons-lang3:$langVersion"
implementation "org.slf4j:slf4j-api:$slf4jVersion"
implementation("com.github.bisq-network:bitcoinj:$bitcoinjVersion") {
exclude(module: 'bcprov-jdk15on')
exclude(module: 'guava')
exclude(module: 'jsr305')
exclude(module: 'okhttp')
exclude(module: 'okio')
exclude(module: 'protobuf-java')
exclude(module: 'slf4j-api')
}
implementation("io.grpc:grpc-protobuf:$grpcVersion") {
exclude(module: 'animal-sniffer-annotations')
exclude(module: 'guava')
}
implementation("io.grpc:grpc-stub:$grpcVersion") {
exclude(module: 'animal-sniffer-annotations')
exclude(module: 'guava')
}
testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion"
testCompileOnly "org.projectlombok:lombok:$lombokVersion"
testImplementation "org.junit.jupiter:junit-jupiter-api:$jupiterVersion"
testImplementation "org.junit.jupiter:junit-jupiter-params:$jupiterVersion"
testRuntimeOnly "javax.annotation:javax.annotation-api:$javaxAnnotationVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$jupiterVersion"
}
}
================================================
FILE: cli/package/create-cli-dist.sh
================================================
#! /bin/bash
VERSION="$1"
if [[ -z "$VERSION" ]]; then
VERSION="SNAPSHOT"
fi
export HAVENO_RELEASE_NAME="haveno-cli-$VERSION"
export HAVENO_RELEASE_ZIP_NAME="$HAVENO_RELEASE_NAME.zip"
export GRADLE_DIST_NAME="cli.tar"
export GRADLE_DIST_PATH="../build/distributions/$GRADLE_DIST_NAME"
arrangegradledist() {
# Arrange $HAVENO_RELEASE_NAME directory structure to contain a runnable
# jar at the top-level, and a lib dir containing dependencies:
# .
# |
# |__ cli.jar
# |__ lib
# |__ |__ dep1.jar
# |__ |__ dep2.jar
# |__ |__ ...
# Copy the build's distribution tarball to this directory.
cp -v $GRADLE_DIST_PATH .
# Create a clean directory to hold the tarball's content.
rm -rf $HAVENO_RELEASE_NAME
mkdir $HAVENO_RELEASE_NAME
# Extract the tarball's content into $HAVENO_RELEASE_NAME.
tar -xf $GRADLE_DIST_NAME -C $HAVENO_RELEASE_NAME
cd $HAVENO_RELEASE_NAME
# Rearrange $HAVENO_RELEASE_NAME contents: move the lib directory up one level.
mv -v cli/lib .
# Rearrange $HAVENO_RELEASE_NAME contents: remove the cli/bin and cli directories.
rm -rf cli
# Rearrange $HAVENO_RELEASE_NAME contents: move the lib/cli.jar up one level.
mv -v lib/cli.jar .
}
writemanifest() {
# Make the cli.jar runnable, and define its dependencies in a MANIFEST.MF update.
echo "Main-Class: haveno.cli.CliMain" > manifest-update.txt
printf "Class-Path: " >> manifest-update.txt
for file in lib/*
do
# Each new line in the classpath must be preceded by two spaces.
printf " %s\n" "$file" >> manifest-update.txt
done
}
updatemanifest() {
# Append contents of to cli.jar's MANIFEST.MF.
jar uvfm cli.jar manifest-update.txt
}
ziprelease() {
cd ..
zip -r $HAVENO_RELEASE_ZIP_NAME $HAVENO_RELEASE_NAME/lib $HAVENO_RELEASE_NAME/cli.jar
}
cleanup() {
rm -v ./$GRADLE_DIST_NAME
rm -r ./$HAVENO_RELEASE_NAME
}
arrangegradledist
writemanifest
updatemanifest
ziprelease
cleanup
================================================
FILE: cli/src/main/java/haveno/cli/CliMain.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.cli;
import haveno.cli.opts.ArgumentList;
import haveno.cli.opts.CancelOfferOptionParser;
import haveno.cli.opts.CreateCryptoCurrencyPaymentAcctOptionParser;
import haveno.cli.opts.CreateOfferOptionParser;
import haveno.cli.opts.CreatePaymentAcctOptionParser;
import haveno.cli.opts.GetAddressBalanceOptionParser;
import haveno.cli.opts.GetBTCMarketPriceOptionParser;
import haveno.cli.opts.GetBalanceOptionParser;
import haveno.cli.opts.GetOffersOptionParser;
import haveno.cli.opts.GetPaymentAcctFormOptionParser;
import haveno.cli.opts.GetTradeOptionParser;
import haveno.cli.opts.GetTradesOptionParser;
import haveno.cli.opts.OfferIdOptionParser;
import haveno.cli.opts.RegisterDisputeAgentOptionParser;
import haveno.cli.opts.RemoveWalletPasswordOptionParser;
import haveno.cli.opts.SendBtcOptionParser;
import haveno.cli.opts.SetWalletPasswordOptionParser;
import haveno.cli.opts.SimpleMethodOptionParser;
import haveno.cli.opts.TakeOfferOptionParser;
import haveno.cli.opts.UnlockWalletOptionParser;
import haveno.cli.opts.WithdrawFundsOptionParser;
import haveno.cli.table.builder.TableBuilder;
import haveno.proto.grpc.OfferInfo;
import io.grpc.StatusRuntimeException;
import joptsimple.OptionParser;
import joptsimple.OptionSet;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Date;
import java.util.List;
import static haveno.cli.CurrencyFormat.formatInternalFiatPrice;
import static haveno.cli.CurrencyFormat.toSatoshis;
import static haveno.cli.Method.canceloffer;
import static haveno.cli.Method.closetrade;
import static haveno.cli.Method.confirmpaymentreceived;
import static haveno.cli.Method.confirmpaymentsent;
import static haveno.cli.Method.createcryptopaymentacct;
import static haveno.cli.Method.createoffer;
import static haveno.cli.Method.createpaymentacct;
import static haveno.cli.Method.editoffer;
import static haveno.cli.Method.failtrade;
import static haveno.cli.Method.getaddressbalance;
import static haveno.cli.Method.getbalance;
import static haveno.cli.Method.getbtcprice;
import static haveno.cli.Method.getfundingaddresses;
import static haveno.cli.Method.getmyoffer;
import static haveno.cli.Method.getmyoffers;
import static haveno.cli.Method.getoffer;
import static haveno.cli.Method.getoffers;
import static haveno.cli.Method.getpaymentacctform;
import static haveno.cli.Method.getpaymentaccts;
import static haveno.cli.Method.getpaymentmethods;
import static haveno.cli.Method.gettrade;
import static haveno.cli.Method.gettrades;
import static haveno.cli.Method.gettransaction;
import static haveno.cli.Method.gettxfeerate;
import static haveno.cli.Method.getunusedbsqaddress;
import static haveno.cli.Method.getversion;
import static haveno.cli.Method.lockwallet;
import static haveno.cli.Method.sendbtc;
import static haveno.cli.Method.settxfeerate;
import static haveno.cli.Method.setwalletpassword;
import static haveno.cli.Method.stop;
import static haveno.cli.Method.takeoffer;
import static haveno.cli.Method.unfailtrade;
import static haveno.cli.Method.unlockwallet;
import static haveno.cli.Method.unsettxfeerate;
import static haveno.cli.Method.withdrawfunds;
import static haveno.cli.opts.OptLabel.OPT_AMOUNT;
import static haveno.cli.opts.OptLabel.OPT_HELP;
import static haveno.cli.opts.OptLabel.OPT_HOST;
import static haveno.cli.opts.OptLabel.OPT_PASSWORD;
import static haveno.cli.opts.OptLabel.OPT_PORT;
import static haveno.cli.opts.OptLabel.OPT_TX_FEE_RATE;
import static haveno.cli.table.builder.TableType.ADDRESS_BALANCE_TBL;
import static haveno.cli.table.builder.TableType.BTC_BALANCE_TBL;
import static haveno.cli.table.builder.TableType.CLOSED_TRADES_TBL;
import static haveno.cli.table.builder.TableType.FAILED_TRADES_TBL;
import static haveno.cli.table.builder.TableType.OFFER_TBL;
import static haveno.cli.table.builder.TableType.OPEN_TRADES_TBL;
import static haveno.cli.table.builder.TableType.PAYMENT_ACCOUNT_TBL;
import static haveno.cli.table.builder.TableType.TRADE_DETAIL_TBL;
import static haveno.proto.grpc.GetTradesRequest.Category.CLOSED;
import static haveno.proto.grpc.GetTradesRequest.Category.OPEN;
import static java.lang.String.format;
import static java.lang.System.err;
import static java.lang.System.exit;
import static java.lang.System.out;
/**
* A command-line client for the Haveno 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) 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 "BTC":
new TableBuilder(BTC_BALANCE_TBL, balances.getBtc()).build().print(out);
break;
case "":
default: {
out.println("BTC");
new TableBuilder(BTC_BALANCE_TBL, balances.getBtc()).build().print(out);
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);
new TableBuilder(ADDRESS_BALANCE_TBL, addressBalance).build().print(out);
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(formatInternalFiatPrice(price));
return;
}
case getfundingaddresses: {
if (new SimpleMethodOptionParser(args).parse().isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var fundingAddresses = client.getFundingAddresses();
new TableBuilder(ADDRESS_BALANCE_TBL, fundingAddresses).build().print(out);
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();
throw new RuntimeException("Send BTC not implemented");
}
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 marketPriceMarginPct = opts.getMktPriceMarginPct();
var securityDepositPct = opts.getSecurityDepositPct();
var triggerPrice = "0"; // Cannot be defined until the new offer is added to book.
OfferInfo offer;
offer = client.createOffer(direction,
currencyCode,
amount,
minAmount,
useMarketBasedPrice,
fixedPrice,
marketPriceMarginPct,
securityDepositPct,
paymentAcctId,
triggerPrice);
new TableBuilder(OFFER_TBL, offer).build().print(out);
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 OfferIdOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var offerId = opts.getOfferId();
var offer = client.getOffer(offerId);
new TableBuilder(OFFER_TBL, offer).build().print(out);
return;
}
case getmyoffer: {
var opts = new OfferIdOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var offerId = opts.getOfferId();
var offer = client.getMyOffer(offerId);
new TableBuilder(OFFER_TBL, offer).build().print(out);
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 offers = client.getOffers(direction, currencyCode);
if (offers.isEmpty())
out.printf("no %s %s offers found%n", direction, currencyCode);
else
new TableBuilder(OFFER_TBL, offers).build().print(out);
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 offers = client.getMyOffers(direction, currencyCode);
if (offers.isEmpty())
out.printf("no %s %s offers found%n", direction, currencyCode);
else
new TableBuilder(OFFER_TBL, offers).build().print(out);
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 trade = client.takeOffer(offerId, paymentAccountId);
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
new TableBuilder(TRADE_DETAIL_TBL, trade).build().print(out);
return;
}
case gettrades: {
var opts = new GetTradesOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var category = opts.getCategory();
var trades = category.equals(OPEN)
? client.getOpenTrades()
: client.getTradeHistory(category);
if (trades.isEmpty()) {
out.printf("no %s trades found%n", category.name().toLowerCase());
} else {
var tableType = category.equals(OPEN)
? OPEN_TRADES_TBL
: category.equals(CLOSED) ? CLOSED_TRADES_TBL : FAILED_TRADES_TBL;
new TableBuilder(tableType, trades).build().print(out);
}
return;
}
case confirmpaymentsent: {
var opts = new GetTradeOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var tradeId = opts.getTradeId();
client.confirmPaymentSent(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 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");
new TableBuilder(PAYMENT_ACCOUNT_TBL, paymentAccount).build().print(out);
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");
new TableBuilder(PAYMENT_ACCOUNT_TBL, paymentAccount).build().print(out);
return;
}
case getpaymentaccts: {
if (new SimpleMethodOptionParser(args).parse().isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var paymentAccounts = client.getPaymentAccounts();
if (paymentAccounts.size() > 0)
new TableBuilder(PAYMENT_ACCOUNT_TBL, paymentAccounts).build().print(out);
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., INVALID_ARGUMENT,
// NOT_FOUND, ..., UNKNOWN from the exception 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("Haveno RPC Client");
stream.println();
stream.println("Usage: haveno-cli [options] [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=]", "Get server wallet balances");
stream.println();
stream.format(rowFormat, getaddressbalance.name(), "--address=", "Get server wallet address balance");
stream.println();
stream.format(rowFormat, getbtcprice.name(), "--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, "", "[--tx-fee-rate=]", "");
stream.println();
stream.format(rowFormat, sendbtc.name(), "--address= --amount= \\", "Send BTC");
stream.format(rowFormat, "", "[--tx-fee-rate=]", "");
stream.format(rowFormat, "", "[--memo=<\"memo\">]", "");
stream.println();
stream.format(rowFormat, gettxfeerate.name(), "", "Get current tx fee rate in sats/byte");
stream.println();
stream.format(rowFormat, settxfeerate.name(), "--tx-fee-rate=", "Set custom tx fee rate in sats/byte");
stream.println();
stream.format(rowFormat, unsettxfeerate.name(), "", "Unset custom tx fee rate");
stream.println();
stream.format(rowFormat, gettransaction.name(), "--transaction-id=", "Get transaction with id");
stream.println();
stream.format(rowFormat, createoffer.name(), "--payment-account= \\", "Create and place an offer");
stream.format(rowFormat, "", "--direction= \\", "");
stream.format(rowFormat, "", "--currency-code= \\", "");
stream.format(rowFormat, "", "--amount= \\", "");
stream.format(rowFormat, "", "[--min-amount=] \\", "");
stream.format(rowFormat, "", "--fixed-price= | --market-price-margin= \\", "");
stream.format(rowFormat, "", "--security-deposit= \\", "");
stream.format(rowFormat, "", "[--fee-currency=]", "");
stream.format(rowFormat, "", "[--trigger-price=]", "");
stream.format(rowFormat, "", "[--swap=]", "");
stream.println();
stream.format(rowFormat, editoffer.name(), "--offer-id= \\", "Edit offer with id");
stream.format(rowFormat, "", "[--fixed-price=] \\", "");
stream.format(rowFormat, "", "[--market-price-margin=] \\", "");
stream.format(rowFormat, "", "[--trigger-price=] \\", "");
stream.format(rowFormat, "", "[--enabled=]", "");
stream.println();
stream.format(rowFormat, canceloffer.name(), "--offer-id=", "Cancel offer with id");
stream.println();
stream.format(rowFormat, getoffer.name(), "--offer-id=", "Get current offer with id");
stream.println();
stream.format(rowFormat, getmyoffer.name(), "--offer-id=", "Get my current offer with id");
stream.println();
stream.format(rowFormat, getoffers.name(), "--direction= \\", "Get current offers");
stream.format(rowFormat, "", "--currency-code=", "");
stream.println();
stream.format(rowFormat, getmyoffers.name(), "--direction= \\", "Get my current offers");
stream.format(rowFormat, "", "--currency-code=", "");
stream.println();
stream.format(rowFormat, takeoffer.name(), "--offer-id= \\", "Take offer with id");
stream.format(rowFormat, "", "[--payment-account=]", "");
stream.format(rowFormat, "", "[--fee-currency=]", "");
stream.println();
stream.format(rowFormat, gettrade.name(), "--trade-id= \\", "Get trade summary or full contract");
stream.format(rowFormat, "", "[--show-contract=]", "");
stream.println();
stream.format(rowFormat, gettrades.name(), "[--category=]", "Get open (default), closed, or failed trades");
stream.println();
stream.format(rowFormat, confirmpaymentsent.name(), "--trade-id=", "Confirm payment started");
stream.println();
stream.format(rowFormat, confirmpaymentreceived.name(), "--trade-id=", "Confirm payment received");
stream.println();
stream.format(rowFormat, closetrade.name(), "--trade-id=", "Close completed trade");
stream.println();
stream.format(rowFormat, withdrawfunds.name(), "--trade-id= --address= \\",
"Withdraw received trade funds to external wallet address");
stream.format(rowFormat, "", "[--memo=<\"memo\">]", "");
stream.println();
stream.format(rowFormat, failtrade.name(), "--trade-id=", "Change open trade to failed trade");
stream.println();
stream.format(rowFormat, unfailtrade.name(), "--trade-id=", "Change failed trade to open trade");
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=", "Get a new payment account form");
stream.println();
stream.format(rowFormat, createpaymentacct.name(), "--payment-account-form=", "Create a new payment account");
stream.println();
stream.format(rowFormat, createcryptopaymentacct.name(), "--account-name= \\", "Create a new cryptocurrency payment account");
stream.format(rowFormat, "", "--currency-code= \\", "");
stream.format(rowFormat, "", "--address=", "");
stream.format(rowFormat, "", "--trade-instant=", "");
stream.println();
stream.format(rowFormat, getpaymentaccts.name(), "", "Get user payment accounts");
stream.println();
stream.format(rowFormat, lockwallet.name(), "", "Remove wallet password from memory, locking the wallet");
stream.println();
stream.format(rowFormat, unlockwallet.name(), "--wallet-password= --timeout=",
"Store wallet password in memory for timeout seconds");
stream.println();
stream.format(rowFormat, setwalletpassword.name(), "--wallet-password= \\",
"Encrypt wallet with password, or set new password on encrypted wallet");
stream.format(rowFormat, "", "[--new-wallet-password=]", "");
stream.println();
stream.format(rowFormat, stop.name(), "", "Shut down the server");
stream.println();
stream.println("Method Help Usage: haveno-cli [options] --help");
stream.println();
} catch (IOException ex) {
ex.printStackTrace(stream);
}
}
}
================================================
FILE: cli/src/main/java/haveno/cli/ColumnHeaderConstants.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.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_BALANCE = "Balance";
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_RESERVED_OFFER_BALANCE = "Reserved Offer Balance";
static final String COL_HEADER_RESERVED_TRADE_BALANCE = "Reserved Trade 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_CRYPTO = "Price in BTC for 1 %-3s";
static final String COL_HEADER_TRADE_AMOUNT = padStart("Amount(%-3s)", 12, ' ');
static final String COL_HEADER_TRADE_BUYER_COST = padEnd("Buyer Cost(%-3s)", 15, ' ');
static final String COL_HEADER_TRADE_DEPOSIT_CONFIRMED = "Deposit Confirmed";
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, ' ');
}
================================================
FILE: cli/src/main/java/haveno/cli/CryptoCurrencyUtil.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.cli;
import java.util.ArrayList;
import java.util.List;
public class CryptoCurrencyUtil {
public static boolean apiDoesSupportCryptoCurrency(String currencyCode) {
return getSupportedCryptoCurrencies().contains(currencyCode.toUpperCase());
}
public static List getSupportedCryptoCurrencies() {
final List result = new ArrayList<>();
result.add("BCH");
result.sort(String::compareTo);
return result;
}
}
================================================
FILE: cli/src/main/java/haveno/cli/CurrencyFormat.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.cli;
import com.google.common.annotations.VisibleForTesting;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.util.Locale;
import static java.lang.String.format;
import static java.math.RoundingMode.HALF_UP;
import static java.math.RoundingMode.UNNECESSARY;
/**
* Utility for formatting amounts, volumes and fees; there is no i18n support in the CLI.
*/
@VisibleForTesting
public class CurrencyFormat {
// Use the US locale as a base for all DecimalFormats, but commas should be omitted from number strings.
private static final DecimalFormatSymbols DECIMAL_FORMAT_SYMBOLS = DecimalFormatSymbols.getInstance(Locale.US);
// Use the US locale as a base for all NumberFormats, but commas should be omitted from number strings.
private static final NumberFormat US_LOCALE_NUMBER_FORMAT = NumberFormat.getInstance(Locale.US);
// Formats numbers for internal use, i.e., grpc request parameters.
private static final DecimalFormat INTERNAL_FIAT_DECIMAL_FORMAT = new DecimalFormat("##############0.0000");
static final BigDecimal SATOSHI_DIVISOR = new BigDecimal(100_000_000);
static final DecimalFormat SATOSHI_FORMAT = new DecimalFormat("###,##0.00000000", DECIMAL_FORMAT_SYMBOLS);
static final DecimalFormat BTC_FORMAT = new DecimalFormat("###,##0.########", DECIMAL_FORMAT_SYMBOLS);
static final DecimalFormat BTC_TX_FEE_FORMAT = new DecimalFormat("###,###,##0", DECIMAL_FORMAT_SYMBOLS);
static final BigDecimal BSQ_SATOSHI_DIVISOR = new BigDecimal(100);
static final DecimalFormat BSQ_FORMAT = new DecimalFormat("###,###,###,##0.00", DECIMAL_FORMAT_SYMBOLS);
public static String formatSatoshis(String sats) {
//noinspection BigDecimalMethodWithoutRoundingCalled
return SATOSHI_FORMAT.format(new BigDecimal(sats).divide(SATOSHI_DIVISOR));
}
@SuppressWarnings("BigDecimalMethodWithoutRoundingCalled")
public static String formatSatoshis(long sats) {
return SATOSHI_FORMAT.format(new BigDecimal(sats).divide(SATOSHI_DIVISOR));
}
@SuppressWarnings("BigDecimalMethodWithoutRoundingCalled")
public static String formatBtc(long sats) {
return BTC_FORMAT.format(new BigDecimal(sats).divide(SATOSHI_DIVISOR));
}
@SuppressWarnings("BigDecimalMethodWithoutRoundingCalled")
public static String formatBsq(long sats) {
return BSQ_FORMAT.format(new BigDecimal(sats).divide(BSQ_SATOSHI_DIVISOR));
}
public static String formatInternalFiatPrice(BigDecimal price) {
INTERNAL_FIAT_DECIMAL_FORMAT.setMinimumFractionDigits(4);
INTERNAL_FIAT_DECIMAL_FORMAT.setMaximumFractionDigits(4);
return INTERNAL_FIAT_DECIMAL_FORMAT.format(price);
}
public static String formatInternalFiatPrice(double price) {
US_LOCALE_NUMBER_FORMAT.setMinimumFractionDigits(4);
US_LOCALE_NUMBER_FORMAT.setMaximumFractionDigits(4);
return US_LOCALE_NUMBER_FORMAT.format(price);
}
public static String formatPrice(long price) {
US_LOCALE_NUMBER_FORMAT.setMinimumFractionDigits(4);
US_LOCALE_NUMBER_FORMAT.setMaximumFractionDigits(4);
US_LOCALE_NUMBER_FORMAT.setRoundingMode(UNNECESSARY);
return US_LOCALE_NUMBER_FORMAT.format((double) price / 10_000);
}
public static String formatFiatVolume(long volume) {
US_LOCALE_NUMBER_FORMAT.setMinimumFractionDigits(0);
US_LOCALE_NUMBER_FORMAT.setMaximumFractionDigits(0);
US_LOCALE_NUMBER_FORMAT.setRoundingMode(HALF_UP);
return US_LOCALE_NUMBER_FORMAT.format((double) volume / 10_000);
}
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 String formatFeeSatoshis(long sats) {
return BTC_TX_FEE_FORMAT.format(BigDecimal.valueOf(sats));
}
}
================================================
FILE: cli/src/main/java/haveno/cli/DirectionFormat.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.cli;
import haveno.proto.grpc.OfferInfo;
import java.util.List;
import java.util.function.Function;
import static haveno.cli.ColumnHeaderConstants.COL_HEADER_DIRECTION;
import static java.lang.String.format;
import static protobuf.OfferDirection.BUY;
import static protobuf.OfferDirection.SELL;
class DirectionFormat {
static int getLongestDirectionColWidth(List offers) {
if (offers.isEmpty() || offers.get(0).getBaseCurrencyCode().equals("XMR"))
return COL_HEADER_DIRECTION.length();
else
return 18; // .e.g., "Sell BSQ (Buy XMR)".length()
}
static final Function directionFormat = (offer) -> {
String baseCurrencyCode = offer.getBaseCurrencyCode();
boolean isCryptoCurrencyOffer = !baseCurrencyCode.equals("XMR");
if (!isCryptoCurrencyOffer) {
return baseCurrencyCode;
} else {
// Return "Sell BSQ (Buy XMR)", or "Buy BSQ (Sell XMR)".
String direction = offer.getDirection();
String mirroredDirection = getMirroredDirection(direction);
Function mixedCase = (word) -> word.charAt(0) + word.substring(1).toLowerCase();
return format("%s %s (%s %s)",
mixedCase.apply(mirroredDirection),
baseCurrencyCode,
mixedCase.apply(direction),
offer.getCounterCurrencyCode());
}
};
static String getMirroredDirection(String directionAsString) {
return directionAsString.equalsIgnoreCase(BUY.name()) ? SELL.name() : BUY.name();
}
}
================================================
FILE: cli/src/main/java/haveno/cli/GrpcClient.java
================================================
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see .
*/
package haveno.cli;
import haveno.cli.request.OffersServiceRequest;
import haveno.cli.request.PaymentAccountsServiceRequest;
import haveno.cli.request.TradesServiceRequest;
import haveno.cli.request.WalletsServiceRequest;
import haveno.proto.grpc.AddressBalanceInfo;
import haveno.proto.grpc.BalancesInfo;
import haveno.proto.grpc.BtcBalanceInfo;
import haveno.proto.grpc.GetMethodHelpRequest;
import haveno.proto.grpc.GetTradesRequest;
import haveno.proto.grpc.GetVersionRequest;
import haveno.proto.grpc.OfferInfo;
import haveno.proto.grpc.RegisterDisputeAgentRequest;
import haveno.proto.grpc.StopRequest;
import haveno.proto.grpc.TradeInfo;
import lombok.extern.slf4j.Slf4j;
import protobuf.PaymentAccount;
import protobuf.PaymentMethod;
import java.util.List;
@SuppressWarnings("ResultOfMethodCallIgnored")
@Slf4j
public final class GrpcClient {
private final GrpcStubs grpcStubs;
private final OffersServiceRequest offersServiceRequest;
private final TradesServiceRequest tradesServiceRequest;
private final WalletsServiceRequest walletsServiceRequest;
private final PaymentAccountsServiceRequest paymentAccountsServiceRequest;
public GrpcClient(String apiHost,
int apiPort,
String apiPassword) {
this.grpcStubs = new GrpcStubs(apiHost, apiPort, apiPassword);
this.offersServiceRequest = new OffersServiceRequest(grpcStubs);
this.tradesServiceRequest = new TradesServiceRequest(grpcStubs);
this.walletsServiceRequest = new WalletsServiceRequest(grpcStubs);
this.paymentAccountsServiceRequest = new PaymentAccountsServiceRequest(grpcStubs);
}
public String getVersion() {
var request = GetVersionRequest.newBuilder().build();
return grpcStubs.versionService.getVersion(request).getVersion();
}
public BalancesInfo getBalances() {
return walletsServiceRequest.getBalances();
}
public BtcBalanceInfo getBtcBalances() {
return walletsServiceRequest.getBtcBalances();
}
public BalancesInfo getBalances(String currencyCode) {
return walletsServiceRequest.getBalances(currencyCode);
}
public AddressBalanceInfo getAddressBalance(String address) {
return walletsServiceRequest.getAddressBalance(address);
}
public double getBtcPrice(String currencyCode) {
return walletsServiceRequest.getBtcPrice(currencyCode);
}
public List getFundingAddresses() {
return walletsServiceRequest.getFundingAddresses();
}
public String getUnusedBtcAddress() {
return walletsServiceRequest.getUnusedBtcAddress();
}
public OfferInfo createFixedPricedOffer(String direction,
String currencyCode,
long amount,
long minAmount,
String fixedPrice,
double securityDepositPct,
String paymentAcctId) {
return offersServiceRequest.createOffer(direction,
currencyCode,
amount,
minAmount,
false,
fixedPrice,
0.00,
securityDepositPct,
paymentAcctId,
"0" /* no trigger price */);
}
public OfferInfo createMarketBasedPricedOffer(String direction,
String currencyCode,
long amount,
long minAmount,
double marketPriceMarginPct,
double securityDepositPct,
String paymentAcctId,
String triggerPrice) {
return offersServiceRequest.createOffer(direction,
currencyCode,
amount,
minAmount,
true,
"0",
marketPriceMarginPct,
securityDepositPct,
paymentAcctId,
triggerPrice);
}
public OfferInfo createOffer(String direction,
String currencyCode,
long amount,
long minAmount,
boolean useMarketBasedPrice,
String fixedPrice,
double marketPriceMarginPct,
double securityDepositPct,
String paymentAcctId,
String triggerPrice) {
return offersServiceRequest.createOffer(direction,
currencyCode,
amount,
minAmount,
useMarketBasedPrice,
fixedPrice,
marketPriceMarginPct,
securityDepositPct,
paymentAcctId,
triggerPrice);
}
public void cancelOffer(String offerId) {
offersServiceRequest.cancelOffer(offerId);
}
public OfferInfo getOffer(String offerId) {
return offersServiceRequest.getOffer(offerId);
}
@Deprecated // Since 5-Dec-2021.
// Endpoint to be removed from future version. Use getOffer service method instead.
public OfferInfo getMyOffer(String offerId) {
return offersServiceRequest.getMyOffer(offerId);
}
public List getOffers(String direction, String currencyCode) {
return offersServiceRequest.getOffers(direction, currencyCode);
}
public List getOffersSortedByDate(String currencyCode) {
return offersServiceRequest.getOffersSortedByDate(currencyCode);
}
public List getOffersSortedByDate(String direction, String currencyCode) {
return offersServiceRequest.getOffersSortedByDate(direction, currencyCode);
}
public List getMyOffers(String direction, String currencyCode) {
return offersServiceRequest.getMyOffers(direction, currencyCode);
}
public List getMyOffersSortedByDate(String currencyCode) {
return offersServiceRequest.getMyOffersSortedByDate(currencyCode);
}
public List getMyOffersSortedByDate(String direction, String currencyCode) {
return offersServiceRequest.getMyOffersSortedByDate(direction, currencyCode);
}
public TradeInfo takeOffer(String offerId, String paymentAccountId) {
return tradesServiceRequest.takeOffer(offerId, paymentAccountId);
}
public TradeInfo getTrade(String tradeId) {
return tradesServiceRequest.getTrade(tradeId);
}
public List