From fc8af8c072af444caafe5b91abce0fa06cd4e78f Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 13 Nov 2020 11:01:02 -0300 Subject: [PATCH 01/52] Define new grpc bsq/btc balances protos This change adds proto serivces and messages to support new api implementations for serving bsq, btc or all wallet balances. - RPC GetBsqBalances will return complete BSQ balance info. - Message BsqBalanceInfo is proto returned by rpc GetBsqBalances. - RPC GetBtcBalances wil return complete BTC balance info. - Message BtcBalanceInfo is proto returned by rpc GetBtcBalances. - RPC GetBalances returns complete BTC and BSQ balance info. - Message BalancesInfo is proto returned by rpc GetBalances. RPC GetBalance remains unchnaged, still returns only the available BTC balance. It may be deprecated and removed in a future PR. --- proto/src/main/proto/grpc.proto | 48 +++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index d9e0a3973d8..901c94afa9e 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -275,6 +275,12 @@ message TradeInfo { service Wallets { rpc GetBalance (GetBalanceRequest) returns (GetBalanceReply) { } + rpc GetBalances (GetBalancesRequest) returns (GetBalancesReply) { + } + rpc GetBsqBalances (GetBsqBalancesRequest) returns (GetBsqBalancesReply) { + } + rpc GetBtcBalances (GetBtcBalancesRequest) returns (GetBtcBalancesReply) { + } rpc GetAddressBalance (GetAddressBalanceRequest) returns (GetAddressBalanceReply) { } rpc GetFundingAddresses (GetFundingAddressesRequest) returns (GetFundingAddressesReply) { @@ -296,6 +302,27 @@ message GetBalanceReply { uint64 balance = 1; } +message GetBalancesRequest { +} + +message GetBalancesReply { + BalancesInfo balances = 1; +} + +message GetBsqBalancesRequest { +} + +message GetBsqBalancesReply { + BsqBalanceInfo bsqBalanceInfo = 1; +} + +message GetBtcBalancesRequest { +} + +message GetBtcBalancesReply { + BtcBalanceInfo btcBalanceInfo = 1; +} + message GetAddressBalanceRequest { string address = 1; } @@ -340,6 +367,27 @@ message UnlockWalletRequest { message UnlockWalletReply { } +message BalancesInfo { + BsqBalanceInfo bsqBalanceInfo = 1; + BtcBalanceInfo btcBalanceInfo = 2; +} + +message BsqBalanceInfo { + uint64 availableConfirmedBalance = 1; + uint64 unverifiedBalance = 2; + uint64 unconfirmedChangeBalance = 3; + uint64 lockedForVotingBalance = 4; + uint64 lockupBondsBalance = 5; + uint64 unlockingBondsBalance = 6; +} + +message BtcBalanceInfo { + uint64 availableBalance = 1; + uint64 reservedBalance = 2; + uint64 totalAvailableBalance = 3; + uint64 lockedBalance = 4; +} + message AddressBalanceInfo { string address = 1; int64 balance = 2; From faf45ec301974b5a45af84b1e2246a6d2c1d02f4 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 13 Nov 2020 11:24:08 -0300 Subject: [PATCH 02/52] Add proto wrappers for serving bsq, btc or all balances This change adds proto wrappers for sending bsq, btc, or all balances to the CLI. They will be used in future api method implementations: getbsqbalance, getbtcbalance and getbalances. --- .../bisq/core/api/model/BalancesInfo.java | 42 +++++++++ .../bisq/core/api/model/BsqBalanceInfo.java | 87 +++++++++++++++++++ .../bisq/core/api/model/BtcBalanceInfo.java | 70 +++++++++++++++ 3 files changed, 199 insertions(+) create mode 100644 core/src/main/java/bisq/core/api/model/BalancesInfo.java create mode 100644 core/src/main/java/bisq/core/api/model/BsqBalanceInfo.java create mode 100644 core/src/main/java/bisq/core/api/model/BtcBalanceInfo.java diff --git a/core/src/main/java/bisq/core/api/model/BalancesInfo.java b/core/src/main/java/bisq/core/api/model/BalancesInfo.java new file mode 100644 index 00000000000..39179fde2b3 --- /dev/null +++ b/core/src/main/java/bisq/core/api/model/BalancesInfo.java @@ -0,0 +1,42 @@ +package bisq.core.api.model; + +import bisq.common.Payload; + +import lombok.Getter; + +@Getter +public class BalancesInfo implements Payload { + + private final BsqBalanceInfo bsqBalanceInfo; + private final BtcBalanceInfo btcBalanceInfo; + + public BalancesInfo(BsqBalanceInfo bsqBalanceInfo, BtcBalanceInfo btcBalanceInfo) { + this.bsqBalanceInfo = bsqBalanceInfo; + this.btcBalanceInfo = btcBalanceInfo; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public bisq.proto.grpc.BalancesInfo toProtoMessage() { + return bisq.proto.grpc.BalancesInfo.newBuilder() + .setBsqBalanceInfo(bsqBalanceInfo.toProtoMessage()) + .setBtcBalanceInfo(btcBalanceInfo.toProtoMessage()) + .build(); + } + + public static BalancesInfo fromProto(bisq.proto.grpc.BalancesInfo proto) { + return new BalancesInfo(BsqBalanceInfo.fromProto(proto.getBsqBalanceInfo()), + BtcBalanceInfo.fromProto(proto.getBtcBalanceInfo())); + } + + @Override + public String toString() { + return "BalancesInfo{" + "\n" + + " " + bsqBalanceInfo.toString() + "\n" + + ", " + btcBalanceInfo.toString() + "\n" + + '}'; + } +} diff --git a/core/src/main/java/bisq/core/api/model/BsqBalanceInfo.java b/core/src/main/java/bisq/core/api/model/BsqBalanceInfo.java new file mode 100644 index 00000000000..ff314e087ba --- /dev/null +++ b/core/src/main/java/bisq/core/api/model/BsqBalanceInfo.java @@ -0,0 +1,87 @@ +package bisq.core.api.model; + +import bisq.common.Payload; + +import com.google.common.annotations.VisibleForTesting; + +import lombok.Getter; + +@Getter +public class BsqBalanceInfo implements Payload { + + // All balances are in BSQ satoshis. + private final long availableConfirmedBalance; + private final long unverifiedBalance; + private final long unconfirmedChangeBalance; + private final long lockedForVotingBalance; + private final long lockupBondsBalance; + private final long unlockingBondsBalance; + + public BsqBalanceInfo(long availableConfirmedBalance, + long unverifiedBalance, + long unconfirmedChangeBalance, + long lockedForVotingBalance, + long lockupBondsBalance, + long unlockingBondsBalance) { + this.availableConfirmedBalance = availableConfirmedBalance; + this.unverifiedBalance = unverifiedBalance; + this.unconfirmedChangeBalance = unconfirmedChangeBalance; + this.lockedForVotingBalance = lockedForVotingBalance; + this.lockupBondsBalance = lockupBondsBalance; + this.unlockingBondsBalance = unlockingBondsBalance; + } + + @VisibleForTesting + public static BsqBalanceInfo valueOf(long availableConfirmedBalance, + long unverifiedBalance, + long unconfirmedChangeBalance, + long lockedForVotingBalance, + long lockupBondsBalance, + long unlockingBondsBalance) { + // Convenience for creating a model instance instead of a proto. + return new BsqBalanceInfo(availableConfirmedBalance, + unverifiedBalance, + unconfirmedChangeBalance, + lockedForVotingBalance, + lockupBondsBalance, + unlockingBondsBalance); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public bisq.proto.grpc.BsqBalanceInfo toProtoMessage() { + return bisq.proto.grpc.BsqBalanceInfo.newBuilder() + .setAvailableConfirmedBalance(availableConfirmedBalance) + .setUnverifiedBalance(unverifiedBalance) + .setUnconfirmedChangeBalance(unconfirmedChangeBalance) + .setLockedForVotingBalance(lockedForVotingBalance) + .setLockupBondsBalance(lockupBondsBalance) + .setUnlockingBondsBalance(unlockingBondsBalance) + .build(); + + } + + public static BsqBalanceInfo fromProto(bisq.proto.grpc.BsqBalanceInfo proto) { + return new BsqBalanceInfo(proto.getAvailableConfirmedBalance(), + proto.getUnverifiedBalance(), + proto.getUnconfirmedChangeBalance(), + proto.getLockedForVotingBalance(), + proto.getLockupBondsBalance(), + proto.getUnlockingBondsBalance()); + } + + @Override + public String toString() { + return "BsqBalanceInfo{" + + "availableConfirmedBalance=" + availableConfirmedBalance + + ", unverifiedBalance=" + unverifiedBalance + + ", unconfirmedChangeBalance=" + unconfirmedChangeBalance + + ", lockedForVotingBalance=" + lockedForVotingBalance + + ", lockupBondsBalance=" + lockupBondsBalance + + ", unlockingBondsBalance=" + unlockingBondsBalance + + '}'; + } +} diff --git a/core/src/main/java/bisq/core/api/model/BtcBalanceInfo.java b/core/src/main/java/bisq/core/api/model/BtcBalanceInfo.java new file mode 100644 index 00000000000..6e8708c3e37 --- /dev/null +++ b/core/src/main/java/bisq/core/api/model/BtcBalanceInfo.java @@ -0,0 +1,70 @@ +package bisq.core.api.model; + +import bisq.common.Payload; + +import com.google.common.annotations.VisibleForTesting; + +import lombok.Getter; + +@Getter +public class BtcBalanceInfo implements Payload { + + // All balances are in BTC satoshis. + private final long availableBalance; + private final long reservedBalance; + private final long totalAvailableBalance; // available + reserved + private final long lockedBalance; + + public BtcBalanceInfo(long availableBalance, + long reservedBalance, + long totalAvailableBalance, + long lockedBalance) { + this.availableBalance = availableBalance; + this.reservedBalance = reservedBalance; + this.totalAvailableBalance = totalAvailableBalance; + this.lockedBalance = lockedBalance; + } + + @VisibleForTesting + public static BtcBalanceInfo valueOf(long availableBalance, + long reservedBalance, + long totalAvailableBalance, + long lockedBalance) { + // Convenience for creating a model instance instead of a proto. + return new BtcBalanceInfo(availableBalance, + reservedBalance, + totalAvailableBalance, + lockedBalance); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public bisq.proto.grpc.BtcBalanceInfo toProtoMessage() { + return bisq.proto.grpc.BtcBalanceInfo.newBuilder() + .setAvailableBalance(availableBalance) + .setReservedBalance(reservedBalance) + .setTotalAvailableBalance(totalAvailableBalance) + .setLockedBalance(lockedBalance) + .build(); + } + + public static BtcBalanceInfo fromProto(bisq.proto.grpc.BtcBalanceInfo proto) { + return new BtcBalanceInfo(proto.getAvailableBalance(), + proto.getReservedBalance(), + proto.getTotalAvailableBalance(), + proto.getLockedBalance()); + } + + @Override + public String toString() { + return "BtcBalanceInfo{" + + "availableBalance=" + availableBalance + + ", reservedBalance=" + reservedBalance + + ", totalAvailableBalance=" + totalAvailableBalance + + ", lockedBalance=" + lockedBalance + + '}'; + } +} From 4c03b463fefb472cf518f055ab17201bec57d352 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 13 Nov 2020 11:35:06 -0300 Subject: [PATCH 03/52] Define proto for api method 'getunusedbsqaddress' This change adds a proto to support a future api implementation for getting an unused, bsq funding address. --- proto/src/main/proto/grpc.proto | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index 901c94afa9e..8f9ca52813c 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -283,6 +283,8 @@ service Wallets { } rpc GetAddressBalance (GetAddressBalanceRequest) returns (GetAddressBalanceReply) { } + rpc GetUnusedBsqAddress (GetUnusedBsqAddressRequest) returns (GetUnusedBsqAddressReply) { + } rpc GetFundingAddresses (GetFundingAddressesRequest) returns (GetFundingAddressesReply) { } rpc SetWalletPassword (SetWalletPasswordRequest) returns (SetWalletPasswordReply) { @@ -331,6 +333,13 @@ message GetAddressBalanceReply { AddressBalanceInfo addressBalanceInfo = 1; } +message GetUnusedBsqAddressRequest { +} + +message GetUnusedBsqAddressReply { + string address = 1; +} + message GetFundingAddressesRequest { } From 3e98910cc6e620ee2a56d001de9b53d984d25498 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 13 Nov 2020 11:44:42 -0300 Subject: [PATCH 04/52] Define proto fapi method 'sendbsq' This change adds the proto to support a future api implementation for transfering bsq. --- proto/src/main/proto/grpc.proto | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index 8f9ca52813c..b18dab2e7ae 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -285,6 +285,8 @@ service Wallets { } rpc GetUnusedBsqAddress (GetUnusedBsqAddressRequest) returns (GetUnusedBsqAddressReply) { } + rpc SendBsq (SendBsqRequest) returns (SendBsqReply) { + } rpc GetFundingAddresses (GetFundingAddressesRequest) returns (GetFundingAddressesReply) { } rpc SetWalletPassword (SetWalletPasswordRequest) returns (SetWalletPasswordReply) { @@ -340,6 +342,14 @@ message GetUnusedBsqAddressReply { string address = 1; } +message SendBsqRequest { + string address = 1; + double amount = 2; +} + +message SendBsqReply { +} + message GetFundingAddressesRequest { } From 9f033ee572aa3afb41cf58e7aa6f64e7a7da9223 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 13 Nov 2020 12:03:15 -0300 Subject: [PATCH 05/52] Add proto fields to support trade fee currency choice This change adds proto support for paying maker fees in bsq or btc. The implementation will come in a future PR. - Added makerFeeCurrencyCode field to CreateOfferRequest proto. - Added isCurrencyForMakerFeeBtc field to OfferInfo proto wrapper. - Add takerFeeCurrencyCode field to TakeOfferRequest proto. --- proto/src/main/proto/grpc.proto | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index b18dab2e7ae..5005ad1eef8 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -81,6 +81,7 @@ message CreateOfferRequest { uint64 minAmount = 7; double buyerSecurityDeposit = 8; string paymentAccountId = 9; + string makerFeeCurrencyCode = 10; } message CreateOfferReply { @@ -105,13 +106,14 @@ message OfferInfo { uint64 volume = 8; uint64 minVolume = 9; uint64 buyerSecurityDeposit = 10; - string paymentAccountId = 11; - string paymentMethodId = 12; - string paymentMethodShortName = 13; - string baseCurrencyCode = 14; - string counterCurrencyCode = 15; - uint64 date = 16; - string state = 17; + bool isCurrencyForMakerFeeBtc = 11; + string paymentAccountId = 12; + string paymentMethodId = 13; + string paymentMethodShortName = 14; + string baseCurrencyCode = 15; + string counterCurrencyCode = 16; + uint64 date = 17; + string state = 18; } /////////////////////////////////////////////////////////////////////////////////////////// @@ -198,6 +200,7 @@ service Trades { message TakeOfferRequest { string offerId = 1; string paymentAccountId = 2; + string takerFeeCurrencyCode = 3; } message TakeOfferReply { From c1c099c8320c5f19bed6e7b4ac1c1f6594be0d78 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 13 Nov 2020 12:40:16 -0300 Subject: [PATCH 06/52] Implement and test api method 'getunusedbsqaddress' - Added new method to CLI, split some long msg strings into two lines, and added a white space after a braceless else statement. - Added the gRPC server boilerplate. - Added the core implementation. - Added a test, and moved method wallet tests into their own package. --- apitest/docs/build-run.md | 2 +- .../java/bisq/apitest/method/MethodTest.java | 5 ++ .../apitest/method/wallet/BsqWalletTest.java | 59 +++++++++++++++++++ .../method/{ => wallet}/GetBalanceTest.java | 6 +- .../{ => wallet}/WalletProtectionTest.java | 6 +- .../bisq/apitest/scenario/WalletTest.java | 2 +- cli/src/main/java/bisq/cli/CliMain.java | 21 +++++-- core/src/main/java/bisq/core/api/CoreApi.java | 4 ++ .../bisq/core/api/CoreWalletsService.java | 8 +++ .../bisq/daemon/grpc/GrpcWalletsService.java | 19 ++++++ 10 files changed, 124 insertions(+), 8 deletions(-) create mode 100644 apitest/src/test/java/bisq/apitest/method/wallet/BsqWalletTest.java rename apitest/src/test/java/bisq/apitest/method/{ => wallet}/GetBalanceTest.java (96%) rename apitest/src/test/java/bisq/apitest/method/{ => wallet}/WalletProtectionTest.java (98%) diff --git a/apitest/docs/build-run.md b/apitest/docs/build-run.md index 308fe02cf66..9d1165ca88e 100644 --- a/apitest/docs/build-run.md +++ b/apitest/docs/build-run.md @@ -48,7 +48,7 @@ To run all test cases in a package: To run a single test case: - $ ./gradlew :apitest:test --tests "bisq.apitest.method.GetBalanceTest" -DrunApiTests=true + $ ./gradlew :apitest:test --tests "bisq.apitest.method.wallet.GetBalanceTest" -DrunApiTests=true To run test cases from Intellij, add two JVM arguments to your JUnit launchers: diff --git a/apitest/src/test/java/bisq/apitest/method/MethodTest.java b/apitest/src/test/java/bisq/apitest/method/MethodTest.java index 43073ba995b..39cb0b5e971 100644 --- a/apitest/src/test/java/bisq/apitest/method/MethodTest.java +++ b/apitest/src/test/java/bisq/apitest/method/MethodTest.java @@ -26,6 +26,7 @@ import bisq.proto.grpc.GetOfferRequest; import bisq.proto.grpc.GetPaymentAccountsRequest; import bisq.proto.grpc.GetTradeRequest; +import bisq.proto.grpc.GetUnusedBsqAddressRequest; import bisq.proto.grpc.KeepFundsRequest; import bisq.proto.grpc.LockWalletRequest; import bisq.proto.grpc.MarketPriceRequest; @@ -107,6 +108,10 @@ protected final GetBalanceRequest createBalanceRequest() { return GetBalanceRequest.newBuilder().build(); } + protected final GetUnusedBsqAddressRequest createGetUnusedBsqAddressRequest() { + return GetUnusedBsqAddressRequest.newBuilder().build(); + } + protected final SetWalletPasswordRequest createSetWalletPasswordRequest(String password) { return SetWalletPasswordRequest.newBuilder().setPassword(password).build(); } diff --git a/apitest/src/test/java/bisq/apitest/method/wallet/BsqWalletTest.java b/apitest/src/test/java/bisq/apitest/method/wallet/BsqWalletTest.java new file mode 100644 index 00000000000..b4ae0bff0b8 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/wallet/BsqWalletTest.java @@ -0,0 +1,59 @@ +package bisq.apitest.method.wallet; + +import org.bitcoinj.core.LegacyAddress; +import org.bitcoinj.core.NetworkParameters; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +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 org.bitcoinj.core.NetworkParameters.PAYMENT_PROTOCOL_ID_MAINNET; +import static org.bitcoinj.core.NetworkParameters.PAYMENT_PROTOCOL_ID_REGTEST; +import static org.bitcoinj.core.NetworkParameters.PAYMENT_PROTOCOL_ID_TESTNET; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; + + + +import bisq.apitest.method.MethodTest; + +// @Disabled +@Slf4j +@TestMethodOrder(OrderAnnotation.class) +public class BsqWalletTest extends MethodTest { + + @BeforeAll + public static void setUp() { + startSupportingApps(false, + true, + bitcoind, + seednode, + alicedaemon); + } + + @Test + @Order(1) + public void testGetUnusedBsqAddress() { + var request = createGetUnusedBsqAddressRequest(); + + String address = grpcStubs(alicedaemon).walletsService.getUnusedBsqAddress(request).getAddress(); + assertFalse(address.isEmpty()); + assertTrue(address.startsWith("B")); + + NetworkParameters networkParameters = LegacyAddress.getParametersFromAddress(address.substring(1)); + String addressNetwork = networkParameters.getPaymentProtocolId(); + assertNotEquals(PAYMENT_PROTOCOL_ID_MAINNET, addressNetwork); + // TODO Fix bug(?) causing the regtest bsq address network to be evaluated as 'testnet' here. + assertTrue(addressNetwork.equals(PAYMENT_PROTOCOL_ID_TESTNET) + || addressNetwork.equals(PAYMENT_PROTOCOL_ID_REGTEST)); + } + +} diff --git a/apitest/src/test/java/bisq/apitest/method/GetBalanceTest.java b/apitest/src/test/java/bisq/apitest/method/wallet/GetBalanceTest.java similarity index 96% rename from apitest/src/test/java/bisq/apitest/method/GetBalanceTest.java rename to apitest/src/test/java/bisq/apitest/method/wallet/GetBalanceTest.java index 1d44590837b..b31ccd99e02 100644 --- a/apitest/src/test/java/bisq/apitest/method/GetBalanceTest.java +++ b/apitest/src/test/java/bisq/apitest/method/wallet/GetBalanceTest.java @@ -15,7 +15,7 @@ * along with Bisq. If not, see . */ -package bisq.apitest.method; +package bisq.apitest.method.wallet; import bisq.proto.grpc.GetBalanceRequest; @@ -36,6 +36,10 @@ import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; + + +import bisq.apitest.method.MethodTest; + @Disabled @Slf4j @TestMethodOrder(OrderAnnotation.class) diff --git a/apitest/src/test/java/bisq/apitest/method/WalletProtectionTest.java b/apitest/src/test/java/bisq/apitest/method/wallet/WalletProtectionTest.java similarity index 98% rename from apitest/src/test/java/bisq/apitest/method/WalletProtectionTest.java rename to apitest/src/test/java/bisq/apitest/method/wallet/WalletProtectionTest.java index 08547e9ebb9..82174f5195a 100644 --- a/apitest/src/test/java/bisq/apitest/method/WalletProtectionTest.java +++ b/apitest/src/test/java/bisq/apitest/method/wallet/WalletProtectionTest.java @@ -1,4 +1,4 @@ -package bisq.apitest.method; +package bisq.apitest.method.wallet; import io.grpc.StatusRuntimeException; @@ -18,6 +18,10 @@ import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; + + +import bisq.apitest.method.MethodTest; + @SuppressWarnings("ResultOfMethodCallIgnored") @Disabled @Slf4j diff --git a/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java b/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java index ecd38dc2295..36e16142eaf 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java @@ -35,7 +35,7 @@ import bisq.apitest.method.MethodTest; -import bisq.apitest.method.WalletProtectionTest; +import bisq.apitest.method.wallet.WalletProtectionTest; @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index ec0e5e71bb6..cfeb1981db9 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -29,6 +29,7 @@ import bisq.proto.grpc.GetOffersRequest; import bisq.proto.grpc.GetPaymentAccountsRequest; import bisq.proto.grpc.GetTradeRequest; +import bisq.proto.grpc.GetUnusedBsqAddressRequest; import bisq.proto.grpc.GetVersionRequest; import bisq.proto.grpc.KeepFundsRequest; import bisq.proto.grpc.LockWalletRequest; @@ -90,6 +91,7 @@ private enum Method { getbalance, getaddressbalance, getfundingaddresses, + getunusedbsqaddress, lockwallet, unlockwallet, removewalletpassword, @@ -205,6 +207,12 @@ public static void run(String[] args) { out.println(formatAddressBalanceTbl(reply.getAddressBalanceInfoList())); return; } + case getunusedbsqaddress: { + var request = GetUnusedBsqAddressRequest.newBuilder().build(); + var reply = walletsService.getUnusedBsqAddress(request); + out.println(reply.getAddress()); + return; + } case createoffer: { if (nonOptionArgs.size() < 9) throw new IllegalArgumentException("incorrect parameter count," @@ -223,6 +231,7 @@ public static void run(String[] args) { marketPriceMargin = new BigDecimal(nonOptionArgs.get(7)); else fixedPrice = nonOptionArgs.get(7); + var securityDeposit = new BigDecimal(nonOptionArgs.get(8)); var request = CreateOfferRequest.newBuilder() @@ -283,7 +292,8 @@ public static void run(String[] args) { } case takeoffer: { if (nonOptionArgs.size() < 3) - throw new IllegalArgumentException("incorrect parameter count, expecting offer id, payment acct id"); + throw new IllegalArgumentException("incorrect parameter count, " + + " expecting offer id, payment acct id"); var offerId = nonOptionArgs.get(1); var paymentAccountId = nonOptionArgs.get(2); @@ -297,7 +307,8 @@ public static void run(String[] args) { } case gettrade: { if (nonOptionArgs.size() < 2) - throw new IllegalArgumentException("incorrect parameter count, expecting trade id, [,showcontract = true|false]"); + throw new IllegalArgumentException("incorrect parameter count, " + + " expecting trade id [,showcontract = true|false]"); var tradeId = nonOptionArgs.get(1); var showContract = false; @@ -352,7 +363,8 @@ public static void run(String[] args) { } case withdrawfunds: { if (nonOptionArgs.size() < 3) - throw new IllegalArgumentException("incorrect parameter count, expecting trade id, bitcoin wallet address"); + throw new IllegalArgumentException("incorrect parameter count, " + + " expecting trade id, bitcoin wallet address"); var tradeId = nonOptionArgs.get(1); var address = nonOptionArgs.get(2); @@ -485,6 +497,7 @@ private static void printHelp(OptionParser parser, PrintStream stream) { stream.format(rowFormat, "getbalance", "", "Get server wallet balance"); stream.format(rowFormat, "getaddressbalance", "address", "Get server wallet address balance"); stream.format(rowFormat, "getfundingaddresses", "", "Get BTC funding addresses"); + stream.format(rowFormat, "getunusedbsqaddress", "", "Get unused BSQ address"); stream.format(rowFormat, "createoffer", "payment acct id, buy | sell, currency code, \\", "Create and place an offer"); stream.format(rowFormat, "", "amount (btc), min amount, use mkt based price, \\", ""); stream.format(rowFormat, "", "fixed price (btc) | mkt price margin (%), \\", ""); @@ -493,7 +506,7 @@ private static void printHelp(OptionParser parser, PrintStream stream) { stream.format(rowFormat, "getoffer", "offer id", "Get current offer with id"); stream.format(rowFormat, "getoffers", "buy | sell, currency code", "Get current offers"); stream.format(rowFormat, "takeoffer", "offer id", "Take offer with id"); - stream.format(rowFormat, "gettrade", "trade id [,showcontract]", "Get trade summary or full contract"); + stream.format(rowFormat, "gettrade", "trade id [,showcontract = true|false]", "Get trade summary or full contract"); stream.format(rowFormat, "confirmpaymentstarted", "trade id", "Confirm payment started"); stream.format(rowFormat, "confirmpaymentreceived", "trade id", "Confirm payment received"); stream.format(rowFormat, "keepfunds", "trade id", "Keep received funds in Bisq wallet"); diff --git a/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java index 7cfcc5ce152..4e569d8a342 100644 --- a/core/src/main/java/bisq/core/api/CoreApi.java +++ b/core/src/main/java/bisq/core/api/CoreApi.java @@ -229,6 +229,10 @@ public List getFundingAddresses() { return walletsService.getFundingAddresses(); } + public String getUnusedBsqAddress() { + return walletsService.getUnusedBsqAddress(); + } + public void setWalletPassword(String password, String newPassword) { walletsService.setWalletPassword(password, newPassword); } diff --git a/core/src/main/java/bisq/core/api/CoreWalletsService.java b/core/src/main/java/bisq/core/api/CoreWalletsService.java index fc15ec5062a..37ec60d0d52 100644 --- a/core/src/main/java/bisq/core/api/CoreWalletsService.java +++ b/core/src/main/java/bisq/core/api/CoreWalletsService.java @@ -20,6 +20,7 @@ import bisq.core.api.model.AddressBalanceInfo; import bisq.core.btc.Balances; import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.BsqWalletService; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.WalletsManager; @@ -55,6 +56,7 @@ class CoreWalletsService { private final Balances balances; private final WalletsManager walletsManager; + private final BsqWalletService bsqWalletService; private final BtcWalletService btcWalletService; @Nullable @@ -66,9 +68,11 @@ class CoreWalletsService { @Inject public CoreWalletsService(Balances balances, WalletsManager walletsManager, + BsqWalletService bsqWalletService, BtcWalletService btcWalletService) { this.balances = balances; this.walletsManager = walletsManager; + this.bsqWalletService = bsqWalletService; this.btcWalletService = btcWalletService; } @@ -134,6 +138,10 @@ List getFundingAddresses() { .collect(Collectors.toList()); } + String getUnusedBsqAddress() { + return bsqWalletService.getUnusedBsqAddressAsString(); + } + int getNumConfirmationsForMostRecentTransaction(String addressString) { Address address = getAddressEntry(addressString).getAddress(); TransactionConfidence confidence = btcWalletService.getConfidenceForAddress(address); diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java index 1b5cb42e4cc..81d0c9d3730 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java @@ -26,6 +26,8 @@ import bisq.proto.grpc.GetBalanceRequest; import bisq.proto.grpc.GetFundingAddressesReply; import bisq.proto.grpc.GetFundingAddressesRequest; +import bisq.proto.grpc.GetUnusedBsqAddressReply; +import bisq.proto.grpc.GetUnusedBsqAddressRequest; import bisq.proto.grpc.LockWalletReply; import bisq.proto.grpc.LockWalletRequest; import bisq.proto.grpc.RemoveWalletPasswordReply; @@ -109,6 +111,23 @@ public void getFundingAddresses(GetFundingAddressesRequest req, throw ex; } } + + @Override + public void getUnusedBsqAddress(GetUnusedBsqAddressRequest req, + StreamObserver responseObserver) { + try { + String address = coreApi.getUnusedBsqAddress(); + var reply = GetUnusedBsqAddressReply.newBuilder() + .setAddress(address) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (IllegalStateException cause) { + var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage())); + responseObserver.onError(ex); + throw ex; + } + } @Override public void setWalletPassword(SetWalletPasswordRequest req, From 7c2068e3c1d3a53ce45124c8a728a4aeec96d28a Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 13 Nov 2020 12:45:40 -0300 Subject: [PATCH 07/52] Add teardown to test case --- .../test/java/bisq/apitest/method/wallet/BsqWalletTest.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apitest/src/test/java/bisq/apitest/method/wallet/BsqWalletTest.java b/apitest/src/test/java/bisq/apitest/method/wallet/BsqWalletTest.java index b4ae0bff0b8..406308a919c 100644 --- a/apitest/src/test/java/bisq/apitest/method/wallet/BsqWalletTest.java +++ b/apitest/src/test/java/bisq/apitest/method/wallet/BsqWalletTest.java @@ -5,6 +5,7 @@ import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -56,4 +57,8 @@ public void testGetUnusedBsqAddress() { || addressNetwork.equals(PAYMENT_PROTOCOL_ID_REGTEST)); } + @AfterAll + public static void tearDown() { + tearDownScaffold(); + } } From 8dc1a74c8b5e11312a5b3e9db5581981dc70c68b Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 13 Nov 2020 12:55:48 -0300 Subject: [PATCH 08/52] Remove trailing spaces in blank line --- daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java index 81d0c9d3730..3e636a663d8 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java @@ -111,7 +111,7 @@ public void getFundingAddresses(GetFundingAddressesRequest req, throw ex; } } - + @Override public void getUnusedBsqAddress(GetUnusedBsqAddressRequest req, StreamObserver responseObserver) { From 208a37b339378d28cf06de2c2b7232b37527b14f Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 13 Nov 2020 14:21:26 -0300 Subject: [PATCH 09/52] Implement and test new getbalance(s) api methods - Added three new methods to CLI: getbalances ... returns complete bsq and btc balance info getbsqbalance ... returns complete bsq balance info getbtcbalance ... returns complete btc balance info The old getbalance method is deprecated and will be removed if there is agreement to do that. - Made the needed changes in the CLI's output formatting classes. - Added new tests to existing BsqWalletTest, added new BtcWalletTest and WalletBalancesTest. - Added disabled tests for funding a bsq wallet (todo in next PR). --- .../java/bisq/apitest/method/MethodTest.java | 67 ++++++- .../apitest/method/wallet/BsqWalletTest.java | 188 +++++++++++++++++- .../apitest/method/wallet/BtcWalletTest.java | 102 ++++++++++ .../apitest/method/wallet/GetBalanceTest.java | 1 + .../method/wallet/WalletBalancesTest.java | 79 ++++++++ .../bisq/apitest/scenario/WalletTest.java | 58 ++++-- cli/src/main/java/bisq/cli/CliMain.java | 33 ++- .../java/bisq/cli/ColumnHeaderConstants.java | 13 +- .../main/java/bisq/cli/CurrencyFormat.java | 9 +- cli/src/main/java/bisq/cli/TableFormat.java | 76 +++++-- core/src/main/java/bisq/core/api/CoreApi.java | 16 ++ .../bisq/core/api/CoreWalletsService.java | 54 +++++ .../bisq/daemon/grpc/GrpcWalletsService.java | 64 +++++- 13 files changed, 706 insertions(+), 54 deletions(-) create mode 100644 apitest/src/test/java/bisq/apitest/method/wallet/BtcWalletTest.java create mode 100644 apitest/src/test/java/bisq/apitest/method/wallet/WalletBalancesTest.java diff --git a/apitest/src/test/java/bisq/apitest/method/MethodTest.java b/apitest/src/test/java/bisq/apitest/method/MethodTest.java index 39cb0b5e971..f9275ffbee5 100644 --- a/apitest/src/test/java/bisq/apitest/method/MethodTest.java +++ b/apitest/src/test/java/bisq/apitest/method/MethodTest.java @@ -17,11 +17,19 @@ package bisq.apitest.method; +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.GetBalanceRequest; +import bisq.proto.grpc.GetBalancesRequest; +import bisq.proto.grpc.GetBsqBalancesRequest; +import bisq.proto.grpc.GetBtcBalancesRequest; import bisq.proto.grpc.GetFundingAddressesRequest; import bisq.proto.grpc.GetOfferRequest; import bisq.proto.grpc.GetPaymentAccountsRequest; @@ -33,6 +41,7 @@ import bisq.proto.grpc.OfferInfo; import bisq.proto.grpc.RegisterDisputeAgentRequest; import bisq.proto.grpc.RemoveWalletPasswordRequest; +import bisq.proto.grpc.SendBsqRequest; import bisq.proto.grpc.SetWalletPasswordRequest; import bisq.proto.grpc.TakeOfferRequest; import bisq.proto.grpc.TradeInfo; @@ -104,12 +113,25 @@ public static void startSupportingApps(boolean registerDisputeAgents, // Convenience methods for building gRPC request objects + @Deprecated protected final GetBalanceRequest createBalanceRequest() { return GetBalanceRequest.newBuilder().build(); } - protected final GetUnusedBsqAddressRequest createGetUnusedBsqAddressRequest() { - return GetUnusedBsqAddressRequest.newBuilder().build(); + protected final GetBalancesRequest createGetBalancesRequest() { + return GetBalancesRequest.newBuilder().build(); + } + + protected final GetAddressBalanceRequest createGetAddressBalanceRequest(String address) { + return GetAddressBalanceRequest.newBuilder().setAddress(address).build(); + } + + protected final GetBsqBalancesRequest createGetBsqBalancesRequest() { + return GetBsqBalancesRequest.newBuilder().build(); + } + + protected final GetBtcBalancesRequest createBtcBalancesRequest() { + return GetBtcBalancesRequest.newBuilder().build(); } protected final SetWalletPasswordRequest createSetWalletPasswordRequest(String password) { @@ -132,6 +154,14 @@ protected final LockWalletRequest createLockWalletRequest() { return LockWalletRequest.newBuilder().build(); } + protected final GetUnusedBsqAddressRequest createGetUnusedBsqAddressRequest() { + return GetUnusedBsqAddressRequest.newBuilder().build(); + } + + protected final SendBsqRequest createSendBsqRequest(String address, double amount) { + return SendBsqRequest.newBuilder().setAddress(address).setAmount(amount).build(); + } + protected final GetFundingAddressesRequest createGetFundingAddressesRequest() { return GetFundingAddressesRequest.newBuilder().build(); } @@ -148,8 +178,12 @@ protected final CancelOfferRequest createCancelOfferRequest(String offerId) { return CancelOfferRequest.newBuilder().setId(offerId).build(); } - protected final TakeOfferRequest createTakeOfferRequest(String offerId, String paymentAccountId) { - return TakeOfferRequest.newBuilder().setOfferId(offerId).setPaymentAccountId(paymentAccountId).build(); + protected final TakeOfferRequest createTakeOfferRequest(String offerId, + String paymentAccountId) { + return TakeOfferRequest.newBuilder() + .setOfferId(offerId) + .setPaymentAccountId(paymentAccountId) + .build(); } protected final GetTradeRequest createGetTradeRequest(String tradeId) { @@ -179,10 +213,27 @@ protected final WithdrawFundsRequest createWithdrawFundsRequest(String tradeId, // Convenience methods for calling frequently used & thoroughly tested gRPC services. + @Deprecated protected final long getBalance(BisqAppConfig bisqAppConfig) { return grpcStubs(bisqAppConfig).walletsService.getBalance(createBalanceRequest()).getBalance(); } + protected final BalancesInfo getBalances(BisqAppConfig bisqAppConfig) { + return grpcStubs(bisqAppConfig).walletsService.getBalances(createGetBalancesRequest()).getBalances(); + } + + protected final BsqBalanceInfo getBsqBalances(BisqAppConfig bisqAppConfig) { + return grpcStubs(bisqAppConfig).walletsService.getBsqBalances(createGetBsqBalancesRequest()).getBsqBalanceInfo(); + } + + protected final BtcBalanceInfo getBtcBalances(BisqAppConfig bisqAppConfig) { + return grpcStubs(bisqAppConfig).walletsService.getBtcBalances(createBtcBalancesRequest()).getBtcBalanceInfo(); + } + + 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)); @@ -193,6 +244,14 @@ protected final void lockWallet(BisqAppConfig bisqAppConfig) { grpcStubs(bisqAppConfig).walletsService.lockWallet(createLockWalletRequest()); } + protected final String getUnusedBsqAddress(BisqAppConfig bisqAppConfig) { + return grpcStubs(bisqAppConfig).walletsService.getUnusedBsqAddress(createGetUnusedBsqAddressRequest()).getAddress(); + } + + protected final void sendBsq(BisqAppConfig bisqAppConfig, String address, double amount) { + grpcStubs(bisqAppConfig).walletsService.sendBsq(createSendBsqRequest(address, amount)); + } + protected final String getUnusedBtcAddress(BisqAppConfig bisqAppConfig) { //noinspection OptionalGetWithoutIsPresent return grpcStubs(bisqAppConfig).walletsService.getFundingAddresses(createGetFundingAddressesRequest()) diff --git a/apitest/src/test/java/bisq/apitest/method/wallet/BsqWalletTest.java b/apitest/src/test/java/bisq/apitest/method/wallet/BsqWalletTest.java index 406308a919c..a9bf4ca09e3 100644 --- a/apitest/src/test/java/bisq/apitest/method/wallet/BsqWalletTest.java +++ b/apitest/src/test/java/bisq/apitest/method/wallet/BsqWalletTest.java @@ -1,5 +1,7 @@ package bisq.apitest.method.wallet; +import bisq.proto.grpc.BsqBalanceInfo; + import org.bitcoinj.core.LegacyAddress; import org.bitcoinj.core.NetworkParameters; @@ -7,16 +9,22 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestMethodOrder; import static 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.cli.TableFormat.formatBsqBalanceInfoTbl; import static org.bitcoinj.core.NetworkParameters.PAYMENT_PROTOCOL_ID_MAINNET; import static org.bitcoinj.core.NetworkParameters.PAYMENT_PROTOCOL_ID_REGTEST; import static org.bitcoinj.core.NetworkParameters.PAYMENT_PROTOCOL_ID_TESTNET; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -24,20 +32,43 @@ +import bisq.apitest.config.BisqAppConfig; import bisq.apitest.method.MethodTest; -// @Disabled +@Disabled @Slf4j @TestMethodOrder(OrderAnnotation.class) public class BsqWalletTest extends MethodTest { + // Alice's regtest BSQ wallet is initialized with 1,000,000 BSQ. + private static final bisq.core.api.model.BsqBalanceInfo ALICES_INITIAL_BSQ_BALANCES = + expectedBsqBalanceModel(100000000, + 0, + 0, + 0, + 0, + 0); + + // Bob's regtest BSQ wallet is initialized with 1,500,000 BSQ. + private static final bisq.core.api.model.BsqBalanceInfo BOBS_INITIAL_BSQ_BALANCES = + expectedBsqBalanceModel(150000000, + 0, + 0, + 0, + 0, + 0); + + private static final double SEND_BSQ_AMOUNT = 25000.50; + @BeforeAll public static void setUp() { startSupportingApps(false, true, bitcoind, seednode, - alicedaemon); + arbdaemon, + alicedaemon, + bobdaemon); } @Test @@ -52,13 +83,164 @@ public void testGetUnusedBsqAddress() { NetworkParameters networkParameters = LegacyAddress.getParametersFromAddress(address.substring(1)); String addressNetwork = networkParameters.getPaymentProtocolId(); assertNotEquals(PAYMENT_PROTOCOL_ID_MAINNET, addressNetwork); - // TODO Fix bug(?) causing the regtest bsq address network to be evaluated as 'testnet' here. + // TODO Fix bug causing the regtest bsq address network to be evaluated as 'testnet' here. assertTrue(addressNetwork.equals(PAYMENT_PROTOCOL_ID_TESTNET) || addressNetwork.equals(PAYMENT_PROTOCOL_ID_REGTEST)); } + @Test + @Order(2) + public void testInitialBsqBalances(final TestInfo testInfo) { + BsqBalanceInfo alicesBsqBalances = getBsqBalances(alicedaemon); + log.info("{} -> Alice's BSQ Initial Balances -> \n{}", + testName(testInfo), + formatBsqBalanceInfoTbl(alicesBsqBalances)); + verifyBsqBalances(ALICES_INITIAL_BSQ_BALANCES, alicesBsqBalances); + + BsqBalanceInfo bobsBsqBalances = getBsqBalances(bobdaemon); + log.info("{} -> Bob's BSQ Initial Balances -> \n{}", + testName(testInfo), + formatBsqBalanceInfoTbl(bobsBsqBalances)); + verifyBsqBalances(BOBS_INITIAL_BSQ_BALANCES, bobsBsqBalances); + } + + @Disabled // TODO + @Test + @Order(3) + public void testSendBsqAndCheckBalancesBeforeGeneratingBtcBlock(final TestInfo testInfo) { + String bobsBsqAddress = getUnusedBsqAddress(bobdaemon); + sendBsq(alicedaemon, bobsBsqAddress, SEND_BSQ_AMOUNT); + sleep(2000); + + BsqBalanceInfo alicesBsqBalances = getBsqBalances(alicedaemon); + BsqBalanceInfo bobsBsqBalances = waitForNonZeroUnverifiedBalance(bobdaemon); + + log.info("BSQ Balances Before BTC Block Gen..."); + printBobAndAliceBsqBalances(testInfo, + bobsBsqBalances, + alicesBsqBalances, + alicedaemon); + + verifyBsqBalances(expectedBsqBalanceModel(150000000, + 2500050, + 0, + 0, + 0, + 0), + bobsBsqBalances); + + verifyBsqBalances(expectedBsqBalanceModel(97499950, + 97499950, + 97499950, + 0, + 0, + 0), + alicesBsqBalances); + } + + @Disabled // TODO + @Test + @Order(4) + public void testBalancesAfterSendingBsqAndGeneratingBtcBlock(final TestInfo testInfo) { + // There is a wallet persist delay; we have to + // wait for both wallets to be saved to disk. + genBtcBlocksThenWait(1, 4000); + + BsqBalanceInfo alicesBsqBalances = getBsqBalances(alicedaemon); + BsqBalanceInfo bobsBsqBalances = waitForNewAvailableConfirmedBalance(bobdaemon, 150000000); + + log.info("See Available Confirmed BSQ Balances..."); + printBobAndAliceBsqBalances(testInfo, + bobsBsqBalances, + alicesBsqBalances, + alicedaemon); + + verifyBsqBalances(expectedBsqBalanceModel(152500050, + 0, + 0, + 0, + 0, + 0), + bobsBsqBalances); + + verifyBsqBalances(expectedBsqBalanceModel(97499950, + 0, + 0, + 0, + 0, + 0), + alicesBsqBalances); + } + @AfterAll public static void tearDown() { tearDownScaffold(); } + + private void verifyBsqBalances(bisq.core.api.model.BsqBalanceInfo expected, + bisq.proto.grpc.BsqBalanceInfo actual) { + assertEquals(expected.getAvailableConfirmedBalance(), actual.getAvailableConfirmedBalance()); + assertEquals(expected.getUnverifiedBalance(), actual.getUnverifiedBalance()); + assertEquals(expected.getUnconfirmedChangeBalance(), actual.getUnconfirmedChangeBalance()); + assertEquals(expected.getLockedForVotingBalance(), actual.getLockedForVotingBalance()); + assertEquals(expected.getLockupBondsBalance(), actual.getLockupBondsBalance()); + assertEquals(expected.getUnlockingBondsBalance(), actual.getUnlockingBondsBalance()); + } + + private BsqBalanceInfo waitForNonZeroUnverifiedBalance(BisqAppConfig daemon) { + // 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); + for (int numRequests = 1; numRequests <= 15 && bsqBalance.getUnverifiedBalance() == 0; numRequests++) { + sleep(1000); + bsqBalance = getBsqBalances(daemon); + } + return bsqBalance; + } + + private BsqBalanceInfo waitForNewAvailableConfirmedBalance(BisqAppConfig daemon, + long staleBalance) { + BsqBalanceInfo bsqBalance = getBsqBalances(daemon); + for (int numRequests = 1; + numRequests <= 15 && bsqBalance.getAvailableConfirmedBalance() == staleBalance; + numRequests++) { + sleep(1000); + bsqBalance = getBsqBalances(daemon); + } + return bsqBalance; + } + + @SuppressWarnings("SameParameterValue") + private void printBobAndAliceBsqBalances(final TestInfo testInfo, + BsqBalanceInfo bobsBsqBalances, + BsqBalanceInfo alicesBsqBalances, + BisqAppConfig senderApp) { + log.info("{} -> Bob's BSQ Balances After {} {} BSQ-> \n{}", + testName(testInfo), + senderApp.equals(bobdaemon) ? "Sending" : "Receiving", + SEND_BSQ_AMOUNT, + formatBsqBalanceInfoTbl(bobsBsqBalances)); + + log.info("{} -> Alice's Balances After {} {} BSQ-> \n{}", + testName(testInfo), + senderApp.equals(alicedaemon) ? "Sending" : "Receiving", + SEND_BSQ_AMOUNT, + formatBsqBalanceInfoTbl(alicesBsqBalances)); + } + + @SuppressWarnings("SameParameterValue") + private static bisq.core.api.model.BsqBalanceInfo expectedBsqBalanceModel(long availableConfirmedBalance, + long unverifiedBalance, + long unconfirmedChangeBalance, + long lockedForVotingBalance, + long lockupBondsBalance, + long unlockingBondsBalance) { + return bisq.core.api.model.BsqBalanceInfo.valueOf(availableConfirmedBalance, + unverifiedBalance, + unconfirmedChangeBalance, + lockedForVotingBalance, + lockupBondsBalance, + unlockingBondsBalance); + } } diff --git a/apitest/src/test/java/bisq/apitest/method/wallet/BtcWalletTest.java b/apitest/src/test/java/bisq/apitest/method/wallet/BtcWalletTest.java new file mode 100644 index 00000000000..fe0949f9425 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/wallet/BtcWalletTest.java @@ -0,0 +1,102 @@ +package bisq.apitest.method.wallet; + +import bisq.proto.grpc.BtcBalanceInfo; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.TestMethodOrder; + +import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind; +import static bisq.apitest.config.BisqAppConfig.alicedaemon; +import static bisq.apitest.config.BisqAppConfig.bobdaemon; +import static bisq.apitest.config.BisqAppConfig.seednode; +import static bisq.cli.TableFormat.formatAddressBalanceTbl; +import static bisq.cli.TableFormat.formatBtcBalanceInfoTbl; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; + + + +import bisq.apitest.method.MethodTest; + +@Disabled +@Slf4j +@TestMethodOrder(OrderAnnotation.class) +public class BtcWalletTest extends MethodTest { + + // All api tests depend on the DAO / regtest environment, and Bob & Alice's wallets + // are initialized with 10 BTC during the scaffolding setup. + private static final bisq.core.api.model.BtcBalanceInfo INITIAL_BTC_BALANCES = + bisq.core.api.model.BtcBalanceInfo.valueOf(1000000000, + 0, + 1000000000, + 0); + + @BeforeAll + public static void setUp() { + startSupportingApps(false, + true, + bitcoind, + seednode, + alicedaemon, + bobdaemon); + } + + @Test + @Order(1) + public void testDeprecatedAvailableBtcBalance() { + // Alice's regtest Bisq wallet was initialized with 10 BTC. + long balance = getBalance(alicedaemon); // @Deprecated method + assertEquals(INITIAL_BTC_BALANCES.getAvailableBalance(), balance); + + // Bob's regtest Bisq wallet was initialized with 10 BTC. + balance = getBalance(bobdaemon); // @Deprecated method + assertEquals(INITIAL_BTC_BALANCES.getAvailableBalance(), balance); + } + + @Test + @Order(2) + public void testFundAlicesBtcWallet(final TestInfo testInfo) { + String newAddress = getUnusedBtcAddress(alicedaemon); + bitcoinCli.sendToAddress(newAddress, "2.5"); + genBtcBlocksThenWait(1, 1500); + + long balance = getBalance(alicedaemon); // @Deprecated method + assertEquals(1250000000, balance); // new balance is 12.5 btc + + log.info("{} -> Alice's Funded Address Balance -> \n{}", + testName(testInfo), + formatAddressBalanceTbl(singletonList(getAddressBalance(alicedaemon, newAddress)))); + + BtcBalanceInfo btcBalanceInfo = getBtcBalances(alicedaemon); // new balance is 12.5 btc + bisq.core.api.model.BtcBalanceInfo alicesExpectedBalances = + bisq.core.api.model.BtcBalanceInfo.valueOf(1250000000, + 0, + 1250000000, + 0); + verifyBtcBalances(alicesExpectedBalances, btcBalanceInfo); + log.info("{} -> Alice's BTC Balances After Sending 2.5 BTC -> \n{}", + testName(testInfo), + formatBtcBalanceInfoTbl(btcBalanceInfo)); + } + + @AfterAll + public static void tearDown() { + tearDownScaffold(); + } + + private void verifyBtcBalances(bisq.core.api.model.BtcBalanceInfo expected, + bisq.proto.grpc.BtcBalanceInfo actual) { + assertEquals(expected.getAvailableBalance(), actual.getAvailableBalance()); + assertEquals(expected.getReservedBalance(), actual.getReservedBalance()); + assertEquals(expected.getTotalAvailableBalance(), actual.getTotalAvailableBalance()); + assertEquals(expected.getLockedBalance(), actual.getLockedBalance()); + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/wallet/GetBalanceTest.java b/apitest/src/test/java/bisq/apitest/method/wallet/GetBalanceTest.java index b31ccd99e02..2c7c3466dda 100644 --- a/apitest/src/test/java/bisq/apitest/method/wallet/GetBalanceTest.java +++ b/apitest/src/test/java/bisq/apitest/method/wallet/GetBalanceTest.java @@ -40,6 +40,7 @@ import bisq.apitest.method.MethodTest; +@Deprecated @Disabled @Slf4j @TestMethodOrder(OrderAnnotation.class) diff --git a/apitest/src/test/java/bisq/apitest/method/wallet/WalletBalancesTest.java b/apitest/src/test/java/bisq/apitest/method/wallet/WalletBalancesTest.java new file mode 100644 index 00000000000..6c168843668 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/wallet/WalletBalancesTest.java @@ -0,0 +1,79 @@ +package bisq.apitest.method.wallet; + +import bisq.proto.grpc.BalancesInfo; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +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.bobdaemon; +import static bisq.apitest.config.BisqAppConfig.seednode; +import static org.junit.jupiter.api.Assertions.assertEquals; + + + +import bisq.apitest.method.MethodTest; +import bisq.cli.TableFormat; + +@Disabled +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class WalletBalancesTest extends MethodTest { + + // All api tests depend on the DAO / regtest environment, and Bob & Alice's wallets + // are initialized with 10 BTC during the scaffolding setup. + private static final bisq.core.api.model.BtcBalanceInfo INITIAL_BTC_BALANCES = + bisq.core.api.model.BtcBalanceInfo.valueOf(1000000000, + 0, + 1000000000, + 0); + + @BeforeAll + public static void setUp() { + startSupportingApps(false, + true, + bitcoind, + seednode, + alicedaemon, + bobdaemon); + } + + @Test + @Order(1) + public void testDeprecatedAvailableBtcBalance() { + // Alice's regtest Bisq wallet was initialized with 10 BTC. + long balance = getBalance(alicedaemon); // @Deprecated method + assertEquals(INITIAL_BTC_BALANCES.getAvailableBalance(), balance); + + // Bob's regtest Bisq wallet was initialized with 10 BTC. + balance = getBalance(bobdaemon); // @Deprecated method + assertEquals(INITIAL_BTC_BALANCES.getAvailableBalance(), balance); + } + + @Test + @Order(2) + public void testNewGetBalances(final TestInfo testInfo) { + BalancesInfo alicesBalances = getBalances(alicedaemon); + BalancesInfo bobsBalances = getBalances(bobdaemon); + + log.info("{} Alice's Balances:\n{}", testName(testInfo), TableFormat.formatBalancesTbls(alicesBalances)); + log.info("{} Bob's Balances:\n{}", testName(testInfo), TableFormat.formatBalancesTbls(bobsBalances)); + + assertEquals(INITIAL_BTC_BALANCES.getAvailableBalance(), alicesBalances.getBtcBalanceInfo().getAvailableBalance()); + assertEquals(INITIAL_BTC_BALANCES.getAvailableBalance(), bobsBalances.getBtcBalanceInfo().getAvailableBalance()); + } + + @AfterAll + public static void tearDown() { + tearDownScaffold(); + } +} diff --git a/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java b/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java index 36e16142eaf..f6240512230 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java @@ -24,55 +24,69 @@ import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestMethodOrder; import static 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 org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.fail; import bisq.apitest.method.MethodTest; +import bisq.apitest.method.wallet.BsqWalletTest; +import bisq.apitest.method.wallet.BtcWalletTest; +import bisq.apitest.method.wallet.WalletBalancesTest; import bisq.apitest.method.wallet.WalletProtectionTest; @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class WalletTest extends MethodTest { - // All tests depend on the DAO / regtest environment, and Alice's wallet is - // initialized with 10 BTC during the scaffolding setup. - @BeforeAll public static void setUp() { - try { - setUpScaffold(bitcoind, seednode, alicedaemon); - genBtcBlocksThenWait(1, 1500); - } catch (Exception ex) { - fail(ex); - } + startSupportingApps(true, + true, + bitcoind, + seednode, + arbdaemon, + alicedaemon, + bobdaemon); } @Test @Order(1) - public void testFundWallet() { - // The regtest Bisq wallet was initialized with 10 BTC. - long balance = getBalance(alicedaemon); - assertEquals(1000000000, balance); + public void testGetWalletBalances(final TestInfo testInfo) { + WalletBalancesTest btcWalletTest = new WalletBalancesTest(); + + btcWalletTest.testDeprecatedAvailableBtcBalance(); + btcWalletTest.testNewGetBalances(testInfo); + } - String unusedAddress = getUnusedBtcAddress(alicedaemon); - bitcoinCli.sendToAddress(unusedAddress, "2.5"); + @Test + @Order(2) + public void testBtcWalletFunding(final TestInfo testInfo) { + BtcWalletTest btcWalletTest = new BtcWalletTest(); - bitcoinCli.generateBlocks(1); - sleep(1500); + btcWalletTest.testDeprecatedAvailableBtcBalance(); + btcWalletTest.testFundAlicesBtcWallet(testInfo); + } - balance = getBalance(alicedaemon); - assertEquals(1250000000L, balance); // new balance is 12.5 btc + @Test + @Order(3) + public void testBsqWalletFunding(final TestInfo testInfo) { + BsqWalletTest bsqWalletTest = new BsqWalletTest(); + + bsqWalletTest.testGetUnusedBsqAddress(); + bsqWalletTest.testInitialBsqBalances(testInfo); + //bsqWalletTest.testSendBsqAndCheckBalancesBeforeGeneratingBtcBlock(testInfo); // TODO + //bsqWalletTest.testBalancesAfterSendingBsqAndGeneratingBtcBlock(testInfo); // TODO } @Test - @Order(2) + @Order(4) public void testWalletProtection() { // Batching all wallet tests in this test case reduces scaffold setup // time. Here, we create a method WalletProtectionTest instance and run each diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index cfeb1981db9..2226d352004 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -24,6 +24,9 @@ import bisq.proto.grpc.CreatePaymentAccountRequest; import bisq.proto.grpc.GetAddressBalanceRequest; import bisq.proto.grpc.GetBalanceRequest; +import bisq.proto.grpc.GetBalancesRequest; +import bisq.proto.grpc.GetBsqBalancesRequest; +import bisq.proto.grpc.GetBtcBalancesRequest; import bisq.proto.grpc.GetFundingAddressesRequest; import bisq.proto.grpc.GetOfferRequest; import bisq.proto.grpc.GetOffersRequest; @@ -58,6 +61,7 @@ import static bisq.cli.CurrencyFormat.toSatoshis; import static bisq.cli.NegativeNumberOptions.hasNegativeNumberOptions; import static bisq.cli.TableFormat.formatAddressBalanceTbl; +import static bisq.cli.TableFormat.formatBalancesTbls; import static bisq.cli.TableFormat.formatOfferTable; import static bisq.cli.TableFormat.formatPaymentAcctTbl; import static java.lang.String.format; @@ -88,7 +92,10 @@ private enum Method { createpaymentacct, getpaymentaccts, getversion, - getbalance, + @Deprecated getbalance, // Use getbalances, return bsq and btc balance info + getbalances, + getbsqbalance, + getbtcbalance, getaddressbalance, getfundingaddresses, getunusedbsqaddress, @@ -185,12 +192,31 @@ public static void run(String[] args) { return; } case getbalance: { + // Deprecated, use getbalances. var request = GetBalanceRequest.newBuilder().build(); var reply = walletsService.getBalance(request); var btcBalance = formatSatoshis(reply.getBalance()); out.println(btcBalance); return; } + case getbalances: { + var request = GetBalancesRequest.newBuilder().build(); + var reply = walletsService.getBalances(request); + out.println(formatBalancesTbls(reply.getBalances())); + return; + } + case getbsqbalance: { + var request = GetBsqBalancesRequest.newBuilder().build(); + var reply = walletsService.getBsqBalances(request); + out.println(reply.getBsqBalanceInfo()); + return; + } + case getbtcbalance: { + var request = GetBtcBalancesRequest.newBuilder().build(); + var reply = walletsService.getBtcBalances(request); + out.println(reply.getBtcBalanceInfo()); + return; + } case getaddressbalance: { if (nonOptionArgs.size() < 2) throw new IllegalArgumentException("no address specified"); @@ -494,7 +520,10 @@ private static void printHelp(OptionParser parser, PrintStream stream) { stream.format(rowFormat, "Method", "Params", "Description"); stream.format(rowFormat, "------", "------", "------------"); stream.format(rowFormat, "getversion", "", "Get server version"); - stream.format(rowFormat, "getbalance", "", "Get server wallet balance"); + stream.format(rowFormat, "getbalance", "", "Get server wallet balance (deprecated, use getbalances"); + stream.format(rowFormat, "getbalances", "", "Get server wallet bsq and btc balances"); + stream.format(rowFormat, "getbsqbalance", "", "Get server wallet bsq balance"); + stream.format(rowFormat, "getbtcbalance", "", "Get server wallet btc balance"); stream.format(rowFormat, "getaddressbalance", "address", "Get server wallet address balance"); stream.format(rowFormat, "getfundingaddresses", "", "Get BTC funding addresses"); stream.format(rowFormat, "getunusedbsqaddress", "", "Get unused BSQ address"); diff --git a/cli/src/main/java/bisq/cli/ColumnHeaderConstants.java b/cli/src/main/java/bisq/cli/ColumnHeaderConstants.java index 03500e4f47a..59b6230a2eb 100644 --- a/cli/src/main/java/bisq/cli/ColumnHeaderConstants.java +++ b/cli/src/main/java/bisq/cli/ColumnHeaderConstants.java @@ -29,9 +29,18 @@ class ColumnHeaderConstants { // such as COL_HEADER_CREATION_DATE, COL_HEADER_VOLUME and COL_HEADER_UUID, the // expected max data string length is accounted for. In others, the column header length // are expected to be greater than any column value length. - static final String COL_HEADER_ADDRESS = padEnd("Address", 34, ' '); + static final String COL_HEADER_ADDRESS = padEnd("%-3s Address", 52, ' '); static final String COL_HEADER_AMOUNT = padEnd("BTC(min - max)", 24, ' '); - static final String COL_HEADER_BALANCE = padStart("Balance", 12, ' '); + static final String COL_HEADER_AVAILABLE_BALANCE = "Available Balance"; + static final String COL_HEADER_AVAILABLE_CONFIRMED_BALANCE = "Available Confirmed Balance"; + static final String COL_HEADER_UNCONFIRMED_CHANGE_BALANCE = "Unconfirmed Change Balance"; + static final String COL_HEADER_RESERVED_BALANCE = "Reserved Balance"; + static final String COL_HEADER_TOTAL_AVAILABLE_BALANCE = "Total Available Balance"; + static final String COL_HEADER_LOCKED_BALANCE = "Locked Balance"; + static final String COL_HEADER_LOCKED_FOR_VOTING_BALANCE = "Locked For Voting Balance"; + static final String COL_HEADER_LOCKUP_BONDS_BALANCE = "Lockup Bonds Balance"; + static final String COL_HEADER_UNLOCKING_BONDS_BALANCE = "Unlocking Bonds Balance"; + static final String COL_HEADER_UNVERIFIED_BALANCE = "Unverified Balance"; static final String COL_HEADER_CONFIRMATIONS = "Confirmations"; static final String COL_HEADER_CREATION_DATE = padEnd("Creation Date (UTC)", 20, ' '); static final String COL_HEADER_CURRENCY = "Currency"; diff --git a/cli/src/main/java/bisq/cli/CurrencyFormat.java b/cli/src/main/java/bisq/cli/CurrencyFormat.java index e4d8f89c6c7..a4766690eff 100644 --- a/cli/src/main/java/bisq/cli/CurrencyFormat.java +++ b/cli/src/main/java/bisq/cli/CurrencyFormat.java @@ -37,12 +37,19 @@ public class CurrencyFormat { static final BigDecimal SATOSHI_DIVISOR = new BigDecimal(100000000); static final DecimalFormat BTC_FORMAT = new DecimalFormat("###,##0.00000000"); - @VisibleForTesting + static final BigDecimal BSQ_SATOSHI_DIVISOR = new BigDecimal(100); + static final DecimalFormat BSQ_FORMAT = new DecimalFormat("###,###,###,##0.00"); + @SuppressWarnings("BigDecimalMethodWithoutRoundingCalled") public static String formatSatoshis(long sats) { return BTC_FORMAT.format(BigDecimal.valueOf(sats).divide(SATOSHI_DIVISOR)); } + @SuppressWarnings("BigDecimalMethodWithoutRoundingCalled") + public static String formatBsq(long sats) { + return BSQ_FORMAT.format(BigDecimal.valueOf(sats).divide(BSQ_SATOSHI_DIVISOR)); + } + static String formatAmountRange(long minAmount, long amount) { return minAmount != amount ? formatSatoshis(minAmount) + " - " + formatSatoshis(amount) diff --git a/cli/src/main/java/bisq/cli/TableFormat.java b/cli/src/main/java/bisq/cli/TableFormat.java index 8336fff9ba1..ff4e440f18f 100644 --- a/cli/src/main/java/bisq/cli/TableFormat.java +++ b/cli/src/main/java/bisq/cli/TableFormat.java @@ -18,10 +18,15 @@ 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.OfferInfo; import protobuf.PaymentAccount; +import com.google.common.annotations.VisibleForTesting; + import java.text.SimpleDateFormat; import java.util.Date; @@ -30,28 +35,28 @@ import java.util.stream.Collectors; import static bisq.cli.ColumnHeaderConstants.*; -import static bisq.cli.CurrencyFormat.formatAmountRange; -import static bisq.cli.CurrencyFormat.formatOfferPrice; -import static bisq.cli.CurrencyFormat.formatSatoshis; -import static bisq.cli.CurrencyFormat.formatVolumeRange; +import static bisq.cli.CurrencyFormat.*; import static com.google.common.base.Strings.padEnd; import static java.lang.String.format; import static java.util.Collections.max; import static java.util.Comparator.comparing; import static java.util.TimeZone.getTimeZone; -class TableFormat { +@VisibleForTesting +public class TableFormat { static final TimeZone TZ_UTC = getTimeZone("UTC"); static final SimpleDateFormat DATE_FORMAT_ISO_8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); - static String formatAddressBalanceTbl(List addressBalanceInfo) { - String headerLine = (COL_HEADER_ADDRESS + COL_HEADER_DELIMITER - + COL_HEADER_BALANCE + COL_HEADER_DELIMITER - + COL_HEADER_CONFIRMATIONS + COL_HEADER_DELIMITER + "\n"); - String colDataFormat = "%-" + COL_HEADER_ADDRESS.length() + "s" // left justify - + " %" + COL_HEADER_BALANCE.length() + "s" // right justify - + " %" + COL_HEADER_CONFIRMATIONS.length() + "d"; // right justify + public static String formatAddressBalanceTbl(List addressBalanceInfo) { + String headerFormatString = COL_HEADER_ADDRESS + COL_HEADER_DELIMITER + + COL_HEADER_AVAILABLE_BALANCE + COL_HEADER_DELIMITER + + COL_HEADER_CONFIRMATIONS + COL_HEADER_DELIMITER + "\n"; + String headerLine = format(headerFormatString, "BTC"); + + String colDataFormat = "%-" + COL_HEADER_ADDRESS.length() + "s" // lt justify + + " %" + (COL_HEADER_AVAILABLE_BALANCE.length() - 1) + "s" // rt justify + + " %" + COL_HEADER_CONFIRMATIONS.length() + "d"; // lt justify return headerLine + addressBalanceInfo.stream() .map(info -> format(colDataFormat, @@ -61,15 +66,58 @@ static String formatAddressBalanceTbl(List addressBalanceInf .collect(Collectors.joining("\n")); } - static String formatOfferTable(List offerInfo, String fiatCurrency) { + public static String formatBalancesTbls(BalancesInfo balancesInfo) { + return "BTC" + "\n" + + formatBtcBalanceInfoTbl(balancesInfo.getBtcBalanceInfo()) + "\n" + + "BSQ" + "\n" + + formatBsqBalanceInfoTbl(balancesInfo.getBsqBalanceInfo()); + } + + public static String formatBsqBalanceInfoTbl(BsqBalanceInfo bsqBalanceInfo) { + String headerLine = COL_HEADER_AVAILABLE_CONFIRMED_BALANCE + COL_HEADER_DELIMITER + + COL_HEADER_UNVERIFIED_BALANCE + COL_HEADER_DELIMITER + + COL_HEADER_UNCONFIRMED_CHANGE_BALANCE + COL_HEADER_DELIMITER + + COL_HEADER_LOCKED_FOR_VOTING_BALANCE + COL_HEADER_DELIMITER + + COL_HEADER_LOCKUP_BONDS_BALANCE + COL_HEADER_DELIMITER + + COL_HEADER_UNLOCKING_BONDS_BALANCE + COL_HEADER_DELIMITER + "\n"; + String colDataFormat = "%" + COL_HEADER_AVAILABLE_CONFIRMED_BALANCE.length() + "s" // rt justify + + " %" + (COL_HEADER_UNVERIFIED_BALANCE.length() + 1) + "s" // rt justify + + " %" + (COL_HEADER_UNCONFIRMED_CHANGE_BALANCE.length() + 1) + "s" // rt justify + + " %" + (COL_HEADER_LOCKED_FOR_VOTING_BALANCE.length() + 1) + "s" // rt justify + + " %" + (COL_HEADER_LOCKUP_BONDS_BALANCE.length() + 1) + "s" // rt justify + + " %" + (COL_HEADER_UNLOCKING_BONDS_BALANCE.length() + 1) + "s"; // rt justify + return headerLine + format(colDataFormat, + formatBsq(bsqBalanceInfo.getAvailableConfirmedBalance()), + formatBsq(bsqBalanceInfo.getUnverifiedBalance()), + formatBsq(bsqBalanceInfo.getUnconfirmedChangeBalance()), + formatBsq(bsqBalanceInfo.getLockedForVotingBalance()), + formatBsq(bsqBalanceInfo.getLockupBondsBalance()), + formatBsq(bsqBalanceInfo.getUnlockingBondsBalance())); + } + + public static String formatBtcBalanceInfoTbl(BtcBalanceInfo btcBalanceInfo) { + String headerLine = COL_HEADER_AVAILABLE_BALANCE + COL_HEADER_DELIMITER + + COL_HEADER_RESERVED_BALANCE + COL_HEADER_DELIMITER + + COL_HEADER_TOTAL_AVAILABLE_BALANCE + COL_HEADER_DELIMITER + + COL_HEADER_LOCKED_BALANCE + COL_HEADER_DELIMITER + "\n"; + String colDataFormat = "%" + COL_HEADER_AVAILABLE_BALANCE.length() + "s" // rt justify + + " %" + (COL_HEADER_RESERVED_BALANCE.length() + 1) + "s" // rt justify + + " %" + (COL_HEADER_TOTAL_AVAILABLE_BALANCE.length() + 1) + "s" // rt justify + + " %" + (COL_HEADER_LOCKED_BALANCE.length() + 1) + "s"; // rt justify + return headerLine + format(colDataFormat, + formatSatoshis(btcBalanceInfo.getAvailableBalance()), + formatSatoshis(btcBalanceInfo.getReservedBalance()), + formatSatoshis(btcBalanceInfo.getTotalAvailableBalance()), + formatSatoshis(btcBalanceInfo.getLockedBalance())); + } + static String formatOfferTable(List 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(), offerInfo.stream() .map(OfferInfo::getPaymentMethodShortName) .collect(Collectors.toList())); - String headersFormat = COL_HEADER_DIRECTION + COL_HEADER_DELIMITER + COL_HEADER_PRICE + COL_HEADER_DELIMITER // includes %s -> fiatCurrency + COL_HEADER_AMOUNT + COL_HEADER_DELIMITER diff --git a/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java index 4e569d8a342..bd1ac198a26 100644 --- a/core/src/main/java/bisq/core/api/CoreApi.java +++ b/core/src/main/java/bisq/core/api/CoreApi.java @@ -18,6 +18,9 @@ package bisq.core.api; import bisq.core.api.model.AddressBalanceInfo; +import bisq.core.api.model.BalancesInfo; +import bisq.core.api.model.BsqBalanceInfo; +import bisq.core.api.model.BtcBalanceInfo; import bisq.core.monetary.Price; import bisq.core.offer.Offer; import bisq.core.offer.OfferPayload; @@ -213,10 +216,23 @@ public String getTradeRole(String tradeId) { // Wallets /////////////////////////////////////////////////////////////////////////////////////////// + @Deprecated public long getAvailableBalance() { return walletsService.getAvailableBalance(); } + public BalancesInfo getBalances() { + return walletsService.getBalances(); + } + + public BsqBalanceInfo getBsqBalances() { + return walletsService.getBsqBalances(); + } + + public BtcBalanceInfo getBtcBalances() { + return walletsService.getBtcBalances(); + } + public long getAddressBalance(String addressString) { return walletsService.getAddressBalance(addressString); } diff --git a/core/src/main/java/bisq/core/api/CoreWalletsService.java b/core/src/main/java/bisq/core/api/CoreWalletsService.java index 37ec60d0d52..3284a457730 100644 --- a/core/src/main/java/bisq/core/api/CoreWalletsService.java +++ b/core/src/main/java/bisq/core/api/CoreWalletsService.java @@ -18,6 +18,9 @@ package bisq.core.api; import bisq.core.api.model.AddressBalanceInfo; +import bisq.core.api.model.BalancesInfo; +import bisq.core.api.model.BsqBalanceInfo; +import bisq.core.api.model.BtcBalanceInfo; import bisq.core.btc.Balances; import bisq.core.btc.model.AddressEntry; import bisq.core.btc.wallet.BsqWalletService; @@ -82,6 +85,7 @@ KeyParameter getKey() { return tempAesKey; } + @Deprecated long getAvailableBalance() { verifyWalletsAreAvailable(); verifyEncryptedWalletIsUnlocked(); @@ -93,6 +97,56 @@ long getAvailableBalance() { return balance.getValue(); } + BalancesInfo getBalances() { + verifyWalletsAreAvailable(); + verifyEncryptedWalletIsUnlocked(); + if (balances.getAvailableBalance().get() == null) + throw new IllegalStateException("balance is not yet available"); + + return new BalancesInfo(getBsqBalances(), getBtcBalances()); + } + + BsqBalanceInfo getBsqBalances() { + verifyWalletsAreAvailable(); + verifyEncryptedWalletIsUnlocked(); + + var availableConfirmedBalance = bsqWalletService.getAvailableConfirmedBalance(); + var unverifiedBalance = bsqWalletService.getUnverifiedBalance(); + var unconfirmedChangeBalance = bsqWalletService.getUnconfirmedChangeBalance(); + var lockedForVotingBalance = bsqWalletService.getLockedForVotingBalance(); + var lockupBondsBalance = bsqWalletService.getLockupBondsBalance(); + var unlockingBondsBalance = bsqWalletService.getUnlockingBondsBalance(); + + return new BsqBalanceInfo(availableConfirmedBalance.value, + unverifiedBalance.value, + unconfirmedChangeBalance.value, + lockedForVotingBalance.value, + lockupBondsBalance.value, + unlockingBondsBalance.value); + } + + BtcBalanceInfo getBtcBalances() { + verifyWalletsAreAvailable(); + verifyEncryptedWalletIsUnlocked(); + + var availableBalance = balances.getAvailableBalance().get(); + if (availableBalance == null) + throw new IllegalStateException("balance is not yet available"); + + var reservedBalance = balances.getReservedBalance().get(); + if (reservedBalance == null) + throw new IllegalStateException("reserved balance is not yet available"); + + var lockedBalance = balances.getLockedBalance().get(); + if (lockedBalance == null) + throw new IllegalStateException("locked balance is not yet available"); + + return new BtcBalanceInfo(availableBalance.value, + reservedBalance.value, + availableBalance.add(reservedBalance).value, + lockedBalance.value); + } + long getAddressBalance(String addressString) { Address address = getAddressEntry(addressString).getAddress(); return btcWalletService.getBalanceForAddress(address).value; diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java index 3e636a663d8..2f35f000779 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java @@ -19,11 +19,19 @@ import bisq.core.api.CoreApi; import bisq.core.api.model.AddressBalanceInfo; +import bisq.core.api.model.BsqBalanceInfo; +import bisq.core.api.model.BtcBalanceInfo; import bisq.proto.grpc.GetAddressBalanceReply; import bisq.proto.grpc.GetAddressBalanceRequest; import bisq.proto.grpc.GetBalanceReply; import bisq.proto.grpc.GetBalanceRequest; +import bisq.proto.grpc.GetBalancesReply; +import bisq.proto.grpc.GetBalancesRequest; +import bisq.proto.grpc.GetBsqBalancesReply; +import bisq.proto.grpc.GetBsqBalancesRequest; +import bisq.proto.grpc.GetBtcBalancesReply; +import bisq.proto.grpc.GetBtcBalancesRequest; import bisq.proto.grpc.GetFundingAddressesReply; import bisq.proto.grpc.GetFundingAddressesRequest; import bisq.proto.grpc.GetUnusedBsqAddressReply; @@ -56,12 +64,8 @@ public GrpcWalletsService(CoreApi coreApi) { this.coreApi = coreApi; } - // TODO we need to support 3 or 4 balance types: available, reserved, lockedInTrade - // and maybe total wallet balance (available+reserved). To not duplicate the methods, - // we should pass an enum type. Enums in proto are a bit cumbersome as they are - // global so you quickly run into namespace conflicts if not always prefixes which - // makes it more verbose. In the core code base we move to the strategy to store the - // enum name and map it. This gives also more flexibility with updates. + + @Deprecated @Override public void getBalance(GetBalanceRequest req, StreamObserver responseObserver) { try { @@ -76,6 +80,54 @@ public void getBalance(GetBalanceRequest req, StreamObserver re } } + @Override + public void getBalances(GetBalancesRequest req, StreamObserver responseObserver) { + try { + var balances = coreApi.getBalances(); + var reply = GetBalancesReply.newBuilder() + .setBalances(balances.toProtoMessage()) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (IllegalStateException cause) { + var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage())); + responseObserver.onError(ex); + throw ex; + } + } + + @Override + public void getBsqBalances(GetBsqBalancesRequest req, StreamObserver responseObserver) { + try { + BsqBalanceInfo bsqBalanceInfo = coreApi.getBsqBalances(); + var reply = GetBsqBalancesReply.newBuilder() + .setBsqBalanceInfo(bsqBalanceInfo.toProtoMessage()) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (IllegalStateException cause) { + var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage())); + responseObserver.onError(ex); + throw ex; + } + } + + @Override + public void getBtcBalances(GetBtcBalancesRequest req, StreamObserver responseObserver) { + try { + BtcBalanceInfo btcBalanceInfo = coreApi.getBtcBalances(); + var reply = GetBtcBalancesReply.newBuilder() + .setBtcBalanceInfo(btcBalanceInfo.toProtoMessage()) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (IllegalStateException cause) { + var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage())); + responseObserver.onError(ex); + throw ex; + } + } + @Override public void getAddressBalance(GetAddressBalanceRequest req, StreamObserver responseObserver) { From 7f0f949a2de9fee0cf91c0b8cd9bd5c0904343e7 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 13 Nov 2020 14:35:26 -0300 Subject: [PATCH 10/52] Resolve unnecessary use of fully qualified name for codacy --- .../src/test/java/bisq/apitest/method/wallet/BsqWalletTest.java | 2 +- .../src/test/java/bisq/apitest/method/wallet/BtcWalletTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apitest/src/test/java/bisq/apitest/method/wallet/BsqWalletTest.java b/apitest/src/test/java/bisq/apitest/method/wallet/BsqWalletTest.java index a9bf4ca09e3..861c1839c5b 100644 --- a/apitest/src/test/java/bisq/apitest/method/wallet/BsqWalletTest.java +++ b/apitest/src/test/java/bisq/apitest/method/wallet/BsqWalletTest.java @@ -178,7 +178,7 @@ public static void tearDown() { } private void verifyBsqBalances(bisq.core.api.model.BsqBalanceInfo expected, - bisq.proto.grpc.BsqBalanceInfo actual) { + BsqBalanceInfo actual) { assertEquals(expected.getAvailableConfirmedBalance(), actual.getAvailableConfirmedBalance()); assertEquals(expected.getUnverifiedBalance(), actual.getUnverifiedBalance()); assertEquals(expected.getUnconfirmedChangeBalance(), actual.getUnconfirmedChangeBalance()); diff --git a/apitest/src/test/java/bisq/apitest/method/wallet/BtcWalletTest.java b/apitest/src/test/java/bisq/apitest/method/wallet/BtcWalletTest.java index fe0949f9425..380198d53ee 100644 --- a/apitest/src/test/java/bisq/apitest/method/wallet/BtcWalletTest.java +++ b/apitest/src/test/java/bisq/apitest/method/wallet/BtcWalletTest.java @@ -93,7 +93,7 @@ public static void tearDown() { } private void verifyBtcBalances(bisq.core.api.model.BtcBalanceInfo expected, - bisq.proto.grpc.BtcBalanceInfo actual) { + BtcBalanceInfo actual) { assertEquals(expected.getAvailableBalance(), actual.getAvailableBalance()); assertEquals(expected.getReservedBalance(), actual.getReservedBalance()); assertEquals(expected.getTotalAvailableBalance(), actual.getTotalAvailableBalance()); From 0d3b3a6ead351d62fcdb23be5b8918280cd071a5 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 13 Nov 2020 14:57:28 -0300 Subject: [PATCH 11/52] Stub out api method 'sendbsq' in core All the boilerplate for this method is included in this change, but the implementation in CoreWalletsService#sendBsq is commented out until the needed logic to send bsq is refactored out of desktop's BsqSendView class -- to be included in the next PR. - Added new method to CLI. - Added the gRPC server boilerplate. - Added the core implementation, commented out. - Enabled tests. --- .../apitest/method/wallet/BsqWalletTest.java | 2 - .../bisq/apitest/scenario/WalletTest.java | 4 +- cli/src/main/java/bisq/cli/CliMain.java | 27 +++++++++++++ core/src/main/java/bisq/core/api/CoreApi.java | 5 +++ .../bisq/core/api/CoreWalletsService.java | 25 ++++++++++++ .../bisq/daemon/grpc/GrpcWalletsService.java | 38 +++++++++++++++++++ 6 files changed, 97 insertions(+), 4 deletions(-) diff --git a/apitest/src/test/java/bisq/apitest/method/wallet/BsqWalletTest.java b/apitest/src/test/java/bisq/apitest/method/wallet/BsqWalletTest.java index 861c1839c5b..d074793e5d4 100644 --- a/apitest/src/test/java/bisq/apitest/method/wallet/BsqWalletTest.java +++ b/apitest/src/test/java/bisq/apitest/method/wallet/BsqWalletTest.java @@ -104,7 +104,6 @@ public void testInitialBsqBalances(final TestInfo testInfo) { verifyBsqBalances(BOBS_INITIAL_BSQ_BALANCES, bobsBsqBalances); } - @Disabled // TODO @Test @Order(3) public void testSendBsqAndCheckBalancesBeforeGeneratingBtcBlock(final TestInfo testInfo) { @@ -138,7 +137,6 @@ public void testSendBsqAndCheckBalancesBeforeGeneratingBtcBlock(final TestInfo t alicesBsqBalances); } - @Disabled // TODO @Test @Order(4) public void testBalancesAfterSendingBsqAndGeneratingBtcBlock(final TestInfo testInfo) { diff --git a/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java b/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java index f6240512230..f27f8f93f27 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java @@ -81,8 +81,8 @@ public void testBsqWalletFunding(final TestInfo testInfo) { bsqWalletTest.testGetUnusedBsqAddress(); bsqWalletTest.testInitialBsqBalances(testInfo); - //bsqWalletTest.testSendBsqAndCheckBalancesBeforeGeneratingBtcBlock(testInfo); // TODO - //bsqWalletTest.testBalancesAfterSendingBsqAndGeneratingBtcBlock(testInfo); // TODO + bsqWalletTest.testSendBsqAndCheckBalancesBeforeGeneratingBtcBlock(testInfo); + bsqWalletTest.testBalancesAfterSendingBsqAndGeneratingBtcBlock(testInfo); } @Test diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index 2226d352004..d61b2ba2063 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -38,6 +38,7 @@ import bisq.proto.grpc.LockWalletRequest; import bisq.proto.grpc.RegisterDisputeAgentRequest; import bisq.proto.grpc.RemoveWalletPasswordRequest; +import bisq.proto.grpc.SendBsqRequest; import bisq.proto.grpc.SetWalletPasswordRequest; import bisq.proto.grpc.TakeOfferRequest; import bisq.proto.grpc.UnlockWalletRequest; @@ -99,6 +100,7 @@ private enum Method { getaddressbalance, getfundingaddresses, getunusedbsqaddress, + sendbsq, lockwallet, unlockwallet, removewalletpassword, @@ -239,6 +241,30 @@ public static void run(String[] args) { out.println(reply.getAddress()); return; } + case sendbsq: { + if (nonOptionArgs.size() < 2) + throw new IllegalArgumentException("no bsq address specified"); + + var address = nonOptionArgs.get(1); + + if (nonOptionArgs.size() < 3) + throw new IllegalArgumentException("no bsq amount specified"); + + double amount; + try { + amount = Double.parseDouble(nonOptionArgs.get(2)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(format("'%s' is not a number", nonOptionArgs.get(2))); + } + + var request = SendBsqRequest.newBuilder() + .setAddress(address) + .setAmount(amount) + .build(); + walletsService.sendBsq(request); + out.printf("%.2f BSQ sent to %s%n", amount, address); + return; + } case createoffer: { if (nonOptionArgs.size() < 9) throw new IllegalArgumentException("incorrect parameter count," @@ -527,6 +553,7 @@ private static void printHelp(OptionParser parser, PrintStream stream) { stream.format(rowFormat, "getaddressbalance", "address", "Get server wallet address balance"); stream.format(rowFormat, "getfundingaddresses", "", "Get BTC funding addresses"); stream.format(rowFormat, "getunusedbsqaddress", "", "Get unused BSQ address"); + stream.format(rowFormat, "sendbsq", "address, amount", "Send BSQ"); stream.format(rowFormat, "createoffer", "payment acct id, buy | sell, currency code, \\", "Create and place an offer"); stream.format(rowFormat, "", "amount (btc), min amount, use mkt based price, \\", ""); stream.format(rowFormat, "", "fixed price (btc) | mkt price margin (%), \\", ""); diff --git a/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java index bd1ac198a26..fceb3c47e55 100644 --- a/core/src/main/java/bisq/core/api/CoreApi.java +++ b/core/src/main/java/bisq/core/api/CoreApi.java @@ -21,6 +21,7 @@ import bisq.core.api.model.BalancesInfo; import bisq.core.api.model.BsqBalanceInfo; import bisq.core.api.model.BtcBalanceInfo; +import bisq.core.btc.wallet.TxBroadcaster; import bisq.core.monetary.Price; import bisq.core.offer.Offer; import bisq.core.offer.OfferPayload; @@ -249,6 +250,10 @@ public String getUnusedBsqAddress() { return walletsService.getUnusedBsqAddress(); } + public void sendBsq(String address, double amount, TxBroadcaster.Callback callback) { + walletsService.sendBsq(address, amount, callback); + } + public void setWalletPassword(String password, String newPassword) { walletsService.setWalletPassword(password, newPassword); } diff --git a/core/src/main/java/bisq/core/api/CoreWalletsService.java b/core/src/main/java/bisq/core/api/CoreWalletsService.java index 3284a457730..c4a8fe6a533 100644 --- a/core/src/main/java/bisq/core/api/CoreWalletsService.java +++ b/core/src/main/java/bisq/core/api/CoreWalletsService.java @@ -25,6 +25,7 @@ import bisq.core.btc.model.AddressEntry; import bisq.core.btc.wallet.BsqWalletService; import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.TxBroadcaster; import bisq.core.btc.wallet.WalletsManager; import bisq.common.Timer; @@ -196,6 +197,30 @@ String getUnusedBsqAddress() { return bsqWalletService.getUnusedBsqAddressAsString(); } + @SuppressWarnings("unused") + void sendBsq(String address, + double amount, + TxBroadcaster.Callback callback) { + + throw new UnsupportedOperationException("sendbsq not implemented"); + + // TODO Uncomment after desktop::BsqSendView refactoring. + /* + try { + LegacyAddress legacyAddress = getValidBsqLegacyAddress(address); + Coin receiverAmount = getValidBsqTransferAmount(amount); + BsqTransferModel model = bsqTransferService.getBsqTransferModel(legacyAddress, receiverAmount); + bsqTransferService.sendFunds(model, callback); + } catch (InsufficientMoneyException + | BsqChangeBelowDustException + | TransactionVerificationException + | WalletException ex) { + log.error("", ex); + throw new IllegalStateException(ex); + } + */ + } + int getNumConfirmationsForMostRecentTransaction(String addressString) { Address address = getAddressEntry(addressString).getAddress(); TransactionConfidence confidence = btcWalletService.getConfidenceForAddress(address); diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java index 2f35f000779..31530f3a660 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java @@ -21,6 +21,8 @@ import bisq.core.api.model.AddressBalanceInfo; import bisq.core.api.model.BsqBalanceInfo; import bisq.core.api.model.BtcBalanceInfo; +import bisq.core.btc.exceptions.TxBroadcastException; +import bisq.core.btc.wallet.TxBroadcaster; import bisq.proto.grpc.GetAddressBalanceReply; import bisq.proto.grpc.GetAddressBalanceRequest; @@ -40,6 +42,8 @@ import bisq.proto.grpc.LockWalletRequest; import bisq.proto.grpc.RemoveWalletPasswordReply; import bisq.proto.grpc.RemoveWalletPasswordRequest; +import bisq.proto.grpc.SendBsqReply; +import bisq.proto.grpc.SendBsqRequest; import bisq.proto.grpc.SetWalletPasswordReply; import bisq.proto.grpc.SetWalletPasswordRequest; import bisq.proto.grpc.UnlockWalletReply; @@ -50,11 +54,16 @@ import io.grpc.StatusRuntimeException; import io.grpc.stub.StreamObserver; +import org.bitcoinj.core.Transaction; + import javax.inject.Inject; import java.util.List; import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; + +@Slf4j class GrpcWalletsService extends WalletsGrpc.WalletsImplBase { private final CoreApi coreApi; @@ -181,6 +190,35 @@ public void getUnusedBsqAddress(GetUnusedBsqAddressRequest req, } } + @Override + public void sendBsq(SendBsqRequest req, + StreamObserver responseObserver) { + try { + coreApi.sendBsq(req.getAddress(), req.getAmount(), new TxBroadcaster.Callback() { + @Override + public void onSuccess(Transaction tx) { + log.info("Successfully published BSQ tx: id {}, output sum {} sats, fee {} sats, size {} bytes", + tx.getTxId().toString(), + tx.getOutputSum(), + tx.getFee(), + tx.getMessageSize()); + var reply = SendBsqReply.newBuilder().build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } + + @Override + public void onFailure(TxBroadcastException ex) { + throw new IllegalStateException(ex); + } + }); + } catch (IllegalStateException cause) { + var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage())); + responseObserver.onError(ex); + throw ex; + } + } + @Override public void setWalletPassword(SetWalletPasswordRequest req, StreamObserver responseObserver) { From dc3274ff5f56ba4dd8d40fef536a74c53ebbf65d Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 13 Nov 2020 14:59:31 -0300 Subject: [PATCH 12/52] Re comment sendbsq tests so travis ci does not fail --- apitest/src/test/java/bisq/apitest/scenario/WalletTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java b/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java index f27f8f93f27..6fde51bcc26 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java @@ -81,8 +81,8 @@ public void testBsqWalletFunding(final TestInfo testInfo) { bsqWalletTest.testGetUnusedBsqAddress(); bsqWalletTest.testInitialBsqBalances(testInfo); - bsqWalletTest.testSendBsqAndCheckBalancesBeforeGeneratingBtcBlock(testInfo); - bsqWalletTest.testBalancesAfterSendingBsqAndGeneratingBtcBlock(testInfo); + // bsqWalletTest.testSendBsqAndCheckBalancesBeforeGeneratingBtcBlock(testInfo); + // bsqWalletTest.testBalancesAfterSendingBsqAndGeneratingBtcBlock(testInfo); } @Test From 446bd328d8366b31a9614d958f7d14094b5982f8 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 13 Nov 2020 15:34:18 -0300 Subject: [PATCH 13/52] Refactor desktop's BsqSendView, share with api Moved just enough code out of BsqSendView to avoid desktop/api 'sendbsq' duplication, at the cost of adding 1 new method to BsqSendView. - Created new BsqTransferModel to hold tx details shared by desktop and api. - Created new BsqTransferService to send bsq using a BsqTransferModel shared by desktop and api. - Uncommented CoreWalletsService#sendBsq implementation. - Uncommented sendbsq tests. --- .../bisq/apitest/scenario/WalletTest.java | 4 +- .../bisq/core/api/CoreWalletsService.java | 44 +++++++++-- .../bisq/core/btc/model/BsqTransferModel.java | 77 +++++++++++++++++++ .../core/btc/wallet/BsqTransferService.java | 59 ++++++++++++++ .../main/dao/wallet/send/BsqSendView.java | 41 ++++++---- 5 files changed, 201 insertions(+), 24 deletions(-) create mode 100644 core/src/main/java/bisq/core/btc/model/BsqTransferModel.java create mode 100644 core/src/main/java/bisq/core/btc/wallet/BsqTransferService.java diff --git a/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java b/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java index 6fde51bcc26..f27f8f93f27 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java @@ -81,8 +81,8 @@ public void testBsqWalletFunding(final TestInfo testInfo) { bsqWalletTest.testGetUnusedBsqAddress(); bsqWalletTest.testInitialBsqBalances(testInfo); - // bsqWalletTest.testSendBsqAndCheckBalancesBeforeGeneratingBtcBlock(testInfo); - // bsqWalletTest.testBalancesAfterSendingBsqAndGeneratingBtcBlock(testInfo); + bsqWalletTest.testSendBsqAndCheckBalancesBeforeGeneratingBtcBlock(testInfo); + bsqWalletTest.testBalancesAfterSendingBsqAndGeneratingBtcBlock(testInfo); } @Test diff --git a/core/src/main/java/bisq/core/api/CoreWalletsService.java b/core/src/main/java/bisq/core/api/CoreWalletsService.java index c4a8fe6a533..1842f324c53 100644 --- a/core/src/main/java/bisq/core/api/CoreWalletsService.java +++ b/core/src/main/java/bisq/core/api/CoreWalletsService.java @@ -22,16 +22,25 @@ import bisq.core.api.model.BsqBalanceInfo; import bisq.core.api.model.BtcBalanceInfo; import bisq.core.btc.Balances; +import bisq.core.btc.exceptions.BsqChangeBelowDustException; +import bisq.core.btc.exceptions.TransactionVerificationException; +import bisq.core.btc.exceptions.WalletException; import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.model.BsqTransferModel; +import bisq.core.btc.wallet.BsqTransferService; import bisq.core.btc.wallet.BsqWalletService; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.TxBroadcaster; import bisq.core.btc.wallet.WalletsManager; +import bisq.core.util.coin.BsqFormatter; import bisq.common.Timer; import bisq.common.UserThread; import org.bitcoinj.core.Address; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.core.LegacyAddress; import org.bitcoinj.core.TransactionConfidence; import org.bitcoinj.crypto.KeyCrypterScrypt; @@ -43,6 +52,8 @@ import org.bouncycastle.crypto.params.KeyParameter; +import java.math.BigDecimal; + import java.util.List; import java.util.Optional; import java.util.function.Function; @@ -52,6 +63,7 @@ import javax.annotation.Nullable; +import static bisq.core.util.ParsingUtils.parseToCoin; import static java.lang.String.format; import static java.util.concurrent.TimeUnit.SECONDS; @@ -61,6 +73,8 @@ class CoreWalletsService { private final Balances balances; private final WalletsManager walletsManager; private final BsqWalletService bsqWalletService; + private final BsqTransferService bsqTransferService; + private final BsqFormatter bsqFormatter; private final BtcWalletService btcWalletService; @Nullable @@ -73,10 +87,14 @@ class CoreWalletsService { public CoreWalletsService(Balances balances, WalletsManager walletsManager, BsqWalletService bsqWalletService, + BsqTransferService bsqTransferService, + BsqFormatter bsqFormatter, BtcWalletService btcWalletService) { this.balances = balances; this.walletsManager = walletsManager; this.bsqWalletService = bsqWalletService; + this.bsqTransferService = bsqTransferService; + this.bsqFormatter = bsqFormatter; this.btcWalletService = btcWalletService; } @@ -197,15 +215,9 @@ String getUnusedBsqAddress() { return bsqWalletService.getUnusedBsqAddressAsString(); } - @SuppressWarnings("unused") void sendBsq(String address, double amount, TxBroadcaster.Callback callback) { - - throw new UnsupportedOperationException("sendbsq not implemented"); - - // TODO Uncomment after desktop::BsqSendView refactoring. - /* try { LegacyAddress legacyAddress = getValidBsqLegacyAddress(address); Coin receiverAmount = getValidBsqTransferAmount(amount); @@ -218,7 +230,6 @@ void sendBsq(String address, log.error("", ex); throw new IllegalStateException(ex); } - */ } int getNumConfirmationsForMostRecentTransaction(String addressString) { @@ -331,6 +342,25 @@ private void verifyEncryptedWalletIsUnlocked() { throw new IllegalStateException("wallet is locked"); } + // Returns a LegacyAddress for the string, or a RuntimeException if invalid. + private LegacyAddress getValidBsqLegacyAddress(String address) { + try { + return bsqFormatter.getAddressFromBsqAddress(address); + } catch (Throwable t) { + log.error("", t); + throw new IllegalStateException(format("%s is not a valid bsq address", address)); + } + } + + // Returns a Coin for the double amount, or a RuntimeException if invalid. + private Coin getValidBsqTransferAmount(double amount) { + Coin amountAsCoin = parseToCoin(new BigDecimal(amount).toString(), bsqFormatter); + if (amountAsCoin.equals(Coin.ZERO)) + throw new IllegalStateException(format("%.2f bsq is an invalid send amount", amount)); + + return amountAsCoin; + } + private KeyCrypterScrypt getKeyCrypterScrypt() { KeyCrypterScrypt keyCrypterScrypt = walletsManager.getKeyCrypterScrypt(); if (keyCrypterScrypt == null) diff --git a/core/src/main/java/bisq/core/btc/model/BsqTransferModel.java b/core/src/main/java/bisq/core/btc/model/BsqTransferModel.java new file mode 100644 index 00000000000..3033c769eee --- /dev/null +++ b/core/src/main/java/bisq/core/btc/model/BsqTransferModel.java @@ -0,0 +1,77 @@ +package bisq.core.btc.model; + +import bisq.core.dao.state.model.blockchain.TxType; +import bisq.core.util.coin.CoinUtil; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.LegacyAddress; +import org.bitcoinj.core.Transaction; + +import lombok.Getter; + +@Getter +public final class BsqTransferModel { + + private final LegacyAddress receiverAddress; + private final Coin receiverAmount; + private final Transaction preparedSendTx; + private final Transaction txWithBtcFee; + private final Transaction signedTx; + private final Coin miningFee; + private final int txSize; + private final TxType txType; + + public BsqTransferModel(LegacyAddress receiverAddress, + Coin receiverAmount, + Transaction preparedSendTx, + Transaction txWithBtcFee, + Transaction signedTx) { + this.receiverAddress = receiverAddress; + this.receiverAmount = receiverAmount; + this.preparedSendTx = preparedSendTx; + this.txWithBtcFee = txWithBtcFee; + this.signedTx = signedTx; + this.miningFee = signedTx.getFee(); + this.txSize = signedTx.bitcoinSerialize().length; + this.txType = TxType.TRANSFER_BSQ; + } + + public String getReceiverAddressAsString() { + return receiverAddress.toString(); + } + + public double getMiningFeeInSatoshisPerByte() { + return CoinUtil.getFeePerByte(miningFee, txSize); + } + + public double getTxSizeInKb() { + return txSize / 1000d; + } + + public String toShortString() { + return "{" + "\n" + + " receiverAddress='" + getReceiverAddressAsString() + '\'' + "\n" + + ", receiverAmount=" + receiverAmount + "\n" + + ", txWithBtcFee.txId=" + txWithBtcFee.getTxId() + "\n" + + ", miningFee=" + miningFee + "\n" + + ", miningFeeInSatoshisPerByte=" + getMiningFeeInSatoshisPerByte() + "\n" + + ", txSizeInKb=" + getTxSizeInKb() + "\n" + + '}'; + } + + @Override + public String toString() { + return "BsqTransferModel{" + "\n" + + " receiverAddress='" + getReceiverAddressAsString() + '\'' + "\n" + + ", receiverAmount=" + receiverAmount + "\n" + + ", preparedSendTx=" + preparedSendTx + "\n" + + ", txWithBtcFee=" + txWithBtcFee + "\n" + + ", signedTx=" + signedTx + "\n" + + ", miningFee=" + miningFee + "\n" + + ", miningFeeInSatoshisPerByte=" + getMiningFeeInSatoshisPerByte() + "\n" + + ", txSize=" + txSize + "\n" + + ", txSizeInKb=" + getTxSizeInKb() + "\n" + + ", txType=" + txType + "\n" + + '}'; + } +} diff --git a/core/src/main/java/bisq/core/btc/wallet/BsqTransferService.java b/core/src/main/java/bisq/core/btc/wallet/BsqTransferService.java new file mode 100644 index 00000000000..b6cc83e8c77 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/wallet/BsqTransferService.java @@ -0,0 +1,59 @@ +package bisq.core.btc.wallet; + +import bisq.core.btc.exceptions.BsqChangeBelowDustException; +import bisq.core.btc.exceptions.TransactionVerificationException; +import bisq.core.btc.exceptions.WalletException; +import bisq.core.btc.model.BsqTransferModel; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.core.LegacyAddress; +import org.bitcoinj.core.Transaction; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Singleton +public class BsqTransferService { + + private final WalletsManager walletsManager; + private final BsqWalletService bsqWalletService; + private final BtcWalletService btcWalletService; + + @Inject + public BsqTransferService(WalletsManager walletsManager, + BsqWalletService bsqWalletService, + BtcWalletService btcWalletService) { + this.walletsManager = walletsManager; + this.bsqWalletService = bsqWalletService; + this.btcWalletService = btcWalletService; + } + + public BsqTransferModel getBsqTransferModel(LegacyAddress address, + Coin receiverAmount) + throws TransactionVerificationException, + WalletException, + BsqChangeBelowDustException, + InsufficientMoneyException { + + Transaction preparedSendTx = bsqWalletService.getPreparedSendBsqTx(address.toString(), receiverAmount); + Transaction txWithBtcFee = btcWalletService.completePreparedSendBsqTx(preparedSendTx, true); + Transaction signedTx = bsqWalletService.signTx(txWithBtcFee); + + return new BsqTransferModel(address, + receiverAmount, + preparedSendTx, + txWithBtcFee, + signedTx); + } + + public void sendFunds(BsqTransferModel bsqTransferModel, TxBroadcaster.Callback callback) { + log.info("Publishing BSQ transfer {}", bsqTransferModel.toShortString()); + walletsManager.publishAndCommitBsqTx(bsqTransferModel.getTxWithBtcFee(), + bsqTransferModel.getTxType(), + callback); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/wallet/send/BsqSendView.java b/desktop/src/main/java/bisq/desktop/main/dao/wallet/send/BsqSendView.java index fa638c4a19c..c3dee6eb0e4 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/wallet/send/BsqSendView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/wallet/send/BsqSendView.java @@ -35,9 +35,13 @@ import bisq.desktop.util.validation.BtcValidator; import bisq.core.btc.exceptions.BsqChangeBelowDustException; +import bisq.core.btc.exceptions.TransactionVerificationException; import bisq.core.btc.exceptions.TxBroadcastException; +import bisq.core.btc.exceptions.WalletException; import bisq.core.btc.listeners.BsqBalanceListener; +import bisq.core.btc.model.BsqTransferModel; import bisq.core.btc.setup.WalletsSetup; +import bisq.core.btc.wallet.BsqTransferService; import bisq.core.btc.wallet.BsqWalletService; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.Restrictions; @@ -46,10 +50,10 @@ import bisq.core.dao.state.model.blockchain.TxType; import bisq.core.locale.Res; import bisq.core.util.FormattingUtils; +import bisq.core.util.ParsingUtils; import bisq.core.util.coin.BsqFormatter; import bisq.core.util.coin.CoinFormatter; import bisq.core.util.coin.CoinUtil; -import bisq.core.util.ParsingUtils; import bisq.core.util.validation.BtcAddressValidator; import bisq.network.p2p.P2PService; @@ -59,6 +63,7 @@ import org.bitcoinj.core.Coin; import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.core.LegacyAddress; import org.bitcoinj.core.Transaction; import javax.inject.Inject; @@ -90,6 +95,7 @@ public class BsqSendView extends ActivatableView implements BsqB private final BtcValidator btcValidator; private final BsqAddressValidator bsqAddressValidator; private final BtcAddressValidator btcAddressValidator; + private final BsqTransferService bsqTransferService; private final WalletPasswordWindow walletPasswordWindow; private int gridRow = 0; @@ -119,6 +125,7 @@ private BsqSendView(BsqWalletService bsqWalletService, BtcValidator btcValidator, BsqAddressValidator bsqAddressValidator, BtcAddressValidator btcAddressValidator, + BsqTransferService bsqTransferService, WalletPasswordWindow walletPasswordWindow) { this.bsqWalletService = bsqWalletService; this.btcWalletService = btcWalletService; @@ -133,6 +140,7 @@ private BsqSendView(BsqWalletService bsqWalletService, this.btcValidator = btcValidator; this.bsqAddressValidator = bsqAddressValidator; this.btcAddressValidator = btcAddressValidator; + this.bsqTransferService = bsqTransferService; this.walletPasswordWindow = walletPasswordWindow; } @@ -241,22 +249,15 @@ private void addSendBsqGroup() { sendBsqButton = addButtonAfterGroup(root, ++gridRow, Res.get("dao.wallet.send.send")); sendBsqButton.setOnAction((event) -> { - // TODO break up in methods if (GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, walletsSetup)) { - String receiversAddressString = bsqFormatter.getAddressFromBsqAddress(receiversAddressInputTextField.getText()).toString(); - Coin receiverAmount = ParsingUtils.parseToCoin(amountInputTextField.getText(), bsqFormatter); try { - Transaction preparedSendTx = bsqWalletService.getPreparedSendBsqTx(receiversAddressString, receiverAmount); - Transaction txWithBtcFee = btcWalletService.completePreparedSendBsqTx(preparedSendTx, true); - Transaction signedTx = bsqWalletService.signTx(txWithBtcFee); - Coin miningFee = signedTx.getFee(); - int txSize = signedTx.bitcoinSerialize().length; - showPublishTxPopup(receiverAmount, - txWithBtcFee, - TxType.TRANSFER_BSQ, - miningFee, - txSize, - receiversAddressInputTextField.getText(), + BsqTransferModel model = getBsqTransferModel(); + showPublishTxPopup(model.getReceiverAmount(), + model.getTxWithBtcFee(), + model.getTxType(), + model.getMiningFee(), + model.getTxSize(), + model.getReceiverAddressAsString(), bsqFormatter, btcFormatter, () -> { @@ -273,6 +274,16 @@ private void addSendBsqGroup() { }); } + private BsqTransferModel getBsqTransferModel() + throws InsufficientMoneyException, + TransactionVerificationException, + BsqChangeBelowDustException, + WalletException { + Coin receiverAmount = ParsingUtils.parseToCoin(amountInputTextField.getText(), bsqFormatter); + LegacyAddress legacyAddress = bsqFormatter.getAddressFromBsqAddress(receiversAddressInputTextField.getText()); + return bsqTransferService.getBsqTransferModel(legacyAddress, receiverAmount); + } + private void setSendBtcGroupVisibleState(boolean visible) { btcTitledGroupBg.setVisible(visible); receiversBtcAddressInputTextField.setVisible(visible); From 4a90b4008aaab8ebe81014f45be7ac15d0171237 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 13 Nov 2020 15:52:30 -0300 Subject: [PATCH 14/52] Resolve 'Avoid creating BigDecimal with a decimal' issue for codacy --- core/src/main/java/bisq/core/api/CoreWalletsService.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/core/src/main/java/bisq/core/api/CoreWalletsService.java b/core/src/main/java/bisq/core/api/CoreWalletsService.java index 1842f324c53..b52e59a00da 100644 --- a/core/src/main/java/bisq/core/api/CoreWalletsService.java +++ b/core/src/main/java/bisq/core/api/CoreWalletsService.java @@ -52,8 +52,6 @@ import org.bouncycastle.crypto.params.KeyParameter; -import java.math.BigDecimal; - import java.util.List; import java.util.Optional; import java.util.function.Function; @@ -354,7 +352,7 @@ private LegacyAddress getValidBsqLegacyAddress(String address) { // Returns a Coin for the double amount, or a RuntimeException if invalid. private Coin getValidBsqTransferAmount(double amount) { - Coin amountAsCoin = parseToCoin(new BigDecimal(amount).toString(), bsqFormatter); + Coin amountAsCoin = parseToCoin(Double.toString(amount), bsqFormatter); if (amountAsCoin.equals(Coin.ZERO)) throw new IllegalStateException(format("%.2f bsq is an invalid send amount", amount)); From 722460e7e5a89406c2704246f4018fb003a800ec Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 13 Nov 2020 16:36:06 -0300 Subject: [PATCH 15/52] Support paying trade fees in bsq or btc (api) - Add optional makerFeeCurrencyCode argument to CLI's 'createoffer'. - Add optional takerFeeCurrencyCode argument to CLI's 'takeoffer'. - Add isCurrencyForMakerFeeBtc field to OfferInfo proto wrapper. - Pass fee currency argument down to CoreOffersService and CoreTradesService's create and take offer methods. - Add maybeSetFeePaymentCurrencyPreference method to OfferUtil to conditionally set the 'payFeeInBtc' preference before creating or taking an offer. - Adjust affected tests. --- .../java/bisq/apitest/method/MethodTest.java | 4 +++- .../method/offer/AbstractOfferTest.java | 24 +++++++++++++++---- .../apitest/method/offer/CancelOfferTest.java | 1 + .../offer/CreateOfferUsingFixedPriceTest.java | 11 +++++++++ ...CreateOfferUsingMarketPriceMarginTest.java | 14 +++++++++++ .../method/offer/ValidateCreateOfferTest.java | 1 + .../method/trade/AbstractTradeTest.java | 20 ++++++++++++---- .../method/trade/TakeBuyBTCOfferTest.java | 13 +++++++--- .../method/trade/TakeSellBTCOfferTest.java | 13 +++++++--- cli/src/main/java/bisq/cli/CliMain.java | 20 ++++++++++++---- core/src/main/java/bisq/core/api/CoreApi.java | 4 ++++ .../java/bisq/core/api/CoreOffersService.java | 8 +++++++ .../java/bisq/core/api/CoreTradesService.java | 8 +++++++ .../java/bisq/core/api/model/OfferInfo.java | 10 ++++++++ .../main/java/bisq/core/offer/OfferUtil.java | 18 ++++++++++++++ .../bisq/daemon/grpc/GrpcOffersService.java | 1 + .../bisq/daemon/grpc/GrpcTradesService.java | 1 + 17 files changed, 150 insertions(+), 21 deletions(-) diff --git a/apitest/src/test/java/bisq/apitest/method/MethodTest.java b/apitest/src/test/java/bisq/apitest/method/MethodTest.java index f9275ffbee5..8ee44427fd7 100644 --- a/apitest/src/test/java/bisq/apitest/method/MethodTest.java +++ b/apitest/src/test/java/bisq/apitest/method/MethodTest.java @@ -179,10 +179,12 @@ protected final CancelOfferRequest createCancelOfferRequest(String offerId) { } protected final TakeOfferRequest createTakeOfferRequest(String offerId, - String paymentAccountId) { + String paymentAccountId, + String takerFeeCurrencyCode) { return TakeOfferRequest.newBuilder() .setOfferId(offerId) .setPaymentAccountId(paymentAccountId) + .setTakerFeeCurrencyCode(takerFeeCurrencyCode) .build(); } diff --git a/apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java b/apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java index fe9a98aaaae..cd9a5771a80 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java @@ -73,22 +73,35 @@ public static void setUp() { protected final OfferInfo createAliceOffer(PaymentAccount paymentAccount, String direction, String currencyCode, - long amount) { - return createMarketBasedPricedOffer(aliceStubs, paymentAccount, direction, currencyCode, amount); + long amount, + String makerFeeCurrencyCode) { + return createMarketBasedPricedOffer(aliceStubs, + paymentAccount, + direction, + currencyCode, + amount, + makerFeeCurrencyCode); } protected final OfferInfo createBobOffer(PaymentAccount paymentAccount, String direction, String currencyCode, - long amount) { - return createMarketBasedPricedOffer(bobStubs, paymentAccount, direction, currencyCode, amount); + 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) { + long amount, + String makerFeeCurrencyCode) { var req = CreateOfferRequest.newBuilder() .setPaymentAccountId(paymentAccount.getId()) .setDirection(direction) @@ -99,6 +112,7 @@ protected final OfferInfo createMarketBasedPricedOffer(GrpcStubs grpcStubs, .setMarketPriceMargin(0.00) .setPrice("0") .setBuyerSecurityDeposit(getDefaultBuyerSecurityDepositAsPercent()) + .setMakerFeeCurrencyCode(makerFeeCurrencyCode) .build(); return grpcStubs.offersService.createOffer(req).getOffer(); } diff --git a/apitest/src/test/java/bisq/apitest/method/offer/CancelOfferTest.java b/apitest/src/test/java/bisq/apitest/method/offer/CancelOfferTest.java index 334fb022bb3..8ec90aedad3 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/CancelOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/CancelOfferTest.java @@ -54,6 +54,7 @@ public void testCancelOffer() { .setMarketPriceMargin(0.00) .setPrice("0") .setBuyerSecurityDeposit(Restrictions.getDefaultBuyerSecurityDepositAsPercent()) + .setMakerFeeCurrencyCode("bsq") .build(); // Create some offers. diff --git a/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingFixedPriceTest.java b/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingFixedPriceTest.java index 72ff91f3115..daa85d5e8e9 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingFixedPriceTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingFixedPriceTest.java @@ -38,6 +38,8 @@ @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest { + private static final String MAKER_FEE_CURRENCY_CODE = "bsq"; + @Test @Order(1) public void testCreateAUDBTCBuyOfferUsingFixedPrice16000() { @@ -51,6 +53,7 @@ public void testCreateAUDBTCBuyOfferUsingFixedPrice16000() { .setMarketPriceMargin(0.00) .setPrice("16000") .setBuyerSecurityDeposit(Restrictions.getDefaultBuyerSecurityDepositAsPercent()) + .setMakerFeeCurrencyCode(MAKER_FEE_CURRENCY_CODE) .build(); var newOffer = aliceStubs.offersService.createOffer(req).getOffer(); String newOfferId = newOffer.getId(); @@ -64,6 +67,7 @@ public void testCreateAUDBTCBuyOfferUsingFixedPrice16000() { assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId()); assertEquals("BTC", newOffer.getBaseCurrencyCode()); assertEquals("AUD", newOffer.getCounterCurrencyCode()); + assertFalse(newOffer.getIsCurrencyForMakerFeeBtc()); newOffer = getOffer(newOfferId); assertEquals(newOfferId, newOffer.getId()); @@ -76,6 +80,7 @@ public void testCreateAUDBTCBuyOfferUsingFixedPrice16000() { assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId()); assertEquals("BTC", newOffer.getBaseCurrencyCode()); assertEquals("AUD", newOffer.getCounterCurrencyCode()); + assertFalse(newOffer.getIsCurrencyForMakerFeeBtc()); } @Test @@ -91,6 +96,7 @@ public void testCreateUSDBTCBuyOfferUsingFixedPrice100001234() { .setMarketPriceMargin(0.00) .setPrice("10000.1234") .setBuyerSecurityDeposit(Restrictions.getDefaultBuyerSecurityDepositAsPercent()) + .setMakerFeeCurrencyCode(MAKER_FEE_CURRENCY_CODE) .build(); var newOffer = aliceStubs.offersService.createOffer(req).getOffer(); String newOfferId = newOffer.getId(); @@ -104,6 +110,7 @@ public void testCreateUSDBTCBuyOfferUsingFixedPrice100001234() { assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId()); assertEquals("BTC", newOffer.getBaseCurrencyCode()); assertEquals("USD", newOffer.getCounterCurrencyCode()); + assertFalse(newOffer.getIsCurrencyForMakerFeeBtc()); newOffer = getOffer(newOfferId); assertEquals(newOfferId, newOffer.getId()); @@ -116,6 +123,7 @@ public void testCreateUSDBTCBuyOfferUsingFixedPrice100001234() { assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId()); assertEquals("BTC", newOffer.getBaseCurrencyCode()); assertEquals("USD", newOffer.getCounterCurrencyCode()); + assertFalse(newOffer.getIsCurrencyForMakerFeeBtc()); } @Test @@ -131,6 +139,7 @@ public void testCreateEURBTCSellOfferUsingFixedPrice95001234() { .setMarketPriceMargin(0.00) .setPrice("9500.1234") .setBuyerSecurityDeposit(Restrictions.getDefaultBuyerSecurityDepositAsPercent()) + .setMakerFeeCurrencyCode(MAKER_FEE_CURRENCY_CODE) .build(); var newOffer = aliceStubs.offersService.createOffer(req).getOffer(); String newOfferId = newOffer.getId(); @@ -144,6 +153,7 @@ public void testCreateEURBTCSellOfferUsingFixedPrice95001234() { assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId()); assertEquals("BTC", newOffer.getBaseCurrencyCode()); assertEquals("EUR", newOffer.getCounterCurrencyCode()); + assertFalse(newOffer.getIsCurrencyForMakerFeeBtc()); newOffer = getOffer(newOfferId); assertEquals(newOfferId, newOffer.getId()); @@ -156,5 +166,6 @@ public void testCreateEURBTCSellOfferUsingFixedPrice95001234() { assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId()); assertEquals("BTC", newOffer.getBaseCurrencyCode()); assertEquals("EUR", newOffer.getCounterCurrencyCode()); + assertFalse(newOffer.getIsCurrencyForMakerFeeBtc()); } } diff --git a/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java b/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java index 345bd130d71..dbf712c9355 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java @@ -50,6 +50,8 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest { private static final double MKT_PRICE_MARGIN_ERROR_TOLERANCE = 0.0050; // 0.50% private static final double MKT_PRICE_MARGIN_WARNING_TOLERANCE = 0.0001; // 0.01% + private static final String MAKER_FEE_CURRENCY_CODE = "btc"; + @Test @Order(1) public void testCreateUSDBTCBuyOffer5PctPriceMargin() { @@ -64,6 +66,7 @@ public void testCreateUSDBTCBuyOffer5PctPriceMargin() { .setMarketPriceMargin(priceMarginPctInput) .setPrice("0") .setBuyerSecurityDeposit(Restrictions.getDefaultBuyerSecurityDepositAsPercent()) + .setMakerFeeCurrencyCode(MAKER_FEE_CURRENCY_CODE) .build(); var newOffer = aliceStubs.offersService.createOffer(req).getOffer(); String newOfferId = newOffer.getId(); @@ -76,6 +79,7 @@ public void testCreateUSDBTCBuyOffer5PctPriceMargin() { assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId()); assertEquals("BTC", newOffer.getBaseCurrencyCode()); assertEquals("USD", newOffer.getCounterCurrencyCode()); + assertTrue(newOffer.getIsCurrencyForMakerFeeBtc()); newOffer = getOffer(newOfferId); assertEquals(newOfferId, newOffer.getId()); @@ -87,6 +91,7 @@ public void testCreateUSDBTCBuyOffer5PctPriceMargin() { assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId()); assertEquals("BTC", newOffer.getBaseCurrencyCode()); assertEquals("USD", newOffer.getCounterCurrencyCode()); + assertTrue(newOffer.getIsCurrencyForMakerFeeBtc()); assertCalculatedPriceIsCorrect(newOffer, priceMarginPctInput); } @@ -105,6 +110,7 @@ public void testCreateNZDBTCBuyOfferMinus2PctPriceMargin() { .setMarketPriceMargin(priceMarginPctInput) .setPrice("0") .setBuyerSecurityDeposit(Restrictions.getDefaultBuyerSecurityDepositAsPercent()) + .setMakerFeeCurrencyCode(MAKER_FEE_CURRENCY_CODE) .build(); var newOffer = aliceStubs.offersService.createOffer(req).getOffer(); String newOfferId = newOffer.getId(); @@ -117,6 +123,7 @@ public void testCreateNZDBTCBuyOfferMinus2PctPriceMargin() { assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId()); assertEquals("BTC", newOffer.getBaseCurrencyCode()); assertEquals("NZD", newOffer.getCounterCurrencyCode()); + assertTrue(newOffer.getIsCurrencyForMakerFeeBtc()); newOffer = getOffer(newOfferId); assertEquals(newOfferId, newOffer.getId()); @@ -128,6 +135,7 @@ public void testCreateNZDBTCBuyOfferMinus2PctPriceMargin() { assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId()); assertEquals("BTC", newOffer.getBaseCurrencyCode()); assertEquals("NZD", newOffer.getCounterCurrencyCode()); + assertTrue(newOffer.getIsCurrencyForMakerFeeBtc()); assertCalculatedPriceIsCorrect(newOffer, priceMarginPctInput); } @@ -146,6 +154,7 @@ public void testCreateGBPBTCSellOfferMinus1Point5PctPriceMargin() { .setMarketPriceMargin(priceMarginPctInput) .setPrice("0") .setBuyerSecurityDeposit(Restrictions.getDefaultBuyerSecurityDepositAsPercent()) + .setMakerFeeCurrencyCode(MAKER_FEE_CURRENCY_CODE) .build(); var newOffer = aliceStubs.offersService.createOffer(req).getOffer(); @@ -159,6 +168,7 @@ public void testCreateGBPBTCSellOfferMinus1Point5PctPriceMargin() { assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId()); assertEquals("BTC", newOffer.getBaseCurrencyCode()); assertEquals("GBP", newOffer.getCounterCurrencyCode()); + assertTrue(newOffer.getIsCurrencyForMakerFeeBtc()); newOffer = getOffer(newOfferId); assertEquals(newOfferId, newOffer.getId()); @@ -170,6 +180,7 @@ public void testCreateGBPBTCSellOfferMinus1Point5PctPriceMargin() { assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId()); assertEquals("BTC", newOffer.getBaseCurrencyCode()); assertEquals("GBP", newOffer.getCounterCurrencyCode()); + assertTrue(newOffer.getIsCurrencyForMakerFeeBtc()); assertCalculatedPriceIsCorrect(newOffer, priceMarginPctInput); } @@ -188,6 +199,7 @@ public void testCreateBRLBTCSellOffer6Point55PctPriceMargin() { .setMarketPriceMargin(priceMarginPctInput) .setPrice("0") .setBuyerSecurityDeposit(Restrictions.getDefaultBuyerSecurityDepositAsPercent()) + .setMakerFeeCurrencyCode(MAKER_FEE_CURRENCY_CODE) .build(); var newOffer = aliceStubs.offersService.createOffer(req).getOffer(); @@ -201,6 +213,7 @@ public void testCreateBRLBTCSellOffer6Point55PctPriceMargin() { assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId()); assertEquals("BTC", newOffer.getBaseCurrencyCode()); assertEquals("BRL", newOffer.getCounterCurrencyCode()); + assertTrue(newOffer.getIsCurrencyForMakerFeeBtc()); newOffer = getOffer(newOfferId); assertEquals(newOfferId, newOffer.getId()); @@ -212,6 +225,7 @@ public void testCreateBRLBTCSellOffer6Point55PctPriceMargin() { assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId()); assertEquals("BTC", newOffer.getBaseCurrencyCode()); assertEquals("BRL", newOffer.getCounterCurrencyCode()); + assertTrue(newOffer.getIsCurrencyForMakerFeeBtc()); assertCalculatedPriceIsCorrect(newOffer, priceMarginPctInput); } diff --git a/apitest/src/test/java/bisq/apitest/method/offer/ValidateCreateOfferTest.java b/apitest/src/test/java/bisq/apitest/method/offer/ValidateCreateOfferTest.java index 3ddd8cb3030..0225238b6a9 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/ValidateCreateOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/ValidateCreateOfferTest.java @@ -52,6 +52,7 @@ public void testAmtTooLargeShouldThrowException() { .setMarketPriceMargin(0.00) .setPrice("10000.0000") .setBuyerSecurityDeposit(Restrictions.getDefaultBuyerSecurityDepositAsPercent()) + .setMakerFeeCurrencyCode("bsq") .build(); @SuppressWarnings("ResultOfMethodCallIgnored") Throwable exception = assertThrows(StatusRuntimeException.class, () -> diff --git a/apitest/src/test/java/bisq/apitest/method/trade/AbstractTradeTest.java b/apitest/src/test/java/bisq/apitest/method/trade/AbstractTradeTest.java index e8537206dbf..2b88e4f2700 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/AbstractTradeTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/AbstractTradeTest.java @@ -27,13 +27,25 @@ public static void initStaticFixtures() { EXPECTED_PROTOCOL_STATUS.init(); } - protected final TradeInfo takeAlicesOffer(String offerId, String paymentAccountId) { - return bobStubs.tradesService.takeOffer(createTakeOfferRequest(offerId, paymentAccountId)).getTrade(); + protected final TradeInfo takeAlicesOffer(String offerId, + String paymentAccountId, + String takerFeeCurrencyCode) { + return bobStubs.tradesService.takeOffer( + createTakeOfferRequest(offerId, + paymentAccountId, + takerFeeCurrencyCode)) + .getTrade(); } @SuppressWarnings("unused") - protected final TradeInfo takeBobsOffer(String offerId, String paymentAccountId) { - return aliceStubs.tradesService.takeOffer(createTakeOfferRequest(offerId, paymentAccountId)).getTrade(); + protected final TradeInfo takeBobsOffer(String offerId, + String paymentAccountId, + String takerFeeCurrencyCode) { + return aliceStubs.tradesService.takeOffer( + createTakeOfferRequest(offerId, + paymentAccountId, + takerFeeCurrencyCode)) + .getTrade(); } protected final void verifyExpectedProtocolStatus(TradeInfo trade) { diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java index 3561787c454..b6fe92c754b 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java @@ -37,6 +37,7 @@ import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED; import static bisq.core.trade.Trade.State.*; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.fail; import static protobuf.Offer.State.OFFER_FEE_PAID; @@ -49,6 +50,9 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest { // Alice is buyer, Bob is seller. + // Maker and Taker fees are in BSQ. + private static final String TRADE_FEE_CURRENCY_CODE = "bsq"; + @Test @Order(1) public void testTakeAlicesBuyOffer(final TestInfo testInfo) { @@ -56,17 +60,20 @@ public void testTakeAlicesBuyOffer(final TestInfo testInfo) { var alicesOffer = createAliceOffer(alicesDummyAcct, "buy", "usd", - 12500000); + 12500000, + TRADE_FEE_CURRENCY_CODE); var offerId = alicesOffer.getId(); + assertFalse(alicesOffer.getIsCurrencyForMakerFeeBtc()); // Wait for Alice's AddToOfferBook task. // Wait times vary; my logs show >= 2 second delay. - sleep(3000); + sleep(3000); // TODO loop instead of hard code wait time assertEquals(1, getOpenOffersCount(aliceStubs, "buy", "usd")); - var trade = takeAlicesOffer(offerId, bobsDummyAcct.getId()); + var trade = takeAlicesOffer(offerId, bobsDummyAcct.getId(), TRADE_FEE_CURRENCY_CODE); assertNotNull(trade); assertEquals(offerId, trade.getTradeId()); + assertFalse(trade.getIsCurrencyForTakerFeeBtc()); // Cache the trade id for the other tests. tradeId = trade.getTradeId(); diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java index 35e13c002fa..d16b746da0c 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java @@ -35,6 +35,7 @@ import static bisq.core.trade.Trade.State.*; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static protobuf.Offer.State.OFFER_FEE_PAID; import static protobuf.OpenOffer.State.AVAILABLE; @@ -46,6 +47,9 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest { // Alice is seller, Bob is buyer. + // Maker and Taker fees are in BTC. + private static final String TRADE_FEE_CURRENCY_CODE = "btc"; + @Test @Order(1) public void testTakeAlicesSellOffer(final TestInfo testInfo) { @@ -53,18 +57,21 @@ public void testTakeAlicesSellOffer(final TestInfo testInfo) { var alicesOffer = createAliceOffer(alicesDummyAcct, "sell", "usd", - 12500000); + 12500000, + TRADE_FEE_CURRENCY_CODE); var offerId = alicesOffer.getId(); + assertTrue(alicesOffer.getIsCurrencyForMakerFeeBtc()); // Wait for Alice's AddToOfferBook task. // Wait times vary; my logs show >= 2 second delay, but taking sell offers // seems to require more time to prepare. - sleep(3000); + sleep(3000); // TODO loop instead of hard code wait time assertEquals(1, getOpenOffersCount(bobStubs, "sell", "usd")); - var trade = takeAlicesOffer(offerId, bobsDummyAcct.getId()); + var trade = takeAlicesOffer(offerId, bobsDummyAcct.getId(), TRADE_FEE_CURRENCY_CODE); assertNotNull(trade); assertEquals(offerId, trade.getTradeId()); + assertTrue(trade.getIsCurrencyForTakerFeeBtc()); // Cache the trade id for the other tests. tradeId = trade.getTradeId(); diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index d61b2ba2063..edcfa365092 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -269,7 +269,8 @@ public static void run(String[] args) { if (nonOptionArgs.size() < 9) throw new IllegalArgumentException("incorrect parameter count," + " expecting payment acct id, buy | sell, currency code, amount, min amount," - + " use-market-based-price, fixed-price | mkt-price-margin, security-deposit"); + + " use-market-based-price, fixed-price | mkt-price-margin, security-deposit" + + " [,maker-fee-currency-code = bsq|btc]"); var paymentAcctId = nonOptionArgs.get(1); var direction = nonOptionArgs.get(2); @@ -285,6 +286,9 @@ public static void run(String[] args) { fixedPrice = nonOptionArgs.get(7); var securityDeposit = new BigDecimal(nonOptionArgs.get(8)); + var makerFeeCurrencyCode = nonOptionArgs.size() == 10 + ? nonOptionArgs.get(9) + : "btc"; var request = CreateOfferRequest.newBuilder() .setDirection(direction) @@ -296,6 +300,7 @@ public static void run(String[] args) { .setMarketPriceMargin(marketPriceMargin.doubleValue()) .setBuyerSecurityDeposit(securityDeposit.doubleValue()) .setPaymentAccountId(paymentAcctId) + .setMakerFeeCurrencyCode(makerFeeCurrencyCode) .build(); var reply = offersService.createOffer(request); out.println(formatOfferTable(singletonList(reply.getOffer()), currencyCode)); @@ -344,14 +349,19 @@ public static void run(String[] args) { } case takeoffer: { if (nonOptionArgs.size() < 3) - throw new IllegalArgumentException("incorrect parameter count, " - + " expecting offer id, payment acct id"); + throw new IllegalArgumentException("incorrect parameter count, " + + " expecting offer id, payment acct id [,taker fee currency code = bsq|btc]"); var offerId = nonOptionArgs.get(1); var paymentAccountId = nonOptionArgs.get(2); + var takerFeeCurrencyCode = nonOptionArgs.size() == 4 + ? nonOptionArgs.get(3) + : "btc"; + var request = TakeOfferRequest.newBuilder() .setOfferId(offerId) .setPaymentAccountId(paymentAccountId) + .setTakerFeeCurrencyCode(takerFeeCurrencyCode) .build(); var reply = tradesService.takeOffer(request); out.printf("trade '%s' successfully taken", reply.getTrade().getShortId()); @@ -556,12 +566,12 @@ private static void printHelp(OptionParser parser, PrintStream stream) { stream.format(rowFormat, "sendbsq", "address, amount", "Send BSQ"); stream.format(rowFormat, "createoffer", "payment acct id, buy | sell, currency code, \\", "Create and place an offer"); stream.format(rowFormat, "", "amount (btc), min amount, use mkt based price, \\", ""); - stream.format(rowFormat, "", "fixed price (btc) | mkt price margin (%), \\", ""); + stream.format(rowFormat, "", "fixed price (btc) | mkt price margin (%) [,maker fee currency code = bsq|btc]\\", ""); stream.format(rowFormat, "", "security deposit (%)", ""); stream.format(rowFormat, "canceloffer", "offer id", "Cancel offer with id"); stream.format(rowFormat, "getoffer", "offer id", "Get current offer with id"); stream.format(rowFormat, "getoffers", "buy | sell, currency code", "Get current offers"); - stream.format(rowFormat, "takeoffer", "offer id", "Take offer with id"); + stream.format(rowFormat, "takeoffer", "offer id, [,taker fee currency code = bsq|btc]", "Take offer with id"); stream.format(rowFormat, "gettrade", "trade id [,showcontract = true|false]", "Get trade summary or full contract"); stream.format(rowFormat, "confirmpaymentstarted", "trade id", "Confirm payment started"); stream.format(rowFormat, "confirmpaymentreceived", "trade id", "Confirm payment received"); diff --git a/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java index fceb3c47e55..bbf3badf6e7 100644 --- a/core/src/main/java/bisq/core/api/CoreApi.java +++ b/core/src/main/java/bisq/core/api/CoreApi.java @@ -111,6 +111,7 @@ public void createAnPlaceOffer(String currencyCode, long minAmountAsLong, double buyerSecurityDeposit, String paymentAccountId, + String makerFeeCurrencyCode, Consumer resultHandler) { coreOffersService.createAndPlaceOffer(currencyCode, directionAsString, @@ -121,6 +122,7 @@ public void createAnPlaceOffer(String currencyCode, minAmountAsLong, buyerSecurityDeposit, paymentAccountId, + makerFeeCurrencyCode, resultHandler); } @@ -182,10 +184,12 @@ public double getMarketPrice(String currencyCode) { public void takeOffer(String offerId, String paymentAccountId, + String takerFeeCurrencyCode, Consumer resultHandler) { Offer offer = coreOffersService.getOffer(offerId); coreTradesService.takeOffer(offer, paymentAccountId, + takerFeeCurrencyCode, resultHandler); } diff --git a/core/src/main/java/bisq/core/api/CoreOffersService.java b/core/src/main/java/bisq/core/api/CoreOffersService.java index 6d8641c0579..bca9dc7cbf9 100644 --- a/core/src/main/java/bisq/core/api/CoreOffersService.java +++ b/core/src/main/java/bisq/core/api/CoreOffersService.java @@ -22,6 +22,7 @@ import bisq.core.offer.CreateOfferService; import bisq.core.offer.Offer; import bisq.core.offer.OfferBookService; +import bisq.core.offer.OfferUtil; import bisq.core.offer.OpenOfferManager; import bisq.core.payment.PaymentAccount; import bisq.core.user.User; @@ -55,16 +56,19 @@ class CoreOffersService { private final CreateOfferService createOfferService; private final OfferBookService offerBookService; private final OpenOfferManager openOfferManager; + private final OfferUtil offerUtil; private final User user; @Inject public CoreOffersService(CreateOfferService createOfferService, OfferBookService offerBookService, OpenOfferManager openOfferManager, + OfferUtil offerUtil, User user) { this.createOfferService = createOfferService; this.offerBookService = offerBookService; this.openOfferManager = openOfferManager; + this.offerUtil = offerUtil; this.user = user; } @@ -105,7 +109,11 @@ void createAndPlaceOffer(String currencyCode, long minAmountAsLong, double buyerSecurityDeposit, String paymentAccountId, + String makerFeeCurrencyCode, Consumer resultHandler) { + + offerUtil.maybeSetFeePaymentCurrencyPreference(makerFeeCurrencyCode); + String upperCaseCurrencyCode = currencyCode.toUpperCase(); String offerId = createOfferService.getRandomOfferId(); Direction direction = Direction.valueOf(directionAsString.toUpperCase()); diff --git a/core/src/main/java/bisq/core/api/CoreTradesService.java b/core/src/main/java/bisq/core/api/CoreTradesService.java index dbc6927f452..4bc678d9263 100644 --- a/core/src/main/java/bisq/core/api/CoreTradesService.java +++ b/core/src/main/java/bisq/core/api/CoreTradesService.java @@ -20,6 +20,7 @@ import bisq.core.btc.model.AddressEntry; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.offer.Offer; +import bisq.core.offer.OfferUtil; import bisq.core.offer.takeoffer.TakeOfferModel; import bisq.core.trade.Tradable; import bisq.core.trade.Trade; @@ -52,6 +53,7 @@ class CoreTradesService { private final CoreWalletsService coreWalletsService; private final BtcWalletService btcWalletService; + private final OfferUtil offerUtil; private final ClosedTradableManager closedTradableManager; private final TakeOfferModel takeOfferModel; private final TradeManager tradeManager; @@ -61,6 +63,7 @@ class CoreTradesService { @Inject public CoreTradesService(CoreWalletsService coreWalletsService, BtcWalletService btcWalletService, + OfferUtil offerUtil, ClosedTradableManager closedTradableManager, TakeOfferModel takeOfferModel, TradeManager tradeManager, @@ -68,6 +71,7 @@ public CoreTradesService(CoreWalletsService coreWalletsService, User user) { this.coreWalletsService = coreWalletsService; this.btcWalletService = btcWalletService; + this.offerUtil = offerUtil; this.closedTradableManager = closedTradableManager; this.takeOfferModel = takeOfferModel; this.tradeManager = tradeManager; @@ -77,7 +81,11 @@ public CoreTradesService(CoreWalletsService coreWalletsService, void takeOffer(Offer offer, String paymentAccountId, + String takerFeeCurrencyCode, Consumer resultHandler) { + + offerUtil.maybeSetFeePaymentCurrencyPreference(takerFeeCurrencyCode); + var paymentAccount = user.getPaymentAccount(paymentAccountId); if (paymentAccount == null) throw new IllegalArgumentException(format("payment account with id '%s' not found", paymentAccountId)); diff --git a/core/src/main/java/bisq/core/api/model/OfferInfo.java b/core/src/main/java/bisq/core/api/model/OfferInfo.java index 219045b2762..cc11aacb1d9 100644 --- a/core/src/main/java/bisq/core/api/model/OfferInfo.java +++ b/core/src/main/java/bisq/core/api/model/OfferInfo.java @@ -46,6 +46,7 @@ public class OfferInfo implements Payload { private final long volume; private final long minVolume; private final long buyerSecurityDeposit; + private final boolean isCurrencyForMakerFeeBtc; private final String paymentAccountId; // only used when creating offer private final String paymentMethodId; private final String paymentMethodShortName; @@ -67,6 +68,7 @@ public OfferInfo(OfferInfoBuilder builder) { this.volume = builder.volume; this.minVolume = builder.minVolume; this.buyerSecurityDeposit = builder.buyerSecurityDeposit; + this.isCurrencyForMakerFeeBtc = builder.isCurrencyForMakerFeeBtc; this.paymentAccountId = builder.paymentAccountId; this.paymentMethodId = builder.paymentMethodId; this.paymentMethodShortName = builder.paymentMethodShortName; @@ -88,6 +90,7 @@ public static OfferInfo toOfferInfo(Offer offer) { .withVolume(Objects.requireNonNull(offer.getVolume()).getValue()) .withMinVolume(Objects.requireNonNull(offer.getMinVolume()).getValue()) .withBuyerSecurityDeposit(offer.getBuyerSecurityDeposit().value) + .withIsCurrencyForMakerFeeBtc(offer.isCurrencyForMakerFeeBtc()) .withPaymentAccountId(offer.getMakerPaymentAccountId()) .withPaymentMethodId(offer.getPaymentMethod().getId()) .withPaymentMethodShortName(offer.getPaymentMethod().getShortName()) @@ -115,6 +118,7 @@ public bisq.proto.grpc.OfferInfo toProtoMessage() { .setVolume(volume) .setMinVolume(minVolume) .setBuyerSecurityDeposit(buyerSecurityDeposit) + .setIsCurrencyForMakerFeeBtc(isCurrencyForMakerFeeBtc) .setPaymentAccountId(paymentAccountId) .setPaymentMethodId(paymentMethodId) .setPaymentMethodShortName(paymentMethodShortName) @@ -147,6 +151,7 @@ public static class OfferInfoBuilder { private long volume; private long minVolume; private long buyerSecurityDeposit; + private boolean isCurrencyForMakerFeeBtc; private String paymentAccountId; private String paymentMethodId; private String paymentMethodShortName; @@ -205,6 +210,11 @@ public OfferInfoBuilder withBuyerSecurityDeposit(long buyerSecurityDeposit) { return this; } + public OfferInfoBuilder withIsCurrencyForMakerFeeBtc(boolean isCurrencyForMakerFeeBtc) { + this.isCurrencyForMakerFeeBtc = isCurrencyForMakerFeeBtc; + return this; + } + public OfferInfoBuilder withPaymentAccountId(String paymentAccountId) { this.paymentAccountId = paymentAccountId; return this; diff --git a/core/src/main/java/bisq/core/offer/OfferUtil.java b/core/src/main/java/bisq/core/offer/OfferUtil.java index 84e783f5b14..5afb6b1ac08 100644 --- a/core/src/main/java/bisq/core/offer/OfferUtil.java +++ b/core/src/main/java/bisq/core/offer/OfferUtil.java @@ -49,6 +49,7 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; +import java.util.function.Predicate; import lombok.extern.slf4j.Slf4j; @@ -63,6 +64,7 @@ import static bisq.core.offer.OfferPayload.*; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import static java.lang.String.format; /** * This class holds utility methods for creating, editing and taking an Offer. @@ -79,6 +81,9 @@ public class OfferUtil { private final P2PService p2PService; private final ReferralIdService referralIdService; + private final Predicate isValidFeePaymentCurrencyCode = (c) -> + c.equalsIgnoreCase("BSQ") || c.equalsIgnoreCase("BTC"); + @Inject public OfferUtil(AccountAgeWitnessService accountAgeWitnessService, BsqWalletService bsqWalletService, @@ -96,6 +101,19 @@ public OfferUtil(AccountAgeWitnessService accountAgeWitnessService, this.referralIdService = referralIdService; } + public void maybeSetFeePaymentCurrencyPreference(String feeCurrencyCode) { + if (!feeCurrencyCode.isEmpty()) { + if (!isValidFeePaymentCurrencyCode.test(feeCurrencyCode)) + throw new IllegalStateException(format("%s cannot be used to pay trade fees", + feeCurrencyCode.toUpperCase())); + + if (feeCurrencyCode.equalsIgnoreCase("BSQ") && preferences.isPayFeeInBtc()) + preferences.setPayFeeInBtc(false); + else if (feeCurrencyCode.equalsIgnoreCase("BTC") && !preferences.isPayFeeInBtc()) + preferences.setPayFeeInBtc(true); + } + } + /** * Given the direction, is this a BUY? * diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java index f03155d2f8d..1efed108ca6 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java @@ -100,6 +100,7 @@ public void createOffer(CreateOfferRequest req, req.getMinAmount(), req.getBuyerSecurityDeposit(), req.getPaymentAccountId(), + req.getMakerFeeCurrencyCode(), offer -> { // This result handling consumer's accept operation will return // the new offer to the gRPC client after async placement is done. diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java index 74cd04ead93..449859a9f63 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java @@ -79,6 +79,7 @@ public void takeOffer(TakeOfferRequest req, try { coreApi.takeOffer(req.getOfferId(), req.getPaymentAccountId(), + req.getTakerFeeCurrencyCode(), trade -> { TradeInfo tradeInfo = toTradeInfo(trade); var reply = TakeOfferReply.newBuilder() From 8157f8fc4ee238de6213803fdf9d7bca22f411e7 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 13 Nov 2020 16:57:18 -0300 Subject: [PATCH 16/52] Delete deprecated api test, adjust api build/run doc --- apitest/docs/build-run.md | 2 +- .../apitest/method/wallet/GetBalanceTest.java | 78 ------------------- 2 files changed, 1 insertion(+), 79 deletions(-) delete mode 100644 apitest/src/test/java/bisq/apitest/method/wallet/GetBalanceTest.java diff --git a/apitest/docs/build-run.md b/apitest/docs/build-run.md index 9d1165ca88e..fc8d8b7b375 100644 --- a/apitest/docs/build-run.md +++ b/apitest/docs/build-run.md @@ -48,7 +48,7 @@ To run all test cases in a package: To run a single test case: - $ ./gradlew :apitest:test --tests "bisq.apitest.method.wallet.GetBalanceTest" -DrunApiTests=true + $ ./gradlew :apitest:test --tests "bisq.apitest.method.wallet.WalletBalancesTest" -DrunApiTests=true To run test cases from Intellij, add two JVM arguments to your JUnit launchers: diff --git a/apitest/src/test/java/bisq/apitest/method/wallet/GetBalanceTest.java b/apitest/src/test/java/bisq/apitest/method/wallet/GetBalanceTest.java deleted file mode 100644 index 2c7c3466dda..00000000000 --- a/apitest/src/test/java/bisq/apitest/method/wallet/GetBalanceTest.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -package bisq.apitest.method.wallet; - -import bisq.proto.grpc.GetBalanceRequest; - -import lombok.extern.slf4j.Slf4j; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestMethodOrder; - -import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind; -import static bisq.apitest.config.BisqAppConfig.alicedaemon; -import static bisq.apitest.config.BisqAppConfig.seednode; -import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.fail; -import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; - - - -import bisq.apitest.method.MethodTest; - -@Deprecated -@Disabled -@Slf4j -@TestMethodOrder(OrderAnnotation.class) -public class GetBalanceTest extends MethodTest { - - @BeforeAll - public static void setUp() { - try { - setUpScaffold(bitcoind, seednode, alicedaemon); - - // Have to generate 1 regtest block for alice's wallet to show 10 BTC balance. - bitcoinCli.generateBlocks(1); - - // Give the alicedaemon time to parse the new block. - MILLISECONDS.sleep(1500); - } catch (Exception ex) { - fail(ex); - } - } - - @Test - @Order(1) - public void testGetBalance() { - // All tests depend on the DAO / regtest environment, and Alice's wallet is - // initialized with 10 BTC during the scaffolding setup. - var balance = grpcStubs(alicedaemon).walletsService - .getBalance(GetBalanceRequest.newBuilder().build()).getBalance(); - assertEquals(1000000000, balance); - } - - @AfterAll - public static void tearDown() { - tearDownScaffold(); - } -} From 7e9ab22d65c713daae79af5a3f4f9a3c72ee58ad Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sat, 14 Nov 2020 13:08:10 -0300 Subject: [PATCH 17/52] Refactor api getbalance methods. This change fixes the recently bloated wallet balances api. Now there is one CLI getbalance [bsq|btc] method that calls a getbalances(currency-code) on the server. The server returns full wallet balance information for BSQ, BTC, or both if the CLI's currency argument is absent. - grpc.proto: Reduced number of getbalance(s) proto services from four to one. - GrpcWalletsService: Reduced number of getbalance(s) gRPC service boilerplate methods from four to one. - CoreApi, CoreWalletsService: Reduced number of getbalance(s) gRPC service implementation methods from four to one. - CliMain: Reduced number of getbalance(s) commands from four to one. - BalancesInfo: Changed BsqBalanceInfo & BtcBalanceInfo field names to bsq and btc, respectively, to make calling their accessors more readable, less verbose. - BtcBalanceInfo, BsqBalanceInfo: Defined static final EMPTY instances as place holders in a BalancesInfo returned by the gRPC server, when one or the other balance types is not requested. Would be nice to serve Optional.empty() instead, but protobuf does not support it or null. - Adjusted affected api tests and build doc. --- apitest/docs/build-run.md | 2 +- .../java/bisq/apitest/method/MethodTest.java | 37 ++----- .../method/trade/TakeBuyBTCOfferTest.java | 5 +- .../method/trade/TakeSellBTCOfferTest.java | 5 +- .../apitest/method/wallet/BtcWalletTest.java | 29 +++-- .../method/wallet/WalletBalancesTest.java | 79 ------------- .../method/wallet/WalletProtectionTest.java | 16 +-- .../scenario/FundWalletScenarioTest.java | 76 ------------- .../bisq/apitest/scenario/WalletTest.java | 16 +-- cli/src/main/java/bisq/cli/CliMain.java | 41 ++----- cli/src/main/java/bisq/cli/TableFormat.java | 4 +- core/src/main/java/bisq/core/api/CoreApi.java | 17 +-- .../bisq/core/api/CoreWalletsService.java | 104 ++++++++++-------- .../bisq/core/api/model/BalancesInfo.java | 27 +++-- .../bisq/core/api/model/BsqBalanceInfo.java | 7 ++ .../bisq/core/api/model/BtcBalanceInfo.java | 5 + .../bisq/daemon/grpc/GrpcWalletsService.java | 58 +--------- proto/src/main/proto/grpc.proto | 35 +----- 18 files changed, 152 insertions(+), 411 deletions(-) delete mode 100644 apitest/src/test/java/bisq/apitest/method/wallet/WalletBalancesTest.java delete mode 100644 apitest/src/test/java/bisq/apitest/scenario/FundWalletScenarioTest.java diff --git a/apitest/docs/build-run.md b/apitest/docs/build-run.md index fc8d8b7b375..58832bf4534 100644 --- a/apitest/docs/build-run.md +++ b/apitest/docs/build-run.md @@ -48,7 +48,7 @@ To run all test cases in a package: To run a single test case: - $ ./gradlew :apitest:test --tests "bisq.apitest.method.wallet.WalletBalancesTest" -DrunApiTests=true + $ ./gradlew :apitest:test --tests "bisq.apitest.scenario.WalletTest" -DrunApiTests=true To run test cases from Intellij, add two JVM arguments to your JUnit launchers: diff --git a/apitest/src/test/java/bisq/apitest/method/MethodTest.java b/apitest/src/test/java/bisq/apitest/method/MethodTest.java index 8ee44427fd7..72634b16785 100644 --- a/apitest/src/test/java/bisq/apitest/method/MethodTest.java +++ b/apitest/src/test/java/bisq/apitest/method/MethodTest.java @@ -26,10 +26,7 @@ import bisq.proto.grpc.ConfirmPaymentStartedRequest; import bisq.proto.grpc.CreatePaymentAccountRequest; import bisq.proto.grpc.GetAddressBalanceRequest; -import bisq.proto.grpc.GetBalanceRequest; import bisq.proto.grpc.GetBalancesRequest; -import bisq.proto.grpc.GetBsqBalancesRequest; -import bisq.proto.grpc.GetBtcBalancesRequest; import bisq.proto.grpc.GetFundingAddressesRequest; import bisq.proto.grpc.GetOfferRequest; import bisq.proto.grpc.GetPaymentAccountsRequest; @@ -112,28 +109,14 @@ public static void startSupportingApps(boolean registerDisputeAgents, } // Convenience methods for building gRPC request objects - - @Deprecated - protected final GetBalanceRequest createBalanceRequest() { - return GetBalanceRequest.newBuilder().build(); - } - - protected final GetBalancesRequest createGetBalancesRequest() { - return GetBalancesRequest.newBuilder().build(); + 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 GetBsqBalancesRequest createGetBsqBalancesRequest() { - return GetBsqBalancesRequest.newBuilder().build(); - } - - protected final GetBtcBalancesRequest createBtcBalancesRequest() { - return GetBtcBalancesRequest.newBuilder().build(); - } - protected final SetWalletPasswordRequest createSetWalletPasswordRequest(String password) { return SetWalletPasswordRequest.newBuilder().setPassword(password).build(); } @@ -214,22 +197,17 @@ protected final WithdrawFundsRequest createWithdrawFundsRequest(String tradeId, } // Convenience methods for calling frequently used & thoroughly tested gRPC services. - - @Deprecated - protected final long getBalance(BisqAppConfig bisqAppConfig) { - return grpcStubs(bisqAppConfig).walletsService.getBalance(createBalanceRequest()).getBalance(); - } - - protected final BalancesInfo getBalances(BisqAppConfig bisqAppConfig) { - return grpcStubs(bisqAppConfig).walletsService.getBalances(createGetBalancesRequest()).getBalances(); + protected final BalancesInfo getBalances(BisqAppConfig bisqAppConfig, String currencyCode) { + return grpcStubs(bisqAppConfig).walletsService.getBalances( + createGetBalancesRequest(currencyCode)).getBalances(); } protected final BsqBalanceInfo getBsqBalances(BisqAppConfig bisqAppConfig) { - return grpcStubs(bisqAppConfig).walletsService.getBsqBalances(createGetBsqBalancesRequest()).getBsqBalanceInfo(); + return getBalances(bisqAppConfig, "bsq").getBsq(); } protected final BtcBalanceInfo getBtcBalances(BisqAppConfig bisqAppConfig) { - return grpcStubs(bisqAppConfig).walletsService.getBtcBalances(createBtcBalancesRequest()).getBtcBalanceInfo(); + return getBalances(bisqAppConfig, "btc").getBtc(); } protected final AddressBalanceInfo getAddressBalance(BisqAppConfig bisqAppConfig, String address) { @@ -251,6 +229,7 @@ protected final String getUnusedBsqAddress(BisqAppConfig bisqAppConfig) { } protected final void sendBsq(BisqAppConfig bisqAppConfig, String address, double amount) { + //noinspection ResultOfMethodCallIgnored grpcStubs(bisqAppConfig).walletsService.sendBsq(createSendBsqRequest(address, amount)); } diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java index b6fe92c754b..ffbf75ffee6 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java @@ -17,6 +17,8 @@ package bisq.apitest.method.trade; +import bisq.proto.grpc.BtcBalanceInfo; + import io.grpc.StatusRuntimeException; import lombok.extern.slf4j.Slf4j; @@ -154,8 +156,9 @@ public void testAlicesKeepFunds(final TestInfo testInfo) { .setPhase(PAYOUT_PUBLISHED); verifyExpectedProtocolStatus(trade); logTrade(log, testInfo, "Alice's view after keeping funds", trade); + BtcBalanceInfo currentBalance = getBtcBalances(bobdaemon); log.info("{} Alice's current available balance: {} BTC", testName(testInfo), - formatSatoshis(getBalance(alicedaemon))); + formatSatoshis(currentBalance.getAvailableBalance())); } } diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java index d16b746da0c..2278ce315cd 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java @@ -17,6 +17,8 @@ package bisq.apitest.method.trade; +import bisq.proto.grpc.BtcBalanceInfo; + import io.grpc.StatusRuntimeException; import lombok.extern.slf4j.Slf4j; @@ -155,8 +157,9 @@ public void testBobsBtcWithdrawalToExternalAddress(final TestInfo testInfo) { .setWithdrawn(true); verifyExpectedProtocolStatus(trade); logTrade(log, testInfo, "Bob's view after withdrawing funds to external wallet", trade); + BtcBalanceInfo currentBalance = getBtcBalances(bobdaemon); log.info("{} Bob's current available balance: {} BTC", testName(testInfo), - formatSatoshis(getBalance(bobdaemon))); + formatSatoshis(currentBalance.getAvailableBalance())); } } diff --git a/apitest/src/test/java/bisq/apitest/method/wallet/BtcWalletTest.java b/apitest/src/test/java/bisq/apitest/method/wallet/BtcWalletTest.java index 380198d53ee..a9d1feb401b 100644 --- a/apitest/src/test/java/bisq/apitest/method/wallet/BtcWalletTest.java +++ b/apitest/src/test/java/bisq/apitest/method/wallet/BtcWalletTest.java @@ -51,14 +51,17 @@ public static void setUp() { @Test @Order(1) - public void testDeprecatedAvailableBtcBalance() { - // Alice's regtest Bisq wallet was initialized with 10 BTC. - long balance = getBalance(alicedaemon); // @Deprecated method - assertEquals(INITIAL_BTC_BALANCES.getAvailableBalance(), balance); - - // Bob's regtest Bisq wallet was initialized with 10 BTC. - balance = getBalance(bobdaemon); // @Deprecated method - assertEquals(INITIAL_BTC_BALANCES.getAvailableBalance(), balance); + public void testInitialBtcBalances(final TestInfo testInfo) { + // Bob & Alice's regtest Bisq wallets were initialized with 10 BTC. + + BtcBalanceInfo alicesBalances = getBtcBalances(alicedaemon); + log.info("{} Alice's BTC Balances:\n{}", testName(testInfo), formatBtcBalanceInfoTbl(alicesBalances)); + + BtcBalanceInfo bobsBalances = getBtcBalances(bobdaemon); + log.info("{} Bob's BTC Balances:\n{}", testName(testInfo), formatBtcBalanceInfoTbl(bobsBalances)); + + assertEquals(INITIAL_BTC_BALANCES.getAvailableBalance(), alicesBalances.getAvailableBalance()); + assertEquals(INITIAL_BTC_BALANCES.getAvailableBalance(), bobsBalances.getAvailableBalance()); } @Test @@ -68,14 +71,16 @@ public void testFundAlicesBtcWallet(final TestInfo testInfo) { bitcoinCli.sendToAddress(newAddress, "2.5"); genBtcBlocksThenWait(1, 1500); - long balance = getBalance(alicedaemon); // @Deprecated method - assertEquals(1250000000, balance); // new balance is 12.5 btc + BtcBalanceInfo btcBalanceInfo = getBtcBalances(alicedaemon); + // New balance is 12.5 BTC + assertEquals(1250000000, btcBalanceInfo.getAvailableBalance()); log.info("{} -> Alice's Funded Address Balance -> \n{}", testName(testInfo), formatAddressBalanceTbl(singletonList(getAddressBalance(alicedaemon, newAddress)))); - BtcBalanceInfo btcBalanceInfo = getBtcBalances(alicedaemon); // new balance is 12.5 btc + // New balance is 12.5 BTC + btcBalanceInfo = getBtcBalances(alicedaemon); bisq.core.api.model.BtcBalanceInfo alicesExpectedBalances = bisq.core.api.model.BtcBalanceInfo.valueOf(1250000000, 0, @@ -93,7 +98,7 @@ public static void tearDown() { } private void verifyBtcBalances(bisq.core.api.model.BtcBalanceInfo expected, - BtcBalanceInfo actual) { + bisq.proto.grpc.BtcBalanceInfo actual) { assertEquals(expected.getAvailableBalance(), actual.getAvailableBalance()); assertEquals(expected.getReservedBalance(), actual.getReservedBalance()); assertEquals(expected.getTotalAvailableBalance(), actual.getTotalAvailableBalance()); diff --git a/apitest/src/test/java/bisq/apitest/method/wallet/WalletBalancesTest.java b/apitest/src/test/java/bisq/apitest/method/wallet/WalletBalancesTest.java deleted file mode 100644 index 6c168843668..00000000000 --- a/apitest/src/test/java/bisq/apitest/method/wallet/WalletBalancesTest.java +++ /dev/null @@ -1,79 +0,0 @@ -package bisq.apitest.method.wallet; - -import bisq.proto.grpc.BalancesInfo; - -import lombok.extern.slf4j.Slf4j; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.MethodOrderer; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInfo; -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.bobdaemon; -import static bisq.apitest.config.BisqAppConfig.seednode; -import static org.junit.jupiter.api.Assertions.assertEquals; - - - -import bisq.apitest.method.MethodTest; -import bisq.cli.TableFormat; - -@Disabled -@Slf4j -@TestMethodOrder(MethodOrderer.OrderAnnotation.class) -public class WalletBalancesTest extends MethodTest { - - // All api tests depend on the DAO / regtest environment, and Bob & Alice's wallets - // are initialized with 10 BTC during the scaffolding setup. - private static final bisq.core.api.model.BtcBalanceInfo INITIAL_BTC_BALANCES = - bisq.core.api.model.BtcBalanceInfo.valueOf(1000000000, - 0, - 1000000000, - 0); - - @BeforeAll - public static void setUp() { - startSupportingApps(false, - true, - bitcoind, - seednode, - alicedaemon, - bobdaemon); - } - - @Test - @Order(1) - public void testDeprecatedAvailableBtcBalance() { - // Alice's regtest Bisq wallet was initialized with 10 BTC. - long balance = getBalance(alicedaemon); // @Deprecated method - assertEquals(INITIAL_BTC_BALANCES.getAvailableBalance(), balance); - - // Bob's regtest Bisq wallet was initialized with 10 BTC. - balance = getBalance(bobdaemon); // @Deprecated method - assertEquals(INITIAL_BTC_BALANCES.getAvailableBalance(), balance); - } - - @Test - @Order(2) - public void testNewGetBalances(final TestInfo testInfo) { - BalancesInfo alicesBalances = getBalances(alicedaemon); - BalancesInfo bobsBalances = getBalances(bobdaemon); - - log.info("{} Alice's Balances:\n{}", testName(testInfo), TableFormat.formatBalancesTbls(alicesBalances)); - log.info("{} Bob's Balances:\n{}", testName(testInfo), TableFormat.formatBalancesTbls(bobsBalances)); - - assertEquals(INITIAL_BTC_BALANCES.getAvailableBalance(), alicesBalances.getBtcBalanceInfo().getAvailableBalance()); - assertEquals(INITIAL_BTC_BALANCES.getAvailableBalance(), bobsBalances.getBtcBalanceInfo().getAvailableBalance()); - } - - @AfterAll - public static void tearDown() { - tearDownScaffold(); - } -} diff --git a/apitest/src/test/java/bisq/apitest/method/wallet/WalletProtectionTest.java b/apitest/src/test/java/bisq/apitest/method/wallet/WalletProtectionTest.java index 82174f5195a..f5dabd90593 100644 --- a/apitest/src/test/java/bisq/apitest/method/wallet/WalletProtectionTest.java +++ b/apitest/src/test/java/bisq/apitest/method/wallet/WalletProtectionTest.java @@ -48,7 +48,7 @@ public void testSetWalletPassword() { @Test @Order(2) public void testGetBalanceOnEncryptedWalletShouldThrowException() { - Throwable exception = assertThrows(StatusRuntimeException.class, () -> getBalance(alicedaemon)); + Throwable exception = assertThrows(StatusRuntimeException.class, () -> getBtcBalances(alicedaemon)); assertEquals("UNKNOWN: wallet is locked", exception.getMessage()); } @@ -57,9 +57,9 @@ public void testGetBalanceOnEncryptedWalletShouldThrowException() { public void testUnlockWalletFor4Seconds() { var request = createUnlockWalletRequest("first-password", 4); grpcStubs(alicedaemon).walletsService.unlockWallet(request); - getBalance(alicedaemon); // should not throw 'wallet locked' exception + getBtcBalances(alicedaemon); // should not throw 'wallet locked' exception sleep(4500); // let unlock timeout expire - Throwable exception = assertThrows(StatusRuntimeException.class, () -> getBalance(alicedaemon)); + Throwable exception = assertThrows(StatusRuntimeException.class, () -> getBtcBalances(alicedaemon)); assertEquals("UNKNOWN: wallet is locked", exception.getMessage()); } @@ -69,7 +69,7 @@ public void testGetBalanceAfterUnlockTimeExpiryShouldThrowException() { var request = createUnlockWalletRequest("first-password", 3); grpcStubs(alicedaemon).walletsService.unlockWallet(request); sleep(4000); // let unlock timeout expire - Throwable exception = assertThrows(StatusRuntimeException.class, () -> getBalance(alicedaemon)); + Throwable exception = assertThrows(StatusRuntimeException.class, () -> getBtcBalances(alicedaemon)); assertEquals("UNKNOWN: wallet is locked", exception.getMessage()); } @@ -79,7 +79,7 @@ public void testLockWalletBeforeUnlockTimeoutExpiry() { unlockWallet(alicedaemon, "first-password", 60); var request = createLockWalletRequest(); grpcStubs(alicedaemon).walletsService.lockWallet(request); - Throwable exception = assertThrows(StatusRuntimeException.class, () -> getBalance(alicedaemon)); + Throwable exception = assertThrows(StatusRuntimeException.class, () -> getBtcBalances(alicedaemon)); assertEquals("UNKNOWN: wallet is locked", exception.getMessage()); } @@ -99,7 +99,7 @@ public void testUnlockWalletTimeoutOverride() { sleep(500); // override unlock timeout after 0.5s unlockWallet(alicedaemon, "first-password", 6); sleep(5000); - getBalance(alicedaemon); // getbalance 5s after resetting unlock timeout to 6s + getBtcBalances(alicedaemon); // getbalance 5s after overriding timeout to 6s } @Test @@ -109,7 +109,7 @@ public void testSetNewWalletPassword() { "first-password", "second-password"); grpcStubs(alicedaemon).walletsService.setWalletPassword(request); unlockWallet(alicedaemon, "second-password", 2); - getBalance(alicedaemon); + getBtcBalances(alicedaemon); sleep(2500); // allow time for wallet save } @@ -128,7 +128,7 @@ public void testSetNewWalletPasswordWithIncorrectNewPasswordShouldThrowException public void testRemoveNewWalletPassword() { var request = createRemoveWalletPasswordRequest("second-password"); grpcStubs(alicedaemon).walletsService.removeWalletPassword(request); - getBalance(alicedaemon); // should not throw 'wallet locked' exception + getBtcBalances(alicedaemon); // should not throw 'wallet locked' exception } @AfterAll diff --git a/apitest/src/test/java/bisq/apitest/scenario/FundWalletScenarioTest.java b/apitest/src/test/java/bisq/apitest/scenario/FundWalletScenarioTest.java deleted file mode 100644 index 4b7d40f516c..00000000000 --- a/apitest/src/test/java/bisq/apitest/scenario/FundWalletScenarioTest.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -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.MethodOrderer; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.Test; -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.concurrent.TimeUnit.MILLISECONDS; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.fail; - - - -import bisq.apitest.method.MethodTest; - -@Slf4j -@TestMethodOrder(MethodOrderer.OrderAnnotation.class) -public class FundWalletScenarioTest extends MethodTest { - - @BeforeAll - public static void setUp() { - try { - setUpScaffold(bitcoind, seednode, alicedaemon); - bitcoinCli.generateBlocks(1); - MILLISECONDS.sleep(1500); - } catch (Exception ex) { - fail(ex); - } - } - - @Test - @Order(1) - public void testFundWallet() { - // bisq wallet was initialized with 10 btc - long balance = getBalance(alicedaemon); - assertEquals(1000000000, balance); - - String unusedAddress = getUnusedBtcAddress(alicedaemon); - bitcoinCli.sendToAddress(unusedAddress, "2.5"); - - bitcoinCli.generateBlocks(1); - sleep(1500); - - balance = getBalance(alicedaemon); - assertEquals(1250000000L, balance); // new balance is 12.5 btc - } - - @AfterAll - public static void tearDown() { - tearDownScaffold(); - } -} diff --git a/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java b/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java index f27f8f93f27..0ef678f9bc9 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java @@ -38,7 +38,6 @@ import bisq.apitest.method.MethodTest; import bisq.apitest.method.wallet.BsqWalletTest; import bisq.apitest.method.wallet.BtcWalletTest; -import bisq.apitest.method.wallet.WalletBalancesTest; import bisq.apitest.method.wallet.WalletProtectionTest; @Slf4j @@ -58,24 +57,15 @@ public static void setUp() { @Test @Order(1) - public void testGetWalletBalances(final TestInfo testInfo) { - WalletBalancesTest btcWalletTest = new WalletBalancesTest(); - - btcWalletTest.testDeprecatedAvailableBtcBalance(); - btcWalletTest.testNewGetBalances(testInfo); - } - - @Test - @Order(2) public void testBtcWalletFunding(final TestInfo testInfo) { BtcWalletTest btcWalletTest = new BtcWalletTest(); - btcWalletTest.testDeprecatedAvailableBtcBalance(); + btcWalletTest.testInitialBtcBalances(testInfo); btcWalletTest.testFundAlicesBtcWallet(testInfo); } @Test - @Order(3) + @Order(2) public void testBsqWalletFunding(final TestInfo testInfo) { BsqWalletTest bsqWalletTest = new BsqWalletTest(); @@ -86,7 +76,7 @@ public void testBsqWalletFunding(final TestInfo testInfo) { } @Test - @Order(4) + @Order(3) public void testWalletProtection() { // Batching all wallet tests in this test case reduces scaffold setup // time. Here, we create a method WalletProtectionTest instance and run each diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index edcfa365092..efc03fc3100 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -23,10 +23,7 @@ import bisq.proto.grpc.CreateOfferRequest; import bisq.proto.grpc.CreatePaymentAccountRequest; import bisq.proto.grpc.GetAddressBalanceRequest; -import bisq.proto.grpc.GetBalanceRequest; import bisq.proto.grpc.GetBalancesRequest; -import bisq.proto.grpc.GetBsqBalancesRequest; -import bisq.proto.grpc.GetBtcBalancesRequest; import bisq.proto.grpc.GetFundingAddressesRequest; import bisq.proto.grpc.GetOfferRequest; import bisq.proto.grpc.GetOffersRequest; @@ -58,7 +55,6 @@ import lombok.extern.slf4j.Slf4j; -import static bisq.cli.CurrencyFormat.formatSatoshis; import static bisq.cli.CurrencyFormat.toSatoshis; import static bisq.cli.NegativeNumberOptions.hasNegativeNumberOptions; import static bisq.cli.TableFormat.formatAddressBalanceTbl; @@ -93,10 +89,7 @@ private enum Method { createpaymentacct, getpaymentaccts, getversion, - @Deprecated getbalance, // Use getbalances, return bsq and btc balance info - getbalances, - getbsqbalance, - getbtcbalance, + getbalance, getaddressbalance, getfundingaddresses, getunusedbsqaddress, @@ -194,31 +187,16 @@ public static void run(String[] args) { return; } case getbalance: { - // Deprecated, use getbalances. - var request = GetBalanceRequest.newBuilder().build(); - var reply = walletsService.getBalance(request); - var btcBalance = formatSatoshis(reply.getBalance()); - out.println(btcBalance); - return; - } - case getbalances: { - var request = GetBalancesRequest.newBuilder().build(); + var currencyCode = nonOptionArgs.size() == 2 + ? nonOptionArgs.get(1) + : ""; + var request = GetBalancesRequest.newBuilder() + .setCurrencyCode(currencyCode) + .build(); var reply = walletsService.getBalances(request); out.println(formatBalancesTbls(reply.getBalances())); return; } - case getbsqbalance: { - var request = GetBsqBalancesRequest.newBuilder().build(); - var reply = walletsService.getBsqBalances(request); - out.println(reply.getBsqBalanceInfo()); - return; - } - case getbtcbalance: { - var request = GetBtcBalancesRequest.newBuilder().build(); - var reply = walletsService.getBtcBalances(request); - out.println(reply.getBtcBalanceInfo()); - return; - } case getaddressbalance: { if (nonOptionArgs.size() < 2) throw new IllegalArgumentException("no address specified"); @@ -556,10 +534,7 @@ private static void printHelp(OptionParser parser, PrintStream stream) { stream.format(rowFormat, "Method", "Params", "Description"); stream.format(rowFormat, "------", "------", "------------"); stream.format(rowFormat, "getversion", "", "Get server version"); - stream.format(rowFormat, "getbalance", "", "Get server wallet balance (deprecated, use getbalances"); - stream.format(rowFormat, "getbalances", "", "Get server wallet bsq and btc balances"); - stream.format(rowFormat, "getbsqbalance", "", "Get server wallet bsq balance"); - stream.format(rowFormat, "getbtcbalance", "", "Get server wallet btc balance"); + stream.format(rowFormat, "getbalance [,currency code = bsq|btc]", "", "Get server wallet balances"); stream.format(rowFormat, "getaddressbalance", "address", "Get server wallet address balance"); stream.format(rowFormat, "getfundingaddresses", "", "Get BTC funding addresses"); stream.format(rowFormat, "getunusedbsqaddress", "", "Get unused BSQ address"); diff --git a/cli/src/main/java/bisq/cli/TableFormat.java b/cli/src/main/java/bisq/cli/TableFormat.java index ff4e440f18f..a23b7a022d8 100644 --- a/cli/src/main/java/bisq/cli/TableFormat.java +++ b/cli/src/main/java/bisq/cli/TableFormat.java @@ -68,9 +68,9 @@ public static String formatAddressBalanceTbl(List addressBal public static String formatBalancesTbls(BalancesInfo balancesInfo) { return "BTC" + "\n" - + formatBtcBalanceInfoTbl(balancesInfo.getBtcBalanceInfo()) + "\n" + + formatBtcBalanceInfoTbl(balancesInfo.getBtc()) + "\n" + "BSQ" + "\n" - + formatBsqBalanceInfoTbl(balancesInfo.getBsqBalanceInfo()); + + formatBsqBalanceInfoTbl(balancesInfo.getBsq()); } public static String formatBsqBalanceInfoTbl(BsqBalanceInfo bsqBalanceInfo) { diff --git a/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java index bbf3badf6e7..360aedb776d 100644 --- a/core/src/main/java/bisq/core/api/CoreApi.java +++ b/core/src/main/java/bisq/core/api/CoreApi.java @@ -221,21 +221,8 @@ public String getTradeRole(String tradeId) { // Wallets /////////////////////////////////////////////////////////////////////////////////////////// - @Deprecated - public long getAvailableBalance() { - return walletsService.getAvailableBalance(); - } - - public BalancesInfo getBalances() { - return walletsService.getBalances(); - } - - public BsqBalanceInfo getBsqBalances() { - return walletsService.getBsqBalances(); - } - - public BtcBalanceInfo getBtcBalances() { - return walletsService.getBtcBalances(); + public BalancesInfo getBalances(String currencyCode) { + return walletsService.getBalances(currencyCode); } public long getAddressBalance(String addressString) { diff --git a/core/src/main/java/bisq/core/api/CoreWalletsService.java b/core/src/main/java/bisq/core/api/CoreWalletsService.java index b52e59a00da..529b97a17f6 100644 --- a/core/src/main/java/bisq/core/api/CoreWalletsService.java +++ b/core/src/main/java/bisq/core/api/CoreWalletsService.java @@ -114,54 +114,21 @@ long getAvailableBalance() { return balance.getValue(); } - BalancesInfo getBalances() { + BalancesInfo getBalances(String currencyCode) { + verifyWalletCurrencyCodeIsValid(currencyCode); verifyWalletsAreAvailable(); verifyEncryptedWalletIsUnlocked(); if (balances.getAvailableBalance().get() == null) throw new IllegalStateException("balance is not yet available"); - return new BalancesInfo(getBsqBalances(), getBtcBalances()); - } - - BsqBalanceInfo getBsqBalances() { - verifyWalletsAreAvailable(); - verifyEncryptedWalletIsUnlocked(); - - var availableConfirmedBalance = bsqWalletService.getAvailableConfirmedBalance(); - var unverifiedBalance = bsqWalletService.getUnverifiedBalance(); - var unconfirmedChangeBalance = bsqWalletService.getUnconfirmedChangeBalance(); - var lockedForVotingBalance = bsqWalletService.getLockedForVotingBalance(); - var lockupBondsBalance = bsqWalletService.getLockupBondsBalance(); - var unlockingBondsBalance = bsqWalletService.getUnlockingBondsBalance(); - - return new BsqBalanceInfo(availableConfirmedBalance.value, - unverifiedBalance.value, - unconfirmedChangeBalance.value, - lockedForVotingBalance.value, - lockupBondsBalance.value, - unlockingBondsBalance.value); - } - - BtcBalanceInfo getBtcBalances() { - verifyWalletsAreAvailable(); - verifyEncryptedWalletIsUnlocked(); - - var availableBalance = balances.getAvailableBalance().get(); - if (availableBalance == null) - throw new IllegalStateException("balance is not yet available"); - - var reservedBalance = balances.getReservedBalance().get(); - if (reservedBalance == null) - throw new IllegalStateException("reserved balance is not yet available"); - - var lockedBalance = balances.getLockedBalance().get(); - if (lockedBalance == null) - throw new IllegalStateException("locked balance is not yet available"); - - return new BtcBalanceInfo(availableBalance.value, - reservedBalance.value, - availableBalance.add(reservedBalance).value, - lockedBalance.value); + switch (currencyCode.trim().toUpperCase()) { + case "BSQ": + return new BalancesInfo(getBsqBalances(), BtcBalanceInfo.EMPTY); + case "BTC": + return new BalancesInfo(BsqBalanceInfo.EMPTY, getBtcBalances()); + default: + return new BalancesInfo(getBsqBalances(), getBtcBalances()); + } } long getAddressBalance(String addressString) { @@ -340,6 +307,57 @@ private void verifyEncryptedWalletIsUnlocked() { throw new IllegalStateException("wallet is locked"); } + // Throws a RuntimeException if wallet currency code is not BSQ or BTC. + private void verifyWalletCurrencyCodeIsValid(String currencyCode) { + if (currencyCode == null || currencyCode.isEmpty()) + return; + + if (!currencyCode.equalsIgnoreCase("BSQ") + && !currencyCode.equalsIgnoreCase("BTC")) + throw new IllegalStateException(format("wallet does not support %s", currencyCode)); + } + + private BsqBalanceInfo getBsqBalances() { + verifyWalletsAreAvailable(); + verifyEncryptedWalletIsUnlocked(); + + var availableConfirmedBalance = bsqWalletService.getAvailableConfirmedBalance(); + var unverifiedBalance = bsqWalletService.getUnverifiedBalance(); + var unconfirmedChangeBalance = bsqWalletService.getUnconfirmedChangeBalance(); + var lockedForVotingBalance = bsqWalletService.getLockedForVotingBalance(); + var lockupBondsBalance = bsqWalletService.getLockupBondsBalance(); + var unlockingBondsBalance = bsqWalletService.getUnlockingBondsBalance(); + + return new BsqBalanceInfo(availableConfirmedBalance.value, + unverifiedBalance.value, + unconfirmedChangeBalance.value, + lockedForVotingBalance.value, + lockupBondsBalance.value, + unlockingBondsBalance.value); + } + + private BtcBalanceInfo getBtcBalances() { + verifyWalletsAreAvailable(); + verifyEncryptedWalletIsUnlocked(); + + var availableBalance = balances.getAvailableBalance().get(); + if (availableBalance == null) + throw new IllegalStateException("balance is not yet available"); + + var reservedBalance = balances.getReservedBalance().get(); + if (reservedBalance == null) + throw new IllegalStateException("reserved balance is not yet available"); + + var lockedBalance = balances.getLockedBalance().get(); + if (lockedBalance == null) + throw new IllegalStateException("locked balance is not yet available"); + + return new BtcBalanceInfo(availableBalance.value, + reservedBalance.value, + availableBalance.add(reservedBalance).value, + lockedBalance.value); + } + // Returns a LegacyAddress for the string, or a RuntimeException if invalid. private LegacyAddress getValidBsqLegacyAddress(String address) { try { diff --git a/core/src/main/java/bisq/core/api/model/BalancesInfo.java b/core/src/main/java/bisq/core/api/model/BalancesInfo.java index 39179fde2b3..cecb5ee4a12 100644 --- a/core/src/main/java/bisq/core/api/model/BalancesInfo.java +++ b/core/src/main/java/bisq/core/api/model/BalancesInfo.java @@ -7,12 +7,15 @@ @Getter public class BalancesInfo implements Payload { - private final BsqBalanceInfo bsqBalanceInfo; - private final BtcBalanceInfo btcBalanceInfo; - - public BalancesInfo(BsqBalanceInfo bsqBalanceInfo, BtcBalanceInfo btcBalanceInfo) { - this.bsqBalanceInfo = bsqBalanceInfo; - this.btcBalanceInfo = btcBalanceInfo; + // Getter names are shortened for readability's sake, i.e., + // balancesInfo.getBtc().getAvailableBalance() is clean than + // balancesInfo.getBtcBalanceInfo().getAvailableBalance(). + private final BsqBalanceInfo bsq; + private final BtcBalanceInfo btc; + + public BalancesInfo(BsqBalanceInfo bsq, BtcBalanceInfo btc) { + this.bsq = bsq; + this.btc = btc; } /////////////////////////////////////////////////////////////////////////////////////////// @@ -22,21 +25,21 @@ public BalancesInfo(BsqBalanceInfo bsqBalanceInfo, BtcBalanceInfo btcBalanceInfo @Override public bisq.proto.grpc.BalancesInfo toProtoMessage() { return bisq.proto.grpc.BalancesInfo.newBuilder() - .setBsqBalanceInfo(bsqBalanceInfo.toProtoMessage()) - .setBtcBalanceInfo(btcBalanceInfo.toProtoMessage()) + .setBsq(bsq.toProtoMessage()) + .setBtc(btc.toProtoMessage()) .build(); } public static BalancesInfo fromProto(bisq.proto.grpc.BalancesInfo proto) { - return new BalancesInfo(BsqBalanceInfo.fromProto(proto.getBsqBalanceInfo()), - BtcBalanceInfo.fromProto(proto.getBtcBalanceInfo())); + return new BalancesInfo(BsqBalanceInfo.fromProto(proto.getBsq()), + BtcBalanceInfo.fromProto(proto.getBtc())); } @Override public String toString() { return "BalancesInfo{" + "\n" + - " " + bsqBalanceInfo.toString() + "\n" + - ", " + btcBalanceInfo.toString() + "\n" + + " " + bsq.toString() + "\n" + + ", " + btc.toString() + "\n" + '}'; } } diff --git a/core/src/main/java/bisq/core/api/model/BsqBalanceInfo.java b/core/src/main/java/bisq/core/api/model/BsqBalanceInfo.java index ff314e087ba..23324e21f33 100644 --- a/core/src/main/java/bisq/core/api/model/BsqBalanceInfo.java +++ b/core/src/main/java/bisq/core/api/model/BsqBalanceInfo.java @@ -9,6 +9,13 @@ @Getter public class BsqBalanceInfo implements Payload { + public static final BsqBalanceInfo EMPTY = new BsqBalanceInfo(-1, + -1, + -1, + -1, + -1, + -1); + // All balances are in BSQ satoshis. private final long availableConfirmedBalance; private final long unverifiedBalance; diff --git a/core/src/main/java/bisq/core/api/model/BtcBalanceInfo.java b/core/src/main/java/bisq/core/api/model/BtcBalanceInfo.java index 6e8708c3e37..e3803b0001e 100644 --- a/core/src/main/java/bisq/core/api/model/BtcBalanceInfo.java +++ b/core/src/main/java/bisq/core/api/model/BtcBalanceInfo.java @@ -9,6 +9,11 @@ @Getter public class BtcBalanceInfo implements Payload { + public static final BtcBalanceInfo EMPTY = new BtcBalanceInfo(-1, + -1, + -1, + -1); + // All balances are in BTC satoshis. private final long availableBalance; private final long reservedBalance; diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java index 31530f3a660..110e4b5cc65 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java @@ -19,21 +19,13 @@ import bisq.core.api.CoreApi; import bisq.core.api.model.AddressBalanceInfo; -import bisq.core.api.model.BsqBalanceInfo; -import bisq.core.api.model.BtcBalanceInfo; import bisq.core.btc.exceptions.TxBroadcastException; import bisq.core.btc.wallet.TxBroadcaster; import bisq.proto.grpc.GetAddressBalanceReply; import bisq.proto.grpc.GetAddressBalanceRequest; -import bisq.proto.grpc.GetBalanceReply; -import bisq.proto.grpc.GetBalanceRequest; import bisq.proto.grpc.GetBalancesReply; import bisq.proto.grpc.GetBalancesRequest; -import bisq.proto.grpc.GetBsqBalancesReply; -import bisq.proto.grpc.GetBsqBalancesRequest; -import bisq.proto.grpc.GetBtcBalancesReply; -import bisq.proto.grpc.GetBtcBalancesRequest; import bisq.proto.grpc.GetFundingAddressesReply; import bisq.proto.grpc.GetFundingAddressesRequest; import bisq.proto.grpc.GetUnusedBsqAddressReply; @@ -73,26 +65,10 @@ public GrpcWalletsService(CoreApi coreApi) { this.coreApi = coreApi; } - - @Deprecated - @Override - public void getBalance(GetBalanceRequest req, StreamObserver responseObserver) { - try { - long availableBalance = coreApi.getAvailableBalance(); - var reply = GetBalanceReply.newBuilder().setBalance(availableBalance).build(); - responseObserver.onNext(reply); - responseObserver.onCompleted(); - } catch (IllegalStateException cause) { - var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage())); - responseObserver.onError(ex); - throw ex; - } - } - @Override public void getBalances(GetBalancesRequest req, StreamObserver responseObserver) { try { - var balances = coreApi.getBalances(); + var balances = coreApi.getBalances(req.getCurrencyCode()); var reply = GetBalancesReply.newBuilder() .setBalances(balances.toProtoMessage()) .build(); @@ -105,38 +81,6 @@ public void getBalances(GetBalancesRequest req, StreamObserver } } - @Override - public void getBsqBalances(GetBsqBalancesRequest req, StreamObserver responseObserver) { - try { - BsqBalanceInfo bsqBalanceInfo = coreApi.getBsqBalances(); - var reply = GetBsqBalancesReply.newBuilder() - .setBsqBalanceInfo(bsqBalanceInfo.toProtoMessage()) - .build(); - responseObserver.onNext(reply); - responseObserver.onCompleted(); - } catch (IllegalStateException cause) { - var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage())); - responseObserver.onError(ex); - throw ex; - } - } - - @Override - public void getBtcBalances(GetBtcBalancesRequest req, StreamObserver responseObserver) { - try { - BtcBalanceInfo btcBalanceInfo = coreApi.getBtcBalances(); - var reply = GetBtcBalancesReply.newBuilder() - .setBtcBalanceInfo(btcBalanceInfo.toProtoMessage()) - .build(); - responseObserver.onNext(reply); - responseObserver.onCompleted(); - } catch (IllegalStateException cause) { - var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage())); - responseObserver.onError(ex); - throw ex; - } - } - @Override public void getAddressBalance(GetAddressBalanceRequest req, StreamObserver responseObserver) { diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index 5005ad1eef8..6dc0be30a59 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -276,14 +276,8 @@ message TradeInfo { /////////////////////////////////////////////////////////////////////////////////////////// service Wallets { - rpc GetBalance (GetBalanceRequest) returns (GetBalanceReply) { - } rpc GetBalances (GetBalancesRequest) returns (GetBalancesReply) { } - rpc GetBsqBalances (GetBsqBalancesRequest) returns (GetBsqBalancesReply) { - } - rpc GetBtcBalances (GetBtcBalancesRequest) returns (GetBtcBalancesReply) { - } rpc GetAddressBalance (GetAddressBalanceRequest) returns (GetAddressBalanceReply) { } rpc GetUnusedBsqAddress (GetUnusedBsqAddressRequest) returns (GetUnusedBsqAddressReply) { @@ -302,34 +296,14 @@ service Wallets { } } -message GetBalanceRequest { -} - -message GetBalanceReply { - uint64 balance = 1; -} - message GetBalancesRequest { + string currencyCode = 1; } message GetBalancesReply { BalancesInfo balances = 1; } -message GetBsqBalancesRequest { -} - -message GetBsqBalancesReply { - BsqBalanceInfo bsqBalanceInfo = 1; -} - -message GetBtcBalancesRequest { -} - -message GetBtcBalancesReply { - BtcBalanceInfo btcBalanceInfo = 1; -} - message GetAddressBalanceRequest { string address = 1; } @@ -390,8 +364,11 @@ message UnlockWalletReply { } message BalancesInfo { - BsqBalanceInfo bsqBalanceInfo = 1; - BtcBalanceInfo btcBalanceInfo = 2; + // Field names are shortened for readability's sake, i.e., + // balancesInfo.getBtc().getAvailableBalance() is cleaner than + // balancesInfo.getBtcBalanceInfo().getAvailableBalance(). + BsqBalanceInfo bsq = 1; + BtcBalanceInfo btc = 2; } message BsqBalanceInfo { From 187a85f95bdc7a62f7faebbef4564fb7a7d69255 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sat, 14 Nov 2020 13:19:24 -0300 Subject: [PATCH 18/52] Fix typo --- core/src/main/java/bisq/core/api/model/BalancesInfo.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/bisq/core/api/model/BalancesInfo.java b/core/src/main/java/bisq/core/api/model/BalancesInfo.java index cecb5ee4a12..3b063bc0d2b 100644 --- a/core/src/main/java/bisq/core/api/model/BalancesInfo.java +++ b/core/src/main/java/bisq/core/api/model/BalancesInfo.java @@ -8,7 +8,7 @@ public class BalancesInfo implements Payload { // Getter names are shortened for readability's sake, i.e., - // balancesInfo.getBtc().getAvailableBalance() is clean than + // balancesInfo.getBtc().getAvailableBalance() is cleaner than // balancesInfo.getBtcBalanceInfo().getAvailableBalance(). private final BsqBalanceInfo bsq; private final BtcBalanceInfo btc; From 34efc049f80e564345adbff0329c3cc2dbd2d0f2 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sat, 14 Nov 2020 13:34:22 -0300 Subject: [PATCH 19/52] Remove unnecessary fully qualified name --- .../src/test/java/bisq/apitest/method/wallet/BtcWalletTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apitest/src/test/java/bisq/apitest/method/wallet/BtcWalletTest.java b/apitest/src/test/java/bisq/apitest/method/wallet/BtcWalletTest.java index a9d1feb401b..daee479b89a 100644 --- a/apitest/src/test/java/bisq/apitest/method/wallet/BtcWalletTest.java +++ b/apitest/src/test/java/bisq/apitest/method/wallet/BtcWalletTest.java @@ -98,7 +98,7 @@ public static void tearDown() { } private void verifyBtcBalances(bisq.core.api.model.BtcBalanceInfo expected, - bisq.proto.grpc.BtcBalanceInfo actual) { + BtcBalanceInfo actual) { assertEquals(expected.getAvailableBalance(), actual.getAvailableBalance()); assertEquals(expected.getReservedBalance(), actual.getReservedBalance()); assertEquals(expected.getTotalAvailableBalance(), actual.getTotalAvailableBalance()); From 530a9f95a4e302e4a9fb01c346a7739988866d33 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sat, 14 Nov 2020 13:34:59 -0300 Subject: [PATCH 20/52] Remove unused imports --- core/src/main/java/bisq/core/api/CoreApi.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java index 360aedb776d..cd5ce5b53f5 100644 --- a/core/src/main/java/bisq/core/api/CoreApi.java +++ b/core/src/main/java/bisq/core/api/CoreApi.java @@ -19,8 +19,6 @@ import bisq.core.api.model.AddressBalanceInfo; import bisq.core.api.model.BalancesInfo; -import bisq.core.api.model.BsqBalanceInfo; -import bisq.core.api.model.BtcBalanceInfo; import bisq.core.btc.wallet.TxBroadcaster; import bisq.core.monetary.Price; import bisq.core.offer.Offer; From ec381522ffeec3b7600e6cd752899660373d2505 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 18 Nov 2020 12:03:22 -0300 Subject: [PATCH 21/52] Add api method 'getpaymentmethods' Returns a list of supported payment method IDs. This gives CLI users the correct payment method id for creating a new payment account. - Added gRPC service GetPaymentMethods to grpc.proto. - Added gRPC boilerplate method to GrpcPaymentAccountsService. - Added implimentation to CoreApi -> CorePaymentAccountsService. - Refactored PaymentAccountTest hierarchy. - Add api method to CLI. --- .../java/bisq/apitest/method/MethodTest.java | 8 +++ .../CreatePaymentAccountTest.java | 6 +- .../method/payment/GetPaymentMethodsTest.java | 53 ++++++++++++++++++ .../apitest/scenario/PaymentAccountTest.java | 56 +++++++++++++++++++ .../bisq/apitest/scenario/StartupTest.java | 8 --- cli/src/main/java/bisq/cli/CliMain.java | 8 +++ core/src/main/java/bisq/core/api/CoreApi.java | 5 ++ .../core/api/CorePaymentAccountsService.java | 10 ++++ .../grpc/GrpcPaymentAccountsService.java | 15 +++++ proto/src/main/proto/grpc.proto | 9 +++ 10 files changed, 169 insertions(+), 9 deletions(-) rename apitest/src/test/java/bisq/apitest/method/{ => payment}/CreatePaymentAccountTest.java (97%) create mode 100644 apitest/src/test/java/bisq/apitest/method/payment/GetPaymentMethodsTest.java create mode 100644 apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java diff --git a/apitest/src/test/java/bisq/apitest/method/MethodTest.java b/apitest/src/test/java/bisq/apitest/method/MethodTest.java index 72634b16785..34fbeffbc56 100644 --- a/apitest/src/test/java/bisq/apitest/method/MethodTest.java +++ b/apitest/src/test/java/bisq/apitest/method/MethodTest.java @@ -30,6 +30,7 @@ import bisq.proto.grpc.GetFundingAddressesRequest; import bisq.proto.grpc.GetOfferRequest; import bisq.proto.grpc.GetPaymentAccountsRequest; +import bisq.proto.grpc.GetPaymentMethodsRequest; import bisq.proto.grpc.GetTradeRequest; import bisq.proto.grpc.GetUnusedBsqAddressRequest; import bisq.proto.grpc.KeepFundsRequest; @@ -46,7 +47,9 @@ import bisq.proto.grpc.WithdrawFundsRequest; import protobuf.PaymentAccount; +import protobuf.PaymentMethod; +import java.util.List; import java.util.stream.Collectors; import static bisq.apitest.config.BisqAppConfig.alicedaemon; @@ -244,6 +247,11 @@ protected final String getUnusedBtcAddress(BisqAppConfig bisqAppConfig) { .getAddress(); } + protected final List getPaymentMethods(BisqAppConfig bisqAppConfig) { + var req = GetPaymentMethodsRequest.newBuilder().build(); + return grpcStubs(bisqAppConfig).paymentAccountsService.getPaymentMethods(req).getPaymentMethodsList(); + } + protected final CreatePaymentAccountRequest createCreatePerfectMoneyPaymentAccountRequest( String accountName, String accountNumber, diff --git a/apitest/src/test/java/bisq/apitest/method/CreatePaymentAccountTest.java b/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java similarity index 97% rename from apitest/src/test/java/bisq/apitest/method/CreatePaymentAccountTest.java rename to apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java index 9e8b0af878a..83e827834e7 100644 --- a/apitest/src/test/java/bisq/apitest/method/CreatePaymentAccountTest.java +++ b/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java @@ -15,7 +15,7 @@ * along with Bisq. If not, see . */ -package bisq.apitest.method; +package bisq.apitest.method.payment; import bisq.proto.grpc.GetPaymentAccountsRequest; @@ -41,6 +41,10 @@ import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; + + +import bisq.apitest.method.MethodTest; + @Disabled @Slf4j @TestMethodOrder(OrderAnnotation.class) diff --git a/apitest/src/test/java/bisq/apitest/method/payment/GetPaymentMethodsTest.java b/apitest/src/test/java/bisq/apitest/method/payment/GetPaymentMethodsTest.java new file mode 100644 index 00000000000..acd791f596c --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/payment/GetPaymentMethodsTest.java @@ -0,0 +1,53 @@ +package bisq.apitest.method.payment; + +import java.util.List; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind; +import static bisq.apitest.config.BisqAppConfig.alicedaemon; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + + + +import bisq.apitest.method.MethodTest; + +@Disabled +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class GetPaymentMethodsTest extends MethodTest { + + @BeforeAll + public static void setUp() { + try { + setUpScaffold(bitcoind, alicedaemon); + } catch (Exception ex) { + fail(ex); + } + } + + @Test + @Order(1) + public void testGetPaymentMethods() { + List paymentMethodIds = getPaymentMethods(alicedaemon) + .stream() + .map(p -> p.getId()) + .collect(Collectors.toList()); + assertTrue(paymentMethodIds.size() > 25); + } + + @AfterAll + public static void tearDown() { + tearDownScaffold(); + } +} diff --git a/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java b/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java new file mode 100644 index 00000000000..af36eba27cb --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java @@ -0,0 +1,56 @@ +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.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +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.arbdaemon; +import static bisq.apitest.config.BisqAppConfig.seednode; +import static org.junit.jupiter.api.Assertions.fail; + + + +import bisq.apitest.method.MethodTest; +import bisq.apitest.method.payment.CreatePaymentAccountTest; +import bisq.apitest.method.payment.GetPaymentMethodsTest; + +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class PaymentAccountTest extends MethodTest { + + @BeforeAll + public static void setUp() { + try { + setUpScaffold(bitcoind, seednode, arbdaemon, alicedaemon); + } catch (Exception ex) { + fail(ex); + } + } + + @Test + @Order(1) + public void testGetPaymentMethods() { + GetPaymentMethodsTest test = new GetPaymentMethodsTest(); + test.testGetPaymentMethods(); + } + + @Test + @Order(2) + public void testCreatePaymentAccount() { + CreatePaymentAccountTest test = new CreatePaymentAccountTest(); + test.testCreatePerfectMoneyUSDPaymentAccount(); + } + + @AfterAll + public static void tearDown() { + tearDownScaffold(); + } + +} diff --git a/apitest/src/test/java/bisq/apitest/scenario/StartupTest.java b/apitest/src/test/java/bisq/apitest/scenario/StartupTest.java index fa81ddff6b9..26a95f3c773 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/StartupTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/StartupTest.java @@ -34,7 +34,6 @@ -import bisq.apitest.method.CreatePaymentAccountTest; import bisq.apitest.method.GetVersionTest; import bisq.apitest.method.MethodTest; import bisq.apitest.method.RegisterDisputeAgentsTest; @@ -71,13 +70,6 @@ public void testRegisterDisputeAgents() { test.testRegisterRefundAgent(); } - @Test - @Order(3) - public void testCreatePaymentAccount() { - CreatePaymentAccountTest test = new CreatePaymentAccountTest(); - test.testCreatePerfectMoneyUSDPaymentAccount(); - } - @AfterAll public static void tearDown() { tearDownScaffold(); diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index efc03fc3100..188a9372881 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -28,6 +28,7 @@ import bisq.proto.grpc.GetOfferRequest; import bisq.proto.grpc.GetOffersRequest; import bisq.proto.grpc.GetPaymentAccountsRequest; +import bisq.proto.grpc.GetPaymentMethodsRequest; import bisq.proto.grpc.GetTradeRequest; import bisq.proto.grpc.GetUnusedBsqAddressRequest; import bisq.proto.grpc.GetVersionRequest; @@ -86,6 +87,7 @@ private enum Method { confirmpaymentreceived, keepfunds, withdrawfunds, + getpaymentmethods, createpaymentacct, getpaymentaccts, getversion, @@ -416,6 +418,11 @@ public static void run(String[] args) { out.printf("funds from trade '%s' sent to btc address '%s'", tradeId, address); return; } + case getpaymentmethods: { + var request = GetPaymentMethodsRequest.newBuilder().build(); + var reply = paymentAccountsService.getPaymentMethods(request); + reply.getPaymentMethodsList().forEach(out::println); + } case createpaymentacct: { if (nonOptionArgs.size() < 5) throw new IllegalArgumentException( @@ -552,6 +559,7 @@ private static void printHelp(OptionParser parser, PrintStream stream) { stream.format(rowFormat, "confirmpaymentreceived", "trade id", "Confirm payment received"); stream.format(rowFormat, "keepfunds", "trade id", "Keep received funds in Bisq wallet"); stream.format(rowFormat, "withdrawfunds", "trade id, bitcoin wallet address", "Withdraw received funds to external wallet address"); + stream.format(rowFormat, "getpaymentmethods", "", "Get list of supported payment account method ids"); stream.format(rowFormat, "createpaymentacct", "account name, account number, currency code", "Create PerfectMoney dummy account"); stream.format(rowFormat, "getpaymentaccts", "", "Get user payment accounts"); stream.format(rowFormat, "lockwallet", "", "Remove wallet password from memory, locking the wallet"); diff --git a/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java index cd5ce5b53f5..cc50da2acdd 100644 --- a/core/src/main/java/bisq/core/api/CoreApi.java +++ b/core/src/main/java/bisq/core/api/CoreApi.java @@ -24,6 +24,7 @@ import bisq.core.offer.Offer; import bisq.core.offer.OfferPayload; import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.PaymentMethod; import bisq.core.trade.Trade; import bisq.core.trade.statistics.TradeStatistics3; import bisq.core.trade.statistics.TradeStatisticsManager; @@ -168,6 +169,10 @@ public Set getPaymentAccounts() { return paymentAccountsService.getPaymentAccounts(); } + public List getPaymentMethods() { + return paymentAccountsService.getPaymentMethods(); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Prices /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java b/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java index a202b0dbdb4..8d8dc3f42ad 100644 --- a/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java +++ b/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java @@ -29,7 +29,10 @@ import javax.inject.Inject; +import java.util.Comparator; +import java.util.List; import java.util.Set; +import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; @@ -75,6 +78,13 @@ Set getPaymentAccounts() { return user.getPaymentAccounts(); } + List getPaymentMethods() { + return PaymentMethod.getPaymentMethods().stream() + .filter(paymentMethod -> !paymentMethod.isAsset()) + .sorted(Comparator.comparing(PaymentMethod::getId)) + .collect(Collectors.toList()); + } + private PaymentAccount getNewPaymentAccount(String paymentMethodId, String accountName, String accountNumber, diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java index 91060cbc829..64175f3d0ff 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java @@ -19,11 +19,14 @@ import bisq.core.api.CoreApi; import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.PaymentMethod; import bisq.proto.grpc.CreatePaymentAccountReply; import bisq.proto.grpc.CreatePaymentAccountRequest; import bisq.proto.grpc.GetPaymentAccountsReply; import bisq.proto.grpc.GetPaymentAccountsRequest; +import bisq.proto.grpc.GetPaymentMethodsReply; +import bisq.proto.grpc.GetPaymentMethodsRequest; import bisq.proto.grpc.PaymentAccountsGrpc; import io.grpc.stub.StreamObserver; @@ -65,4 +68,16 @@ public void getPaymentAccounts(GetPaymentAccountsRequest req, responseObserver.onNext(reply); responseObserver.onCompleted(); } + + @Override + public void getPaymentMethods(GetPaymentMethodsRequest req, + StreamObserver responseObserver) { + var paymentMethods = coreApi.getPaymentMethods().stream() + .map(PaymentMethod::toProtoMessage) + .collect(Collectors.toList()); + var reply = GetPaymentMethodsReply.newBuilder() + .addAllPaymentMethods(paymentMethods).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } } diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index 6dc0be30a59..b29ad979eba 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -125,6 +125,8 @@ service PaymentAccounts { } rpc GetPaymentAccounts (GetPaymentAccountsRequest) returns (GetPaymentAccountsReply) { } + rpc GetPaymentMethods (GetPaymentMethodsRequest) returns (GetPaymentMethodsReply) { + } } message CreatePaymentAccountRequest { @@ -145,6 +147,13 @@ message GetPaymentAccountsReply { repeated PaymentAccount paymentAccounts = 1; } +message GetPaymentMethodsRequest { +} + +message GetPaymentMethodsReply { + repeated PaymentMethod paymentMethods = 1; +} + /////////////////////////////////////////////////////////////////////////////////////////// // Price /////////////////////////////////////////////////////////////////////////////////////////// From a46526198dc4667621d0d3bc6f5b86d7a4f2d6a3 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 18 Nov 2020 12:29:59 -0300 Subject: [PATCH 22/52] Avoid codacy issue over use of fully qualified name Had to change the getPaymentMethods() names to getPaymentMethodIds() to avoid this codacy issue: "Unnecessary use of fully qualified name 'PaymentMethod.getPaymentMethods' due to existing static import 'bisq.core.payment.payload.PaymentMethod.*'" If 'PaymentMethod.getPaymentMethods' was changed to 'getPaymentMethods', a recursive loop would result, ending in an out of stack heap crash. This renaming of the method on the server is correct, but the CLI's 'getpaymentmethods' name was not changed. --- core/src/main/java/bisq/core/api/CoreApi.java | 4 ++-- .../main/java/bisq/core/api/CorePaymentAccountsService.java | 4 ++-- .../java/bisq/daemon/grpc/GrpcPaymentAccountsService.java | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java index cc50da2acdd..95ceece230f 100644 --- a/core/src/main/java/bisq/core/api/CoreApi.java +++ b/core/src/main/java/bisq/core/api/CoreApi.java @@ -169,8 +169,8 @@ public Set getPaymentAccounts() { return paymentAccountsService.getPaymentAccounts(); } - public List getPaymentMethods() { - return paymentAccountsService.getPaymentMethods(); + public List getPaymentMethodIds() { + return paymentAccountsService.getPaymentMethodIds(); } /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java b/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java index 8d8dc3f42ad..b27abfeaf47 100644 --- a/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java +++ b/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java @@ -78,8 +78,8 @@ Set getPaymentAccounts() { return user.getPaymentAccounts(); } - List getPaymentMethods() { - return PaymentMethod.getPaymentMethods().stream() + List getPaymentMethodIds() { + return getPaymentMethods().stream() .filter(paymentMethod -> !paymentMethod.isAsset()) .sorted(Comparator.comparing(PaymentMethod::getId)) .collect(Collectors.toList()); diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java index 64175f3d0ff..898219ac7c3 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java @@ -72,7 +72,7 @@ public void getPaymentAccounts(GetPaymentAccountsRequest req, @Override public void getPaymentMethods(GetPaymentMethodsRequest req, StreamObserver responseObserver) { - var paymentMethods = coreApi.getPaymentMethods().stream() + var paymentMethods = coreApi.getPaymentMethodIds().stream() .map(PaymentMethod::toProtoMessage) .collect(Collectors.toList()); var reply = GetPaymentMethodsReply.newBuilder() From 0046b08f9d748f2bca0595cc1f245633977b2aac Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 18 Nov 2020 12:47:12 -0300 Subject: [PATCH 23/52] Revert "Avoid codacy issue over use of fully qualified name" This reverts commit a46526198dc4667621d0d3bc6f5b86d7a4f2d6a3. The service side method name change is not correct. --- core/src/main/java/bisq/core/api/CoreApi.java | 4 ++-- .../main/java/bisq/core/api/CorePaymentAccountsService.java | 4 ++-- .../java/bisq/daemon/grpc/GrpcPaymentAccountsService.java | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java index 95ceece230f..cc50da2acdd 100644 --- a/core/src/main/java/bisq/core/api/CoreApi.java +++ b/core/src/main/java/bisq/core/api/CoreApi.java @@ -169,8 +169,8 @@ public Set getPaymentAccounts() { return paymentAccountsService.getPaymentAccounts(); } - public List getPaymentMethodIds() { - return paymentAccountsService.getPaymentMethodIds(); + public List getPaymentMethods() { + return paymentAccountsService.getPaymentMethods(); } /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java b/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java index b27abfeaf47..8d8dc3f42ad 100644 --- a/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java +++ b/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java @@ -78,8 +78,8 @@ Set getPaymentAccounts() { return user.getPaymentAccounts(); } - List getPaymentMethodIds() { - return getPaymentMethods().stream() + List getPaymentMethods() { + return PaymentMethod.getPaymentMethods().stream() .filter(paymentMethod -> !paymentMethod.isAsset()) .sorted(Comparator.comparing(PaymentMethod::getId)) .collect(Collectors.toList()); diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java index 898219ac7c3..64175f3d0ff 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java @@ -72,7 +72,7 @@ public void getPaymentAccounts(GetPaymentAccountsRequest req, @Override public void getPaymentMethods(GetPaymentMethodsRequest req, StreamObserver responseObserver) { - var paymentMethods = coreApi.getPaymentMethodIds().stream() + var paymentMethods = coreApi.getPaymentMethods().stream() .map(PaymentMethod::toProtoMessage) .collect(Collectors.toList()); var reply = GetPaymentMethodsReply.newBuilder() From 7a7d5ba0a77e0227711acd01e80cf8fc44c08726 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 18 Nov 2020 12:53:36 -0300 Subject: [PATCH 24/52] Print the payment method id (only) --- cli/src/main/java/bisq/cli/CliMain.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index 188a9372881..7330587200a 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -421,7 +421,7 @@ public static void run(String[] args) { case getpaymentmethods: { var request = GetPaymentMethodsRequest.newBuilder().build(); var reply = paymentAccountsService.getPaymentMethods(request); - reply.getPaymentMethodsList().forEach(out::println); + reply.getPaymentMethodsList().forEach(p -> out.println(p.getId())); } case createpaymentacct: { if (nonOptionArgs.size() < 5) From 0e0af2090078f60a068bde3574c4fae01ea5ed0b Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 18 Nov 2020 12:58:58 -0300 Subject: [PATCH 25/52] Avoid codacy issue over use of fully qualified name Changed the core getPaymentMethods() -> getFiatPaymentMethods() to avoid "Unnecessary use of fully qualified name 'PaymentMethod.getPaymentMethods' due to existing static import 'bisq.core.payment.payload.PaymentMethod.*'" --- core/src/main/java/bisq/core/api/CoreApi.java | 4 ++-- .../main/java/bisq/core/api/CorePaymentAccountsService.java | 2 +- .../java/bisq/daemon/grpc/GrpcPaymentAccountsService.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java index cc50da2acdd..766a113bf36 100644 --- a/core/src/main/java/bisq/core/api/CoreApi.java +++ b/core/src/main/java/bisq/core/api/CoreApi.java @@ -169,8 +169,8 @@ public Set getPaymentAccounts() { return paymentAccountsService.getPaymentAccounts(); } - public List getPaymentMethods() { - return paymentAccountsService.getPaymentMethods(); + public List getFiatPaymentMethods() { + return paymentAccountsService.getFiatPaymentMethods(); } /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java b/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java index 8d8dc3f42ad..4b06680d3a6 100644 --- a/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java +++ b/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java @@ -78,7 +78,7 @@ Set getPaymentAccounts() { return user.getPaymentAccounts(); } - List getPaymentMethods() { + List getFiatPaymentMethods() { return PaymentMethod.getPaymentMethods().stream() .filter(paymentMethod -> !paymentMethod.isAsset()) .sorted(Comparator.comparing(PaymentMethod::getId)) diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java index 64175f3d0ff..a2f02ff8ae0 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java @@ -72,7 +72,7 @@ public void getPaymentAccounts(GetPaymentAccountsRequest req, @Override public void getPaymentMethods(GetPaymentMethodsRequest req, StreamObserver responseObserver) { - var paymentMethods = coreApi.getPaymentMethods().stream() + var paymentMethods = coreApi.getFiatPaymentMethods().stream() .map(PaymentMethod::toProtoMessage) .collect(Collectors.toList()); var reply = GetPaymentMethodsReply.newBuilder() From 7d0648afd09dd6a77a17c11571cede86027ae9be Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 18 Nov 2020 13:10:39 -0300 Subject: [PATCH 26/52] Make codacy happy again --- .../src/main/java/bisq/core/api/CorePaymentAccountsService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java b/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java index 4b06680d3a6..bc54b64f14e 100644 --- a/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java +++ b/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java @@ -79,7 +79,7 @@ Set getPaymentAccounts() { } List getFiatPaymentMethods() { - return PaymentMethod.getPaymentMethods().stream() + return getPaymentMethods().stream() .filter(paymentMethod -> !paymentMethod.isAsset()) .sorted(Comparator.comparing(PaymentMethod::getId)) .collect(Collectors.toList()); From 32ed7ac4060cb050f782cf745054bb4fd54ced25 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 18 Nov 2020 16:11:10 -0300 Subject: [PATCH 27/52] Add ReflectionUtils to common.util pkg This class will aid the api's (create) PaymentAccount json form serialization/de-serialization. --- .../bisq/common/util/ReflectionUtils.java | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 common/src/main/java/bisq/common/util/ReflectionUtils.java diff --git a/common/src/main/java/bisq/common/util/ReflectionUtils.java b/common/src/main/java/bisq/common/util/ReflectionUtils.java new file mode 100644 index 00000000000..e70162ecb23 --- /dev/null +++ b/common/src/main/java/bisq/common/util/ReflectionUtils.java @@ -0,0 +1,108 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.common.util; + + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +import static java.util.Arrays.stream; +import static org.apache.commons.lang3.StringUtils.capitalize; + +public class ReflectionUtils { + + /** + * Recursively loads a list of fields for a given class and its superclasses, + * using a filter predicate to exclude any unwanted fields. + * + * @param fields The list of fields being loaded for a class hierarchy. + * @param clazz The lowest level class in a hierarchy; excluding Object.class. + * @param isExcludedField The field exclusion predicate. + */ + public static void loadFieldListForClassHierarchy(List fields, + Class clazz, + Predicate isExcludedField) { + fields.addAll(stream(clazz.getDeclaredFields()) + .filter(f -> !isExcludedField.test(f)) + .collect(Collectors.toList())); + + Class superclass = clazz.getSuperclass(); + if (!Objects.equals(superclass, Object.class)) + loadFieldListForClassHierarchy(fields, + superclass, + isExcludedField); + } + + /** + * Returns an Optional of a setter method for a given field and a class hierarchy, + * or Optional.empty() if it does not exist. + * + * @param field The field used to find a setter method. + * @param clazz The lowest level class in a hierarchy; excluding Object.class. + * @return Optional of the setter method for a field in the class hierarchy, + * or Optional.empty() if it does not exist. + */ + public static Optional getSetterMethodForFieldInClassHierarchy(Field field, + Class clazz) { + Optional setter = stream(clazz.getDeclaredMethods()) + .filter((m) -> isSetterForField(m, field)) + .findFirst(); + + if (setter.isPresent()) + return setter; + + Class superclass = clazz.getSuperclass(); + if (!Objects.equals(superclass, Object.class)) { + setter = getSetterMethodForFieldInClassHierarchy(field, superclass); + if (setter.isPresent()) + return setter; + } + + return Optional.empty(); + } + + public static boolean isSetterForField(Method m, Field f) { + return m.getName().startsWith("set") + && m.getName().endsWith(capitalize(f.getName())) + && m.getReturnType().getName().equals("void") + && m.getParameterCount() == 1 + && m.getParameterTypes()[0].getName().equals(f.getType().getName()); + } + + public static boolean isSetterOnClass(Method setter, Class clazz) { + return clazz.equals(setter.getDeclaringClass()); + } + + public static String getVisibilityModifierAsString(Field field) { + if (Modifier.isPrivate(field.getModifiers())) + return "private"; + else if (Modifier.isProtected(field.getModifiers())) + return "protected"; + else if (Modifier.isPublic(field.getModifiers())) + return "public"; + else + return ""; + } +} From c25debaf99f11d6037c075b48d39efb43f4e8843 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 18 Nov 2020 16:12:42 -0300 Subject: [PATCH 28/52] Add new (gson) PaymentAccountTypeAdapter to core.api.model This class does most of the work of the api's (create) PaymentAccount json form serialization/de-serialization. --- .../api/model/PaymentAccountTypeAdapter.java | 329 ++++++++++++++++++ 1 file changed, 329 insertions(+) create mode 100644 core/src/main/java/bisq/core/api/model/PaymentAccountTypeAdapter.java diff --git a/core/src/main/java/bisq/core/api/model/PaymentAccountTypeAdapter.java b/core/src/main/java/bisq/core/api/model/PaymentAccountTypeAdapter.java new file mode 100644 index 00000000000..151a4e0e782 --- /dev/null +++ b/core/src/main/java/bisq/core/api/model/PaymentAccountTypeAdapter.java @@ -0,0 +1,329 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.api.model; + + +import bisq.core.locale.Country; +import bisq.core.locale.FiatCurrency; +import bisq.core.payment.CountryBasedPaymentAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.PaymentAccountPayload; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Predicate; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.common.util.ReflectionUtils.getSetterMethodForFieldInClassHierarchy; +import static bisq.common.util.ReflectionUtils.getVisibilityModifierAsString; +import static bisq.common.util.ReflectionUtils.isSetterOnClass; +import static bisq.common.util.ReflectionUtils.loadFieldListForClassHierarchy; +import static bisq.core.locale.CountryUtil.findCountryByCode; +import static bisq.core.locale.CurrencyUtil.getCurrencyByCountryCode; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.lang.String.format; +import static java.util.Comparator.comparing; + +@Slf4j +class PaymentAccountTypeAdapter extends TypeAdapter { + + private final Class paymentAccountType; + private final Class paymentAccountPayloadType; + private final Map> fieldSettersMap; + private final Predicate isExcludedField; + + /** + * Constructor used when de-serializing a json payment account form into a + * PaymentAccount instance. + * + * @param paymentAccountType the PaymentAccount subclass being instantiated + */ + public PaymentAccountTypeAdapter(Class paymentAccountType) { + this(paymentAccountType, new String[]{}); + } + + /** + * Constructor used when serializing a PaymentAccount subclass instance into a json + * payment account json form. + * + * @param paymentAccountType the PaymentAccount subclass being serialized + * @param excludedFields a string array of field names to exclude from the serialized + * payment account json form. + */ + public PaymentAccountTypeAdapter(Class paymentAccountType, String[] excludedFields) { + this.paymentAccountType = paymentAccountType; + this.paymentAccountPayloadType = getPaymentAccountPayloadType(); + this.isExcludedField = (f) -> Arrays.stream(excludedFields).anyMatch(e -> e.equals(f.getName())); + this.fieldSettersMap = getFieldSetterMap(); + } + + @Override + public void write(JsonWriter out, PaymentAccount account) throws IOException { + // We write a blank payment acct form for a payment method id. + // We're not serializing a real payment account instance here. + if (log.isDebugEnabled()) + log.debug("Writing PaymentAccount json form for fields with accessors..."); + + out.beginObject(); + writeCommonFields(out, account); + fieldSettersMap.forEach((field, value) -> { + try { + // Write out a json element if there is a @Setter for this field. + if (value.isPresent()) { + if (log.isDebugEnabled()) + log.debug("Append form with settable field: {} {} {} setter: {}", + getVisibilityModifierAsString(field), + field.getType().getSimpleName(), + field.getName(), + value); + + String fieldName = field.getName(); + out.name(fieldName); + out.value("Your " + fieldName.toLowerCase()); + } + } catch (Exception ex) { + throw new IllegalStateException( + format("Could not serialize a %s to json", account.getClass().getSimpleName()), ex); + } + }); + out.endObject(); + if (log.isDebugEnabled()) + log.debug("Done writing PaymentAccount json form."); + } + + @Override + public PaymentAccount read(JsonReader in) throws IOException { + if (log.isDebugEnabled()) + log.debug("De-serializing json to new {} ...", paymentAccountType.getSimpleName()); + + PaymentAccount account = initNewPaymentAccount(); + in.beginObject(); + while (in.hasNext()) { + String currentFieldName = in.nextName(); + + // Some of the fields are common to all payment account types. + if (didReadCommonField(in, account, currentFieldName)) + continue; + + // If the account is a subclass of CountryBasedPaymentAccount, set the + // account's Country, and use the Country to derive and set the account's + // FiatCurrency. + if (didReadCountryField(in, account, currentFieldName)) + continue; + + try { + Optional field = fieldSettersMap.keySet().stream() + .filter(k -> k.getName().equals(currentFieldName)).findFirst(); + + field.ifPresentOrElse((f) -> invokeSetterMethod(account, f, in), () -> { + throw new IllegalStateException( + format("Could not de-serialize json to a '%s' because there is no %s field.", + account.getClass().getSimpleName(), + currentFieldName)); + }); + } catch (Exception ex) { + throw new IllegalStateException( + format("Could not de-serialize json to a '%s'.", + account.getClass().getSimpleName()), ex); + } + } + in.endObject(); + if (log.isDebugEnabled()) + log.debug("Done de-serializing json."); + + return account; + } + + private void invokeSetterMethod(PaymentAccount account, Field field, JsonReader jsonReader) { + Optional setter = fieldSettersMap.get(field); + if (setter.isPresent()) { + try { + // The setter might be on the PaymentAccount instance, or its + // PaymentAccountPayload instance. + if (isSetterOnPaymentAccountClass(setter.get(), account)) { + setter.get().invoke(account, nextStringOrNull(jsonReader)); + } else if (isSetterOnPaymentAccountPayloadClass(setter.get(), account)) { + setter.get().invoke(account.getPaymentAccountPayload(), nextStringOrNull(jsonReader)); + } else { + String exMsg = format("Could not de-serialize json to a '%s' using reflection" + + " because the setter's declaring class was not found.", + account.getClass().getSimpleName()); + throw new IllegalStateException(exMsg); + } + } catch (IllegalAccessException | InvocationTargetException ex) { + throw new IllegalStateException( + format("Could not de-serialize json to a '%s' due to reflection error.", + account.getClass().getSimpleName()), ex); + } + } else { + throw new IllegalStateException( + format("Could not de-serialize json to a '%s' because there is no setter for field %s.", + account.getClass().getSimpleName(), + field.getName())); + } + } + + private boolean isSetterOnPaymentAccountClass(Method setter, PaymentAccount account) { + return isSetterOnClass(setter, account.getClass()); + } + + private boolean isSetterOnPaymentAccountPayloadClass(Method setter, PaymentAccount account) { + return isSetterOnClass(setter, account.getPaymentAccountPayload().getClass()) + || isSetterOnClass(setter, account.getPaymentAccountPayload().getClass().getSuperclass()); + } + + private Map> getFieldSetterMap() { + List orderedFields = getOrderedFields(); + Map> map = new LinkedHashMap<>(); + for (Field field : orderedFields) { + Optional setter = getSetterMethodForFieldInClassHierarchy(field, paymentAccountType) + .or(() -> getSetterMethodForFieldInClassHierarchy(field, paymentAccountPayloadType)); + map.put(field, setter); + } + return Collections.unmodifiableMap(map); + } + + private List getOrderedFields() { + List fields = new ArrayList<>(); + loadFieldListForClassHierarchy(fields, paymentAccountType, isExcludedField); + loadFieldListForClassHierarchy(fields, paymentAccountPayloadType, isExcludedField); + fields.sort(comparing(Field::getName)); + return fields; + } + + private String nextStringOrNull(JsonReader in) { + try { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } else { + return in.nextString(); + } + } catch (IOException ex) { + throw new IllegalStateException("Could not peek at next String value in JsonReader.", ex); + } + } + + @SuppressWarnings("unused") + private Long nextLongOrNull(JsonReader in) { + try { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } else { + return in.nextLong(); + } + } catch (IOException ex) { + throw new IllegalStateException("Could not peek at next Long value in JsonReader.", ex); + } + } + + private void writeCommonFields(JsonWriter out, PaymentAccount account) throws IOException { + if (log.isDebugEnabled()) + log.debug("writeCommonFields(out, {}) -> paymentMethodId = {}", + account, account.getPaymentMethod().getId()); + + out.name("_COMMENT_"); + out.value("Please do not edit the paymentMethodId field."); + + out.name("paymentMethodId"); + out.value(account.getPaymentMethod().getId()); + } + + private boolean didReadCommonField(JsonReader in, PaymentAccount account, String fieldName) { + switch (fieldName) { + case "_COMMENT_": + case "paymentMethodId": + // skip + nextStringOrNull(in); + return true; + case "accountName": + account.setAccountName(nextStringOrNull(in)); + return true; + default: + return false; + } + } + + private boolean didReadCountryField(JsonReader in, PaymentAccount account, String fieldName) { + if (account.isCountryBasedPaymentAccount() && fieldName.equals("country")) { + // Read the country code, and use it to set the account's country and single + // trade currency fields. + String countryCode = nextStringOrNull(in); + Optional country = findCountryByCode(countryCode); + if (country.isPresent()) { + ((CountryBasedPaymentAccount) account).setCountry(country.get()); + FiatCurrency fiatCurrency = getCurrencyByCountryCode(checkNotNull(countryCode)); + account.setSingleTradeCurrency(fiatCurrency); + return true; + } else { + throw new IllegalStateException( + format("Could not de-serialize json to a '%s' because %s is an invalid country code.", + account.getClass().getSimpleName(), countryCode)); + } + } else { + return false; + } + } + + private Class getPaymentAccountPayloadType() { + try { + Package pkg = PaymentAccountPayload.class.getPackage(); + //noinspection unchecked + return (Class) Class.forName(pkg.getName() + + "." + paymentAccountType.getSimpleName() + "Payload"); + } catch (Exception ex) { + throw new IllegalStateException( + format("Could not get payload class for %s", + paymentAccountType.getSimpleName()), ex); + } + } + + private PaymentAccount initNewPaymentAccount() { + try { + Constructor constructor = paymentAccountType.getDeclaredConstructor(); + PaymentAccount paymentAccount = (PaymentAccount) constructor.newInstance(); + paymentAccount.init(); + return paymentAccount; + } catch (NoSuchMethodException ex) { + throw new IllegalStateException(format("No default declared constructor found for class %s", + paymentAccountType.getSimpleName()), ex); + } catch (IllegalAccessException | InstantiationException | InvocationTargetException ex) { + throw new IllegalStateException(format("Could not instantiate class %s", + paymentAccountType.getSimpleName()), ex); + } + } +} From 1f84ad0fd119a84391d347d2586f2ac01cc3857f Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 18 Nov 2020 16:15:13 -0300 Subject: [PATCH 29/52] Add isCountryBasedPaymentAccount to abstract PaymentAccount --- core/src/main/java/bisq/core/payment/PaymentAccount.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/src/main/java/bisq/core/payment/PaymentAccount.java b/core/src/main/java/bisq/core/payment/PaymentAccount.java index b38649ef942..f94811585f1 100644 --- a/core/src/main/java/bisq/core/payment/PaymentAccount.java +++ b/core/src/main/java/bisq/core/payment/PaymentAccount.java @@ -173,6 +173,10 @@ public String getOwnerId() { return paymentAccountPayload.getOwnerId(); } + public boolean isCountryBasedPaymentAccount() { + return this instanceof CountryBasedPaymentAccount; + } + public boolean isHalCashAccount() { return this instanceof HalCashAccount; } From 32dd727a8917d989c282783ca82d0639a7aee25b Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 18 Nov 2020 16:16:04 -0300 Subject: [PATCH 30/52] Add new PaymentAccountForm to core.api.model This class uses the gson PaymentAccountTypeAdapter to generate editable json forms for PaymentAccount sub-types, and instantiate PaymentAccount instances from edited json forms. --- .../core/api/model/PaymentAccountForm.java | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 core/src/main/java/bisq/core/api/model/PaymentAccountForm.java diff --git a/core/src/main/java/bisq/core/api/model/PaymentAccountForm.java b/core/src/main/java/bisq/core/api/model/PaymentAccountForm.java new file mode 100644 index 00000000000..780c501bbc7 --- /dev/null +++ b/core/src/main/java/bisq/core/api/model/PaymentAccountForm.java @@ -0,0 +1,216 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.api.model; + +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.PaymentAccountFactory; +import bisq.core.payment.payload.PaymentMethod; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import javax.inject.Singleton; + +import com.google.common.annotations.VisibleForTesting; + +import java.net.URI; +import java.net.URISyntaxException; + +import java.nio.file.Files; +import java.nio.file.Paths; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; + +import java.util.Map; + +import java.lang.reflect.Type; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.core.payment.payload.PaymentMethod.getPaymentMethodById; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.lang.String.format; +import static java.lang.System.getProperty; +import static java.nio.charset.StandardCharsets.UTF_8; + + +/** + *

+ * An instance of this class can write new payment account forms (editable json files), + * and de-serialize edited json files into {@link bisq.core.payment.PaymentAccount} + * instances. + *

+ *

+ * Example use case: (1) ask for a blank Hal Cash account form, (2) edit it, (3) derive a + * {@link bisq.core.payment.HalCashAccount} instance from the edited json file. + *

+ *
+ *

+ * (1) Ask for a hal cash account form: Pass a {@link bisq.core.payment.payload.PaymentMethod#HAL_CASH_ID} + * to {@link bisq.core.api.model.PaymentAccountForm#getPaymentAccountForm(String)} to + * get the json Hal Cash payment account form: + *

+ * {
+ *   "_COMMENT_": "Please do not edit the paymentMethodId field.",
+ *   "paymentMethodId": "HAL_CASH",
+ *   "accountName": "Your accountname",
+ *   "mobileNr": "Your mobilenr"
+ * }
+ * 
+ *

+ *

+ * (2) Save the Hal Cash payment account form to disk, and edit it: + *

+ * {
+ *   "_COMMENT_": "Please do not edit the paymentMethodId field.",
+ *   "paymentMethodId": "HAL_CASH",
+ *   "accountName": "Hal Cash Acct",
+ *   "mobileNr": "798 123 456"
+ * }
+ * 
+ *

+ * (3) De-serialize the edited json account form: Pass the edited json file to + * {@link bisq.core.api.model.PaymentAccountForm#toPaymentAccount(File)}, + * and get a {@link bisq.core.payment.HalCashAccount} instance. + *
+ * PaymentAccount(
+ * paymentMethod=PaymentMethod(id=HAL_CASH,
+ *                             maxTradePeriod=86400000,
+ *                             maxTradeLimit=50000000),
+ * id=e33c9d94-1a1a-43fd-aa11-fcaacbb46100,
+ * creationDate=Mon Nov 16 12:26:43 BRST 2020,
+ * paymentAccountPayload=HalCashAccountPayload(mobileNr=798 123 456),
+ * accountName=Hal Cash Acct,
+ * tradeCurrencies=[FiatCurrency(currency=EUR)],
+ * selectedTradeCurrency=FiatCurrency(currency=EUR)
+ * )
+ * 
+ */ +@Singleton +@Slf4j +public class PaymentAccountForm { + + private final GsonBuilder gsonBuilder = new GsonBuilder() + .setPrettyPrinting() + .serializeNulls(); + + // Names of PaymentAccount fields to exclude from json forms. + private final String[] excludedFields = new String[]{ + "log", + "id", + "acceptedCountryCodes", + "countryCode", + "creationDate", + "excludeFromJsonDataMap", + "maxTradePeriod", + "paymentAccountPayload", + "paymentMethod", + "paymentMethodId", + "selectedTradeCurrency", + "tradeCurrencies", + "HOLDER_NAME", + "SALT" + }; + + /** + * Returns a blank payment account form (json) for the given paymentMethodId. + * + * @param paymentMethodId Determines what kind of json form to return. + * @return A uniquely named tmp file used to define new payment account details. + */ + public File getPaymentAccountForm(String paymentMethodId) { + PaymentMethod paymentMethod = getPaymentMethodById(paymentMethodId); + File file = getTmpJsonFile(paymentMethodId); + try (OutputStreamWriter outputStreamWriter = new OutputStreamWriter(new FileOutputStream(checkNotNull(file), false), UTF_8)) { + PaymentAccount paymentAccount = PaymentAccountFactory.getPaymentAccount(paymentMethod); + Class clazz = paymentAccount.getClass(); + Gson gson = gsonBuilder.registerTypeAdapter(clazz, new PaymentAccountTypeAdapter(clazz, excludedFields)).create(); + String json = gson.toJson(paymentAccount); // serializes target to json + outputStreamWriter.write(json); + } catch (Exception ex) { + log.error(format("Could not export json file for a %s account.", paymentMethod.getShortName()), ex); + } + return file; + } + + /** + * De-serialize a PaymentAccount json form into a populated PaymentAccount instance. + * + * @param jsonForm The file representing a new payment account form. + * @return A populated PaymentAccount subclass instance. + */ + public PaymentAccount toPaymentAccount(File jsonForm) { + String json = toJsonString(jsonForm); + Class clazz = getPaymentAccountClassFromJson(json); + Gson gson = gsonBuilder.registerTypeAdapter(clazz, new PaymentAccountTypeAdapter(clazz)).create(); + return gson.fromJson(json, clazz); + } + + public String toJsonString(File jsonFile) { + try { + checkNotNull(jsonFile, "json file cannot be null"); + return new String(Files.readAllBytes(Paths.get(jsonFile.getAbsolutePath()))); + } catch (IOException ex) { + throw new IllegalStateException(format("Could not read content from file '%s'", + jsonFile.getAbsolutePath()), ex); + } + } + + public URI getClickableURI(File jsonForm) { + try { + return new URI("file", + "", + jsonForm.toURI().getPath(), + null, + null); + } catch (URISyntaxException ex) { + throw new IllegalArgumentException("", ex); + } + } + + @VisibleForTesting + public static File getTmpJsonFile(String paymentMethodId) { + File file = null; + try { + // Creates a tmp file that includes a random number string between the + // prefix and suffix, i.e., sepa_form_13243546575879.json, so there is + // little chance this will fail because the tmp file already exists. + file = File.createTempFile(paymentMethodId.toLowerCase() + "_form_", + ".json", + Paths.get(getProperty("java.io.tmpdir")).toFile()); + } catch (IOException ex) { + log.error("", ex); + } + return file; + } + + private Class getPaymentAccountClassFromJson(String json) { + Map jsonMap = gsonBuilder.create().fromJson(json, (Type) Object.class); + String paymentMethodId = checkNotNull((String) jsonMap.get("paymentMethodId"), + format("Could not find a paymentMethodId in the json string: %s", json)); + return getPaymentAccountClass(paymentMethodId); + } + + private Class getPaymentAccountClass(String paymentMethodId) { + PaymentMethod paymentMethod = getPaymentMethodById(paymentMethodId); + return PaymentAccountFactory.getPaymentAccount(paymentMethod).getClass(); + } +} From 8996fa17483cdfae3537abcda117ceee975e6b6b Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 18 Nov 2020 16:17:24 -0300 Subject: [PATCH 31/52] Add boilerplate for new 'getpaymentacctform' api method --- core/src/main/java/bisq/core/api/CoreApi.java | 4 ++++ .../bisq/core/api/CorePaymentAccountsService.java | 15 +++++++++++++++ .../daemon/grpc/GrpcPaymentAccountsService.java | 13 +++++++++++++ proto/src/main/proto/grpc.proto | 10 ++++++++++ 4 files changed, 42 insertions(+) diff --git a/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java index 766a113bf36..6add2a7d424 100644 --- a/core/src/main/java/bisq/core/api/CoreApi.java +++ b/core/src/main/java/bisq/core/api/CoreApi.java @@ -173,6 +173,10 @@ public List getFiatPaymentMethods() { return paymentAccountsService.getFiatPaymentMethods(); } + public String getPaymentAccountForm(String paymentMethodId) { + return paymentAccountsService.getPaymentAccountFormAsString(paymentMethodId); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Prices /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java b/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java index bc54b64f14e..3b1149e9bcf 100644 --- a/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java +++ b/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java @@ -18,6 +18,7 @@ package bisq.core.api; import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.api.model.PaymentAccountForm; import bisq.core.locale.FiatCurrency; import bisq.core.payment.PaymentAccount; import bisq.core.payment.PaymentAccountFactory; @@ -29,6 +30,8 @@ import javax.inject.Inject; +import java.io.File; + import java.util.Comparator; import java.util.List; import java.util.Set; @@ -44,14 +47,17 @@ class CorePaymentAccountsService { private final Config config; private final AccountAgeWitnessService accountAgeWitnessService; + private final PaymentAccountForm paymentAccountForm; private final User user; @Inject public CorePaymentAccountsService(Config config, AccountAgeWitnessService accountAgeWitnessService, + PaymentAccountForm paymentAccountForm, User user) { this.config = config; this.accountAgeWitnessService = accountAgeWitnessService; + this.paymentAccountForm = paymentAccountForm; this.user = user; } @@ -85,6 +91,15 @@ List getFiatPaymentMethods() { .collect(Collectors.toList()); } + String getPaymentAccountFormAsString(String paymentMethodId) { + File jsonForm = getPaymentAccountForm(paymentMethodId); + return paymentAccountForm.toJsonString(jsonForm); + } + + File getPaymentAccountForm(String paymentMethodId) { + return paymentAccountForm.getPaymentAccountForm(paymentMethodId); + } + private PaymentAccount getNewPaymentAccount(String paymentMethodId, String accountName, String accountNumber, diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java index a2f02ff8ae0..35bb3b322bb 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java @@ -23,6 +23,8 @@ import bisq.proto.grpc.CreatePaymentAccountReply; import bisq.proto.grpc.CreatePaymentAccountRequest; +import bisq.proto.grpc.GetPaymentAccountFormReply; +import bisq.proto.grpc.GetPaymentAccountFormRequest; import bisq.proto.grpc.GetPaymentAccountsReply; import bisq.proto.grpc.GetPaymentAccountsRequest; import bisq.proto.grpc.GetPaymentMethodsReply; @@ -80,4 +82,15 @@ public void getPaymentMethods(GetPaymentMethodsRequest req, responseObserver.onNext(reply); responseObserver.onCompleted(); } + + @Override + public void getPaymentAccountForm(GetPaymentAccountFormRequest req, + StreamObserver responseObserver) { + var paymentAccountFormJson = coreApi.getPaymentAccountForm(req.getPaymentMethodId()); + var reply = GetPaymentAccountFormReply.newBuilder() + .setPaymentAccountFormJson(paymentAccountFormJson) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } } diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index b29ad979eba..0f5430b429b 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -127,6 +127,8 @@ service PaymentAccounts { } rpc GetPaymentMethods (GetPaymentMethodsRequest) returns (GetPaymentMethodsReply) { } + rpc GetPaymentAccountForm (GetPaymentAccountFormRequest) returns (GetPaymentAccountFormReply) { + } } message CreatePaymentAccountRequest { @@ -154,6 +156,14 @@ message GetPaymentMethodsReply { repeated PaymentMethod paymentMethods = 1; } +message GetPaymentAccountFormRequest { + string paymentMethodId = 1; +} + +message GetPaymentAccountFormReply { + string paymentAccountFormJson = 1; +} + /////////////////////////////////////////////////////////////////////////////////////////// // Price /////////////////////////////////////////////////////////////////////////////////////////// From dc227ec8cc25405188581043a33eb4b3a8664b11 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 18 Nov 2020 16:18:15 -0300 Subject: [PATCH 32/52] Add new api method 'getpaymentacctform' to CLI --- cli/src/main/java/bisq/cli/CliMain.java | 44 +++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index 7330587200a..f031015eda3 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -27,6 +27,7 @@ import bisq.proto.grpc.GetFundingAddressesRequest; 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; @@ -47,11 +48,18 @@ import joptsimple.OptionParser; import joptsimple.OptionSet; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import java.io.FileNotFoundException; import java.io.IOException; import java.io.PrintStream; +import java.io.PrintWriter; import java.math.BigDecimal; +import java.util.Date; import java.util.List; import lombok.extern.slf4j.Slf4j; @@ -88,6 +96,7 @@ private enum Method { keepfunds, withdrawfunds, getpaymentmethods, + getpaymentacctform, createpaymentacct, getpaymentaccts, getversion, @@ -422,6 +431,23 @@ public static void run(String[] args) { var request = GetPaymentMethodsRequest.newBuilder().build(); var reply = paymentAccountsService.getPaymentMethods(request); reply.getPaymentMethodsList().forEach(p -> out.println(p.getId())); + return; + } + case getpaymentacctform: { + if (nonOptionArgs.size() < 2) + throw new IllegalArgumentException("incorrect parameter count, expecting payment method id"); + + var paymentMethodId = nonOptionArgs.get(1); + var request = GetPaymentAccountFormRequest.newBuilder() + .setPaymentMethodId(paymentMethodId) + .build(); + String jsonString = paymentAccountsService.getPaymentAccountForm(request) + .getPaymentAccountFormJson(); + out.printf("new payment account form: %s%n", jsonString); + saveFileToDisk(paymentMethodId.toLowerCase(), + ".json", + jsonString); + return; } case createpaymentacct: { if (nonOptionArgs.size() < 5) @@ -529,6 +555,23 @@ private static Method getMethodFromCmd(String methodName) { return Method.valueOf(methodName.toLowerCase()); } + private static void saveFileToDisk(String prefix, String suffix, String text) { + String timestamp = Long.toUnsignedString(new Date().getTime()); + String relativeFileName = prefix + "_" + timestamp + suffix; + try { + Path path = Paths.get(relativeFileName); + if (!Files.exists(path)) { + try (PrintWriter out = new PrintWriter(path.toString())) { + out.println(text); + } + } else { + throw new IllegalStateException(format("could not overwrite existing file '%s'", relativeFileName)); + } + } catch (FileNotFoundException e) { + throw new IllegalStateException(format("could not create file '%s'", relativeFileName)); + } + } + private static void printHelp(OptionParser parser, PrintStream stream) { try { stream.println("Bisq RPC Client"); @@ -560,6 +603,7 @@ private static void printHelp(OptionParser parser, PrintStream stream) { stream.format(rowFormat, "keepfunds", "trade id", "Keep received funds in Bisq wallet"); stream.format(rowFormat, "withdrawfunds", "trade id, bitcoin wallet address", "Withdraw received funds to external wallet address"); stream.format(rowFormat, "getpaymentmethods", "", "Get list of supported payment account method ids"); + stream.format(rowFormat, "getpaymentacctform", "payment method id", "Get a new payment account form"); stream.format(rowFormat, "createpaymentacct", "account name, account number, currency code", "Create PerfectMoney dummy account"); stream.format(rowFormat, "getpaymentaccts", "", "Get user payment accounts"); stream.format(rowFormat, "lockwallet", "", "Remove wallet password from memory, locking the wallet"); From fdb89a26d0c9cae35c27d9c6f2c565f08aa3db23 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 18 Nov 2020 16:18:57 -0300 Subject: [PATCH 33/52] Test new api method 'getpaymentacctform' --- .../java/bisq/apitest/method/MethodTest.java | 28 +++ .../payment/AbstractPaymentAccountTest.java | 126 ++++++++++ .../payment/CreatePaymentAccountTest.java | 230 +++++++++++++++++- .../apitest/scenario/PaymentAccountTest.java | 23 +- 4 files changed, 393 insertions(+), 14 deletions(-) create mode 100644 apitest/src/test/java/bisq/apitest/method/payment/AbstractPaymentAccountTest.java diff --git a/apitest/src/test/java/bisq/apitest/method/MethodTest.java b/apitest/src/test/java/bisq/apitest/method/MethodTest.java index 34fbeffbc56..4667dc770f6 100644 --- a/apitest/src/test/java/bisq/apitest/method/MethodTest.java +++ b/apitest/src/test/java/bisq/apitest/method/MethodTest.java @@ -17,6 +17,8 @@ package bisq.apitest.method; +import bisq.core.api.model.PaymentAccountForm; + import bisq.proto.grpc.AddressBalanceInfo; import bisq.proto.grpc.BalancesInfo; import bisq.proto.grpc.BsqBalanceInfo; @@ -29,6 +31,7 @@ import bisq.proto.grpc.GetBalancesRequest; import bisq.proto.grpc.GetFundingAddressesRequest; 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; @@ -49,6 +52,12 @@ import protobuf.PaymentAccount; import protobuf.PaymentMethod; +import java.nio.charset.StandardCharsets; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; + import java.util.List; import java.util.stream.Collectors; @@ -252,6 +261,25 @@ protected final List getPaymentMethods(BisqAppConfig bisqAppConfi return grpcStubs(bisqAppConfig).paymentAccountsService.getPaymentMethods(req).getPaymentMethodsList(); } + protected final File getPaymentAccountForm(BisqAppConfig bisqAppConfig, 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(); + // Write the json string to a file here in the test case. + File jsonFile = PaymentAccountForm.getTmpJsonFile(paymentMethodId); + try (PrintWriter out = new PrintWriter(jsonFile, StandardCharsets.UTF_8)) { + out.println(jsonString); + } catch (IOException ex) { + fail("Could not create tmp payment account form.", ex); + } + return jsonFile; + } + protected final CreatePaymentAccountRequest createCreatePerfectMoneyPaymentAccountRequest( String accountName, String accountNumber, diff --git a/apitest/src/test/java/bisq/apitest/method/payment/AbstractPaymentAccountTest.java b/apitest/src/test/java/bisq/apitest/method/payment/AbstractPaymentAccountTest.java new file mode 100644 index 00000000000..9f11c5b8a5c --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/payment/AbstractPaymentAccountTest.java @@ -0,0 +1,126 @@ +package bisq.apitest.method.payment; + +import bisq.core.api.model.PaymentAccountForm; +import bisq.core.locale.Res; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.stream.JsonWriter; + +import java.nio.file.Paths; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; + +import java.util.HashMap; +import java.util.Map; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.BeforeEach; + +import static java.lang.String.format; +import static java.lang.System.getProperty; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + + + +import bisq.apitest.method.MethodTest; + +@Slf4j +public class AbstractPaymentAccountTest extends MethodTest { + + static final String PROPERTY_NAME_COMMENT = "_COMMENT_"; + static final String PROPERTY_VALUE_COMMENT = "Please do not edit the paymentMethodId field."; + + static final String PROPERTY_NAME_PAYMENT_METHOD_ID = "paymentMethodId"; + + static final String PROPERTY_NAME_ACCOUNT_NAME = "accountName"; + static final String PROPERTY_NAME_ACCOUNT_NR = "accountNr"; + static final String PROPERTY_NAME_ACCOUNT_TYPE = "accountType"; + static final String PROPERTY_NAME_BANK_ACCOUNT_NAME = "bankAccountName"; + static final String PROPERTY_NAME_BANK_ACCOUNT_NUMBER = "bankAccountNumber"; + static final String PROPERTY_NAME_BANK_ACCOUNT_TYPE = "bankAccountType"; + static final String PROPERTY_NAME_BANK_BRANCH_CODE = "bankBranchCode"; + static final String PROPERTY_NAME_BANK_BRANCH_NAME = "bankBranchName"; + static final String PROPERTY_NAME_BANK_CODE = "bankCode"; + @SuppressWarnings("unused") + static final String PROPERTY_NAME_BANK_ID = "bankId"; + static final String PROPERTY_NAME_BANK_NAME = "bankName"; + static final String PROPERTY_NAME_BRANCH_ID = "branchId"; + static final String PROPERTY_NAME_BIC = "bic"; + static final String PROPERTY_NAME_COUNTRY = "country"; + static final String PROPERTY_NAME_CITY = "city"; + static final String PROPERTY_NAME_CONTACT = "contact"; + static final String PROPERTY_NAME_EMAIL = "email"; + static final String PROPERTY_NAME_EMAIL_OR_MOBILE_NR = "emailOrMobileNr"; + static final String PROPERTY_NAME_EXTRA_INFO = "extraInfo"; + static final String PROPERTY_NAME_HOLDER_NAME = "holderName"; + static final String PROPERTY_NAME_HOLDER_TAX_ID = "holderTaxId"; + static final String PROPERTY_NAME_IBAN = "iban"; + static final String PROPERTY_NAME_MOBILE_NR = "mobileNr"; + static final String PROPERTY_NAME_NATIONAL_ACCOUNT_ID = "nationalAccountId"; + static final String PROPERTY_NAME_PAY_ID = "payid"; + static final String PROPERTY_NAME_POSTAL_ADDRESS = "postalAddress"; + + static final Gson GSON = new GsonBuilder() + .setPrettyPrinting() + .serializeNulls() + .create(); + + static final Map EXPECTED_FORM = new HashMap<>(); + + // A payment account serializer / deserializer. + static final PaymentAccountForm PAYMENT_ACCOUNT_FORM = new PaymentAccountForm(); + + @BeforeEach + public void setup() { + Res.setup(); + EXPECTED_FORM.clear(); + } + + protected final void verifyEmptyForm(File jsonForm, String paymentMethodId, String... fields) { + @SuppressWarnings("unchecked") + Map emptyForm = (Map) GSON.fromJson( + PAYMENT_ACCOUNT_FORM.toJsonString(jsonForm), + Object.class); + assertNotNull(emptyForm); + assertEquals(PROPERTY_VALUE_COMMENT, emptyForm.get(PROPERTY_NAME_COMMENT)); + assertEquals(paymentMethodId, emptyForm.get(PROPERTY_NAME_PAYMENT_METHOD_ID)); + assertEquals("Your accountname", emptyForm.get(PROPERTY_NAME_ACCOUNT_NAME)); + for (String field : fields) { + assertEquals("Your " + field.toLowerCase(), emptyForm.get(field)); + } + } + + protected final File fillPaymentAccountForm() { + File tmpJsonForm = null; + try { + tmpJsonForm = File.createTempFile("temp_acct_form_", + ".json", + Paths.get(getProperty("java.io.tmpdir")).toFile()); + tmpJsonForm.deleteOnExit(); + JsonWriter writer = new JsonWriter(new OutputStreamWriter(new FileOutputStream(tmpJsonForm), UTF_8)); + writer.beginObject(); + writer.name(PROPERTY_NAME_COMMENT); + writer.value(PROPERTY_VALUE_COMMENT); + for (Map.Entry entry : EXPECTED_FORM.entrySet()) { + String k = entry.getKey(); + Object v = entry.getValue(); + writer.name(k); + writer.value(v.toString()); + } + writer.endObject(); + writer.close(); + } catch (IOException ex) { + log.error("", ex); + fail(format("Could not write json file from form entries %s", EXPECTED_FORM)); + } + return tmpJsonForm; + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java b/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java index 83e827834e7..725448a2266 100644 --- a/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java +++ b/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java @@ -22,6 +22,8 @@ import protobuf.PaymentAccount; import protobuf.PerfectMoneyAccountPayload; +import java.io.File; + import java.util.List; import java.util.stream.Collectors; @@ -32,26 +34,24 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestMethodOrder; import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind; import static bisq.apitest.config.BisqAppConfig.alicedaemon; +import static bisq.core.payment.payload.PaymentMethod.*; import static java.util.Comparator.comparing; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; - - -import bisq.apitest.method.MethodTest; - @Disabled @Slf4j @TestMethodOrder(OrderAnnotation.class) -public class CreatePaymentAccountTest extends MethodTest { +public class CreatePaymentAccountTest extends AbstractPaymentAccountTest { - static final String PERFECT_MONEY_ACCT_NAME = "Perfect Money USD"; - static final String PERFECT_MONEY_ACCT_NUMBER = "0123456789"; + // TODO Test PaymentAccountForm's PaymentAccount toPaymentAccount(File jsonForm) + // after replacement api method 'createpaymentacct' is implemented. @BeforeAll public static void setUp() { @@ -64,7 +64,221 @@ public static void setUp() { @Test @Order(1) - public void testCreatePerfectMoneyUSDPaymentAccount() { + public void testCreateAustraliaPayidAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, AUSTRALIA_PAYID_ID); + verifyEmptyForm(emptyForm, + AUSTRALIA_PAYID_ID, + PROPERTY_NAME_BANK_ACCOUNT_NAME); + + EXPECTED_FORM.put(PROPERTY_NAME_PAYMENT_METHOD_ID, AUSTRALIA_PAYID_ID); + EXPECTED_FORM.put(PROPERTY_NAME_ACCOUNT_NAME, "Australia Pay ID Account"); + EXPECTED_FORM.put(PROPERTY_NAME_PAY_ID, "123 456 789"); + EXPECTED_FORM.put(PROPERTY_NAME_BANK_ACCOUNT_NAME, "Credit Union Australia"); + + File completedForm = fillPaymentAccountForm(); + log.info("Completed form: {}", PAYMENT_ACCOUNT_FORM.toJsonString(completedForm)); + } + + @Test + public void testBrazilNationalBankAccountForm(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, NATIONAL_BANK_ID); + verifyEmptyForm(emptyForm, + NATIONAL_BANK_ID, + PROPERTY_NAME_ACCOUNT_NR, + PROPERTY_NAME_ACCOUNT_TYPE, + PROPERTY_NAME_BANK_NAME, + PROPERTY_NAME_BRANCH_ID, + PROPERTY_NAME_COUNTRY, + PROPERTY_NAME_HOLDER_NAME, + PROPERTY_NAME_HOLDER_TAX_ID, + PROPERTY_NAME_NATIONAL_ACCOUNT_ID); + + EXPECTED_FORM.put(PROPERTY_NAME_PAYMENT_METHOD_ID, NATIONAL_BANK_ID); + EXPECTED_FORM.put(PROPERTY_NAME_ACCOUNT_NAME, "Banco do Brasil"); + EXPECTED_FORM.put(PROPERTY_NAME_ACCOUNT_NR, "456789-87"); + // No BankId is required for BR. + EXPECTED_FORM.put(PROPERTY_NAME_BANK_NAME, "Banco do Brasil"); + EXPECTED_FORM.put(PROPERTY_NAME_BRANCH_ID, "456789-10"); + EXPECTED_FORM.put(PROPERTY_NAME_COUNTRY, "BR"); + EXPECTED_FORM.put(PROPERTY_NAME_HOLDER_NAME, "Joao da Silva"); + EXPECTED_FORM.put(PROPERTY_NAME_HOLDER_TAX_ID, "123456789"); + EXPECTED_FORM.put(PROPERTY_NAME_NATIONAL_ACCOUNT_ID, "123456789"); + + File completedForm = fillPaymentAccountForm(); + log.info("Completed form: {}", PAYMENT_ACCOUNT_FORM.toJsonString(completedForm)); + + } + + @Test + public void testChaseQuickPayAccountForm(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, CHASE_QUICK_PAY_ID); + verifyEmptyForm(emptyForm, + CHASE_QUICK_PAY_ID, + PROPERTY_NAME_EMAIL, + PROPERTY_NAME_HOLDER_NAME); + + EXPECTED_FORM.put(PROPERTY_NAME_PAYMENT_METHOD_ID, CHASE_QUICK_PAY_ID); + EXPECTED_FORM.put(PROPERTY_NAME_ACCOUNT_NAME, "Quick Pay Acct"); + EXPECTED_FORM.put(PROPERTY_NAME_EMAIL, "johndoe@quickpay.com"); + EXPECTED_FORM.put(PROPERTY_NAME_HOLDER_NAME, "John Doe"); + + File completedForm = fillPaymentAccountForm(); + log.info("Completed form: {}", PAYMENT_ACCOUNT_FORM.toJsonString(completedForm)); + + } + + @Test + public void testClearXChangeAccountForm(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, CLEAR_X_CHANGE_ID); + verifyEmptyForm(emptyForm, + CLEAR_X_CHANGE_ID, + PROPERTY_NAME_EMAIL_OR_MOBILE_NR, + PROPERTY_NAME_HOLDER_NAME); + + EXPECTED_FORM.put(PROPERTY_NAME_PAYMENT_METHOD_ID, CLEAR_X_CHANGE_ID); + EXPECTED_FORM.put(PROPERTY_NAME_ACCOUNT_NAME, "USD Zelle Account"); + EXPECTED_FORM.put(PROPERTY_NAME_EMAIL_OR_MOBILE_NR, "jane@doe.com"); + EXPECTED_FORM.put(PROPERTY_NAME_HOLDER_NAME, "Jane Doe"); + + File completedForm = fillPaymentAccountForm(); + log.info("Completed form: {}", PAYMENT_ACCOUNT_FORM.toJsonString(completedForm)); + } + + @Test + public void testF2FAccountForm(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, F2F_ID); + verifyEmptyForm(emptyForm, + F2F_ID, + PROPERTY_NAME_COUNTRY, + PROPERTY_NAME_CITY, + PROPERTY_NAME_CONTACT, + PROPERTY_NAME_EXTRA_INFO); + + + EXPECTED_FORM.put(PROPERTY_NAME_PAYMENT_METHOD_ID, F2F_ID); + EXPECTED_FORM.put(PROPERTY_NAME_ACCOUNT_NAME, "Conta Cara a Cara"); + EXPECTED_FORM.put(PROPERTY_NAME_COUNTRY, "BR"); + EXPECTED_FORM.put(PROPERTY_NAME_CITY, "Rio de Janeiro"); + EXPECTED_FORM.put(PROPERTY_NAME_CONTACT, "Freddy Beira Mar"); + EXPECTED_FORM.put(PROPERTY_NAME_EXTRA_INFO, "So fim de semana"); + + File completedForm = fillPaymentAccountForm(); + log.info("Completed form: {}", PAYMENT_ACCOUNT_FORM.toJsonString(completedForm)); + } + + @Test + public void testHalCashAccountForm(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, HAL_CASH_ID); + verifyEmptyForm(emptyForm, + HAL_CASH_ID, + PROPERTY_NAME_MOBILE_NR); + + EXPECTED_FORM.put(PROPERTY_NAME_PAYMENT_METHOD_ID, HAL_CASH_ID); + EXPECTED_FORM.put(PROPERTY_NAME_ACCOUNT_NAME, "Hal Cash Acct"); + EXPECTED_FORM.put(PROPERTY_NAME_MOBILE_NR, "798 123 456"); + + File completedForm = fillPaymentAccountForm(); + log.info("Completed form: {}", PAYMENT_ACCOUNT_FORM.toJsonString(completedForm)); + } + + @Test + public void testJapanBankAccountForm(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, JAPAN_BANK_ID); + verifyEmptyForm(emptyForm, + JAPAN_BANK_ID, + PROPERTY_NAME_BANK_NAME, + PROPERTY_NAME_BANK_CODE, + PROPERTY_NAME_BANK_BRANCH_CODE, + PROPERTY_NAME_BANK_BRANCH_NAME, + PROPERTY_NAME_BANK_ACCOUNT_NAME, + PROPERTY_NAME_BANK_ACCOUNT_TYPE, + PROPERTY_NAME_BANK_ACCOUNT_NUMBER); + + EXPECTED_FORM.put(PROPERTY_NAME_PAYMENT_METHOD_ID, JAPAN_BANK_ID); + EXPECTED_FORM.put(PROPERTY_NAME_ACCOUNT_NAME, "Fukuoka Account"); + EXPECTED_FORM.put(PROPERTY_NAME_BANK_NAME, "Bank of Kyoto"); + EXPECTED_FORM.put(PROPERTY_NAME_BANK_CODE, "FKBKJPJT"); + EXPECTED_FORM.put(PROPERTY_NAME_BANK_BRANCH_CODE, "8100-8727"); + EXPECTED_FORM.put(PROPERTY_NAME_BANK_BRANCH_NAME, "Fukuoka Branch"); + EXPECTED_FORM.put(PROPERTY_NAME_BANK_ACCOUNT_NAME, "Fukuoka Account"); + EXPECTED_FORM.put(PROPERTY_NAME_BANK_ACCOUNT_TYPE, "Yen Account"); + EXPECTED_FORM.put(PROPERTY_NAME_BANK_ACCOUNT_NUMBER, "8100-8727-0000"); + + File completedForm = fillPaymentAccountForm(); + log.info("Completed form: {}", PAYMENT_ACCOUNT_FORM.toJsonString(completedForm)); + } + + @Test + public void testSepaAccountForm(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, SEPA_ID); + verifyEmptyForm(emptyForm, + SEPA_ID, + PROPERTY_NAME_COUNTRY, + PROPERTY_NAME_HOLDER_NAME, + PROPERTY_NAME_IBAN, + PROPERTY_NAME_BIC); + + EXPECTED_FORM.put(PROPERTY_NAME_PAYMENT_METHOD_ID, SEPA_ID); + EXPECTED_FORM.put(PROPERTY_NAME_ACCOUNT_NAME, "Conta Sepa"); + EXPECTED_FORM.put(PROPERTY_NAME_COUNTRY, "PT"); + EXPECTED_FORM.put(PROPERTY_NAME_HOLDER_NAME, "Jose da Silva"); + EXPECTED_FORM.put(PROPERTY_NAME_IBAN, "909-909"); + EXPECTED_FORM.put(PROPERTY_NAME_BIC, "909"); + + File completedForm = fillPaymentAccountForm(); + log.info("Completed form: {}", PAYMENT_ACCOUNT_FORM.toJsonString(completedForm)); + } + + @Test + public void testSwishAccountForm(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, SWISH_ID); + verifyEmptyForm(emptyForm, + SWISH_ID, + PROPERTY_NAME_MOBILE_NR, + PROPERTY_NAME_HOLDER_NAME); + + EXPECTED_FORM.put(PROPERTY_NAME_PAYMENT_METHOD_ID, SWISH_ID); + EXPECTED_FORM.put(PROPERTY_NAME_ACCOUNT_NAME, "Swish Account"); + EXPECTED_FORM.put(PROPERTY_NAME_MOBILE_NR, "+46 7 6060 0101"); + EXPECTED_FORM.put(PROPERTY_NAME_HOLDER_NAME, "Swish Account Holder"); + + File completedForm = fillPaymentAccountForm(); + log.info("Completed form: {}", PAYMENT_ACCOUNT_FORM.toJsonString(completedForm)); + + } + + @Test + public void testUSPostalMoneyOrderAccountForm(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, US_POSTAL_MONEY_ORDER_ID); + verifyEmptyForm(emptyForm, + US_POSTAL_MONEY_ORDER_ID, + PROPERTY_NAME_HOLDER_NAME, + PROPERTY_NAME_POSTAL_ADDRESS); + + EXPECTED_FORM.put(PROPERTY_NAME_PAYMENT_METHOD_ID, US_POSTAL_MONEY_ORDER_ID); + EXPECTED_FORM.put(PROPERTY_NAME_ACCOUNT_NAME, "Bubba's Acct"); + EXPECTED_FORM.put(PROPERTY_NAME_HOLDER_NAME, "Bubba"); + EXPECTED_FORM.put(PROPERTY_NAME_POSTAL_ADDRESS, "000 Westwood Terrace Austin, TX 78700"); + + File completedForm = fillPaymentAccountForm(); + log.info("Completed form: {}", PAYMENT_ACCOUNT_FORM.toJsonString(completedForm)); + } + + private File getEmptyForm(TestInfo testInfo, String paymentMethodId) { + File emptyForm = getPaymentAccountForm(alicedaemon, paymentMethodId); + // A short cut over the API: + // File emptyForm = PAYMENT_ACCOUNT_FORM.getPaymentAccountForm(paymentMethodId); + emptyForm.deleteOnExit(); + log.info("{} Empty form saved to {}", testName(testInfo), PAYMENT_ACCOUNT_FORM.getClickableURI(emptyForm)); + return emptyForm; + } + + @Test + @Order(2) + public void testDeprecatedCreatePerfectMoneyUSDPaymentAccount() { + String PERFECT_MONEY_ACCT_NAME = "Perfect Money USD"; + String PERFECT_MONEY_ACCT_NUMBER = "0123456789"; + var perfectMoneyPaymentAccountRequest = createCreatePerfectMoneyPaymentAccountRequest( PERFECT_MONEY_ACCT_NAME, PERFECT_MONEY_ACCT_NUMBER, diff --git a/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java b/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java index af36eba27cb..111f791acd3 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java @@ -7,12 +7,10 @@ import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestMethodOrder; -import static 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.seednode; import static org.junit.jupiter.api.Assertions.fail; @@ -28,7 +26,8 @@ public class PaymentAccountTest extends MethodTest { @BeforeAll public static void setUp() { try { - setUpScaffold(bitcoind, seednode, arbdaemon, alicedaemon); + // setUpScaffold(bitcoind, seednode, arbdaemon, alicedaemon); + setUpScaffold(alicedaemon); } catch (Exception ex) { fail(ex); } @@ -43,9 +42,21 @@ public void testGetPaymentMethods() { @Test @Order(2) - public void testCreatePaymentAccount() { + public void testCreatePaymentAccount(TestInfo testInfo) { CreatePaymentAccountTest test = new CreatePaymentAccountTest(); - test.testCreatePerfectMoneyUSDPaymentAccount(); + + test.testCreateAustraliaPayidAccount(testInfo); + test.testBrazilNationalBankAccountForm(testInfo); + test.testChaseQuickPayAccountForm(testInfo); + test.testClearXChangeAccountForm(testInfo); + test.testF2FAccountForm(testInfo); + test.testHalCashAccountForm(testInfo); + test.testJapanBankAccountForm(testInfo); + test.testSepaAccountForm(testInfo); + test.testSwishAccountForm(testInfo); + test.testUSPostalMoneyOrderAccountForm(testInfo); + + test.testDeprecatedCreatePerfectMoneyUSDPaymentAccount(); } @AfterAll From 35c1c4e9443b2931ac1e43e60ddd6f1022a048d9 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 18 Nov 2020 16:42:17 -0300 Subject: [PATCH 34/52] Ensure EXPECTED_FORM.clear() is never skipped --- .../payment/AbstractPaymentAccountTest.java | 15 +++++++++++++++ .../method/payment/CreatePaymentAccountTest.java | 10 ---------- .../bisq/apitest/scenario/PaymentAccountTest.java | 5 ++--- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/apitest/src/test/java/bisq/apitest/method/payment/AbstractPaymentAccountTest.java b/apitest/src/test/java/bisq/apitest/method/payment/AbstractPaymentAccountTest.java index 9f11c5b8a5c..92a9e66a3b1 100644 --- a/apitest/src/test/java/bisq/apitest/method/payment/AbstractPaymentAccountTest.java +++ b/apitest/src/test/java/bisq/apitest/method/payment/AbstractPaymentAccountTest.java @@ -20,7 +20,9 @@ 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; @@ -81,7 +83,20 @@ public class AbstractPaymentAccountTest extends MethodTest { @BeforeEach public void setup() { Res.setup(); + } + + protected final File getEmptyForm(TestInfo testInfo, String paymentMethodId) { + // This would normally be done in @BeforeEach, but these test cases might be + // called from a single 'scenario' test case, and the @BeforeEach -> clear() + // would be skipped. EXPECTED_FORM.clear(); + + File emptyForm = getPaymentAccountForm(alicedaemon, paymentMethodId); + // A short cut over the API: + // File emptyForm = PAYMENT_ACCOUNT_FORM.getPaymentAccountForm(paymentMethodId); + emptyForm.deleteOnExit(); + log.info("{} Empty form saved to {}", testName(testInfo), PAYMENT_ACCOUNT_FORM.getClickableURI(emptyForm)); + return emptyForm; } protected final void verifyEmptyForm(File jsonForm, String paymentMethodId, String... fields) { diff --git a/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java b/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java index 725448a2266..e77fb2db63e 100644 --- a/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java +++ b/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java @@ -264,17 +264,7 @@ public void testUSPostalMoneyOrderAccountForm(TestInfo testInfo) { log.info("Completed form: {}", PAYMENT_ACCOUNT_FORM.toJsonString(completedForm)); } - private File getEmptyForm(TestInfo testInfo, String paymentMethodId) { - File emptyForm = getPaymentAccountForm(alicedaemon, paymentMethodId); - // A short cut over the API: - // File emptyForm = PAYMENT_ACCOUNT_FORM.getPaymentAccountForm(paymentMethodId); - emptyForm.deleteOnExit(); - log.info("{} Empty form saved to {}", testName(testInfo), PAYMENT_ACCOUNT_FORM.getClickableURI(emptyForm)); - return emptyForm; - } - @Test - @Order(2) public void testDeprecatedCreatePerfectMoneyUSDPaymentAccount() { String PERFECT_MONEY_ACCT_NAME = "Perfect Money USD"; String PERFECT_MONEY_ACCT_NUMBER = "0123456789"; diff --git a/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java b/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java index 111f791acd3..168ad730fb1 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java @@ -15,18 +15,17 @@ -import bisq.apitest.method.MethodTest; +import bisq.apitest.method.payment.AbstractPaymentAccountTest; import bisq.apitest.method.payment.CreatePaymentAccountTest; import bisq.apitest.method.payment.GetPaymentMethodsTest; @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) -public class PaymentAccountTest extends MethodTest { +public class PaymentAccountTest extends AbstractPaymentAccountTest { @BeforeAll public static void setUp() { try { - // setUpScaffold(bitcoind, seednode, arbdaemon, alicedaemon); setUpScaffold(alicedaemon); } catch (Exception ex) { fail(ex); From 08228d07bc92de1feaf722afa0dd4bd068aba671 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 18 Nov 2020 19:23:21 -0300 Subject: [PATCH 35/52] Replace existing api method 'createpaymentacct' impl In the CLI, this method now takes a single argument, the path to a json file (a payment account form). The json text is read from the file and sent to the server, where it is serialized to a new PaymentAccount instance, saved, and sent back to the CLI as a protobuf.PaymentAccount. - Modified existing gRPC boilerplate in grpc.proto and GrpcPaymentAccountsService. - Modified existing method signatures in CoreApi and CorePaymentAccountsService. - Replaced existing method impl in CorePaymentAccountsService and removed dead code. - Replaced the CLI's existing 'createpaymentacct' method impl. - Modified existing tests. --- .../java/bisq/apitest/method/MethodTest.java | 28 ++- .../payment/AbstractPaymentAccountTest.java | 33 ++- .../payment/CreatePaymentAccountTest.java | 214 +++++++++++++----- .../apitest/scenario/PaymentAccountTest.java | 6 +- cli/src/main/java/bisq/cli/CliMain.java | 35 +-- core/src/main/java/bisq/core/api/CoreApi.java | 10 +- .../core/api/CorePaymentAccountsService.java | 95 +------- .../core/api/model/PaymentAccountForm.java | 23 +- .../grpc/GrpcPaymentAccountsService.java | 9 +- proto/src/main/proto/grpc.proto | 7 +- 10 files changed, 262 insertions(+), 198 deletions(-) diff --git a/apitest/src/test/java/bisq/apitest/method/MethodTest.java b/apitest/src/test/java/bisq/apitest/method/MethodTest.java index 4667dc770f6..0187306b103 100644 --- a/apitest/src/test/java/bisq/apitest/method/MethodTest.java +++ b/apitest/src/test/java/bisq/apitest/method/MethodTest.java @@ -18,6 +18,7 @@ package bisq.apitest.method; import bisq.core.api.model.PaymentAccountForm; +import bisq.core.proto.CoreProtoResolver; import bisq.proto.grpc.AddressBalanceInfo; import bisq.proto.grpc.BalancesInfo; @@ -65,7 +66,6 @@ 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 bisq.core.payment.payload.PaymentMethod.PERFECT_MONEY; import static java.util.Arrays.stream; import static java.util.Comparator.comparing; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -89,6 +89,8 @@ public class MethodTest extends ApiTestCase { protected static PaymentAccount alicesDummyAcct; protected static PaymentAccount bobsDummyAcct; + private static final CoreProtoResolver CORE_PROTO_RESOLVER = new CoreProtoResolver(); + public static void startSupportingApps(boolean registerDisputeAgents, boolean generateBtcBlock, Enum... supportingApps) { @@ -280,16 +282,16 @@ protected final File getPaymentAccountForm(BisqAppConfig bisqAppConfig, String p return jsonFile; } - protected final CreatePaymentAccountRequest createCreatePerfectMoneyPaymentAccountRequest( - String accountName, - String accountNumber, - String currencyCode) { - return CreatePaymentAccountRequest.newBuilder() - .setPaymentMethodId(PERFECT_MONEY.getId()) - .setAccountName(accountName) - .setAccountNumber(accountNumber) - .setCurrencyCode(currencyCode) + 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 PaymentAccount getDefaultPerfectDummyPaymentAccount(BisqAppConfig bisqAppConfig) { @@ -356,10 +358,14 @@ protected static RegisterDisputeAgentRequest createRegisterDisputeAgentRequest(S .setRegistrationKey(DEV_PRIVILEGE_PRIV_KEY).build(); } - @SuppressWarnings("ResultOfMethodCallIgnored") + @SuppressWarnings({"ResultOfMethodCallIgnored", "SameParameterValue"}) protected static void registerDisputeAgents(BisqAppConfig bisqAppConfig) { var disputeAgentsService = grpcStubs(bisqAppConfig).disputeAgentsService; disputeAgentsService.registerDisputeAgent(createRegisterDisputeAgentRequest(MEDIATOR)); disputeAgentsService.registerDisputeAgent(createRegisterDisputeAgentRequest(REFUND_AGENT)); } + + private bisq.core.payment.PaymentAccount fromProto(protobuf.PaymentAccount proto) { + return bisq.core.payment.PaymentAccount.fromProto(proto, CORE_PROTO_RESOLVER); + } } diff --git a/apitest/src/test/java/bisq/apitest/method/payment/AbstractPaymentAccountTest.java b/apitest/src/test/java/bisq/apitest/method/payment/AbstractPaymentAccountTest.java index 92a9e66a3b1..1eb7f91c9a4 100644 --- a/apitest/src/test/java/bisq/apitest/method/payment/AbstractPaymentAccountTest.java +++ b/apitest/src/test/java/bisq/apitest/method/payment/AbstractPaymentAccountTest.java @@ -2,6 +2,9 @@ import bisq.core.api.model.PaymentAccountForm; import bisq.core.locale.Res; +import bisq.core.payment.PaymentAccount; + +import bisq.proto.grpc.GetPaymentAccountsRequest; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -16,6 +19,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.Optional; import lombok.extern.slf4j.Slf4j; @@ -28,6 +32,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -95,7 +100,10 @@ protected final File getEmptyForm(TestInfo testInfo, String paymentMethodId) { // A short cut over the API: // File emptyForm = PAYMENT_ACCOUNT_FORM.getPaymentAccountForm(paymentMethodId); emptyForm.deleteOnExit(); - log.info("{} Empty form saved to {}", testName(testInfo), PAYMENT_ACCOUNT_FORM.getClickableURI(emptyForm)); + + if (log.isDebugEnabled()) + log.debug("{} Empty form saved to {}", testName(testInfo), PAYMENT_ACCOUNT_FORM.getClickableURI(emptyForm)); + return emptyForm; } @@ -138,4 +146,27 @@ protected final File fillPaymentAccountForm() { } return tmpJsonForm; } + + protected final void verifyCommonFormEntries(PaymentAccount paymentAccount) { + // All PaymentAccount subclasses have paymentMethodId and an accountName fields. + assertNotNull(paymentAccount); + assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_PAYMENT_METHOD_ID), paymentAccount.getPaymentMethod().getId()); + assertTrue(paymentAccount.getCreationDate().getTime() > 0); + assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_ACCOUNT_NAME), paymentAccount.getAccountName()); + } + + protected final void verifyAccountFiatCurrency(PaymentAccount paymentAccount, String expectedCurrencyCode) { + assertNotNull(paymentAccount.getSingleTradeCurrency()); + assertEquals(expectedCurrencyCode, paymentAccount.getSingleTradeCurrency().getCode()); + } + + protected final void verifyUserPayloadHasPaymentAccountWithId(String paymentAccountId) { + var getPaymentAccountsRequest = GetPaymentAccountsRequest.newBuilder().build(); + var reply = grpcStubs(alicedaemon) + .paymentAccountsService.getPaymentAccounts(getPaymentAccountsRequest); + Optional paymentAccount = reply.getPaymentAccountsList().stream() + .filter(a -> a.getId().equals(paymentAccountId)) + .findFirst(); + assertTrue(paymentAccount.isPresent()); + } } diff --git a/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java b/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java index e77fb2db63e..09dae2ba1d7 100644 --- a/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java +++ b/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java @@ -17,15 +17,21 @@ package bisq.apitest.method.payment; -import bisq.proto.grpc.GetPaymentAccountsRequest; - -import protobuf.PaymentAccount; -import protobuf.PerfectMoneyAccountPayload; +import bisq.core.payment.AustraliaPayid; +import bisq.core.payment.ChaseQuickPayAccount; +import bisq.core.payment.ClearXchangeAccount; +import bisq.core.payment.F2FAccount; +import bisq.core.payment.HalCashAccount; +import bisq.core.payment.JapanBankAccount; +import bisq.core.payment.NationalBankAccount; +import bisq.core.payment.SepaAccount; +import bisq.core.payment.SwishAccount; +import bisq.core.payment.USPostalMoneyOrderAccount; +import bisq.core.payment.payload.BankAccountPayload; import java.io.File; -import java.util.List; -import java.util.stream.Collectors; +import java.util.Objects; import lombok.extern.slf4j.Slf4j; @@ -40,8 +46,7 @@ import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind; import static bisq.apitest.config.BisqAppConfig.alicedaemon; import static bisq.core.payment.payload.PaymentMethod.*; -import static java.util.Comparator.comparing; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.Assert.assertEquals; import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; @@ -50,9 +55,6 @@ @TestMethodOrder(OrderAnnotation.class) public class CreatePaymentAccountTest extends AbstractPaymentAccountTest { - // TODO Test PaymentAccountForm's PaymentAccount toPaymentAccount(File jsonForm) - // after replacement api method 'createpaymentacct' is implemented. - @BeforeAll public static void setUp() { try { @@ -76,7 +78,18 @@ public void testCreateAustraliaPayidAccount(TestInfo testInfo) { EXPECTED_FORM.put(PROPERTY_NAME_BANK_ACCOUNT_NAME, "Credit Union Australia"); File completedForm = fillPaymentAccountForm(); - log.info("Completed form: {}", PAYMENT_ACCOUNT_FORM.toJsonString(completedForm)); + String jsonString = PAYMENT_ACCOUNT_FORM.toJsonString(completedForm); + if (log.isDebugEnabled()) + log.debug("Completed form: {}", jsonString); + + AustraliaPayid paymentAccount = (AustraliaPayid) createPaymentAccount(alicedaemon, jsonString); + verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId()); + verifyAccountFiatCurrency(paymentAccount, "AUD"); + verifyCommonFormEntries(paymentAccount); + assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_PAY_ID), paymentAccount.getPayid()); + assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_BANK_ACCOUNT_NAME), paymentAccount.getBankAccountName()); + if (log.isDebugEnabled()) + log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount); } @Test @@ -105,8 +118,28 @@ public void testBrazilNationalBankAccountForm(TestInfo testInfo) { EXPECTED_FORM.put(PROPERTY_NAME_NATIONAL_ACCOUNT_ID, "123456789"); File completedForm = fillPaymentAccountForm(); - log.info("Completed form: {}", PAYMENT_ACCOUNT_FORM.toJsonString(completedForm)); - + String jsonString = PAYMENT_ACCOUNT_FORM.toJsonString(completedForm); + if (log.isDebugEnabled()) + log.debug("Completed form: {}", jsonString); + + NationalBankAccount paymentAccount = (NationalBankAccount) createPaymentAccount(alicedaemon, jsonString); + verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId()); + verifyAccountFiatCurrency(paymentAccount, "BRL"); + verifyCommonFormEntries(paymentAccount); + assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_COUNTRY), + Objects.requireNonNull(paymentAccount.getCountry()).code); + + BankAccountPayload payload = (BankAccountPayload) paymentAccount.getPaymentAccountPayload(); + assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_ACCOUNT_NR), payload.getAccountNr()); + // When no BankId is required, getBankId() returns bankName. + assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_BANK_NAME), payload.getBankId()); + assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_BANK_NAME), payload.getBankName()); + assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_BRANCH_ID), payload.getBranchId()); + assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_HOLDER_NAME), payload.getHolderName()); + assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_HOLDER_TAX_ID), payload.getHolderTaxId()); + assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_NATIONAL_ACCOUNT_ID), payload.getNationalAccountId()); + if (log.isDebugEnabled()) + log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount); } @Test @@ -123,8 +156,18 @@ public void testChaseQuickPayAccountForm(TestInfo testInfo) { EXPECTED_FORM.put(PROPERTY_NAME_HOLDER_NAME, "John Doe"); File completedForm = fillPaymentAccountForm(); - log.info("Completed form: {}", PAYMENT_ACCOUNT_FORM.toJsonString(completedForm)); - + String jsonString = PAYMENT_ACCOUNT_FORM.toJsonString(completedForm); + if (log.isDebugEnabled()) + log.debug("Completed form: {}", jsonString); + + ChaseQuickPayAccount paymentAccount = (ChaseQuickPayAccount) createPaymentAccount(alicedaemon, jsonString); + verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId()); + verifyAccountFiatCurrency(paymentAccount, "USD"); + verifyCommonFormEntries(paymentAccount); + assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail()); + assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName()); + if (log.isDebugEnabled()) + log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount); } @Test @@ -141,7 +184,18 @@ public void testClearXChangeAccountForm(TestInfo testInfo) { EXPECTED_FORM.put(PROPERTY_NAME_HOLDER_NAME, "Jane Doe"); File completedForm = fillPaymentAccountForm(); - log.info("Completed form: {}", PAYMENT_ACCOUNT_FORM.toJsonString(completedForm)); + String jsonString = PAYMENT_ACCOUNT_FORM.toJsonString(completedForm); + if (log.isDebugEnabled()) + log.debug("Completed form: {}", jsonString); + + ClearXchangeAccount paymentAccount = (ClearXchangeAccount) createPaymentAccount(alicedaemon, jsonString); + verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId()); + verifyAccountFiatCurrency(paymentAccount, "USD"); + verifyCommonFormEntries(paymentAccount); + assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_EMAIL_OR_MOBILE_NR), paymentAccount.getEmailOrMobileNr()); + assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName()); + if (log.isDebugEnabled()) + log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount); } @Test @@ -163,7 +217,21 @@ public void testF2FAccountForm(TestInfo testInfo) { EXPECTED_FORM.put(PROPERTY_NAME_EXTRA_INFO, "So fim de semana"); File completedForm = fillPaymentAccountForm(); - log.info("Completed form: {}", PAYMENT_ACCOUNT_FORM.toJsonString(completedForm)); + String jsonString = PAYMENT_ACCOUNT_FORM.toJsonString(completedForm); + if (log.isDebugEnabled()) + log.debug("Completed form: {}", jsonString); + + F2FAccount paymentAccount = (F2FAccount) createPaymentAccount(alicedaemon, jsonString); + verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId()); + verifyAccountFiatCurrency(paymentAccount, "BRL"); + verifyCommonFormEntries(paymentAccount); + assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_COUNTRY), + Objects.requireNonNull(paymentAccount.getCountry()).code); + assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_CITY), paymentAccount.getCity()); + assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_CONTACT), paymentAccount.getContact()); + assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_EXTRA_INFO), paymentAccount.getExtraInfo()); + if (log.isDebugEnabled()) + log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount); } @Test @@ -178,7 +246,17 @@ public void testHalCashAccountForm(TestInfo testInfo) { EXPECTED_FORM.put(PROPERTY_NAME_MOBILE_NR, "798 123 456"); File completedForm = fillPaymentAccountForm(); - log.info("Completed form: {}", PAYMENT_ACCOUNT_FORM.toJsonString(completedForm)); + String jsonString = PAYMENT_ACCOUNT_FORM.toJsonString(completedForm); + if (log.isDebugEnabled()) + log.debug("Completed form: {}", jsonString); + + HalCashAccount paymentAccount = (HalCashAccount) createPaymentAccount(alicedaemon, jsonString); + verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId()); + verifyAccountFiatCurrency(paymentAccount, "EUR"); + verifyCommonFormEntries(paymentAccount); + assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_MOBILE_NR), paymentAccount.getMobileNr()); + if (log.isDebugEnabled()) + log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount); } @Test @@ -205,7 +283,23 @@ public void testJapanBankAccountForm(TestInfo testInfo) { EXPECTED_FORM.put(PROPERTY_NAME_BANK_ACCOUNT_NUMBER, "8100-8727-0000"); File completedForm = fillPaymentAccountForm(); - log.info("Completed form: {}", PAYMENT_ACCOUNT_FORM.toJsonString(completedForm)); + String jsonString = PAYMENT_ACCOUNT_FORM.toJsonString(completedForm); + if (log.isDebugEnabled()) + log.debug("Completed form: {}", jsonString); + + JapanBankAccount paymentAccount = (JapanBankAccount) createPaymentAccount(alicedaemon, jsonString); + verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId()); + verifyAccountFiatCurrency(paymentAccount, "JPY"); + verifyCommonFormEntries(paymentAccount); + assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_BANK_CODE), paymentAccount.getBankCode()); + assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_BANK_NAME), paymentAccount.getBankName()); + assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_BANK_BRANCH_CODE), paymentAccount.getBankBranchCode()); + assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_BANK_BRANCH_NAME), paymentAccount.getBankBranchName()); + assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_BANK_ACCOUNT_NAME), paymentAccount.getBankAccountName()); + assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_BANK_ACCOUNT_TYPE), paymentAccount.getBankAccountType()); + assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_BANK_ACCOUNT_NUMBER), paymentAccount.getBankAccountNumber()); + if (log.isDebugEnabled()) + log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount); } @Test @@ -226,7 +320,22 @@ public void testSepaAccountForm(TestInfo testInfo) { EXPECTED_FORM.put(PROPERTY_NAME_BIC, "909"); File completedForm = fillPaymentAccountForm(); - log.info("Completed form: {}", PAYMENT_ACCOUNT_FORM.toJsonString(completedForm)); + String jsonString = PAYMENT_ACCOUNT_FORM.toJsonString(completedForm); + if (log.isDebugEnabled()) + log.debug("Completed form: {}", jsonString); + + SepaAccount paymentAccount = (SepaAccount) createPaymentAccount(alicedaemon, jsonString); + verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId()); + assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_COUNTRY), Objects.requireNonNull(paymentAccount.getCountry()).code); + verifyAccountFiatCurrency(paymentAccount, "EUR"); + verifyCommonFormEntries(paymentAccount); + assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName()); + assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_IBAN), paymentAccount.getIban()); + assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_BIC), paymentAccount.getBic()); + // bankId == bic + assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_BIC), paymentAccount.getBankId()); + if (log.isDebugEnabled()) + log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount); } @Test @@ -243,8 +352,18 @@ public void testSwishAccountForm(TestInfo testInfo) { EXPECTED_FORM.put(PROPERTY_NAME_HOLDER_NAME, "Swish Account Holder"); File completedForm = fillPaymentAccountForm(); - log.info("Completed form: {}", PAYMENT_ACCOUNT_FORM.toJsonString(completedForm)); - + String jsonString = PAYMENT_ACCOUNT_FORM.toJsonString(completedForm); + if (log.isDebugEnabled()) + log.debug("Completed form: {}", jsonString); + + SwishAccount paymentAccount = (SwishAccount) createPaymentAccount(alicedaemon, jsonString); + verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId()); + verifyAccountFiatCurrency(paymentAccount, "SEK"); + verifyCommonFormEntries(paymentAccount); + assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_MOBILE_NR), paymentAccount.getMobileNr()); + assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName()); + if (log.isDebugEnabled()) + log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount); } @Test @@ -261,43 +380,18 @@ public void testUSPostalMoneyOrderAccountForm(TestInfo testInfo) { EXPECTED_FORM.put(PROPERTY_NAME_POSTAL_ADDRESS, "000 Westwood Terrace Austin, TX 78700"); File completedForm = fillPaymentAccountForm(); - log.info("Completed form: {}", PAYMENT_ACCOUNT_FORM.toJsonString(completedForm)); - } - - @Test - public void testDeprecatedCreatePerfectMoneyUSDPaymentAccount() { - String PERFECT_MONEY_ACCT_NAME = "Perfect Money USD"; - String PERFECT_MONEY_ACCT_NUMBER = "0123456789"; - - var perfectMoneyPaymentAccountRequest = createCreatePerfectMoneyPaymentAccountRequest( - PERFECT_MONEY_ACCT_NAME, - PERFECT_MONEY_ACCT_NUMBER, - "USD"); - //noinspection ResultOfMethodCallIgnored - grpcStubs(alicedaemon).paymentAccountsService.createPaymentAccount(perfectMoneyPaymentAccountRequest); - - var getPaymentAccountsRequest = GetPaymentAccountsRequest.newBuilder().build(); - var reply = grpcStubs(alicedaemon).paymentAccountsService.getPaymentAccounts(getPaymentAccountsRequest); - - // The daemon is running against the regtest/dao setup files, and was set up with - // two dummy accounts ("PerfectMoney dummy", "ETH dummy") before any tests ran. - // We just added 1 test account, making 3 total. - assertEquals(3, reply.getPaymentAccountsCount()); - - // Sort the returned list by creation date; the last item in the sorted - // list will be the payment acct we just created. - List paymentAccountList = reply.getPaymentAccountsList().stream() - .sorted(comparing(PaymentAccount::getCreationDate)) - .collect(Collectors.toList()); - PaymentAccount paymentAccount = paymentAccountList.get(2); - PerfectMoneyAccountPayload perfectMoneyAccount = paymentAccount - .getPaymentAccountPayload() - .getPerfectMoneyAccountPayload(); - - assertEquals(PERFECT_MONEY_ACCT_NAME, paymentAccount.getAccountName()); - assertEquals("USD", - paymentAccount.getSelectedTradeCurrency().getFiatCurrency().getCurrency().getCurrencyCode()); - assertEquals(PERFECT_MONEY_ACCT_NUMBER, perfectMoneyAccount.getAccountNr()); + String jsonString = PAYMENT_ACCOUNT_FORM.toJsonString(completedForm); + if (log.isDebugEnabled()) + log.debug("Completed form: {}", jsonString); + + USPostalMoneyOrderAccount paymentAccount = (USPostalMoneyOrderAccount) createPaymentAccount(alicedaemon, jsonString); + verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId()); + verifyAccountFiatCurrency(paymentAccount, "USD"); + verifyCommonFormEntries(paymentAccount); + assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName()); + assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_POSTAL_ADDRESS), paymentAccount.getPostalAddress()); + if (log.isDebugEnabled()) + log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount); } @AfterAll diff --git a/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java b/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java index 168ad730fb1..6227c674746 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java @@ -10,7 +10,9 @@ import org.junit.jupiter.api.TestInfo; 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 org.junit.jupiter.api.Assertions.fail; @@ -26,7 +28,7 @@ public class PaymentAccountTest extends AbstractPaymentAccountTest { @BeforeAll public static void setUp() { try { - setUpScaffold(alicedaemon); + setUpScaffold(bitcoind, seednode, alicedaemon); } catch (Exception ex) { fail(ex); } @@ -54,8 +56,6 @@ public void testCreatePaymentAccount(TestInfo testInfo) { test.testSepaAccountForm(testInfo); test.testSwishAccountForm(testInfo); test.testUSPostalMoneyOrderAccountForm(testInfo); - - test.testDeprecatedCreatePerfectMoneyUSDPaymentAccount(); } @AfterAll diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index f031015eda3..6439510266e 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -450,23 +450,28 @@ public static void run(String[] args) { return; } case createpaymentacct: { - if (nonOptionArgs.size() < 5) + if (nonOptionArgs.size() < 2) throw new IllegalArgumentException( - "incorrect parameter count, expecting payment method id," - + " account name, account number, currency code"); + "incorrect parameter count, expecting path to payment account form"); - var paymentMethodId = nonOptionArgs.get(1); - var accountName = nonOptionArgs.get(2); - var accountNumber = nonOptionArgs.get(3); - var currencyCode = nonOptionArgs.get(4); + var paymentAccountFormPath = Paths.get(nonOptionArgs.get(1)); + if (!paymentAccountFormPath.toFile().exists()) + throw new IllegalStateException( + format("%s could not be found", paymentAccountFormPath.toString())); + + String jsonString; + try { + jsonString = new String(Files.readAllBytes(paymentAccountFormPath)); + } catch (IOException e) { + throw new IllegalStateException( + format("could not read %s", paymentAccountFormPath.toString())); + } var request = CreatePaymentAccountRequest.newBuilder() - .setPaymentMethodId(paymentMethodId) - .setAccountName(accountName) - .setAccountNumber(accountNumber) - .setCurrencyCode(currencyCode).build(); + .setPaymentAccountForm(jsonString) + .build(); paymentAccountsService.createPaymentAccount(request); - out.printf("payment account %s saved", accountName); + out.println("payment account saved"); return; } case getpaymentaccts: { @@ -555,7 +560,9 @@ private static Method getMethodFromCmd(String methodName) { return Method.valueOf(methodName.toLowerCase()); } - private static void saveFileToDisk(String prefix, String suffix, String text) { + private static void saveFileToDisk(String prefix, + @SuppressWarnings("SameParameterValue") String suffix, + String text) { String timestamp = Long.toUnsignedString(new Date().getTime()); String relativeFileName = prefix + "_" + timestamp + suffix; try { @@ -604,7 +611,7 @@ private static void printHelp(OptionParser parser, PrintStream stream) { stream.format(rowFormat, "withdrawfunds", "trade id, bitcoin wallet address", "Withdraw received funds to external wallet address"); stream.format(rowFormat, "getpaymentmethods", "", "Get list of supported payment account method ids"); stream.format(rowFormat, "getpaymentacctform", "payment method id", "Get a new payment account form"); - stream.format(rowFormat, "createpaymentacct", "account name, account number, currency code", "Create PerfectMoney dummy account"); + stream.format(rowFormat, "createpaymentacct", "path to payment account form", "Create a new payment account"); stream.format(rowFormat, "getpaymentaccts", "", "Get user payment accounts"); stream.format(rowFormat, "lockwallet", "", "Remove wallet password from memory, locking the wallet"); stream.format(rowFormat, "unlockwallet", "password timeout", diff --git a/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java index 6add2a7d424..96e2d3055d2 100644 --- a/core/src/main/java/bisq/core/api/CoreApi.java +++ b/core/src/main/java/bisq/core/api/CoreApi.java @@ -155,14 +155,8 @@ public void cancelOffer(String id) { // PaymentAccounts /////////////////////////////////////////////////////////////////////////////////////////// - public void createPaymentAccount(String paymentMethodId, - String accountName, - String accountNumber, - String currencyCode) { - paymentAccountsService.createPaymentAccount(paymentMethodId, - accountName, - accountNumber, - currencyCode); + public PaymentAccount createPaymentAccount(String jsonString) { + return paymentAccountsService.createPaymentAccount(jsonString); } public Set getPaymentAccounts() { diff --git a/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java b/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java index 3b1149e9bcf..dacb79567fd 100644 --- a/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java +++ b/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java @@ -19,15 +19,10 @@ import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.api.model.PaymentAccountForm; -import bisq.core.locale.FiatCurrency; import bisq.core.payment.PaymentAccount; -import bisq.core.payment.PaymentAccountFactory; -import bisq.core.payment.PerfectMoneyAccount; import bisq.core.payment.payload.PaymentMethod; import bisq.core.user.User; -import bisq.common.config.Config; - import javax.inject.Inject; import java.io.File; @@ -39,45 +34,30 @@ import lombok.extern.slf4j.Slf4j; -import static bisq.core.payment.payload.PaymentMethod.*; -import static com.google.common.base.Preconditions.checkNotNull; - @Slf4j class CorePaymentAccountsService { - private final Config config; private final AccountAgeWitnessService accountAgeWitnessService; private final PaymentAccountForm paymentAccountForm; private final User user; @Inject - public CorePaymentAccountsService(Config config, - AccountAgeWitnessService accountAgeWitnessService, + public CorePaymentAccountsService(AccountAgeWitnessService accountAgeWitnessService, PaymentAccountForm paymentAccountForm, User user) { - this.config = config; this.accountAgeWitnessService = accountAgeWitnessService; this.paymentAccountForm = paymentAccountForm; this.user = user; } - void createPaymentAccount(String paymentMethodId, - String accountName, - String accountNumber, - String currencyCode) { - - PaymentAccount paymentAccount = getNewPaymentAccount(paymentMethodId, - accountName, - accountNumber, - currencyCode); - + PaymentAccount createPaymentAccount(String jsonString) { + PaymentAccount paymentAccount = paymentAccountForm.toPaymentAccount(jsonString); user.addPaymentAccountIfNotExists(paymentAccount); - - // Don't do this on mainnet until thoroughly tested. - if (config.baseCurrencyNetwork.isRegtest()) - accountAgeWitnessService.publishMyAccountAgeWitness(paymentAccount.getPaymentAccountPayload()); - - log.info("Payment account {} saved", paymentAccount.getId()); + accountAgeWitnessService.publishMyAccountAgeWitness(paymentAccount.getPaymentAccountPayload()); + log.info("Saved payment account with id {} and payment method {}.", + paymentAccount.getId(), + paymentAccount.getPaymentAccountPayload().getPaymentMethodId()); + return paymentAccount; } Set getPaymentAccounts() { @@ -85,7 +65,7 @@ Set getPaymentAccounts() { } List getFiatPaymentMethods() { - return getPaymentMethods().stream() + return PaymentMethod.getPaymentMethods().stream() .filter(paymentMethod -> !paymentMethod.isAsset()) .sorted(Comparator.comparing(PaymentMethod::getId)) .collect(Collectors.toList()); @@ -99,61 +79,4 @@ String getPaymentAccountFormAsString(String paymentMethodId) { File getPaymentAccountForm(String paymentMethodId) { return paymentAccountForm.getPaymentAccountForm(paymentMethodId); } - - private PaymentAccount getNewPaymentAccount(String paymentMethodId, - String accountName, - String accountNumber, - String currencyCode) { - PaymentAccount paymentAccount = null; - PaymentMethod paymentMethod = getPaymentMethodById(paymentMethodId); - - switch (paymentMethod.getId()) { - case UPHOLD_ID: - case MONEY_BEAM_ID: - case POPMONEY_ID: - case REVOLUT_ID: - //noinspection DuplicateBranchesInSwitch - log.error("PaymentMethod {} not supported yet.", paymentMethod); - break; - case PERFECT_MONEY_ID: - // Create and persist a PerfectMoney dummy payment account. There is no - // guard against creating accounts with duplicate names & numbers, only - // the uuid and creation date are unique. - paymentAccount = PaymentAccountFactory.getPaymentAccount(paymentMethod); - paymentAccount.init(); - paymentAccount.setAccountName(accountName); - ((PerfectMoneyAccount) paymentAccount).setAccountNr(accountNumber); - paymentAccount.setSingleTradeCurrency(new FiatCurrency(currencyCode)); - break; - case SEPA_ID: - case SEPA_INSTANT_ID: - case FASTER_PAYMENTS_ID: - case NATIONAL_BANK_ID: - case SAME_BANK_ID: - case SPECIFIC_BANKS_ID: - case JAPAN_BANK_ID: - case ALI_PAY_ID: - case WECHAT_PAY_ID: - case SWISH_ID: - case CLEAR_X_CHANGE_ID: - case CHASE_QUICK_PAY_ID: - case INTERAC_E_TRANSFER_ID: - case US_POSTAL_MONEY_ORDER_ID: - case MONEY_GRAM_ID: - case WESTERN_UNION_ID: - case CASH_DEPOSIT_ID: - case HAL_CASH_ID: - case F2F_ID: - case PROMPT_PAY_ID: - case ADVANCED_CASH_ID: - default: - log.error("PaymentMethod {} not supported yet.", paymentMethod); - break; - } - - checkNotNull(paymentAccount, - "Could not create payment account with paymentMethodId " - + paymentMethodId + "."); - return paymentAccount; - } } diff --git a/core/src/main/java/bisq/core/api/model/PaymentAccountForm.java b/core/src/main/java/bisq/core/api/model/PaymentAccountForm.java index 780c501bbc7..3906c97a236 100644 --- a/core/src/main/java/bisq/core/api/model/PaymentAccountForm.java +++ b/core/src/main/java/bisq/core/api/model/PaymentAccountForm.java @@ -88,7 +88,8 @@ * *

* (3) De-serialize the edited json account form: Pass the edited json file to - * {@link bisq.core.api.model.PaymentAccountForm#toPaymentAccount(File)}, + * {@link bisq.core.api.model.PaymentAccountForm#toPaymentAccount(File)}, or + * a json string to {@link bisq.core.api.model.PaymentAccountForm#toPaymentAccount(String)} * and get a {@link bisq.core.payment.HalCashAccount} instance. *
  * PaymentAccount(
@@ -152,16 +153,27 @@ public File getPaymentAccountForm(String paymentMethodId) {
     }
 
     /**
-     * De-serialize a PaymentAccount json form into a populated PaymentAccount instance.
+     * De-serialize a PaymentAccount json form into a new PaymentAccount instance.
      *
      * @param jsonForm The file representing a new payment account form.
      * @return A populated PaymentAccount subclass instance.
      */
+    @VisibleForTesting
     public PaymentAccount toPaymentAccount(File jsonForm) {
-        String json = toJsonString(jsonForm);
-        Class clazz = getPaymentAccountClassFromJson(json);
+        String jsonString = toJsonString(jsonForm);
+        return toPaymentAccount(jsonString);
+    }
+
+    /**
+     * De-serialize a PaymentAccount json string into a new PaymentAccount instance.
+     *
+     * @param jsonString The json data representing a new payment account form.
+     * @return A populated PaymentAccount subclass instance.
+     */
+    public PaymentAccount toPaymentAccount(String jsonString) {
+        Class clazz = getPaymentAccountClassFromJson(jsonString);
         Gson gson = gsonBuilder.registerTypeAdapter(clazz, new PaymentAccountTypeAdapter(clazz)).create();
-        return gson.fromJson(json, clazz);
+        return gson.fromJson(jsonString, clazz);
     }
 
     public String toJsonString(File jsonFile) {
@@ -174,6 +186,7 @@ public String toJsonString(File jsonFile) {
         }
     }
 
+    @VisibleForTesting
     public URI getClickableURI(File jsonForm) {
         try {
             return new URI("file",
diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java
index 35bb3b322bb..d48499c85ff 100644
--- a/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java
+++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java
@@ -50,11 +50,10 @@ public GrpcPaymentAccountsService(CoreApi coreApi) {
     @Override
     public void createPaymentAccount(CreatePaymentAccountRequest req,
                                      StreamObserver responseObserver) {
-        coreApi.createPaymentAccount(req.getPaymentMethodId(),
-                req.getAccountName(),
-                req.getAccountNumber(),
-                req.getCurrencyCode());
-        var reply = CreatePaymentAccountReply.newBuilder().build();
+        PaymentAccount paymentAccount = coreApi.createPaymentAccount(req.getPaymentAccountForm());
+        var reply = CreatePaymentAccountReply.newBuilder()
+                .setPaymentAccount(paymentAccount.toProtoMessage())
+                .build();
         responseObserver.onNext(reply);
         responseObserver.onCompleted();
     }
diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto
index 0f5430b429b..b20a508ddea 100644
--- a/proto/src/main/proto/grpc.proto
+++ b/proto/src/main/proto/grpc.proto
@@ -132,14 +132,11 @@ service PaymentAccounts {
 }
 
 message CreatePaymentAccountRequest {
-    string paymentMethodId = 1;
-    string accountName = 2;
-    string accountNumber = 3;
-    // TODO  Support all currencies. Maybe add a repeated and if only one is used its a singletonList.
-    string currencyCode = 4;
+    string paymentAccountForm = 1;
 }
 
 message CreatePaymentAccountReply {
+    PaymentAccount paymentAccount = 1;
 }
 
 message GetPaymentAccountsRequest {

From 138822edadea4e2a04ad0292f5016e5bdccf06ac Mon Sep 17 00:00:00 2001
From: ghubstan <36207203+ghubstan@users.noreply.github.com>
Date: Wed, 18 Nov 2020 20:05:15 -0300
Subject: [PATCH 36/52] Avoid 'unnecessary use of fully qualified name' codacy
 issue

---
 apitest/src/test/java/bisq/apitest/method/MethodTest.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/apitest/src/test/java/bisq/apitest/method/MethodTest.java b/apitest/src/test/java/bisq/apitest/method/MethodTest.java
index 0187306b103..03cb6cb0b3c 100644
--- a/apitest/src/test/java/bisq/apitest/method/MethodTest.java
+++ b/apitest/src/test/java/bisq/apitest/method/MethodTest.java
@@ -365,7 +365,7 @@ protected static void registerDisputeAgents(BisqAppConfig bisqAppConfig) {
         disputeAgentsService.registerDisputeAgent(createRegisterDisputeAgentRequest(REFUND_AGENT));
     }
 
-    private bisq.core.payment.PaymentAccount fromProto(protobuf.PaymentAccount proto) {
+    private bisq.core.payment.PaymentAccount fromProto(PaymentAccount proto) {
         return bisq.core.payment.PaymentAccount.fromProto(proto, CORE_PROTO_RESOLVER);
     }
 }

From cb9a68bae197459e7b2dc7a8de1f9e4fa6944394 Mon Sep 17 00:00:00 2001
From: ghubstan <36207203+ghubstan@users.noreply.github.com>
Date: Thu, 19 Nov 2020 12:01:41 -0300
Subject: [PATCH 37/52] Tidy up payment accts api related msgs on CLI

---
 cli/src/main/java/bisq/cli/CliMain.java | 26 +++++++++++++++++++------
 1 file changed, 20 insertions(+), 6 deletions(-)

diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java
index 6439510266e..f3d2b806ada 100644
--- a/cli/src/main/java/bisq/cli/CliMain.java
+++ b/cli/src/main/java/bisq/cli/CliMain.java
@@ -43,6 +43,8 @@
 import bisq.proto.grpc.UnlockWalletRequest;
 import bisq.proto.grpc.WithdrawFundsRequest;
 
+import protobuf.PaymentAccount;
+
 import io.grpc.StatusRuntimeException;
 
 import joptsimple.OptionParser;
@@ -52,6 +54,7 @@
 import java.nio.file.Path;
 import java.nio.file.Paths;
 
+import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.PrintStream;
@@ -443,10 +446,12 @@ public static void run(String[] args) {
                             .build();
                     String jsonString = paymentAccountsService.getPaymentAccountForm(request)
                             .getPaymentAccountFormJson();
-                    out.printf("new payment account form: %s%n", jsonString);
-                    saveFileToDisk(paymentMethodId.toLowerCase(),
+                    File jsonFile = saveFileToDisk(paymentMethodId.toLowerCase(),
                             ".json",
                             jsonString);
+                    out.printf("Payment account form %s%nsaved to %s%n",
+                            jsonString, jsonFile.getAbsolutePath());
+                    out.println("Edit the file, and use as the argument to a 'createpaymentacct' command.");
                     return;
                 }
                 case createpaymentacct: {
@@ -457,7 +462,8 @@ public static void run(String[] args) {
                     var paymentAccountFormPath = Paths.get(nonOptionArgs.get(1));
                     if (!paymentAccountFormPath.toFile().exists())
                         throw new IllegalStateException(
-                                format("%s could not be found", paymentAccountFormPath.toString()));
+                                format("payment account form '%s' could not be found",
+                                        paymentAccountFormPath.toString()));
 
                     String jsonString;
                     try {
@@ -470,14 +476,21 @@ public static void run(String[] args) {
                     var request = CreatePaymentAccountRequest.newBuilder()
                             .setPaymentAccountForm(jsonString)
                             .build();
-                    paymentAccountsService.createPaymentAccount(request);
+                    var reply = paymentAccountsService.createPaymentAccount(request);
                     out.println("payment account saved");
+                    out.println(formatPaymentAcctTbl(singletonList(reply.getPaymentAccount())));
                     return;
                 }
                 case getpaymentaccts: {
                     var request = GetPaymentAccountsRequest.newBuilder().build();
                     var reply = paymentAccountsService.getPaymentAccounts(request);
-                    out.println(formatPaymentAcctTbl(reply.getPaymentAccountsList()));
+
+                    List paymentAccounts = reply.getPaymentAccountsList();
+                    if (paymentAccounts.size() > 0)
+                        out.println(formatPaymentAcctTbl(paymentAccounts));
+                    else
+                        out.println("no payment accounts are saved");
+
                     return;
                 }
                 case lockwallet: {
@@ -560,7 +573,7 @@ private static Method getMethodFromCmd(String methodName) {
         return Method.valueOf(methodName.toLowerCase());
     }
 
-    private static void saveFileToDisk(String prefix,
+    private static File saveFileToDisk(String prefix,
                                        @SuppressWarnings("SameParameterValue") String suffix,
                                        String text) {
         String timestamp = Long.toUnsignedString(new Date().getTime());
@@ -571,6 +584,7 @@ private static void saveFileToDisk(String prefix,
                 try (PrintWriter out = new PrintWriter(path.toString())) {
                     out.println(text);
                 }
+                return path.toAbsolutePath().toFile();
             } else {
                 throw new IllegalStateException(format("could not overwrite existing file '%s'", relativeFileName));
             }

From ff887eb3392f62b4502fa3fdc595aa600b15d7cc Mon Sep 17 00:00:00 2001
From: ghubstan <36207203+ghubstan@users.noreply.github.com>
Date: Thu, 19 Nov 2020 13:38:34 -0300
Subject: [PATCH 38/52] Fix createpaymentacct validation problems

- Add missing boilerplate error handling to GrpcPaymentAccountsService.

- Edit some PaymentAccountForm & PaymentAccountTypeAdapter exception
  messages that would be passed to CLI;  they should be in the same
  style as existing CLI err msgs.
---
 .../core/api/model/PaymentAccountForm.java    |  9 +-
 .../api/model/PaymentAccountTypeAdapter.java  | 61 ++++++------
 .../grpc/GrpcPaymentAccountsService.java      | 94 ++++++++++++++-----
 3 files changed, 106 insertions(+), 58 deletions(-)

diff --git a/core/src/main/java/bisq/core/api/model/PaymentAccountForm.java b/core/src/main/java/bisq/core/api/model/PaymentAccountForm.java
index 3906c97a236..5742de2cb3a 100644
--- a/core/src/main/java/bisq/core/api/model/PaymentAccountForm.java
+++ b/core/src/main/java/bisq/core/api/model/PaymentAccountForm.java
@@ -147,7 +147,9 @@ public File getPaymentAccountForm(String paymentMethodId) {
             String json = gson.toJson(paymentAccount); // serializes target to json
             outputStreamWriter.write(json);
         } catch (Exception ex) {
-            log.error(format("Could not export json file for a %s account.", paymentMethod.getShortName()), ex);
+            log.error(format("Could not write json file for a %s account.", paymentMethodId), ex);
+            throw new IllegalStateException(
+                    format("cannot create a payment account form for a %s payment method", paymentMethodId));
         }
         return file;
     }
@@ -158,6 +160,7 @@ public File getPaymentAccountForm(String paymentMethodId) {
      * @param jsonForm The file representing a new payment account form.
      * @return A populated PaymentAccount subclass instance.
      */
+    @SuppressWarnings("unused")
     @VisibleForTesting
     public PaymentAccount toPaymentAccount(File jsonForm) {
         String jsonString = toJsonString(jsonForm);
@@ -181,7 +184,7 @@ public String toJsonString(File jsonFile) {
             checkNotNull(jsonFile, "json file cannot be null");
             return new String(Files.readAllBytes(Paths.get(jsonFile.getAbsolutePath())));
         } catch (IOException ex) {
-            throw new IllegalStateException(format("Could not read content from file '%s'",
+            throw new IllegalStateException(format("cannot read content of file '%s'",
                     jsonFile.getAbsolutePath()), ex);
         }
     }
@@ -218,7 +221,7 @@ public static File getTmpJsonFile(String paymentMethodId) {
     private Class getPaymentAccountClassFromJson(String json) {
         Map jsonMap = gsonBuilder.create().fromJson(json, (Type) Object.class);
         String paymentMethodId = checkNotNull((String) jsonMap.get("paymentMethodId"),
-                format("Could not find a paymentMethodId in the json string: %s", json));
+                format("cannot not find a paymentMethodId in json string: %s", json));
         return getPaymentAccountClass(paymentMethodId);
     }
 
diff --git a/core/src/main/java/bisq/core/api/model/PaymentAccountTypeAdapter.java b/core/src/main/java/bisq/core/api/model/PaymentAccountTypeAdapter.java
index 151a4e0e782..fe5e81035a7 100644
--- a/core/src/main/java/bisq/core/api/model/PaymentAccountTypeAdapter.java
+++ b/core/src/main/java/bisq/core/api/model/PaymentAccountTypeAdapter.java
@@ -116,7 +116,7 @@ public void write(JsonWriter out, PaymentAccount account) throws IOException {
                 }
             } catch (Exception ex) {
                 throw new IllegalStateException(
-                        format("Could not serialize a %s to json", account.getClass().getSimpleName()), ex);
+                        format("cannot not serialize a %s", account.getClass().getSimpleName()), ex);
             }
         });
         out.endObject();
@@ -144,21 +144,16 @@ public PaymentAccount read(JsonReader in) throws IOException {
             if (didReadCountryField(in, account, currentFieldName))
                 continue;
 
-            try {
-                Optional field = fieldSettersMap.keySet().stream()
-                        .filter(k -> k.getName().equals(currentFieldName)).findFirst();
-
-                field.ifPresentOrElse((f) -> invokeSetterMethod(account, f, in), () -> {
-                    throw new IllegalStateException(
-                            format("Could not de-serialize json to a '%s' because there is no %s field.",
-                                    account.getClass().getSimpleName(),
-                                    currentFieldName));
-                });
-            } catch (Exception ex) {
+            Optional field = fieldSettersMap.keySet().stream()
+                    .filter(k -> k.getName().equals(currentFieldName)).findFirst();
+
+            field.ifPresentOrElse((f) -> invokeSetterMethod(account, f, in), () -> {
                 throw new IllegalStateException(
-                        format("Could not de-serialize json to a '%s'.",
-                                account.getClass().getSimpleName()), ex);
-            }
+                        format("programmer error: cannot de-serialize json to a '%s' "
+                                        + " because there is no %s field.",
+                                account.getClass().getSimpleName(),
+                                currentFieldName));
+            });
         }
         in.endObject();
         if (log.isDebugEnabled())
@@ -178,19 +173,20 @@ private void invokeSetterMethod(PaymentAccount account, Field field, JsonReader
                 } else if (isSetterOnPaymentAccountPayloadClass(setter.get(), account)) {
                     setter.get().invoke(account.getPaymentAccountPayload(), nextStringOrNull(jsonReader));
                 } else {
-                    String exMsg = format("Could not de-serialize json to a '%s' using reflection"
-                                    + " because the setter's declaring class was not found.",
+                    String exMsg = format("programmer error: cannot de-serialize json to a '%s' using reflection"
+                                    + " because the setter method's declaring class was not found.",
                             account.getClass().getSimpleName());
                     throw new IllegalStateException(exMsg);
                 }
             } catch (IllegalAccessException | InvocationTargetException ex) {
                 throw new IllegalStateException(
-                        format("Could not de-serialize json to a '%s' due to reflection error.",
+                        format("programmer error: cannot de-serialize json to a '%s' due to reflection error.",
                                 account.getClass().getSimpleName()), ex);
             }
         } else {
             throw new IllegalStateException(
-                    format("Could not de-serialize json to a '%s' because there is no setter for field %s.",
+                    format("programmer error: cannot de-serialize json to a '%s' "
+                                    + " because there is no setter method for field %s.",
                             account.getClass().getSimpleName(),
                             field.getName()));
         }
@@ -233,7 +229,9 @@ private String nextStringOrNull(JsonReader in) {
                 return in.nextString();
             }
         } catch (IOException ex) {
-            throw new IllegalStateException("Could not peek at next String value in JsonReader.", ex);
+            throw new IllegalStateException(
+                    "programmer error: cannot not peek at next string value in json reader.",
+                    ex);
         }
     }
 
@@ -247,7 +245,9 @@ private Long nextLongOrNull(JsonReader in) {
                 return in.nextLong();
             }
         } catch (IOException ex) {
-            throw new IllegalStateException("Could not peek at next Long value in JsonReader.", ex);
+            throw new IllegalStateException(
+                    "programmer error: cannot not peek at next long value in json reader.",
+                    ex);
         }
     }
 
@@ -290,9 +290,8 @@ private boolean didReadCountryField(JsonReader in, PaymentAccount account, Strin
                 account.setSingleTradeCurrency(fiatCurrency);
                 return true;
             } else {
-                throw new IllegalStateException(
-                        format("Could not de-serialize json to a '%s' because %s is an invalid country code.",
-                                account.getClass().getSimpleName(), countryCode));
+                throw new IllegalArgumentException(
+                        format("'%s' is an invalid country code.", countryCode));
             }
         } else {
             return false;
@@ -307,7 +306,7 @@ private Class getPaymentAccountPayloadType() {
                     + "." + paymentAccountType.getSimpleName() + "Payload");
         } catch (Exception ex) {
             throw new IllegalStateException(
-                    format("Could not get payload class for %s",
+                    format("programmer error: cannot get payload class for %s",
                             paymentAccountType.getSimpleName()), ex);
         }
     }
@@ -319,11 +318,15 @@ private PaymentAccount initNewPaymentAccount() {
             paymentAccount.init();
             return paymentAccount;
         } catch (NoSuchMethodException ex) {
-            throw new IllegalStateException(format("No default declared constructor  found for class %s",
-                    paymentAccountType.getSimpleName()), ex);
+            throw new IllegalStateException(
+                    format("programmer error: no default declared constructor  found for class %s",
+                            paymentAccountType.getSimpleName()),
+                    ex);
         } catch (IllegalAccessException | InstantiationException | InvocationTargetException ex) {
-            throw new IllegalStateException(format("Could not instantiate class %s",
-                    paymentAccountType.getSimpleName()), ex);
+            throw new IllegalStateException(
+                    format("programmer error: cannot instantiate class %s",
+                            paymentAccountType.getSimpleName()),
+                    ex);
         }
     }
 }
diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java
index d48499c85ff..0438d33655d 100644
--- a/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java
+++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java
@@ -31,6 +31,8 @@
 import bisq.proto.grpc.GetPaymentMethodsRequest;
 import bisq.proto.grpc.PaymentAccountsGrpc;
 
+import io.grpc.Status;
+import io.grpc.StatusRuntimeException;
 import io.grpc.stub.StreamObserver;
 
 import javax.inject.Inject;
@@ -50,46 +52,86 @@ public GrpcPaymentAccountsService(CoreApi coreApi) {
     @Override
     public void createPaymentAccount(CreatePaymentAccountRequest req,
                                      StreamObserver responseObserver) {
-        PaymentAccount paymentAccount = coreApi.createPaymentAccount(req.getPaymentAccountForm());
-        var reply = CreatePaymentAccountReply.newBuilder()
-                .setPaymentAccount(paymentAccount.toProtoMessage())
-                .build();
-        responseObserver.onNext(reply);
-        responseObserver.onCompleted();
+        try {
+            PaymentAccount paymentAccount = coreApi.createPaymentAccount(req.getPaymentAccountForm());
+            var reply = CreatePaymentAccountReply.newBuilder()
+                    .setPaymentAccount(paymentAccount.toProtoMessage())
+                    .build();
+            responseObserver.onNext(reply);
+            responseObserver.onCompleted();
+        } catch (IllegalArgumentException cause) {
+            var ex = new StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription(cause.getMessage()));
+            responseObserver.onError(ex);
+            throw ex;
+        } catch (IllegalStateException cause) {
+            var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
+            responseObserver.onError(ex);
+            throw ex;
+        }
     }
 
     @Override
     public void getPaymentAccounts(GetPaymentAccountsRequest req,
                                    StreamObserver responseObserver) {
-        var paymentAccounts = coreApi.getPaymentAccounts().stream()
-                .map(PaymentAccount::toProtoMessage)
-                .collect(Collectors.toList());
-        var reply = GetPaymentAccountsReply.newBuilder()
-                .addAllPaymentAccounts(paymentAccounts).build();
-        responseObserver.onNext(reply);
-        responseObserver.onCompleted();
+        try {
+            var paymentAccounts = coreApi.getPaymentAccounts().stream()
+                    .map(PaymentAccount::toProtoMessage)
+                    .collect(Collectors.toList());
+            var reply = GetPaymentAccountsReply.newBuilder()
+                    .addAllPaymentAccounts(paymentAccounts).build();
+            responseObserver.onNext(reply);
+            responseObserver.onCompleted();
+        } catch (IllegalArgumentException cause) {
+            var ex = new StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription(cause.getMessage()));
+            responseObserver.onError(ex);
+            throw ex;
+        } catch (IllegalStateException cause) {
+            var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
+            responseObserver.onError(ex);
+            throw ex;
+        }
     }
 
     @Override
     public void getPaymentMethods(GetPaymentMethodsRequest req,
                                   StreamObserver responseObserver) {
-        var paymentMethods = coreApi.getFiatPaymentMethods().stream()
-                .map(PaymentMethod::toProtoMessage)
-                .collect(Collectors.toList());
-        var reply = GetPaymentMethodsReply.newBuilder()
-                .addAllPaymentMethods(paymentMethods).build();
-        responseObserver.onNext(reply);
-        responseObserver.onCompleted();
+        try {
+            var paymentMethods = coreApi.getFiatPaymentMethods().stream()
+                    .map(PaymentMethod::toProtoMessage)
+                    .collect(Collectors.toList());
+            var reply = GetPaymentMethodsReply.newBuilder()
+                    .addAllPaymentMethods(paymentMethods).build();
+            responseObserver.onNext(reply);
+            responseObserver.onCompleted();
+        } catch (IllegalArgumentException cause) {
+            var ex = new StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription(cause.getMessage()));
+            responseObserver.onError(ex);
+            throw ex;
+        } catch (IllegalStateException cause) {
+            var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
+            responseObserver.onError(ex);
+            throw ex;
+        }
     }
 
     @Override
     public void getPaymentAccountForm(GetPaymentAccountFormRequest req,
                                       StreamObserver responseObserver) {
-        var paymentAccountFormJson = coreApi.getPaymentAccountForm(req.getPaymentMethodId());
-        var reply = GetPaymentAccountFormReply.newBuilder()
-                .setPaymentAccountFormJson(paymentAccountFormJson)
-                .build();
-        responseObserver.onNext(reply);
-        responseObserver.onCompleted();
+        try {
+            var paymentAccountFormJson = coreApi.getPaymentAccountForm(req.getPaymentMethodId());
+            var reply = GetPaymentAccountFormReply.newBuilder()
+                    .setPaymentAccountFormJson(paymentAccountFormJson)
+                    .build();
+            responseObserver.onNext(reply);
+            responseObserver.onCompleted();
+        } catch (IllegalArgumentException cause) {
+            var ex = new StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription(cause.getMessage()));
+            responseObserver.onError(ex);
+            throw ex;
+        } catch (IllegalStateException cause) {
+            var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
+            responseObserver.onError(ex);
+            throw ex;
+        }
     }
 }

From 58f1af6f45530c17827f2694ab237ba07b1719fd Mon Sep 17 00:00:00 2001
From: ghubstan <36207203+ghubstan@users.noreply.github.com>
Date: Fri, 20 Nov 2020 12:00:22 -0300
Subject: [PATCH 39/52] Log server stack traces, pass concise err msgs to CLI

For exceptions caught in the server, we want to log a reason and a
full stack trace, then send less verbose exception msgs up to the CLI.
---
 .../core/api/model/PaymentAccountForm.java    | 30 ++++++----
 .../api/model/PaymentAccountTypeAdapter.java  | 58 ++++++++++---------
 2 files changed, 52 insertions(+), 36 deletions(-)

diff --git a/core/src/main/java/bisq/core/api/model/PaymentAccountForm.java b/core/src/main/java/bisq/core/api/model/PaymentAccountForm.java
index 5742de2cb3a..b82656b4330 100644
--- a/core/src/main/java/bisq/core/api/model/PaymentAccountForm.java
+++ b/core/src/main/java/bisq/core/api/model/PaymentAccountForm.java
@@ -28,6 +28,8 @@
 
 import com.google.common.annotations.VisibleForTesting;
 
+import org.apache.commons.lang3.StringUtils;
+
 import java.net.URI;
 import java.net.URISyntaxException;
 
@@ -147,9 +149,9 @@ public File getPaymentAccountForm(String paymentMethodId) {
             String json = gson.toJson(paymentAccount); // serializes target to json
             outputStreamWriter.write(json);
         } catch (Exception ex) {
-            log.error(format("Could not write json file for a %s account.", paymentMethodId), ex);
-            throw new IllegalStateException(
-                    format("cannot create a payment account form for a %s payment method", paymentMethodId));
+            String errMsg = format("cannot create a payment account form for a %s payment method", paymentMethodId);
+            log.error(StringUtils.capitalize(errMsg) + ".", ex);
+            throw new IllegalStateException(errMsg);
         }
         return file;
     }
@@ -184,27 +186,32 @@ public String toJsonString(File jsonFile) {
             checkNotNull(jsonFile, "json file cannot be null");
             return new String(Files.readAllBytes(Paths.get(jsonFile.getAbsolutePath())));
         } catch (IOException ex) {
-            throw new IllegalStateException(format("cannot read content of file '%s'",
-                    jsonFile.getAbsolutePath()), ex);
+            String errMsg = format("cannot read json string from file '%s'",
+                    jsonFile.getAbsolutePath());
+            log.error(StringUtils.capitalize(errMsg) + ".", ex);
+            throw new IllegalStateException(errMsg);
         }
     }
 
     @VisibleForTesting
-    public URI getClickableURI(File jsonForm) {
+    public URI getClickableURI(File jsonFile) {
         try {
             return new URI("file",
                     "",
-                    jsonForm.toURI().getPath(),
+                    jsonFile.toURI().getPath(),
                     null,
                     null);
         } catch (URISyntaxException ex) {
-            throw new IllegalArgumentException("", ex);
+            String errMsg = format("cannot create clickable url to file '%s'",
+                    jsonFile.getAbsolutePath());
+            log.error(StringUtils.capitalize(errMsg) + ".", ex);
+            throw new IllegalStateException(errMsg);
         }
     }
 
     @VisibleForTesting
     public static File getTmpJsonFile(String paymentMethodId) {
-        File file = null;
+        File file;
         try {
             // Creates a tmp file that includes a random number string between the
             // prefix and suffix, i.e., sepa_form_13243546575879.json, so there is
@@ -213,7 +220,10 @@ public static File getTmpJsonFile(String paymentMethodId) {
                     ".json",
                     Paths.get(getProperty("java.io.tmpdir")).toFile());
         } catch (IOException ex) {
-            log.error("", ex);
+            String errMsg = format("cannot create json file for a %s payment method",
+                    paymentMethodId);
+            log.error(StringUtils.capitalize(errMsg) + ".", ex);
+            throw new IllegalStateException(errMsg);
         }
         return file;
     }
diff --git a/core/src/main/java/bisq/core/api/model/PaymentAccountTypeAdapter.java b/core/src/main/java/bisq/core/api/model/PaymentAccountTypeAdapter.java
index fe5e81035a7..e60d5c722a7 100644
--- a/core/src/main/java/bisq/core/api/model/PaymentAccountTypeAdapter.java
+++ b/core/src/main/java/bisq/core/api/model/PaymentAccountTypeAdapter.java
@@ -29,6 +29,8 @@
 import com.google.gson.stream.JsonToken;
 import com.google.gson.stream.JsonWriter;
 
+import org.apache.commons.lang3.StringUtils;
+
 import java.io.IOException;
 
 import java.util.ArrayList;
@@ -115,8 +117,10 @@ public void write(JsonWriter out, PaymentAccount account) throws IOException {
                     out.value("Your " + fieldName.toLowerCase());
                 }
             } catch (Exception ex) {
-                throw new IllegalStateException(
-                        format("cannot not serialize a %s", account.getClass().getSimpleName()), ex);
+                String errMsg = format("cannot create a new %s json form",
+                        account.getClass().getSimpleName());
+                log.error(StringUtils.capitalize(errMsg) + ".", ex);
+                throw new IllegalStateException("programmer error: " + errMsg);
             }
         });
         out.endObject();
@@ -173,15 +177,17 @@ private void invokeSetterMethod(PaymentAccount account, Field field, JsonReader
                 } else if (isSetterOnPaymentAccountPayloadClass(setter.get(), account)) {
                     setter.get().invoke(account.getPaymentAccountPayload(), nextStringOrNull(jsonReader));
                 } else {
-                    String exMsg = format("programmer error: cannot de-serialize json to a '%s' using reflection"
+                    String errMsg = format("programmer error: cannot de-serialize json to a '%s' using reflection"
                                     + " because the setter method's declaring class was not found.",
                             account.getClass().getSimpleName());
-                    throw new IllegalStateException(exMsg);
+                    throw new IllegalStateException(errMsg);
                 }
             } catch (IllegalAccessException | InvocationTargetException ex) {
-                throw new IllegalStateException(
-                        format("programmer error: cannot de-serialize json to a '%s' due to reflection error.",
-                                account.getClass().getSimpleName()), ex);
+                String errMsg = format("cannot set field value for %s on %s",
+                        field.getName(),
+                        account.getClass().getSimpleName());
+                log.error(StringUtils.capitalize(errMsg) + ".", ex);
+                throw new IllegalStateException("programmer error: " + errMsg);
             }
         } else {
             throw new IllegalStateException(
@@ -229,9 +235,9 @@ private String nextStringOrNull(JsonReader in) {
                 return in.nextString();
             }
         } catch (IOException ex) {
-            throw new IllegalStateException(
-                    "programmer error: cannot not peek at next string value in json reader.",
-                    ex);
+            String errMsg = "cannot see next string in json reader";
+            log.error(StringUtils.capitalize(errMsg) + ".", ex);
+            throw new IllegalStateException("programmer error: " + errMsg);
         }
     }
 
@@ -245,9 +251,9 @@ private Long nextLongOrNull(JsonReader in) {
                 return in.nextLong();
             }
         } catch (IOException ex) {
-            throw new IllegalStateException(
-                    "programmer error: cannot not peek at next long value in json reader.",
-                    ex);
+            String errMsg = "cannot see next long in json reader";
+            log.error(StringUtils.capitalize(errMsg) + ".", ex);
+            throw new IllegalStateException("programmer error: " + errMsg);
         }
     }
 
@@ -305,9 +311,11 @@ private Class getPaymentAccountPayloadType() {
             return (Class) Class.forName(pkg.getName()
                     + "." + paymentAccountType.getSimpleName() + "Payload");
         } catch (Exception ex) {
-            throw new IllegalStateException(
-                    format("programmer error: cannot get payload class for %s",
-                            paymentAccountType.getSimpleName()), ex);
+            String errMsg = format("cannot get the payload class for %s",
+                    paymentAccountType.getSimpleName());
+            log.error(StringUtils.capitalize(errMsg) + ".", ex);
+            throw new IllegalStateException("programmer error: " + errMsg);
+
         }
     }
 
@@ -317,16 +325,14 @@ private PaymentAccount initNewPaymentAccount() {
             PaymentAccount paymentAccount = (PaymentAccount) constructor.newInstance();
             paymentAccount.init();
             return paymentAccount;
-        } catch (NoSuchMethodException ex) {
-            throw new IllegalStateException(
-                    format("programmer error: no default declared constructor  found for class %s",
-                            paymentAccountType.getSimpleName()),
-                    ex);
-        } catch (IllegalAccessException | InstantiationException | InvocationTargetException ex) {
-            throw new IllegalStateException(
-                    format("programmer error: cannot instantiate class %s",
-                            paymentAccountType.getSimpleName()),
-                    ex);
+        } catch (NoSuchMethodException
+                | IllegalAccessException
+                | InstantiationException
+                | InvocationTargetException ex) {
+            String errMsg = format("cannot instantiate a new %s",
+                    paymentAccountType.getSimpleName());
+            log.error(StringUtils.capitalize(errMsg) + ".", ex);
+            throw new IllegalStateException("programmer error: " + errMsg);
         }
     }
 }

From 459b2a9c4983f9326301b2dcdff87109ce76d4aa Mon Sep 17 00:00:00 2001
From: ghubstan <36207203+ghubstan@users.noreply.github.com>
Date: Fri, 20 Nov 2020 16:46:36 -0300
Subject: [PATCH 40/52] Add create payment account (api) tests

---
 .../java/bisq/apitest/method/MethodTest.java  |  11 +-
 .../payment/AbstractPaymentAccountTest.java   |  87 +-
 .../payment/CreatePaymentAccountTest.java     | 788 ++++++++++++++----
 .../method/payment/GetPaymentMethodsTest.java |   4 +-
 .../apitest/scenario/PaymentAccountTest.java  |  46 +-
 5 files changed, 718 insertions(+), 218 deletions(-)

diff --git a/apitest/src/test/java/bisq/apitest/method/MethodTest.java b/apitest/src/test/java/bisq/apitest/method/MethodTest.java
index 03cb6cb0b3c..153f3c90c53 100644
--- a/apitest/src/test/java/bisq/apitest/method/MethodTest.java
+++ b/apitest/src/test/java/bisq/apitest/method/MethodTest.java
@@ -294,14 +294,17 @@ protected final bisq.core.payment.PaymentAccount createPaymentAccount(BisqAppCon
         return fromProto(paymentAccountsService.createPaymentAccount(req).getPaymentAccount());
     }
 
-    protected static PaymentAccount getDefaultPerfectDummyPaymentAccount(BisqAppConfig bisqAppConfig) {
+    protected static List getPaymentAccounts(BisqAppConfig bisqAppConfig) {
         var req = GetPaymentAccountsRequest.newBuilder().build();
-        var paymentAccountsService = grpcStubs(bisqAppConfig).paymentAccountsService;
-        PaymentAccount paymentAccount = paymentAccountsService.getPaymentAccounts(req)
+        return grpcStubs(bisqAppConfig).paymentAccountsService.getPaymentAccounts(req)
                 .getPaymentAccountsList()
                 .stream()
                 .sorted(comparing(PaymentAccount::getCreationDate))
-                .collect(Collectors.toList()).get(0);
+                .collect(Collectors.toList());
+    }
+
+    protected static PaymentAccount getDefaultPerfectDummyPaymentAccount(BisqAppConfig bisqAppConfig) {
+        PaymentAccount paymentAccount = getPaymentAccounts(bisqAppConfig).get(0);
         assertEquals("PerfectMoney dummy", paymentAccount.getAccountName());
         return paymentAccount;
     }
diff --git a/apitest/src/test/java/bisq/apitest/method/payment/AbstractPaymentAccountTest.java b/apitest/src/test/java/bisq/apitest/method/payment/AbstractPaymentAccountTest.java
index 1eb7f91c9a4..dfefd3d2828 100644
--- a/apitest/src/test/java/bisq/apitest/method/payment/AbstractPaymentAccountTest.java
+++ b/apitest/src/test/java/bisq/apitest/method/payment/AbstractPaymentAccountTest.java
@@ -2,6 +2,7 @@
 
 import bisq.core.api.model.PaymentAccountForm;
 import bisq.core.locale.Res;
+import bisq.core.locale.TradeCurrency;
 import bisq.core.payment.PaymentAccount;
 
 import bisq.proto.grpc.GetPaymentAccountsRequest;
@@ -18,6 +19,7 @@
 import java.io.OutputStreamWriter;
 
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 
@@ -30,10 +32,7 @@
 import static java.lang.String.format;
 import static java.lang.System.getProperty;
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
+import static org.junit.Assert.*;
 
 
 
@@ -47,9 +46,11 @@ public class AbstractPaymentAccountTest extends MethodTest {
 
     static final String PROPERTY_NAME_PAYMENT_METHOD_ID = "paymentMethodId";
 
+    static final String PROPERTY_NAME_ACCOUNT_ID = "accountId";
     static final String PROPERTY_NAME_ACCOUNT_NAME = "accountName";
     static final String PROPERTY_NAME_ACCOUNT_NR = "accountNr";
     static final String PROPERTY_NAME_ACCOUNT_TYPE = "accountType";
+    static final String PROPERTY_NAME_ANSWER = "answer";
     static final String PROPERTY_NAME_BANK_ACCOUNT_NAME = "bankAccountName";
     static final String PROPERTY_NAME_BANK_ACCOUNT_NUMBER = "bankAccountNumber";
     static final String PROPERTY_NAME_BANK_ACCOUNT_TYPE = "bankAccountType";
@@ -67,6 +68,7 @@ public class AbstractPaymentAccountTest extends MethodTest {
     static final String PROPERTY_NAME_EMAIL = "email";
     static final String PROPERTY_NAME_EMAIL_OR_MOBILE_NR = "emailOrMobileNr";
     static final String PROPERTY_NAME_EXTRA_INFO = "extraInfo";
+    static final String PROPERTY_NAME_HOLDER_EMAIL = "holderEmail";
     static final String PROPERTY_NAME_HOLDER_NAME = "holderName";
     static final String PROPERTY_NAME_HOLDER_TAX_ID = "holderTaxId";
     static final String PROPERTY_NAME_IBAN = "iban";
@@ -74,13 +76,19 @@ public class AbstractPaymentAccountTest extends MethodTest {
     static final String PROPERTY_NAME_NATIONAL_ACCOUNT_ID = "nationalAccountId";
     static final String PROPERTY_NAME_PAY_ID = "payid";
     static final String PROPERTY_NAME_POSTAL_ADDRESS = "postalAddress";
+    static final String PROPERTY_NAME_PROMPT_PAY_ID = "promptPayId";
+    static final String PROPERTY_NAME_QUESTION = "question";
+    static final String PROPERTY_NAME_REQUIREMENTS = "requirements";
+    static final String PROPERTY_NAME_SORT_CODE = "sortCode";
+    static final String PROPERTY_NAME_STATE = "state";
+    static final String PROPERTY_NAME_USERNAME = "userName";
 
     static final Gson GSON = new GsonBuilder()
             .setPrettyPrinting()
             .serializeNulls()
             .create();
 
-    static final Map EXPECTED_FORM = new HashMap<>();
+    static final Map COMPLETED_FORM_MAP = new HashMap<>();
 
     // A payment account serializer / deserializer.
     static final PaymentAccountForm PAYMENT_ACCOUNT_FORM = new PaymentAccountForm();
@@ -94,7 +102,7 @@ protected final File getEmptyForm(TestInfo testInfo, String paymentMethodId) {
         // This would normally be done in @BeforeEach, but these test cases might be
         // called from a single 'scenario' test case, and the @BeforeEach -> clear()
         // would be skipped.
-        EXPECTED_FORM.clear();
+        COMPLETED_FORM_MAP.clear();
 
         File emptyForm = getPaymentAccountForm(alicedaemon, paymentMethodId);
         // A short cut over the API:
@@ -121,7 +129,46 @@ protected final void verifyEmptyForm(File jsonForm, String paymentMethodId, Stri
         }
     }
 
-    protected final File fillPaymentAccountForm() {
+    protected final void verifyCommonFormEntries(PaymentAccount paymentAccount) {
+        // All PaymentAccount subclasses have paymentMethodId and an accountName fields.
+        assertNotNull(paymentAccount);
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_PAYMENT_METHOD_ID), paymentAccount.getPaymentMethod().getId());
+        assertTrue(paymentAccount.getCreationDate().getTime() > 0);
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NAME), paymentAccount.getAccountName());
+    }
+
+    protected final void verifyAccountSingleTradeCurrency(PaymentAccount paymentAccount,
+                                                          String expectedCurrencyCode) {
+        assertNotNull(paymentAccount.getSingleTradeCurrency());
+        assertEquals(expectedCurrencyCode, paymentAccount.getSingleTradeCurrency().getCode());
+    }
+
+    protected final void verifyAccountTradeCurrencies(List expectedTradeCurrencies,
+                                                      PaymentAccount paymentAccount) {
+        assertNotNull(paymentAccount.getTradeCurrencies());
+        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 paymentAccount = reply.getPaymentAccountsList().stream()
+                .filter(a -> a.getId().equals(paymentAccountId))
+                .findFirst();
+        assertTrue(paymentAccount.isPresent());
+    }
+
+    protected final String getCompletedFormAsJsonString() {
+        File completedForm = fillPaymentAccountForm();
+        String jsonString = PAYMENT_ACCOUNT_FORM.toJsonString(completedForm);
+        if (log.isDebugEnabled())
+            log.debug("Completed form: {}", jsonString);
+
+        return jsonString;
+    }
+
+    private File fillPaymentAccountForm() {
         File tmpJsonForm = null;
         try {
             tmpJsonForm = File.createTempFile("temp_acct_form_",
@@ -132,7 +179,7 @@ protected final File fillPaymentAccountForm() {
             writer.beginObject();
             writer.name(PROPERTY_NAME_COMMENT);
             writer.value(PROPERTY_VALUE_COMMENT);
-            for (Map.Entry entry : EXPECTED_FORM.entrySet()) {
+            for (Map.Entry entry : COMPLETED_FORM_MAP.entrySet()) {
                 String k = entry.getKey();
                 Object v = entry.getValue();
                 writer.name(k);
@@ -142,31 +189,9 @@ protected final File fillPaymentAccountForm() {
             writer.close();
         } catch (IOException ex) {
             log.error("", ex);
-            fail(format("Could not write json file from form entries %s", EXPECTED_FORM));
+            fail(format("Could not write json file from form entries %s", COMPLETED_FORM_MAP));
         }
         return tmpJsonForm;
     }
 
-    protected final void verifyCommonFormEntries(PaymentAccount paymentAccount) {
-        // All PaymentAccount subclasses have paymentMethodId and an accountName fields.
-        assertNotNull(paymentAccount);
-        assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_PAYMENT_METHOD_ID), paymentAccount.getPaymentMethod().getId());
-        assertTrue(paymentAccount.getCreationDate().getTime() > 0);
-        assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_ACCOUNT_NAME), paymentAccount.getAccountName());
-    }
-
-    protected final void verifyAccountFiatCurrency(PaymentAccount paymentAccount, String expectedCurrencyCode) {
-        assertNotNull(paymentAccount.getSingleTradeCurrency());
-        assertEquals(expectedCurrencyCode, paymentAccount.getSingleTradeCurrency().getCode());
-    }
-
-    protected final void verifyUserPayloadHasPaymentAccountWithId(String paymentAccountId) {
-        var getPaymentAccountsRequest = GetPaymentAccountsRequest.newBuilder().build();
-        var reply = grpcStubs(alicedaemon)
-                .paymentAccountsService.getPaymentAccounts(getPaymentAccountsRequest);
-        Optional paymentAccount = reply.getPaymentAccountsList().stream()
-                .filter(a -> a.getId().equals(paymentAccountId))
-                .findFirst();
-        assertTrue(paymentAccount.isPresent());
-    }
 }
diff --git a/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java b/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java
index 09dae2ba1d7..a861569405c 100644
--- a/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java
+++ b/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java
@@ -17,17 +17,38 @@
 
 package bisq.apitest.method.payment;
 
+import bisq.core.payment.AdvancedCashAccount;
+import bisq.core.payment.AliPayAccount;
 import bisq.core.payment.AustraliaPayid;
+import bisq.core.payment.CashDepositAccount;
 import bisq.core.payment.ChaseQuickPayAccount;
 import bisq.core.payment.ClearXchangeAccount;
 import bisq.core.payment.F2FAccount;
+import bisq.core.payment.FasterPaymentsAccount;
 import bisq.core.payment.HalCashAccount;
+import bisq.core.payment.InteracETransferAccount;
 import bisq.core.payment.JapanBankAccount;
+import bisq.core.payment.MoneyBeamAccount;
+import bisq.core.payment.MoneyGramAccount;
 import bisq.core.payment.NationalBankAccount;
+import bisq.core.payment.PerfectMoneyAccount;
+import bisq.core.payment.PopmoneyAccount;
+import bisq.core.payment.PromptPayAccount;
+import bisq.core.payment.RevolutAccount;
+import bisq.core.payment.SameBankAccount;
 import bisq.core.payment.SepaAccount;
+import bisq.core.payment.SepaInstantAccount;
+import bisq.core.payment.SpecificBanksAccount;
 import bisq.core.payment.SwishAccount;
+import bisq.core.payment.TransferwiseAccount;
 import bisq.core.payment.USPostalMoneyOrderAccount;
+import bisq.core.payment.UpholdAccount;
+import bisq.core.payment.WeChatPayAccount;
+import bisq.core.payment.WesternUnionAccount;
 import bisq.core.payment.payload.BankAccountPayload;
+import bisq.core.payment.payload.CashDepositAccountPayload;
+import bisq.core.payment.payload.SameBankAccountPayload;
+import bisq.core.payment.payload.SpecificBanksAccountPayload;
 
 import java.io.File;
 
@@ -38,13 +59,13 @@
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Disabled;
-import org.junit.jupiter.api.Order;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.TestInfo;
 import org.junit.jupiter.api.TestMethodOrder;
 
 import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind;
 import static bisq.apitest.config.BisqAppConfig.alicedaemon;
+import static bisq.core.locale.CurrencyUtil.*;
 import static bisq.core.payment.payload.PaymentMethod.*;
 import static org.junit.Assert.assertEquals;
 import static org.junit.jupiter.api.Assertions.fail;
@@ -65,35 +86,119 @@ public static void setUp() {
     }
 
     @Test
-    @Order(1)
+    public void testCreateAdvancedCashAccount(TestInfo testInfo) {
+        File emptyForm = getEmptyForm(testInfo, ADVANCED_CASH_ID);
+        verifyEmptyForm(emptyForm,
+                ADVANCED_CASH_ID,
+                PROPERTY_NAME_ACCOUNT_NR);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, ADVANCED_CASH_ID);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Advanced Cash Acct");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "0000 1111 2222");
+        String jsonString = getCompletedFormAsJsonString();
+        AdvancedCashAccount paymentAccount = (AdvancedCashAccount) createPaymentAccount(alicedaemon, jsonString);
+        verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
+        verifyAccountTradeCurrencies(getAllAdvancedCashCurrencies(), paymentAccount);
+        verifyCommonFormEntries(paymentAccount);
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr());
+        if (log.isDebugEnabled())
+            log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
+    }
+
+    @Test
+    public void testCreateAliPayAccount(TestInfo testInfo) {
+        File emptyForm = getEmptyForm(testInfo, ALI_PAY_ID);
+        verifyEmptyForm(emptyForm,
+                ALI_PAY_ID,
+                PROPERTY_NAME_ACCOUNT_NR);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, ALI_PAY_ID);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Ali Pay Acct");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "2222 3333 4444");
+        String jsonString = getCompletedFormAsJsonString();
+        AliPayAccount paymentAccount = (AliPayAccount) createPaymentAccount(alicedaemon, jsonString);
+        verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
+        verifyAccountSingleTradeCurrency(paymentAccount, "CNY");
+        verifyCommonFormEntries(paymentAccount);
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr());
+        if (log.isDebugEnabled())
+            log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
+    }
+
+    @Test
     public void testCreateAustraliaPayidAccount(TestInfo testInfo) {
         File emptyForm = getEmptyForm(testInfo, AUSTRALIA_PAYID_ID);
         verifyEmptyForm(emptyForm,
                 AUSTRALIA_PAYID_ID,
                 PROPERTY_NAME_BANK_ACCOUNT_NAME);
-
-        EXPECTED_FORM.put(PROPERTY_NAME_PAYMENT_METHOD_ID, AUSTRALIA_PAYID_ID);
-        EXPECTED_FORM.put(PROPERTY_NAME_ACCOUNT_NAME, "Australia Pay ID Account");
-        EXPECTED_FORM.put(PROPERTY_NAME_PAY_ID, "123 456 789");
-        EXPECTED_FORM.put(PROPERTY_NAME_BANK_ACCOUNT_NAME, "Credit Union Australia");
-
-        File completedForm = fillPaymentAccountForm();
-        String jsonString = PAYMENT_ACCOUNT_FORM.toJsonString(completedForm);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, AUSTRALIA_PAYID_ID);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Australia Pay ID Account");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAY_ID, "123 456 789");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_ACCOUNT_NAME, "Credit Union Australia");
+        String jsonString = getCompletedFormAsJsonString();
+        AustraliaPayid paymentAccount = (AustraliaPayid) createPaymentAccount(alicedaemon, jsonString);
+        verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
+        verifyAccountSingleTradeCurrency(paymentAccount, "AUD");
+        verifyCommonFormEntries(paymentAccount);
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_PAY_ID), paymentAccount.getPayid());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_ACCOUNT_NAME), paymentAccount.getBankAccountName());
         if (log.isDebugEnabled())
-            log.debug("Completed form: {}", jsonString);
+            log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
+    }
 
-        AustraliaPayid paymentAccount = (AustraliaPayid) createPaymentAccount(alicedaemon, jsonString);
+
+    @Test
+    public void testCreateCashDepositAccount(TestInfo testInfo) {
+        File emptyForm = getEmptyForm(testInfo, CASH_DEPOSIT_ID);
+        verifyEmptyForm(emptyForm,
+                CASH_DEPOSIT_ID,
+                PROPERTY_NAME_ACCOUNT_NR,
+                PROPERTY_NAME_ACCOUNT_TYPE,
+                PROPERTY_NAME_BANK_ID,
+                PROPERTY_NAME_BANK_NAME,
+                PROPERTY_NAME_BRANCH_ID,
+                PROPERTY_NAME_COUNTRY,
+                PROPERTY_NAME_HOLDER_EMAIL,
+                PROPERTY_NAME_HOLDER_NAME,
+                PROPERTY_NAME_HOLDER_TAX_ID,
+                PROPERTY_NAME_NATIONAL_ACCOUNT_ID,
+                PROPERTY_NAME_REQUIREMENTS);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, CASH_DEPOSIT_ID);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Cash Deposit Account");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "4444 5555 6666");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_TYPE, "Checking");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_ID, "0001");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_NAME, "BoF");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_BRANCH_ID, "99-8888-7654");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "FR");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_EMAIL, "jean@johnson.info");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jean Johnson");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_TAX_ID, "123456789");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_NATIONAL_ACCOUNT_ID, "123456789");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_REQUIREMENTS, "Requirements...");
+        String jsonString = getCompletedFormAsJsonString();
+        CashDepositAccount paymentAccount = (CashDepositAccount) createPaymentAccount(alicedaemon, jsonString);
         verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
-        verifyAccountFiatCurrency(paymentAccount, "AUD");
+        verifyAccountSingleTradeCurrency(paymentAccount, "EUR");
         verifyCommonFormEntries(paymentAccount);
-        assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_PAY_ID), paymentAccount.getPayid());
-        assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_BANK_ACCOUNT_NAME), paymentAccount.getBankAccountName());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
+                Objects.requireNonNull(paymentAccount.getCountry()).code);
+
+        CashDepositAccountPayload payload = (CashDepositAccountPayload) paymentAccount.getPaymentAccountPayload();
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), payload.getAccountNr());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_TYPE), payload.getAccountType());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_ID), payload.getBankId());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), payload.getBankName());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BRANCH_ID), payload.getBranchId());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_EMAIL), payload.getHolderEmail());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), payload.getHolderName());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_TAX_ID), payload.getHolderTaxId());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_NATIONAL_ACCOUNT_ID), payload.getNationalAccountId());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_REQUIREMENTS), payload.getRequirements());
         if (log.isDebugEnabled())
             log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
     }
 
     @Test
-    public void testBrazilNationalBankAccountForm(TestInfo testInfo) {
+    public void testCreateBrazilNationalBankAccount(TestInfo testInfo) {
         File emptyForm = getEmptyForm(testInfo, NATIONAL_BANK_ID);
         verifyEmptyForm(emptyForm,
                 NATIONAL_BANK_ID,
@@ -105,101 +210,83 @@ public void testBrazilNationalBankAccountForm(TestInfo testInfo) {
                 PROPERTY_NAME_HOLDER_NAME,
                 PROPERTY_NAME_HOLDER_TAX_ID,
                 PROPERTY_NAME_NATIONAL_ACCOUNT_ID);
-
-        EXPECTED_FORM.put(PROPERTY_NAME_PAYMENT_METHOD_ID, NATIONAL_BANK_ID);
-        EXPECTED_FORM.put(PROPERTY_NAME_ACCOUNT_NAME, "Banco do Brasil");
-        EXPECTED_FORM.put(PROPERTY_NAME_ACCOUNT_NR, "456789-87");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, NATIONAL_BANK_ID);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Banco do Brasil");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "456789-87");
         // No BankId is required for BR.
-        EXPECTED_FORM.put(PROPERTY_NAME_BANK_NAME, "Banco do Brasil");
-        EXPECTED_FORM.put(PROPERTY_NAME_BRANCH_ID, "456789-10");
-        EXPECTED_FORM.put(PROPERTY_NAME_COUNTRY, "BR");
-        EXPECTED_FORM.put(PROPERTY_NAME_HOLDER_NAME, "Joao da Silva");
-        EXPECTED_FORM.put(PROPERTY_NAME_HOLDER_TAX_ID, "123456789");
-        EXPECTED_FORM.put(PROPERTY_NAME_NATIONAL_ACCOUNT_ID, "123456789");
-
-        File completedForm = fillPaymentAccountForm();
-        String jsonString = PAYMENT_ACCOUNT_FORM.toJsonString(completedForm);
-        if (log.isDebugEnabled())
-            log.debug("Completed form: {}", jsonString);
-
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_NAME, "Banco do Brasil");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_BRANCH_ID, "456789-10");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "BR");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Joao da Silva");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_TAX_ID, "123456789");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_NATIONAL_ACCOUNT_ID, "123456789");
+        String jsonString = getCompletedFormAsJsonString();
         NationalBankAccount paymentAccount = (NationalBankAccount) createPaymentAccount(alicedaemon, jsonString);
         verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
-        verifyAccountFiatCurrency(paymentAccount, "BRL");
+        verifyAccountSingleTradeCurrency(paymentAccount, "BRL");
         verifyCommonFormEntries(paymentAccount);
-        assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_COUNTRY),
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
                 Objects.requireNonNull(paymentAccount.getCountry()).code);
 
         BankAccountPayload payload = (BankAccountPayload) paymentAccount.getPaymentAccountPayload();
-        assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_ACCOUNT_NR), payload.getAccountNr());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), payload.getAccountNr());
         // When no BankId is required, getBankId() returns bankName.
-        assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_BANK_NAME), payload.getBankId());
-        assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_BANK_NAME), payload.getBankName());
-        assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_BRANCH_ID), payload.getBranchId());
-        assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_HOLDER_NAME), payload.getHolderName());
-        assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_HOLDER_TAX_ID), payload.getHolderTaxId());
-        assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_NATIONAL_ACCOUNT_ID), payload.getNationalAccountId());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), payload.getBankId());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), payload.getBankName());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BRANCH_ID), payload.getBranchId());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), payload.getHolderName());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_TAX_ID), payload.getHolderTaxId());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_NATIONAL_ACCOUNT_ID), payload.getNationalAccountId());
         if (log.isDebugEnabled())
             log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
     }
 
     @Test
-    public void testChaseQuickPayAccountForm(TestInfo testInfo) {
+    public void testCreateChaseQuickPayAccount(TestInfo testInfo) {
         File emptyForm = getEmptyForm(testInfo, CHASE_QUICK_PAY_ID);
         verifyEmptyForm(emptyForm,
                 CHASE_QUICK_PAY_ID,
                 PROPERTY_NAME_EMAIL,
                 PROPERTY_NAME_HOLDER_NAME);
-
-        EXPECTED_FORM.put(PROPERTY_NAME_PAYMENT_METHOD_ID, CHASE_QUICK_PAY_ID);
-        EXPECTED_FORM.put(PROPERTY_NAME_ACCOUNT_NAME, "Quick Pay Acct");
-        EXPECTED_FORM.put(PROPERTY_NAME_EMAIL, "johndoe@quickpay.com");
-        EXPECTED_FORM.put(PROPERTY_NAME_HOLDER_NAME, "John Doe");
-
-        File completedForm = fillPaymentAccountForm();
-        String jsonString = PAYMENT_ACCOUNT_FORM.toJsonString(completedForm);
-        if (log.isDebugEnabled())
-            log.debug("Completed form: {}", jsonString);
-
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, CHASE_QUICK_PAY_ID);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Quick Pay Acct");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "johndoe@quickpay.com");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "John Doe");
+        String jsonString = getCompletedFormAsJsonString();
         ChaseQuickPayAccount paymentAccount = (ChaseQuickPayAccount) createPaymentAccount(alicedaemon, jsonString);
         verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
-        verifyAccountFiatCurrency(paymentAccount, "USD");
+        verifyAccountSingleTradeCurrency(paymentAccount, "USD");
         verifyCommonFormEntries(paymentAccount);
-        assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail());
-        assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
         if (log.isDebugEnabled())
             log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
     }
 
     @Test
-    public void testClearXChangeAccountForm(TestInfo testInfo) {
+    public void testCreateClearXChangeAccount(TestInfo testInfo) {
         File emptyForm = getEmptyForm(testInfo, CLEAR_X_CHANGE_ID);
         verifyEmptyForm(emptyForm,
                 CLEAR_X_CHANGE_ID,
                 PROPERTY_NAME_EMAIL_OR_MOBILE_NR,
                 PROPERTY_NAME_HOLDER_NAME);
-
-        EXPECTED_FORM.put(PROPERTY_NAME_PAYMENT_METHOD_ID, CLEAR_X_CHANGE_ID);
-        EXPECTED_FORM.put(PROPERTY_NAME_ACCOUNT_NAME, "USD Zelle Account");
-        EXPECTED_FORM.put(PROPERTY_NAME_EMAIL_OR_MOBILE_NR, "jane@doe.com");
-        EXPECTED_FORM.put(PROPERTY_NAME_HOLDER_NAME, "Jane Doe");
-
-        File completedForm = fillPaymentAccountForm();
-        String jsonString = PAYMENT_ACCOUNT_FORM.toJsonString(completedForm);
-        if (log.isDebugEnabled())
-            log.debug("Completed form: {}", jsonString);
-
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, CLEAR_X_CHANGE_ID);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "USD Zelle Account");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL_OR_MOBILE_NR, "jane@doe.com");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jane Doe");
+        String jsonString = getCompletedFormAsJsonString();
         ClearXchangeAccount paymentAccount = (ClearXchangeAccount) createPaymentAccount(alicedaemon, jsonString);
         verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
-        verifyAccountFiatCurrency(paymentAccount, "USD");
+        verifyAccountSingleTradeCurrency(paymentAccount, "USD");
         verifyCommonFormEntries(paymentAccount);
-        assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_EMAIL_OR_MOBILE_NR), paymentAccount.getEmailOrMobileNr());
-        assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL_OR_MOBILE_NR), paymentAccount.getEmailOrMobileNr());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
         if (log.isDebugEnabled())
             log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
     }
 
     @Test
-    public void testF2FAccountForm(TestInfo testInfo) {
+    public void testCreateF2FAccount(TestInfo testInfo) {
         File emptyForm = getEmptyForm(testInfo, F2F_ID);
         verifyEmptyForm(emptyForm,
                 F2F_ID,
@@ -207,60 +294,97 @@ public void testF2FAccountForm(TestInfo testInfo) {
                 PROPERTY_NAME_CITY,
                 PROPERTY_NAME_CONTACT,
                 PROPERTY_NAME_EXTRA_INFO);
-
-
-        EXPECTED_FORM.put(PROPERTY_NAME_PAYMENT_METHOD_ID, F2F_ID);
-        EXPECTED_FORM.put(PROPERTY_NAME_ACCOUNT_NAME, "Conta Cara a Cara");
-        EXPECTED_FORM.put(PROPERTY_NAME_COUNTRY, "BR");
-        EXPECTED_FORM.put(PROPERTY_NAME_CITY, "Rio de Janeiro");
-        EXPECTED_FORM.put(PROPERTY_NAME_CONTACT, "Freddy Beira Mar");
-        EXPECTED_FORM.put(PROPERTY_NAME_EXTRA_INFO, "So fim de semana");
-
-        File completedForm = fillPaymentAccountForm();
-        String jsonString = PAYMENT_ACCOUNT_FORM.toJsonString(completedForm);
-        if (log.isDebugEnabled())
-            log.debug("Completed form: {}", jsonString);
-
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, F2F_ID);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Conta Cara a Cara");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "BR");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_CITY, "Rio de Janeiro");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_CONTACT, "Freddy Beira Mar");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_EXTRA_INFO, "So fim de semana");
+        String jsonString = getCompletedFormAsJsonString();
         F2FAccount paymentAccount = (F2FAccount) createPaymentAccount(alicedaemon, jsonString);
         verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
-        verifyAccountFiatCurrency(paymentAccount, "BRL");
+        verifyAccountSingleTradeCurrency(paymentAccount, "BRL");
         verifyCommonFormEntries(paymentAccount);
-        assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_COUNTRY),
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
                 Objects.requireNonNull(paymentAccount.getCountry()).code);
-        assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_CITY), paymentAccount.getCity());
-        assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_CONTACT), paymentAccount.getContact());
-        assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_EXTRA_INFO), paymentAccount.getExtraInfo());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_CITY), paymentAccount.getCity());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_CONTACT), paymentAccount.getContact());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EXTRA_INFO), paymentAccount.getExtraInfo());
         if (log.isDebugEnabled())
             log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
     }
 
     @Test
-    public void testHalCashAccountForm(TestInfo testInfo) {
+    public void testCreateFasterPaymentsAccount(TestInfo testInfo) {
+        File emptyForm = getEmptyForm(testInfo, FASTER_PAYMENTS_ID);
+        verifyEmptyForm(emptyForm,
+                FASTER_PAYMENTS_ID,
+                PROPERTY_NAME_ACCOUNT_NR,
+                PROPERTY_NAME_SORT_CODE);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, FASTER_PAYMENTS_ID);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Faster Payments Acct");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "9999 8888 7777");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_SORT_CODE, "3127");
+        String jsonString = getCompletedFormAsJsonString();
+        FasterPaymentsAccount paymentAccount = (FasterPaymentsAccount) createPaymentAccount(alicedaemon, jsonString);
+        verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
+        verifyAccountSingleTradeCurrency(paymentAccount, "GBP");
+        verifyCommonFormEntries(paymentAccount);
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SORT_CODE), paymentAccount.getSortCode());
+        if (log.isDebugEnabled())
+            log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
+    }
+
+    @Test
+    public void testCreateHalCashAccount(TestInfo testInfo) {
         File emptyForm = getEmptyForm(testInfo, HAL_CASH_ID);
         verifyEmptyForm(emptyForm,
                 HAL_CASH_ID,
                 PROPERTY_NAME_MOBILE_NR);
-
-        EXPECTED_FORM.put(PROPERTY_NAME_PAYMENT_METHOD_ID, HAL_CASH_ID);
-        EXPECTED_FORM.put(PROPERTY_NAME_ACCOUNT_NAME, "Hal Cash Acct");
-        EXPECTED_FORM.put(PROPERTY_NAME_MOBILE_NR, "798 123 456");
-
-        File completedForm = fillPaymentAccountForm();
-        String jsonString = PAYMENT_ACCOUNT_FORM.toJsonString(completedForm);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, HAL_CASH_ID);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Hal Cash Acct");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_MOBILE_NR, "798 123 456");
+        String jsonString = getCompletedFormAsJsonString();
+        HalCashAccount paymentAccount = (HalCashAccount) createPaymentAccount(alicedaemon, jsonString);
+        verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
+        verifyAccountSingleTradeCurrency(paymentAccount, "EUR");
+        verifyCommonFormEntries(paymentAccount);
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_MOBILE_NR), paymentAccount.getMobileNr());
         if (log.isDebugEnabled())
-            log.debug("Completed form: {}", jsonString);
+            log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
+    }
 
-        HalCashAccount paymentAccount = (HalCashAccount) createPaymentAccount(alicedaemon, jsonString);
+    @Test
+    public void testCreateInteracETransferAccount(TestInfo testInfo) {
+        File emptyForm = getEmptyForm(testInfo, INTERAC_E_TRANSFER_ID);
+        verifyEmptyForm(emptyForm,
+                INTERAC_E_TRANSFER_ID,
+                PROPERTY_NAME_HOLDER_NAME,
+                PROPERTY_NAME_EMAIL,
+                PROPERTY_NAME_QUESTION,
+                PROPERTY_NAME_ANSWER);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, INTERAC_E_TRANSFER_ID);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Interac Transfer Acct");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "John Doe");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "john@doe.info");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_QUESTION, "What is my dog's name?");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ANSWER, "Fido");
+        String jsonString = getCompletedFormAsJsonString();
+        InteracETransferAccount paymentAccount = (InteracETransferAccount) createPaymentAccount(alicedaemon, jsonString);
         verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
-        verifyAccountFiatCurrency(paymentAccount, "EUR");
+        verifyAccountSingleTradeCurrency(paymentAccount, "CAD");
         verifyCommonFormEntries(paymentAccount);
-        assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_MOBILE_NR), paymentAccount.getMobileNr());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_QUESTION), paymentAccount.getQuestion());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ANSWER), paymentAccount.getAnswer());
         if (log.isDebugEnabled())
             log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
     }
 
     @Test
-    public void testJapanBankAccountForm(TestInfo testInfo) {
+    public void testCreateJapanBankAccount(TestInfo testInfo) {
         File emptyForm = getEmptyForm(testInfo, JAPAN_BANK_ID);
         verifyEmptyForm(emptyForm,
                 JAPAN_BANK_ID,
@@ -271,125 +395,445 @@ public void testJapanBankAccountForm(TestInfo testInfo) {
                 PROPERTY_NAME_BANK_ACCOUNT_NAME,
                 PROPERTY_NAME_BANK_ACCOUNT_TYPE,
                 PROPERTY_NAME_BANK_ACCOUNT_NUMBER);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, JAPAN_BANK_ID);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Fukuoka Account");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_NAME, "Bank of Kyoto");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_CODE, "FKBKJPJT");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_BRANCH_CODE, "8100-8727");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_BRANCH_NAME, "Fukuoka Branch");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_ACCOUNT_NAME, "Fukuoka Account");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_ACCOUNT_TYPE, "Yen Account");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_ACCOUNT_NUMBER, "8100-8727-0000");
+        String jsonString = getCompletedFormAsJsonString();
+        JapanBankAccount paymentAccount = (JapanBankAccount) createPaymentAccount(alicedaemon, jsonString);
+        verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
+        verifyAccountSingleTradeCurrency(paymentAccount, "JPY");
+        verifyCommonFormEntries(paymentAccount);
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_CODE), paymentAccount.getBankCode());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), paymentAccount.getBankName());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_BRANCH_CODE), paymentAccount.getBankBranchCode());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_BRANCH_NAME), paymentAccount.getBankBranchName());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_ACCOUNT_NAME), paymentAccount.getBankAccountName());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_ACCOUNT_TYPE), paymentAccount.getBankAccountType());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_ACCOUNT_NUMBER), paymentAccount.getBankAccountNumber());
+        if (log.isDebugEnabled())
+            log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
+    }
 
-        EXPECTED_FORM.put(PROPERTY_NAME_PAYMENT_METHOD_ID, JAPAN_BANK_ID);
-        EXPECTED_FORM.put(PROPERTY_NAME_ACCOUNT_NAME, "Fukuoka Account");
-        EXPECTED_FORM.put(PROPERTY_NAME_BANK_NAME, "Bank of Kyoto");
-        EXPECTED_FORM.put(PROPERTY_NAME_BANK_CODE, "FKBKJPJT");
-        EXPECTED_FORM.put(PROPERTY_NAME_BANK_BRANCH_CODE, "8100-8727");
-        EXPECTED_FORM.put(PROPERTY_NAME_BANK_BRANCH_NAME, "Fukuoka Branch");
-        EXPECTED_FORM.put(PROPERTY_NAME_BANK_ACCOUNT_NAME, "Fukuoka Account");
-        EXPECTED_FORM.put(PROPERTY_NAME_BANK_ACCOUNT_TYPE, "Yen Account");
-        EXPECTED_FORM.put(PROPERTY_NAME_BANK_ACCOUNT_NUMBER, "8100-8727-0000");
-
-        File completedForm = fillPaymentAccountForm();
-        String jsonString = PAYMENT_ACCOUNT_FORM.toJsonString(completedForm);
+    @Test
+    public void testCreateMoneyBeamAccount(TestInfo testInfo) {
+        File emptyForm = getEmptyForm(testInfo, MONEY_BEAM_ID);
+        verifyEmptyForm(emptyForm,
+                MONEY_BEAM_ID,
+                PROPERTY_NAME_ACCOUNT_ID);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, MONEY_BEAM_ID);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Money Beam Acct");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_ID, "MB 0000 1111");
+        String jsonString = getCompletedFormAsJsonString();
+        MoneyBeamAccount paymentAccount = (MoneyBeamAccount) createPaymentAccount(alicedaemon, jsonString);
+        verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
+        verifyAccountSingleTradeCurrency(paymentAccount, "EUR");
+        verifyCommonFormEntries(paymentAccount);
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_ID), paymentAccount.getAccountId());
         if (log.isDebugEnabled())
-            log.debug("Completed form: {}", jsonString);
+            log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
+    }
 
-        JapanBankAccount paymentAccount = (JapanBankAccount) createPaymentAccount(alicedaemon, jsonString);
+    @Disabled
+    @Test
+    public void testCreateMoneyGramAccount(TestInfo testInfo) {
+        // TODO Check the MoneyGramAccount class, it seems it should be a subclass
+        //   of CountryBasedPaymentAccount, and these setters seem wrong:
+        //      setState(email)
+        //      setHolderName(email)
+        // This is the only test that is breaking as of Fri 20 Nov 2020 03:18:34 PM -03.
+        File emptyForm = getEmptyForm(testInfo, MONEY_GRAM_ID);
+        verifyEmptyForm(emptyForm,
+                MONEY_GRAM_ID,
+                PROPERTY_NAME_HOLDER_NAME,
+                PROPERTY_NAME_EMAIL,
+                PROPERTY_NAME_COUNTRY,
+                PROPERTY_NAME_STATE);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, MONEY_GRAM_ID);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Money Gram Acct");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "John Doe");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "john@doe.info");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "US");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_STATE, "NY");
+        String jsonString = getCompletedFormAsJsonString();
+        MoneyGramAccount paymentAccount = (MoneyGramAccount) createPaymentAccount(alicedaemon, jsonString);
         verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
-        verifyAccountFiatCurrency(paymentAccount, "JPY");
+        verifyAccountTradeCurrencies(getAllMoneyGramCurrencies(), paymentAccount);
         verifyCommonFormEntries(paymentAccount);
-        assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_BANK_CODE), paymentAccount.getBankCode());
-        assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_BANK_NAME), paymentAccount.getBankName());
-        assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_BANK_BRANCH_CODE), paymentAccount.getBankBranchCode());
-        assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_BANK_BRANCH_NAME), paymentAccount.getBankBranchName());
-        assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_BANK_ACCOUNT_NAME), paymentAccount.getBankAccountName());
-        assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_BANK_ACCOUNT_TYPE), paymentAccount.getBankAccountType());
-        assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_BANK_ACCOUNT_NUMBER), paymentAccount.getBankAccountNumber());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getFullName());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY), paymentAccount.getCountry());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_STATE), paymentAccount.getState());
         if (log.isDebugEnabled())
             log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
     }
 
     @Test
-    public void testSepaAccountForm(TestInfo testInfo) {
-        File emptyForm = getEmptyForm(testInfo, SEPA_ID);
+    public void testCreatePerfectMoneyAccount(TestInfo testInfo) {
+        File emptyForm = getEmptyForm(testInfo, PERFECT_MONEY_ID);
         verifyEmptyForm(emptyForm,
-                SEPA_ID,
+                PERFECT_MONEY_ID,
+                PROPERTY_NAME_ACCOUNT_NR);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, PERFECT_MONEY_ID);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Perfect Money Acct");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "PM 0000 1111");
+        String jsonString = getCompletedFormAsJsonString();
+        PerfectMoneyAccount paymentAccount = (PerfectMoneyAccount) createPaymentAccount(alicedaemon, jsonString);
+        verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
+        verifyAccountSingleTradeCurrency(paymentAccount, "USD");
+        verifyCommonFormEntries(paymentAccount);
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr());
+        if (log.isDebugEnabled())
+            log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
+    }
+
+    @Test
+    public void testCreatePopmoneyAccount(TestInfo testInfo) {
+        File emptyForm = getEmptyForm(testInfo, POPMONEY_ID);
+        verifyEmptyForm(emptyForm,
+                POPMONEY_ID,
+                PROPERTY_NAME_ACCOUNT_ID,
+                PROPERTY_NAME_HOLDER_NAME);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, POPMONEY_ID);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Pop Money Acct");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_ID, "POPMONEY 0000 1111");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jane Doe");
+        String jsonString = getCompletedFormAsJsonString();
+        PopmoneyAccount paymentAccount = (PopmoneyAccount) createPaymentAccount(alicedaemon, jsonString);
+        verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
+        verifyAccountSingleTradeCurrency(paymentAccount, "USD");
+        verifyCommonFormEntries(paymentAccount);
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_ID), paymentAccount.getAccountId());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
+        if (log.isDebugEnabled())
+            log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
+    }
+
+    @Test
+    public void testCreatePromptPayAccount(TestInfo testInfo) {
+        File emptyForm = getEmptyForm(testInfo, PROMPT_PAY_ID);
+        verifyEmptyForm(emptyForm,
+                PROMPT_PAY_ID,
+                PROPERTY_NAME_PROMPT_PAY_ID);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, PROMPT_PAY_ID);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Prompt Pay Acct");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_PROMPT_PAY_ID, "PP 0000 1111");
+        String jsonString = getCompletedFormAsJsonString();
+        PromptPayAccount paymentAccount = (PromptPayAccount) createPaymentAccount(alicedaemon, jsonString);
+        verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
+        verifyAccountSingleTradeCurrency(paymentAccount, "THB");
+        verifyCommonFormEntries(paymentAccount);
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_PROMPT_PAY_ID), paymentAccount.getPromptPayId());
+        if (log.isDebugEnabled())
+            log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
+    }
+
+    @Test
+    public void testCreateRevolutAccount(TestInfo testInfo) {
+        File emptyForm = getEmptyForm(testInfo, REVOLUT_ID);
+        verifyEmptyForm(emptyForm,
+                REVOLUT_ID,
+                PROPERTY_NAME_USERNAME);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, REVOLUT_ID);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Revolut Acct");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_USERNAME, "revolut123");
+        String jsonString = getCompletedFormAsJsonString();
+        RevolutAccount paymentAccount = (RevolutAccount) createPaymentAccount(alicedaemon, jsonString);
+        verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
+        verifyAccountTradeCurrencies(getAllRevolutCurrencies(), paymentAccount);
+        verifyCommonFormEntries(paymentAccount);
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_USERNAME), paymentAccount.getUserName());
+        if (log.isDebugEnabled())
+            log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
+    }
+
+    @Test
+    public void testCreateSameBankAccount(TestInfo testInfo) {
+        File emptyForm = getEmptyForm(testInfo, SAME_BANK_ID);
+        verifyEmptyForm(emptyForm,
+                SAME_BANK_ID,
+                PROPERTY_NAME_ACCOUNT_NR,
+                PROPERTY_NAME_ACCOUNT_TYPE,
+                PROPERTY_NAME_BANK_NAME,
+                PROPERTY_NAME_BRANCH_ID,
+                PROPERTY_NAME_COUNTRY,
+                PROPERTY_NAME_HOLDER_NAME,
+                PROPERTY_NAME_HOLDER_TAX_ID,
+                PROPERTY_NAME_NATIONAL_ACCOUNT_ID);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, SAME_BANK_ID);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Same Bank Acct");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "000 1 4567");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_TYPE, "Checking");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_NAME, "HSBC");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_BRANCH_ID, "111");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "GB");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jane Doe");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_TAX_ID, "123456789");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_NATIONAL_ACCOUNT_ID, "123456789");
+        String jsonString = getCompletedFormAsJsonString();
+        SameBankAccount paymentAccount = (SameBankAccount) createPaymentAccount(alicedaemon, jsonString);
+        verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
+        verifyAccountSingleTradeCurrency(paymentAccount, "GBP");
+        verifyCommonFormEntries(paymentAccount);
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
+                Objects.requireNonNull(paymentAccount.getCountry()).code);
+        SameBankAccountPayload payload = (SameBankAccountPayload) paymentAccount.getPaymentAccountPayload();
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), payload.getAccountNr());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_TYPE), payload.getAccountType());
+        // The bankId == bankName because bank id is not required in the UK.
+        assertEquals(payload.getBankId(), payload.getBankName());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), payload.getBankName());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BRANCH_ID), payload.getBranchId());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), payload.getHolderName());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_TAX_ID), payload.getHolderTaxId());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_NATIONAL_ACCOUNT_ID), payload.getNationalAccountId());
+        if (log.isDebugEnabled())
+            log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
+    }
+
+    @Test
+    public void testCreateSepaInstantAccount(TestInfo testInfo) {
+        File emptyForm = getEmptyForm(testInfo, SEPA_INSTANT_ID);
+        verifyEmptyForm(emptyForm,
+                SEPA_INSTANT_ID,
                 PROPERTY_NAME_COUNTRY,
                 PROPERTY_NAME_HOLDER_NAME,
                 PROPERTY_NAME_IBAN,
                 PROPERTY_NAME_BIC);
-
-        EXPECTED_FORM.put(PROPERTY_NAME_PAYMENT_METHOD_ID, SEPA_ID);
-        EXPECTED_FORM.put(PROPERTY_NAME_ACCOUNT_NAME, "Conta Sepa");
-        EXPECTED_FORM.put(PROPERTY_NAME_COUNTRY, "PT");
-        EXPECTED_FORM.put(PROPERTY_NAME_HOLDER_NAME, "Jose da Silva");
-        EXPECTED_FORM.put(PROPERTY_NAME_IBAN, "909-909");
-        EXPECTED_FORM.put(PROPERTY_NAME_BIC, "909");
-
-        File completedForm = fillPaymentAccountForm();
-        String jsonString = PAYMENT_ACCOUNT_FORM.toJsonString(completedForm);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, SEPA_INSTANT_ID);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Conta Sepa Instant");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "PT");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jose da Silva");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_IBAN, "909-909");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_BIC, "909");
+        String jsonString = getCompletedFormAsJsonString();
+        SepaInstantAccount paymentAccount = (SepaInstantAccount) createPaymentAccount(alicedaemon, jsonString);
+        verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
+                Objects.requireNonNull(paymentAccount.getCountry()).code);
+        verifyAccountSingleTradeCurrency(paymentAccount, "EUR");
+        verifyCommonFormEntries(paymentAccount);
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_IBAN), paymentAccount.getIban());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BIC), paymentAccount.getBic());
+        // bankId == bic
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BIC), paymentAccount.getBankId());
         if (log.isDebugEnabled())
-            log.debug("Completed form: {}", jsonString);
+            log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
+    }
 
+    @Test
+    public void testCreateSepaAccount(TestInfo testInfo) {
+        File emptyForm = getEmptyForm(testInfo, SEPA_ID);
+        verifyEmptyForm(emptyForm,
+                SEPA_ID,
+                PROPERTY_NAME_COUNTRY,
+                PROPERTY_NAME_HOLDER_NAME,
+                PROPERTY_NAME_IBAN,
+                PROPERTY_NAME_BIC);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, SEPA_ID);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Conta Sepa");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "PT");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jose da Silva");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_IBAN, "909-909");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_BIC, "909");
+        String jsonString = getCompletedFormAsJsonString();
         SepaAccount paymentAccount = (SepaAccount) createPaymentAccount(alicedaemon, jsonString);
         verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
-        assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_COUNTRY), Objects.requireNonNull(paymentAccount.getCountry()).code);
-        verifyAccountFiatCurrency(paymentAccount, "EUR");
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
+                Objects.requireNonNull(paymentAccount.getCountry()).code);
+        verifyAccountSingleTradeCurrency(paymentAccount, "EUR");
         verifyCommonFormEntries(paymentAccount);
-        assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
-        assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_IBAN), paymentAccount.getIban());
-        assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_BIC), paymentAccount.getBic());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_IBAN), paymentAccount.getIban());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BIC), paymentAccount.getBic());
         // bankId == bic
-        assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_BIC), paymentAccount.getBankId());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BIC), paymentAccount.getBankId());
         if (log.isDebugEnabled())
             log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
     }
 
     @Test
-    public void testSwishAccountForm(TestInfo testInfo) {
+    public void testCreateSpecificBanksAccount(TestInfo testInfo) {
+        // TODO Supporting set of accepted banks may require some refactoring
+        //  of the SpecificBanksAccount and SpecificBanksAccountPayload classes, i.e.,
+        //      public void setAcceptedBanks(String... bankNames) { ... }
+        File emptyForm = getEmptyForm(testInfo, SPECIFIC_BANKS_ID);
+        verifyEmptyForm(emptyForm,
+                SPECIFIC_BANKS_ID,
+                PROPERTY_NAME_ACCOUNT_NR,
+                PROPERTY_NAME_ACCOUNT_TYPE,
+                PROPERTY_NAME_BANK_NAME,
+                PROPERTY_NAME_BRANCH_ID,
+                PROPERTY_NAME_COUNTRY,
+                PROPERTY_NAME_HOLDER_NAME,
+                PROPERTY_NAME_HOLDER_TAX_ID,
+                PROPERTY_NAME_NATIONAL_ACCOUNT_ID);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, SPECIFIC_BANKS_ID);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Specific Banks Acct");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "000 1 4567");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_TYPE, "Checking");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_NAME, "HSBC");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_BRANCH_ID, "111");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "GB");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jane Doe");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_TAX_ID, "123456789");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_NATIONAL_ACCOUNT_ID, "123456789");
+        String jsonString = getCompletedFormAsJsonString();
+        SpecificBanksAccount paymentAccount = (SpecificBanksAccount) createPaymentAccount(alicedaemon, jsonString);
+        verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
+        verifyAccountSingleTradeCurrency(paymentAccount, "GBP");
+        verifyCommonFormEntries(paymentAccount);
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
+                Objects.requireNonNull(paymentAccount.getCountry()).code);
+        SpecificBanksAccountPayload payload = (SpecificBanksAccountPayload) paymentAccount.getPaymentAccountPayload();
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), payload.getAccountNr());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_TYPE), payload.getAccountType());
+        // The bankId == bankName because bank id is not required in the UK.
+        assertEquals(payload.getBankId(), payload.getBankName());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), payload.getBankName());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BRANCH_ID), payload.getBranchId());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), payload.getHolderName());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_TAX_ID), payload.getHolderTaxId());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_NATIONAL_ACCOUNT_ID), payload.getNationalAccountId());
+        if (log.isDebugEnabled())
+            log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
+    }
+
+    @Test
+    public void testCreateSwishAccount(TestInfo testInfo) {
         File emptyForm = getEmptyForm(testInfo, SWISH_ID);
         verifyEmptyForm(emptyForm,
                 SWISH_ID,
                 PROPERTY_NAME_MOBILE_NR,
                 PROPERTY_NAME_HOLDER_NAME);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, SWISH_ID);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Swish Account");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_MOBILE_NR, "+46 7 6060 0101");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Swish Account Holder");
+        String jsonString = getCompletedFormAsJsonString();
+        SwishAccount paymentAccount = (SwishAccount) createPaymentAccount(alicedaemon, jsonString);
+        verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
+        verifyAccountSingleTradeCurrency(paymentAccount, "SEK");
+        verifyCommonFormEntries(paymentAccount);
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_MOBILE_NR), paymentAccount.getMobileNr());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
+        if (log.isDebugEnabled())
+            log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
+    }
 
-        EXPECTED_FORM.put(PROPERTY_NAME_PAYMENT_METHOD_ID, SWISH_ID);
-        EXPECTED_FORM.put(PROPERTY_NAME_ACCOUNT_NAME, "Swish Account");
-        EXPECTED_FORM.put(PROPERTY_NAME_MOBILE_NR, "+46 7 6060 0101");
-        EXPECTED_FORM.put(PROPERTY_NAME_HOLDER_NAME, "Swish Account Holder");
-
-        File completedForm = fillPaymentAccountForm();
-        String jsonString = PAYMENT_ACCOUNT_FORM.toJsonString(completedForm);
+    @Test
+    public void testCreateTransferwiseAccount(TestInfo testInfo) {
+        File emptyForm = getEmptyForm(testInfo, TRANSFERWISE_ID);
+        verifyEmptyForm(emptyForm,
+                TRANSFERWISE_ID,
+                PROPERTY_NAME_EMAIL);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, TRANSFERWISE_ID);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Transferwise Acct");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "jan@doe.info");
+        String jsonString = getCompletedFormAsJsonString();
+        TransferwiseAccount paymentAccount = (TransferwiseAccount) createPaymentAccount(alicedaemon, jsonString);
+        verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
+        verifyAccountTradeCurrencies(getAllTransferwiseCurrencies(), paymentAccount);
+        verifyCommonFormEntries(paymentAccount);
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail());
         if (log.isDebugEnabled())
-            log.debug("Completed form: {}", jsonString);
+            log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
+    }
 
-        SwishAccount paymentAccount = (SwishAccount) createPaymentAccount(alicedaemon, jsonString);
+    @Test
+    public void testCreateUpholdAccount(TestInfo testInfo) {
+        File emptyForm = getEmptyForm(testInfo, UPHOLD_ID);
+        verifyEmptyForm(emptyForm,
+                UPHOLD_ID,
+                PROPERTY_NAME_ACCOUNT_ID);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, UPHOLD_ID);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Uphold Acct");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_ID, "UA 9876");
+        String jsonString = getCompletedFormAsJsonString();
+        UpholdAccount paymentAccount = (UpholdAccount) createPaymentAccount(alicedaemon, jsonString);
         verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
-        verifyAccountFiatCurrency(paymentAccount, "SEK");
+        verifyAccountTradeCurrencies(getAllUpholdCurrencies(), paymentAccount);
         verifyCommonFormEntries(paymentAccount);
-        assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_MOBILE_NR), paymentAccount.getMobileNr());
-        assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_ID), paymentAccount.getAccountId());
         if (log.isDebugEnabled())
             log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
     }
 
     @Test
-    public void testUSPostalMoneyOrderAccountForm(TestInfo testInfo) {
+    public void testCreateUSPostalMoneyOrderAccount(TestInfo testInfo) {
         File emptyForm = getEmptyForm(testInfo, US_POSTAL_MONEY_ORDER_ID);
         verifyEmptyForm(emptyForm,
                 US_POSTAL_MONEY_ORDER_ID,
                 PROPERTY_NAME_HOLDER_NAME,
                 PROPERTY_NAME_POSTAL_ADDRESS);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, US_POSTAL_MONEY_ORDER_ID);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Bubba's Acct");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Bubba");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_POSTAL_ADDRESS, "000 Westwood Terrace Austin, TX 78700");
+        String jsonString = getCompletedFormAsJsonString();
+        USPostalMoneyOrderAccount paymentAccount = (USPostalMoneyOrderAccount) createPaymentAccount(alicedaemon, jsonString);
+        verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
+        verifyAccountSingleTradeCurrency(paymentAccount, "USD");
+        verifyCommonFormEntries(paymentAccount);
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_POSTAL_ADDRESS), paymentAccount.getPostalAddress());
+        if (log.isDebugEnabled())
+            log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
+    }
 
-        EXPECTED_FORM.put(PROPERTY_NAME_PAYMENT_METHOD_ID, US_POSTAL_MONEY_ORDER_ID);
-        EXPECTED_FORM.put(PROPERTY_NAME_ACCOUNT_NAME, "Bubba's Acct");
-        EXPECTED_FORM.put(PROPERTY_NAME_HOLDER_NAME, "Bubba");
-        EXPECTED_FORM.put(PROPERTY_NAME_POSTAL_ADDRESS, "000 Westwood Terrace Austin, TX 78700");
-
-        File completedForm = fillPaymentAccountForm();
-        String jsonString = PAYMENT_ACCOUNT_FORM.toJsonString(completedForm);
+    @Test
+    public void testCreateWeChatPayAccount(TestInfo testInfo) {
+        File emptyForm = getEmptyForm(testInfo, WECHAT_PAY_ID);
+        verifyEmptyForm(emptyForm,
+                WECHAT_PAY_ID,
+                PROPERTY_NAME_ACCOUNT_NR);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, WECHAT_PAY_ID);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "WeChat Pay Acct");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "WC 1234");
+        String jsonString = getCompletedFormAsJsonString();
+        WeChatPayAccount paymentAccount = (WeChatPayAccount) createPaymentAccount(alicedaemon, jsonString);
+        verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
+        verifyAccountSingleTradeCurrency(paymentAccount, "CNY");
+        verifyCommonFormEntries(paymentAccount);
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr());
         if (log.isDebugEnabled())
-            log.debug("Completed form: {}", jsonString);
+            log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
+    }
 
-        USPostalMoneyOrderAccount paymentAccount = (USPostalMoneyOrderAccount) createPaymentAccount(alicedaemon, jsonString);
+    @Test
+    public void testCreateWesternUnionAccount(TestInfo testInfo) {
+        File emptyForm = getEmptyForm(testInfo, WESTERN_UNION_ID);
+        verifyEmptyForm(emptyForm,
+                WESTERN_UNION_ID,
+                PROPERTY_NAME_HOLDER_NAME,
+                PROPERTY_NAME_CITY,
+                PROPERTY_NAME_STATE,
+                PROPERTY_NAME_COUNTRY,
+                PROPERTY_NAME_EMAIL);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, WESTERN_UNION_ID);
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Western Union Acct");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jane Doe");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_CITY, "Fargo");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_STATE, "North Dakota");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "US");
+        COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "jane@doe.info");
+        String jsonString = getCompletedFormAsJsonString();
+        WesternUnionAccount paymentAccount = (WesternUnionAccount) createPaymentAccount(alicedaemon, jsonString);
         verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
-        verifyAccountFiatCurrency(paymentAccount, "USD");
+        verifyAccountSingleTradeCurrency(paymentAccount, "USD");
         verifyCommonFormEntries(paymentAccount);
-        assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
-        assertEquals(EXPECTED_FORM.get(PROPERTY_NAME_POSTAL_ADDRESS), paymentAccount.getPostalAddress());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getFullName());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_CITY), paymentAccount.getCity());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_STATE), paymentAccount.getState());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
+                Objects.requireNonNull(paymentAccount.getCountry()).code);
         if (log.isDebugEnabled())
             log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
     }
diff --git a/apitest/src/test/java/bisq/apitest/method/payment/GetPaymentMethodsTest.java b/apitest/src/test/java/bisq/apitest/method/payment/GetPaymentMethodsTest.java
index acd791f596c..50c5d5b61cd 100644
--- a/apitest/src/test/java/bisq/apitest/method/payment/GetPaymentMethodsTest.java
+++ b/apitest/src/test/java/bisq/apitest/method/payment/GetPaymentMethodsTest.java
@@ -15,7 +15,7 @@
 
 import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind;
 import static bisq.apitest.config.BisqAppConfig.alicedaemon;
-import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.fail;
 
 
@@ -43,7 +43,7 @@ public void testGetPaymentMethods() {
                 .stream()
                 .map(p -> p.getId())
                 .collect(Collectors.toList());
-        assertTrue(paymentMethodIds.size() > 25);
+        assertEquals(28, paymentMethodIds.size());
     }
 
     @AfterAll
diff --git a/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java b/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java
index 6227c674746..76bc15f1858 100644
--- a/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java
+++ b/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java
@@ -13,6 +13,7 @@
 import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind;
 import static bisq.apitest.config.BisqAppConfig.alicedaemon;
 import static bisq.apitest.config.BisqAppConfig.seednode;
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.fail;
 
 
@@ -46,16 +47,43 @@ public void testGetPaymentMethods() {
     public void testCreatePaymentAccount(TestInfo testInfo) {
         CreatePaymentAccountTest test = new CreatePaymentAccountTest();
 
+        test.testCreateAdvancedCashAccount(testInfo);
+        test.testCreateAliPayAccount(testInfo);
         test.testCreateAustraliaPayidAccount(testInfo);
-        test.testBrazilNationalBankAccountForm(testInfo);
-        test.testChaseQuickPayAccountForm(testInfo);
-        test.testClearXChangeAccountForm(testInfo);
-        test.testF2FAccountForm(testInfo);
-        test.testHalCashAccountForm(testInfo);
-        test.testJapanBankAccountForm(testInfo);
-        test.testSepaAccountForm(testInfo);
-        test.testSwishAccountForm(testInfo);
-        test.testUSPostalMoneyOrderAccountForm(testInfo);
+        test.testCreateCashDepositAccount(testInfo);
+        test.testCreateBrazilNationalBankAccount(testInfo);
+        test.testCreateChaseQuickPayAccount(testInfo);
+        test.testCreateClearXChangeAccount(testInfo);
+        test.testCreateF2FAccount(testInfo);
+        test.testCreateFasterPaymentsAccount(testInfo);
+        test.testCreateHalCashAccount(testInfo);
+        test.testCreateInteracETransferAccount(testInfo);
+        test.testCreateJapanBankAccount(testInfo);
+        test.testCreateMoneyBeamAccount(testInfo);
+        // test.testCreateMoneyGramAccount(testInfo);
+        test.testCreatePerfectMoneyAccount(testInfo);
+        test.testCreatePopmoneyAccount(testInfo);
+        test.testCreatePromptPayAccount(testInfo);
+        test.testCreateRevolutAccount(testInfo);
+        test.testCreateSameBankAccount(testInfo);
+        test.testCreateSepaInstantAccount(testInfo);
+        test.testCreateSepaAccount(testInfo);
+        test.testCreateSpecificBanksAccount(testInfo);
+        test.testCreateSwishAccount(testInfo);
+        test.testCreateTransferwiseAccount(testInfo);
+        test.testCreateUpholdAccount(testInfo);
+        test.testCreateUSPostalMoneyOrderAccount(testInfo);
+        test.testCreateWeChatPayAccount(testInfo);
+        test.testCreateWesternUnionAccount(testInfo);
+    }
+
+    @Test
+    @Order(3)
+    public void testTestNumSavedPaymentAccounts(TestInfo testInfo) {
+        // Two dummy (usd +eth) accounts are set up as defaults in regtest / dao mode,
+        // then we added more payment accounts in testCreatePaymentAccount().
+        int expectedNumPaymentAccounts = 2 + 27;
+        assertEquals(expectedNumPaymentAccounts, getPaymentAccounts(alicedaemon).size());
     }
 
     @AfterAll

From bb6762a23e410fdc2504b6320c48aeb98275be9f Mon Sep 17 00:00:00 2001
From: ghubstan <36207203+ghubstan@users.noreply.github.com>
Date: Fri, 20 Nov 2020 16:53:59 -0300
Subject: [PATCH 41/52] Reverse argument order for assert(expected, actual)
 convention

---
 .../payment/AbstractPaymentAccountTest.java   |  3 +-
 .../payment/CreatePaymentAccountTest.java     | 47 +++++++++----------
 2 files changed, 24 insertions(+), 26 deletions(-)

diff --git a/apitest/src/test/java/bisq/apitest/method/payment/AbstractPaymentAccountTest.java b/apitest/src/test/java/bisq/apitest/method/payment/AbstractPaymentAccountTest.java
index dfefd3d2828..14b145e0444 100644
--- a/apitest/src/test/java/bisq/apitest/method/payment/AbstractPaymentAccountTest.java
+++ b/apitest/src/test/java/bisq/apitest/method/payment/AbstractPaymentAccountTest.java
@@ -137,8 +137,7 @@ protected final void verifyCommonFormEntries(PaymentAccount paymentAccount) {
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NAME), paymentAccount.getAccountName());
     }
 
-    protected final void verifyAccountSingleTradeCurrency(PaymentAccount paymentAccount,
-                                                          String expectedCurrencyCode) {
+    protected final void verifyAccountSingleTradeCurrency(String expectedCurrencyCode, PaymentAccount paymentAccount) {
         assertNotNull(paymentAccount.getSingleTradeCurrency());
         assertEquals(expectedCurrencyCode, paymentAccount.getSingleTradeCurrency().getCode());
     }
diff --git a/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java b/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java
index a861569405c..40ccb7b1022 100644
--- a/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java
+++ b/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java
@@ -116,7 +116,7 @@ public void testCreateAliPayAccount(TestInfo testInfo) {
         String jsonString = getCompletedFormAsJsonString();
         AliPayAccount paymentAccount = (AliPayAccount) createPaymentAccount(alicedaemon, jsonString);
         verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
-        verifyAccountSingleTradeCurrency(paymentAccount, "CNY");
+        verifyAccountSingleTradeCurrency("CNY", paymentAccount);
         verifyCommonFormEntries(paymentAccount);
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr());
         if (log.isDebugEnabled())
@@ -136,7 +136,7 @@ public void testCreateAustraliaPayidAccount(TestInfo testInfo) {
         String jsonString = getCompletedFormAsJsonString();
         AustraliaPayid paymentAccount = (AustraliaPayid) createPaymentAccount(alicedaemon, jsonString);
         verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
-        verifyAccountSingleTradeCurrency(paymentAccount, "AUD");
+        verifyAccountSingleTradeCurrency("AUD", paymentAccount);
         verifyCommonFormEntries(paymentAccount);
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_PAY_ID), paymentAccount.getPayid());
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_ACCOUNT_NAME), paymentAccount.getBankAccountName());
@@ -144,7 +144,6 @@ public void testCreateAustraliaPayidAccount(TestInfo testInfo) {
             log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
     }
 
-
     @Test
     public void testCreateCashDepositAccount(TestInfo testInfo) {
         File emptyForm = getEmptyForm(testInfo, CASH_DEPOSIT_ID);
@@ -177,7 +176,7 @@ public void testCreateCashDepositAccount(TestInfo testInfo) {
         String jsonString = getCompletedFormAsJsonString();
         CashDepositAccount paymentAccount = (CashDepositAccount) createPaymentAccount(alicedaemon, jsonString);
         verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
-        verifyAccountSingleTradeCurrency(paymentAccount, "EUR");
+        verifyAccountSingleTradeCurrency("EUR", paymentAccount);
         verifyCommonFormEntries(paymentAccount);
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
                 Objects.requireNonNull(paymentAccount.getCountry()).code);
@@ -223,7 +222,7 @@ public void testCreateBrazilNationalBankAccount(TestInfo testInfo) {
         String jsonString = getCompletedFormAsJsonString();
         NationalBankAccount paymentAccount = (NationalBankAccount) createPaymentAccount(alicedaemon, jsonString);
         verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
-        verifyAccountSingleTradeCurrency(paymentAccount, "BRL");
+        verifyAccountSingleTradeCurrency("BRL", paymentAccount);
         verifyCommonFormEntries(paymentAccount);
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
                 Objects.requireNonNull(paymentAccount.getCountry()).code);
@@ -255,7 +254,7 @@ public void testCreateChaseQuickPayAccount(TestInfo testInfo) {
         String jsonString = getCompletedFormAsJsonString();
         ChaseQuickPayAccount paymentAccount = (ChaseQuickPayAccount) createPaymentAccount(alicedaemon, jsonString);
         verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
-        verifyAccountSingleTradeCurrency(paymentAccount, "USD");
+        verifyAccountSingleTradeCurrency("USD", paymentAccount);
         verifyCommonFormEntries(paymentAccount);
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail());
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
@@ -277,7 +276,7 @@ public void testCreateClearXChangeAccount(TestInfo testInfo) {
         String jsonString = getCompletedFormAsJsonString();
         ClearXchangeAccount paymentAccount = (ClearXchangeAccount) createPaymentAccount(alicedaemon, jsonString);
         verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
-        verifyAccountSingleTradeCurrency(paymentAccount, "USD");
+        verifyAccountSingleTradeCurrency("USD", paymentAccount);
         verifyCommonFormEntries(paymentAccount);
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL_OR_MOBILE_NR), paymentAccount.getEmailOrMobileNr());
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
@@ -303,7 +302,7 @@ public void testCreateF2FAccount(TestInfo testInfo) {
         String jsonString = getCompletedFormAsJsonString();
         F2FAccount paymentAccount = (F2FAccount) createPaymentAccount(alicedaemon, jsonString);
         verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
-        verifyAccountSingleTradeCurrency(paymentAccount, "BRL");
+        verifyAccountSingleTradeCurrency("BRL", paymentAccount);
         verifyCommonFormEntries(paymentAccount);
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
                 Objects.requireNonNull(paymentAccount.getCountry()).code);
@@ -328,7 +327,7 @@ public void testCreateFasterPaymentsAccount(TestInfo testInfo) {
         String jsonString = getCompletedFormAsJsonString();
         FasterPaymentsAccount paymentAccount = (FasterPaymentsAccount) createPaymentAccount(alicedaemon, jsonString);
         verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
-        verifyAccountSingleTradeCurrency(paymentAccount, "GBP");
+        verifyAccountSingleTradeCurrency("GBP", paymentAccount);
         verifyCommonFormEntries(paymentAccount);
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr());
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SORT_CODE), paymentAccount.getSortCode());
@@ -348,7 +347,7 @@ public void testCreateHalCashAccount(TestInfo testInfo) {
         String jsonString = getCompletedFormAsJsonString();
         HalCashAccount paymentAccount = (HalCashAccount) createPaymentAccount(alicedaemon, jsonString);
         verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
-        verifyAccountSingleTradeCurrency(paymentAccount, "EUR");
+        verifyAccountSingleTradeCurrency("EUR", paymentAccount);
         verifyCommonFormEntries(paymentAccount);
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_MOBILE_NR), paymentAccount.getMobileNr());
         if (log.isDebugEnabled())
@@ -373,7 +372,7 @@ public void testCreateInteracETransferAccount(TestInfo testInfo) {
         String jsonString = getCompletedFormAsJsonString();
         InteracETransferAccount paymentAccount = (InteracETransferAccount) createPaymentAccount(alicedaemon, jsonString);
         verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
-        verifyAccountSingleTradeCurrency(paymentAccount, "CAD");
+        verifyAccountSingleTradeCurrency("CAD", paymentAccount);
         verifyCommonFormEntries(paymentAccount);
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail());
@@ -407,7 +406,7 @@ public void testCreateJapanBankAccount(TestInfo testInfo) {
         String jsonString = getCompletedFormAsJsonString();
         JapanBankAccount paymentAccount = (JapanBankAccount) createPaymentAccount(alicedaemon, jsonString);
         verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
-        verifyAccountSingleTradeCurrency(paymentAccount, "JPY");
+        verifyAccountSingleTradeCurrency("JPY", paymentAccount);
         verifyCommonFormEntries(paymentAccount);
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_CODE), paymentAccount.getBankCode());
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), paymentAccount.getBankName());
@@ -432,7 +431,7 @@ public void testCreateMoneyBeamAccount(TestInfo testInfo) {
         String jsonString = getCompletedFormAsJsonString();
         MoneyBeamAccount paymentAccount = (MoneyBeamAccount) createPaymentAccount(alicedaemon, jsonString);
         verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
-        verifyAccountSingleTradeCurrency(paymentAccount, "EUR");
+        verifyAccountSingleTradeCurrency("EUR", paymentAccount);
         verifyCommonFormEntries(paymentAccount);
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_ID), paymentAccount.getAccountId());
         if (log.isDebugEnabled())
@@ -485,7 +484,7 @@ public void testCreatePerfectMoneyAccount(TestInfo testInfo) {
         String jsonString = getCompletedFormAsJsonString();
         PerfectMoneyAccount paymentAccount = (PerfectMoneyAccount) createPaymentAccount(alicedaemon, jsonString);
         verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
-        verifyAccountSingleTradeCurrency(paymentAccount, "USD");
+        verifyAccountSingleTradeCurrency("USD", paymentAccount);
         verifyCommonFormEntries(paymentAccount);
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr());
         if (log.isDebugEnabled())
@@ -506,7 +505,7 @@ public void testCreatePopmoneyAccount(TestInfo testInfo) {
         String jsonString = getCompletedFormAsJsonString();
         PopmoneyAccount paymentAccount = (PopmoneyAccount) createPaymentAccount(alicedaemon, jsonString);
         verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
-        verifyAccountSingleTradeCurrency(paymentAccount, "USD");
+        verifyAccountSingleTradeCurrency("USD", paymentAccount);
         verifyCommonFormEntries(paymentAccount);
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_ID), paymentAccount.getAccountId());
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
@@ -526,7 +525,7 @@ public void testCreatePromptPayAccount(TestInfo testInfo) {
         String jsonString = getCompletedFormAsJsonString();
         PromptPayAccount paymentAccount = (PromptPayAccount) createPaymentAccount(alicedaemon, jsonString);
         verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
-        verifyAccountSingleTradeCurrency(paymentAccount, "THB");
+        verifyAccountSingleTradeCurrency("THB", paymentAccount);
         verifyCommonFormEntries(paymentAccount);
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_PROMPT_PAY_ID), paymentAccount.getPromptPayId());
         if (log.isDebugEnabled())
@@ -578,7 +577,7 @@ public void testCreateSameBankAccount(TestInfo testInfo) {
         String jsonString = getCompletedFormAsJsonString();
         SameBankAccount paymentAccount = (SameBankAccount) createPaymentAccount(alicedaemon, jsonString);
         verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
-        verifyAccountSingleTradeCurrency(paymentAccount, "GBP");
+        verifyAccountSingleTradeCurrency("GBP", paymentAccount);
         verifyCommonFormEntries(paymentAccount);
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
                 Objects.requireNonNull(paymentAccount.getCountry()).code);
@@ -616,7 +615,7 @@ public void testCreateSepaInstantAccount(TestInfo testInfo) {
         verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
                 Objects.requireNonNull(paymentAccount.getCountry()).code);
-        verifyAccountSingleTradeCurrency(paymentAccount, "EUR");
+        verifyAccountSingleTradeCurrency("EUR", paymentAccount);
         verifyCommonFormEntries(paymentAccount);
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_IBAN), paymentAccount.getIban());
@@ -647,7 +646,7 @@ public void testCreateSepaAccount(TestInfo testInfo) {
         verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
                 Objects.requireNonNull(paymentAccount.getCountry()).code);
-        verifyAccountSingleTradeCurrency(paymentAccount, "EUR");
+        verifyAccountSingleTradeCurrency("EUR", paymentAccount);
         verifyCommonFormEntries(paymentAccount);
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_IBAN), paymentAccount.getIban());
@@ -687,7 +686,7 @@ public void testCreateSpecificBanksAccount(TestInfo testInfo) {
         String jsonString = getCompletedFormAsJsonString();
         SpecificBanksAccount paymentAccount = (SpecificBanksAccount) createPaymentAccount(alicedaemon, jsonString);
         verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
-        verifyAccountSingleTradeCurrency(paymentAccount, "GBP");
+        verifyAccountSingleTradeCurrency("GBP", paymentAccount);
         verifyCommonFormEntries(paymentAccount);
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
                 Objects.requireNonNull(paymentAccount.getCountry()).code);
@@ -719,7 +718,7 @@ public void testCreateSwishAccount(TestInfo testInfo) {
         String jsonString = getCompletedFormAsJsonString();
         SwishAccount paymentAccount = (SwishAccount) createPaymentAccount(alicedaemon, jsonString);
         verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
-        verifyAccountSingleTradeCurrency(paymentAccount, "SEK");
+        verifyAccountSingleTradeCurrency("SEK", paymentAccount);
         verifyCommonFormEntries(paymentAccount);
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_MOBILE_NR), paymentAccount.getMobileNr());
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
@@ -779,7 +778,7 @@ public void testCreateUSPostalMoneyOrderAccount(TestInfo testInfo) {
         String jsonString = getCompletedFormAsJsonString();
         USPostalMoneyOrderAccount paymentAccount = (USPostalMoneyOrderAccount) createPaymentAccount(alicedaemon, jsonString);
         verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
-        verifyAccountSingleTradeCurrency(paymentAccount, "USD");
+        verifyAccountSingleTradeCurrency("USD", paymentAccount);
         verifyCommonFormEntries(paymentAccount);
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_POSTAL_ADDRESS), paymentAccount.getPostalAddress());
@@ -799,7 +798,7 @@ public void testCreateWeChatPayAccount(TestInfo testInfo) {
         String jsonString = getCompletedFormAsJsonString();
         WeChatPayAccount paymentAccount = (WeChatPayAccount) createPaymentAccount(alicedaemon, jsonString);
         verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
-        verifyAccountSingleTradeCurrency(paymentAccount, "CNY");
+        verifyAccountSingleTradeCurrency("CNY", paymentAccount);
         verifyCommonFormEntries(paymentAccount);
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr());
         if (log.isDebugEnabled())
@@ -826,7 +825,7 @@ public void testCreateWesternUnionAccount(TestInfo testInfo) {
         String jsonString = getCompletedFormAsJsonString();
         WesternUnionAccount paymentAccount = (WesternUnionAccount) createPaymentAccount(alicedaemon, jsonString);
         verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
-        verifyAccountSingleTradeCurrency(paymentAccount, "USD");
+        verifyAccountSingleTradeCurrency("USD", paymentAccount);
         verifyCommonFormEntries(paymentAccount);
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getFullName());
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_CITY), paymentAccount.getCity());

From 636fac9170e0ea90b4ba2b94a3705d8a0e6bd649 Mon Sep 17 00:00:00 2001
From: ghubstan <36207203+ghubstan@users.noreply.github.com>
Date: Sat, 21 Nov 2020 15:21:10 -0300
Subject: [PATCH 42/52] Add isMoneyGramAccount to abstract PaymentAccount

---
 core/src/main/java/bisq/core/payment/PaymentAccount.java | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/core/src/main/java/bisq/core/payment/PaymentAccount.java b/core/src/main/java/bisq/core/payment/PaymentAccount.java
index f94811585f1..b61b86d482b 100644
--- a/core/src/main/java/bisq/core/payment/PaymentAccount.java
+++ b/core/src/main/java/bisq/core/payment/PaymentAccount.java
@@ -181,6 +181,10 @@ public boolean isHalCashAccount() {
         return this instanceof HalCashAccount;
     }
 
+    public boolean isMoneyGramAccount() {
+        return this instanceof MoneyGramAccount;
+    }
+
     /**
      * Return an Optional of the trade currency for this payment account, or
      * Optional.empty() if none is found.  If this payment account has a selected

From 23bfa2e7fc7948114e19dccbe41b307ee70a50d0 Mon Sep 17 00:00:00 2001
From: ghubstan <36207203+ghubstan@users.noreply.github.com>
Date: Sat, 21 Nov 2020 15:22:15 -0300
Subject: [PATCH 43/52] Refactor didReadCountryField to set country on
 MoneyGram acct

MoneyGram is not a CountryBasedPaymentAccount, but it does
have a country field.
---
 .../api/model/PaymentAccountTypeAdapter.java  | 31 ++++++++++++-------
 1 file changed, 20 insertions(+), 11 deletions(-)

diff --git a/core/src/main/java/bisq/core/api/model/PaymentAccountTypeAdapter.java b/core/src/main/java/bisq/core/api/model/PaymentAccountTypeAdapter.java
index e60d5c722a7..13646d3f0d0 100644
--- a/core/src/main/java/bisq/core/api/model/PaymentAccountTypeAdapter.java
+++ b/core/src/main/java/bisq/core/api/model/PaymentAccountTypeAdapter.java
@@ -21,6 +21,7 @@
 import bisq.core.locale.Country;
 import bisq.core.locale.FiatCurrency;
 import bisq.core.payment.CountryBasedPaymentAccount;
+import bisq.core.payment.MoneyGramAccount;
 import bisq.core.payment.PaymentAccount;
 import bisq.core.payment.payload.PaymentAccountPayload;
 
@@ -285,22 +286,31 @@ private boolean didReadCommonField(JsonReader in, PaymentAccount account, String
     }
 
     private boolean didReadCountryField(JsonReader in, PaymentAccount account, String fieldName) {
-        if (account.isCountryBasedPaymentAccount() && fieldName.equals("country")) {
-            // Read the country code, and use it to set the account's country and single
-            // trade currency fields.
-            String countryCode = nextStringOrNull(in);
-            Optional country = findCountryByCode(countryCode);
-            if (country.isPresent()) {
+        if (!fieldName.equals("country"))
+            return false;
+
+        String countryCode = nextStringOrNull(in);
+        Optional country = findCountryByCode(countryCode);
+        if (country.isPresent()) {
+
+            if (account.isCountryBasedPaymentAccount()) {
                 ((CountryBasedPaymentAccount) account).setCountry(country.get());
                 FiatCurrency fiatCurrency = getCurrencyByCountryCode(checkNotNull(countryCode));
                 account.setSingleTradeCurrency(fiatCurrency);
-                return true;
+            } else if (account.isMoneyGramAccount()) {
+                ((MoneyGramAccount) account).setCountry(country.get());
             } else {
-                throw new IllegalArgumentException(
-                        format("'%s' is an invalid country code.", countryCode));
+                String errMsg = format("cannot set the country on a %s",
+                        paymentAccountType.getSimpleName());
+                log.error(StringUtils.capitalize(errMsg) + ".");
+                throw new IllegalStateException("programmer error: " + errMsg);
             }
+
+            return true;
+
         } else {
-            return false;
+            throw new IllegalArgumentException(
+                    format("'%s' is an invalid country code.", countryCode));
         }
     }
 
@@ -315,7 +325,6 @@ private Class getPaymentAccountPayloadType() {
                     paymentAccountType.getSimpleName());
             log.error(StringUtils.capitalize(errMsg) + ".", ex);
             throw new IllegalStateException("programmer error: " + errMsg);
-
         }
     }
 

From 76097fc9dc1ec8f8bb7234db92d21d4abc40f762 Mon Sep 17 00:00:00 2001
From: ghubstan <36207203+ghubstan@users.noreply.github.com>
Date: Sat, 21 Nov 2020 15:24:33 -0300
Subject: [PATCH 44/52] Enable CreateMoneyGramAccount test

---
 .../method/payment/CreatePaymentAccountTest.java |  9 ++-------
 .../apitest/scenario/PaymentAccountTest.java     | 16 ++++++----------
 2 files changed, 8 insertions(+), 17 deletions(-)

diff --git a/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java b/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java
index 40ccb7b1022..5e1305b0c0c 100644
--- a/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java
+++ b/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java
@@ -438,14 +438,8 @@ public void testCreateMoneyBeamAccount(TestInfo testInfo) {
             log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
     }
 
-    @Disabled
     @Test
     public void testCreateMoneyGramAccount(TestInfo testInfo) {
-        // TODO Check the MoneyGramAccount class, it seems it should be a subclass
-        //   of CountryBasedPaymentAccount, and these setters seem wrong:
-        //      setState(email)
-        //      setHolderName(email)
-        // This is the only test that is breaking as of Fri 20 Nov 2020 03:18:34 PM -03.
         File emptyForm = getEmptyForm(testInfo, MONEY_GRAM_ID);
         verifyEmptyForm(emptyForm,
                 MONEY_GRAM_ID,
@@ -466,7 +460,8 @@ public void testCreateMoneyGramAccount(TestInfo testInfo) {
         verifyCommonFormEntries(paymentAccount);
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getFullName());
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail());
-        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY), paymentAccount.getCountry());
+        assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
+                Objects.requireNonNull(paymentAccount.getCountry()).code);
         assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_STATE), paymentAccount.getState());
         if (log.isDebugEnabled())
             log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
diff --git a/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java b/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java
index 76bc15f1858..897e8bea3d0 100644
--- a/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java
+++ b/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java
@@ -26,6 +26,10 @@
 @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
 public class PaymentAccountTest extends AbstractPaymentAccountTest {
 
+    // Two dummy (usd +eth) accounts are set up as defaults in regtest / dao mode,
+    // then we add 28 more payment accounts in testCreatePaymentAccount().
+    private static final int EXPECTED_NUM_PAYMENT_ACCOUNTS = 2 + 28;
+
     @BeforeAll
     public static void setUp() {
         try {
@@ -60,7 +64,7 @@ public void testCreatePaymentAccount(TestInfo testInfo) {
         test.testCreateInteracETransferAccount(testInfo);
         test.testCreateJapanBankAccount(testInfo);
         test.testCreateMoneyBeamAccount(testInfo);
-        // test.testCreateMoneyGramAccount(testInfo);
+        test.testCreateMoneyGramAccount(testInfo);
         test.testCreatePerfectMoneyAccount(testInfo);
         test.testCreatePopmoneyAccount(testInfo);
         test.testCreatePromptPayAccount(testInfo);
@@ -75,20 +79,12 @@ public void testCreatePaymentAccount(TestInfo testInfo) {
         test.testCreateUSPostalMoneyOrderAccount(testInfo);
         test.testCreateWeChatPayAccount(testInfo);
         test.testCreateWesternUnionAccount(testInfo);
-    }
 
-    @Test
-    @Order(3)
-    public void testTestNumSavedPaymentAccounts(TestInfo testInfo) {
-        // Two dummy (usd +eth) accounts are set up as defaults in regtest / dao mode,
-        // then we added more payment accounts in testCreatePaymentAccount().
-        int expectedNumPaymentAccounts = 2 + 27;
-        assertEquals(expectedNumPaymentAccounts, getPaymentAccounts(alicedaemon).size());
+        assertEquals(EXPECTED_NUM_PAYMENT_ACCOUNTS, getPaymentAccounts(alicedaemon).size());
     }
 
     @AfterAll
     public static void tearDown() {
         tearDownScaffold();
     }
-
 }

From 424f9480ade352f6e79378fe79f84580c357a555 Mon Sep 17 00:00:00 2001
From: ghubstan <36207203+ghubstan@users.noreply.github.com>
Date: Sun, 22 Nov 2020 16:12:34 -0300
Subject: [PATCH 45/52] Fix broken tests

- Change the getbalance tests to just check a sucessful '0' status code.

  The api's getbalance method returns full balance info now, not just a
  formatted long.  We still assert the cmd did not fail, but don't check
  the value string.

- Remove obsolete createpaymentacct tests.

  This is well tested in apitest, and testing this cmd is unnecesarily
  complex to do in a bats script.
---
 apitest/scripts/mainnet-test.sh | 16 ----------------
 1 file changed, 16 deletions(-)

diff --git a/apitest/scripts/mainnet-test.sh b/apitest/scripts/mainnet-test.sh
index 72b29cdfbf3..2cb436dbf1c 100755
--- a/apitest/scripts/mainnet-test.sh
+++ b/apitest/scripts/mainnet-test.sh
@@ -93,8 +93,6 @@
 @test "test getbalance while wallet unlocked for 8s" {
   run ./bisq-cli --password=xyz getbalance
   [ "$status" -eq 0 ]
-  echo "actual output:  $output" >&2
-  [ "$output" = "0.00000000" ]
   sleep 8
 }
 
@@ -145,8 +143,6 @@
 @test "test getbalance when wallet available & unlocked with 0 btc balance" {
   run ./bisq-cli --password=xyz getbalance
   [ "$status" -eq 0 ]
-  echo "actual output:  $output" >&2
-  [ "$output" = "0.00000000" ]
 }
 
 @test "test getfundingaddresses" {
@@ -168,18 +164,6 @@
   [ "$output" = "Error: address bogus not found in wallet" ]
 }
 
-@test "test createpaymentacct PerfectMoneyDummy (missing name, nbr, ccy params)" {
-  run ./bisq-cli --password=xyz createpaymentacct PERFECT_MONEY
-  [ "$status" -eq 1 ]
- echo "actual output:  $output" >&2
-  [ "$output" = "Error: incorrect parameter count, expecting payment method id, account name, account number, currency code" ]
-}
-
-@test "test createpaymentacct PERFECT_MONEY PerfectMoneyDummy 0123456789 USD" {
-  run ./bisq-cli --password=xyz createpaymentacct PERFECT_MONEY PerfectMoneyDummy 0123456789 USD
-  [ "$status" -eq 0 ]
-}
-
 @test "test getpaymentaccts" {
   run ./bisq-cli --password=xyz getpaymentaccts
   [ "$status" -eq 0 ]

From daeb34b3d28c6118a904d53a917a37acbfe91938 Mon Sep 17 00:00:00 2001
From: ghubstan <36207203+ghubstan@users.noreply.github.com>
Date: Sun, 22 Nov 2020 16:25:21 -0300
Subject: [PATCH 46/52] Add two new api method tests

Keeping the bats test simple.  More complex use cases are tested in
apitest, and we don't want to be trading on mainnet with a bats test.

- Add new getunusedbsqaddress test, assert success return status.
- Add new getpaymentmethods test, assert success return status.
---
 apitest/scripts/mainnet-test.sh | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/apitest/scripts/mainnet-test.sh b/apitest/scripts/mainnet-test.sh
index 2cb436dbf1c..48fe4023bf1 100755
--- a/apitest/scripts/mainnet-test.sh
+++ b/apitest/scripts/mainnet-test.sh
@@ -150,6 +150,11 @@
   [ "$status" -eq 0 ]
 }
 
+@test "test getunusedbsqaddress" {
+  run ./bisq-cli --password=xyz getfundingaddresses
+  [ "$status" -eq 0 ]
+}
+
 @test "test getaddressbalance missing address argument" {
   run ./bisq-cli --password=xyz getaddressbalance
   [ "$status" -eq 1 ]
@@ -164,6 +169,11 @@
   [ "$output" = "Error: address bogus not found in wallet" ]
 }
 
+@test "test getpaymentmethods" {
+  run ./bisq-cli --password=xyz getpaymentmethods
+  [ "$status" -eq 0 ]
+}
+
 @test "test getpaymentaccts" {
   run ./bisq-cli --password=xyz getpaymentaccts
   [ "$status" -eq 0 ]

From 08d56733a864332e0f66cb4d01721214a8b1c4e3 Mon Sep 17 00:00:00 2001
From: ghubstan <36207203+ghubstan@users.noreply.github.com>
Date: Sun, 22 Nov 2020 16:54:24 -0300
Subject: [PATCH 47/52] Fix getbalance out bug in CLI

Running the bats test exposed a bug that shows BSQ and BTC balance
info no matter what currency code argument is passed to the server.
---
 cli/src/main/java/bisq/cli/CliMain.java | 18 +++++++++++++-----
 1 file changed, 13 insertions(+), 5 deletions(-)

diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java
index f3d2b806ada..48493eebaac 100644
--- a/cli/src/main/java/bisq/cli/CliMain.java
+++ b/cli/src/main/java/bisq/cli/CliMain.java
@@ -69,10 +69,7 @@
 
 import static bisq.cli.CurrencyFormat.toSatoshis;
 import static bisq.cli.NegativeNumberOptions.hasNegativeNumberOptions;
-import static bisq.cli.TableFormat.formatAddressBalanceTbl;
-import static bisq.cli.TableFormat.formatBalancesTbls;
-import static bisq.cli.TableFormat.formatOfferTable;
-import static bisq.cli.TableFormat.formatPaymentAcctTbl;
+import static bisq.cli.TableFormat.*;
 import static java.lang.String.format;
 import static java.lang.System.err;
 import static java.lang.System.exit;
@@ -208,7 +205,18 @@ public static void run(String[] args) {
                             .setCurrencyCode(currencyCode)
                             .build();
                     var reply = walletsService.getBalances(request);
-                    out.println(formatBalancesTbls(reply.getBalances()));
+                    switch (currencyCode.toUpperCase()) {
+                        case "BSQ":
+                            out.println(formatBsqBalanceInfoTbl(reply.getBalances().getBsq()));
+                            break;
+                        case "BTC":
+                            out.println(formatBtcBalanceInfoTbl(reply.getBalances().getBtc()));
+                            break;
+                        case "":
+                        default:
+                            out.println(formatBalancesTbls(reply.getBalances()));
+                            break;
+                    }
                     return;
                 }
                 case getaddressbalance: {

From ab5780e0e105d3b00b0dcd91394e33f1c6368c04 Mon Sep 17 00:00:00 2001
From: ghubstan <36207203+ghubstan@users.noreply.github.com>
Date: Sun, 22 Nov 2020 18:58:43 -0300
Subject: [PATCH 48/52] Print 'no offers found' msg in CLI if list is empty

---
 cli/src/main/java/bisq/cli/CliMain.java | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java
index 48493eebaac..69f6be5a7a7 100644
--- a/cli/src/main/java/bisq/cli/CliMain.java
+++ b/cli/src/main/java/bisq/cli/CliMain.java
@@ -35,6 +35,7 @@
 import bisq.proto.grpc.GetVersionRequest;
 import bisq.proto.grpc.KeepFundsRequest;
 import bisq.proto.grpc.LockWalletRequest;
+import bisq.proto.grpc.OfferInfo;
 import bisq.proto.grpc.RegisterDisputeAgentRequest;
 import bisq.proto.grpc.RemoveWalletPasswordRequest;
 import bisq.proto.grpc.SendBsqRequest;
@@ -344,7 +345,13 @@ public static void run(String[] args) {
                             .setCurrencyCode(currencyCode)
                             .build();
                     var reply = offersService.getOffers(request);
-                    out.println(formatOfferTable(reply.getOffersList(), currencyCode));
+
+                    List offers = reply.getOffersList();
+                    if(offers.isEmpty())
+                        out.println("no offers found");
+                    else
+                        out.println(formatOfferTable(reply.getOffersList(), currencyCode));
+
                     return;
                 }
                 case takeoffer: {

From c88543848d7d553df18afc4d2ccd10ff009075af Mon Sep 17 00:00:00 2001
From: ghubstan <36207203+ghubstan@users.noreply.github.com>
Date: Mon, 23 Nov 2020 12:06:57 -0300
Subject: [PATCH 49/52] Fix minor error & help msg bugs

---
 cli/src/main/java/bisq/cli/CliMain.java | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java
index 69f6be5a7a7..c6a420253cc 100644
--- a/cli/src/main/java/bisq/cli/CliMain.java
+++ b/cli/src/main/java/bisq/cli/CliMain.java
@@ -347,8 +347,8 @@ public static void run(String[] args) {
                     var reply = offersService.getOffers(request);
 
                     List offers = reply.getOffersList();
-                    if(offers.isEmpty())
-                        out.println("no offers found");
+                    if (offers.isEmpty())
+                        out.printf("no %s %s offers found\n", direction, currencyCode);
                     else
                         out.println(formatOfferTable(reply.getOffersList(), currencyCode));
 
@@ -627,8 +627,8 @@ private static void printHelp(OptionParser parser, PrintStream stream) {
             stream.format(rowFormat, "sendbsq", "address, amount", "Send BSQ");
             stream.format(rowFormat, "createoffer", "payment acct id, buy | sell, currency code, \\", "Create and place an offer");
             stream.format(rowFormat, "", "amount (btc), min amount, use mkt based price, \\", "");
-            stream.format(rowFormat, "", "fixed price (btc) | mkt price margin (%) [,maker fee currency code = bsq|btc]\\", "");
-            stream.format(rowFormat, "", "security deposit (%)", "");
+            stream.format(rowFormat, "", "fixed price (btc) | mkt price margin (%), security deposit (%) \\", "");
+            stream.format(rowFormat, "", "[,maker fee currency code = bsq|btc]", "");
             stream.format(rowFormat, "canceloffer", "offer id", "Cancel offer with id");
             stream.format(rowFormat, "getoffer", "offer id", "Get current offer with id");
             stream.format(rowFormat, "getoffers", "buy | sell, currency code", "Get current offers");

From f50634ee4a4fe22c9209727f4c78fe4c0a29a1df Mon Sep 17 00:00:00 2001
From: ghubstan <36207203+ghubstan@users.noreply.github.com>
Date: Thu, 26 Nov 2020 13:34:05 -0300
Subject: [PATCH 50/52] Resolve BsqSendView file conflict

Restore BsqSendView to version @ commit 48a515be01dd674d241be04cddb18c0e823e91b4.
Author: jmacxx
Date:   Tue Nov 10 19:35:31 2020 -0600

    Add encrypted wallet password prompt when sending funds from BSQ wallet

    Correct wording of transaction confirmation popup to use 'mining fee'
    instead of 'transaction fee' to make it consistent with wording of
    the BTC confirmation popup.
---
 .../main/dao/wallet/send/BsqSendView.java     | 41 +++++++------------
 1 file changed, 15 insertions(+), 26 deletions(-)

diff --git a/desktop/src/main/java/bisq/desktop/main/dao/wallet/send/BsqSendView.java b/desktop/src/main/java/bisq/desktop/main/dao/wallet/send/BsqSendView.java
index c3dee6eb0e4..fa638c4a19c 100644
--- a/desktop/src/main/java/bisq/desktop/main/dao/wallet/send/BsqSendView.java
+++ b/desktop/src/main/java/bisq/desktop/main/dao/wallet/send/BsqSendView.java
@@ -35,13 +35,9 @@
 import bisq.desktop.util.validation.BtcValidator;
 
 import bisq.core.btc.exceptions.BsqChangeBelowDustException;
-import bisq.core.btc.exceptions.TransactionVerificationException;
 import bisq.core.btc.exceptions.TxBroadcastException;
-import bisq.core.btc.exceptions.WalletException;
 import bisq.core.btc.listeners.BsqBalanceListener;
-import bisq.core.btc.model.BsqTransferModel;
 import bisq.core.btc.setup.WalletsSetup;
-import bisq.core.btc.wallet.BsqTransferService;
 import bisq.core.btc.wallet.BsqWalletService;
 import bisq.core.btc.wallet.BtcWalletService;
 import bisq.core.btc.wallet.Restrictions;
@@ -50,10 +46,10 @@
 import bisq.core.dao.state.model.blockchain.TxType;
 import bisq.core.locale.Res;
 import bisq.core.util.FormattingUtils;
-import bisq.core.util.ParsingUtils;
 import bisq.core.util.coin.BsqFormatter;
 import bisq.core.util.coin.CoinFormatter;
 import bisq.core.util.coin.CoinUtil;
+import bisq.core.util.ParsingUtils;
 import bisq.core.util.validation.BtcAddressValidator;
 
 import bisq.network.p2p.P2PService;
@@ -63,7 +59,6 @@
 
 import org.bitcoinj.core.Coin;
 import org.bitcoinj.core.InsufficientMoneyException;
-import org.bitcoinj.core.LegacyAddress;
 import org.bitcoinj.core.Transaction;
 
 import javax.inject.Inject;
@@ -95,7 +90,6 @@ public class BsqSendView extends ActivatableView implements BsqB
     private final BtcValidator btcValidator;
     private final BsqAddressValidator bsqAddressValidator;
     private final BtcAddressValidator btcAddressValidator;
-    private final BsqTransferService bsqTransferService;
     private final WalletPasswordWindow walletPasswordWindow;
 
     private int gridRow = 0;
@@ -125,7 +119,6 @@ private BsqSendView(BsqWalletService bsqWalletService,
                         BtcValidator btcValidator,
                         BsqAddressValidator bsqAddressValidator,
                         BtcAddressValidator btcAddressValidator,
-                        BsqTransferService bsqTransferService,
                         WalletPasswordWindow walletPasswordWindow) {
         this.bsqWalletService = bsqWalletService;
         this.btcWalletService = btcWalletService;
@@ -140,7 +133,6 @@ private BsqSendView(BsqWalletService bsqWalletService,
         this.btcValidator = btcValidator;
         this.bsqAddressValidator = bsqAddressValidator;
         this.btcAddressValidator = btcAddressValidator;
-        this.bsqTransferService = bsqTransferService;
         this.walletPasswordWindow = walletPasswordWindow;
     }
 
@@ -249,15 +241,22 @@ private void addSendBsqGroup() {
         sendBsqButton = addButtonAfterGroup(root, ++gridRow, Res.get("dao.wallet.send.send"));
 
         sendBsqButton.setOnAction((event) -> {
+            // TODO break up in methods
             if (GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, walletsSetup)) {
+                String receiversAddressString = bsqFormatter.getAddressFromBsqAddress(receiversAddressInputTextField.getText()).toString();
+                Coin receiverAmount = ParsingUtils.parseToCoin(amountInputTextField.getText(), bsqFormatter);
                 try {
-                    BsqTransferModel model = getBsqTransferModel();
-                    showPublishTxPopup(model.getReceiverAmount(),
-                            model.getTxWithBtcFee(),
-                            model.getTxType(),
-                            model.getMiningFee(),
-                            model.getTxSize(),
-                            model.getReceiverAddressAsString(),
+                    Transaction preparedSendTx = bsqWalletService.getPreparedSendBsqTx(receiversAddressString, receiverAmount);
+                    Transaction txWithBtcFee = btcWalletService.completePreparedSendBsqTx(preparedSendTx, true);
+                    Transaction signedTx = bsqWalletService.signTx(txWithBtcFee);
+                    Coin miningFee = signedTx.getFee();
+                    int txSize = signedTx.bitcoinSerialize().length;
+                    showPublishTxPopup(receiverAmount,
+                            txWithBtcFee,
+                            TxType.TRANSFER_BSQ,
+                            miningFee,
+                            txSize,
+                            receiversAddressInputTextField.getText(),
                             bsqFormatter,
                             btcFormatter,
                             () -> {
@@ -274,16 +273,6 @@ private void addSendBsqGroup() {
         });
     }
 
-    private BsqTransferModel getBsqTransferModel()
-            throws InsufficientMoneyException,
-            TransactionVerificationException,
-            BsqChangeBelowDustException,
-            WalletException {
-        Coin receiverAmount = ParsingUtils.parseToCoin(amountInputTextField.getText(), bsqFormatter);
-        LegacyAddress legacyAddress = bsqFormatter.getAddressFromBsqAddress(receiversAddressInputTextField.getText());
-        return bsqTransferService.getBsqTransferModel(legacyAddress, receiverAmount);
-    }
-
     private void setSendBtcGroupVisibleState(boolean visible) {
         btcTitledGroupBg.setVisible(visible);
         receiversBtcAddressInputTextField.setVisible(visible);

From 904352ee2ed8101c3b9742b177745b823484ca81 Mon Sep 17 00:00:00 2001
From: ghubstan <36207203+ghubstan@users.noreply.github.com>
Date: Thu, 26 Nov 2020 16:50:53 -0300
Subject: [PATCH 51/52] Adjust to changed CoinUtil

---
 core/src/main/java/bisq/core/btc/model/BsqTransferModel.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/core/src/main/java/bisq/core/btc/model/BsqTransferModel.java b/core/src/main/java/bisq/core/btc/model/BsqTransferModel.java
index 3033c769eee..38e4c4b5423 100644
--- a/core/src/main/java/bisq/core/btc/model/BsqTransferModel.java
+++ b/core/src/main/java/bisq/core/btc/model/BsqTransferModel.java
@@ -41,7 +41,7 @@ public String getReceiverAddressAsString() {
     }
 
     public double getMiningFeeInSatoshisPerByte() {
-        return CoinUtil.getFeePerByte(miningFee, txSize);
+        return CoinUtil.getFeePerVbyte(miningFee, txSize);
     }
 
     public double getTxSizeInKb() {

From a1c5dd5aebb7f02a58430ab7f9941ec45ee9cfd4 Mon Sep 17 00:00:00 2001
From: ghubstan <36207203+ghubstan@users.noreply.github.com>
Date: Thu, 26 Nov 2020 16:53:27 -0300
Subject: [PATCH 52/52] Adjust test for a new payment method

---
 .../java/bisq/apitest/method/payment/GetPaymentMethodsTest.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/apitest/src/test/java/bisq/apitest/method/payment/GetPaymentMethodsTest.java b/apitest/src/test/java/bisq/apitest/method/payment/GetPaymentMethodsTest.java
index 50c5d5b61cd..52ff85b7f49 100644
--- a/apitest/src/test/java/bisq/apitest/method/payment/GetPaymentMethodsTest.java
+++ b/apitest/src/test/java/bisq/apitest/method/payment/GetPaymentMethodsTest.java
@@ -43,7 +43,7 @@ public void testGetPaymentMethods() {
                 .stream()
                 .map(p -> p.getId())
                 .collect(Collectors.toList());
-        assertEquals(28, paymentMethodIds.size());
+        assertEquals(29, paymentMethodIds.size());
     }
 
     @AfterAll