diff --git a/apitest/docs/build-run.md b/apitest/docs/build-run.md index 308fe02cf66..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.GetBalanceTest" -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/scripts/mainnet-test.sh b/apitest/scripts/mainnet-test.sh index 72b29cdfbf3..48fe4023bf1 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" { @@ -154,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 ] @@ -168,15 +169,8 @@ [ "$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 +@test "test getpaymentmethods" { + run ./bisq-cli --password=xyz getpaymentmethods [ "$status" -eq 0 ] } diff --git a/apitest/src/test/java/bisq/apitest/method/CreatePaymentAccountTest.java b/apitest/src/test/java/bisq/apitest/method/CreatePaymentAccountTest.java deleted file mode 100644 index 9e8b0af878a..00000000000 --- a/apitest/src/test/java/bisq/apitest/method/CreatePaymentAccountTest.java +++ /dev/null @@ -1,99 +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; - -import bisq.proto.grpc.GetPaymentAccountsRequest; - -import protobuf.PaymentAccount; -import protobuf.PerfectMoneyAccountPayload; - -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.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 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; - -@Disabled -@Slf4j -@TestMethodOrder(OrderAnnotation.class) -public class CreatePaymentAccountTest extends MethodTest { - - static final String PERFECT_MONEY_ACCT_NAME = "Perfect Money USD"; - static final String PERFECT_MONEY_ACCT_NUMBER = "0123456789"; - - @BeforeAll - public static void setUp() { - try { - setUpScaffold(bitcoind, alicedaemon); - } catch (Exception ex) { - fail(ex); - } - } - - @Test - @Order(1) - public void testCreatePerfectMoneyUSDPaymentAccount() { - 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()); - } - - @AfterAll - public static void tearDown() { - tearDownScaffold(); - } -} diff --git a/apitest/src/test/java/bisq/apitest/method/GetBalanceTest.java b/apitest/src/test/java/bisq/apitest/method/GetBalanceTest.java deleted file mode 100644 index 1d44590837b..00000000000 --- a/apitest/src/test/java/bisq/apitest/method/GetBalanceTest.java +++ /dev/null @@ -1,73 +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; - -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; - -@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(); - } -} diff --git a/apitest/src/test/java/bisq/apitest/method/MethodTest.java b/apitest/src/test/java/bisq/apitest/method/MethodTest.java index 43073ba995b..153f3c90c53 100644 --- a/apitest/src/test/java/bisq/apitest/method/MethodTest.java +++ b/apitest/src/test/java/bisq/apitest/method/MethodTest.java @@ -17,21 +17,33 @@ 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; +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.GetBalanceRequest; +import bisq.proto.grpc.GetAddressBalanceRequest; +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; +import bisq.proto.grpc.GetUnusedBsqAddressRequest; import bisq.proto.grpc.KeepFundsRequest; import bisq.proto.grpc.LockWalletRequest; import bisq.proto.grpc.MarketPriceRequest; import bisq.proto.grpc.OfferInfo; import bisq.proto.grpc.RegisterDisputeAgentRequest; import bisq.proto.grpc.RemoveWalletPasswordRequest; +import bisq.proto.grpc.SendBsqRequest; import bisq.proto.grpc.SetWalletPasswordRequest; import bisq.proto.grpc.TakeOfferRequest; import bisq.proto.grpc.TradeInfo; @@ -39,14 +51,21 @@ import bisq.proto.grpc.WithdrawFundsRequest; 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; import static bisq.apitest.config.BisqAppConfig.alicedaemon; import static bisq.apitest.config.BisqAppConfig.arbdaemon; import static bisq.apitest.config.BisqAppConfig.bobdaemon; import static bisq.common.app.DevEnv.DEV_PRIVILEGE_PRIV_KEY; -import static 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; @@ -70,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) { @@ -102,9 +123,12 @@ public static void startSupportingApps(boolean registerDisputeAgents, } // Convenience methods for building gRPC request objects + protected final GetBalancesRequest createGetBalancesRequest(String currencyCode) { + return GetBalancesRequest.newBuilder().setCurrencyCode(currencyCode).build(); + } - protected final GetBalanceRequest createBalanceRequest() { - return GetBalanceRequest.newBuilder().build(); + protected final GetAddressBalanceRequest createGetAddressBalanceRequest(String address) { + return GetAddressBalanceRequest.newBuilder().setAddress(address).build(); } protected final SetWalletPasswordRequest createSetWalletPasswordRequest(String password) { @@ -127,6 +151,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(); } @@ -143,8 +175,14 @@ 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, + String takerFeeCurrencyCode) { + return TakeOfferRequest.newBuilder() + .setOfferId(offerId) + .setPaymentAccountId(paymentAccountId) + .setTakerFeeCurrencyCode(takerFeeCurrencyCode) + .build(); } protected final GetTradeRequest createGetTradeRequest(String tradeId) { @@ -173,9 +211,21 @@ protected final WithdrawFundsRequest createWithdrawFundsRequest(String tradeId, } // Convenience methods for calling frequently used & thoroughly tested gRPC services. + protected final BalancesInfo getBalances(BisqAppConfig bisqAppConfig, String currencyCode) { + return grpcStubs(bisqAppConfig).walletsService.getBalances( + createGetBalancesRequest(currencyCode)).getBalances(); + } - protected final long getBalance(BisqAppConfig bisqAppConfig) { - return grpcStubs(bisqAppConfig).walletsService.getBalance(createBalanceRequest()).getBalance(); + protected final BsqBalanceInfo getBsqBalances(BisqAppConfig bisqAppConfig) { + return getBalances(bisqAppConfig, "bsq").getBsq(); + } + + protected final BtcBalanceInfo getBtcBalances(BisqAppConfig bisqAppConfig) { + return getBalances(bisqAppConfig, "btc").getBtc(); + } + + protected final AddressBalanceInfo getAddressBalance(BisqAppConfig bisqAppConfig, String address) { + return grpcStubs(bisqAppConfig).walletsService.getAddressBalance(createGetAddressBalanceRequest(address)).getAddressBalanceInfo(); } protected final void unlockWallet(BisqAppConfig bisqAppConfig, String password, long timeout) { @@ -188,6 +238,15 @@ 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) { + //noinspection ResultOfMethodCallIgnored + grpcStubs(bisqAppConfig).walletsService.sendBsq(createSendBsqRequest(address, amount)); + } + protected final String getUnusedBtcAddress(BisqAppConfig bisqAppConfig) { //noinspection OptionalGetWithoutIsPresent return grpcStubs(bisqAppConfig).walletsService.getFundingAddresses(createGetFundingAddressesRequest()) @@ -199,26 +258,53 @@ protected final String getUnusedBtcAddress(BisqAppConfig bisqAppConfig) { .getAddress(); } - 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 List getPaymentMethods(BisqAppConfig bisqAppConfig) { + var req = GetPaymentMethodsRequest.newBuilder().build(); + 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 static PaymentAccount getDefaultPerfectDummyPaymentAccount(BisqAppConfig bisqAppConfig) { - var req = GetPaymentAccountsRequest.newBuilder().build(); + protected final bisq.core.payment.PaymentAccount createPaymentAccount(BisqAppConfig bisqAppConfig, + String jsonString) { + var req = CreatePaymentAccountRequest.newBuilder() + .setPaymentAccountForm(jsonString) + .build(); var paymentAccountsService = grpcStubs(bisqAppConfig).paymentAccountsService; - PaymentAccount paymentAccount = paymentAccountsService.getPaymentAccounts(req) + // Normally, we can do asserts on the protos from the gRPC service, but in this + // case we need to return a bisq.core.payment.PaymentAccount so it can be cast + // to its sub type. + return fromProto(paymentAccountsService.createPaymentAccount(req).getPaymentAccount()); + } + + protected static List getPaymentAccounts(BisqAppConfig bisqAppConfig) { + var req = GetPaymentAccountsRequest.newBuilder().build(); + 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; } @@ -275,10 +361,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(PaymentAccount proto) { + return bisq.core.payment.PaymentAccount.fromProto(proto, CORE_PROTO_RESOLVER); + } } 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/payment/AbstractPaymentAccountTest.java b/apitest/src/test/java/bisq/apitest/method/payment/AbstractPaymentAccountTest.java new file mode 100644 index 00000000000..14b145e0444 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/payment/AbstractPaymentAccountTest.java @@ -0,0 +1,196 @@ +package bisq.apitest.method.payment; + +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; + +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.List; +import java.util.Map; +import java.util.Optional; + +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; +import static org.junit.Assert.*; + + + +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_ID = "accountId"; + static final String PROPERTY_NAME_ACCOUNT_NAME = "accountName"; + static final String PROPERTY_NAME_ACCOUNT_NR = "accountNr"; + static final String PROPERTY_NAME_ACCOUNT_TYPE = "accountType"; + static final String PROPERTY_NAME_ANSWER = "answer"; + static final String PROPERTY_NAME_BANK_ACCOUNT_NAME = "bankAccountName"; + static final String PROPERTY_NAME_BANK_ACCOUNT_NUMBER = "bankAccountNumber"; + static final String PROPERTY_NAME_BANK_ACCOUNT_TYPE = "bankAccountType"; + static final String PROPERTY_NAME_BANK_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_EMAIL = "holderEmail"; + static final String PROPERTY_NAME_HOLDER_NAME = "holderName"; + static final String PROPERTY_NAME_HOLDER_TAX_ID = "holderTaxId"; + static final String PROPERTY_NAME_IBAN = "iban"; + static final String PROPERTY_NAME_MOBILE_NR = "mobileNr"; + static final String PROPERTY_NAME_NATIONAL_ACCOUNT_ID = "nationalAccountId"; + static final String PROPERTY_NAME_PAY_ID = "payid"; + static final String PROPERTY_NAME_POSTAL_ADDRESS = "postalAddress"; + static final String PROPERTY_NAME_PROMPT_PAY_ID = "promptPayId"; + static final String PROPERTY_NAME_QUESTION = "question"; + static final String PROPERTY_NAME_REQUIREMENTS = "requirements"; + static final String PROPERTY_NAME_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 COMPLETED_FORM_MAP = new HashMap<>(); + + // A payment account serializer / deserializer. + static final PaymentAccountForm PAYMENT_ACCOUNT_FORM = new PaymentAccountForm(); + + @BeforeEach + public void setup() { + Res.setup(); + } + + protected final File getEmptyForm(TestInfo testInfo, String paymentMethodId) { + // This would normally be done in @BeforeEach, but these test cases might be + // called from a single 'scenario' test case, and the @BeforeEach -> clear() + // would be skipped. + COMPLETED_FORM_MAP.clear(); + + File emptyForm = getPaymentAccountForm(alicedaemon, paymentMethodId); + // A short cut over the API: + // File emptyForm = PAYMENT_ACCOUNT_FORM.getPaymentAccountForm(paymentMethodId); + emptyForm.deleteOnExit(); + + if (log.isDebugEnabled()) + log.debug("{} Empty form saved to {}", testName(testInfo), PAYMENT_ACCOUNT_FORM.getClickableURI(emptyForm)); + + return emptyForm; + } + + 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 void verifyCommonFormEntries(PaymentAccount paymentAccount) { + // All PaymentAccount subclasses have paymentMethodId and an accountName fields. + assertNotNull(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_PAYMENT_METHOD_ID), paymentAccount.getPaymentMethod().getId()); + assertTrue(paymentAccount.getCreationDate().getTime() > 0); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NAME), paymentAccount.getAccountName()); + } + + protected final void verifyAccountSingleTradeCurrency(String expectedCurrencyCode, PaymentAccount paymentAccount) { + assertNotNull(paymentAccount.getSingleTradeCurrency()); + assertEquals(expectedCurrencyCode, paymentAccount.getSingleTradeCurrency().getCode()); + } + + protected final void verifyAccountTradeCurrencies(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_", + ".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 : COMPLETED_FORM_MAP.entrySet()) { + String k = entry.getKey(); + Object v = entry.getValue(); + writer.name(k); + writer.value(v.toString()); + } + writer.endObject(); + writer.close(); + } catch (IOException ex) { + log.error("", ex); + fail(format("Could not write json file from form entries %s", COMPLETED_FORM_MAP)); + } + 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 new file mode 100644 index 00000000000..5e1305b0c0c --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java @@ -0,0 +1,839 @@ +/* + * 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.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; + +import java.util.Objects; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.TestMethodOrder; + +import 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; +import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; + +@Disabled +@Slf4j +@TestMethodOrder(OrderAnnotation.class) +public class CreatePaymentAccountTest extends AbstractPaymentAccountTest { + + @BeforeAll + public static void setUp() { + try { + setUpScaffold(bitcoind, alicedaemon); + } catch (Exception ex) { + fail(ex); + } + } + + @Test + public void testCreateAdvancedCashAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, ADVANCED_CASH_ID); + verifyEmptyForm(emptyForm, + ADVANCED_CASH_ID, + PROPERTY_NAME_ACCOUNT_NR); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, ADVANCED_CASH_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Advanced Cash Acct"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "0000 1111 2222"); + 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("CNY", 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 testCreateAustraliaPayidAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, AUSTRALIA_PAYID_ID); + verifyEmptyForm(emptyForm, + AUSTRALIA_PAYID_ID, + PROPERTY_NAME_BANK_ACCOUNT_NAME); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, AUSTRALIA_PAYID_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Australia Pay ID 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("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()); + if (log.isDebugEnabled()) + log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount); + } + + @Test + public void testCreateCashDepositAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, CASH_DEPOSIT_ID); + verifyEmptyForm(emptyForm, + CASH_DEPOSIT_ID, + PROPERTY_NAME_ACCOUNT_NR, + PROPERTY_NAME_ACCOUNT_TYPE, + PROPERTY_NAME_BANK_ID, + PROPERTY_NAME_BANK_NAME, + PROPERTY_NAME_BRANCH_ID, + PROPERTY_NAME_COUNTRY, + PROPERTY_NAME_HOLDER_EMAIL, + PROPERTY_NAME_HOLDER_NAME, + PROPERTY_NAME_HOLDER_TAX_ID, + PROPERTY_NAME_NATIONAL_ACCOUNT_ID, + PROPERTY_NAME_REQUIREMENTS); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, CASH_DEPOSIT_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Cash Deposit Account"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "4444 5555 6666"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_TYPE, "Checking"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_ID, "0001"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_NAME, "BoF"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_BRANCH_ID, "99-8888-7654"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "FR"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_EMAIL, "jean@johnson.info"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jean Johnson"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_TAX_ID, "123456789"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_NATIONAL_ACCOUNT_ID, "123456789"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_REQUIREMENTS, "Requirements..."); + String jsonString = getCompletedFormAsJsonString(); + CashDepositAccount paymentAccount = (CashDepositAccount) createPaymentAccount(alicedaemon, jsonString); + verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId()); + verifyAccountSingleTradeCurrency("EUR", paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY), + Objects.requireNonNull(paymentAccount.getCountry()).code); + + CashDepositAccountPayload payload = (CashDepositAccountPayload) paymentAccount.getPaymentAccountPayload(); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), payload.getAccountNr()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_TYPE), payload.getAccountType()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_ID), payload.getBankId()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), payload.getBankName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BRANCH_ID), payload.getBranchId()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_EMAIL), payload.getHolderEmail()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), payload.getHolderName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_TAX_ID), payload.getHolderTaxId()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_NATIONAL_ACCOUNT_ID), payload.getNationalAccountId()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_REQUIREMENTS), payload.getRequirements()); + if (log.isDebugEnabled()) + log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount); + } + + @Test + public void testCreateBrazilNationalBankAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, NATIONAL_BANK_ID); + verifyEmptyForm(emptyForm, + NATIONAL_BANK_ID, + PROPERTY_NAME_ACCOUNT_NR, + PROPERTY_NAME_ACCOUNT_TYPE, + PROPERTY_NAME_BANK_NAME, + PROPERTY_NAME_BRANCH_ID, + PROPERTY_NAME_COUNTRY, + PROPERTY_NAME_HOLDER_NAME, + PROPERTY_NAME_HOLDER_TAX_ID, + PROPERTY_NAME_NATIONAL_ACCOUNT_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, NATIONAL_BANK_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Banco do Brasil"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "456789-87"); + // No BankId is required for BR. + COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_NAME, "Banco do Brasil"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_BRANCH_ID, "456789-10"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "BR"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Joao da Silva"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_TAX_ID, "123456789"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_NATIONAL_ACCOUNT_ID, "123456789"); + String jsonString = getCompletedFormAsJsonString(); + NationalBankAccount paymentAccount = (NationalBankAccount) createPaymentAccount(alicedaemon, jsonString); + verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId()); + verifyAccountSingleTradeCurrency("BRL", paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY), + Objects.requireNonNull(paymentAccount.getCountry()).code); + + BankAccountPayload payload = (BankAccountPayload) paymentAccount.getPaymentAccountPayload(); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), payload.getAccountNr()); + // When no BankId is required, getBankId() returns bankName. + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), payload.getBankId()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), payload.getBankName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BRANCH_ID), payload.getBranchId()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), payload.getHolderName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_TAX_ID), payload.getHolderTaxId()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_NATIONAL_ACCOUNT_ID), payload.getNationalAccountId()); + if (log.isDebugEnabled()) + log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount); + } + + @Test + 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); + 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()); + 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()); + if (log.isDebugEnabled()) + log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount); + } + + @Test + 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); + 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()); + 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()); + if (log.isDebugEnabled()) + log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount); + } + + @Test + public void testCreateF2FAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, F2F_ID); + verifyEmptyForm(emptyForm, + F2F_ID, + PROPERTY_NAME_COUNTRY, + PROPERTY_NAME_CITY, + PROPERTY_NAME_CONTACT, + PROPERTY_NAME_EXTRA_INFO); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, F2F_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Conta Cara a Cara"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "BR"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_CITY, "Rio de Janeiro"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_CONTACT, "Freddy Beira Mar"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_EXTRA_INFO, "So fim de semana"); + String jsonString = getCompletedFormAsJsonString(); + F2FAccount paymentAccount = (F2FAccount) createPaymentAccount(alicedaemon, jsonString); + verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId()); + verifyAccountSingleTradeCurrency("BRL", paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY), + Objects.requireNonNull(paymentAccount.getCountry()).code); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_CITY), paymentAccount.getCity()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_CONTACT), paymentAccount.getContact()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EXTRA_INFO), paymentAccount.getExtraInfo()); + if (log.isDebugEnabled()) + log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount); + } + + @Test + public void testCreateFasterPaymentsAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, FASTER_PAYMENTS_ID); + verifyEmptyForm(emptyForm, + FASTER_PAYMENTS_ID, + PROPERTY_NAME_ACCOUNT_NR, + PROPERTY_NAME_SORT_CODE); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, FASTER_PAYMENTS_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Faster Payments Acct"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "9999 8888 7777"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_SORT_CODE, "3127"); + String jsonString = getCompletedFormAsJsonString(); + FasterPaymentsAccount paymentAccount = (FasterPaymentsAccount) createPaymentAccount(alicedaemon, jsonString); + verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId()); + verifyAccountSingleTradeCurrency("GBP", paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SORT_CODE), paymentAccount.getSortCode()); + 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); + 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("EUR", paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_MOBILE_NR), paymentAccount.getMobileNr()); + if (log.isDebugEnabled()) + log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount); + } + + @Test + public void testCreateInteracETransferAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, INTERAC_E_TRANSFER_ID); + verifyEmptyForm(emptyForm, + INTERAC_E_TRANSFER_ID, + PROPERTY_NAME_HOLDER_NAME, + PROPERTY_NAME_EMAIL, + PROPERTY_NAME_QUESTION, + PROPERTY_NAME_ANSWER); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, INTERAC_E_TRANSFER_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Interac Transfer Acct"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "John Doe"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "john@doe.info"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_QUESTION, "What is my dog's name?"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ANSWER, "Fido"); + String jsonString = getCompletedFormAsJsonString(); + InteracETransferAccount paymentAccount = (InteracETransferAccount) createPaymentAccount(alicedaemon, jsonString); + verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId()); + verifyAccountSingleTradeCurrency("CAD", paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_QUESTION), paymentAccount.getQuestion()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ANSWER), paymentAccount.getAnswer()); + if (log.isDebugEnabled()) + log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount); + } + + @Test + public void testCreateJapanBankAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, JAPAN_BANK_ID); + verifyEmptyForm(emptyForm, + JAPAN_BANK_ID, + PROPERTY_NAME_BANK_NAME, + PROPERTY_NAME_BANK_CODE, + PROPERTY_NAME_BANK_BRANCH_CODE, + PROPERTY_NAME_BANK_BRANCH_NAME, + PROPERTY_NAME_BANK_ACCOUNT_NAME, + PROPERTY_NAME_BANK_ACCOUNT_TYPE, + PROPERTY_NAME_BANK_ACCOUNT_NUMBER); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, JAPAN_BANK_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Fukuoka Account"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_NAME, "Bank of Kyoto"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_CODE, "FKBKJPJT"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_BRANCH_CODE, "8100-8727"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_BRANCH_NAME, "Fukuoka Branch"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_ACCOUNT_NAME, "Fukuoka Account"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_ACCOUNT_TYPE, "Yen Account"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_ACCOUNT_NUMBER, "8100-8727-0000"); + String jsonString = getCompletedFormAsJsonString(); + JapanBankAccount paymentAccount = (JapanBankAccount) createPaymentAccount(alicedaemon, jsonString); + verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId()); + verifyAccountSingleTradeCurrency("JPY", paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_CODE), paymentAccount.getBankCode()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), paymentAccount.getBankName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_BRANCH_CODE), paymentAccount.getBankBranchCode()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_BRANCH_NAME), paymentAccount.getBankBranchName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_ACCOUNT_NAME), paymentAccount.getBankAccountName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_ACCOUNT_TYPE), paymentAccount.getBankAccountType()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_ACCOUNT_NUMBER), paymentAccount.getBankAccountNumber()); + if (log.isDebugEnabled()) + log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount); + } + + @Test + public void testCreateMoneyBeamAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, MONEY_BEAM_ID); + verifyEmptyForm(emptyForm, + MONEY_BEAM_ID, + PROPERTY_NAME_ACCOUNT_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, MONEY_BEAM_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Money Beam Acct"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_ID, "MB 0000 1111"); + String jsonString = getCompletedFormAsJsonString(); + MoneyBeamAccount paymentAccount = (MoneyBeamAccount) createPaymentAccount(alicedaemon, jsonString); + verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId()); + verifyAccountSingleTradeCurrency("EUR", paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_ID), paymentAccount.getAccountId()); + if (log.isDebugEnabled()) + log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount); + } + + @Test + public void testCreateMoneyGramAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, MONEY_GRAM_ID); + verifyEmptyForm(emptyForm, + MONEY_GRAM_ID, + PROPERTY_NAME_HOLDER_NAME, + PROPERTY_NAME_EMAIL, + PROPERTY_NAME_COUNTRY, + PROPERTY_NAME_STATE); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, MONEY_GRAM_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Money Gram Acct"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_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()); + verifyAccountTradeCurrencies(getAllMoneyGramCurrencies(), paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getFullName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY), + Objects.requireNonNull(paymentAccount.getCountry()).code); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_STATE), paymentAccount.getState()); + if (log.isDebugEnabled()) + log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount); + } + + @Test + public void testCreatePerfectMoneyAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, PERFECT_MONEY_ID); + verifyEmptyForm(emptyForm, + PERFECT_MONEY_ID, + PROPERTY_NAME_ACCOUNT_NR); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, PERFECT_MONEY_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Perfect Money Acct"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "PM 0000 1111"); + String jsonString = getCompletedFormAsJsonString(); + PerfectMoneyAccount paymentAccount = (PerfectMoneyAccount) createPaymentAccount(alicedaemon, jsonString); + verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId()); + verifyAccountSingleTradeCurrency("USD", 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 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("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()); + 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("THB", paymentAccount); + 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("GBP", paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY), + Objects.requireNonNull(paymentAccount.getCountry()).code); + SameBankAccountPayload payload = (SameBankAccountPayload) paymentAccount.getPaymentAccountPayload(); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), payload.getAccountNr()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_TYPE), payload.getAccountType()); + // The bankId == bankName because bank id is not required in the UK. + assertEquals(payload.getBankId(), payload.getBankName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), payload.getBankName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BRANCH_ID), payload.getBranchId()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), payload.getHolderName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_TAX_ID), payload.getHolderTaxId()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_NATIONAL_ACCOUNT_ID), payload.getNationalAccountId()); + 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); + 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("EUR", paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_IBAN), paymentAccount.getIban()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BIC), paymentAccount.getBic()); + // bankId == bic + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BIC), paymentAccount.getBankId()); + if (log.isDebugEnabled()) + 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(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY), + Objects.requireNonNull(paymentAccount.getCountry()).code); + verifyAccountSingleTradeCurrency("EUR", paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_IBAN), paymentAccount.getIban()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BIC), paymentAccount.getBic()); + // bankId == bic + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BIC), paymentAccount.getBankId()); + if (log.isDebugEnabled()) + log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount); + } + + @Test + public void testCreateSpecificBanksAccount(TestInfo testInfo) { + // TODO Supporting set of accepted banks may require some refactoring + // of the SpecificBanksAccount and SpecificBanksAccountPayload classes, i.e., + // public void setAcceptedBanks(String... bankNames) { ... } + File emptyForm = getEmptyForm(testInfo, SPECIFIC_BANKS_ID); + verifyEmptyForm(emptyForm, + SPECIFIC_BANKS_ID, + PROPERTY_NAME_ACCOUNT_NR, + PROPERTY_NAME_ACCOUNT_TYPE, + PROPERTY_NAME_BANK_NAME, + PROPERTY_NAME_BRANCH_ID, + PROPERTY_NAME_COUNTRY, + PROPERTY_NAME_HOLDER_NAME, + PROPERTY_NAME_HOLDER_TAX_ID, + PROPERTY_NAME_NATIONAL_ACCOUNT_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, SPECIFIC_BANKS_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Specific Banks Acct"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "000 1 4567"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_TYPE, "Checking"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_NAME, "HSBC"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_BRANCH_ID, "111"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "GB"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jane Doe"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_TAX_ID, "123456789"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_NATIONAL_ACCOUNT_ID, "123456789"); + String jsonString = getCompletedFormAsJsonString(); + SpecificBanksAccount paymentAccount = (SpecificBanksAccount) createPaymentAccount(alicedaemon, jsonString); + verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId()); + verifyAccountSingleTradeCurrency("GBP", paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY), + Objects.requireNonNull(paymentAccount.getCountry()).code); + SpecificBanksAccountPayload payload = (SpecificBanksAccountPayload) paymentAccount.getPaymentAccountPayload(); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), payload.getAccountNr()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_TYPE), payload.getAccountType()); + // The bankId == bankName because bank id is not required in the UK. + assertEquals(payload.getBankId(), payload.getBankName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), payload.getBankName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BRANCH_ID), payload.getBranchId()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), payload.getHolderName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_TAX_ID), payload.getHolderTaxId()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_NATIONAL_ACCOUNT_ID), payload.getNationalAccountId()); + 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("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()); + if (log.isDebugEnabled()) + log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount); + } + + @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("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount); + } + + @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()); + verifyAccountTradeCurrencies(getAllUpholdCurrencies(), paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_ID), paymentAccount.getAccountId()); + if (log.isDebugEnabled()) + log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount); + } + + @Test + public void testCreateUSPostalMoneyOrderAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, US_POSTAL_MONEY_ORDER_ID); + verifyEmptyForm(emptyForm, + US_POSTAL_MONEY_ORDER_ID, + PROPERTY_NAME_HOLDER_NAME, + PROPERTY_NAME_POSTAL_ADDRESS); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, US_POSTAL_MONEY_ORDER_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Bubba's Acct"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Bubba"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_POSTAL_ADDRESS, "000 Westwood Terrace Austin, TX 78700"); + String jsonString = getCompletedFormAsJsonString(); + USPostalMoneyOrderAccount paymentAccount = (USPostalMoneyOrderAccount) createPaymentAccount(alicedaemon, jsonString); + verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId()); + verifyAccountSingleTradeCurrency("USD", paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_POSTAL_ADDRESS), paymentAccount.getPostalAddress()); + if (log.isDebugEnabled()) + log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount); + } + + @Test + public void testCreateWeChatPayAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, WECHAT_PAY_ID); + verifyEmptyForm(emptyForm, + WECHAT_PAY_ID, + PROPERTY_NAME_ACCOUNT_NR); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, WECHAT_PAY_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "WeChat Pay Acct"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "WC 1234"); + String jsonString = getCompletedFormAsJsonString(); + WeChatPayAccount paymentAccount = (WeChatPayAccount) createPaymentAccount(alicedaemon, jsonString); + verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId()); + verifyAccountSingleTradeCurrency("CNY", 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 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()); + verifyAccountSingleTradeCurrency("USD", paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getFullName()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_CITY), paymentAccount.getCity()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_STATE), paymentAccount.getState()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY), + Objects.requireNonNull(paymentAccount.getCountry()).code); + if (log.isDebugEnabled()) + log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount); + } + + @AfterAll + public static void tearDown() { + tearDownScaffold(); + } +} 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..52ff85b7f49 --- /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.assertEquals; +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()); + assertEquals(29, paymentMethodIds.size()); + } + + @AfterAll + public static void tearDown() { + tearDownScaffold(); + } +} 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..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; @@ -37,6 +39,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 +52,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 +62,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(); @@ -147,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 35e13c002fa..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; @@ -35,6 +37,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 +49,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 +59,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(); @@ -148,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/BsqWalletTest.java b/apitest/src/test/java/bisq/apitest/method/wallet/BsqWalletTest.java new file mode 100644 index 00000000000..d074793e5d4 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/wallet/BsqWalletTest.java @@ -0,0 +1,244 @@ +package bisq.apitest.method.wallet; + +import bisq.proto.grpc.BsqBalanceInfo; + +import org.bitcoinj.core.LegacyAddress; +import org.bitcoinj.core.NetworkParameters; + +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.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; +import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; + + + +import bisq.apitest.config.BisqAppConfig; +import bisq.apitest.method.MethodTest; + +@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, + arbdaemon, + alicedaemon, + bobdaemon); + } + + @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)); + } + + @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); + } + + @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); + } + + @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, + 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..daee479b89a --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/wallet/BtcWalletTest.java @@ -0,0 +1,107 @@ +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 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 + @Order(2) + public void testFundAlicesBtcWallet(final TestInfo testInfo) { + String newAddress = getUnusedBtcAddress(alicedaemon); + bitcoinCli.sendToAddress(newAddress, "2.5"); + genBtcBlocksThenWait(1, 1500); + + 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)))); + + // New balance is 12.5 BTC + btcBalanceInfo = getBtcBalances(alicedaemon); + 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, + 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/WalletProtectionTest.java b/apitest/src/test/java/bisq/apitest/method/wallet/WalletProtectionTest.java similarity index 89% 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..f5dabd90593 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 @@ -44,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()); } @@ -53,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()); } @@ -65,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()); } @@ -75,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()); } @@ -95,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 @@ -105,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 } @@ -124,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/PaymentAccountTest.java b/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java new file mode 100644 index 00000000000..897e8bea3d0 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java @@ -0,0 +1,90 @@ +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.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.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + + + +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 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 { + setUpScaffold(bitcoind, seednode, alicedaemon); + } catch (Exception ex) { + fail(ex); + } + } + + @Test + @Order(1) + public void testGetPaymentMethods() { + GetPaymentMethodsTest test = new GetPaymentMethodsTest(); + test.testGetPaymentMethods(); + } + + @Test + @Order(2) + public void testCreatePaymentAccount(TestInfo testInfo) { + CreatePaymentAccountTest test = new CreatePaymentAccountTest(); + + test.testCreateAdvancedCashAccount(testInfo); + test.testCreateAliPayAccount(testInfo); + test.testCreateAustraliaPayidAccount(testInfo); + test.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); + + assertEquals(EXPECTED_NUM_PAYMENT_ACCOUNTS, getPaymentAccounts(alicedaemon).size()); + } + + @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/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java b/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java index ecd38dc2295..0ef678f9bc9 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java @@ -24,55 +24,59 @@ 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.WalletProtectionTest; +import bisq.apitest.method.wallet.BsqWalletTest; +import bisq.apitest.method.wallet.BtcWalletTest; +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 testBtcWalletFunding(final TestInfo testInfo) { + BtcWalletTest btcWalletTest = new BtcWalletTest(); - String unusedAddress = getUnusedBtcAddress(alicedaemon); - bitcoinCli.sendToAddress(unusedAddress, "2.5"); + btcWalletTest.testInitialBtcBalances(testInfo); + btcWalletTest.testFundAlicesBtcWallet(testInfo); + } - bitcoinCli.generateBlocks(1); - sleep(1500); + @Test + @Order(2) + public void testBsqWalletFunding(final TestInfo testInfo) { + BsqWalletTest bsqWalletTest = new BsqWalletTest(); - balance = getBalance(alicedaemon); - assertEquals(1250000000L, balance); // new balance is 12.5 btc + bsqWalletTest.testGetUnusedBsqAddress(); + bsqWalletTest.testInitialBsqBalances(testInfo); + bsqWalletTest.testSendBsqAndCheckBalancesBeforeGeneratingBtcBlock(testInfo); + bsqWalletTest.testBalancesAfterSendingBsqAndGeneratingBtcBlock(testInfo); } @Test - @Order(2) + @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 ec0e5e71bb6..c6a420253cc 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -23,42 +23,54 @@ 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.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; +import bisq.proto.grpc.GetUnusedBsqAddressRequest; 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; import bisq.proto.grpc.SetWalletPasswordRequest; import bisq.proto.grpc.TakeOfferRequest; import bisq.proto.grpc.UnlockWalletRequest; import bisq.proto.grpc.WithdrawFundsRequest; +import protobuf.PaymentAccount; + import io.grpc.StatusRuntimeException; import joptsimple.OptionParser; import joptsimple.OptionSet; +import java.nio.file.Files; +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; +import java.io.PrintWriter; import java.math.BigDecimal; +import java.util.Date; import java.util.List; 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; -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; @@ -84,12 +96,16 @@ private enum Method { confirmpaymentreceived, keepfunds, withdrawfunds, + getpaymentmethods, + getpaymentacctform, createpaymentacct, getpaymentaccts, getversion, getbalance, getaddressbalance, getfundingaddresses, + getunusedbsqaddress, + sendbsq, lockwallet, unlockwallet, removewalletpassword, @@ -183,10 +199,25 @@ public static void run(String[] args) { return; } case getbalance: { - var request = GetBalanceRequest.newBuilder().build(); - var reply = walletsService.getBalance(request); - var btcBalance = formatSatoshis(reply.getBalance()); - out.println(btcBalance); + var currencyCode = nonOptionArgs.size() == 2 + ? nonOptionArgs.get(1) + : ""; + var request = GetBalancesRequest.newBuilder() + .setCurrencyCode(currencyCode) + .build(); + var reply = walletsService.getBalances(request); + 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: { @@ -205,11 +236,42 @@ 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 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," + " 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); @@ -223,7 +285,11 @@ 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 makerFeeCurrencyCode = nonOptionArgs.size() == 10 + ? nonOptionArgs.get(9) + : "btc"; var request = CreateOfferRequest.newBuilder() .setDirection(direction) @@ -235,6 +301,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)); @@ -278,18 +345,30 @@ 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.printf("no %s %s offers found\n", direction, currencyCode); + else + out.println(formatOfferTable(reply.getOffersList(), currencyCode)); + return; } 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()); @@ -297,7 +376,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 +432,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); @@ -364,30 +445,67 @@ 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(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(); + 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: { - 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("payment account form '%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(); - paymentAccountsService.createPaymentAccount(request); - out.printf("payment account %s saved", accountName); + .setPaymentAccountForm(jsonString) + .build(); + 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: { @@ -470,6 +588,26 @@ private static Method getMethodFromCmd(String methodName) { return Method.valueOf(methodName.toLowerCase()); } + private static File saveFileToDisk(String prefix, + @SuppressWarnings("SameParameterValue") String suffix, + String text) { + String timestamp = Long.toUnsignedString(new Date().getTime()); + String relativeFileName = prefix + "_" + timestamp + suffix; + try { + Path path = Paths.get(relativeFileName); + if (!Files.exists(path)) { + try (PrintWriter out = new PrintWriter(path.toString())) { + out.println(text); + } + return path.toAbsolutePath().toFile(); + } else { + throw new IllegalStateException(format("could not overwrite existing file '%s'", relativeFileName)); + } + } catch (FileNotFoundException e) { + throw new IllegalStateException(format("could not create file '%s'", relativeFileName)); + } + } + private static void printHelp(OptionParser parser, PrintStream stream) { try { stream.println("Bisq RPC Client"); @@ -482,23 +620,27 @@ 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 [,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"); + 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, "", "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"); - 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, "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"); 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, "createpaymentacct", "account name, account number, currency code", "Create PerfectMoney dummy account"); + 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", "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/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..a23b7a022d8 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.getBtc()) + "\n" + + "BSQ" + "\n" + + formatBsqBalanceInfoTbl(balancesInfo.getBsq()); + } + + 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/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 ""; + } +} diff --git a/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java index 7cfcc5ce152..96e2d3055d2 100644 --- a/core/src/main/java/bisq/core/api/CoreApi.java +++ b/core/src/main/java/bisq/core/api/CoreApi.java @@ -18,10 +18,13 @@ package bisq.core.api; import bisq.core.api.model.AddressBalanceInfo; +import bisq.core.api.model.BalancesInfo; +import bisq.core.btc.wallet.TxBroadcaster; import bisq.core.monetary.Price; 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; @@ -107,6 +110,7 @@ public void createAnPlaceOffer(String currencyCode, long minAmountAsLong, double buyerSecurityDeposit, String paymentAccountId, + String makerFeeCurrencyCode, Consumer resultHandler) { coreOffersService.createAndPlaceOffer(currencyCode, directionAsString, @@ -117,6 +121,7 @@ public void createAnPlaceOffer(String currencyCode, minAmountAsLong, buyerSecurityDeposit, paymentAccountId, + makerFeeCurrencyCode, resultHandler); } @@ -150,20 +155,22 @@ 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() { return paymentAccountsService.getPaymentAccounts(); } + public List getFiatPaymentMethods() { + return paymentAccountsService.getFiatPaymentMethods(); + } + + public String getPaymentAccountForm(String paymentMethodId) { + return paymentAccountsService.getPaymentAccountFormAsString(paymentMethodId); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Prices /////////////////////////////////////////////////////////////////////////////////////////// @@ -178,10 +185,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); } @@ -213,8 +222,8 @@ public String getTradeRole(String tradeId) { // Wallets /////////////////////////////////////////////////////////////////////////////////////////// - public long getAvailableBalance() { - return walletsService.getAvailableBalance(); + public BalancesInfo getBalances(String currencyCode) { + return walletsService.getBalances(currencyCode); } public long getAddressBalance(String addressString) { @@ -229,6 +238,14 @@ public List getFundingAddresses() { return walletsService.getFundingAddresses(); } + 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/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/CorePaymentAccountsService.java b/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java index a202b0dbdb4..dacb79567fd 100644 --- a/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java +++ b/core/src/main/java/bisq/core/api/CorePaymentAccountsService.java @@ -18,117 +18,65 @@ package bisq.core.api; import bisq.core.account.witness.AccountAgeWitnessService; -import bisq.core.locale.FiatCurrency; +import bisq.core.api.model.PaymentAccountForm; 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; + +import java.util.Comparator; +import java.util.List; import java.util.Set; +import java.util.stream.Collectors; 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() { return user.getPaymentAccounts(); } - private PaymentAccount getNewPaymentAccount(String paymentMethodId, - String accountName, - String accountNumber, - String currencyCode) { - PaymentAccount paymentAccount = null; - PaymentMethod paymentMethod = getPaymentMethodById(paymentMethodId); + List getFiatPaymentMethods() { + return PaymentMethod.getPaymentMethods().stream() + .filter(paymentMethod -> !paymentMethod.isAsset()) + .sorted(Comparator.comparing(PaymentMethod::getId)) + .collect(Collectors.toList()); + } - 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; - } + String getPaymentAccountFormAsString(String paymentMethodId) { + File jsonForm = getPaymentAccountForm(paymentMethodId); + return paymentAccountForm.toJsonString(jsonForm); + } - checkNotNull(paymentAccount, - "Could not create payment account with paymentMethodId " - + paymentMethodId + "."); - return paymentAccount; + File getPaymentAccountForm(String paymentMethodId) { + return paymentAccountForm.getPaymentAccountForm(paymentMethodId); } } 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/CoreWalletsService.java b/core/src/main/java/bisq/core/api/CoreWalletsService.java index fc15ec5062a..529b97a17f6 100644 --- a/core/src/main/java/bisq/core/api/CoreWalletsService.java +++ b/core/src/main/java/bisq/core/api/CoreWalletsService.java @@ -18,15 +18,29 @@ 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.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; @@ -47,6 +61,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; @@ -55,6 +70,9 @@ 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 @@ -66,9 +84,15 @@ class CoreWalletsService { @Inject 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; } @@ -78,6 +102,7 @@ KeyParameter getKey() { return tempAesKey; } + @Deprecated long getAvailableBalance() { verifyWalletsAreAvailable(); verifyEncryptedWalletIsUnlocked(); @@ -89,6 +114,23 @@ long getAvailableBalance() { return balance.getValue(); } + BalancesInfo getBalances(String currencyCode) { + verifyWalletCurrencyCodeIsValid(currencyCode); + verifyWalletsAreAvailable(); + verifyEncryptedWalletIsUnlocked(); + if (balances.getAvailableBalance().get() == null) + throw new IllegalStateException("balance is not yet available"); + + 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) { Address address = getAddressEntry(addressString).getAddress(); return btcWalletService.getBalanceForAddress(address).value; @@ -134,6 +176,27 @@ List getFundingAddresses() { .collect(Collectors.toList()); } + String getUnusedBsqAddress() { + return bsqWalletService.getUnusedBsqAddressAsString(); + } + + void sendBsq(String address, + double amount, + TxBroadcaster.Callback callback) { + 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); @@ -244,6 +307,76 @@ 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 { + 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(Double.toString(amount), 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/api/model/BalancesInfo.java b/core/src/main/java/bisq/core/api/model/BalancesInfo.java new file mode 100644 index 00000000000..3b063bc0d2b --- /dev/null +++ b/core/src/main/java/bisq/core/api/model/BalancesInfo.java @@ -0,0 +1,45 @@ +package bisq.core.api.model; + +import bisq.common.Payload; + +import lombok.Getter; + +@Getter +public class BalancesInfo implements Payload { + + // Getter names are shortened for readability's sake, i.e., + // balancesInfo.getBtc().getAvailableBalance() is cleaner than + // balancesInfo.getBtcBalanceInfo().getAvailableBalance(). + private final BsqBalanceInfo bsq; + private final BtcBalanceInfo btc; + + public BalancesInfo(BsqBalanceInfo bsq, BtcBalanceInfo btc) { + this.bsq = bsq; + this.btc = btc; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public bisq.proto.grpc.BalancesInfo toProtoMessage() { + return bisq.proto.grpc.BalancesInfo.newBuilder() + .setBsq(bsq.toProtoMessage()) + .setBtc(btc.toProtoMessage()) + .build(); + } + + public static BalancesInfo fromProto(bisq.proto.grpc.BalancesInfo proto) { + return new BalancesInfo(BsqBalanceInfo.fromProto(proto.getBsq()), + BtcBalanceInfo.fromProto(proto.getBtc())); + } + + @Override + public String toString() { + return "BalancesInfo{" + "\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 new file mode 100644 index 00000000000..23324e21f33 --- /dev/null +++ b/core/src/main/java/bisq/core/api/model/BsqBalanceInfo.java @@ -0,0 +1,94 @@ +package bisq.core.api.model; + +import bisq.common.Payload; + +import com.google.common.annotations.VisibleForTesting; + +import lombok.Getter; + +@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; + 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..e3803b0001e --- /dev/null +++ b/core/src/main/java/bisq/core/api/model/BtcBalanceInfo.java @@ -0,0 +1,75 @@ +package bisq.core.api.model; + +import bisq.common.Payload; + +import com.google.common.annotations.VisibleForTesting; + +import lombok.Getter; + +@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; + 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 + + '}'; + } +} 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/api/model/PaymentAccountForm.java b/core/src/main/java/bisq/core/api/model/PaymentAccountForm.java new file mode 100644 index 00000000000..b82656b4330 --- /dev/null +++ b/core/src/main/java/bisq/core/api/model/PaymentAccountForm.java @@ -0,0 +1,242 @@ +/* + * 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 org.apache.commons.lang3.StringUtils; + +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)}, or + * a json string to {@link bisq.core.api.model.PaymentAccountForm#toPaymentAccount(String)} + * 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) { + 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; + } + + /** + * 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. + */ + @SuppressWarnings("unused") + @VisibleForTesting + public PaymentAccount toPaymentAccount(File jsonForm) { + 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(jsonString, 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) { + 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 jsonFile) { + try { + return new URI("file", + "", + jsonFile.toURI().getPath(), + null, + null); + } catch (URISyntaxException 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; + 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) { + 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; + } + + private Class getPaymentAccountClassFromJson(String json) { + Map jsonMap = gsonBuilder.create().fromJson(json, (Type) Object.class); + String paymentMethodId = checkNotNull((String) jsonMap.get("paymentMethodId"), + format("cannot not find a paymentMethodId in json string: %s", json)); + return getPaymentAccountClass(paymentMethodId); + } + + private Class getPaymentAccountClass(String paymentMethodId) { + PaymentMethod paymentMethod = getPaymentMethodById(paymentMethodId); + return PaymentAccountFactory.getPaymentAccount(paymentMethod).getClass(); + } +} 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..13646d3f0d0 --- /dev/null +++ b/core/src/main/java/bisq/core/api/model/PaymentAccountTypeAdapter.java @@ -0,0 +1,347 @@ +/* + * 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.MoneyGramAccount; +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 org.apache.commons.lang3.StringUtils; + +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) { + 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(); + 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; + + Optional field = fieldSettersMap.keySet().stream() + .filter(k -> k.getName().equals(currentFieldName)).findFirst(); + + field.ifPresentOrElse((f) -> invokeSetterMethod(account, f, in), () -> { + throw new IllegalStateException( + 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()) + 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 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(errMsg); + } + } catch (IllegalAccessException | InvocationTargetException 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( + format("programmer error: cannot de-serialize json to a '%s' " + + " because there is no setter method 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) { + String errMsg = "cannot see next string in json reader"; + log.error(StringUtils.capitalize(errMsg) + ".", ex); + throw new IllegalStateException("programmer error: " + errMsg); + } + } + + @SuppressWarnings("unused") + private Long nextLongOrNull(JsonReader in) { + try { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } else { + return in.nextLong(); + } + } catch (IOException ex) { + String errMsg = "cannot see next long in json reader"; + log.error(StringUtils.capitalize(errMsg) + ".", ex); + throw new IllegalStateException("programmer error: " + errMsg); + } + } + + 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 (!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); + } else if (account.isMoneyGramAccount()) { + ((MoneyGramAccount) account).setCountry(country.get()); + } else { + 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 { + throw new IllegalArgumentException( + format("'%s' is an invalid country code.", countryCode)); + } + } + + private Class getPaymentAccountPayloadType() { + try { + Package pkg = PaymentAccountPayload.class.getPackage(); + //noinspection unchecked + return (Class) Class.forName(pkg.getName() + + "." + paymentAccountType.getSimpleName() + "Payload"); + } catch (Exception 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); + } + } + + private PaymentAccount initNewPaymentAccount() { + try { + Constructor constructor = paymentAccountType.getDeclaredConstructor(); + PaymentAccount paymentAccount = (PaymentAccount) constructor.newInstance(); + paymentAccount.init(); + return paymentAccount; + } 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); + } + } +} 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..38e4c4b5423 --- /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.getFeePerVbyte(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/core/src/main/java/bisq/core/offer/OfferUtil.java b/core/src/main/java/bisq/core/offer/OfferUtil.java index e85881c2112..50fd938d6e9 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/core/src/main/java/bisq/core/payment/PaymentAccount.java b/core/src/main/java/bisq/core/payment/PaymentAccount.java index b38649ef942..b61b86d482b 100644 --- a/core/src/main/java/bisq/core/payment/PaymentAccount.java +++ b/core/src/main/java/bisq/core/payment/PaymentAccount.java @@ -173,10 +173,18 @@ public String getOwnerId() { return paymentAccountPayload.getOwnerId(); } + public boolean isCountryBasedPaymentAccount() { + return this instanceof CountryBasedPaymentAccount; + } + 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 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/GrpcPaymentAccountsService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java index 91060cbc829..0438d33655d 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcPaymentAccountsService.java @@ -19,13 +19,20 @@ 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.GetPaymentAccountFormReply; +import bisq.proto.grpc.GetPaymentAccountFormRequest; 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.Status; +import io.grpc.StatusRuntimeException; import io.grpc.stub.StreamObserver; import javax.inject.Inject; @@ -45,24 +52,86 @@ 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(); - 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) { + 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) { + 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; + } } } 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() diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java index 1b5cb42e4cc..110e4b5cc65 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java @@ -19,17 +19,23 @@ import bisq.core.api.CoreApi; import bisq.core.api.model.AddressBalanceInfo; +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.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; 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; @@ -40,11 +46,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; @@ -54,17 +65,13 @@ 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. @Override - public void getBalance(GetBalanceRequest req, StreamObserver responseObserver) { + public void getBalances(GetBalancesRequest req, StreamObserver responseObserver) { try { - long availableBalance = coreApi.getAvailableBalance(); - var reply = GetBalanceReply.newBuilder().setBalance(availableBalance).build(); + var balances = coreApi.getBalances(req.getCurrencyCode()); + var reply = GetBalancesReply.newBuilder() + .setBalances(balances.toProtoMessage()) + .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (IllegalStateException cause) { @@ -110,6 +117,52 @@ public void getFundingAddresses(GetFundingAddressesRequest req, } } + @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 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) { diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index d9e0a3973d8..b20a508ddea 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; } /////////////////////////////////////////////////////////////////////////////////////////// @@ -123,17 +125,18 @@ service PaymentAccounts { } rpc GetPaymentAccounts (GetPaymentAccountsRequest) returns (GetPaymentAccountsReply) { } + rpc GetPaymentMethods (GetPaymentMethodsRequest) returns (GetPaymentMethodsReply) { + } + rpc GetPaymentAccountForm (GetPaymentAccountFormRequest) returns (GetPaymentAccountFormReply) { + } } 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 { @@ -143,6 +146,21 @@ message GetPaymentAccountsReply { repeated PaymentAccount paymentAccounts = 1; } +message GetPaymentMethodsRequest { +} + +message GetPaymentMethodsReply { + repeated PaymentMethod paymentMethods = 1; +} + +message GetPaymentAccountFormRequest { + string paymentMethodId = 1; +} + +message GetPaymentAccountFormReply { + string paymentAccountFormJson = 1; +} + /////////////////////////////////////////////////////////////////////////////////////////// // Price /////////////////////////////////////////////////////////////////////////////////////////// @@ -198,6 +216,7 @@ service Trades { message TakeOfferRequest { string offerId = 1; string paymentAccountId = 2; + string takerFeeCurrencyCode = 3; } message TakeOfferReply { @@ -273,10 +292,14 @@ message TradeInfo { /////////////////////////////////////////////////////////////////////////////////////////// service Wallets { - rpc GetBalance (GetBalanceRequest) returns (GetBalanceReply) { + rpc GetBalances (GetBalancesRequest) returns (GetBalancesReply) { } rpc GetAddressBalance (GetAddressBalanceRequest) returns (GetAddressBalanceReply) { } + rpc GetUnusedBsqAddress (GetUnusedBsqAddressRequest) returns (GetUnusedBsqAddressReply) { + } + rpc SendBsq (SendBsqRequest) returns (SendBsqReply) { + } rpc GetFundingAddresses (GetFundingAddressesRequest) returns (GetFundingAddressesReply) { } rpc SetWalletPassword (SetWalletPasswordRequest) returns (SetWalletPasswordReply) { @@ -289,11 +312,12 @@ service Wallets { } } -message GetBalanceRequest { +message GetBalancesRequest { + string currencyCode = 1; } -message GetBalanceReply { - uint64 balance = 1; +message GetBalancesReply { + BalancesInfo balances = 1; } message GetAddressBalanceRequest { @@ -304,6 +328,21 @@ message GetAddressBalanceReply { AddressBalanceInfo addressBalanceInfo = 1; } +message GetUnusedBsqAddressRequest { +} + +message GetUnusedBsqAddressReply { + string address = 1; +} + +message SendBsqRequest { + string address = 1; + double amount = 2; +} + +message SendBsqReply { +} + message GetFundingAddressesRequest { } @@ -340,6 +379,30 @@ message UnlockWalletRequest { message UnlockWalletReply { } +message BalancesInfo { + // 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 { + 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;