Merge branch 'master' of github.com:bisq-network/bisq into hotfix/v1.5.8

# Conflicts:
#	build.gradle
#	desktop/package/linux/Dockerfile
#	desktop/package/linux/package.sh
#	desktop/package/linux/release.sh
#	desktop/package/macosx/create_app.sh
#	desktop/package/macosx/finalize.sh
#	desktop/package/macosx/insert_snapshot_version.sh
#	desktop/package/windows/package.bat
#	desktop/package/windows/release.bat
#	relay/src/main/resources/version.txt
This commit is contained in:
Christoph Atteneder 2021-03-02 10:06:49 +01:00
commit bdf6463e61
No known key found for this signature in database
GPG key ID: CD5DC1C529CDFD3B
58 changed files with 3781 additions and 1221 deletions

View file

@ -17,26 +17,27 @@
package bisq.apitest;
import java.net.InetAddress;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.junit.jupiter.api.TestInfo;
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
import static bisq.apitest.config.BisqAppConfig.arbdaemon;
import static bisq.apitest.config.BisqAppConfig.bobdaemon;
import static java.net.InetAddress.getLoopbackAddress;
import static java.util.Arrays.stream;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import bisq.apitest.config.ApiTestConfig;
import bisq.apitest.config.BisqAppConfig;
import bisq.apitest.method.BitcoinCliHelper;
import bisq.cli.GrpcStubs;
import bisq.cli.GrpcClient;
/**
* Base class for all test types: 'method', 'scenario' and 'e2e'.
@ -50,8 +51,8 @@ import bisq.cli.GrpcStubs;
* <p>
* Those documents contain information about the configurations used by this test harness:
* bitcoin-core's bitcoin.conf and blocknotify values, bisq instance options, the DAO genesis
* transaction id, initial BSQ and BTC balances for Bob & Alice accounts, and default
* PerfectMoney dummy payment accounts (USD) for Bob and Alice.
* transaction id, initial BSQ and BTC balances for Bob & Alice accounts, and Bob and
* Alice's default payment accounts.
* <p>
* During a build, the
* <a href="https://github.com/bisq-network/bisq/blob/master/docs/dao-setup.zip">dao-setup.zip</a>
@ -69,8 +70,12 @@ public class ApiTestCase {
protected static ApiTestConfig config;
protected static BitcoinCliHelper bitcoinCli;
// gRPC service stubs are used by method & scenario tests, but not e2e tests.
private static final Map<BisqAppConfig, GrpcStubs> grpcStubsCache = new HashMap<>();
@Nullable
protected static GrpcClient arbClient;
@Nullable
protected static GrpcClient aliceClient;
@Nullable
protected static GrpcClient bobClient;
public static void setUpScaffold(Enum<?>... supportingApps)
throws InterruptedException, ExecutionException, IOException {
@ -79,6 +84,7 @@ public class ApiTestCase {
.setUp();
config = scaffold.config;
bitcoinCli = new BitcoinCliHelper((config));
createGrpcClients();
}
public static void setUpScaffold(String[] params)
@ -90,24 +96,28 @@ public class ApiTestCase {
scaffold = new Scaffold(params).setUp();
config = scaffold.config;
bitcoinCli = new BitcoinCliHelper((config));
createGrpcClients();
}
public static void tearDownScaffold() {
scaffold.tearDown();
}
protected static String getEnumArrayAsString(Enum<?>[] supportingApps) {
return stream(supportingApps).map(Enum::name).collect(Collectors.joining(","));
}
protected static GrpcStubs grpcStubs(BisqAppConfig bisqAppConfig) {
if (grpcStubsCache.containsKey(bisqAppConfig)) {
return grpcStubsCache.get(bisqAppConfig);
} else {
GrpcStubs stubs = new GrpcStubs(InetAddress.getLoopbackAddress().getHostAddress(),
bisqAppConfig.apiPort, config.apiPassword);
grpcStubsCache.put(bisqAppConfig, stubs);
return stubs;
protected static void createGrpcClients() {
if (config.supportingApps.contains(alicedaemon.name())) {
aliceClient = new GrpcClient(getLoopbackAddress().getHostAddress(),
alicedaemon.apiPort,
config.apiPassword);
}
if (config.supportingApps.contains(bobdaemon.name())) {
bobClient = new GrpcClient(getLoopbackAddress().getHostAddress(),
bobdaemon.apiPort,
config.apiPassword);
}
if (config.supportingApps.contains(arbdaemon.name())) {
arbClient = new GrpcClient(getLoopbackAddress().getHostAddress(),
arbdaemon.apiPort,
config.apiPassword);
}
}

View file

@ -17,8 +17,6 @@
package bisq.apitest.method;
import bisq.proto.grpc.GetMethodHelpRequest;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
@ -51,10 +49,7 @@ public class GetMethodHelpTest extends MethodTest {
@Test
@Order(1)
public void testGetCreateOfferHelp() {
var help = grpcStubs(alicedaemon).helpService
.getMethodHelp(GetMethodHelpRequest.newBuilder()
.setMethodName(createoffer.name()).build())
.getMethodHelp();
var help = aliceClient.getMethodHelp(createoffer);
assertNotNull(help);
}

View file

@ -17,8 +17,6 @@
package bisq.apitest.method;
import bisq.proto.grpc.GetVersionRequest;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
@ -51,8 +49,7 @@ public class GetVersionTest extends MethodTest {
@Test
@Order(1)
public void testGetVersion() {
var version = grpcStubs(alicedaemon).versionService
.getVersion(GetVersionRequest.newBuilder().build()).getVersion();
var version = aliceClient.getVersion();
assertEquals(VERSION, version);
}

View file

@ -18,76 +18,27 @@
package bisq.apitest.method;
import bisq.core.api.model.PaymentAccountForm;
import bisq.core.api.model.TxFeeRateInfo;
import bisq.core.payment.F2FAccount;
import bisq.core.proto.CoreProtoResolver;
import bisq.common.util.Utilities;
import bisq.proto.grpc.AddressBalanceInfo;
import bisq.proto.grpc.BalancesInfo;
import bisq.proto.grpc.BsqBalanceInfo;
import bisq.proto.grpc.BtcBalanceInfo;
import bisq.proto.grpc.CancelOfferRequest;
import bisq.proto.grpc.ConfirmPaymentReceivedRequest;
import bisq.proto.grpc.ConfirmPaymentStartedRequest;
import bisq.proto.grpc.CreatePaymentAccountRequest;
import bisq.proto.grpc.GetAddressBalanceRequest;
import bisq.proto.grpc.GetBalancesRequest;
import bisq.proto.grpc.GetFundingAddressesRequest;
import bisq.proto.grpc.GetMethodHelpRequest;
import bisq.proto.grpc.GetMyOfferRequest;
import bisq.proto.grpc.GetOfferRequest;
import bisq.proto.grpc.GetPaymentAccountFormRequest;
import bisq.proto.grpc.GetPaymentAccountsRequest;
import bisq.proto.grpc.GetPaymentMethodsRequest;
import bisq.proto.grpc.GetTradeRequest;
import bisq.proto.grpc.GetTransactionRequest;
import bisq.proto.grpc.GetTxFeeRateRequest;
import bisq.proto.grpc.GetUnusedBsqAddressRequest;
import bisq.proto.grpc.KeepFundsRequest;
import bisq.proto.grpc.LockWalletRequest;
import bisq.proto.grpc.MarketPriceRequest;
import bisq.proto.grpc.OfferInfo;
import bisq.proto.grpc.RegisterDisputeAgentRequest;
import bisq.proto.grpc.RemoveWalletPasswordRequest;
import bisq.proto.grpc.SendBsqRequest;
import bisq.proto.grpc.SendBtcRequest;
import bisq.proto.grpc.SetTxFeeRatePreferenceRequest;
import bisq.proto.grpc.SetWalletPasswordRequest;
import bisq.proto.grpc.TakeOfferRequest;
import bisq.proto.grpc.TradeInfo;
import bisq.proto.grpc.TxInfo;
import bisq.proto.grpc.UnlockWalletRequest;
import bisq.proto.grpc.UnsetTxFeeRatePreferenceRequest;
import bisq.proto.grpc.WithdrawFundsRequest;
import protobuf.PaymentAccount;
import protobuf.PaymentMethod;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
import static bisq.apitest.config.BisqAppConfig.arbdaemon;
import static bisq.apitest.config.BisqAppConfig.bobdaemon;
import static bisq.common.app.DevEnv.DEV_PRIVILEGE_PRIV_KEY;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Arrays.stream;
import static java.util.Comparator.comparing;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
import bisq.apitest.ApiTestCase;
import bisq.apitest.config.BisqAppConfig;
import bisq.cli.GrpcStubs;
import bisq.cli.GrpcClient;
public class MethodTest extends ApiTestCase {
@ -95,13 +46,7 @@ public class MethodTest extends ApiTestCase {
protected static final String MEDIATOR = "mediator";
protected static final String REFUND_AGENT = "refundagent";
protected static GrpcStubs aliceStubs;
protected static GrpcStubs bobStubs;
protected static PaymentAccount alicesDummyAcct;
protected static PaymentAccount bobsDummyAcct;
private static final CoreProtoResolver CORE_PROTO_RESOLVER = new CoreProtoResolver();
protected static final CoreProtoResolver CORE_PROTO_RESOLVER = new CoreProtoResolver();
private static final Function<Enum<?>[], String> toNameList = (enums) ->
stream(enums).map(Enum::name).collect(Collectors.joining(","));
@ -116,7 +61,7 @@ public class MethodTest extends ApiTestCase {
"--callRateMeteringConfigPath", callRateMeteringConfigFile.getAbsolutePath(),
"--enableBisqDebugging", "false"
});
doPostStartup(registerDisputeAgents, generateBtcBlock, supportingApps);
doPostStartup(registerDisputeAgents, generateBtcBlock);
} catch (Exception ex) {
fail(ex);
}
@ -130,27 +75,16 @@ public class MethodTest extends ApiTestCase {
"--supportingApps", toNameList.apply(supportingApps),
"--enableBisqDebugging", "false"
});
doPostStartup(registerDisputeAgents, generateBtcBlock, supportingApps);
doPostStartup(registerDisputeAgents, generateBtcBlock);
} catch (Exception ex) {
fail(ex);
}
}
protected static void doPostStartup(boolean registerDisputeAgents,
boolean generateBtcBlock,
Enum<?>... supportingApps) {
boolean generateBtcBlock) {
if (registerDisputeAgents) {
registerDisputeAgents(arbdaemon);
}
if (stream(supportingApps).map(Enum::name).anyMatch(name -> name.equals(alicedaemon.name()))) {
aliceStubs = grpcStubs(alicedaemon);
alicesDummyAcct = getDefaultPerfectDummyPaymentAccount(alicedaemon);
}
if (stream(supportingApps).map(Enum::name).anyMatch(name -> name.equals(bobdaemon.name()))) {
bobStubs = grpcStubs(bobdaemon);
bobsDummyAcct = getDefaultPerfectDummyPaymentAccount(bobdaemon);
registerDisputeAgents();
}
// Generate 1 regtest block for alice's and/or bob's wallet to
@ -159,212 +93,11 @@ public class MethodTest extends ApiTestCase {
genBtcBlocksThenWait(1, 1500);
}
// Convenience methods for building gRPC request objects
protected final GetBalancesRequest createGetBalancesRequest(String currencyCode) {
return GetBalancesRequest.newBuilder().setCurrencyCode(currencyCode).build();
}
protected final GetAddressBalanceRequest createGetAddressBalanceRequest(String address) {
return GetAddressBalanceRequest.newBuilder().setAddress(address).build();
}
protected final SetWalletPasswordRequest createSetWalletPasswordRequest(String password) {
return SetWalletPasswordRequest.newBuilder().setPassword(password).build();
}
protected final SetWalletPasswordRequest createSetWalletPasswordRequest(String oldPassword, String newPassword) {
return SetWalletPasswordRequest.newBuilder().setPassword(oldPassword).setNewPassword(newPassword).build();
}
protected final RemoveWalletPasswordRequest createRemoveWalletPasswordRequest(String password) {
return RemoveWalletPasswordRequest.newBuilder().setPassword(password).build();
}
protected final UnlockWalletRequest createUnlockWalletRequest(String password, long timeout) {
return UnlockWalletRequest.newBuilder().setPassword(password).setTimeout(timeout).build();
}
protected final LockWalletRequest createLockWalletRequest() {
return LockWalletRequest.newBuilder().build();
}
protected final GetUnusedBsqAddressRequest createGetUnusedBsqAddressRequest() {
return GetUnusedBsqAddressRequest.newBuilder().build();
}
protected final SendBsqRequest createSendBsqRequest(String address,
String amount,
String txFeeRate) {
return SendBsqRequest.newBuilder()
.setAddress(address)
.setAmount(amount)
.setTxFeeRate(txFeeRate)
.build();
}
protected final SendBtcRequest createSendBtcRequest(String address,
String amount,
String txFeeRate,
String memo) {
return SendBtcRequest.newBuilder()
.setAddress(address)
.setAmount(amount)
.setTxFeeRate(txFeeRate)
.setMemo(memo)
.build();
}
protected final GetFundingAddressesRequest createGetFundingAddressesRequest() {
return GetFundingAddressesRequest.newBuilder().build();
}
protected final MarketPriceRequest createMarketPriceRequest(String currencyCode) {
return MarketPriceRequest.newBuilder().setCurrencyCode(currencyCode).build();
}
protected final GetOfferRequest createGetOfferRequest(String offerId) {
return GetOfferRequest.newBuilder().setId(offerId).build();
}
protected final GetMyOfferRequest createGetMyOfferRequest(String offerId) {
return GetMyOfferRequest.newBuilder().setId(offerId).build();
}
protected final CancelOfferRequest createCancelOfferRequest(String offerId) {
return CancelOfferRequest.newBuilder().setId(offerId).build();
}
protected final TakeOfferRequest createTakeOfferRequest(String offerId,
String paymentAccountId,
String takerFeeCurrencyCode) {
return TakeOfferRequest.newBuilder()
.setOfferId(offerId)
.setPaymentAccountId(paymentAccountId)
.setTakerFeeCurrencyCode(takerFeeCurrencyCode)
.build();
}
protected final GetTradeRequest createGetTradeRequest(String tradeId) {
return GetTradeRequest.newBuilder().setTradeId(tradeId).build();
}
protected final ConfirmPaymentStartedRequest createConfirmPaymentStartedRequest(String tradeId) {
return ConfirmPaymentStartedRequest.newBuilder().setTradeId(tradeId).build();
}
protected final ConfirmPaymentReceivedRequest createConfirmPaymentReceivedRequest(String tradeId) {
return ConfirmPaymentReceivedRequest.newBuilder().setTradeId(tradeId).build();
}
protected final KeepFundsRequest createKeepFundsRequest(String tradeId) {
return KeepFundsRequest.newBuilder()
.setTradeId(tradeId)
.build();
}
protected final WithdrawFundsRequest createWithdrawFundsRequest(String tradeId,
String address,
String memo) {
return WithdrawFundsRequest.newBuilder()
.setTradeId(tradeId)
.setAddress(address)
.setMemo(memo)
.build();
}
protected final GetMethodHelpRequest createGetMethodHelpRequest(String methodName) {
return GetMethodHelpRequest.newBuilder().setMethodName(methodName).build();
}
// Convenience methods for calling frequently used & thoroughly tested gRPC services.
protected final BalancesInfo getBalances(BisqAppConfig bisqAppConfig, String currencyCode) {
return grpcStubs(bisqAppConfig).walletsService.getBalances(
createGetBalancesRequest(currencyCode)).getBalances();
}
protected final BsqBalanceInfo getBsqBalances(BisqAppConfig bisqAppConfig) {
return getBalances(bisqAppConfig, "bsq").getBsq();
}
protected final BtcBalanceInfo getBtcBalances(BisqAppConfig bisqAppConfig) {
return getBalances(bisqAppConfig, "btc").getBtc();
}
protected final AddressBalanceInfo getAddressBalance(BisqAppConfig bisqAppConfig, String address) {
return grpcStubs(bisqAppConfig).walletsService.getAddressBalance(createGetAddressBalanceRequest(address)).getAddressBalanceInfo();
}
protected final void unlockWallet(BisqAppConfig bisqAppConfig, String password, long timeout) {
//noinspection ResultOfMethodCallIgnored
grpcStubs(bisqAppConfig).walletsService.unlockWallet(createUnlockWalletRequest(password, timeout));
}
protected final void lockWallet(BisqAppConfig bisqAppConfig) {
//noinspection ResultOfMethodCallIgnored
grpcStubs(bisqAppConfig).walletsService.lockWallet(createLockWalletRequest());
}
protected final String getUnusedBsqAddress(BisqAppConfig bisqAppConfig) {
return grpcStubs(bisqAppConfig).walletsService.getUnusedBsqAddress(createGetUnusedBsqAddressRequest()).getAddress();
}
protected final TxInfo sendBsq(BisqAppConfig bisqAppConfig,
String address,
String amount) {
return sendBsq(bisqAppConfig, address, amount, "");
}
protected final TxInfo sendBsq(BisqAppConfig bisqAppConfig,
String address,
String amount,
String txFeeRate) {
//noinspection ResultOfMethodCallIgnored
return grpcStubs(bisqAppConfig).walletsService.sendBsq(createSendBsqRequest(address,
amount,
txFeeRate))
.getTxInfo();
}
protected final TxInfo sendBtc(BisqAppConfig bisqAppConfig, String address, String amount) {
return sendBtc(bisqAppConfig, address, amount, "", "");
}
protected final TxInfo sendBtc(BisqAppConfig bisqAppConfig,
String address,
String amount,
String txFeeRate,
String memo) {
//noinspection ResultOfMethodCallIgnored
return grpcStubs(bisqAppConfig).walletsService.sendBtc(
createSendBtcRequest(address, amount, txFeeRate, memo))
.getTxInfo();
}
protected final String getUnusedBtcAddress(BisqAppConfig bisqAppConfig) {
//noinspection OptionalGetWithoutIsPresent
return grpcStubs(bisqAppConfig).walletsService.getFundingAddresses(createGetFundingAddressesRequest())
.getAddressBalanceInfoList()
.stream()
.filter(a -> a.getBalance() == 0 && a.getNumConfirmations() == 0)
.findFirst()
.get()
.getAddress();
}
protected final List<PaymentMethod> getPaymentMethods(BisqAppConfig bisqAppConfig) {
var req = GetPaymentMethodsRequest.newBuilder().build();
return grpcStubs(bisqAppConfig).paymentAccountsService.getPaymentMethods(req).getPaymentMethodsList();
}
protected final File getPaymentAccountForm(BisqAppConfig bisqAppConfig, String paymentMethodId) {
protected final File getPaymentAccountForm(GrpcClient grpcClient, String paymentMethodId) {
// We take seemingly unnecessary steps to get a File object, but the point is to
// test the API, and we do not directly ask bisq.core.api.model.PaymentAccountForm
// for an empty json form (file).
var req = GetPaymentAccountFormRequest.newBuilder()
.setPaymentMethodId(paymentMethodId)
.build();
String jsonString = grpcStubs(bisqAppConfig).paymentAccountsService.getPaymentAccountForm(req)
.getPaymentAccountFormJson();
String jsonString = grpcClient.getPaymentAcctFormAsJson(paymentMethodId);
// Write the json string to a file here in the test case.
File jsonFile = PaymentAccountForm.getTmpJsonFile(paymentMethodId);
try (PrintWriter out = new PrintWriter(jsonFile, UTF_8)) {
@ -375,113 +108,9 @@ public class MethodTest extends ApiTestCase {
return jsonFile;
}
protected final bisq.core.payment.PaymentAccount createPaymentAccount(BisqAppConfig bisqAppConfig,
String jsonString) {
var req = CreatePaymentAccountRequest.newBuilder()
.setPaymentAccountForm(jsonString)
.build();
var paymentAccountsService = grpcStubs(bisqAppConfig).paymentAccountsService;
// Normally, we can do asserts on the protos from the gRPC service, but in this
// case we need to return a bisq.core.payment.PaymentAccount so it can be cast
// to its sub type.
return fromProto(paymentAccountsService.createPaymentAccount(req).getPaymentAccount());
}
protected static List<PaymentAccount> getPaymentAccounts(BisqAppConfig bisqAppConfig) {
var req = GetPaymentAccountsRequest.newBuilder().build();
return grpcStubs(bisqAppConfig).paymentAccountsService.getPaymentAccounts(req)
.getPaymentAccountsList()
.stream()
.sorted(comparing(PaymentAccount::getCreationDate))
.collect(Collectors.toList());
}
protected static PaymentAccount getDefaultPerfectDummyPaymentAccount(BisqAppConfig bisqAppConfig) {
PaymentAccount paymentAccount = getPaymentAccounts(bisqAppConfig).get(0);
assertEquals("PerfectMoney dummy", paymentAccount.getAccountName());
return paymentAccount;
}
protected final double getMarketPrice(BisqAppConfig bisqAppConfig, String currencyCode) {
var req = createMarketPriceRequest(currencyCode);
return grpcStubs(bisqAppConfig).priceService.getMarketPrice(req).getPrice();
}
protected final OfferInfo getOffer(BisqAppConfig bisqAppConfig, String offerId) {
var req = createGetOfferRequest(offerId);
return grpcStubs(bisqAppConfig).offersService.getOffer(req).getOffer();
}
protected final OfferInfo getMyOffer(BisqAppConfig bisqAppConfig, String offerId) {
var req = createGetMyOfferRequest(offerId);
return grpcStubs(bisqAppConfig).offersService.getMyOffer(req).getOffer();
}
@SuppressWarnings("ResultOfMethodCallIgnored")
protected final void cancelOffer(BisqAppConfig bisqAppConfig, String offerId) {
var req = createCancelOfferRequest(offerId);
grpcStubs(bisqAppConfig).offersService.cancelOffer(req);
}
protected final TradeInfo getTrade(BisqAppConfig bisqAppConfig, String tradeId) {
var req = createGetTradeRequest(tradeId);
return grpcStubs(bisqAppConfig).tradesService.getTrade(req).getTrade();
}
@SuppressWarnings("ResultOfMethodCallIgnored")
protected final void confirmPaymentStarted(BisqAppConfig bisqAppConfig, String tradeId) {
var req = createConfirmPaymentStartedRequest(tradeId);
grpcStubs(bisqAppConfig).tradesService.confirmPaymentStarted(req);
}
@SuppressWarnings("ResultOfMethodCallIgnored")
protected final void confirmPaymentReceived(BisqAppConfig bisqAppConfig, String tradeId) {
var req = createConfirmPaymentReceivedRequest(tradeId);
grpcStubs(bisqAppConfig).tradesService.confirmPaymentReceived(req);
}
@SuppressWarnings("ResultOfMethodCallIgnored")
protected final void keepFunds(BisqAppConfig bisqAppConfig, String tradeId) {
var req = createKeepFundsRequest(tradeId);
grpcStubs(bisqAppConfig).tradesService.keepFunds(req);
}
@SuppressWarnings("ResultOfMethodCallIgnored")
protected final void withdrawFunds(BisqAppConfig bisqAppConfig,
String tradeId,
String address,
String memo) {
var req = createWithdrawFundsRequest(tradeId, address, memo);
grpcStubs(bisqAppConfig).tradesService.withdrawFunds(req);
}
protected final TxFeeRateInfo getTxFeeRate(BisqAppConfig bisqAppConfig) {
var req = GetTxFeeRateRequest.newBuilder().build();
return TxFeeRateInfo.fromProto(
grpcStubs(bisqAppConfig).walletsService.getTxFeeRate(req).getTxFeeRateInfo());
}
protected final TxFeeRateInfo setTxFeeRate(BisqAppConfig bisqAppConfig, long feeRate) {
var req = SetTxFeeRatePreferenceRequest.newBuilder()
.setTxFeeRatePreference(feeRate)
.build();
return TxFeeRateInfo.fromProto(
grpcStubs(bisqAppConfig).walletsService.setTxFeeRatePreference(req).getTxFeeRateInfo());
}
protected final TxFeeRateInfo unsetTxFeeRate(BisqAppConfig bisqAppConfig) {
var req = UnsetTxFeeRatePreferenceRequest.newBuilder().build();
return TxFeeRateInfo.fromProto(
grpcStubs(bisqAppConfig).walletsService.unsetTxFeeRatePreference(req).getTxFeeRateInfo());
}
protected final TxInfo getTransaction(BisqAppConfig bisqAppConfig, String txId) {
var req = GetTransactionRequest.newBuilder().setTxId(txId).build();
return grpcStubs(bisqAppConfig).walletsService.getTransaction(req).getTxInfo();
}
public bisq.core.payment.PaymentAccount createDummyF2FAccount(BisqAppConfig bisqAppConfig,
String countryCode) {
protected bisq.core.payment.PaymentAccount createDummyF2FAccount(GrpcClient grpcClient,
String countryCode) {
String f2fAccountJsonString = "{\n" +
" \"_COMMENTS_\": \"This is a dummy account.\",\n" +
" \"paymentMethodId\": \"F2F\",\n" +
@ -491,35 +120,26 @@ public class MethodTest extends ApiTestCase {
" \"country\": \"" + countryCode.toUpperCase() + "\",\n" +
" \"extraInfo\": \"Salt Lick #213\"\n" +
"}\n";
F2FAccount f2FAccount = (F2FAccount) createPaymentAccount(bisqAppConfig, f2fAccountJsonString);
F2FAccount f2FAccount = (F2FAccount) createPaymentAccount(grpcClient, f2fAccountJsonString);
return f2FAccount;
}
protected final String getMethodHelp(BisqAppConfig bisqAppConfig, String methodName) {
var req = createGetMethodHelpRequest(methodName);
return grpcStubs(bisqAppConfig).helpService.getMethodHelp(req).getMethodHelp();
protected final bisq.core.payment.PaymentAccount createPaymentAccount(GrpcClient grpcClient, String jsonString) {
// Normally, we do asserts on the protos from the gRPC service, but in this
// case we need a bisq.core.payment.PaymentAccount so it can be cast to its
// sub type.
var paymentAccount = grpcClient.createPaymentAccount(jsonString);
return bisq.core.payment.PaymentAccount.fromProto(paymentAccount, CORE_PROTO_RESOLVER);
}
// Static conveniences for test methods and test case fixture setups.
protected static RegisterDisputeAgentRequest createRegisterDisputeAgentRequest(String disputeAgentType) {
return RegisterDisputeAgentRequest.newBuilder()
.setDisputeAgentType(disputeAgentType.toLowerCase())
.setRegistrationKey(DEV_PRIVILEGE_PRIV_KEY).build();
}
@SuppressWarnings({"ResultOfMethodCallIgnored", "SameParameterValue"})
protected static void registerDisputeAgents(BisqAppConfig bisqAppConfig) {
var disputeAgentsService = grpcStubs(bisqAppConfig).disputeAgentsService;
disputeAgentsService.registerDisputeAgent(createRegisterDisputeAgentRequest(MEDIATOR));
disputeAgentsService.registerDisputeAgent(createRegisterDisputeAgentRequest(REFUND_AGENT));
protected static void registerDisputeAgents() {
arbClient.registerDisputeAgent(MEDIATOR, DEV_PRIVILEGE_PRIV_KEY);
arbClient.registerDisputeAgent(REFUND_AGENT, DEV_PRIVILEGE_PRIV_KEY);
}
protected static String encodeToHex(String s) {
return Utilities.bytesAsHexString(s.getBytes(UTF_8));
}
private bisq.core.payment.PaymentAccount fromProto(PaymentAccount proto) {
return bisq.core.payment.PaymentAccount.fromProto(proto, CORE_PROTO_RESOLVER);
}
}

View file

@ -17,8 +17,6 @@
package bisq.apitest.method;
import bisq.proto.grpc.RegisterDisputeAgentRequest;
import io.grpc.StatusRuntimeException;
import lombok.extern.slf4j.Slf4j;
@ -58,9 +56,8 @@ public class RegisterDisputeAgentsTest extends MethodTest {
@Test
@Order(1)
public void testRegisterArbitratorShouldThrowException() {
var req = createRegisterDisputeAgentRequest(ARBITRATOR);
Throwable exception = assertThrows(StatusRuntimeException.class, () ->
grpcStubs(arbdaemon).disputeAgentsService.registerDisputeAgent(req));
arbClient.registerDisputeAgent(ARBITRATOR, DEV_PRIVILEGE_PRIV_KEY));
assertEquals("INVALID_ARGUMENT: arbitrators must be registered in a Bisq UI",
exception.getMessage());
}
@ -68,9 +65,8 @@ public class RegisterDisputeAgentsTest extends MethodTest {
@Test
@Order(2)
public void testInvalidDisputeAgentTypeArgShouldThrowException() {
var req = createRegisterDisputeAgentRequest("badagent");
Throwable exception = assertThrows(StatusRuntimeException.class, () ->
grpcStubs(arbdaemon).disputeAgentsService.registerDisputeAgent(req));
arbClient.registerDisputeAgent("badagent", DEV_PRIVILEGE_PRIV_KEY));
assertEquals("INVALID_ARGUMENT: unknown dispute agent type 'badagent'",
exception.getMessage());
}
@ -78,11 +74,8 @@ public class RegisterDisputeAgentsTest extends MethodTest {
@Test
@Order(3)
public void testInvalidRegistrationKeyArgShouldThrowException() {
var req = RegisterDisputeAgentRequest.newBuilder()
.setDisputeAgentType(REFUND_AGENT)
.setRegistrationKey("invalid" + DEV_PRIVILEGE_PRIV_KEY).build();
Throwable exception = assertThrows(StatusRuntimeException.class, () ->
grpcStubs(arbdaemon).disputeAgentsService.registerDisputeAgent(req));
arbClient.registerDisputeAgent(REFUND_AGENT, "invalid" + DEV_PRIVILEGE_PRIV_KEY));
assertEquals("INVALID_ARGUMENT: invalid registration key",
exception.getMessage());
}
@ -90,15 +83,13 @@ public class RegisterDisputeAgentsTest extends MethodTest {
@Test
@Order(4)
public void testRegisterMediator() {
var req = createRegisterDisputeAgentRequest(MEDIATOR);
grpcStubs(arbdaemon).disputeAgentsService.registerDisputeAgent(req);
arbClient.registerDisputeAgent(MEDIATOR, DEV_PRIVILEGE_PRIV_KEY);
}
@Test
@Order(5)
public void testRegisterRefundAgent() {
var req = createRegisterDisputeAgentRequest(REFUND_AGENT);
grpcStubs(arbdaemon).disputeAgentsService.registerDisputeAgent(req);
arbClient.registerDisputeAgent(REFUND_AGENT, DEV_PRIVILEGE_PRIV_KEY);
}
@AfterAll

View file

@ -18,20 +18,11 @@
package bisq.apitest.method.offer;
import bisq.core.monetary.Altcoin;
import bisq.core.payment.PaymentAccount;
import bisq.proto.grpc.CreateOfferRequest;
import bisq.proto.grpc.GetMyOffersRequest;
import bisq.proto.grpc.GetOffersRequest;
import bisq.proto.grpc.OfferInfo;
import org.bitcoinj.utils.Fiat;
import java.math.BigDecimal;
import java.util.List;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
@ -44,17 +35,12 @@ import static bisq.apitest.config.BisqAppConfig.bobdaemon;
import static bisq.apitest.config.BisqAppConfig.seednode;
import static bisq.common.util.MathUtils.roundDouble;
import static bisq.common.util.MathUtils.scaleDownByPowerOf10;
import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent;
import static bisq.core.locale.CurrencyUtil.isCryptoCurrency;
import static java.lang.String.format;
import static java.math.RoundingMode.HALF_UP;
import static java.util.Comparator.comparing;
import static org.junit.jupiter.api.Assertions.fail;
import bisq.apitest.method.MethodTest;
import bisq.cli.GrpcStubs;
@Slf4j
public abstract class AbstractOfferTest extends MethodTest {
@ -70,109 +56,11 @@ public abstract class AbstractOfferTest extends MethodTest {
bobdaemon);
}
protected final OfferInfo createAliceOffer(PaymentAccount paymentAccount,
String direction,
String currencyCode,
long amount,
String makerFeeCurrencyCode) {
return createMarketBasedPricedOffer(aliceStubs,
paymentAccount,
direction,
currencyCode,
amount,
makerFeeCurrencyCode);
}
protected final OfferInfo createBobOffer(PaymentAccount paymentAccount,
String direction,
String currencyCode,
long amount,
String makerFeeCurrencyCode) {
return createMarketBasedPricedOffer(bobStubs,
paymentAccount,
direction,
currencyCode,
amount,
makerFeeCurrencyCode);
}
protected final OfferInfo createMarketBasedPricedOffer(GrpcStubs grpcStubs,
PaymentAccount paymentAccount,
String direction,
String currencyCode,
long amount,
String makerFeeCurrencyCode) {
var req = CreateOfferRequest.newBuilder()
.setPaymentAccountId(paymentAccount.getId())
.setDirection(direction)
.setCurrencyCode(currencyCode)
.setAmount(amount)
.setMinAmount(amount)
.setUseMarketBasedPrice(true)
.setMarketPriceMargin(0.00)
.setPrice("0")
.setBuyerSecurityDeposit(getDefaultBuyerSecurityDepositAsPercent())
.setMakerFeeCurrencyCode(makerFeeCurrencyCode)
.build();
return grpcStubs.offersService.createOffer(req).getOffer();
}
protected final OfferInfo getOffer(String offerId) {
return aliceStubs.offersService.getOffer(createGetOfferRequest(offerId)).getOffer();
}
protected final OfferInfo getMyOffer(String offerId) {
return aliceStubs.offersService.getMyOffer(createGetMyOfferRequest(offerId)).getOffer();
}
@SuppressWarnings("ResultOfMethodCallIgnored")
protected final void cancelOffer(GrpcStubs grpcStubs, String offerId) {
grpcStubs.offersService.cancelOffer(createCancelOfferRequest(offerId));
}
protected final OfferInfo getMostRecentOffer(GrpcStubs grpcStubs, String direction, String currencyCode) {
List<OfferInfo> offerInfoList = getOffersSortedByDate(grpcStubs, direction, currencyCode);
if (offerInfoList.isEmpty())
fail(format("No %s offers found for currency %s", direction, currencyCode));
return offerInfoList.get(offerInfoList.size() - 1);
}
protected final List<OfferInfo> getOffersSortedByDate(GrpcStubs grpcStubs,
String direction,
String currencyCode) {
var req = GetOffersRequest.newBuilder()
.setDirection(direction)
.setCurrencyCode(currencyCode).build();
var reply = grpcStubs.offersService.getOffers(req);
return sortOffersByDate(reply.getOffersList());
}
protected final List<OfferInfo> getMyOffersSortedByDate(GrpcStubs grpcStubs,
String direction,
String currencyCode) {
var req = GetMyOffersRequest.newBuilder()
.setDirection(direction)
.setCurrencyCode(currencyCode).build();
var reply = grpcStubs.offersService.getMyOffers(req);
return sortOffersByDate(reply.getOffersList());
}
protected final List<OfferInfo> sortOffersByDate(List<OfferInfo> offerInfoList) {
return offerInfoList.stream()
.sorted(comparing(OfferInfo::getDate))
.collect(Collectors.toList());
}
protected double getScaledOfferPrice(double offerPrice, String currencyCode) {
int precision = isCryptoCurrency(currencyCode) ? Altcoin.SMALLEST_UNIT_EXPONENT : Fiat.SMALLEST_UNIT_EXPONENT;
return scaleDownByPowerOf10(offerPrice, precision);
}
protected final double getMarketPrice(String currencyCode) {
return getMarketPrice(alicedaemon, currencyCode);
}
protected final double getPercentageDifference(double price1, double price2) {
return BigDecimal.valueOf(roundDouble((1 - (price1 / price2)), 5))
.setScale(4, HALF_UP)

View file

@ -17,13 +17,12 @@
package bisq.apitest.method.offer;
import bisq.core.btc.wallet.Restrictions;
import bisq.core.payment.PaymentAccount;
import bisq.proto.grpc.CreateOfferRequest;
import bisq.proto.grpc.OfferInfo;
import java.util.List;
import java.util.function.Consumer;
import lombok.extern.slf4j.Slf4j;
@ -33,7 +32,7 @@ import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent;
import static org.junit.jupiter.api.Assertions.assertEquals;
@Disabled
@ -45,45 +44,43 @@ public class CancelOfferTest extends AbstractOfferTest {
private static final String CURRENCY_CODE = "cad";
private static final int MAX_OFFERS = 3;
private final Consumer<String> createOfferToCancel = (paymentAccountId) -> {
aliceClient.createMarketBasedPricedOffer(DIRECTION,
CURRENCY_CODE,
10000000L,
10000000L,
0.00,
getDefaultBuyerSecurityDepositAsPercent(),
paymentAccountId,
"bsq");
};
@Test
@Order(1)
public void testCancelOffer() {
PaymentAccount cadAccount = createDummyF2FAccount(alicedaemon, "CA");
var req = CreateOfferRequest.newBuilder()
.setPaymentAccountId(cadAccount.getId())
.setDirection(DIRECTION)
.setCurrencyCode(CURRENCY_CODE)
.setAmount(10000000)
.setMinAmount(10000000)
.setUseMarketBasedPrice(true)
.setMarketPriceMargin(0.00)
.setPrice("0")
.setBuyerSecurityDeposit(Restrictions.getDefaultBuyerSecurityDepositAsPercent())
.setMakerFeeCurrencyCode("bsq")
.build();
PaymentAccount cadAccount = createDummyF2FAccount(aliceClient, "CA");
// Create some offers.
for (int i = 1; i <= MAX_OFFERS; i++) {
//noinspection ResultOfMethodCallIgnored
aliceStubs.offersService.createOffer(req);
createOfferToCancel.accept(cadAccount.getId());
// Wait for Alice's AddToOfferBook task.
// Wait times vary; my logs show >= 2 second delay.
sleep(2500);
}
List<OfferInfo> offers = getMyOffersSortedByDate(aliceStubs, DIRECTION, CURRENCY_CODE);
List<OfferInfo> offers = aliceClient.getMyOffersSortedByDate(DIRECTION, CURRENCY_CODE);
assertEquals(MAX_OFFERS, offers.size());
// Cancel the offers, checking the open offer count after each offer removal.
for (int i = 1; i <= MAX_OFFERS; i++) {
cancelOffer(aliceStubs, offers.remove(0).getId());
offers = getMyOffersSortedByDate(aliceStubs, DIRECTION, CURRENCY_CODE);
aliceClient.cancelOffer(offers.remove(0).getId());
offers = aliceClient.getMyOffersSortedByDate(DIRECTION, CURRENCY_CODE);
assertEquals(MAX_OFFERS - i, offers.size());
}
sleep(1000); // wait for offer removal
offers = getMyOffersSortedByDate(aliceStubs, DIRECTION, CURRENCY_CODE);
offers = aliceClient.getMyOffersSortedByDate(DIRECTION, CURRENCY_CODE);
assertEquals(0, offers.size());
}
}

View file

@ -19,8 +19,6 @@ package bisq.apitest.method.offer;
import bisq.core.payment.PaymentAccount;
import bisq.proto.grpc.CreateOfferRequest;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Disabled;
@ -29,7 +27,6 @@ import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@ -45,20 +42,15 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
@Test
@Order(1)
public void testCreateAUDBTCBuyOfferUsingFixedPrice16000() {
PaymentAccount audAccount = createDummyF2FAccount(alicedaemon, "AU");
var req = CreateOfferRequest.newBuilder()
.setPaymentAccountId(audAccount.getId())
.setDirection("buy")
.setCurrencyCode("aud")
.setAmount(10000000)
.setMinAmount(10000000)
.setUseMarketBasedPrice(false)
.setMarketPriceMargin(0.00)
.setPrice("36000")
.setBuyerSecurityDeposit(getDefaultBuyerSecurityDepositAsPercent())
.setMakerFeeCurrencyCode(MAKER_FEE_CURRENCY_CODE)
.build();
var newOffer = aliceStubs.offersService.createOffer(req).getOffer();
PaymentAccount audAccount = createDummyF2FAccount(aliceClient, "AU");
var newOffer = aliceClient.createFixedPricedOffer("buy",
"aud",
10000000L,
10000000L,
"36000",
getDefaultBuyerSecurityDepositAsPercent(),
audAccount.getId(),
MAKER_FEE_CURRENCY_CODE);
String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId);
assertEquals("BUY", newOffer.getDirection());
@ -72,7 +64,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
assertEquals("AUD", newOffer.getCounterCurrencyCode());
assertFalse(newOffer.getIsCurrencyForMakerFeeBtc());
newOffer = getMyOffer(newOfferId);
newOffer = aliceClient.getMyOffer(newOfferId);
assertEquals(newOfferId, newOffer.getId());
assertEquals("BUY", newOffer.getDirection());
assertFalse(newOffer.getUseMarketBasedPrice());
@ -89,20 +81,15 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
@Test
@Order(2)
public void testCreateUSDBTCBuyOfferUsingFixedPrice100001234() {
PaymentAccount usdAccount = createDummyF2FAccount(alicedaemon, "US");
var req = CreateOfferRequest.newBuilder()
.setPaymentAccountId(usdAccount.getId())
.setDirection("buy")
.setCurrencyCode("usd")
.setAmount(10000000)
.setMinAmount(10000000)
.setUseMarketBasedPrice(false)
.setMarketPriceMargin(0.00)
.setPrice("30000.1234")
.setBuyerSecurityDeposit(getDefaultBuyerSecurityDepositAsPercent())
.setMakerFeeCurrencyCode(MAKER_FEE_CURRENCY_CODE)
.build();
var newOffer = aliceStubs.offersService.createOffer(req).getOffer();
PaymentAccount usdAccount = createDummyF2FAccount(aliceClient, "US");
var newOffer = aliceClient.createFixedPricedOffer("buy",
"usd",
10000000L,
10000000L,
"30000.1234",
getDefaultBuyerSecurityDepositAsPercent(),
usdAccount.getId(),
MAKER_FEE_CURRENCY_CODE);
String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId);
assertEquals("BUY", newOffer.getDirection());
@ -116,7 +103,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
assertEquals("USD", newOffer.getCounterCurrencyCode());
assertFalse(newOffer.getIsCurrencyForMakerFeeBtc());
newOffer = getMyOffer(newOfferId);
newOffer = aliceClient.getMyOffer(newOfferId);
assertEquals(newOfferId, newOffer.getId());
assertEquals("BUY", newOffer.getDirection());
assertFalse(newOffer.getUseMarketBasedPrice());
@ -133,20 +120,15 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
@Test
@Order(3)
public void testCreateEURBTCSellOfferUsingFixedPrice95001234() {
PaymentAccount eurAccount = createDummyF2FAccount(alicedaemon, "FR");
var req = CreateOfferRequest.newBuilder()
.setPaymentAccountId(eurAccount.getId())
.setDirection("sell")
.setCurrencyCode("eur")
.setAmount(10000000)
.setMinAmount(10000000)
.setUseMarketBasedPrice(false)
.setMarketPriceMargin(0.00)
.setPrice("29500.1234")
.setBuyerSecurityDeposit(getDefaultBuyerSecurityDepositAsPercent())
.setMakerFeeCurrencyCode(MAKER_FEE_CURRENCY_CODE)
.build();
var newOffer = aliceStubs.offersService.createOffer(req).getOffer();
PaymentAccount eurAccount = createDummyF2FAccount(aliceClient, "FR");
var newOffer = aliceClient.createFixedPricedOffer("sell",
"eur",
10000000L,
10000000L,
"29500.1234",
getDefaultBuyerSecurityDepositAsPercent(),
eurAccount.getId(),
MAKER_FEE_CURRENCY_CODE);
String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId);
assertEquals("SELL", newOffer.getDirection());
@ -160,7 +142,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
assertEquals("EUR", newOffer.getCounterCurrencyCode());
assertFalse(newOffer.getIsCurrencyForMakerFeeBtc());
newOffer = getMyOffer(newOfferId);
newOffer = aliceClient.getMyOffer(newOfferId);
assertEquals(newOfferId, newOffer.getId());
assertEquals("SELL", newOffer.getDirection());
assertFalse(newOffer.getUseMarketBasedPrice());

View file

@ -19,7 +19,6 @@ package bisq.apitest.method.offer;
import bisq.core.payment.PaymentAccount;
import bisq.proto.grpc.CreateOfferRequest;
import bisq.proto.grpc.OfferInfo;
import java.text.DecimalFormat;
@ -32,7 +31,6 @@ import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
import static bisq.common.util.MathUtils.scaleDownByPowerOf10;
import static bisq.common.util.MathUtils.scaleUpByPowerOf10;
import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent;
@ -57,21 +55,16 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
@Test
@Order(1)
public void testCreateUSDBTCBuyOffer5PctPriceMargin() {
PaymentAccount usdAccount = createDummyF2FAccount(alicedaemon, "US");
PaymentAccount usdAccount = createDummyF2FAccount(aliceClient, "US");
double priceMarginPctInput = 5.00;
var req = CreateOfferRequest.newBuilder()
.setPaymentAccountId(usdAccount.getId())
.setDirection("buy")
.setCurrencyCode("usd")
.setAmount(10000000)
.setMinAmount(10000000)
.setUseMarketBasedPrice(true)
.setMarketPriceMargin(priceMarginPctInput)
.setPrice("0")
.setBuyerSecurityDeposit(getDefaultBuyerSecurityDepositAsPercent())
.setMakerFeeCurrencyCode(MAKER_FEE_CURRENCY_CODE)
.build();
var newOffer = aliceStubs.offersService.createOffer(req).getOffer();
var newOffer = aliceClient.createMarketBasedPricedOffer("buy",
"usd",
10000000L,
10000000L,
priceMarginPctInput,
getDefaultBuyerSecurityDepositAsPercent(),
usdAccount.getId(),
MAKER_FEE_CURRENCY_CODE);
String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId);
assertEquals("BUY", newOffer.getDirection());
@ -84,7 +77,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
assertEquals("USD", newOffer.getCounterCurrencyCode());
assertTrue(newOffer.getIsCurrencyForMakerFeeBtc());
newOffer = getMyOffer(newOfferId);
newOffer = aliceClient.getMyOffer(newOfferId);
assertEquals(newOfferId, newOffer.getId());
assertEquals("BUY", newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice());
@ -102,8 +95,9 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
@Test
@Order(2)
public void testCreateNZDBTCBuyOfferMinus2PctPriceMargin() {
PaymentAccount nzdAccount = createDummyF2FAccount(alicedaemon, "NZ");
PaymentAccount nzdAccount = createDummyF2FAccount(aliceClient, "NZ");
double priceMarginPctInput = -2.00;
/*
var req = CreateOfferRequest.newBuilder()
.setPaymentAccountId(nzdAccount.getId())
.setDirection("buy")
@ -117,6 +111,16 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
.setMakerFeeCurrencyCode(MAKER_FEE_CURRENCY_CODE)
.build();
var newOffer = aliceStubs.offersService.createOffer(req).getOffer();
*/
var newOffer = aliceClient.createMarketBasedPricedOffer("buy",
"nzd",
10000000L,
10000000L,
priceMarginPctInput,
getDefaultBuyerSecurityDepositAsPercent(),
nzdAccount.getId(),
MAKER_FEE_CURRENCY_CODE);
String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId);
assertEquals("BUY", newOffer.getDirection());
@ -129,7 +133,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
assertEquals("NZD", newOffer.getCounterCurrencyCode());
assertTrue(newOffer.getIsCurrencyForMakerFeeBtc());
newOffer = getMyOffer(newOfferId);
newOffer = aliceClient.getMyOffer(newOfferId);
assertEquals(newOfferId, newOffer.getId());
assertEquals("BUY", newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice());
@ -147,22 +151,16 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
@Test
@Order(3)
public void testCreateGBPBTCSellOfferMinus1Point5PctPriceMargin() {
PaymentAccount gbpAccount = createDummyF2FAccount(alicedaemon, "GB");
PaymentAccount gbpAccount = createDummyF2FAccount(aliceClient, "GB");
double priceMarginPctInput = -1.5;
var req = CreateOfferRequest.newBuilder()
.setPaymentAccountId(gbpAccount.getId())
.setDirection("sell")
.setCurrencyCode("gbp")
.setAmount(10000000)
.setMinAmount(10000000)
.setUseMarketBasedPrice(true)
.setMarketPriceMargin(priceMarginPctInput)
.setPrice("0")
.setBuyerSecurityDeposit(getDefaultBuyerSecurityDepositAsPercent())
.setMakerFeeCurrencyCode(MAKER_FEE_CURRENCY_CODE)
.build();
var newOffer = aliceStubs.offersService.createOffer(req).getOffer();
var newOffer = aliceClient.createMarketBasedPricedOffer("sell",
"gbp",
10000000L,
10000000L,
priceMarginPctInput,
getDefaultBuyerSecurityDepositAsPercent(),
gbpAccount.getId(),
MAKER_FEE_CURRENCY_CODE);
String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId);
assertEquals("SELL", newOffer.getDirection());
@ -175,7 +173,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
assertEquals("GBP", newOffer.getCounterCurrencyCode());
assertTrue(newOffer.getIsCurrencyForMakerFeeBtc());
newOffer = getMyOffer(newOfferId);
newOffer = aliceClient.getMyOffer(newOfferId);
assertEquals(newOfferId, newOffer.getId());
assertEquals("SELL", newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice());
@ -193,22 +191,16 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
@Test
@Order(4)
public void testCreateBRLBTCSellOffer6Point55PctPriceMargin() {
PaymentAccount brlAccount = createDummyF2FAccount(alicedaemon, "BR");
PaymentAccount brlAccount = createDummyF2FAccount(aliceClient, "BR");
double priceMarginPctInput = 6.55;
var req = CreateOfferRequest.newBuilder()
.setPaymentAccountId(brlAccount.getId())
.setDirection("sell")
.setCurrencyCode("brl")
.setAmount(10000000)
.setMinAmount(10000000)
.setUseMarketBasedPrice(true)
.setMarketPriceMargin(priceMarginPctInput)
.setPrice("0")
.setBuyerSecurityDeposit(getDefaultBuyerSecurityDepositAsPercent())
.setMakerFeeCurrencyCode(MAKER_FEE_CURRENCY_CODE)
.build();
var newOffer = aliceStubs.offersService.createOffer(req).getOffer();
var newOffer = aliceClient.createMarketBasedPricedOffer("sell",
"brl",
10000000L,
10000000L,
priceMarginPctInput,
getDefaultBuyerSecurityDepositAsPercent(),
brlAccount.getId(),
MAKER_FEE_CURRENCY_CODE);
String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId);
assertEquals("SELL", newOffer.getDirection());
@ -221,7 +213,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
assertEquals("BRL", newOffer.getCounterCurrencyCode());
assertTrue(newOffer.getIsCurrencyForMakerFeeBtc());
newOffer = getMyOffer(newOfferId);
newOffer = aliceClient.getMyOffer(newOfferId);
assertEquals(newOfferId, newOffer.getId());
assertEquals("SELL", newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice());
@ -239,7 +231,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
private void assertCalculatedPriceIsCorrect(OfferInfo offer, double priceMarginPctInput) {
assertTrue(() -> {
String counterCurrencyCode = offer.getCounterCurrencyCode();
double mktPrice = getMarketPrice(counterCurrencyCode);
double mktPrice = aliceClient.getBtcPrice(counterCurrencyCode);
double scaledOfferPrice = getScaledOfferPrice(offer.getPrice(), counterCurrencyCode);
double expectedDiffPct = scaleDownByPowerOf10(priceMarginPctInput, 2);
double actualDiffPct = offer.getDirection().equals(BUY.name())

View file

@ -19,8 +19,6 @@ package bisq.apitest.method.offer;
import bisq.core.payment.PaymentAccount;
import bisq.proto.grpc.CreateOfferRequest;
import io.grpc.StatusRuntimeException;
import lombok.extern.slf4j.Slf4j;
@ -31,7 +29,6 @@ import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@ -44,22 +41,17 @@ public class ValidateCreateOfferTest extends AbstractOfferTest {
@Test
@Order(1)
public void testAmtTooLargeShouldThrowException() {
PaymentAccount usdAccount = createDummyF2FAccount(alicedaemon, "US");
var req = CreateOfferRequest.newBuilder()
.setPaymentAccountId(usdAccount.getId())
.setDirection("buy")
.setCurrencyCode("usd")
.setAmount(100000000000L)
.setMinAmount(100000000000L)
.setUseMarketBasedPrice(false)
.setMarketPriceMargin(0.00)
.setPrice("10000.0000")
.setBuyerSecurityDeposit(getDefaultBuyerSecurityDepositAsPercent())
.setMakerFeeCurrencyCode("bsq")
.build();
PaymentAccount usdAccount = createDummyF2FAccount(aliceClient, "US");
@SuppressWarnings("ResultOfMethodCallIgnored")
Throwable exception = assertThrows(StatusRuntimeException.class, () ->
aliceStubs.offersService.createOffer(req).getOffer());
aliceClient.createFixedPricedOffer("buy",
"usd",
100000000000L, // exceeds amount limit
100000000000L,
"10000.0000",
getDefaultBuyerSecurityDepositAsPercent(),
usdAccount.getId(),
"bsq"));
assertEquals("UNKNOWN: An error occurred at task: ValidateOffer",
exception.getMessage());
}

View file

@ -5,8 +5,6 @@ import bisq.core.locale.Res;
import bisq.core.locale.TradeCurrency;
import bisq.core.payment.PaymentAccount;
import bisq.proto.grpc.GetPaymentAccountsRequest;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.stream.JsonWriter;
@ -29,7 +27,6 @@ import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestInfo;
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
import static java.lang.String.format;
import static java.lang.System.getProperty;
import static java.nio.charset.StandardCharsets.UTF_8;
@ -38,6 +35,7 @@ import static org.junit.jupiter.api.Assertions.*;
import bisq.apitest.method.MethodTest;
import bisq.cli.GrpcClient;
@Slf4j
public class AbstractPaymentAccountTest extends MethodTest {
@ -110,7 +108,7 @@ public class AbstractPaymentAccountTest extends MethodTest {
// would be skipped.
COMPLETED_FORM_MAP.clear();
File emptyForm = getPaymentAccountForm(alicedaemon, paymentMethodId);
File emptyForm = getPaymentAccountForm(aliceClient, paymentMethodId);
// A short cut over the API:
// File emptyForm = PAYMENT_ACCOUNT_FORM.getPaymentAccountForm(paymentMethodId);
log.debug("{} Empty form saved to {}",
@ -153,11 +151,10 @@ public class AbstractPaymentAccountTest extends MethodTest {
assertArrayEquals(expectedTradeCurrencies.toArray(), paymentAccount.getTradeCurrencies().toArray());
}
protected final void verifyUserPayloadHasPaymentAccountWithId(String paymentAccountId) {
var getPaymentAccountsRequest = GetPaymentAccountsRequest.newBuilder().build();
var reply = grpcStubs(alicedaemon)
.paymentAccountsService.getPaymentAccounts(getPaymentAccountsRequest);
Optional<protobuf.PaymentAccount> paymentAccount = reply.getPaymentAccountsList().stream()
protected final void verifyUserPayloadHasPaymentAccountWithId(GrpcClient grpcClient,
String paymentAccountId) {
Optional<protobuf.PaymentAccount> paymentAccount = grpcClient.getPaymentAccounts()
.stream()
.filter(a -> a.getId().equals(paymentAccountId))
.findFirst();
assertTrue(paymentAccount.isPresent());

View file

@ -99,8 +99,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "0000 1111 2222");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Advanced Cash Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
AdvancedCashAccount paymentAccount = (AdvancedCashAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
AdvancedCashAccount paymentAccount = (AdvancedCashAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountTradeCurrencies(getAllAdvancedCashCurrencies(), paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr());
@ -119,8 +119,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "2222 3333 4444");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
AliPayAccount paymentAccount = (AliPayAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
AliPayAccount paymentAccount = (AliPayAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("CNY", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr());
@ -139,8 +139,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_ACCOUNT_NAME, "Credit Union Australia");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Australia Pay ID Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
AustraliaPayid paymentAccount = (AustraliaPayid) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
AustraliaPayid paymentAccount = (AustraliaPayid) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("AUD", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_PAY_ID), paymentAccount.getPayid());
@ -180,8 +180,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_REQUIREMENTS, "Requirements...");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
CashDepositAccount paymentAccount = (CashDepositAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
CashDepositAccount paymentAccount = (CashDepositAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("EUR", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
@ -226,8 +226,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_NATIONAL_ACCOUNT_ID, "123456789");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Banco do Brasil Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
NationalBankAccount paymentAccount = (NationalBankAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
NationalBankAccount paymentAccount = (NationalBankAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("BRL", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
@ -259,8 +259,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "John Doe");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
ChaseQuickPayAccount paymentAccount = (ChaseQuickPayAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
ChaseQuickPayAccount paymentAccount = (ChaseQuickPayAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("USD", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail());
@ -281,8 +281,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jane Doe");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Zelle Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
ClearXchangeAccount paymentAccount = (ClearXchangeAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
ClearXchangeAccount paymentAccount = (ClearXchangeAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("USD", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL_OR_MOBILE_NR), paymentAccount.getEmailOrMobileNr());
@ -308,8 +308,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_EXTRA_INFO, "So fim de semana");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
F2FAccount paymentAccount = (F2FAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
F2FAccount paymentAccount = (F2FAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("BRL", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
@ -333,8 +333,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SORT_CODE, "3127");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Faster Payments Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
FasterPaymentsAccount paymentAccount = (FasterPaymentsAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
FasterPaymentsAccount paymentAccount = (FasterPaymentsAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("GBP", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr());
@ -354,8 +354,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_MOBILE_NR, "798 123 456");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
HalCashAccount paymentAccount = (HalCashAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
HalCashAccount paymentAccount = (HalCashAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("EUR", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_MOBILE_NR), paymentAccount.getMobileNr());
@ -379,8 +379,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ANSWER, "Fido");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Interac Transfer Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
InteracETransferAccount paymentAccount = (InteracETransferAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
InteracETransferAccount paymentAccount = (InteracETransferAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("CAD", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
@ -414,8 +414,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_ACCOUNT_NUMBER, "8100-8727-0000");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
JapanBankAccount paymentAccount = (JapanBankAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
JapanBankAccount paymentAccount = (JapanBankAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("JPY", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_CODE), paymentAccount.getBankCode());
@ -439,8 +439,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_ID, "MB 0000 1111");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Money Beam Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
MoneyBeamAccount paymentAccount = (MoneyBeamAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
MoneyBeamAccount paymentAccount = (MoneyBeamAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("EUR", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_ID), paymentAccount.getAccountId());
@ -465,8 +465,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_STATE, "NY");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
MoneyGramAccount paymentAccount = (MoneyGramAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
MoneyGramAccount paymentAccount = (MoneyGramAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountTradeCurrencies(getAllMoneyGramCurrencies(), paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getFullName());
@ -488,8 +488,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "PM 0000 1111");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Perfect Money Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
PerfectMoneyAccount paymentAccount = (PerfectMoneyAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
PerfectMoneyAccount paymentAccount = (PerfectMoneyAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("USD", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr());
@ -510,8 +510,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jane Doe");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
PopmoneyAccount paymentAccount = (PopmoneyAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
PopmoneyAccount paymentAccount = (PopmoneyAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("USD", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_ID), paymentAccount.getAccountId());
@ -530,8 +530,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PROMPT_PAY_ID, "PP 0000 1111");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Prompt Pay Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
PromptPayAccount paymentAccount = (PromptPayAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
PromptPayAccount paymentAccount = (PromptPayAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("THB", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_PROMPT_PAY_ID), paymentAccount.getPromptPayId());
@ -550,8 +550,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_USERNAME, "revolut123");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
RevolutAccount paymentAccount = (RevolutAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
RevolutAccount paymentAccount = (RevolutAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountTradeCurrencies(getAllRevolutCurrencies(), paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_USERNAME), paymentAccount.getUserName());
@ -583,8 +583,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_NATIONAL_ACCOUNT_ID, "123456789");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Same Bank Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
SameBankAccount paymentAccount = (SameBankAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
SameBankAccount paymentAccount = (SameBankAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("GBP", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
@ -620,8 +620,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BIC, "909");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
SepaInstantAccount paymentAccount = (SepaInstantAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
SepaInstantAccount paymentAccount = (SepaInstantAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
Objects.requireNonNull(paymentAccount.getCountry()).code);
verifyAccountSingleTradeCurrency("EUR", paymentAccount);
@ -651,8 +651,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BIC, "909");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Conta Sepa Salt"));
String jsonString = getCompletedFormAsJsonString();
SepaAccount paymentAccount = (SepaAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
SepaAccount paymentAccount = (SepaAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
Objects.requireNonNull(paymentAccount.getCountry()).code);
verifyAccountSingleTradeCurrency("EUR", paymentAccount);
@ -694,8 +694,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_NATIONAL_ACCOUNT_ID, "123456789");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
SpecificBanksAccount paymentAccount = (SpecificBanksAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
SpecificBanksAccount paymentAccount = (SpecificBanksAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("GBP", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
@ -726,8 +726,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Swish Acct Holder");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Swish Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
SwishAccount paymentAccount = (SwishAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
SwishAccount paymentAccount = (SwishAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("SEK", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_MOBILE_NR), paymentAccount.getMobileNr());
@ -747,8 +747,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "jan@doe.info");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
TransferwiseAccount paymentAccount = (TransferwiseAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
TransferwiseAccount paymentAccount = (TransferwiseAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
// As per commit 88f26f93241af698ae689bf081205d0f9dc929fa
// Do not autofill all currencies by default but keep all unselected.
// verifyAccountTradeCurrencies(getAllTransferwiseCurrencies(), paymentAccount);
@ -769,8 +769,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_ID, "UA 9876");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Uphold Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
UpholdAccount paymentAccount = (UpholdAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
UpholdAccount paymentAccount = (UpholdAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountTradeCurrencies(getAllUpholdCurrencies(), paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_ID), paymentAccount.getAccountId());
@ -791,8 +791,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_POSTAL_ADDRESS, "000 Westwood Terrace Austin, TX 78700");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
USPostalMoneyOrderAccount paymentAccount = (USPostalMoneyOrderAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
USPostalMoneyOrderAccount paymentAccount = (USPostalMoneyOrderAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("USD", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
@ -811,8 +811,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "WC 1234");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored WeChat Pay Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
WeChatPayAccount paymentAccount = (WeChatPayAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
WeChatPayAccount paymentAccount = (WeChatPayAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("CNY", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr());
@ -839,8 +839,8 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "jane@doe.info");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
WesternUnionAccount paymentAccount = (WesternUnionAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
WesternUnionAccount paymentAccount = (WesternUnionAccount) createPaymentAccount(aliceClient, jsonString);
verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId());
verifyAccountSingleTradeCurrency("USD", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getFullName());

View file

@ -41,7 +41,7 @@ public class GetPaymentMethodsTest extends MethodTest {
@Test
@Order(1)
public void testGetPaymentMethods() {
List<String> paymentMethodIds = getPaymentMethods(alicedaemon)
List<String> paymentMethodIds = aliceClient.getPaymentMethods()
.stream()
.map(PaymentMethod::getId)
.collect(Collectors.toList());

View file

@ -30,22 +30,14 @@ public class AbstractTradeTest extends AbstractOfferTest {
protected final TradeInfo takeAlicesOffer(String offerId,
String paymentAccountId,
String takerFeeCurrencyCode) {
return bobStubs.tradesService.takeOffer(
createTakeOfferRequest(offerId,
paymentAccountId,
takerFeeCurrencyCode))
.getTrade();
return bobClient.takeOffer(offerId, paymentAccountId, takerFeeCurrencyCode);
}
@SuppressWarnings("unused")
protected final TradeInfo takeBobsOffer(String offerId,
String paymentAccountId,
String takerFeeCurrencyCode) {
return aliceStubs.tradesService.takeOffer(
createTakeOfferRequest(offerId,
paymentAccountId,
takerFeeCurrencyCode))
.getTrade();
return aliceClient.takeOffer(offerId, paymentAccountId, takerFeeCurrencyCode);
}
protected final void verifyExpectedProtocolStatus(TradeInfo trade) {

View file

@ -32,9 +32,8 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
import static bisq.apitest.config.BisqAppConfig.bobdaemon;
import static bisq.cli.CurrencyFormat.formatSatoshis;
import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent;
import static bisq.core.trade.Trade.Phase.DEPOSIT_CONFIRMED;
import static bisq.core.trade.Trade.Phase.DEPOSIT_PUBLISHED;
import static bisq.core.trade.Trade.Phase.FIAT_SENT;
@ -61,11 +60,14 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest {
@Order(1)
public void testTakeAlicesBuyOffer(final TestInfo testInfo) {
try {
PaymentAccount alicesUsdAccount = createDummyF2FAccount(alicedaemon, "US");
var alicesOffer = createAliceOffer(alicesUsdAccount,
"buy",
PaymentAccount alicesUsdAccount = createDummyF2FAccount(aliceClient, "US");
var alicesOffer = aliceClient.createMarketBasedPricedOffer("buy",
"usd",
12500000,
12500000, // min-amount = amount
0.00,
getDefaultBuyerSecurityDepositAsPercent(),
alicesUsdAccount.getId(),
TRADE_FEE_CURRENCY_CODE);
var offerId = alicesOffer.getId();
assertFalse(alicesOffer.getIsCurrencyForMakerFeeBtc());
@ -73,10 +75,10 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest {
// Wait for Alice's AddToOfferBook task.
// Wait times vary; my logs show >= 2 second delay.
sleep(3000); // TODO loop instead of hard code wait time
var alicesUsdOffers = getMyOffersSortedByDate(aliceStubs, "buy", "usd");
var alicesUsdOffers = aliceClient.getMyOffersSortedByDate("buy", "usd");
assertEquals(1, alicesUsdOffers.size());
PaymentAccount bobsUsdAccount = createDummyF2FAccount(bobdaemon, "US");
PaymentAccount bobsUsdAccount = createDummyF2FAccount(bobClient, "US");
var trade = takeAlicesOffer(offerId, bobsUsdAccount.getId(), TRADE_FEE_CURRENCY_CODE);
assertNotNull(trade);
assertEquals(offerId, trade.getTradeId());
@ -85,10 +87,10 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest {
tradeId = trade.getTradeId();
genBtcBlocksThenWait(1, 1000);
alicesUsdOffers = getMyOffersSortedByDate(aliceStubs, "buy", "usd");
alicesUsdOffers = aliceClient.getMyOffersSortedByDate("buy", "usd");
assertEquals(0, alicesUsdOffers.size());
trade = getTrade(bobdaemon, trade.getTradeId());
trade = bobClient.getTrade(trade.getTradeId());
EXPECTED_PROTOCOL_STATUS.setState(SELLER_PUBLISHED_DEPOSIT_TX)
.setPhase(DEPOSIT_PUBLISHED)
.setDepositPublished(true);
@ -96,7 +98,7 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest {
logTrade(log, testInfo, "Bob's view after taking offer and sending deposit", trade);
genBtcBlocksThenWait(1, 1000);
trade = getTrade(bobdaemon, trade.getTradeId());
trade = bobClient.getTrade(trade.getTradeId());
EXPECTED_PROTOCOL_STATUS.setState(DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN)
.setPhase(DEPOSIT_CONFIRMED)
.setDepositConfirmed(true);
@ -111,11 +113,11 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest {
@Order(2)
public void testAlicesConfirmPaymentStarted(final TestInfo testInfo) {
try {
var trade = getTrade(alicedaemon, tradeId);
confirmPaymentStarted(alicedaemon, trade.getTradeId());
var trade = aliceClient.getTrade(tradeId);
aliceClient.confirmPaymentStarted(trade.getTradeId());
sleep(3000);
trade = getTrade(alicedaemon, tradeId);
trade = aliceClient.getTrade(tradeId);
assertEquals(OFFER_FEE_PAID.name(), trade.getOffer().getState());
EXPECTED_PROTOCOL_STATUS.setState(BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG)
.setPhase(FIAT_SENT)
@ -130,11 +132,11 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest {
@Test
@Order(3)
public void testBobsConfirmPaymentReceived(final TestInfo testInfo) {
var trade = getTrade(bobdaemon, tradeId);
confirmPaymentReceived(bobdaemon, trade.getTradeId());
var trade = bobClient.getTrade(tradeId);
bobClient.confirmPaymentReceived(trade.getTradeId());
sleep(3000);
trade = getTrade(bobdaemon, tradeId);
trade = bobClient.getTrade(tradeId);
// Note: offer.state == available
assertEquals(AVAILABLE.name(), trade.getOffer().getState());
EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG)
@ -150,19 +152,19 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest {
public void testAlicesKeepFunds(final TestInfo testInfo) {
genBtcBlocksThenWait(1, 1000);
var trade = getTrade(alicedaemon, tradeId);
var trade = aliceClient.getTrade(tradeId);
logTrade(log, testInfo, "Alice's view before keeping funds", trade);
keepFunds(alicedaemon, tradeId);
aliceClient.keepFunds(tradeId);
genBtcBlocksThenWait(1, 1000);
trade = getTrade(alicedaemon, tradeId);
trade = aliceClient.getTrade(tradeId);
EXPECTED_PROTOCOL_STATUS.setState(BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG)
.setPhase(PAYOUT_PUBLISHED);
verifyExpectedProtocolStatus(trade);
logTrade(log, testInfo, "Alice's view after keeping funds", trade);
BtcBalanceInfo currentBalance = getBtcBalances(bobdaemon);
BtcBalanceInfo currentBalance = aliceClient.getBtcBalances();
log.debug("{} Alice's current available balance: {} BTC",
testName(testInfo),
formatSatoshis(currentBalance.getAvailableBalance()));

View file

@ -32,9 +32,8 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
import static bisq.apitest.config.BisqAppConfig.bobdaemon;
import static bisq.cli.CurrencyFormat.formatSatoshis;
import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent;
import static bisq.core.trade.Trade.Phase.*;
import static bisq.core.trade.Trade.State.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ -60,11 +59,14 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
@Order(1)
public void testTakeAlicesSellOffer(final TestInfo testInfo) {
try {
PaymentAccount alicesUsdAccount = createDummyF2FAccount(alicedaemon, "US");
var alicesOffer = createAliceOffer(alicesUsdAccount,
"sell",
PaymentAccount alicesUsdAccount = createDummyF2FAccount(aliceClient, "US");
var alicesOffer = aliceClient.createMarketBasedPricedOffer("sell",
"usd",
12500000,
12500000L,
12500000L, // min-amount = amount
0.00,
getDefaultBuyerSecurityDepositAsPercent(),
alicesUsdAccount.getId(),
TRADE_FEE_CURRENCY_CODE);
var offerId = alicesOffer.getId();
assertTrue(alicesOffer.getIsCurrencyForMakerFeeBtc());
@ -73,10 +75,10 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
// Wait times vary; my logs show >= 2 second delay, but taking sell offers
// seems to require more time to prepare.
sleep(3000); // TODO loop instead of hard code wait time
var alicesUsdOffers = getMyOffersSortedByDate(aliceStubs, "sell", "usd");
var alicesUsdOffers = aliceClient.getMyOffersSortedByDate("sell", "usd");
assertEquals(1, alicesUsdOffers.size());
PaymentAccount bobsUsdAccount = createDummyF2FAccount(bobdaemon, "US");
PaymentAccount bobsUsdAccount = createDummyF2FAccount(bobClient, "US");
var trade = takeAlicesOffer(offerId, bobsUsdAccount.getId(), TRADE_FEE_CURRENCY_CODE);
assertNotNull(trade);
assertEquals(offerId, trade.getTradeId());
@ -85,10 +87,10 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
tradeId = trade.getTradeId();
genBtcBlocksThenWait(1, 4000);
var takeableUsdOffers = getOffersSortedByDate(bobStubs, "sell", "usd");
var takeableUsdOffers = bobClient.getOffersSortedByDate("sell", "usd");
assertEquals(0, takeableUsdOffers.size());
trade = getTrade(bobdaemon, trade.getTradeId());
trade = bobClient.getTrade(trade.getTradeId());
EXPECTED_PROTOCOL_STATUS.setState(BUYER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG)
.setPhase(DEPOSIT_PUBLISHED)
.setDepositPublished(true);
@ -97,7 +99,7 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
logTrade(log, testInfo, "Bob's view after taking offer and sending deposit", trade);
genBtcBlocksThenWait(1, 1000);
trade = getTrade(bobdaemon, trade.getTradeId());
trade = bobClient.getTrade(trade.getTradeId());
EXPECTED_PROTOCOL_STATUS.setState(DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN)
.setPhase(DEPOSIT_CONFIRMED)
.setDepositConfirmed(true);
@ -112,11 +114,11 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
@Order(2)
public void testBobsConfirmPaymentStarted(final TestInfo testInfo) {
try {
var trade = getTrade(bobdaemon, tradeId);
confirmPaymentStarted(bobdaemon, trade.getTradeId());
var trade = bobClient.getTrade(tradeId);
bobClient.confirmPaymentStarted(tradeId);
sleep(3000);
trade = getTrade(bobdaemon, tradeId);
trade = bobClient.getTrade(tradeId);
// Note: offer.state == available
assertEquals(AVAILABLE.name(), trade.getOffer().getState());
EXPECTED_PROTOCOL_STATUS.setState(BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG)
@ -132,11 +134,11 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
@Test
@Order(3)
public void testAlicesConfirmPaymentReceived(final TestInfo testInfo) {
var trade = getTrade(alicedaemon, tradeId);
confirmPaymentReceived(alicedaemon, trade.getTradeId());
var trade = aliceClient.getTrade(tradeId);
aliceClient.confirmPaymentReceived(trade.getTradeId());
sleep(3000);
trade = getTrade(alicedaemon, tradeId);
trade = aliceClient.getTrade(tradeId);
assertEquals(OFFER_FEE_PAID.name(), trade.getOffer().getState());
EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG)
.setPhase(PAYOUT_PUBLISHED)
@ -151,21 +153,21 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
public void testBobsBtcWithdrawalToExternalAddress(final TestInfo testInfo) {
genBtcBlocksThenWait(1, 1000);
var trade = getTrade(bobdaemon, tradeId);
var trade = bobClient.getTrade(tradeId);
logTrade(log, testInfo, "Bob's view before withdrawing funds to external wallet", trade);
String toAddress = bitcoinCli.getNewBtcAddress();
withdrawFunds(bobdaemon, tradeId, toAddress, WITHDRAWAL_TX_MEMO);
bobClient.withdrawFunds(tradeId, toAddress, WITHDRAWAL_TX_MEMO);
genBtcBlocksThenWait(1, 1000);
trade = getTrade(bobdaemon, tradeId);
trade = bobClient.getTrade(tradeId);
EXPECTED_PROTOCOL_STATUS.setState(WITHDRAW_COMPLETED)
.setPhase(WITHDRAWN)
.setWithdrawn(true);
verifyExpectedProtocolStatus(trade);
logTrade(log, testInfo, "Bob's view after withdrawing funds to external wallet", trade);
BtcBalanceInfo currentBalance = getBtcBalances(bobdaemon);
BtcBalanceInfo currentBalance = bobClient.getBtcBalances();
log.debug("{} Bob's current available balance: {} BTC",
testName(testInfo),
formatSatoshis(currentBalance.getAvailableBalance()));

View file

@ -37,6 +37,7 @@ import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import bisq.apitest.config.BisqAppConfig;
import bisq.apitest.method.MethodTest;
import bisq.cli.GrpcClient;
@Disabled
@Slf4j
@ -59,9 +60,7 @@ public class BsqWalletTest extends MethodTest {
@Test
@Order(1)
public void testGetUnusedBsqAddress() {
var request = createGetUnusedBsqAddressRequest();
String address = grpcStubs(alicedaemon).walletsService.getUnusedBsqAddress(request).getAddress();
var address = aliceClient.getUnusedBsqAddress();
assertFalse(address.isEmpty());
assertTrue(address.startsWith("B"));
@ -76,13 +75,13 @@ public class BsqWalletTest extends MethodTest {
@Test
@Order(2)
public void testInitialBsqBalances(final TestInfo testInfo) {
BsqBalanceInfo alicesBsqBalances = getBsqBalances(alicedaemon);
BsqBalanceInfo alicesBsqBalances = aliceClient.getBsqBalances();
log.debug("{} -> Alice's BSQ Initial Balances -> \n{}",
testName(testInfo),
formatBsqBalanceInfoTbl(alicesBsqBalances));
verifyBsqBalances(ALICES_INITIAL_BSQ_BALANCES, alicesBsqBalances);
BsqBalanceInfo bobsBsqBalances = getBsqBalances(bobdaemon);
BsqBalanceInfo bobsBsqBalances = bobClient.getBsqBalances();
log.debug("{} -> Bob's BSQ Initial Balances -> \n{}",
testName(testInfo),
formatBsqBalanceInfoTbl(bobsBsqBalances));
@ -92,12 +91,12 @@ public class BsqWalletTest extends MethodTest {
@Test
@Order(3)
public void testSendBsqAndCheckBalancesBeforeGeneratingBtcBlock(final TestInfo testInfo) {
String bobsBsqAddress = getUnusedBsqAddress(bobdaemon);
sendBsq(alicedaemon, bobsBsqAddress, SEND_BSQ_AMOUNT, "100");
String bobsBsqAddress = bobClient.getUnusedBsqAddress();
aliceClient.sendBsq(bobsBsqAddress, SEND_BSQ_AMOUNT, "100");
sleep(2000);
BsqBalanceInfo alicesBsqBalances = getBsqBalances(alicedaemon);
BsqBalanceInfo bobsBsqBalances = waitForNonZeroBsqUnverifiedBalance(bobdaemon);
BsqBalanceInfo alicesBsqBalances = aliceClient.getBsqBalances();
BsqBalanceInfo bobsBsqBalances = waitForNonZeroBsqUnverifiedBalance(bobClient);
log.debug("BSQ Balances Before BTC Block Gen...");
printBobAndAliceBsqBalances(testInfo,
@ -129,8 +128,8 @@ public class BsqWalletTest extends MethodTest {
// wait for both wallets to be saved to disk.
genBtcBlocksThenWait(1, 4000);
BsqBalanceInfo alicesBsqBalances = getBsqBalances(alicedaemon);
BsqBalanceInfo bobsBsqBalances = waitForBsqNewAvailableConfirmedBalance(bobdaemon, 150000000);
BsqBalanceInfo alicesBsqBalances = aliceClient.getBalances().getBsq();
BsqBalanceInfo bobsBsqBalances = waitForBsqNewAvailableConfirmedBalance(bobClient, 150000000);
log.debug("See Available Confirmed BSQ Balances...");
printBobAndAliceBsqBalances(testInfo,
@ -160,26 +159,26 @@ public class BsqWalletTest extends MethodTest {
tearDownScaffold();
}
private BsqBalanceInfo waitForNonZeroBsqUnverifiedBalance(BisqAppConfig daemon) {
private BsqBalanceInfo waitForNonZeroBsqUnverifiedBalance(GrpcClient grpcClient) {
// A BSQ recipient needs to wait for her daemon to detect a new tx.
// Loop here until her unverifiedBalance != 0, or give up after 15 seconds.
// A slow test is preferred over a flaky test.
BsqBalanceInfo bsqBalance = getBsqBalances(daemon);
BsqBalanceInfo bsqBalance = grpcClient.getBsqBalances();
for (int numRequests = 1; numRequests <= 15 && bsqBalance.getUnverifiedBalance() == 0; numRequests++) {
sleep(1000);
bsqBalance = getBsqBalances(daemon);
bsqBalance = grpcClient.getBsqBalances();
}
return bsqBalance;
}
private BsqBalanceInfo waitForBsqNewAvailableConfirmedBalance(BisqAppConfig daemon,
private BsqBalanceInfo waitForBsqNewAvailableConfirmedBalance(GrpcClient grpcClient,
long staleBalance) {
BsqBalanceInfo bsqBalance = getBsqBalances(daemon);
BsqBalanceInfo bsqBalance = grpcClient.getBsqBalances();
for (int numRequests = 1;
numRequests <= 15 && bsqBalance.getAvailableConfirmedBalance() == staleBalance;
numRequests++) {
sleep(1000);
bsqBalance = getBsqBalances(daemon);
bsqBalance = grpcClient.getBsqBalances();
}
return bsqBalance;
}

View file

@ -2,6 +2,8 @@ package bisq.apitest.method.wallet;
import bisq.core.api.model.TxFeeRateInfo;
import io.grpc.StatusRuntimeException;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
@ -15,8 +17,11 @@ import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind;
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
import static bisq.apitest.config.BisqAppConfig.seednode;
import static bisq.common.config.BaseCurrencyNetwork.BTC_DAO_REGTEST;
import static java.lang.String.format;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
@ -41,7 +46,7 @@ public class BtcTxFeeRateTest extends MethodTest {
@Test
@Order(1)
public void testGetTxFeeRate(final TestInfo testInfo) {
TxFeeRateInfo txFeeRateInfo = getTxFeeRate(alicedaemon);
var txFeeRateInfo = TxFeeRateInfo.fromProto(aliceClient.getTxFeeRate());
log.debug("{} -> Fee rate with no preference: {}", testName(testInfo), txFeeRateInfo);
assertFalse(txFeeRateInfo.isUseCustomTxFeeRate());
@ -50,19 +55,30 @@ public class BtcTxFeeRateTest extends MethodTest {
@Test
@Order(2)
public void testSetTxFeeRate(final TestInfo testInfo) {
TxFeeRateInfo txFeeRateInfo = setTxFeeRate(alicedaemon, 10);
log.debug("{} -> Fee rates with custom preference: {}", testName(testInfo), txFeeRateInfo);
assertTrue(txFeeRateInfo.isUseCustomTxFeeRate());
assertEquals(10, txFeeRateInfo.getCustomTxFeeRate());
assertTrue(txFeeRateInfo.getFeeServiceRate() > 0);
public void testSetInvalidTxFeeRateShouldThrowException(final TestInfo testInfo) {
Throwable exception = assertThrows(StatusRuntimeException.class, () ->
aliceClient.setTxFeeRate(10));
String expectedExceptionMessage =
format("UNKNOWN: tx fee rate preference must be >= %d sats/byte",
BTC_DAO_REGTEST.getDefaultMinFeePerVbyte());
assertEquals(expectedExceptionMessage, exception.getMessage());
}
@Test
@Order(3)
public void testSetValidTxFeeRate(final TestInfo testInfo) {
var txFeeRateInfo = TxFeeRateInfo.fromProto(aliceClient.setTxFeeRate(15));
log.debug("{} -> Fee rates with custom preference: {}", testName(testInfo), txFeeRateInfo);
assertTrue(txFeeRateInfo.isUseCustomTxFeeRate());
assertEquals(15, txFeeRateInfo.getCustomTxFeeRate());
assertTrue(txFeeRateInfo.getFeeServiceRate() > 0);
}
@Test
@Order(4)
public void testUnsetTxFeeRate(final TestInfo testInfo) {
TxFeeRateInfo txFeeRateInfo = unsetTxFeeRate(alicedaemon);
var txFeeRateInfo = TxFeeRateInfo.fromProto(aliceClient.unsetTxFeeRate());
log.debug("{} -> Fee rate with no preference: {}", testName(testInfo), txFeeRateInfo);
assertFalse(txFeeRateInfo.isUseCustomTxFeeRate());

View file

@ -53,10 +53,10 @@ public class BtcWalletTest extends MethodTest {
public void testInitialBtcBalances(final TestInfo testInfo) {
// Bob & Alice's regtest Bisq wallets were initialized with 10 BTC.
BtcBalanceInfo alicesBalances = getBtcBalances(alicedaemon);
BtcBalanceInfo alicesBalances = aliceClient.getBtcBalances();
log.debug("{} Alice's BTC Balances:\n{}", testName(testInfo), formatBtcBalanceInfoTbl(alicesBalances));
BtcBalanceInfo bobsBalances = getBtcBalances(bobdaemon);
BtcBalanceInfo bobsBalances = bobClient.getBtcBalances();
log.debug("{} Bob's BTC Balances:\n{}", testName(testInfo), formatBtcBalanceInfoTbl(bobsBalances));
assertEquals(INITIAL_BTC_BALANCES.getAvailableBalance(), alicesBalances.getAvailableBalance());
@ -66,20 +66,20 @@ public class BtcWalletTest extends MethodTest {
@Test
@Order(2)
public void testFundAlicesBtcWallet(final TestInfo testInfo) {
String newAddress = getUnusedBtcAddress(alicedaemon);
String newAddress = aliceClient.getUnusedBtcAddress();
bitcoinCli.sendToAddress(newAddress, "2.5");
genBtcBlocksThenWait(1, 1000);
BtcBalanceInfo btcBalanceInfo = getBtcBalances(alicedaemon);
BtcBalanceInfo btcBalanceInfo = aliceClient.getBtcBalances();
// New balance is 12.5 BTC
assertEquals(1250000000, btcBalanceInfo.getAvailableBalance());
log.debug("{} -> Alice's Funded Address Balance -> \n{}",
testName(testInfo),
formatAddressBalanceTbl(singletonList(getAddressBalance(alicedaemon, newAddress))));
formatAddressBalanceTbl(singletonList(aliceClient.getAddressBalance(newAddress))));
// New balance is 12.5 BTC
btcBalanceInfo = getBtcBalances(alicedaemon);
btcBalanceInfo = aliceClient.getBtcBalances();
bisq.core.api.model.BtcBalanceInfo alicesExpectedBalances =
bisq.core.api.model.BtcBalanceInfo.valueOf(1250000000,
0,
@ -94,11 +94,10 @@ public class BtcWalletTest extends MethodTest {
@Test
@Order(3)
public void testAliceSendBTCToBob(TestInfo testInfo) {
String bobsBtcAddress = getUnusedBtcAddress(bobdaemon);
String bobsBtcAddress = bobClient.getUnusedBtcAddress();
log.debug("Sending 5.5 BTC From Alice to Bob @ {}", bobsBtcAddress);
TxInfo txInfo = sendBtc(alicedaemon,
bobsBtcAddress,
TxInfo txInfo = aliceClient.sendBtc(bobsBtcAddress,
"5.50",
"100",
TX_MEMO);
@ -109,11 +108,11 @@ public class BtcWalletTest extends MethodTest {
genBtcBlocksThenWait(1, 1000);
// Fetch the tx and check for confirmation and memo.
txInfo = getTransaction(alicedaemon, txInfo.getTxId());
txInfo = aliceClient.getTransaction(txInfo.getTxId());
assertFalse(txInfo.getIsPending());
assertEquals(TX_MEMO, txInfo.getMemo());
BtcBalanceInfo alicesBalances = getBtcBalances(alicedaemon);
BtcBalanceInfo alicesBalances = aliceClient.getBtcBalances();
log.debug("{} Alice's BTC Balances:\n{}",
testName(testInfo),
formatBtcBalanceInfoTbl(alicesBalances));
@ -124,7 +123,7 @@ public class BtcWalletTest extends MethodTest {
0);
verifyBtcBalances(alicesExpectedBalances, alicesBalances);
BtcBalanceInfo bobsBalances = getBtcBalances(bobdaemon);
BtcBalanceInfo bobsBalances = bobClient.getBtcBalances();
log.debug("{} Bob's BTC Balances:\n{}",
testName(testInfo),
formatBtcBalanceInfoTbl(bobsBalances));

View file

@ -41,94 +41,83 @@ public class WalletProtectionTest extends MethodTest {
@Test
@Order(1)
public void testSetWalletPassword() {
var request = createSetWalletPasswordRequest("first-password");
grpcStubs(alicedaemon).walletsService.setWalletPassword(request);
aliceClient.setWalletPassword("first-password");
}
@Test
@Order(2)
public void testGetBalanceOnEncryptedWalletShouldThrowException() {
Throwable exception = assertThrows(StatusRuntimeException.class, () -> getBtcBalances(alicedaemon));
Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getBtcBalances());
assertEquals("UNKNOWN: wallet is locked", exception.getMessage());
}
@Test
@Order(3)
public void testUnlockWalletFor4Seconds() {
var request = createUnlockWalletRequest("first-password", 4);
grpcStubs(alicedaemon).walletsService.unlockWallet(request);
getBtcBalances(alicedaemon); // should not throw 'wallet locked' exception
aliceClient.unlockWallet("first-password", 4);
aliceClient.getBtcBalances(); // should not throw 'wallet locked' exception
sleep(4500); // let unlock timeout expire
Throwable exception = assertThrows(StatusRuntimeException.class, () -> getBtcBalances(alicedaemon));
Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getBtcBalances());
assertEquals("UNKNOWN: wallet is locked", exception.getMessage());
}
@Test
@Order(4)
public void testGetBalanceAfterUnlockTimeExpiryShouldThrowException() {
var request = createUnlockWalletRequest("first-password", 3);
grpcStubs(alicedaemon).walletsService.unlockWallet(request);
aliceClient.unlockWallet("first-password", 3);
sleep(4000); // let unlock timeout expire
Throwable exception = assertThrows(StatusRuntimeException.class, () -> getBtcBalances(alicedaemon));
Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getBtcBalances());
assertEquals("UNKNOWN: wallet is locked", exception.getMessage());
}
@Test
@Order(5)
public void testLockWalletBeforeUnlockTimeoutExpiry() {
unlockWallet(alicedaemon, "first-password", 60);
var request = createLockWalletRequest();
grpcStubs(alicedaemon).walletsService.lockWallet(request);
Throwable exception = assertThrows(StatusRuntimeException.class, () -> getBtcBalances(alicedaemon));
aliceClient.unlockWallet("first-password", 60);
aliceClient.lockWallet();
Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getBtcBalances());
assertEquals("UNKNOWN: wallet is locked", exception.getMessage());
}
@Test
@Order(6)
public void testLockWalletWhenWalletAlreadyLockedShouldThrowException() {
var request = createLockWalletRequest();
Throwable exception = assertThrows(StatusRuntimeException.class, () ->
grpcStubs(alicedaemon).walletsService.lockWallet(request));
Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.lockWallet());
assertEquals("UNKNOWN: wallet is already locked", exception.getMessage());
}
@Test
@Order(7)
public void testUnlockWalletTimeoutOverride() {
unlockWallet(alicedaemon, "first-password", 2);
aliceClient.unlockWallet("first-password", 2);
sleep(500); // override unlock timeout after 0.5s
unlockWallet(alicedaemon, "first-password", 6);
aliceClient.unlockWallet("first-password", 6);
sleep(5000);
getBtcBalances(alicedaemon); // getbalance 5s after overriding timeout to 6s
aliceClient.getBtcBalances(); // getbalance 5s after overriding timeout to 6s
}
@Test
@Order(8)
public void testSetNewWalletPassword() {
var request = createSetWalletPasswordRequest(
"first-password", "second-password");
grpcStubs(alicedaemon).walletsService.setWalletPassword(request);
unlockWallet(alicedaemon, "second-password", 2);
getBtcBalances(alicedaemon);
aliceClient.setWalletPassword("first-password", "second-password");
sleep(2500); // allow time for wallet save
aliceClient.unlockWallet("second-password", 2);
aliceClient.getBtcBalances();
}
@Test
@Order(9)
public void testSetNewWalletPasswordWithIncorrectNewPasswordShouldThrowException() {
var request = createSetWalletPasswordRequest(
"bad old password", "irrelevant");
Throwable exception = assertThrows(StatusRuntimeException.class, () ->
grpcStubs(alicedaemon).walletsService.setWalletPassword(request));
aliceClient.setWalletPassword("bad old password", "irrelevant"));
assertEquals("UNKNOWN: incorrect old password", exception.getMessage());
}
@Test
@Order(10)
public void testRemoveNewWalletPassword() {
var request = createRemoveWalletPasswordRequest("second-password");
grpcStubs(alicedaemon).walletsService.removeWalletPassword(request);
getBtcBalances(alicedaemon); // should not throw 'wallet locked' exception
aliceClient.removeWalletPassword("second-password");
aliceClient.getBtcBalances(); // should not throw 'wallet locked' exception
}
@AfterAll

View file

@ -13,6 +13,7 @@ import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind;
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
import static bisq.apitest.config.BisqAppConfig.seednode;
import static java.util.Objects.requireNonNull;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
@ -80,7 +81,8 @@ public class PaymentAccountTest extends AbstractPaymentAccountTest {
test.testCreateWeChatPayAccount(testInfo);
test.testCreateWesternUnionAccount(testInfo);
assertEquals(EXPECTED_NUM_PAYMENT_ACCOUNTS, getPaymentAccounts(alicedaemon).size());
var paymentAccounts = requireNonNull(aliceClient).getPaymentAccounts();
assertEquals(EXPECTED_NUM_PAYMENT_ACCOUNTS, paymentAccounts.size());
}
@AfterAll

View file

@ -0,0 +1,121 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.scenario;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.condition.EnabledIf;
import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind;
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
import static bisq.apitest.config.BisqAppConfig.arbdaemon;
import static bisq.apitest.config.BisqAppConfig.bobdaemon;
import static bisq.apitest.config.BisqAppConfig.seednode;
import static bisq.apitest.scenario.bot.shutdown.ManualShutdown.startShutdownTimer;
import static org.junit.jupiter.api.Assertions.fail;
import bisq.apitest.config.ApiTestConfig;
import bisq.apitest.method.BitcoinCliHelper;
import bisq.apitest.scenario.bot.AbstractBotTest;
import bisq.apitest.scenario.bot.BotClient;
import bisq.apitest.scenario.bot.RobotBob;
import bisq.apitest.scenario.bot.script.BashScriptGenerator;
import bisq.apitest.scenario.bot.shutdown.ManualBotShutdownException;
// The test case is enabled if AbstractBotTest#botScriptExists() returns true.
@EnabledIf("botScriptExists")
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class ScriptedBotTest extends AbstractBotTest {
private RobotBob robotBob;
@BeforeAll
public static void startTestHarness() {
botScript = deserializeBotScript();
if (botScript.isUseTestHarness()) {
startSupportingApps(true,
true,
bitcoind,
seednode,
arbdaemon,
alicedaemon,
bobdaemon);
} else {
// We need just enough configurations to make sure Bob and testers use
// the right apiPassword, to create a bitcoin-cli helper, and RobotBob's
// gRPC stubs. But the user will have to register dispute agents before
// an offer can be taken.
config = new ApiTestConfig("--apiPassword", "xyz");
bitcoinCli = new BitcoinCliHelper(config);
log.warn("Don't forget to register dispute agents before trying to trade with me.");
}
botClient = new BotClient(bobClient);
}
@BeforeEach
public void initRobotBob() {
try {
BashScriptGenerator bashScriptGenerator = getBashScriptGenerator();
robotBob = new RobotBob(botClient, botScript, bitcoinCli, bashScriptGenerator);
} catch (Exception ex) {
fail(ex);
}
}
@Test
@Order(1)
public void runRobotBob() {
try {
startShutdownTimer();
robotBob.run();
} catch (ManualBotShutdownException ex) {
// This exception is thrown if a /tmp/bottest-shutdown file was found.
// You can also kill -15 <pid>
// of worker.org.gradle.process.internal.worker.GradleWorkerMain 'Gradle Test Executor #'
//
// This will cleanly shut everything down as well, but you will see a
// Process 'Gradle Test Executor #' finished with non-zero exit value 143 error,
// which you may think is a test failure.
log.warn("{} Shutting down test case before test completion;"
+ " this is not a test failure.",
ex.getMessage());
} catch (Throwable throwable) {
fail(throwable);
}
}
@AfterAll
public static void tearDown() {
if (botScript.isUseTestHarness())
tearDownScaffold();
}
}

View file

@ -104,7 +104,8 @@ public class WalletTest extends MethodTest {
BtcTxFeeRateTest test = new BtcTxFeeRateTest();
test.testGetTxFeeRate(testInfo);
test.testSetTxFeeRate(testInfo);
test.testSetInvalidTxFeeRateShouldThrowException(testInfo);
test.testSetValidTxFeeRate(testInfo);
test.testUnsetTxFeeRate(testInfo);
}

View file

@ -0,0 +1,110 @@
/*
* 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.getPaymentMethodById;
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.",
getPaymentMethodById(paymentMethodId).getDisplayString()));
}
} else {
String countryCode = botScript.getCountryCode();
Country country = findCountryByCode(countryCode).orElseThrow(() ->
new IllegalArgumentException(countryCode + " is not a valid iso country code."));
return accountGenerator.createF2FPaymentAccount(country,
"Alice's " + country.name + " F2F Account");
}
}
protected static BotScript deserializeBotScript() {
try {
File botScriptFile = new File(getProperty("java.io.tmpdir"), BOT_SCRIPT_NAME);
String json = new String(readAllBytes(Paths.get(botScriptFile.getPath())));
return new GsonBuilder().setPrettyPrinting().create().fromJson(json, BotScript.class);
} catch (IOException ex) {
throw new IllegalStateException("Error reading script bot file contents.", ex);
}
}
@SuppressWarnings("unused") // This is used by the jupiter framework.
protected static boolean botScriptExists() {
File botScriptFile = new File(getProperty("java.io.tmpdir"), BOT_SCRIPT_NAME);
if (botScriptFile.exists()) {
botScriptFile.deleteOnExit();
log.info("Enabled, found {}.", botScriptFile.getPath());
return true;
} else {
log.info("Skipped, no bot script.\n\tTo generate a bot-script.json file, see BotScriptGenerator.");
return false;
}
}
}

View file

@ -0,0 +1,77 @@
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.getPaymentMethodById;
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.",
getPaymentMethodById(paymentMethodId).getDisplayString()));
}
} else {
Country country = findCountry(botScript.getCountryCode());
return accountGenerator.createF2FPaymentAccount(country, country.name + " F2F Account");
}
}
private Country findCountry(String countryCode) {
return findCountryByCode(countryCode).orElseThrow(() ->
new IllegalArgumentException(countryCode + " is not a valid iso country code."));
}
}

View file

@ -0,0 +1,339 @@
/*
* 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.
*
* TODO Consider if the duplication smell is bad enough to force a BotClient user
* to use the GrpcClient instead (and delete this class). But right now, I think it is
* OK because moving some of the non-gRPC related methods to GrpcClient is even smellier.
*
*/
@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
* @return OfferInfo
*/
public OfferInfo createOfferAtMarketBasedPrice(PaymentAccount paymentAccount,
String direction,
String currencyCode,
long amountInSatoshis,
long minAmountInSatoshis,
double priceMarginAsPercent,
double securityDepositAsPercent,
String feeCurrency) {
return grpcClient.createMarketBasedPricedOffer(direction,
currencyCode,
amountInSatoshis,
minAmountInSatoshis,
priceMarginAsPercent,
securityDepositAsPercent,
paymentAccount.getId(),
feeCurrency);
}
/**
* 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);
}
/**
* 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).getIsFiatSent();
}
/**
* 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).getIsFiatReceived();
}
/**
* 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 'keep funds in wallet message' for a trade with the given tradeId,
* or throws an exception.
* @param tradeId
*/
public void sendKeepFundsMessage(String tradeId) {
grpcClient.keepFunds(tradeId);
}
/**
* Create and save a new PaymentAccount with details in the given json.
* @param json
* @return PaymentAccount
*/
public PaymentAccount createNewPaymentAccount(String json) {
return grpcClient.createPaymentAccount(json);
}
/**
* Returns a persisted PaymentAccount with the given paymentAccountId, or throws
* an exception.
* @param paymentAccountId The id of the PaymentAccount being looked up.
* @return PaymentAccount
*/
public PaymentAccount getPaymentAccount(String paymentAccountId) {
return grpcClient.getPaymentAccounts().stream()
.filter(a -> (a.getId().equals(paymentAccountId)))
.findFirst()
.orElseThrow(() ->
new PaymentAccountNotFoundException("Could not find a payment account with id "
+ paymentAccountId + "."));
}
/**
* Returns a persisted PaymentAccount with the given accountName, or throws
* an exception.
* @param accountName
* @return PaymentAccount
*/
public PaymentAccount getPaymentAccountWithName(String accountName) {
var req = GetPaymentAccountsRequest.newBuilder().build();
return grpcClient.getPaymentAccounts().stream()
.filter(a -> (a.getAccountName().equals(accountName)))
.findFirst()
.orElseThrow(() ->
new PaymentAccountNotFoundException("Could not find a payment account with name "
+ accountName + "."));
}
public String toCleanGrpcExceptionMessage(Exception ex) {
return capitalize(ex.getMessage().replaceFirst("^[A-Z_]+: ", ""));
}
}

View file

@ -0,0 +1,68 @@
package bisq.apitest.scenario.bot;
import bisq.core.api.model.PaymentAccountForm;
import bisq.core.locale.Country;
import protobuf.PaymentAccount;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.File;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import static bisq.core.payment.payload.PaymentMethod.CLEAR_X_CHANGE_ID;
import static bisq.core.payment.payload.PaymentMethod.F2F_ID;
@Slf4j
public class BotPaymentAccountGenerator {
private final Gson gson = new GsonBuilder().setPrettyPrinting().serializeNulls().create();
private final BotClient botClient;
public BotPaymentAccountGenerator(BotClient botClient) {
this.botClient = botClient;
}
public PaymentAccount createF2FPaymentAccount(Country country, String accountName) {
try {
return botClient.getPaymentAccountWithName(accountName);
} catch (PaymentAccountNotFoundException ignored) {
// Ignore not found exception, create a new account.
}
Map<String, Object> p = getPaymentAccountFormMap(F2F_ID);
p.put("accountName", accountName);
p.put("city", country.name + " City");
p.put("country", country.code);
p.put("contact", "By Semaphore");
p.put("extraInfo", "");
// Convert the map back to a json string and create the payment account over gRPC.
return botClient.createNewPaymentAccount(gson.toJson(p));
}
public PaymentAccount createZellePaymentAccount(String accountName, String holderName) {
try {
return botClient.getPaymentAccountWithName(accountName);
} catch (PaymentAccountNotFoundException ignored) {
// Ignore not found exception, create a new account.
}
Map<String, Object> p = getPaymentAccountFormMap(CLEAR_X_CHANGE_ID);
p.put("accountName", accountName);
p.put("emailOrMobileNr", holderName + "@zelle.com");
p.put("holderName", holderName);
return botClient.createNewPaymentAccount(gson.toJson(p));
}
private Map<String, Object> getPaymentAccountFormMap(String paymentMethodId) {
PaymentAccountForm paymentAccountForm = new PaymentAccountForm();
File jsonFormTemplate = paymentAccountForm.getPaymentAccountForm(paymentMethodId);
jsonFormTemplate.deleteOnExit();
String jsonString = paymentAccountForm.toJsonString(jsonFormTemplate);
//noinspection unchecked
return (Map<String, Object>) gson.fromJson(jsonString, Object.class);
}
}

View file

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

View file

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

View file

@ -0,0 +1,177 @@
/*
* 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.cli.CurrencyFormat.formatMarketPrice;
import static bisq.cli.CurrencyFormat.formatSatoshis;
import static bisq.common.util.MathUtils.scaleDownByPowerOf10;
import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent;
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,
getDefaultBuyerSecurityDepositAsPercent(),
feeCurrency);
} else {
this.offer = botClient.createOfferAtFixedPrice(paymentAccount,
direction,
currencyCode,
amount,
minAmount,
fixedOfferPrice,
getDefaultBuyerSecurityDepositAsPercent(),
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 = {} {}", formatMarketPrice(Double.parseDouble(fixedOfferPrice)), currencyCode);
} else {
log.info("Fixed Offer Price = {} {}", fixedOfferPrice, currencyCode);
}
log.info("Current Market Price = {} {}", formatMarketPrice(currentMarketPrice), currencyCode);
}
}

View file

@ -0,0 +1,141 @@
/*
* 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.TableFormat.formatBalancesTbls;
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;
@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.");
}
log.info("Completed {} successful trade{}. Current Balance:\n{}",
++numTrades,
numTrades == 1 ? "" : "s",
formatBalancesTbls(botClient.getBalance()));
if (numTrades < actions.length) {
try {
SECONDS.sleep(20);
} catch (InterruptedException ignored) {
// empty
}
}
} // end of actions loop
if (stayAlive)
waitForManualShutdown();
else
warnCLIUserBeforeShutdown();
}
private void checkActionIsValid(String action) {
if (!action.equalsIgnoreCase(MAKE) && !action.equalsIgnoreCase(TAKE))
throw new IllegalStateException(action + " is not a valid bot action; must be 'make' or 'take'");
}
private void waitForManualShutdown() {
String harnessOrCase = isUsingTestHarness ? "harness" : "case";
log.info("All script actions have been completed, but the test {} will stay alive"
+ " until a /tmp/bottest-shutdown file is detected.",
harnessOrCase);
log.info("When ready to shutdown the test {}, run '$ touch /tmp/bottest-shutdown'.",
harnessOrCase);
if (!isUsingTestHarness) {
log.warn("You will have to manually shutdown the bitcoind and Bisq nodes"
+ " running outside of the test harness.");
}
try {
while (!isShutdownCalled()) {
SECONDS.sleep(10);
}
log.warn("Manual shutdown signal received.");
} catch (ManualBotShutdownException ex) {
log.warn(ex.getMessage());
} catch (InterruptedException ignored) {
// empty
}
}
private void warnCLIUserBeforeShutdown() {
if (isUsingTestHarness) {
long delayInSeconds = 30;
log.warn("All script actions have been completed. You have {} seconds to complete any"
+ " remaining tasks before the test harness shuts down.",
delayInSeconds);
try {
SECONDS.sleep(delayInSeconds);
} catch (InterruptedException ignored) {
// empty
}
} else {
log.info("Shutting down test case");
}
}
}

View file

@ -0,0 +1,349 @@
/*
* 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 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.TradeFormat;
@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.getIsFiatSent()) {
log.info("Buyer has started payment for trade:\n{}", TradeFormat.format(t));
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.getIsFiatReceived()) {
log.info("Seller has received payment for trade:\n{}", TradeFormat.format(t));
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(),
TradeFormat.format(t));
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> keepFundsFromTrade = (trade) -> {
initProtocolStep.accept(KEEP_FUNDS);
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 'keep funds' command.");
this.getBotClient().sendKeepFundsMessage(trade.getTradeId());
return trade;
};
protected void createPaymentStartedScript(TradeInfo trade) {
File script = bashScriptGenerator.createPaymentStartedScript(trade);
printCliHintAndOrScript(script, "The manual CLI side can send a 'payment started' message");
}
protected void createPaymentReceivedScript(TradeInfo trade) {
File script = bashScriptGenerator.createPaymentReceivedScript(trade);
printCliHintAndOrScript(script, "The manual CLI side can sent a 'payment received confirmation' message");
}
protected void createKeepFundsScript(TradeInfo trade) {
File script = bashScriptGenerator.createKeepFundsScript(trade);
printCliHintAndOrScript(script, "The manual CLI side can close the trade");
}
protected void createGetBalanceScript() {
File script = bashScriptGenerator.createGetBalanceScript();
printCliHintAndOrScript(script, "The manual CLI side can view current balances");
}
protected void createGenerateBtcBlockScript() {
String newBitcoinCoreAddress = bitcoinCli.getNewBtcAddress();
File script = bashScriptGenerator.createGenerateBtcBlockScript(newBitcoinCoreAddress);
printCliHintAndOrScript(script, "The manual CLI side can generate 1 btc block");
}
protected void printCliHintAndOrScript(File script, String hint) {
log.info("{} by running bash script '{}'.", hint, script.getAbsolutePath());
if (this.getBashScriptGenerator().isPrintCliScripts())
this.getBashScriptGenerator().printCliScript(script, log);
sleep(5000); // Allow 5s for CLI user to read the hint.
}
protected void sleep(long ms) {
try {
MILLISECONDS.sleep(ms);
} catch (InterruptedException ignored) {
// empty
}
}
private void waitForTakerFeeTxPublished(String tradeId) {
waitForTakerDepositFee(tradeId, WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED);
}
private void waitForTakerFeeTxConfirmed(String tradeId) {
waitForTakerDepositFee(tradeId, WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED);
}
private void waitForTakerDepositFee(String tradeId, ProtocolStep depositTxProtocolStep) {
initProtocolStep.accept(depositTxProtocolStep);
validateCurrentProtocolStep(WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED, WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED);
try {
log.info(waitingForDepositFeeTxMsg(tradeId));
while (isWithinProtocolStepTimeLimit()) {
checkIfShutdownCalled("Interrupted before checking taker deposit fee tx is published and confirmed.");
try {
var trade = this.getBotClient().getTrade(tradeId);
if (isDepositFeeTxStepComplete.test(trade))
return;
else
sleep(randomDelay.get());
} catch (Exception ex) {
if (this.getBotClient().tradeContractIsNotReady.test(ex, tradeId))
sleep(randomDelay.get());
else
throw new IllegalStateException(this.getBotClient().toCleanGrpcExceptionMessage(ex));
}
} // end while
throw new IllegalStateException(stoppedWaitingForDepositFeeTxMsg(this.getBotClient().getTrade(tradeId).getDepositTxId()));
} catch (ManualBotShutdownException ex) {
throw ex; // not an error, tells bot to shutdown
} catch (Exception ex) {
throw new IllegalStateException("Error while waiting for taker deposit tx to be published or confirmed.", ex);
}
}
private final Predicate<TradeInfo> isDepositFeeTxStepComplete = (trade) -> {
if (currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED) && trade.getIsDepositPublished()) {
log.info("Taker deposit fee tx {} has been published.", trade.getDepositTxId());
return true;
} else if (currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED) && trade.getIsDepositConfirmed()) {
log.info("Taker deposit fee tx {} has been confirmed.", trade.getDepositTxId());
return true;
} else {
return false;
}
};
private void validateCurrentProtocolStep(Enum<?>... validBotSteps) {
for (Enum<?> validBotStep : validBotSteps) {
if (currentProtocolStep.equals(validBotStep))
return;
}
throw new IllegalStateException("Unexpected bot step: " + currentProtocolStep.name() + ".\n"
+ "Must be one of "
+ stream(validBotSteps).map((Enum::name)).collect(Collectors.joining(","))
+ ".");
}
private String waitingForDepositFeeTxMsg(String tradeId) {
return format("Waiting for taker deposit fee tx for trade %s to be %s.",
tradeId,
currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED) ? "published" : "confirmed");
}
private String stoppedWaitingForDepositFeeTxMsg(String txId) {
return format("Taker deposit fee tx %s is took too long to be %s; we won't wait any longer.",
txId,
currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED) ? "published" : "confirmed");
}
}

View file

@ -0,0 +1,114 @@
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.TableFormat.formatOfferTable;
import static java.util.Collections.singletonList;
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.TradeFormat;
@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(keepFundsFromTrade);
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, formatOfferTable(singletonList(offer), currencyCode));
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, TradeFormat.format(trade));
return Optional.of(trade);
} catch (Exception ex) {
// Get trade will throw a non-fatal gRPC exception if not found.
log.info(this.getBotClient().toCleanGrpcExceptionMessage(ex));
return Optional.empty();
}
}
private void createTakeOfferCliScript(OfferInfo offer) {
File script = bashScriptGenerator.createTakeOfferScript(offer);
printCliHintAndOrScript(script, "The manual CLI side can take the offer");
}
}

View file

@ -0,0 +1,17 @@
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,
KEEP_FUNDS,
DONE
}

View file

@ -0,0 +1,136 @@
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.TableFormat.formatOfferTable;
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;
@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(keepFundsFromTrade);
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{}", formatOfferTable(offers, currencyCode));
OfferInfo offer = offers.get(0);
log.info("Will take first offer {}", offer.getId());
return Optional.of(offer);
} else {
log.info("No buy or sell {} offers found.", currencyCode);
return Optional.empty();
}
};
private final Supplier<OfferInfo> findOffer = () -> {
initProtocolStep.accept(FIND_OFFER);
createMakeOfferScript();
try {
log.info("Impatiently waiting for at least one {} offer to be created, repeatedly calling getoffers.", currencyCode);
while (isWithinProtocolStepTimeLimit()) {
checkIfShutdownCalled("Interrupted while checking offers.");
try {
Optional<OfferInfo> offer = firstOffer.get();
if (offer.isPresent())
return offer.get();
else
sleep(randomDelay.get());
} catch (Exception ex) {
throw new IllegalStateException(this.getBotClient().toCleanGrpcExceptionMessage(ex), ex);
}
} // end while
throw new IllegalStateException("Offer was never created; we won't wait any longer.");
} catch (ManualBotShutdownException ex) {
throw ex; // not an error, tells bot to shutdown
} catch (Exception ex) {
throw new IllegalStateException("Error while waiting for a new offer.", ex);
}
};
private final Function<OfferInfo, TradeInfo> takeOffer = (offer) -> {
initProtocolStep.accept(TAKE_OFFER);
checkIfShutdownCalled("Interrupted before taking offer.");
String feeCurrency = RANDOM.nextBoolean() ? "BSQ" : "BTC";
return botClient.takeOffer(offer.getId(), paymentAccount, feeCurrency);
};
private void createMakeOfferScript() {
String direction = RANDOM.nextBoolean() ? "BUY" : "SELL";
String feeCurrency = RANDOM.nextBoolean() ? "BSQ" : "BTC";
boolean createMarginPricedOffer = RANDOM.nextBoolean();
// If not using an F2F account, don't go over possible 0.01 BTC
// limit if account is not signed.
String amount = paymentAccount.getPaymentMethod().getId().equals(F2F_ID)
? "0.25"
: "0.01";
File script;
if (createMarginPricedOffer) {
script = bashScriptGenerator.createMakeMarginPricedOfferScript(direction,
currencyCode,
amount,
"0.0",
"15.0",
feeCurrency);
} else {
script = bashScriptGenerator.createMakeFixedPricedOfferScript(direction,
currencyCode,
amount,
botClient.getCurrentBTCMarketPriceAsIntegerString(currencyCode),
"15.0",
feeCurrency);
}
printCliHintAndOrScript(script, "The manual CLI side can create an offer");
}
}

View file

@ -0,0 +1,235 @@
/*
* 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 keepfunds --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("keepfunds.sh",
paymentStartedCmd,
"sleep 2",
getTradeCmd,
getBalanceCmd);
}
public File createGetBalanceScript() {
String getBalanceCmd = format("%s getbalance", cliBase);
return createCliScript("getbalance.sh", getBalanceCmd);
}
public File createGenerateBtcBlockScript(String address) {
String bitcoinCliCmd = format("bitcoin-cli -regtest -rpcport=19443 -rpcuser=apitest"
+ " -rpcpassword=apitest generatetoaddress 1 \"%s\"",
address);
return createCliScript("genbtcblk.sh",
bitcoinCliCmd);
}
public File createCliScript(String scriptName, String... commands) {
String filename = getProperty("java.io.tmpdir") + File.separator + scriptName;
File oldScript = new File(filename);
if (oldScript.exists()) {
try {
FileUtil.deleteFileIfExists(oldScript);
} catch (IOException ex) {
throw new IllegalStateException("Unable to delete old script.", ex);
}
}
File script = new File(filename);
try {
List<CharSequence> lines = new ArrayList<>();
lines.add("#!/bin/bash");
lines.add("############################################################");
lines.add("# This example CLI script may be overwritten during the test");
lines.add("# run, and will be deleted when the test harness shuts down.");
lines.add("# Make a copy if you want to save it.");
lines.add("############################################################");
lines.add("set -x");
Collections.addAll(lines, commands);
Files.asCharSink(script, UTF_8, APPEND).writeLines(lines);
if (!script.setExecutable(true))
throw new IllegalStateException("Unable to set script owner's execute permission.");
} catch (IOException ex) {
log.error("", ex);
throw new IllegalStateException(ex);
} finally {
script.deleteOnExit();
}
return script;
}
public void printCliScript(File cliScript,
org.slf4j.Logger logger) {
try {
String contents = new String(readAllBytes(Paths.get(cliScript.getPath())));
logger.info("CLI script {}:\n{}", cliScript.getAbsolutePath(), contents);
} catch (IOException ex) {
throw new IllegalStateException("Error reading CLI script contents.", ex);
}
}
}

View file

@ -0,0 +1,78 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.scenario.bot.script;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import javax.annotation.Nullable;
@Getter
@ToString
public
class BotScript {
// Common, default is true.
private final boolean useTestHarness;
// Used only with test harness. Mutually exclusive, but if both are not null,
// the botPaymentMethodId takes precedence over countryCode.
@Nullable
private final String botPaymentMethodId;
@Nullable
private final String countryCode;
// Used only without test harness.
@Nullable
@Setter
private String paymentAccountIdForBot;
@Nullable
@Setter
private String paymentAccountIdForCliScripts;
// Common, used with or without test harness.
private final int apiPortForCliScripts;
private final String[] actions;
private final long protocolStepTimeLimitInMinutes;
private final boolean printCliScripts;
private final boolean stayAlive;
@SuppressWarnings("NullableProblems")
BotScript(boolean useTestHarness,
String botPaymentMethodId,
String countryCode,
String paymentAccountIdForBot,
String paymentAccountIdForCliScripts,
String[] actions,
int apiPortForCliScripts,
long protocolStepTimeLimitInMinutes,
boolean printCliScripts,
boolean stayAlive) {
this.useTestHarness = useTestHarness;
this.botPaymentMethodId = botPaymentMethodId;
this.countryCode = countryCode != null ? countryCode.toUpperCase() : null;
this.paymentAccountIdForBot = paymentAccountIdForBot;
this.paymentAccountIdForCliScripts = paymentAccountIdForCliScripts;
this.apiPortForCliScripts = apiPortForCliScripts;
this.actions = actions;
this.protocolStepTimeLimitInMinutes = protocolStepTimeLimitInMinutes;
this.printCliScripts = printCliScripts;
this.stayAlive = stayAlive;
}
}

View file

@ -0,0 +1,247 @@
/*
* 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.JsonFileManager;
import bisq.common.util.Utilities;
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 Utilities.objectToJson(new BotScript(
useTestHarness,
botPaymentMethodId,
countryCode,
paymentAccountIdForBot,
paymentAccountIdForCliScripts,
actions.split("\\s*,\\s*").clone(),
apiPortForCliScripts,
protocolStepTimeLimitInMinutes,
printCliScripts,
stayAlive));
}
public static void main(String[] args) {
BotScriptGenerator generator = new BotScriptGenerator(args);
String json = generator.generateBotScriptTemplate();
String destDir = getProperty("java.io.tmpdir");
JsonFileManager jsonFileManager = new JsonFileManager(new File(destDir));
jsonFileManager.writeToDisc(json, "bot-script");
JsonFileManager.shutDownAllInstances();
log.info("Saved {}/bot-script.json", destDir);
log.info("bot-script.json contents\n{}", json);
}
// Makes a formatter with a given overall row width of 120 and column separator width of 2.
private static class HelpFormatter extends BuiltinHelpFormatter {
public HelpFormatter() {
super(120, 2);
}
}
}

View file

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

View file

@ -0,0 +1,64 @@
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);
}
}
}

View file

@ -17,45 +17,7 @@
package bisq.cli;
import bisq.proto.grpc.CancelOfferRequest;
import bisq.proto.grpc.ConfirmPaymentReceivedRequest;
import bisq.proto.grpc.ConfirmPaymentStartedRequest;
import bisq.proto.grpc.CreateOfferRequest;
import bisq.proto.grpc.CreatePaymentAccountRequest;
import bisq.proto.grpc.GetAddressBalanceRequest;
import bisq.proto.grpc.GetBalancesRequest;
import bisq.proto.grpc.GetFundingAddressesRequest;
import bisq.proto.grpc.GetMethodHelpRequest;
import bisq.proto.grpc.GetMyOfferRequest;
import bisq.proto.grpc.GetMyOffersRequest;
import bisq.proto.grpc.GetOfferRequest;
import bisq.proto.grpc.GetOffersRequest;
import bisq.proto.grpc.GetPaymentAccountFormRequest;
import bisq.proto.grpc.GetPaymentAccountsRequest;
import bisq.proto.grpc.GetPaymentMethodsRequest;
import bisq.proto.grpc.GetTradeRequest;
import bisq.proto.grpc.GetTransactionRequest;
import bisq.proto.grpc.GetTxFeeRateRequest;
import bisq.proto.grpc.GetUnusedBsqAddressRequest;
import bisq.proto.grpc.GetVersionRequest;
import bisq.proto.grpc.KeepFundsRequest;
import bisq.proto.grpc.LockWalletRequest;
import bisq.proto.grpc.MarketPriceRequest;
import bisq.proto.grpc.OfferInfo;
import bisq.proto.grpc.RegisterDisputeAgentRequest;
import bisq.proto.grpc.RemoveWalletPasswordRequest;
import bisq.proto.grpc.SendBsqRequest;
import bisq.proto.grpc.SendBtcRequest;
import bisq.proto.grpc.SetTxFeeRatePreferenceRequest;
import bisq.proto.grpc.SetWalletPasswordRequest;
import bisq.proto.grpc.StopRequest;
import bisq.proto.grpc.TakeOfferRequest;
import bisq.proto.grpc.TxInfo;
import bisq.proto.grpc.UnlockWalletRequest;
import bisq.proto.grpc.UnsetTxFeeRatePreferenceRequest;
import bisq.proto.grpc.WithdrawFundsRequest;
import protobuf.PaymentAccount;
import io.grpc.StatusRuntimeException;
@ -87,7 +49,6 @@ import static bisq.cli.opts.OptLabel.OPT_HELP;
import static bisq.cli.opts.OptLabel.OPT_HOST;
import static bisq.cli.opts.OptLabel.OPT_PASSWORD;
import static bisq.cli.opts.OptLabel.OPT_PORT;
import static bisq.proto.grpc.HelpGrpc.HelpBlockingStub;
import static java.lang.String.format;
import static java.lang.System.err;
import static java.lang.System.exit;
@ -122,7 +83,6 @@ import bisq.cli.opts.WithdrawFundsOptionParser;
/**
* A command-line client for the Bisq gRPC API.
*/
@SuppressWarnings("ResultOfMethodCallIgnored")
@Slf4j
public class CliMain {
@ -188,50 +148,36 @@ public class CliMain {
throw new IllegalArgumentException(format("'%s' is not a supported method", methodName));
}
GrpcStubs grpcStubs = new GrpcStubs(host, port, password);
var disputeAgentsService = grpcStubs.disputeAgentsService;
var helpService = grpcStubs.helpService;
var offersService = grpcStubs.offersService;
var paymentAccountsService = grpcStubs.paymentAccountsService;
var priceService = grpcStubs.priceService;
var shutdownService = grpcStubs.shutdownService;
var tradesService = grpcStubs.tradesService;
var versionService = grpcStubs.versionService;
var walletsService = grpcStubs.walletsService;
GrpcClient client = new GrpcClient(host, port, password);
try {
switch (method) {
case getversion: {
if (new SimpleMethodOptionParser(args).parse().isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var request = GetVersionRequest.newBuilder().build();
var version = versionService.getVersion(request).getVersion();
var version = client.getVersion();
out.println(version);
return;
}
case getbalance: {
var opts = new GetBalanceOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var currencyCode = opts.getCurrencyCode();
var request = GetBalancesRequest.newBuilder()
.setCurrencyCode(currencyCode)
.build();
var reply = walletsService.getBalances(request);
var balances = client.getBalances(currencyCode);
switch (currencyCode.toUpperCase()) {
case "BSQ":
out.println(formatBsqBalanceInfoTbl(reply.getBalances().getBsq()));
out.println(formatBsqBalanceInfoTbl(balances.getBsq()));
break;
case "BTC":
out.println(formatBtcBalanceInfoTbl(reply.getBalances().getBtc()));
out.println(formatBtcBalanceInfoTbl(balances.getBtc()));
break;
case "":
default:
out.println(formatBalancesTbls(reply.getBalances()));
out.println(formatBalancesTbls(balances));
break;
}
return;
@ -239,54 +185,47 @@ public class CliMain {
case getaddressbalance: {
var opts = new GetAddressBalanceOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var address = opts.getAddress();
var request = GetAddressBalanceRequest.newBuilder()
.setAddress(address).build();
var reply = walletsService.getAddressBalance(request);
out.println(formatAddressBalanceTbl(singletonList(reply.getAddressBalanceInfo())));
var addressBalance = client.getAddressBalance(address);
out.println(formatAddressBalanceTbl(singletonList(addressBalance)));
return;
}
case getbtcprice: {
var opts = new GetBTCMarketPriceOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var currencyCode = opts.getCurrencyCode();
var request = MarketPriceRequest.newBuilder()
.setCurrencyCode(currencyCode)
.build();
var reply = priceService.getMarketPrice(request);
out.println(formatMarketPrice(reply.getPrice()));
var price = client.getBtcPrice(currencyCode);
out.println(formatMarketPrice(price));
return;
}
case getfundingaddresses: {
if (new SimpleMethodOptionParser(args).parse().isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var request = GetFundingAddressesRequest.newBuilder().build();
var reply = walletsService.getFundingAddresses(request);
out.println(formatAddressBalanceTbl(reply.getAddressBalanceInfoList()));
var fundingAddresses = client.getFundingAddresses();
out.println(formatAddressBalanceTbl(fundingAddresses));
return;
}
case getunusedbsqaddress: {
if (new SimpleMethodOptionParser(args).parse().isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var request = GetUnusedBsqAddressRequest.newBuilder().build();
var reply = walletsService.getUnusedBsqAddress(request);
out.println(reply.getAddress());
var address = client.getUnusedBsqAddress();
out.println(address);
return;
}
case sendbsq: {
var opts = new SendBsqOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var address = opts.getAddress();
@ -297,13 +236,7 @@ public class CliMain {
if (txFeeRate.isEmpty())
verifyStringIsValidLong(txFeeRate);
var request = SendBsqRequest.newBuilder()
.setAddress(address)
.setAmount(amount)
.setTxFeeRate(txFeeRate)
.build();
var reply = walletsService.sendBsq(request);
TxInfo txInfo = reply.getTxInfo();
var txInfo = client.sendBsq(address, amount, txFeeRate);
out.printf("%s bsq sent to %s in tx %s%n",
amount,
address,
@ -313,7 +246,7 @@ public class CliMain {
case sendbtc: {
var opts = new SendBtcOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var address = opts.getAddress();
@ -325,14 +258,8 @@ public class CliMain {
verifyStringIsValidLong(txFeeRate);
var memo = opts.getMemo();
var request = SendBtcRequest.newBuilder()
.setAddress(address)
.setAmount(amount)
.setTxFeeRate(txFeeRate)
.setMemo(memo)
.build();
var reply = walletsService.sendBtc(request);
TxInfo txInfo = reply.getTxInfo();
var txInfo = client.sendBtc(address, amount, txFeeRate, memo);
out.printf("%s btc sent to %s in tx %s%n",
amount,
address,
@ -341,56 +268,47 @@ public class CliMain {
}
case gettxfeerate: {
if (new SimpleMethodOptionParser(args).parse().isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var request = GetTxFeeRateRequest.newBuilder().build();
var reply = walletsService.getTxFeeRate(request);
out.println(formatTxFeeRateInfo(reply.getTxFeeRateInfo()));
var txFeeRate = client.getTxFeeRate();
out.println(formatTxFeeRateInfo(txFeeRate));
return;
}
case settxfeerate: {
var opts = new SetTxFeeRateOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var txFeeRate = toLong(opts.getFeeRate());
var request = SetTxFeeRatePreferenceRequest.newBuilder()
.setTxFeeRatePreference(txFeeRate)
.build();
var reply = walletsService.setTxFeeRatePreference(request);
out.println(formatTxFeeRateInfo(reply.getTxFeeRateInfo()));
var txFeeRate = client.setTxFeeRate(toLong(opts.getFeeRate()));
out.println(formatTxFeeRateInfo(txFeeRate));
return;
}
case unsettxfeerate: {
if (new SimpleMethodOptionParser(args).parse().isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var request = UnsetTxFeeRatePreferenceRequest.newBuilder().build();
var reply = walletsService.unsetTxFeeRatePreference(request);
out.println(formatTxFeeRateInfo(reply.getTxFeeRateInfo()));
var txFeeRate = client.unsetTxFeeRate();
out.println(formatTxFeeRateInfo(txFeeRate));
return;
}
case gettransaction: {
var opts = new GetTransactionOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var txId = opts.getTxId();
var request = GetTransactionRequest.newBuilder()
.setTxId(txId)
.build();
var reply = walletsService.getTransaction(request);
out.println(TransactionFormat.format(reply.getTxInfo()));
var tx = client.getTransaction(txId);
out.println(TransactionFormat.format(tx));
return;
}
case createoffer: {
var opts = new CreateOfferOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var paymentAcctId = opts.getPaymentAccountId();
@ -403,232 +321,178 @@ public class CliMain {
var marketPriceMargin = opts.getMktPriceMarginAsBigDecimal();
var securityDeposit = toSecurityDepositAsPct(opts.getSecurityDeposit());
var makerFeeCurrencyCode = opts.getMakerFeeCurrencyCode();
var request = CreateOfferRequest.newBuilder()
.setDirection(direction)
.setCurrencyCode(currencyCode)
.setAmount(amount)
.setMinAmount(minAmount)
.setUseMarketBasedPrice(useMarketBasedPrice)
.setPrice(fixedPrice)
.setMarketPriceMargin(marketPriceMargin.doubleValue())
.setBuyerSecurityDeposit(securityDeposit)
.setPaymentAccountId(paymentAcctId)
.setMakerFeeCurrencyCode(makerFeeCurrencyCode)
.build();
var reply = offersService.createOffer(request);
out.println(formatOfferTable(singletonList(reply.getOffer()), currencyCode));
var offer = client.createOffer(direction,
currencyCode,
amount,
minAmount,
useMarketBasedPrice,
fixedPrice,
marketPriceMargin.doubleValue(),
securityDeposit,
paymentAcctId,
makerFeeCurrencyCode);
out.println(formatOfferTable(singletonList(offer), currencyCode));
return;
}
case canceloffer: {
var opts = new CancelOfferOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var offerId = opts.getOfferId();
var request = CancelOfferRequest.newBuilder()
.setId(offerId)
.build();
offersService.cancelOffer(request);
client.cancelOffer(offerId);
out.println("offer canceled and removed from offer book");
return;
}
case getoffer: {
var opts = new GetOfferOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var offerId = opts.getOfferId();
var request = GetOfferRequest.newBuilder()
.setId(offerId)
.build();
var reply = offersService.getOffer(request);
out.println(formatOfferTable(singletonList(reply.getOffer()),
reply.getOffer().getCounterCurrencyCode()));
var offer = client.getOffer(offerId);
out.println(formatOfferTable(singletonList(offer), offer.getCounterCurrencyCode()));
return;
}
case getmyoffer: {
var opts = new GetOfferOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var offerId = opts.getOfferId();
var request = GetMyOfferRequest.newBuilder()
.setId(offerId)
.build();
var reply = offersService.getMyOffer(request);
out.println(formatOfferTable(singletonList(reply.getOffer()),
reply.getOffer().getCounterCurrencyCode()));
var offer = client.getMyOffer(offerId);
out.println(formatOfferTable(singletonList(offer), offer.getCounterCurrencyCode()));
return;
}
case getoffers: {
var opts = new GetOffersOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var direction = opts.getDirection();
var currencyCode = opts.getCurrencyCode();
var request = GetOffersRequest.newBuilder()
.setDirection(direction)
.setCurrencyCode(currencyCode)
.build();
var reply = offersService.getOffers(request);
List<OfferInfo> offers = reply.getOffersList();
List<OfferInfo> offers = client.getOffers(direction, currencyCode);
if (offers.isEmpty())
out.printf("no %s %s offers found%n", direction, currencyCode);
else
out.println(formatOfferTable(reply.getOffersList(), currencyCode));
out.println(formatOfferTable(offers, currencyCode));
return;
}
case getmyoffers: {
var opts = new GetOffersOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var direction = opts.getDirection();
var currencyCode = opts.getCurrencyCode();
var request = GetMyOffersRequest.newBuilder()
.setDirection(direction)
.setCurrencyCode(currencyCode)
.build();
var reply = offersService.getMyOffers(request);
List<OfferInfo> offers = reply.getOffersList();
List<OfferInfo> offers = client.getMyOffers(direction, currencyCode);
if (offers.isEmpty())
out.printf("no %s %s offers found%n", direction, currencyCode);
else
out.println(formatOfferTable(reply.getOffersList(), currencyCode));
out.println(formatOfferTable(offers, currencyCode));
return;
}
case takeoffer: {
var opts = new TakeOfferOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var offerId = opts.getOfferId();
var paymentAccountId = opts.getPaymentAccountId();
var takerFeeCurrencyCode = opts.getTakerFeeCurrencyCode();
var request = TakeOfferRequest.newBuilder()
.setOfferId(offerId)
.setPaymentAccountId(paymentAccountId)
.setTakerFeeCurrencyCode(takerFeeCurrencyCode)
.build();
var reply = tradesService.takeOffer(request);
out.printf("trade %s successfully taken%n", reply.getTrade().getTradeId());
var trade = client.takeOffer(offerId, paymentAccountId, takerFeeCurrencyCode);
out.printf("trade %s successfully taken%n", trade.getTradeId());
return;
}
case gettrade: {
// TODO make short-id a valid argument?
var opts = new GetTradeOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var tradeId = opts.getTradeId();
var showContract = opts.getShowContract();
var request = GetTradeRequest.newBuilder()
.setTradeId(tradeId)
.build();
var reply = tradesService.getTrade(request);
var trade = client.getTrade(tradeId);
if (showContract)
out.println(reply.getTrade().getContractAsJson());
out.println(trade.getContractAsJson());
else
out.println(TradeFormat.format(reply.getTrade()));
out.println(TradeFormat.format(trade));
return;
}
case confirmpaymentstarted: {
var opts = new GetTradeOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var tradeId = opts.getTradeId();
var request = ConfirmPaymentStartedRequest.newBuilder()
.setTradeId(tradeId)
.build();
tradesService.confirmPaymentStarted(request);
client.confirmPaymentStarted(tradeId);
out.printf("trade %s payment started message sent%n", tradeId);
return;
}
case confirmpaymentreceived: {
var opts = new GetTradeOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var tradeId = opts.getTradeId();
var request = ConfirmPaymentReceivedRequest.newBuilder()
.setTradeId(tradeId)
.build();
tradesService.confirmPaymentReceived(request);
client.confirmPaymentReceived(tradeId);
out.printf("trade %s payment received message sent%n", tradeId);
return;
}
case keepfunds: {
var opts = new GetTradeOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var tradeId = opts.getTradeId();
var request = KeepFundsRequest.newBuilder()
.setTradeId(tradeId)
.build();
tradesService.keepFunds(request);
client.keepFunds(tradeId);
out.printf("funds from trade %s saved in bisq wallet%n", tradeId);
return;
}
case withdrawfunds: {
var opts = new WithdrawFundsOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var tradeId = opts.getTradeId();
var address = opts.getAddress();
// Multi-word memos must be double quoted.
var memo = opts.getMemo();
var request = WithdrawFundsRequest.newBuilder()
.setTradeId(tradeId)
.setAddress(address)
.setMemo(memo)
.build();
tradesService.withdrawFunds(request);
client.withdrawFunds(tradeId, address, memo);
out.printf("trade %s funds sent to btc address %s%n", tradeId, address);
return;
}
case getpaymentmethods: {
if (new SimpleMethodOptionParser(args).parse().isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var request = GetPaymentMethodsRequest.newBuilder().build();
var reply = paymentAccountsService.getPaymentMethods(request);
reply.getPaymentMethodsList().forEach(p -> out.println(p.getId()));
var paymentMethods = client.getPaymentMethods();
paymentMethods.forEach(p -> out.println(p.getId()));
return;
}
case getpaymentacctform: {
var opts = new GetPaymentAcctFormOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var paymentMethodId = opts.getPaymentMethodId();
var request = GetPaymentAccountFormRequest.newBuilder()
.setPaymentMethodId(paymentMethodId)
.build();
String jsonString = paymentAccountsService.getPaymentAccountForm(request)
.getPaymentAccountFormJson();
String jsonString = client.getPaymentAcctFormAsJson(paymentMethodId);
File jsonFile = saveFileToDisk(paymentMethodId.toLowerCase(),
".json",
jsonString);
@ -640,7 +504,7 @@ public class CliMain {
case createpaymentacct: {
var opts = new CreatePaymentAcctOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var paymentAccountForm = opts.getPaymentAcctForm();
@ -651,24 +515,17 @@ public class CliMain {
throw new IllegalStateException(
format("could not read %s", paymentAccountForm.toString()));
}
var request = CreatePaymentAccountRequest.newBuilder()
.setPaymentAccountForm(jsonString)
.build();
var reply = paymentAccountsService.createPaymentAccount(request);
var paymentAccount = client.createPaymentAccount(jsonString);
out.println("payment account saved");
out.println(formatPaymentAcctTbl(singletonList(reply.getPaymentAccount())));
out.println(formatPaymentAcctTbl(singletonList(paymentAccount)));
return;
}
case getpaymentaccts: {
if (new SimpleMethodOptionParser(args).parse().isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var request = GetPaymentAccountsRequest.newBuilder().build();
var reply = paymentAccountsService.getPaymentAccounts(request);
List<PaymentAccount> paymentAccounts = reply.getPaymentAccountsList();
var paymentAccounts = client.getPaymentAccounts();
if (paymentAccounts.size() > 0)
out.println(formatPaymentAcctTbl(paymentAccounts));
else
@ -678,78 +535,66 @@ public class CliMain {
}
case lockwallet: {
if (new SimpleMethodOptionParser(args).parse().isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var request = LockWalletRequest.newBuilder().build();
walletsService.lockWallet(request);
client.lockWallet();
out.println("wallet locked");
return;
}
case unlockwallet: {
var opts = new UnlockWalletOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var walletPassword = opts.getPassword();
var timeout = opts.getUnlockTimeout();
var request = UnlockWalletRequest.newBuilder()
.setPassword(walletPassword)
.setTimeout(timeout).build();
walletsService.unlockWallet(request);
client.unlockWallet(walletPassword, timeout);
out.println("wallet unlocked");
return;
}
case removewalletpassword: {
var opts = new RemoveWalletPasswordOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var walletPassword = opts.getPassword();
var request = RemoveWalletPasswordRequest.newBuilder()
.setPassword(walletPassword).build();
walletsService.removeWalletPassword(request);
client.removeWalletPassword(walletPassword);
out.println("wallet decrypted");
return;
}
case setwalletpassword: {
var opts = new SetWalletPasswordOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var walletPassword = opts.getPassword();
var newWalletPassword = opts.getNewPassword();
var requestBuilder = SetWalletPasswordRequest.newBuilder()
.setPassword(walletPassword)
.setNewPassword(newWalletPassword);
walletsService.setWalletPassword(requestBuilder.build());
client.setWalletPassword(walletPassword, newWalletPassword);
out.println("wallet encrypted" + (!newWalletPassword.isEmpty() ? " with new password" : ""));
return;
}
case registerdisputeagent: {
var opts = new RegisterDisputeAgentOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var disputeAgentType = opts.getDisputeAgentType();
var registrationKey = opts.getRegistrationKey();
var requestBuilder = RegisterDisputeAgentRequest.newBuilder()
.setDisputeAgentType(disputeAgentType).setRegistrationKey(registrationKey);
disputeAgentsService.registerDisputeAgent(requestBuilder.build());
client.registerDisputeAgent(disputeAgentType, registrationKey);
out.println(disputeAgentType + " registered");
return;
}
case stop: {
if (new SimpleMethodOptionParser(args).parse().isForHelp()) {
out.println(getMethodHelp(helpService, method));
out.println(client.getMethodHelp(method));
return;
}
var request = StopRequest.newBuilder().build();
shutdownService.stop(request);
client.stopServer();
out.println("server shutdown signal received");
return;
}
@ -914,10 +759,4 @@ public class CliMain {
ex.printStackTrace(stream);
}
}
private static String getMethodHelp(HelpBlockingStub helpService, Method method) {
var request = GetMethodHelpRequest.newBuilder().setMethodName(method.name()).build();
var reply = helpService.getMethodHelp(request);
return reply.getMethodHelp();
}
}

View file

@ -65,37 +65,37 @@ public class CurrencyFormat {
formatFeeSatoshis(txFeeRateInfo.getFeeServiceRate()));
}
static String formatAmountRange(long minAmount, long amount) {
public static String formatAmountRange(long minAmount, long amount) {
return minAmount != amount
? formatSatoshis(minAmount) + " - " + formatSatoshis(amount)
: formatSatoshis(amount);
}
static String formatVolumeRange(long minVolume, long volume) {
public static String formatVolumeRange(long minVolume, long volume) {
return minVolume != volume
? formatOfferVolume(minVolume) + " - " + formatOfferVolume(volume)
: formatOfferVolume(volume);
}
static String formatMarketPrice(double price) {
public static String formatMarketPrice(double price) {
NUMBER_FORMAT.setMinimumFractionDigits(4);
return NUMBER_FORMAT.format(price);
}
static String formatOfferPrice(long price) {
public static String formatOfferPrice(long price) {
NUMBER_FORMAT.setMaximumFractionDigits(4);
NUMBER_FORMAT.setMinimumFractionDigits(4);
NUMBER_FORMAT.setRoundingMode(RoundingMode.UNNECESSARY);
return NUMBER_FORMAT.format((double) price / 10000);
}
static String formatOfferVolume(long volume) {
public static String formatOfferVolume(long volume) {
NUMBER_FORMAT.setMaximumFractionDigits(0);
NUMBER_FORMAT.setRoundingMode(RoundingMode.UNNECESSARY);
return NUMBER_FORMAT.format((double) volume / 10000);
}
static long toSatoshis(String btc) {
public static long toSatoshis(String btc) {
if (btc.startsWith("-"))
throw new IllegalArgumentException(format("'%s' is not a positive number", btc));
@ -106,7 +106,7 @@ public class CurrencyFormat {
}
}
static double toSecurityDepositAsPct(String securityDepositInput) {
public static double toSecurityDepositAsPct(String securityDepositInput) {
try {
return new BigDecimal(securityDepositInput)
.multiply(SECURITY_DEPOSIT_MULTIPLICAND).doubleValue();
@ -116,7 +116,7 @@ public class CurrencyFormat {
}
@SuppressWarnings("BigDecimalMethodWithoutRoundingCalled")
private static String formatFeeSatoshis(long sats) {
public static String formatFeeSatoshis(long sats) {
return BTC_TX_FEE_FORMAT.format(BigDecimal.valueOf(sats).divide(SATOSHI_DIVISOR));
}
}

View file

@ -0,0 +1,424 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.cli;
import bisq.proto.grpc.AddressBalanceInfo;
import bisq.proto.grpc.BalancesInfo;
import bisq.proto.grpc.BsqBalanceInfo;
import bisq.proto.grpc.BtcBalanceInfo;
import bisq.proto.grpc.CancelOfferRequest;
import bisq.proto.grpc.ConfirmPaymentReceivedRequest;
import bisq.proto.grpc.ConfirmPaymentStartedRequest;
import bisq.proto.grpc.CreateOfferRequest;
import bisq.proto.grpc.CreatePaymentAccountRequest;
import bisq.proto.grpc.GetAddressBalanceRequest;
import bisq.proto.grpc.GetBalancesRequest;
import bisq.proto.grpc.GetFundingAddressesRequest;
import bisq.proto.grpc.GetMethodHelpRequest;
import bisq.proto.grpc.GetMyOfferRequest;
import bisq.proto.grpc.GetMyOffersRequest;
import bisq.proto.grpc.GetOfferRequest;
import bisq.proto.grpc.GetOffersRequest;
import bisq.proto.grpc.GetPaymentAccountFormRequest;
import bisq.proto.grpc.GetPaymentAccountsRequest;
import bisq.proto.grpc.GetPaymentMethodsRequest;
import bisq.proto.grpc.GetTradeRequest;
import bisq.proto.grpc.GetTransactionRequest;
import bisq.proto.grpc.GetTxFeeRateRequest;
import bisq.proto.grpc.GetUnusedBsqAddressRequest;
import bisq.proto.grpc.GetVersionRequest;
import bisq.proto.grpc.KeepFundsRequest;
import bisq.proto.grpc.LockWalletRequest;
import bisq.proto.grpc.MarketPriceRequest;
import bisq.proto.grpc.OfferInfo;
import bisq.proto.grpc.RegisterDisputeAgentRequest;
import bisq.proto.grpc.RemoveWalletPasswordRequest;
import bisq.proto.grpc.SendBsqRequest;
import bisq.proto.grpc.SendBtcRequest;
import bisq.proto.grpc.SetTxFeeRatePreferenceRequest;
import bisq.proto.grpc.SetWalletPasswordRequest;
import bisq.proto.grpc.StopRequest;
import bisq.proto.grpc.TakeOfferRequest;
import bisq.proto.grpc.TradeInfo;
import bisq.proto.grpc.TxFeeRateInfo;
import bisq.proto.grpc.TxInfo;
import bisq.proto.grpc.UnlockWalletRequest;
import bisq.proto.grpc.UnsetTxFeeRatePreferenceRequest;
import bisq.proto.grpc.WithdrawFundsRequest;
import protobuf.PaymentAccount;
import protobuf.PaymentMethod;
import java.util.List;
import java.util.stream.Collectors;
import static java.util.Comparator.comparing;
@SuppressWarnings("ResultOfMethodCallIgnored")
public final class GrpcClient {
private final GrpcStubs grpcStubs;
public GrpcClient(String apiHost, int apiPort, String apiPassword) {
this.grpcStubs = new GrpcStubs(apiHost, apiPort, apiPassword);
}
public String getVersion() {
var request = GetVersionRequest.newBuilder().build();
return grpcStubs.versionService.getVersion(request).getVersion();
}
public BalancesInfo getBalances() {
return getBalances("");
}
public BsqBalanceInfo getBsqBalances() {
return getBalances("BSQ").getBsq();
}
public BtcBalanceInfo getBtcBalances() {
return getBalances("BTC").getBtc();
}
public BalancesInfo getBalances(String currencyCode) {
var request = GetBalancesRequest.newBuilder()
.setCurrencyCode(currencyCode)
.build();
return grpcStubs.walletsService.getBalances(request).getBalances();
}
public AddressBalanceInfo getAddressBalance(String address) {
var request = GetAddressBalanceRequest.newBuilder()
.setAddress(address).build();
return grpcStubs.walletsService.getAddressBalance(request).getAddressBalanceInfo();
}
public double getBtcPrice(String currencyCode) {
var request = MarketPriceRequest.newBuilder()
.setCurrencyCode(currencyCode)
.build();
return grpcStubs.priceService.getMarketPrice(request).getPrice();
}
public List<AddressBalanceInfo> getFundingAddresses() {
var request = GetFundingAddressesRequest.newBuilder().build();
return grpcStubs.walletsService.getFundingAddresses(request).getAddressBalanceInfoList();
}
public String getUnusedBsqAddress() {
var request = GetUnusedBsqAddressRequest.newBuilder().build();
return grpcStubs.walletsService.getUnusedBsqAddress(request).getAddress();
}
public String getUnusedBtcAddress() {
var request = GetFundingAddressesRequest.newBuilder().build();
//noinspection OptionalGetWithoutIsPresent
return grpcStubs.walletsService.getFundingAddresses(request)
.getAddressBalanceInfoList()
.stream()
.filter(a -> a.getBalance() == 0 && a.getNumConfirmations() == 0)
.findFirst()
.get()
.getAddress();
}
public TxInfo sendBsq(String address, String amount, String txFeeRate) {
var request = SendBsqRequest.newBuilder()
.setAddress(address)
.setAmount(amount)
.setTxFeeRate(txFeeRate)
.build();
return grpcStubs.walletsService.sendBsq(request).getTxInfo();
}
public TxInfo sendBtc(String address, String amount, String txFeeRate, String memo) {
var request = SendBtcRequest.newBuilder()
.setAddress(address)
.setAmount(amount)
.setTxFeeRate(txFeeRate)
.setMemo(memo)
.build();
return grpcStubs.walletsService.sendBtc(request).getTxInfo();
}
public TxFeeRateInfo getTxFeeRate() {
var request = GetTxFeeRateRequest.newBuilder().build();
return grpcStubs.walletsService.getTxFeeRate(request).getTxFeeRateInfo();
}
public TxFeeRateInfo setTxFeeRate(long txFeeRate) {
var request = SetTxFeeRatePreferenceRequest.newBuilder()
.setTxFeeRatePreference(txFeeRate)
.build();
return grpcStubs.walletsService.setTxFeeRatePreference(request).getTxFeeRateInfo();
}
public TxFeeRateInfo unsetTxFeeRate() {
var request = UnsetTxFeeRatePreferenceRequest.newBuilder().build();
return grpcStubs.walletsService.unsetTxFeeRatePreference(request).getTxFeeRateInfo();
}
public TxInfo getTransaction(String txId) {
var request = GetTransactionRequest.newBuilder()
.setTxId(txId)
.build();
return grpcStubs.walletsService.getTransaction(request).getTxInfo();
}
public OfferInfo createFixedPricedOffer(String direction,
String currencyCode,
long amount,
long minAmount,
String fixedPrice,
double securityDeposit,
String paymentAcctId,
String makerFeeCurrencyCode) {
return createOffer(direction,
currencyCode,
amount,
minAmount,
false,
fixedPrice,
0.00,
securityDeposit,
paymentAcctId,
makerFeeCurrencyCode);
}
public OfferInfo createMarketBasedPricedOffer(String direction,
String currencyCode,
long amount,
long minAmount,
double marketPriceMargin,
double securityDeposit,
String paymentAcctId,
String makerFeeCurrencyCode) {
return createOffer(direction,
currencyCode,
amount,
minAmount,
true,
"0",
marketPriceMargin,
securityDeposit,
paymentAcctId,
makerFeeCurrencyCode);
}
// TODO make private, move to bottom of class
public OfferInfo createOffer(String direction,
String currencyCode,
long amount,
long minAmount,
boolean useMarketBasedPrice,
String fixedPrice,
double marketPriceMargin,
double securityDeposit,
String paymentAcctId,
String makerFeeCurrencyCode) {
var request = CreateOfferRequest.newBuilder()
.setDirection(direction)
.setCurrencyCode(currencyCode)
.setAmount(amount)
.setMinAmount(minAmount)
.setUseMarketBasedPrice(useMarketBasedPrice)
.setPrice(fixedPrice)
.setMarketPriceMargin(marketPriceMargin)
.setBuyerSecurityDeposit(securityDeposit)
.setPaymentAccountId(paymentAcctId)
.setMakerFeeCurrencyCode(makerFeeCurrencyCode)
.build();
return grpcStubs.offersService.createOffer(request).getOffer();
}
public void cancelOffer(String offerId) {
var request = CancelOfferRequest.newBuilder()
.setId(offerId)
.build();
grpcStubs.offersService.cancelOffer(request);
}
public OfferInfo getOffer(String offerId) {
var request = GetOfferRequest.newBuilder()
.setId(offerId)
.build();
return grpcStubs.offersService.getOffer(request).getOffer();
}
public OfferInfo getMyOffer(String offerId) {
var request = GetMyOfferRequest.newBuilder()
.setId(offerId)
.build();
return grpcStubs.offersService.getMyOffer(request).getOffer();
}
public List<OfferInfo> getOffers(String direction, String currencyCode) {
var request = GetOffersRequest.newBuilder()
.setDirection(direction)
.setCurrencyCode(currencyCode)
.build();
return grpcStubs.offersService.getOffers(request).getOffersList();
}
public List<OfferInfo> getOffersSortedByDate(String direction, String currencyCode) {
var offers = getOffers(direction, currencyCode);
return offers.isEmpty() ? offers : sortOffersByDate(offers);
}
public List<OfferInfo> getMyOffers(String direction, String currencyCode) {
var request = GetMyOffersRequest.newBuilder()
.setDirection(direction)
.setCurrencyCode(currencyCode)
.build();
return grpcStubs.offersService.getMyOffers(request).getOffersList();
}
public List<OfferInfo> getMyOffersSortedByDate(String direction, String currencyCode) {
var offers = getMyOffers(direction, currencyCode);
return offers.isEmpty() ? offers : sortOffersByDate(offers);
}
public OfferInfo getMostRecentOffer(String direction, String currencyCode) {
List<OfferInfo> offers = getOffersSortedByDate(direction, currencyCode);
return offers.isEmpty() ? null : offers.get(offers.size() - 1);
}
// TODO move to bottom of class
private List<OfferInfo> sortOffersByDate(List<OfferInfo> offerInfoList) {
return offerInfoList.stream()
.sorted(comparing(OfferInfo::getDate))
.collect(Collectors.toList());
}
public TradeInfo takeOffer(String offerId, String paymentAccountId, String takerFeeCurrencyCode) {
var request = TakeOfferRequest.newBuilder()
.setOfferId(offerId)
.setPaymentAccountId(paymentAccountId)
.setTakerFeeCurrencyCode(takerFeeCurrencyCode)
.build();
return grpcStubs.tradesService.takeOffer(request).getTrade();
}
public TradeInfo getTrade(String tradeId) {
var request = GetTradeRequest.newBuilder()
.setTradeId(tradeId)
.build();
return grpcStubs.tradesService.getTrade(request).getTrade();
}
public void confirmPaymentStarted(String tradeId) {
var request = ConfirmPaymentStartedRequest.newBuilder()
.setTradeId(tradeId)
.build();
grpcStubs.tradesService.confirmPaymentStarted(request);
}
public void confirmPaymentReceived(String tradeId) {
var request = ConfirmPaymentReceivedRequest.newBuilder()
.setTradeId(tradeId)
.build();
grpcStubs.tradesService.confirmPaymentReceived(request);
}
public void keepFunds(String tradeId) {
var request = KeepFundsRequest.newBuilder()
.setTradeId(tradeId)
.build();
grpcStubs.tradesService.keepFunds(request);
}
public void withdrawFunds(String tradeId, String address, String memo) {
var request = WithdrawFundsRequest.newBuilder()
.setTradeId(tradeId)
.setAddress(address)
.setMemo(memo)
.build();
grpcStubs.tradesService.withdrawFunds(request);
}
public List<PaymentMethod> getPaymentMethods() {
var request = GetPaymentMethodsRequest.newBuilder().build();
return grpcStubs.paymentAccountsService.getPaymentMethods(request).getPaymentMethodsList();
}
public String getPaymentAcctFormAsJson(String paymentMethodId) {
var request = GetPaymentAccountFormRequest.newBuilder()
.setPaymentMethodId(paymentMethodId)
.build();
return grpcStubs.paymentAccountsService.getPaymentAccountForm(request).getPaymentAccountFormJson();
}
public PaymentAccount createPaymentAccount(String json) {
var request = CreatePaymentAccountRequest.newBuilder()
.setPaymentAccountForm(json)
.build();
return grpcStubs.paymentAccountsService.createPaymentAccount(request).getPaymentAccount();
}
public List<PaymentAccount> getPaymentAccounts() {
var request = GetPaymentAccountsRequest.newBuilder().build();
return grpcStubs.paymentAccountsService.getPaymentAccounts(request).getPaymentAccountsList();
}
public void lockWallet() {
var request = LockWalletRequest.newBuilder().build();
grpcStubs.walletsService.lockWallet(request);
}
public void unlockWallet(String walletPassword, long timeout) {
var request = UnlockWalletRequest.newBuilder()
.setPassword(walletPassword)
.setTimeout(timeout).build();
grpcStubs.walletsService.unlockWallet(request);
}
public void removeWalletPassword(String walletPassword) {
var request = RemoveWalletPasswordRequest.newBuilder()
.setPassword(walletPassword).build();
grpcStubs.walletsService.removeWalletPassword(request);
}
public void setWalletPassword(String walletPassword) {
var request = SetWalletPasswordRequest.newBuilder()
.setPassword(walletPassword).build();
grpcStubs.walletsService.setWalletPassword(request);
}
public void setWalletPassword(String oldWalletPassword, String newWalletPassword) {
var request = SetWalletPasswordRequest.newBuilder()
.setPassword(oldWalletPassword)
.setNewPassword(newWalletPassword).build();
grpcStubs.walletsService.setWalletPassword(request);
}
public void registerDisputeAgent(String disputeAgentType, String registrationKey) {
var request = RegisterDisputeAgentRequest.newBuilder()
.setDisputeAgentType(disputeAgentType).setRegistrationKey(registrationKey).build();
grpcStubs.disputeAgentsService.registerDisputeAgent(request);
}
public void stopServer() {
var request = StopRequest.newBuilder().build();
grpcStubs.shutdownService.stop(request);
}
public String getMethodHelp(Method method) {
var request = GetMethodHelpRequest.newBuilder().setMethodName(method.name()).build();
return grpcStubs.helpService.getMethodHelp(request).getMethodHelp();
}
}

View file

@ -32,7 +32,7 @@ import io.grpc.ManagedChannelBuilder;
import static java.util.concurrent.TimeUnit.SECONDS;
public class GrpcStubs {
public final class GrpcStubs {
public final DisputeAgentsGrpc.DisputeAgentsBlockingStub disputeAgentsService;
public final HelpGrpc.HelpBlockingStub helpService;

View file

@ -111,7 +111,7 @@ public class TableFormat {
formatSatoshis(btcBalanceInfo.getLockedBalance()));
}
static String formatOfferTable(List<OfferInfo> offerInfo, String fiatCurrency) {
public static String formatOfferTable(List<OfferInfo> offerInfo, String fiatCurrency) {
// Some column values might be longer than header, so we need to calculate them.
int paymentMethodColWidth = getLengthOfLongestColumn(
COL_HEADER_PAYMENT_METHOD.length(),
@ -147,7 +147,7 @@ public class TableFormat {
.collect(Collectors.joining("\n"));
}
static String formatPaymentAcctTbl(List<PaymentAccount> paymentAccounts) {
public static String formatPaymentAcctTbl(List<PaymentAccount> paymentAccounts) {
// Some column values might be longer than header, so we need to calculate them.
int nameColWidth = getLengthOfLongestColumn(
COL_HEADER_NAME.length(),

View file

@ -79,6 +79,7 @@ import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
import static bisq.common.config.BaseCurrencyNetwork.BTC_DAO_REGTEST;
import static bisq.core.btc.wallet.Restrictions.getMinNonDustOutput;
import static bisq.core.util.ParsingUtils.parseToCoin;
import static java.lang.String.format;
@ -311,8 +312,13 @@ class CoreWalletsService {
void setTxFeeRatePreference(long txFeeRate,
ResultHandler resultHandler) {
if (txFeeRate <= 0)
throw new IllegalStateException("cannot create transactions without fees");
long minFeePerVbyte = BTC_DAO_REGTEST.getDefaultMinFeePerVbyte();
// TODO Replace line above with line below, after commit
// c33ac1b9834fb9f7f14e553d09776f94efc9d13d is merged.
// long minFeePerVbyte = feeService.getMinFeePerVByte();
if (txFeeRate < minFeePerVbyte)
throw new IllegalStateException(
format("tx fee rate preference must be >= %d sats/byte", minFeePerVbyte));
preferences.setUseCustomWithdrawalTxFee(true);
Coin satsPerByte = Coin.valueOf(txFeeRate);

View file

@ -67,8 +67,8 @@ public class TxFeeRateInfo implements Payload {
public String toString() {
return "TxFeeRateInfo{" + "\n" +
" useCustomTxFeeRate=" + useCustomTxFeeRate + "\n" +
", customTxFeeRate=" + customTxFeeRate + "sats/byte" + "\n" +
", feeServiceRate=" + feeServiceRate + "sats/byte" + "\n" +
", customTxFeeRate=" + customTxFeeRate + " sats/byte" + "\n" +
", feeServiceRate=" + feeServiceRate + " sats/byte" + "\n" +
", lastFeeServiceRequestTs=" + lastFeeServiceRequestTs + "\n" +
'}';
}

View file

@ -33,8 +33,11 @@ import java.util.Collections;
import java.util.List;
import java.util.Set;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
/**
* Used from org.bitcoinj.wallet.DefaultCoinSelector but added selectOutput method and changed static methods to
* instance methods.
@ -49,6 +52,12 @@ public abstract class BisqDefaultCoinSelector implements CoinSelector {
protected final boolean permitForeignPendingTx;
// TransactionOutputs to be used as candidates in the select method.
// We reset the value to null just after we have applied it inside the select method.
@Nullable
@Setter
protected Set<TransactionOutput> utxoCandidates;
public CoinSelection select(Coin target, Set<TransactionOutput> candidates) {
return select(target, new ArrayList<>(candidates));
}
@ -65,7 +74,16 @@ public abstract class BisqDefaultCoinSelector implements CoinSelector {
public CoinSelection select(Coin target, List<TransactionOutput> candidates) {
ArrayList<TransactionOutput> selected = new ArrayList<>();
// Sort the inputs by age*value so we get the highest "coin days" spent.
ArrayList<TransactionOutput> sortedOutputs = new ArrayList<>(candidates);
ArrayList<TransactionOutput> sortedOutputs;
if (utxoCandidates != null) {
sortedOutputs = new ArrayList<>(utxoCandidates);
// We reuse the selectors. Reset the transactionOutputCandidates field
utxoCandidates = null;
} else {
sortedOutputs = new ArrayList<>(candidates);
}
// If we spend all we don't need to sort
if (!target.equals(NetworkParameters.MAX_MONEY))
sortOutputs(sortedOutputs);
@ -120,6 +138,9 @@ public abstract class BisqDefaultCoinSelector implements CoinSelector {
abstract boolean isTxOutputSpendable(TransactionOutput output);
// TODO Why it uses coin age and not try to minimize number of inputs as the highest priority?
// Asked Oscar and he also don't knows why coin age is used. Should be changed so that min. number of inputs is
// target.
protected void sortOutputs(ArrayList<TransactionOutput> outputs) {
Collections.sort(outputs, (a, b) -> {
int depth1 = a.getParentTransactionDepthInBlocks();

View file

@ -72,6 +72,8 @@ import java.util.stream.Stream;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.bitcoinj.core.TransactionConfidence.ConfidenceType.BUILDING;
@ -135,6 +137,8 @@ public class BsqWalletService extends WalletService implements DaoStateListener
this.unconfirmedBsqChangeOutputListService = unconfirmedBsqChangeOutputListService;
this.daoKillSwitch = daoKillSwitch;
nonBsqCoinSelector.setPreferences(preferences);
walletsSetup.addSetupCompletedHandler(() -> {
wallet = walletsSetup.getBsqWallet();
if (wallet != null) {
@ -313,6 +317,16 @@ public class BsqWalletService extends WalletService implements DaoStateListener
walletTransactionsChangeListeners.remove(listener);
}
public List<TransactionOutput> getSpendableBsqTransactionOutputs() {
return new ArrayList<>(bsqCoinSelector.select(NetworkParameters.MAX_MONEY,
wallet.calculateAllSpendCandidates()).gathered);
}
public List<TransactionOutput> getSpendableNonBsqTransactionOutputs() {
return new ArrayList<>(nonBsqCoinSelector.select(NetworkParameters.MAX_MONEY,
wallet.calculateAllSpendCandidates()).gathered);
}
///////////////////////////////////////////////////////////////////////////////////////////
// BSQ TransactionOutputs and Transactions
@ -511,7 +525,19 @@ public class BsqWalletService extends WalletService implements DaoStateListener
///////////////////////////////////////////////////////////////////////////////////////////
public Transaction getPreparedSendBsqTx(String receiverAddress, Coin receiverAmount)
throws AddressFormatException, InsufficientBsqException, WalletException, TransactionVerificationException, BsqChangeBelowDustException {
throws AddressFormatException, InsufficientBsqException, WalletException,
TransactionVerificationException, BsqChangeBelowDustException {
return getPreparedSendTx(receiverAddress, receiverAmount, bsqCoinSelector, false);
}
public Transaction getPreparedSendBsqTx(String receiverAddress,
Coin receiverAmount,
@Nullable Set<TransactionOutput> utxoCandidates)
throws AddressFormatException, InsufficientBsqException, WalletException,
TransactionVerificationException, BsqChangeBelowDustException {
if (utxoCandidates != null) {
bsqCoinSelector.setUtxoCandidates(utxoCandidates);
}
return getPreparedSendTx(receiverAddress, receiverAmount, bsqCoinSelector, false);
}
@ -520,7 +546,19 @@ public class BsqWalletService extends WalletService implements DaoStateListener
///////////////////////////////////////////////////////////////////////////////////////////
public Transaction getPreparedSendBtcTx(String receiverAddress, Coin receiverAmount)
throws AddressFormatException, InsufficientBsqException, WalletException, TransactionVerificationException, BsqChangeBelowDustException {
throws AddressFormatException, InsufficientBsqException, WalletException,
TransactionVerificationException, BsqChangeBelowDustException {
return getPreparedSendTx(receiverAddress, receiverAmount, nonBsqCoinSelector, true);
}
public Transaction getPreparedSendBtcTx(String receiverAddress,
Coin receiverAmount,
@Nullable Set<TransactionOutput> utxoCandidates)
throws AddressFormatException, InsufficientBsqException, WalletException,
TransactionVerificationException, BsqChangeBelowDustException {
if (utxoCandidates != null) {
nonBsqCoinSelector.setUtxoCandidates(utxoCandidates);
}
return getPreparedSendTx(receiverAddress, receiverAmount, nonBsqCoinSelector, true);
}

View file

@ -19,6 +19,7 @@ package bisq.core.btc.wallet;
import bisq.core.dao.state.DaoStateService;
import bisq.core.dao.state.model.blockchain.TxOutputKey;
import bisq.core.user.Preferences;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionConfidence;
@ -26,6 +27,7 @@ import org.bitcoinj.core.TransactionOutput;
import javax.inject.Inject;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
/**
@ -35,6 +37,8 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class NonBsqCoinSelector extends BisqDefaultCoinSelector {
private DaoStateService daoStateService;
@Setter
private Preferences preferences;
@Inject
public NonBsqCoinSelector(DaoStateService daoStateService) {
@ -60,9 +64,9 @@ public class NonBsqCoinSelector extends BisqDefaultCoinSelector {
return !daoStateService.existsTxOutput(key) || daoStateService.isRejectedIssuanceOutput(key);
}
// BTC utxo in the BSQ wallet are usually from rejected comp request so we don't expect dust attack utxos here.
// Prevent usage of dust attack utxos
@Override
protected boolean isDustAttackUtxo(TransactionOutput output) {
return false;
return output.getValue().value < preferences.getIgnoreDustThreshold();
}
}

View file

@ -78,6 +78,7 @@ shared.offerType=Offer type
shared.details=Details
shared.address=Address
shared.balanceWithCur=Balance in {0}
shared.utxo=Unspent transaction output
shared.txId=Transaction ID
shared.confirmations=Confirmations
shared.revert=Revert Tx
@ -2270,6 +2271,7 @@ dao.wallet.send.receiverAddress=Receiver's BSQ address
dao.wallet.send.receiverBtcAddress=Receiver's BTC address
dao.wallet.send.setDestinationAddress=Fill in your destination address
dao.wallet.send.send=Send BSQ funds
dao.wallet.send.inputControl=Select inputs
dao.wallet.send.sendBtc=Send BTC funds
dao.wallet.send.sendFunds.headline=Confirm withdrawal request
dao.wallet.send.sendFunds.details=Sending: {0}\nTo receiving address: {1}.\nRequired mining fee is: {2} ({3} satoshis/vbyte)\nTransaction vsize: {4} vKb\n\nThe recipient will receive: {5}\n\nAre you sure you want to withdraw that amount?
@ -2487,6 +2489,9 @@ dao.factsAndFigures.transactions.irregularTx=No. of all irregular transactions
# Windows
####################################################################
inputControlWindow.headline=Select inputs for transaction
inputControlWindow.balanceLabel=Available balance
contractWindow.title=Dispute details
contractWindow.dates=Offer date / Trade date
contractWindow.btcAddresses=Bitcoin address BTC buyer / BTC seller
@ -3619,6 +3624,7 @@ validation.inputTooSmall=Input has to be larger than {0}
validation.inputToBeAtLeast=Input has to be at least {0}
validation.amountBelowDust=An amount below the dust limit of {0} satoshi is not allowed.
validation.length=Length must be between {0} and {1}
validation.fixedLength=Length must be {0}
validation.pattern=Input must be of format: {0}
validation.noHexString=The input is not in HEX format.
validation.advancedCash.invalidFormat=Must be a valid email or wallet id of format: X000000000000

View file

@ -76,7 +76,7 @@ public class InputTextField extends JFXTextField {
validationResult.addListener((ov, oldValue, newValue) -> {
if (newValue != null) {
resetValidation();
jfxValidationWrapper.resetValidation();
if (!newValue.isValid) {
if (!newValue.errorMessageEquals(oldValue)) { // avoid blinking
validate(); // ensure that the new error message replaces the old one
@ -92,9 +92,7 @@ public class InputTextField extends JFXTextField {
});
textProperty().addListener((o, oldValue, newValue) -> {
if (validator != null) {
this.validationResult.set(validator.validate(getText()));
}
refreshValidation();
});
focusedProperty().addListener((o, oldValue, newValue) -> {
@ -108,6 +106,7 @@ public class InputTextField extends JFXTextField {
});
}
public InputTextField(double inputLineExtension) {
this();
this.inputLineExtension = inputLineExtension;
@ -119,6 +118,19 @@ public class InputTextField extends JFXTextField {
public void resetValidation() {
jfxValidationWrapper.resetValidation();
String input = getText();
if (input.isEmpty()) {
validationResult.set(new InputValidator.ValidationResult(true));
} else {
validationResult.set(validator.validate(input));
}
}
public void refreshValidation() {
if (validator != null) {
this.validationResult.set(validator.validate(getText()));
}
}
///////////////////////////////////////////////////////////////////////////////////////////

View file

@ -28,7 +28,9 @@ import bisq.desktop.main.funds.FundsView;
import bisq.desktop.main.funds.deposit.DepositView;
import bisq.desktop.main.overlays.popups.Popup;
import bisq.desktop.main.overlays.windows.TxDetailsBsq;
import bisq.desktop.main.overlays.windows.TxInputSelectionWindow;
import bisq.desktop.main.overlays.windows.WalletPasswordWindow;
import bisq.desktop.util.FormBuilder;
import bisq.desktop.util.GUIUtil;
import bisq.desktop.util.Layout;
import bisq.desktop.util.validation.BsqAddressValidator;
@ -47,6 +49,7 @@ import bisq.core.btc.wallet.WalletsManager;
import bisq.core.dao.state.model.blockchain.TxType;
import bisq.core.locale.Res;
import bisq.core.user.DontShowAgainLookup;
import bisq.core.user.Preferences;
import bisq.core.util.FormattingUtils;
import bisq.core.util.ParsingUtils;
import bisq.core.util.coin.BsqFormatter;
@ -58,10 +61,12 @@ import bisq.network.p2p.P2PService;
import bisq.common.UserThread;
import bisq.common.handlers.ResultHandler;
import bisq.common.util.Tuple2;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.InsufficientMoneyException;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionOutput;
import javax.inject.Inject;
import javax.inject.Named;
@ -71,9 +76,14 @@ import javafx.scene.layout.GridPane;
import javafx.beans.value.ChangeListener;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import static bisq.desktop.util.FormBuilder.addButtonAfterGroup;
import static bisq.desktop.util.FormBuilder.addInputTextField;
import static bisq.desktop.util.FormBuilder.addTitledGroupBg;
@ -92,15 +102,20 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
private final BtcValidator btcValidator;
private final BsqAddressValidator bsqAddressValidator;
private final BtcAddressValidator btcAddressValidator;
private final Preferences preferences;
private final WalletPasswordWindow walletPasswordWindow;
private int gridRow = 0;
private InputTextField amountInputTextField, btcAmountInputTextField;
private Button sendBsqButton, sendBtcButton;
private Button sendBsqButton, sendBtcButton, bsqInputControlButton, btcInputControlButton;
private InputTextField receiversAddressInputTextField, receiversBtcAddressInputTextField;
private ChangeListener<Boolean> focusOutListener;
private TitledGroupBg btcTitledGroupBg;
private ChangeListener<String> inputTextFieldListener;
@Nullable
private Set<TransactionOutput> bsqUtxoCandidates;
@Nullable
private Set<TransactionOutput> btcUtxoCandidates;
///////////////////////////////////////////////////////////////////////////////////////////
@ -121,6 +136,7 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
BtcValidator btcValidator,
BsqAddressValidator bsqAddressValidator,
BtcAddressValidator btcAddressValidator,
Preferences preferences,
WalletPasswordWindow walletPasswordWindow) {
this.bsqWalletService = bsqWalletService;
this.btcWalletService = btcWalletService;
@ -135,6 +151,7 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
this.btcValidator = btcValidator;
this.bsqAddressValidator = bsqAddressValidator;
this.btcAddressValidator = btcAddressValidator;
this.preferences = preferences;
this.walletPasswordWindow = walletPasswordWindow;
}
@ -159,6 +176,16 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
setSendBtcGroupVisibleState(false);
bsqBalanceUtil.activate();
receiversAddressInputTextField.resetValidation();
amountInputTextField.resetValidation();
receiversBtcAddressInputTextField.resetValidation();
btcAmountInputTextField.resetValidation();
sendBsqButton.setOnAction((event) -> onSendBsq());
bsqInputControlButton.setOnAction((event) -> onBsqInputControl());
sendBtcButton.setOnAction((event) -> onSendBtc());
btcInputControlButton.setOnAction((event) -> onBtcInputControl());
receiversAddressInputTextField.focusedProperty().addListener(focusOutListener);
amountInputTextField.focusedProperty().addListener(focusOutListener);
receiversBtcAddressInputTextField.focusedProperty().addListener(focusOutListener);
@ -171,11 +198,16 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
bsqWalletService.addBsqBalanceListener(this);
// We reset the input selection at activate to have all inputs selected, otherwise the user
// might get confused if he had deselected inputs earlier and cannot spend the full balance.
bsqUtxoCandidates = null;
btcUtxoCandidates = null;
onUpdateBalances();
}
private void onUpdateBalances() {
onUpdateBalances(bsqWalletService.getAvailableConfirmedBalance(),
onUpdateBalances(getSpendableBsqBalance(),
bsqWalletService.getAvailableNonBsqBalance(),
bsqWalletService.getUnverifiedBalance(),
bsqWalletService.getUnconfirmedChangeBalance(),
@ -200,6 +232,11 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
btcAmountInputTextField.textProperty().removeListener(inputTextFieldListener);
bsqWalletService.removeBsqBalanceListener(this);
sendBsqButton.setOnAction(null);
btcInputControlButton.setOnAction(null);
sendBtcButton.setOnAction(null);
bsqInputControlButton.setOnAction(null);
}
@Override
@ -210,16 +247,24 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
Coin lockedForVotingBalance,
Coin lockupBondsBalance,
Coin unlockingBondsBalance) {
updateBsqValidator(availableConfirmedBalance);
updateBtcValidator(availableNonBsqBalance);
setSendBtcGroupVisibleState(availableNonBsqBalance.isPositive());
}
private void updateBsqValidator(Coin availableConfirmedBalance) {
bsqValidator.setAvailableBalance(availableConfirmedBalance);
boolean isValid = bsqAddressValidator.validate(receiversAddressInputTextField.getText()).isValid &&
bsqValidator.validate(amountInputTextField.getText()).isValid;
sendBsqButton.setDisable(!isValid);
}
boolean isBtcValid = btcAddressValidator.validate(receiversBtcAddressInputTextField.getText()).isValid &&
private void updateBtcValidator(Coin availableConfirmedBalance) {
btcValidator.setMaxValue(availableConfirmedBalance);
boolean isValid = btcAddressValidator.validate(receiversBtcAddressInputTextField.getText()).isValid &&
btcValidator.validate(btcAmountInputTextField.getText()).isValid;
sendBtcButton.setDisable(!isBtcValid);
setSendBtcGroupVisibleState(availableNonBsqBalance.isPositive());
sendBtcButton.setDisable(!isValid);
}
private void addSendBsqGroup() {
@ -240,9 +285,10 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
onUpdateBalances();
};
sendBsqButton = addButtonAfterGroup(root, ++gridRow, Res.get("dao.wallet.send.send"));
sendBsqButton.setOnAction((event) -> onSendBsq());
Tuple2<Button, Button> tuple = FormBuilder.add2ButtonsAfterGroup(root, ++gridRow,
Res.get("dao.wallet.send.send"), Res.get("dao.wallet.send.inputControl"));
sendBsqButton = tuple.first;
bsqInputControlButton = tuple.second;
}
private void onSendBsq() {
@ -253,7 +299,8 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
String receiversAddressString = bsqFormatter.getAddressFromBsqAddress(receiversAddressInputTextField.getText()).toString();
Coin receiverAmount = ParsingUtils.parseToCoin(amountInputTextField.getText(), bsqFormatter);
try {
Transaction preparedSendTx = bsqWalletService.getPreparedSendBsqTx(receiversAddressString, receiverAmount);
Transaction preparedSendTx = bsqWalletService.getPreparedSendBsqTx(receiversAddressString,
receiverAmount, bsqUtxoCandidates);
Transaction txWithBtcFee = btcWalletService.completePreparedSendBsqTx(preparedSendTx);
Transaction signedTx = bsqWalletService.signTx(txWithBtcFee);
Coin miningFee = signedTx.getFee();
@ -267,12 +314,11 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
bsqFormatter,
btcFormatter,
() -> {
receiversAddressInputTextField.setValidator(null);
receiversAddressInputTextField.setText("");
receiversAddressInputTextField.setValidator(bsqAddressValidator);
amountInputTextField.setValidator(null);
amountInputTextField.setText("");
amountInputTextField.setValidator(bsqValidator);
receiversAddressInputTextField.resetValidation();
amountInputTextField.resetValidation();
});
} catch (BsqChangeBelowDustException e) {
String msg = Res.get("popup.warning.bsqChangeBelowDustException", bsqFormatter.formatCoinWithCode(e.getOutputValue()));
@ -282,16 +328,49 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
}
}
private void onBsqInputControl() {
List<TransactionOutput> unspentTransactionOutputs = bsqWalletService.getSpendableBsqTransactionOutputs();
if (bsqUtxoCandidates == null) {
bsqUtxoCandidates = new HashSet<>(unspentTransactionOutputs);
} else {
// If we had some selection stored we need to update to already spent entries
bsqUtxoCandidates = bsqUtxoCandidates.stream().
filter(e -> unspentTransactionOutputs.contains(e)).
collect(Collectors.toSet());
}
TxInputSelectionWindow txInputSelectionWindow = new TxInputSelectionWindow(unspentTransactionOutputs,
bsqUtxoCandidates,
preferences,
bsqFormatter);
txInputSelectionWindow.onAction(() -> setBsqUtxoCandidates(txInputSelectionWindow.getCandidates()))
.show();
}
private void setBsqUtxoCandidates(Set<TransactionOutput> candidates) {
this.bsqUtxoCandidates = candidates;
updateBsqValidator(getSpendableBsqBalance());
amountInputTextField.refreshValidation();
}
// We have used input selection it is the sum of our selected inputs, otherwise the availableConfirmedBalance
private Coin getSpendableBsqBalance() {
return bsqUtxoCandidates != null ?
Coin.valueOf(bsqUtxoCandidates.stream().mapToLong(e -> e.getValue().value).sum()) :
bsqWalletService.getAvailableConfirmedBalance();
}
private void setSendBtcGroupVisibleState(boolean visible) {
btcTitledGroupBg.setVisible(visible);
receiversBtcAddressInputTextField.setVisible(visible);
btcAmountInputTextField.setVisible(visible);
sendBtcButton.setVisible(visible);
btcInputControlButton.setVisible(visible);
btcTitledGroupBg.setManaged(visible);
receiversBtcAddressInputTextField.setManaged(visible);
btcAmountInputTextField.setManaged(visible);
sendBtcButton.setManaged(visible);
btcInputControlButton.setManaged(visible);
}
private void addSendBtcGroup() {
@ -306,43 +385,81 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
btcAmountInputTextField.setValidator(btcValidator);
GridPane.setColumnSpan(btcAmountInputTextField, 3);
sendBtcButton = addButtonAfterGroup(root, ++gridRow, Res.get("dao.wallet.send.sendBtc"));
Tuple2<Button, Button> tuple = FormBuilder.add2ButtonsAfterGroup(root, ++gridRow,
Res.get("dao.wallet.send.sendBtc"), Res.get("dao.wallet.send.inputControl"));
sendBtcButton = tuple.first;
btcInputControlButton = tuple.second;
}
sendBtcButton.setOnAction((event) -> {
if (GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, walletsSetup)) {
String receiversAddressString = receiversBtcAddressInputTextField.getText();
Coin receiverAmount = bsqFormatter.parseToBTC(btcAmountInputTextField.getText());
try {
Transaction preparedSendTx = bsqWalletService.getPreparedSendBtcTx(receiversAddressString, receiverAmount);
Transaction txWithBtcFee = btcWalletService.completePreparedSendBsqTx(preparedSendTx);
Transaction signedTx = bsqWalletService.signTx(txWithBtcFee);
Coin miningFee = signedTx.getFee();
private void onBtcInputControl() {
List<TransactionOutput> unspentTransactionOutputs = bsqWalletService.getSpendableNonBsqTransactionOutputs();
if (btcUtxoCandidates == null) {
btcUtxoCandidates = new HashSet<>(unspentTransactionOutputs);
} else {
// If we had some selection stored we need to update to already spent entries
btcUtxoCandidates = btcUtxoCandidates.stream().
filter(e -> unspentTransactionOutputs.contains(e)).
collect(Collectors.toSet());
}
TxInputSelectionWindow txInputSelectionWindow = new TxInputSelectionWindow(unspentTransactionOutputs,
btcUtxoCandidates,
preferences,
btcFormatter);
txInputSelectionWindow.onAction(() -> setBtcUtxoCandidates(txInputSelectionWindow.getCandidates())).
show();
}
if (miningFee.getValue() >= receiverAmount.getValue())
GUIUtil.showWantToBurnBTCPopup(miningFee, receiverAmount, btcFormatter);
else {
int txVsize = signedTx.getVsize();
showPublishTxPopup(receiverAmount,
txWithBtcFee,
TxType.INVALID,
miningFee,
txVsize, receiversBtcAddressInputTextField.getText(),
btcFormatter,
btcFormatter,
() -> {
receiversBtcAddressInputTextField.setText("");
btcAmountInputTextField.setText("");
});
private void setBtcUtxoCandidates(Set<TransactionOutput> candidates) {
this.btcUtxoCandidates = candidates;
updateBtcValidator(getSpendableBtcBalance());
btcAmountInputTextField.refreshValidation();
}
}
} catch (BsqChangeBelowDustException e) {
String msg = Res.get("popup.warning.btcChangeBelowDustException", btcFormatter.formatCoinWithCode(e.getOutputValue()));
new Popup().warning(msg).show();
} catch (Throwable t) {
handleError(t);
}
private Coin getSpendableBtcBalance() {
return btcUtxoCandidates != null ?
Coin.valueOf(btcUtxoCandidates.stream().mapToLong(e -> e.getValue().value).sum()) :
bsqWalletService.getAvailableNonBsqBalance();
}
private void onSendBtc() {
if (!GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, walletsSetup)) {
return;
}
String receiversAddressString = receiversBtcAddressInputTextField.getText();
Coin receiverAmount = bsqFormatter.parseToBTC(btcAmountInputTextField.getText());
try {
Transaction preparedSendTx = bsqWalletService.getPreparedSendBtcTx(receiversAddressString, receiverAmount, btcUtxoCandidates);
Transaction txWithBtcFee = btcWalletService.completePreparedSendBsqTx(preparedSendTx);
Transaction signedTx = bsqWalletService.signTx(txWithBtcFee);
Coin miningFee = signedTx.getFee();
if (miningFee.getValue() >= receiverAmount.getValue())
GUIUtil.showWantToBurnBTCPopup(miningFee, receiverAmount, btcFormatter);
else {
int txVsize = signedTx.getVsize();
showPublishTxPopup(receiverAmount,
txWithBtcFee,
TxType.INVALID,
miningFee,
txVsize, receiversBtcAddressInputTextField.getText(),
btcFormatter,
btcFormatter,
() -> {
receiversBtcAddressInputTextField.setText("");
btcAmountInputTextField.setText("");
receiversBtcAddressInputTextField.resetValidation();
btcAmountInputTextField.resetValidation();
});
}
});
} catch (BsqChangeBelowDustException e) {
String msg = Res.get("popup.warning.btcChangeBelowDustException", btcFormatter.formatCoinWithCode(e.getOutputValue()));
new Popup().warning(msg).show();
} catch (Throwable t) {
handleError(t);
}
}
private void handleError(Throwable t) {
@ -415,4 +532,3 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
walletsManager.publishAndCommitBsqTx(txWithBtcFee, txType, callback);
}
}

View file

@ -0,0 +1,246 @@
/*
* 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.desktop.main.overlays.windows;
import bisq.desktop.components.AutoTooltipCheckBox;
import bisq.desktop.components.AutoTooltipLabel;
import bisq.desktop.components.AutoTooltipTableColumn;
import bisq.desktop.components.BalanceTextField;
import bisq.desktop.components.ExternalHyperlink;
import bisq.desktop.components.HyperlinkWithIcon;
import bisq.desktop.main.overlays.Overlay;
import bisq.desktop.util.FormBuilder;
import bisq.desktop.util.GUIUtil;
import bisq.desktop.util.Layout;
import bisq.core.locale.Res;
import bisq.core.user.Preferences;
import bisq.core.util.coin.CoinFormatter;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.TransactionOutput;
import javafx.scene.control.CheckBox;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.geometry.Insets;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.SortedList;
import javafx.util.Callback;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.Getter;
import lombok.Setter;
public class TxInputSelectionWindow extends Overlay<TxInputSelectionWindow> {
private static class TransactionOutputItem {
@Getter
private final TransactionOutput transactionOutput;
@Getter
@Setter
private boolean isSelected;
public TransactionOutputItem(TransactionOutput transactionOutput, boolean isSelected) {
this.transactionOutput = transactionOutput;
this.isSelected = isSelected;
}
}
private final List<TransactionOutput> spendableTransactionOutputs;
@Getter
private final Set<TransactionOutput> candidates;
private final Preferences preferences;
private final CoinFormatter formatter;
private BalanceTextField balanceTextField;
private TableView<TransactionOutputItem> tableView;
public TxInputSelectionWindow(List<TransactionOutput> spendableTransactionOutputs,
Set<TransactionOutput> candidates,
Preferences preferences,
CoinFormatter formatter) {
this.spendableTransactionOutputs = spendableTransactionOutputs;
this.candidates = candidates;
this.preferences = preferences;
this.formatter = formatter;
type = Type.Attention;
}
public void show() {
rowIndex = 0;
width = 900;
if (headLine == null) {
headLine = Res.get("inputControlWindow.headline");
}
createGridPane();
gridPane.setHgap(15);
addHeadLine();
addContent();
addButtons();
addDontShowAgainCheckBox();
applyStyles();
display();
}
protected void addContent() {
tableView = new TableView<>();
tableView.setPlaceholder(new AutoTooltipLabel(Res.get("table.placeholder.noData")));
tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
GridPane.setRowIndex(tableView, rowIndex++);
GridPane.setMargin(tableView, new Insets(Layout.GROUP_DISTANCE, 0, 0, 0));
GridPane.setColumnSpan(tableView, 2);
GridPane.setVgrow(tableView, Priority.ALWAYS);
gridPane.getChildren().add(tableView);
createColumns();
ObservableList<TransactionOutputItem> items = FXCollections.observableArrayList(spendableTransactionOutputs.stream()
.map(transactionOutput -> new TransactionOutputItem(transactionOutput, candidates.contains(transactionOutput)))
.collect(Collectors.toList()));
tableView.setItems(new SortedList<>(items));
GUIUtil.setFitToRowsForTableView(tableView, 26, 28, 0, items.size());
balanceTextField = FormBuilder.addBalanceTextField(gridPane, rowIndex++, Res.get("inputControlWindow.balanceLabel"), Layout.FIRST_ROW_DISTANCE);
balanceTextField.setFormatter(formatter);
updateBalance();
}
private void updateBalance() {
balanceTextField.setBalance(Coin.valueOf(candidates.stream()
.mapToLong(transactionOutput -> transactionOutput.getValue().value)
.sum()));
}
private void onChangeCheckBox(TransactionOutputItem transactionOutputItem) {
if (transactionOutputItem.isSelected()) {
candidates.add(transactionOutputItem.getTransactionOutput());
} else {
candidates.remove(transactionOutputItem.getTransactionOutput());
}
updateBalance();
}
private void createColumns() {
TableColumn<TransactionOutputItem, TransactionOutputItem> column;
column = new AutoTooltipTableColumn<>(Res.get("shared.select"));
column.getStyleClass().add("first-column");
column.setSortable(false);
column.setMinWidth(60);
column.setMaxWidth(column.getMinWidth());
column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue()));
column.setCellFactory(
new Callback<>() {
@Override
public TableCell<TransactionOutputItem, TransactionOutputItem> call(
TableColumn<TransactionOutputItem, TransactionOutputItem> column) {
return new TableCell<>() {
@Override
public void updateItem(TransactionOutputItem item, boolean empty) {
super.updateItem(item, empty);
final CheckBox checkBox = new AutoTooltipCheckBox();
if (item != null && !empty) {
checkBox.setSelected(item.isSelected());
checkBox.setOnAction(e -> {
item.setSelected(checkBox.isSelected());
onChangeCheckBox(item);
});
setGraphic(checkBox);
} else {
if (checkBox != null) {
checkBox.setOnAction(null);
}
setGraphic(null);
}
}
};
}
});
tableView.getColumns().add(column);
column = new AutoTooltipTableColumn<>(Res.get("shared.balance"));
column.setMinWidth(100);
column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue()));
column.setCellFactory(
new Callback<>() {
@Override
public TableCell<TransactionOutputItem, TransactionOutputItem> call(
TableColumn<TransactionOutputItem, TransactionOutputItem> column) {
return new TableCell<>() {
@Override
public void updateItem(TransactionOutputItem item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
setText(formatter.formatCoinWithCode(item.getTransactionOutput().getValue()));
} else {
setText("");
}
}
};
}
});
tableView.getColumns().add(column);
column = new AutoTooltipTableColumn<>(Res.get("shared.utxo"));
column.setSortable(false);
column.setMinWidth(550);
column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue()));
column.setCellFactory(
new Callback<>() {
@Override
public TableCell<TransactionOutputItem, TransactionOutputItem> call(
TableColumn<TransactionOutputItem, TransactionOutputItem> column) {
return new TableCell<>() {
private HyperlinkWithIcon hyperlinkWithIcon;
@Override
public void updateItem(TransactionOutputItem item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
TransactionOutput transactionOutput = item.getTransactionOutput();
String txId = transactionOutput.getParentTransaction().getTxId().toString();
hyperlinkWithIcon = new ExternalHyperlink(txId + ":" + transactionOutput.getIndex());
hyperlinkWithIcon.setOnAction(event -> GUIUtil.openWebPage(preferences.getBsqBlockChainExplorer().txUrl + txId, false));
hyperlinkWithIcon.setTooltip(new Tooltip(Res.get("tooltip.openBlockchainForTx", txId)));
setGraphic(hyperlinkWithIcon);
} else {
if (hyperlinkWithIcon != null) {
hyperlinkWithIcon.setOnAction(null);
}
setGraphic(null);
}
}
};
}
});
tableView.getColumns().add(column);
}
}

View file

@ -1681,11 +1681,14 @@ public class FormBuilder {
public static BalanceTextField addBalanceTextField(GridPane gridPane, int rowIndex, String title) {
return addBalanceTextField(gridPane, rowIndex, title, 20);
}
public static BalanceTextField addBalanceTextField(GridPane gridPane, int rowIndex, String title, double top) {
BalanceTextField balanceTextField = new BalanceTextField(title);
GridPane.setRowIndex(balanceTextField, rowIndex);
GridPane.setColumnIndex(balanceTextField, 0);
GridPane.setMargin(balanceTextField, new Insets(20, 0, 0, 0));
GridPane.setMargin(balanceTextField, new Insets(top, 0, 0, 0));
gridPane.getChildren().add(balanceTextField);
return balanceTextField;

View file

@ -16,6 +16,7 @@ public class JFXInputValidator extends ValidatorBase {
}
public void resetValidation() {
message.set(null);
hasErrors.set(false);
}

View file

@ -21,6 +21,10 @@ public class LengthValidator extends InputValidator {
ValidationResult result = new ValidationResult(true);
int length = (input == null) ? 0 : input.length();
if (this.minLength == this.maxLength) {
if (length != this.minLength)
result = new ValidationResult(false, Res.get("validation.fixedLength", this.minLength));
} else
if (length < this.minLength || length > this.maxLength)
result = new ValidationResult(false, Res.get("validation.length", this.minLength, this.maxLength));

View file

@ -69,5 +69,4 @@ public class LengthValidatorTest {
assertFalse(validator2.validate(null).isValid); // too short
assertFalse(validator2.validate("123456789").isValid); // too long
}
}