mirror of
https://github.com/bisq-network/bisq.git
synced 2024-11-19 01:41:11 +01:00
Merge pull request #6343 from ghubstan/remove-dead-pkg
Remove dead API test harness code
This commit is contained in:
commit
f69815d98e
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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."));
|
||||
}
|
||||
}
|
@ -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_]+: ", ""));
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user