Merge pull request #6343 from ghubstan/remove-dead-pkg

Remove dead API test harness code
This commit is contained in:
Christoph Atteneder 2022-09-09 09:28:56 +02:00 committed by GitHub
commit f69815d98e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 0 additions and 2392 deletions

View File

@ -1,121 +0,0 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.scenario;
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 bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind;
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
import static bisq.apitest.config.BisqAppConfig.arbdaemon;
import static bisq.apitest.config.BisqAppConfig.bobdaemon;
import static bisq.apitest.config.BisqAppConfig.seednode;
import static bisq.apitest.scenario.bot.shutdown.ManualShutdown.startShutdownTimer;
import static org.junit.jupiter.api.Assertions.fail;
import bisq.apitest.config.ApiTestConfig;
import bisq.apitest.method.BitcoinCliHelper;
import bisq.apitest.scenario.bot.AbstractBotTest;
import bisq.apitest.scenario.bot.BotClient;
import bisq.apitest.scenario.bot.RobotBob;
import bisq.apitest.scenario.bot.script.BashScriptGenerator;
import bisq.apitest.scenario.bot.shutdown.ManualBotShutdownException;
// 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 <pid>
// 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();
}
}

View File

@ -1,110 +0,0 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.scenario.bot;
import bisq.core.locale.Country;
import protobuf.PaymentAccount;
import com.google.gson.GsonBuilder;
import java.nio.file.Paths;
import java.io.File;
import java.io.IOException;
import lombok.extern.slf4j.Slf4j;
import static bisq.core.locale.CountryUtil.findCountryByCode;
import static bisq.core.payment.payload.PaymentMethod.CLEAR_X_CHANGE_ID;
import static bisq.core.payment.payload.PaymentMethod.getPaymentMethod;
import static java.lang.String.format;
import static java.lang.System.getProperty;
import static java.nio.file.Files.readAllBytes;
import bisq.apitest.method.MethodTest;
import bisq.apitest.scenario.bot.script.BashScriptGenerator;
import bisq.apitest.scenario.bot.script.BotScript;
@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(CLEAR_X_CHANGE_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;
}
}
}

View File

@ -1,77 +0,0 @@
package bisq.apitest.scenario.bot;
import bisq.core.locale.Country;
import protobuf.PaymentAccount;
import lombok.extern.slf4j.Slf4j;
import static bisq.core.locale.CountryUtil.findCountryByCode;
import static bisq.core.payment.payload.PaymentMethod.CLEAR_X_CHANGE_ID;
import static bisq.core.payment.payload.PaymentMethod.getPaymentMethod;
import static java.lang.String.format;
import static java.util.concurrent.TimeUnit.MINUTES;
import bisq.apitest.method.BitcoinCliHelper;
import bisq.apitest.scenario.bot.script.BashScriptGenerator;
import bisq.apitest.scenario.bot.script.BotScript;
@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(CLEAR_X_CHANGE_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."));
}
}

View File

@ -1,337 +0,0 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.scenario.bot;
import bisq.proto.grpc.BalancesInfo;
import bisq.proto.grpc.GetPaymentAccountsRequest;
import bisq.proto.grpc.OfferInfo;
import bisq.proto.grpc.TradeInfo;
import protobuf.PaymentAccount;
import java.text.DecimalFormat;
import java.util.List;
import java.util.function.BiPredicate;
import lombok.extern.slf4j.Slf4j;
import static org.apache.commons.lang3.StringUtils.capitalize;
import bisq.cli.GrpcClient;
/**
* Convenience GrpcClient wrapper for bots using gRPC services.
*/
@SuppressWarnings({"JavaDoc", "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 BSQ and BTC 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<OfferInfo>
*/
public List<OfferInfo> 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<OfferInfo>
*/
public List<OfferInfo> getBuyOffers(String currencyCode) {
return grpcClient.getOffers("BUY", currencyCode);
}
/**
* Return SELL offers for the given currencyCode.
* @param currencyCode
* @return List<OfferInfo>
*/
public List<OfferInfo> 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 feeCurrency,
String triggerPrice) {
return grpcClient.createMarketBasedPricedOffer(direction,
currencyCode,
amountInSatoshis,
minAmountInSatoshis,
priceMarginAsPercent,
securityDepositAsPercent,
paymentAccount.getId(),
feeCurrency,
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,
String feeCurrency) {
return grpcClient.createFixedPricedOffer(direction,
currencyCode,
amountInSatoshis,
minAmountInSatoshis,
fixedOfferPriceAsString,
securityDepositAsPercent,
paymentAccount.getId(),
feeCurrency);
}
public TradeInfo takeOffer(String offerId, PaymentAccount paymentAccount, String feeCurrency) {
return grpcClient.takeOffer(offerId, paymentAccount.getId(), feeCurrency, 0L);
}
/**
* 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<Exception, String> 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).getIsDepositConfirmed();
}
/**
* 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 isTradePaymentStartedSent(String tradeId) {
return grpcClient.getTrade(tradeId).getIsPaymentStartedMessageSent();
}
/**
* 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).getIsPaymentReceivedMessageSent();
}
/**
* 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 sendConfirmPaymentStartedMessage(String tradeId) {
grpcClient.confirmPaymentStarted(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);
}
/**
* Sends a 'closetrade' for a trade with the given tradeId,
* or throws an exception.
* @param tradeId
*/
public void sendCloseTradeMessage(String tradeId) {
grpcClient.closeTrade(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_]+: ", ""));
}
}

View File

@ -1,68 +0,0 @@
package bisq.apitest.scenario.bot;
import bisq.core.api.model.PaymentAccountForm;
import bisq.core.locale.Country;
import protobuf.PaymentAccount;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.File;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import static bisq.core.payment.payload.PaymentMethod.CLEAR_X_CHANGE_ID;
import static bisq.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<String, Object> 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<String, Object> p = getPaymentAccountFormMap(CLEAR_X_CHANGE_ID);
p.put("accountName", accountName);
p.put("emailOrMobileNr", holderName + "@zelle.com");
p.put("holderName", holderName);
return botClient.createNewPaymentAccount(gson.toJson(p));
}
private Map<String, Object> getPaymentAccountFormMap(String paymentMethodId) {
PaymentAccountForm paymentAccountForm = new PaymentAccountForm();
File jsonFormTemplate = paymentAccountForm.getPaymentAccountForm(paymentMethodId);
jsonFormTemplate.deleteOnExit();
String jsonString = paymentAccountForm.toJsonString(jsonFormTemplate);
//noinspection unchecked
return (Map<String, Object>) gson.fromJson(jsonString, Object.class);
}
}

View File

@ -1,35 +0,0 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.scenario.bot;
import bisq.common.BisqException;
@SuppressWarnings("unused")
public class InvalidRandomOfferException extends BisqException {
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);
}
}

View File

@ -1,35 +0,0 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.scenario.bot;
import bisq.common.BisqException;
@SuppressWarnings("unused")
public class PaymentAccountNotFoundException extends BisqException {
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);
}
}

View File

@ -1,178 +0,0 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.scenario.bot;
import bisq.proto.grpc.OfferInfo;
import protobuf.PaymentAccount;
import java.security.SecureRandom;
import java.text.DecimalFormat;
import java.math.BigDecimal;
import java.util.Objects;
import java.util.function.Supplier;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import static bisq.apitest.method.offer.AbstractOfferTest.defaultBuyerSecurityDepositPct;
import static bisq.cli.CurrencyFormat.formatInternalFiatPrice;
import static bisq.cli.CurrencyFormat.formatSatoshis;
import static bisq.common.util.MathUtils.scaleDownByPowerOf10;
import static bisq.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<Long> nextAmount = () ->
this.getPaymentAccount().getPaymentMethod().getId().equals(F2F_ID)
? (long) (10000000 + RANDOM.nextInt(2500000))
: (long) (750000 + RANDOM.nextInt(250000));
@SuppressWarnings("FieldCanBeLocal")
private final Supplier<Long> 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<Double> 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 final String feeCurrency;
@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();
this.feeCurrency = RANDOM.nextBoolean() ? "BSQ" : "BTC";
}
public RandomOffer create() throws InvalidRandomOfferException {
try {
printDescription();
if (useMarketBasedPrice) {
this.offer = botClient.createOfferAtMarketBasedPrice(paymentAccount,
direction,
currencyCode,
amount,
minAmount,
priceMargin,
defaultBuyerSecurityDepositPct.get(),
feeCurrency,
"0" /*no trigger price*/);
} else {
this.offer = botClient.createOfferAtFixedPrice(paymentAccount,
direction,
currencyCode,
amount,
minAmount,
fixedOfferPrice,
defaultBuyerSecurityDepositPct.get(),
feeCurrency);
}
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);
}
}

View File

@ -1,149 +0,0 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.scenario.bot;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import static bisq.apitest.scenario.bot.protocol.ProtocolStep.DONE;
import static bisq.apitest.scenario.bot.shutdown.ManualShutdown.isShutdownCalled;
import static bisq.cli.table.builder.TableType.BSQ_BALANCE_TBL;
import static bisq.cli.table.builder.TableType.BTC_BALANCE_TBL;
import static java.util.concurrent.TimeUnit.SECONDS;
import bisq.apitest.method.BitcoinCliHelper;
import bisq.apitest.scenario.bot.protocol.BotProtocol;
import bisq.apitest.scenario.bot.protocol.MakerBotProtocol;
import bisq.apitest.scenario.bot.protocol.TakerBotProtocol;
import bisq.apitest.scenario.bot.script.BashScriptGenerator;
import bisq.apitest.scenario.bot.script.BotScript;
import bisq.apitest.scenario.bot.shutdown.ManualBotShutdownException;
import bisq.cli.table.builder.TableBuilder;
@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");
balancesBuilder.append("BSQ").append("\n");
balancesBuilder.append(new TableBuilder(BSQ_BALANCE_TBL, botClient.getBalance().getBsq()).build().toString());
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 Bisq 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");
}
}
}

View File

@ -1,352 +0,0 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.scenario.bot.protocol;
import bisq.proto.grpc.TradeInfo;
import protobuf.PaymentAccount;
import java.security.SecureRandom;
import java.io.File;
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 lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import static bisq.apitest.scenario.bot.protocol.ProtocolStep.*;
import static bisq.apitest.scenario.bot.shutdown.ManualShutdown.checkIfShutdownCalled;
import static bisq.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;
import bisq.apitest.method.BitcoinCliHelper;
import bisq.apitest.scenario.bot.BotClient;
import bisq.apitest.scenario.bot.script.BashScriptGenerator;
import bisq.apitest.scenario.bot.shutdown.ManualBotShutdownException;
import bisq.cli.table.builder.TableBuilder;
@Slf4j
public abstract class BotProtocol {
static final SecureRandom RANDOM = new SecureRandom();
static final String BUY = "BUY";
static final String SELL = "SELL";
protected final Supplier<Long> randomDelay = () -> (long) (2000 + RANDOM.nextInt(5000));
protected final AtomicLong protocolStepStartTime = new AtomicLong(0);
protected final Consumer<ProtocolStep> 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<TradeInfo, TradeInfo> waitForTakerFeeTxConfirm = (trade) -> {
sleep(5000);
waitForTakerFeeTxPublished(trade.getTradeId());
waitForTakerFeeTxConfirmed(trade.getTradeId());
return trade;
};
protected final Function<TradeInfo, TradeInfo> waitForPaymentStartedMessage = (trade) -> {
initProtocolStep.accept(WAIT_FOR_PAYMENT_STARTED_MESSAGE);
try {
createPaymentStartedScript(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.getIsPaymentStartedMessageSent()) {
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<TradeInfo, TradeInfo> sendPaymentStartedMessage = (trade) -> {
initProtocolStep.accept(SEND_PAYMENT_STARTED_MESSAGE);
checkIfShutdownCalled("Interrupted before sending 'payment started' message.");
this.getBotClient().sendConfirmPaymentStartedMessage(trade.getTradeId());
return trade;
};
protected final Function<TradeInfo, TradeInfo> 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.getIsPaymentReceivedMessageSent()) {
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<TradeInfo, TradeInfo> 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<TradeInfo, TradeInfo> 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 final Function<TradeInfo, TradeInfo> closeTrade = (trade) -> {
initProtocolStep.accept(CLOSE_TRADE);
var isBuy = trade.getOffer().getDirection().equalsIgnoreCase(BUY);
var isSell = trade.getOffer().getDirection().equalsIgnoreCase(SELL);
var cliUserIsSeller = (this instanceof MakerBotProtocol && isBuy) || (this instanceof TakerBotProtocol && isSell);
if (cliUserIsSeller) {
createKeepFundsScript(trade);
} else {
createGetBalanceScript();
}
checkIfShutdownCalled("Interrupted before closing trade with 'closetrade' command.");
this.getBotClient().sendCloseTradeMessage(trade.getTradeId());
return trade;
};
protected void createPaymentStartedScript(TradeInfo trade) {
File script = bashScriptGenerator.createPaymentStartedScript(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).getDepositTxId()));
} 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<TradeInfo> isDepositFeeTxStepComplete = (trade) -> {
if (currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED) && trade.getIsDepositPublished()) {
log.info("Taker deposit fee tx {} has been published.", trade.getDepositTxId());
return true;
} else if (currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED) && trade.getIsDepositConfirmed()) {
log.info("Taker deposit fee tx {} has been confirmed.", trade.getDepositTxId());
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");
}
}

View File

@ -1,116 +0,0 @@
package bisq.apitest.scenario.bot.protocol;
import bisq.proto.grpc.OfferInfo;
import bisq.proto.grpc.TradeInfo;
import protobuf.PaymentAccount;
import java.io.File;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
import lombok.extern.slf4j.Slf4j;
import static bisq.apitest.scenario.bot.protocol.ProtocolStep.DONE;
import static bisq.apitest.scenario.bot.protocol.ProtocolStep.WAIT_FOR_OFFER_TAKER;
import static bisq.apitest.scenario.bot.shutdown.ManualShutdown.checkIfShutdownCalled;
import static bisq.cli.table.builder.TableType.OFFER_TBL;
import static bisq.cli.table.builder.TableType.TRADE_DETAIL_TBL;
import bisq.apitest.method.BitcoinCliHelper;
import bisq.apitest.scenario.bot.BotClient;
import bisq.apitest.scenario.bot.RandomOffer;
import bisq.apitest.scenario.bot.script.BashScriptGenerator;
import bisq.apitest.scenario.bot.shutdown.ManualBotShutdownException;
import bisq.cli.table.builder.TableBuilder;
@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<Supplier<OfferInfo>, TradeInfo> makeTrade = waitForNewTrade.andThen(waitForTakerFeeTxConfirm);
var trade = makeTrade.apply(randomOffer);
var makerIsBuyer = trade.getOffer().getDirection().equalsIgnoreCase(BUY);
Function<TradeInfo, TradeInfo> completeFiatTransaction = makerIsBuyer
? sendPaymentStartedMessage.andThen(waitForPaymentReceivedConfirmation)
: waitForPaymentStartedMessage.andThen(sendPaymentReceivedMessage);
completeFiatTransaction.apply(trade);
Function<TradeInfo, TradeInfo> closeTrade = waitForPayoutTx.andThen(this.closeTrade);
closeTrade.apply(trade);
currentProtocolStep = DONE;
}
private final Supplier<OfferInfo> 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<Supplier<OfferInfo>, 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<TradeInfo> 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");
}
}

View File

@ -1,17 +0,0 @@
package bisq.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_STARTED_MESSAGE,
WAIT_FOR_PAYMENT_STARTED_MESSAGE,
SEND_PAYMENT_RECEIVED_CONFIRMATION_MESSAGE,
WAIT_FOR_PAYMENT_RECEIVED_CONFIRMATION_MESSAGE,
WAIT_FOR_PAYOUT_TX,
CLOSE_TRADE,
DONE
}

View File

@ -1,137 +0,0 @@
package bisq.apitest.scenario.bot.protocol;
import bisq.proto.grpc.OfferInfo;
import bisq.proto.grpc.TradeInfo;
import protobuf.PaymentAccount;
import java.io.File;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
import lombok.extern.slf4j.Slf4j;
import static bisq.apitest.scenario.bot.protocol.ProtocolStep.DONE;
import static bisq.apitest.scenario.bot.protocol.ProtocolStep.FIND_OFFER;
import static bisq.apitest.scenario.bot.protocol.ProtocolStep.TAKE_OFFER;
import static bisq.apitest.scenario.bot.shutdown.ManualShutdown.checkIfShutdownCalled;
import static bisq.cli.table.builder.TableType.OFFER_TBL;
import static bisq.core.payment.payload.PaymentMethod.F2F_ID;
import bisq.apitest.method.BitcoinCliHelper;
import bisq.apitest.scenario.bot.BotClient;
import bisq.apitest.scenario.bot.script.BashScriptGenerator;
import bisq.apitest.scenario.bot.shutdown.ManualBotShutdownException;
import bisq.cli.table.builder.TableBuilder;
@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<OfferInfo, TradeInfo> takeTrade = takeOffer.andThen(waitForTakerFeeTxConfirm);
var trade = takeTrade.apply(findOffer.get());
var takerIsSeller = trade.getOffer().getDirection().equalsIgnoreCase(BUY);
Function<TradeInfo, TradeInfo> completeFiatTransaction = takerIsSeller
? waitForPaymentStartedMessage.andThen(sendPaymentReceivedMessage)
: sendPaymentStartedMessage.andThen(waitForPaymentReceivedConfirmation);
completeFiatTransaction.apply(trade);
Function<TradeInfo, TradeInfo> closeTrade = waitForPayoutTx.andThen(this.closeTrade);
closeTrade.apply(trade);
currentProtocolStep = DONE;
}
private final Supplier<Optional<OfferInfo>> 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<OfferInfo> 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<OfferInfo> 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<OfferInfo, TradeInfo> takeOffer = (offer) -> {
initProtocolStep.accept(TAKE_OFFER);
checkIfShutdownCalled("Interrupted before taking offer.");
String feeCurrency = RANDOM.nextBoolean() ? "BSQ" : "BTC";
return botClient.takeOffer(offer.getId(), paymentAccount, feeCurrency);
};
private void createMakeOfferScript() {
String direction = RANDOM.nextBoolean() ? "BUY" : "SELL";
String feeCurrency = RANDOM.nextBoolean() ? "BSQ" : "BTC";
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",
feeCurrency);
} else {
script = bashScriptGenerator.createMakeFixedPricedOfferScript(direction,
currencyCode,
amount,
botClient.getCurrentBTCMarketPriceAsIntegerString(currencyCode),
"15.0",
feeCurrency);
}
printCliHintAndOrScript(script, "The manual CLI side can create an offer");
}
}

View File

@ -1,235 +0,0 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.scenario.bot.script;
import bisq.common.file.FileUtil;
import bisq.proto.grpc.OfferInfo;
import bisq.proto.grpc.TradeInfo;
import com.google.common.io.Files;
import java.nio.file.Paths;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
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("./bisq-cli --password=%s --port=%d", apiPassword, apiPort);
}
public File createMakeMarginPricedOfferScript(String direction,
String currencyCode,
String amount,
String marketPriceMargin,
String securityDeposit,
String feeCurrency) {
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,
feeCurrency);
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 feeCurrency) {
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,
feeCurrency);
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 --fee-currency=BSQ",
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 createPaymentStartedScript(TradeInfo trade) {
String paymentStartedCmd = format("%s confirmpaymentstarted --trade-id=%s",
cliBase,
trade.getTradeId());
String getTradeCmd = format("%s gettrade --trade-id=%s", cliBase, trade.getTradeId());
return createCliScript("confirmpaymentstarted.sh",
paymentStartedCmd,
"sleep 2",
getTradeCmd);
}
public File createPaymentReceivedScript(TradeInfo trade) {
String paymentStartedCmd = format("%s confirmpaymentreceived --trade-id=%s",
cliBase,
trade.getTradeId());
String getTradeCmd = format("%s gettrade --trade-id=%s", cliBase, trade.getTradeId());
return createCliScript("confirmpaymentreceived.sh",
paymentStartedCmd,
"sleep 2",
getTradeCmd);
}
public File createKeepFundsScript(TradeInfo trade) {
String paymentStartedCmd = 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",
paymentStartedCmd,
"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<CharSequence> 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);
}
}
}

View File

@ -1,78 +0,0 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.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;
}
}

View File

@ -1,248 +0,0 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.scenario.bot.script;
import bisq.core.util.JsonUtil;
import bisq.common.file.JsonFileManager;
import joptsimple.BuiltinHelpFormatter;
import joptsimple.OptionParser;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
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<Boolean> useTestHarnessOpt = parser
.accepts("use-testharness", "Use the test harness, or manually start your own nodes.")
.withRequiredArg()
.ofType(Boolean.class)
.defaultsTo(true);
OptionSpec<String> actionsOpt = parser
.accepts("actions", "A comma delimited list with no spaces, e.g., make,take,take,make,...")
.withRequiredArg();
OptionSpec<String> 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<String> 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<Integer> 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<String> 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<String> 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<Integer> protocolStepTimeLimitInMinutesOpt = parser
.accepts("step-time-limit", "Each protocol step's time limit in minutes")
.withRequiredArg()
.ofType(Integer.class)
.defaultsTo(60);
OptionSpec<Boolean> printCliScriptsOpt = parser
.accepts("print-cli-scripts", "Print the generated CLI scripts from bot")
.withRequiredArg()
.ofType(Boolean.class)
.defaultsTo(false);
OptionSpec<Boolean> 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=<payment-method-id> OR");
log.error(" \t\t(2) --country-code=<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 CLEAR_X_CHANGE_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=<port>");
log.error(" \t\t(2) --payment-account-for-bot=<payment-account-id>");
log.error(" \t\t(3) --payment-account-for-cli-scripts=<payment-account-id>");
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=<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=CLEAR_X_CHANGE").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=<port>").append("\n");
builder.append("\t\t").append("--payment-account-for-bot=<payment-account-id>").append("\n");
builder.append("\t\t").append("--payment-account-for-cli-scripts=<payment-account-id>").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);
}
}
}

View File

@ -1,35 +0,0 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.scenario.bot.shutdown;
import bisq.common.BisqException;
@SuppressWarnings("unused")
public class ManualBotShutdownException extends BisqException {
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);
}
}

View File

@ -1,64 +0,0 @@
package bisq.apitest.scenario.bot.shutdown;
import bisq.common.UserThread;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
import lombok.extern.slf4j.Slf4j;
import static bisq.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 bisq 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);
}
}
}