diff --git a/apitest/docs/build-run.md b/apitest/docs/build-run.md index 308fe02cf66..9d1165ca88e 100644 --- a/apitest/docs/build-run.md +++ b/apitest/docs/build-run.md @@ -48,7 +48,7 @@ To run all test cases in a package: To run a single test case: - $ ./gradlew :apitest:test --tests "bisq.apitest.method.GetBalanceTest" -DrunApiTests=true + $ ./gradlew :apitest:test --tests "bisq.apitest.method.wallet.GetBalanceTest" -DrunApiTests=true To run test cases from Intellij, add two JVM arguments to your JUnit launchers: diff --git a/apitest/src/test/java/bisq/apitest/method/MethodTest.java b/apitest/src/test/java/bisq/apitest/method/MethodTest.java index 43073ba995b..f9275ffbee5 100644 --- a/apitest/src/test/java/bisq/apitest/method/MethodTest.java +++ b/apitest/src/test/java/bisq/apitest/method/MethodTest.java @@ -17,21 +17,31 @@ package bisq.apitest.method; +import bisq.proto.grpc.AddressBalanceInfo; +import bisq.proto.grpc.BalancesInfo; +import bisq.proto.grpc.BsqBalanceInfo; +import bisq.proto.grpc.BtcBalanceInfo; import bisq.proto.grpc.CancelOfferRequest; import bisq.proto.grpc.ConfirmPaymentReceivedRequest; import bisq.proto.grpc.ConfirmPaymentStartedRequest; import bisq.proto.grpc.CreatePaymentAccountRequest; +import bisq.proto.grpc.GetAddressBalanceRequest; import bisq.proto.grpc.GetBalanceRequest; +import bisq.proto.grpc.GetBalancesRequest; +import bisq.proto.grpc.GetBsqBalancesRequest; +import bisq.proto.grpc.GetBtcBalancesRequest; import bisq.proto.grpc.GetFundingAddressesRequest; import bisq.proto.grpc.GetOfferRequest; import bisq.proto.grpc.GetPaymentAccountsRequest; 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; @@ -103,10 +113,27 @@ public static void startSupportingApps(boolean registerDisputeAgents, // Convenience methods for building gRPC request objects + @Deprecated protected final GetBalanceRequest createBalanceRequest() { return GetBalanceRequest.newBuilder().build(); } + protected final GetBalancesRequest createGetBalancesRequest() { + return GetBalancesRequest.newBuilder().build(); + } + + protected final GetAddressBalanceRequest createGetAddressBalanceRequest(String address) { + return GetAddressBalanceRequest.newBuilder().setAddress(address).build(); + } + + protected final GetBsqBalancesRequest createGetBsqBalancesRequest() { + return GetBsqBalancesRequest.newBuilder().build(); + } + + protected final GetBtcBalancesRequest createBtcBalancesRequest() { + return GetBtcBalancesRequest.newBuilder().build(); + } + protected final SetWalletPasswordRequest createSetWalletPasswordRequest(String password) { return SetWalletPasswordRequest.newBuilder().setPassword(password).build(); } @@ -127,6 +154,14 @@ protected final LockWalletRequest createLockWalletRequest() { return LockWalletRequest.newBuilder().build(); } + protected final GetUnusedBsqAddressRequest createGetUnusedBsqAddressRequest() { + return GetUnusedBsqAddressRequest.newBuilder().build(); + } + + protected final SendBsqRequest createSendBsqRequest(String address, double amount) { + return SendBsqRequest.newBuilder().setAddress(address).setAmount(amount).build(); + } + protected final GetFundingAddressesRequest createGetFundingAddressesRequest() { return GetFundingAddressesRequest.newBuilder().build(); } @@ -143,8 +178,12 @@ protected final CancelOfferRequest createCancelOfferRequest(String offerId) { return CancelOfferRequest.newBuilder().setId(offerId).build(); } - protected final TakeOfferRequest createTakeOfferRequest(String offerId, String paymentAccountId) { - return TakeOfferRequest.newBuilder().setOfferId(offerId).setPaymentAccountId(paymentAccountId).build(); + protected final TakeOfferRequest createTakeOfferRequest(String offerId, + String paymentAccountId) { + return TakeOfferRequest.newBuilder() + .setOfferId(offerId) + .setPaymentAccountId(paymentAccountId) + .build(); } protected final GetTradeRequest createGetTradeRequest(String tradeId) { @@ -174,10 +213,27 @@ protected final WithdrawFundsRequest createWithdrawFundsRequest(String tradeId, // Convenience methods for calling frequently used & thoroughly tested gRPC services. + @Deprecated protected final long getBalance(BisqAppConfig bisqAppConfig) { return grpcStubs(bisqAppConfig).walletsService.getBalance(createBalanceRequest()).getBalance(); } + protected final BalancesInfo getBalances(BisqAppConfig bisqAppConfig) { + return grpcStubs(bisqAppConfig).walletsService.getBalances(createGetBalancesRequest()).getBalances(); + } + + protected final BsqBalanceInfo getBsqBalances(BisqAppConfig bisqAppConfig) { + return grpcStubs(bisqAppConfig).walletsService.getBsqBalances(createGetBsqBalancesRequest()).getBsqBalanceInfo(); + } + + protected final BtcBalanceInfo getBtcBalances(BisqAppConfig bisqAppConfig) { + return grpcStubs(bisqAppConfig).walletsService.getBtcBalances(createBtcBalancesRequest()).getBtcBalanceInfo(); + } + + protected final AddressBalanceInfo getAddressBalance(BisqAppConfig bisqAppConfig, String address) { + return grpcStubs(bisqAppConfig).walletsService.getAddressBalance(createGetAddressBalanceRequest(address)).getAddressBalanceInfo(); + } + protected final void unlockWallet(BisqAppConfig bisqAppConfig, String password, long timeout) { //noinspection ResultOfMethodCallIgnored grpcStubs(bisqAppConfig).walletsService.unlockWallet(createUnlockWalletRequest(password, timeout)); @@ -188,6 +244,14 @@ protected final void lockWallet(BisqAppConfig bisqAppConfig) { grpcStubs(bisqAppConfig).walletsService.lockWallet(createLockWalletRequest()); } + protected final String getUnusedBsqAddress(BisqAppConfig bisqAppConfig) { + return grpcStubs(bisqAppConfig).walletsService.getUnusedBsqAddress(createGetUnusedBsqAddressRequest()).getAddress(); + } + + protected final void sendBsq(BisqAppConfig bisqAppConfig, String address, double amount) { + grpcStubs(bisqAppConfig).walletsService.sendBsq(createSendBsqRequest(address, amount)); + } + protected final String getUnusedBtcAddress(BisqAppConfig bisqAppConfig) { //noinspection OptionalGetWithoutIsPresent return grpcStubs(bisqAppConfig).walletsService.getFundingAddresses(createGetFundingAddressesRequest()) diff --git a/apitest/src/test/java/bisq/apitest/method/wallet/BsqWalletTest.java b/apitest/src/test/java/bisq/apitest/method/wallet/BsqWalletTest.java new file mode 100644 index 00000000000..861c1839c5b --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/wallet/BsqWalletTest.java @@ -0,0 +1,246 @@ +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); + } + + @Disabled // TODO + @Test + @Order(3) + public void testSendBsqAndCheckBalancesBeforeGeneratingBtcBlock(final TestInfo testInfo) { + String bobsBsqAddress = getUnusedBsqAddress(bobdaemon); + sendBsq(alicedaemon, bobsBsqAddress, SEND_BSQ_AMOUNT); + sleep(2000); + + BsqBalanceInfo alicesBsqBalances = getBsqBalances(alicedaemon); + BsqBalanceInfo bobsBsqBalances = waitForNonZeroUnverifiedBalance(bobdaemon); + + log.info("BSQ Balances Before BTC Block Gen..."); + printBobAndAliceBsqBalances(testInfo, + bobsBsqBalances, + alicesBsqBalances, + alicedaemon); + + verifyBsqBalances(expectedBsqBalanceModel(150000000, + 2500050, + 0, + 0, + 0, + 0), + bobsBsqBalances); + + verifyBsqBalances(expectedBsqBalanceModel(97499950, + 97499950, + 97499950, + 0, + 0, + 0), + alicesBsqBalances); + } + + @Disabled // TODO + @Test + @Order(4) + public void testBalancesAfterSendingBsqAndGeneratingBtcBlock(final TestInfo testInfo) { + // There is a wallet persist delay; we have to + // wait for both wallets to be saved to disk. + genBtcBlocksThenWait(1, 4000); + + BsqBalanceInfo alicesBsqBalances = getBsqBalances(alicedaemon); + BsqBalanceInfo bobsBsqBalances = waitForNewAvailableConfirmedBalance(bobdaemon, 150000000); + + log.info("See Available Confirmed BSQ Balances..."); + printBobAndAliceBsqBalances(testInfo, + bobsBsqBalances, + alicesBsqBalances, + alicedaemon); + + verifyBsqBalances(expectedBsqBalanceModel(152500050, + 0, + 0, + 0, + 0, + 0), + bobsBsqBalances); + + verifyBsqBalances(expectedBsqBalanceModel(97499950, + 0, + 0, + 0, + 0, + 0), + alicesBsqBalances); + } + + @AfterAll + public static void tearDown() { + tearDownScaffold(); + } + + private void verifyBsqBalances(bisq.core.api.model.BsqBalanceInfo expected, + 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..380198d53ee --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/wallet/BtcWalletTest.java @@ -0,0 +1,102 @@ +package bisq.apitest.method.wallet; + +import bisq.proto.grpc.BtcBalanceInfo; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.TestMethodOrder; + +import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind; +import static bisq.apitest.config.BisqAppConfig.alicedaemon; +import static bisq.apitest.config.BisqAppConfig.bobdaemon; +import static bisq.apitest.config.BisqAppConfig.seednode; +import static bisq.cli.TableFormat.formatAddressBalanceTbl; +import static bisq.cli.TableFormat.formatBtcBalanceInfoTbl; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; + + + +import bisq.apitest.method.MethodTest; + +@Disabled +@Slf4j +@TestMethodOrder(OrderAnnotation.class) +public class BtcWalletTest extends MethodTest { + + // All api tests depend on the DAO / regtest environment, and Bob & Alice's wallets + // are initialized with 10 BTC during the scaffolding setup. + private static final bisq.core.api.model.BtcBalanceInfo INITIAL_BTC_BALANCES = + bisq.core.api.model.BtcBalanceInfo.valueOf(1000000000, + 0, + 1000000000, + 0); + + @BeforeAll + public static void setUp() { + startSupportingApps(false, + true, + bitcoind, + seednode, + alicedaemon, + bobdaemon); + } + + @Test + @Order(1) + public void testDeprecatedAvailableBtcBalance() { + // Alice's regtest Bisq wallet was initialized with 10 BTC. + long balance = getBalance(alicedaemon); // @Deprecated method + assertEquals(INITIAL_BTC_BALANCES.getAvailableBalance(), balance); + + // Bob's regtest Bisq wallet was initialized with 10 BTC. + balance = getBalance(bobdaemon); // @Deprecated method + assertEquals(INITIAL_BTC_BALANCES.getAvailableBalance(), balance); + } + + @Test + @Order(2) + public void testFundAlicesBtcWallet(final TestInfo testInfo) { + String newAddress = getUnusedBtcAddress(alicedaemon); + bitcoinCli.sendToAddress(newAddress, "2.5"); + genBtcBlocksThenWait(1, 1500); + + long balance = getBalance(alicedaemon); // @Deprecated method + assertEquals(1250000000, balance); // new balance is 12.5 btc + + log.info("{} -> Alice's Funded Address Balance -> \n{}", + testName(testInfo), + formatAddressBalanceTbl(singletonList(getAddressBalance(alicedaemon, newAddress)))); + + BtcBalanceInfo btcBalanceInfo = getBtcBalances(alicedaemon); // new balance is 12.5 btc + bisq.core.api.model.BtcBalanceInfo alicesExpectedBalances = + bisq.core.api.model.BtcBalanceInfo.valueOf(1250000000, + 0, + 1250000000, + 0); + verifyBtcBalances(alicesExpectedBalances, btcBalanceInfo); + log.info("{} -> Alice's BTC Balances After Sending 2.5 BTC -> \n{}", + testName(testInfo), + formatBtcBalanceInfoTbl(btcBalanceInfo)); + } + + @AfterAll + public static void tearDown() { + tearDownScaffold(); + } + + private void verifyBtcBalances(bisq.core.api.model.BtcBalanceInfo expected, + 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/GetBalanceTest.java b/apitest/src/test/java/bisq/apitest/method/wallet/GetBalanceTest.java similarity index 96% rename from apitest/src/test/java/bisq/apitest/method/GetBalanceTest.java rename to apitest/src/test/java/bisq/apitest/method/wallet/GetBalanceTest.java index 1d44590837b..2c7c3466dda 100644 --- a/apitest/src/test/java/bisq/apitest/method/GetBalanceTest.java +++ b/apitest/src/test/java/bisq/apitest/method/wallet/GetBalanceTest.java @@ -15,7 +15,7 @@ * along with Bisq. If not, see . */ -package bisq.apitest.method; +package bisq.apitest.method.wallet; import bisq.proto.grpc.GetBalanceRequest; @@ -36,6 +36,11 @@ import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; + + +import bisq.apitest.method.MethodTest; + +@Deprecated @Disabled @Slf4j @TestMethodOrder(OrderAnnotation.class) diff --git a/apitest/src/test/java/bisq/apitest/method/wallet/WalletBalancesTest.java b/apitest/src/test/java/bisq/apitest/method/wallet/WalletBalancesTest.java new file mode 100644 index 00000000000..6c168843668 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/wallet/WalletBalancesTest.java @@ -0,0 +1,79 @@ +package bisq.apitest.method.wallet; + +import bisq.proto.grpc.BalancesInfo; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.TestMethodOrder; + +import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind; +import static bisq.apitest.config.BisqAppConfig.alicedaemon; +import static bisq.apitest.config.BisqAppConfig.bobdaemon; +import static bisq.apitest.config.BisqAppConfig.seednode; +import static org.junit.jupiter.api.Assertions.assertEquals; + + + +import bisq.apitest.method.MethodTest; +import bisq.cli.TableFormat; + +@Disabled +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class WalletBalancesTest extends MethodTest { + + // All api tests depend on the DAO / regtest environment, and Bob & Alice's wallets + // are initialized with 10 BTC during the scaffolding setup. + private static final bisq.core.api.model.BtcBalanceInfo INITIAL_BTC_BALANCES = + bisq.core.api.model.BtcBalanceInfo.valueOf(1000000000, + 0, + 1000000000, + 0); + + @BeforeAll + public static void setUp() { + startSupportingApps(false, + true, + bitcoind, + seednode, + alicedaemon, + bobdaemon); + } + + @Test + @Order(1) + public void testDeprecatedAvailableBtcBalance() { + // Alice's regtest Bisq wallet was initialized with 10 BTC. + long balance = getBalance(alicedaemon); // @Deprecated method + assertEquals(INITIAL_BTC_BALANCES.getAvailableBalance(), balance); + + // Bob's regtest Bisq wallet was initialized with 10 BTC. + balance = getBalance(bobdaemon); // @Deprecated method + assertEquals(INITIAL_BTC_BALANCES.getAvailableBalance(), balance); + } + + @Test + @Order(2) + public void testNewGetBalances(final TestInfo testInfo) { + BalancesInfo alicesBalances = getBalances(alicedaemon); + BalancesInfo bobsBalances = getBalances(bobdaemon); + + log.info("{} Alice's Balances:\n{}", testName(testInfo), TableFormat.formatBalancesTbls(alicesBalances)); + log.info("{} Bob's Balances:\n{}", testName(testInfo), TableFormat.formatBalancesTbls(bobsBalances)); + + assertEquals(INITIAL_BTC_BALANCES.getAvailableBalance(), alicesBalances.getBtcBalanceInfo().getAvailableBalance()); + assertEquals(INITIAL_BTC_BALANCES.getAvailableBalance(), bobsBalances.getBtcBalanceInfo().getAvailableBalance()); + } + + @AfterAll + public static void tearDown() { + tearDownScaffold(); + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/WalletProtectionTest.java b/apitest/src/test/java/bisq/apitest/method/wallet/WalletProtectionTest.java similarity index 98% rename from apitest/src/test/java/bisq/apitest/method/WalletProtectionTest.java rename to apitest/src/test/java/bisq/apitest/method/wallet/WalletProtectionTest.java index 08547e9ebb9..82174f5195a 100644 --- a/apitest/src/test/java/bisq/apitest/method/WalletProtectionTest.java +++ b/apitest/src/test/java/bisq/apitest/method/wallet/WalletProtectionTest.java @@ -1,4 +1,4 @@ -package bisq.apitest.method; +package bisq.apitest.method.wallet; import io.grpc.StatusRuntimeException; @@ -18,6 +18,10 @@ import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; + + +import bisq.apitest.method.MethodTest; + @SuppressWarnings("ResultOfMethodCallIgnored") @Disabled @Slf4j diff --git a/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java b/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java index ecd38dc2295..f6240512230 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/WalletTest.java @@ -24,55 +24,69 @@ import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestMethodOrder; import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind; import static bisq.apitest.config.BisqAppConfig.alicedaemon; +import static bisq.apitest.config.BisqAppConfig.arbdaemon; +import static bisq.apitest.config.BisqAppConfig.bobdaemon; import static bisq.apitest.config.BisqAppConfig.seednode; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.fail; import bisq.apitest.method.MethodTest; -import bisq.apitest.method.WalletProtectionTest; +import bisq.apitest.method.wallet.BsqWalletTest; +import bisq.apitest.method.wallet.BtcWalletTest; +import bisq.apitest.method.wallet.WalletBalancesTest; +import bisq.apitest.method.wallet.WalletProtectionTest; @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class WalletTest extends MethodTest { - // All tests depend on the DAO / regtest environment, and Alice's wallet is - // initialized with 10 BTC during the scaffolding setup. - @BeforeAll public static void setUp() { - try { - setUpScaffold(bitcoind, seednode, alicedaemon); - genBtcBlocksThenWait(1, 1500); - } catch (Exception ex) { - fail(ex); - } + startSupportingApps(true, + true, + bitcoind, + seednode, + arbdaemon, + alicedaemon, + bobdaemon); } @Test @Order(1) - public void testFundWallet() { - // The regtest Bisq wallet was initialized with 10 BTC. - long balance = getBalance(alicedaemon); - assertEquals(1000000000, balance); + public void testGetWalletBalances(final TestInfo testInfo) { + WalletBalancesTest btcWalletTest = new WalletBalancesTest(); + + btcWalletTest.testDeprecatedAvailableBtcBalance(); + btcWalletTest.testNewGetBalances(testInfo); + } - String unusedAddress = getUnusedBtcAddress(alicedaemon); - bitcoinCli.sendToAddress(unusedAddress, "2.5"); + @Test + @Order(2) + public void testBtcWalletFunding(final TestInfo testInfo) { + BtcWalletTest btcWalletTest = new BtcWalletTest(); - bitcoinCli.generateBlocks(1); - sleep(1500); + btcWalletTest.testDeprecatedAvailableBtcBalance(); + btcWalletTest.testFundAlicesBtcWallet(testInfo); + } - balance = getBalance(alicedaemon); - assertEquals(1250000000L, balance); // new balance is 12.5 btc + @Test + @Order(3) + public void testBsqWalletFunding(final TestInfo testInfo) { + BsqWalletTest bsqWalletTest = new BsqWalletTest(); + + bsqWalletTest.testGetUnusedBsqAddress(); + bsqWalletTest.testInitialBsqBalances(testInfo); + //bsqWalletTest.testSendBsqAndCheckBalancesBeforeGeneratingBtcBlock(testInfo); // TODO + //bsqWalletTest.testBalancesAfterSendingBsqAndGeneratingBtcBlock(testInfo); // TODO } @Test - @Order(2) + @Order(4) public void testWalletProtection() { // Batching all wallet tests in this test case reduces scaffold setup // time. Here, we create a method WalletProtectionTest instance and run each diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index ec0e5e71bb6..2226d352004 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -24,11 +24,15 @@ import bisq.proto.grpc.CreatePaymentAccountRequest; import bisq.proto.grpc.GetAddressBalanceRequest; import bisq.proto.grpc.GetBalanceRequest; +import bisq.proto.grpc.GetBalancesRequest; +import bisq.proto.grpc.GetBsqBalancesRequest; +import bisq.proto.grpc.GetBtcBalancesRequest; import bisq.proto.grpc.GetFundingAddressesRequest; import bisq.proto.grpc.GetOfferRequest; import bisq.proto.grpc.GetOffersRequest; import bisq.proto.grpc.GetPaymentAccountsRequest; import bisq.proto.grpc.GetTradeRequest; +import bisq.proto.grpc.GetUnusedBsqAddressRequest; import bisq.proto.grpc.GetVersionRequest; import bisq.proto.grpc.KeepFundsRequest; import bisq.proto.grpc.LockWalletRequest; @@ -57,6 +61,7 @@ import static bisq.cli.CurrencyFormat.toSatoshis; import static bisq.cli.NegativeNumberOptions.hasNegativeNumberOptions; import static bisq.cli.TableFormat.formatAddressBalanceTbl; +import static bisq.cli.TableFormat.formatBalancesTbls; import static bisq.cli.TableFormat.formatOfferTable; import static bisq.cli.TableFormat.formatPaymentAcctTbl; import static java.lang.String.format; @@ -87,9 +92,13 @@ private enum Method { createpaymentacct, getpaymentaccts, getversion, - getbalance, + @Deprecated getbalance, // Use getbalances, return bsq and btc balance info + getbalances, + getbsqbalance, + getbtcbalance, getaddressbalance, getfundingaddresses, + getunusedbsqaddress, lockwallet, unlockwallet, removewalletpassword, @@ -183,12 +192,31 @@ public static void run(String[] args) { return; } case getbalance: { + // Deprecated, use getbalances. var request = GetBalanceRequest.newBuilder().build(); var reply = walletsService.getBalance(request); var btcBalance = formatSatoshis(reply.getBalance()); out.println(btcBalance); return; } + case getbalances: { + var request = GetBalancesRequest.newBuilder().build(); + var reply = walletsService.getBalances(request); + out.println(formatBalancesTbls(reply.getBalances())); + return; + } + case getbsqbalance: { + var request = GetBsqBalancesRequest.newBuilder().build(); + var reply = walletsService.getBsqBalances(request); + out.println(reply.getBsqBalanceInfo()); + return; + } + case getbtcbalance: { + var request = GetBtcBalancesRequest.newBuilder().build(); + var reply = walletsService.getBtcBalances(request); + out.println(reply.getBtcBalanceInfo()); + return; + } case getaddressbalance: { if (nonOptionArgs.size() < 2) throw new IllegalArgumentException("no address specified"); @@ -205,6 +233,12 @@ public static void run(String[] args) { out.println(formatAddressBalanceTbl(reply.getAddressBalanceInfoList())); return; } + case getunusedbsqaddress: { + var request = GetUnusedBsqAddressRequest.newBuilder().build(); + var reply = walletsService.getUnusedBsqAddress(request); + out.println(reply.getAddress()); + return; + } case createoffer: { if (nonOptionArgs.size() < 9) throw new IllegalArgumentException("incorrect parameter count," @@ -223,6 +257,7 @@ public static void run(String[] args) { marketPriceMargin = new BigDecimal(nonOptionArgs.get(7)); else fixedPrice = nonOptionArgs.get(7); + var securityDeposit = new BigDecimal(nonOptionArgs.get(8)); var request = CreateOfferRequest.newBuilder() @@ -283,7 +318,8 @@ public static void run(String[] args) { } case takeoffer: { if (nonOptionArgs.size() < 3) - throw new IllegalArgumentException("incorrect parameter count, expecting offer id, payment acct id"); + throw new IllegalArgumentException("incorrect parameter count, " + + " expecting offer id, payment acct id"); var offerId = nonOptionArgs.get(1); var paymentAccountId = nonOptionArgs.get(2); @@ -297,7 +333,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 +389,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); @@ -482,9 +520,13 @@ private static void printHelp(OptionParser parser, PrintStream stream) { stream.format(rowFormat, "Method", "Params", "Description"); stream.format(rowFormat, "------", "------", "------------"); stream.format(rowFormat, "getversion", "", "Get server version"); - stream.format(rowFormat, "getbalance", "", "Get server wallet balance"); + stream.format(rowFormat, "getbalance", "", "Get server wallet balance (deprecated, use getbalances"); + stream.format(rowFormat, "getbalances", "", "Get server wallet bsq and btc balances"); + stream.format(rowFormat, "getbsqbalance", "", "Get server wallet bsq balance"); + stream.format(rowFormat, "getbtcbalance", "", "Get server wallet btc balance"); stream.format(rowFormat, "getaddressbalance", "address", "Get server wallet address balance"); stream.format(rowFormat, "getfundingaddresses", "", "Get BTC funding addresses"); + stream.format(rowFormat, "getunusedbsqaddress", "", "Get unused BSQ address"); stream.format(rowFormat, "createoffer", "payment acct id, buy | sell, currency code, \\", "Create and place an offer"); stream.format(rowFormat, "", "amount (btc), min amount, use mkt based price, \\", ""); stream.format(rowFormat, "", "fixed price (btc) | mkt price margin (%), \\", ""); @@ -493,7 +535,7 @@ private static void printHelp(OptionParser parser, PrintStream stream) { stream.format(rowFormat, "getoffer", "offer id", "Get current offer with id"); stream.format(rowFormat, "getoffers", "buy | sell, currency code", "Get current offers"); stream.format(rowFormat, "takeoffer", "offer id", "Take offer with id"); - stream.format(rowFormat, "gettrade", "trade id [,showcontract]", "Get trade summary or full contract"); + stream.format(rowFormat, "gettrade", "trade id [,showcontract = true|false]", "Get trade summary or full contract"); stream.format(rowFormat, "confirmpaymentstarted", "trade id", "Confirm payment started"); stream.format(rowFormat, "confirmpaymentreceived", "trade id", "Confirm payment received"); stream.format(rowFormat, "keepfunds", "trade id", "Keep received funds in Bisq wallet"); diff --git a/cli/src/main/java/bisq/cli/ColumnHeaderConstants.java b/cli/src/main/java/bisq/cli/ColumnHeaderConstants.java index 03500e4f47a..59b6230a2eb 100644 --- a/cli/src/main/java/bisq/cli/ColumnHeaderConstants.java +++ b/cli/src/main/java/bisq/cli/ColumnHeaderConstants.java @@ -29,9 +29,18 @@ class ColumnHeaderConstants { // such as COL_HEADER_CREATION_DATE, COL_HEADER_VOLUME and COL_HEADER_UUID, the // expected max data string length is accounted for. In others, the column header length // are expected to be greater than any column value length. - static final String COL_HEADER_ADDRESS = padEnd("Address", 34, ' '); + static final String COL_HEADER_ADDRESS = padEnd("%-3s Address", 52, ' '); static final String COL_HEADER_AMOUNT = padEnd("BTC(min - max)", 24, ' '); - static final String COL_HEADER_BALANCE = padStart("Balance", 12, ' '); + static final String COL_HEADER_AVAILABLE_BALANCE = "Available Balance"; + static final String COL_HEADER_AVAILABLE_CONFIRMED_BALANCE = "Available Confirmed Balance"; + static final String COL_HEADER_UNCONFIRMED_CHANGE_BALANCE = "Unconfirmed Change Balance"; + static final String COL_HEADER_RESERVED_BALANCE = "Reserved Balance"; + static final String COL_HEADER_TOTAL_AVAILABLE_BALANCE = "Total Available Balance"; + static final String COL_HEADER_LOCKED_BALANCE = "Locked Balance"; + static final String COL_HEADER_LOCKED_FOR_VOTING_BALANCE = "Locked For Voting Balance"; + static final String COL_HEADER_LOCKUP_BONDS_BALANCE = "Lockup Bonds Balance"; + static final String COL_HEADER_UNLOCKING_BONDS_BALANCE = "Unlocking Bonds Balance"; + static final String COL_HEADER_UNVERIFIED_BALANCE = "Unverified Balance"; static final String COL_HEADER_CONFIRMATIONS = "Confirmations"; static final String COL_HEADER_CREATION_DATE = padEnd("Creation Date (UTC)", 20, ' '); static final String COL_HEADER_CURRENCY = "Currency"; diff --git a/cli/src/main/java/bisq/cli/CurrencyFormat.java b/cli/src/main/java/bisq/cli/CurrencyFormat.java index e4d8f89c6c7..a4766690eff 100644 --- a/cli/src/main/java/bisq/cli/CurrencyFormat.java +++ b/cli/src/main/java/bisq/cli/CurrencyFormat.java @@ -37,12 +37,19 @@ public class CurrencyFormat { static final BigDecimal SATOSHI_DIVISOR = new BigDecimal(100000000); static final DecimalFormat BTC_FORMAT = new DecimalFormat("###,##0.00000000"); - @VisibleForTesting + static final BigDecimal BSQ_SATOSHI_DIVISOR = new BigDecimal(100); + static final DecimalFormat BSQ_FORMAT = new DecimalFormat("###,###,###,##0.00"); + @SuppressWarnings("BigDecimalMethodWithoutRoundingCalled") public static String formatSatoshis(long sats) { return BTC_FORMAT.format(BigDecimal.valueOf(sats).divide(SATOSHI_DIVISOR)); } + @SuppressWarnings("BigDecimalMethodWithoutRoundingCalled") + public static String formatBsq(long sats) { + return BSQ_FORMAT.format(BigDecimal.valueOf(sats).divide(BSQ_SATOSHI_DIVISOR)); + } + static String formatAmountRange(long minAmount, long amount) { return minAmount != amount ? formatSatoshis(minAmount) + " - " + formatSatoshis(amount) diff --git a/cli/src/main/java/bisq/cli/TableFormat.java b/cli/src/main/java/bisq/cli/TableFormat.java index 8336fff9ba1..ff4e440f18f 100644 --- a/cli/src/main/java/bisq/cli/TableFormat.java +++ b/cli/src/main/java/bisq/cli/TableFormat.java @@ -18,10 +18,15 @@ package bisq.cli; import bisq.proto.grpc.AddressBalanceInfo; +import bisq.proto.grpc.BalancesInfo; +import bisq.proto.grpc.BsqBalanceInfo; +import bisq.proto.grpc.BtcBalanceInfo; import bisq.proto.grpc.OfferInfo; import protobuf.PaymentAccount; +import com.google.common.annotations.VisibleForTesting; + import java.text.SimpleDateFormat; import java.util.Date; @@ -30,28 +35,28 @@ import java.util.stream.Collectors; import static bisq.cli.ColumnHeaderConstants.*; -import static bisq.cli.CurrencyFormat.formatAmountRange; -import static bisq.cli.CurrencyFormat.formatOfferPrice; -import static bisq.cli.CurrencyFormat.formatSatoshis; -import static bisq.cli.CurrencyFormat.formatVolumeRange; +import static bisq.cli.CurrencyFormat.*; import static com.google.common.base.Strings.padEnd; import static java.lang.String.format; import static java.util.Collections.max; import static java.util.Comparator.comparing; import static java.util.TimeZone.getTimeZone; -class TableFormat { +@VisibleForTesting +public class TableFormat { static final TimeZone TZ_UTC = getTimeZone("UTC"); static final SimpleDateFormat DATE_FORMAT_ISO_8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); - static String formatAddressBalanceTbl(List addressBalanceInfo) { - String headerLine = (COL_HEADER_ADDRESS + COL_HEADER_DELIMITER - + COL_HEADER_BALANCE + COL_HEADER_DELIMITER - + COL_HEADER_CONFIRMATIONS + COL_HEADER_DELIMITER + "\n"); - String colDataFormat = "%-" + COL_HEADER_ADDRESS.length() + "s" // left justify - + " %" + COL_HEADER_BALANCE.length() + "s" // right justify - + " %" + COL_HEADER_CONFIRMATIONS.length() + "d"; // right justify + public static String formatAddressBalanceTbl(List addressBalanceInfo) { + String headerFormatString = COL_HEADER_ADDRESS + COL_HEADER_DELIMITER + + COL_HEADER_AVAILABLE_BALANCE + COL_HEADER_DELIMITER + + COL_HEADER_CONFIRMATIONS + COL_HEADER_DELIMITER + "\n"; + String headerLine = format(headerFormatString, "BTC"); + + String colDataFormat = "%-" + COL_HEADER_ADDRESS.length() + "s" // lt justify + + " %" + (COL_HEADER_AVAILABLE_BALANCE.length() - 1) + "s" // rt justify + + " %" + COL_HEADER_CONFIRMATIONS.length() + "d"; // lt justify return headerLine + addressBalanceInfo.stream() .map(info -> format(colDataFormat, @@ -61,15 +66,58 @@ static String formatAddressBalanceTbl(List addressBalanceInf .collect(Collectors.joining("\n")); } - static String formatOfferTable(List offerInfo, String fiatCurrency) { + public static String formatBalancesTbls(BalancesInfo balancesInfo) { + return "BTC" + "\n" + + formatBtcBalanceInfoTbl(balancesInfo.getBtcBalanceInfo()) + "\n" + + "BSQ" + "\n" + + formatBsqBalanceInfoTbl(balancesInfo.getBsqBalanceInfo()); + } + + public static String formatBsqBalanceInfoTbl(BsqBalanceInfo bsqBalanceInfo) { + String headerLine = COL_HEADER_AVAILABLE_CONFIRMED_BALANCE + COL_HEADER_DELIMITER + + COL_HEADER_UNVERIFIED_BALANCE + COL_HEADER_DELIMITER + + COL_HEADER_UNCONFIRMED_CHANGE_BALANCE + COL_HEADER_DELIMITER + + COL_HEADER_LOCKED_FOR_VOTING_BALANCE + COL_HEADER_DELIMITER + + COL_HEADER_LOCKUP_BONDS_BALANCE + COL_HEADER_DELIMITER + + COL_HEADER_UNLOCKING_BONDS_BALANCE + COL_HEADER_DELIMITER + "\n"; + String colDataFormat = "%" + COL_HEADER_AVAILABLE_CONFIRMED_BALANCE.length() + "s" // rt justify + + " %" + (COL_HEADER_UNVERIFIED_BALANCE.length() + 1) + "s" // rt justify + + " %" + (COL_HEADER_UNCONFIRMED_CHANGE_BALANCE.length() + 1) + "s" // rt justify + + " %" + (COL_HEADER_LOCKED_FOR_VOTING_BALANCE.length() + 1) + "s" // rt justify + + " %" + (COL_HEADER_LOCKUP_BONDS_BALANCE.length() + 1) + "s" // rt justify + + " %" + (COL_HEADER_UNLOCKING_BONDS_BALANCE.length() + 1) + "s"; // rt justify + return headerLine + format(colDataFormat, + formatBsq(bsqBalanceInfo.getAvailableConfirmedBalance()), + formatBsq(bsqBalanceInfo.getUnverifiedBalance()), + formatBsq(bsqBalanceInfo.getUnconfirmedChangeBalance()), + formatBsq(bsqBalanceInfo.getLockedForVotingBalance()), + formatBsq(bsqBalanceInfo.getLockupBondsBalance()), + formatBsq(bsqBalanceInfo.getUnlockingBondsBalance())); + } + + public static String formatBtcBalanceInfoTbl(BtcBalanceInfo btcBalanceInfo) { + String headerLine = COL_HEADER_AVAILABLE_BALANCE + COL_HEADER_DELIMITER + + COL_HEADER_RESERVED_BALANCE + COL_HEADER_DELIMITER + + COL_HEADER_TOTAL_AVAILABLE_BALANCE + COL_HEADER_DELIMITER + + COL_HEADER_LOCKED_BALANCE + COL_HEADER_DELIMITER + "\n"; + String colDataFormat = "%" + COL_HEADER_AVAILABLE_BALANCE.length() + "s" // rt justify + + " %" + (COL_HEADER_RESERVED_BALANCE.length() + 1) + "s" // rt justify + + " %" + (COL_HEADER_TOTAL_AVAILABLE_BALANCE.length() + 1) + "s" // rt justify + + " %" + (COL_HEADER_LOCKED_BALANCE.length() + 1) + "s"; // rt justify + return headerLine + format(colDataFormat, + formatSatoshis(btcBalanceInfo.getAvailableBalance()), + formatSatoshis(btcBalanceInfo.getReservedBalance()), + formatSatoshis(btcBalanceInfo.getTotalAvailableBalance()), + formatSatoshis(btcBalanceInfo.getLockedBalance())); + } + static String formatOfferTable(List offerInfo, String fiatCurrency) { // Some column values might be longer than header, so we need to calculate them. int paymentMethodColWidth = getLengthOfLongestColumn( COL_HEADER_PAYMENT_METHOD.length(), offerInfo.stream() .map(OfferInfo::getPaymentMethodShortName) .collect(Collectors.toList())); - String headersFormat = COL_HEADER_DIRECTION + COL_HEADER_DELIMITER + COL_HEADER_PRICE + COL_HEADER_DELIMITER // includes %s -> fiatCurrency + COL_HEADER_AMOUNT + COL_HEADER_DELIMITER diff --git a/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java index 7cfcc5ce152..bd1ac198a26 100644 --- a/core/src/main/java/bisq/core/api/CoreApi.java +++ b/core/src/main/java/bisq/core/api/CoreApi.java @@ -18,6 +18,9 @@ package bisq.core.api; import bisq.core.api.model.AddressBalanceInfo; +import bisq.core.api.model.BalancesInfo; +import bisq.core.api.model.BsqBalanceInfo; +import bisq.core.api.model.BtcBalanceInfo; import bisq.core.monetary.Price; import bisq.core.offer.Offer; import bisq.core.offer.OfferPayload; @@ -213,10 +216,23 @@ public String getTradeRole(String tradeId) { // Wallets /////////////////////////////////////////////////////////////////////////////////////////// + @Deprecated public long getAvailableBalance() { return walletsService.getAvailableBalance(); } + public BalancesInfo getBalances() { + return walletsService.getBalances(); + } + + public BsqBalanceInfo getBsqBalances() { + return walletsService.getBsqBalances(); + } + + public BtcBalanceInfo getBtcBalances() { + return walletsService.getBtcBalances(); + } + public long getAddressBalance(String addressString) { return walletsService.getAddressBalance(addressString); } @@ -229,6 +245,10 @@ public List getFundingAddresses() { return walletsService.getFundingAddresses(); } + public String getUnusedBsqAddress() { + return walletsService.getUnusedBsqAddress(); + } + public void setWalletPassword(String password, String newPassword) { walletsService.setWalletPassword(password, newPassword); } diff --git a/core/src/main/java/bisq/core/api/CoreWalletsService.java b/core/src/main/java/bisq/core/api/CoreWalletsService.java index fc15ec5062a..3284a457730 100644 --- a/core/src/main/java/bisq/core/api/CoreWalletsService.java +++ b/core/src/main/java/bisq/core/api/CoreWalletsService.java @@ -18,8 +18,12 @@ package bisq.core.api; import bisq.core.api.model.AddressBalanceInfo; +import bisq.core.api.model.BalancesInfo; +import bisq.core.api.model.BsqBalanceInfo; +import bisq.core.api.model.BtcBalanceInfo; import bisq.core.btc.Balances; import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.BsqWalletService; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.WalletsManager; @@ -55,6 +59,7 @@ class CoreWalletsService { private final Balances balances; private final WalletsManager walletsManager; + private final BsqWalletService bsqWalletService; private final BtcWalletService btcWalletService; @Nullable @@ -66,9 +71,11 @@ class CoreWalletsService { @Inject public CoreWalletsService(Balances balances, WalletsManager walletsManager, + BsqWalletService bsqWalletService, BtcWalletService btcWalletService) { this.balances = balances; this.walletsManager = walletsManager; + this.bsqWalletService = bsqWalletService; this.btcWalletService = btcWalletService; } @@ -78,6 +85,7 @@ KeyParameter getKey() { return tempAesKey; } + @Deprecated long getAvailableBalance() { verifyWalletsAreAvailable(); verifyEncryptedWalletIsUnlocked(); @@ -89,6 +97,56 @@ long getAvailableBalance() { return balance.getValue(); } + BalancesInfo getBalances() { + verifyWalletsAreAvailable(); + verifyEncryptedWalletIsUnlocked(); + if (balances.getAvailableBalance().get() == null) + throw new IllegalStateException("balance is not yet available"); + + return new BalancesInfo(getBsqBalances(), getBtcBalances()); + } + + BsqBalanceInfo getBsqBalances() { + verifyWalletsAreAvailable(); + verifyEncryptedWalletIsUnlocked(); + + var availableConfirmedBalance = bsqWalletService.getAvailableConfirmedBalance(); + var unverifiedBalance = bsqWalletService.getUnverifiedBalance(); + var unconfirmedChangeBalance = bsqWalletService.getUnconfirmedChangeBalance(); + var lockedForVotingBalance = bsqWalletService.getLockedForVotingBalance(); + var lockupBondsBalance = bsqWalletService.getLockupBondsBalance(); + var unlockingBondsBalance = bsqWalletService.getUnlockingBondsBalance(); + + return new BsqBalanceInfo(availableConfirmedBalance.value, + unverifiedBalance.value, + unconfirmedChangeBalance.value, + lockedForVotingBalance.value, + lockupBondsBalance.value, + unlockingBondsBalance.value); + } + + BtcBalanceInfo getBtcBalances() { + verifyWalletsAreAvailable(); + verifyEncryptedWalletIsUnlocked(); + + var availableBalance = balances.getAvailableBalance().get(); + if (availableBalance == null) + throw new IllegalStateException("balance is not yet available"); + + var reservedBalance = balances.getReservedBalance().get(); + if (reservedBalance == null) + throw new IllegalStateException("reserved balance is not yet available"); + + var lockedBalance = balances.getLockedBalance().get(); + if (lockedBalance == null) + throw new IllegalStateException("locked balance is not yet available"); + + return new BtcBalanceInfo(availableBalance.value, + reservedBalance.value, + availableBalance.add(reservedBalance).value, + lockedBalance.value); + } + long getAddressBalance(String addressString) { Address address = getAddressEntry(addressString).getAddress(); return btcWalletService.getBalanceForAddress(address).value; @@ -134,6 +192,10 @@ List getFundingAddresses() { .collect(Collectors.toList()); } + String getUnusedBsqAddress() { + return bsqWalletService.getUnusedBsqAddressAsString(); + } + int getNumConfirmationsForMostRecentTransaction(String addressString) { Address address = getAddressEntry(addressString).getAddress(); TransactionConfidence confidence = btcWalletService.getConfidenceForAddress(address); diff --git a/core/src/main/java/bisq/core/api/model/BalancesInfo.java b/core/src/main/java/bisq/core/api/model/BalancesInfo.java new file mode 100644 index 00000000000..39179fde2b3 --- /dev/null +++ b/core/src/main/java/bisq/core/api/model/BalancesInfo.java @@ -0,0 +1,42 @@ +package bisq.core.api.model; + +import bisq.common.Payload; + +import lombok.Getter; + +@Getter +public class BalancesInfo implements Payload { + + private final BsqBalanceInfo bsqBalanceInfo; + private final BtcBalanceInfo btcBalanceInfo; + + public BalancesInfo(BsqBalanceInfo bsqBalanceInfo, BtcBalanceInfo btcBalanceInfo) { + this.bsqBalanceInfo = bsqBalanceInfo; + this.btcBalanceInfo = btcBalanceInfo; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public bisq.proto.grpc.BalancesInfo toProtoMessage() { + return bisq.proto.grpc.BalancesInfo.newBuilder() + .setBsqBalanceInfo(bsqBalanceInfo.toProtoMessage()) + .setBtcBalanceInfo(btcBalanceInfo.toProtoMessage()) + .build(); + } + + public static BalancesInfo fromProto(bisq.proto.grpc.BalancesInfo proto) { + return new BalancesInfo(BsqBalanceInfo.fromProto(proto.getBsqBalanceInfo()), + BtcBalanceInfo.fromProto(proto.getBtcBalanceInfo())); + } + + @Override + public String toString() { + return "BalancesInfo{" + "\n" + + " " + bsqBalanceInfo.toString() + "\n" + + ", " + btcBalanceInfo.toString() + "\n" + + '}'; + } +} diff --git a/core/src/main/java/bisq/core/api/model/BsqBalanceInfo.java b/core/src/main/java/bisq/core/api/model/BsqBalanceInfo.java new file mode 100644 index 00000000000..ff314e087ba --- /dev/null +++ b/core/src/main/java/bisq/core/api/model/BsqBalanceInfo.java @@ -0,0 +1,87 @@ +package bisq.core.api.model; + +import bisq.common.Payload; + +import com.google.common.annotations.VisibleForTesting; + +import lombok.Getter; + +@Getter +public class BsqBalanceInfo implements Payload { + + // All balances are in BSQ satoshis. + private final long availableConfirmedBalance; + private final long unverifiedBalance; + private final long unconfirmedChangeBalance; + private final long lockedForVotingBalance; + private final long lockupBondsBalance; + private final long unlockingBondsBalance; + + public BsqBalanceInfo(long availableConfirmedBalance, + long unverifiedBalance, + long unconfirmedChangeBalance, + long lockedForVotingBalance, + long lockupBondsBalance, + long unlockingBondsBalance) { + this.availableConfirmedBalance = availableConfirmedBalance; + this.unverifiedBalance = unverifiedBalance; + this.unconfirmedChangeBalance = unconfirmedChangeBalance; + this.lockedForVotingBalance = lockedForVotingBalance; + this.lockupBondsBalance = lockupBondsBalance; + this.unlockingBondsBalance = unlockingBondsBalance; + } + + @VisibleForTesting + public static BsqBalanceInfo valueOf(long availableConfirmedBalance, + long unverifiedBalance, + long unconfirmedChangeBalance, + long lockedForVotingBalance, + long lockupBondsBalance, + long unlockingBondsBalance) { + // Convenience for creating a model instance instead of a proto. + return new BsqBalanceInfo(availableConfirmedBalance, + unverifiedBalance, + unconfirmedChangeBalance, + lockedForVotingBalance, + lockupBondsBalance, + unlockingBondsBalance); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public bisq.proto.grpc.BsqBalanceInfo toProtoMessage() { + return bisq.proto.grpc.BsqBalanceInfo.newBuilder() + .setAvailableConfirmedBalance(availableConfirmedBalance) + .setUnverifiedBalance(unverifiedBalance) + .setUnconfirmedChangeBalance(unconfirmedChangeBalance) + .setLockedForVotingBalance(lockedForVotingBalance) + .setLockupBondsBalance(lockupBondsBalance) + .setUnlockingBondsBalance(unlockingBondsBalance) + .build(); + + } + + public static BsqBalanceInfo fromProto(bisq.proto.grpc.BsqBalanceInfo proto) { + return new BsqBalanceInfo(proto.getAvailableConfirmedBalance(), + proto.getUnverifiedBalance(), + proto.getUnconfirmedChangeBalance(), + proto.getLockedForVotingBalance(), + proto.getLockupBondsBalance(), + proto.getUnlockingBondsBalance()); + } + + @Override + public String toString() { + return "BsqBalanceInfo{" + + "availableConfirmedBalance=" + availableConfirmedBalance + + ", unverifiedBalance=" + unverifiedBalance + + ", unconfirmedChangeBalance=" + unconfirmedChangeBalance + + ", lockedForVotingBalance=" + lockedForVotingBalance + + ", lockupBondsBalance=" + lockupBondsBalance + + ", unlockingBondsBalance=" + unlockingBondsBalance + + '}'; + } +} diff --git a/core/src/main/java/bisq/core/api/model/BtcBalanceInfo.java b/core/src/main/java/bisq/core/api/model/BtcBalanceInfo.java new file mode 100644 index 00000000000..6e8708c3e37 --- /dev/null +++ b/core/src/main/java/bisq/core/api/model/BtcBalanceInfo.java @@ -0,0 +1,70 @@ +package bisq.core.api.model; + +import bisq.common.Payload; + +import com.google.common.annotations.VisibleForTesting; + +import lombok.Getter; + +@Getter +public class BtcBalanceInfo implements Payload { + + // All balances are in BTC satoshis. + private final long availableBalance; + private final long reservedBalance; + private final long totalAvailableBalance; // available + reserved + private final long lockedBalance; + + public BtcBalanceInfo(long availableBalance, + long reservedBalance, + long totalAvailableBalance, + long lockedBalance) { + this.availableBalance = availableBalance; + this.reservedBalance = reservedBalance; + this.totalAvailableBalance = totalAvailableBalance; + this.lockedBalance = lockedBalance; + } + + @VisibleForTesting + public static BtcBalanceInfo valueOf(long availableBalance, + long reservedBalance, + long totalAvailableBalance, + long lockedBalance) { + // Convenience for creating a model instance instead of a proto. + return new BtcBalanceInfo(availableBalance, + reservedBalance, + totalAvailableBalance, + lockedBalance); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public bisq.proto.grpc.BtcBalanceInfo toProtoMessage() { + return bisq.proto.grpc.BtcBalanceInfo.newBuilder() + .setAvailableBalance(availableBalance) + .setReservedBalance(reservedBalance) + .setTotalAvailableBalance(totalAvailableBalance) + .setLockedBalance(lockedBalance) + .build(); + } + + public static BtcBalanceInfo fromProto(bisq.proto.grpc.BtcBalanceInfo proto) { + return new BtcBalanceInfo(proto.getAvailableBalance(), + proto.getReservedBalance(), + proto.getTotalAvailableBalance(), + proto.getLockedBalance()); + } + + @Override + public String toString() { + return "BtcBalanceInfo{" + + "availableBalance=" + availableBalance + + ", reservedBalance=" + reservedBalance + + ", totalAvailableBalance=" + totalAvailableBalance + + ", lockedBalance=" + lockedBalance + + '}'; + } +} diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java index 1b5cb42e4cc..2f35f000779 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java @@ -19,13 +19,23 @@ import bisq.core.api.CoreApi; import bisq.core.api.model.AddressBalanceInfo; +import bisq.core.api.model.BsqBalanceInfo; +import bisq.core.api.model.BtcBalanceInfo; import bisq.proto.grpc.GetAddressBalanceReply; import bisq.proto.grpc.GetAddressBalanceRequest; import bisq.proto.grpc.GetBalanceReply; import bisq.proto.grpc.GetBalanceRequest; +import bisq.proto.grpc.GetBalancesReply; +import bisq.proto.grpc.GetBalancesRequest; +import bisq.proto.grpc.GetBsqBalancesReply; +import bisq.proto.grpc.GetBsqBalancesRequest; +import bisq.proto.grpc.GetBtcBalancesReply; +import bisq.proto.grpc.GetBtcBalancesRequest; import bisq.proto.grpc.GetFundingAddressesReply; import bisq.proto.grpc.GetFundingAddressesRequest; +import bisq.proto.grpc.GetUnusedBsqAddressReply; +import bisq.proto.grpc.GetUnusedBsqAddressRequest; import bisq.proto.grpc.LockWalletReply; import bisq.proto.grpc.LockWalletRequest; import bisq.proto.grpc.RemoveWalletPasswordReply; @@ -54,12 +64,8 @@ public GrpcWalletsService(CoreApi coreApi) { this.coreApi = coreApi; } - // TODO we need to support 3 or 4 balance types: available, reserved, lockedInTrade - // and maybe total wallet balance (available+reserved). To not duplicate the methods, - // we should pass an enum type. Enums in proto are a bit cumbersome as they are - // global so you quickly run into namespace conflicts if not always prefixes which - // makes it more verbose. In the core code base we move to the strategy to store the - // enum name and map it. This gives also more flexibility with updates. + + @Deprecated @Override public void getBalance(GetBalanceRequest req, StreamObserver responseObserver) { try { @@ -74,6 +80,54 @@ public void getBalance(GetBalanceRequest req, StreamObserver re } } + @Override + public void getBalances(GetBalancesRequest req, StreamObserver responseObserver) { + try { + var balances = coreApi.getBalances(); + var reply = GetBalancesReply.newBuilder() + .setBalances(balances.toProtoMessage()) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (IllegalStateException cause) { + var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage())); + responseObserver.onError(ex); + throw ex; + } + } + + @Override + public void getBsqBalances(GetBsqBalancesRequest req, StreamObserver responseObserver) { + try { + BsqBalanceInfo bsqBalanceInfo = coreApi.getBsqBalances(); + var reply = GetBsqBalancesReply.newBuilder() + .setBsqBalanceInfo(bsqBalanceInfo.toProtoMessage()) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (IllegalStateException cause) { + var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage())); + responseObserver.onError(ex); + throw ex; + } + } + + @Override + public void getBtcBalances(GetBtcBalancesRequest req, StreamObserver responseObserver) { + try { + BtcBalanceInfo btcBalanceInfo = coreApi.getBtcBalances(); + var reply = GetBtcBalancesReply.newBuilder() + .setBtcBalanceInfo(btcBalanceInfo.toProtoMessage()) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (IllegalStateException cause) { + var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage())); + responseObserver.onError(ex); + throw ex; + } + } + @Override public void getAddressBalance(GetAddressBalanceRequest req, StreamObserver responseObserver) { @@ -110,6 +164,23 @@ 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 setWalletPassword(SetWalletPasswordRequest req, StreamObserver responseObserver) { diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index d9e0a3973d8..5005ad1eef8 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -81,6 +81,7 @@ message CreateOfferRequest { uint64 minAmount = 7; double buyerSecurityDeposit = 8; string paymentAccountId = 9; + string makerFeeCurrencyCode = 10; } message CreateOfferReply { @@ -105,13 +106,14 @@ message OfferInfo { uint64 volume = 8; uint64 minVolume = 9; uint64 buyerSecurityDeposit = 10; - string paymentAccountId = 11; - string paymentMethodId = 12; - string paymentMethodShortName = 13; - string baseCurrencyCode = 14; - string counterCurrencyCode = 15; - uint64 date = 16; - string state = 17; + bool isCurrencyForMakerFeeBtc = 11; + string paymentAccountId = 12; + string paymentMethodId = 13; + string paymentMethodShortName = 14; + string baseCurrencyCode = 15; + string counterCurrencyCode = 16; + uint64 date = 17; + string state = 18; } /////////////////////////////////////////////////////////////////////////////////////////// @@ -198,6 +200,7 @@ service Trades { message TakeOfferRequest { string offerId = 1; string paymentAccountId = 2; + string takerFeeCurrencyCode = 3; } message TakeOfferReply { @@ -275,8 +278,18 @@ message TradeInfo { service Wallets { rpc GetBalance (GetBalanceRequest) returns (GetBalanceReply) { } + rpc GetBalances (GetBalancesRequest) returns (GetBalancesReply) { + } + rpc GetBsqBalances (GetBsqBalancesRequest) returns (GetBsqBalancesReply) { + } + rpc GetBtcBalances (GetBtcBalancesRequest) returns (GetBtcBalancesReply) { + } rpc GetAddressBalance (GetAddressBalanceRequest) returns (GetAddressBalanceReply) { } + rpc GetUnusedBsqAddress (GetUnusedBsqAddressRequest) returns (GetUnusedBsqAddressReply) { + } + rpc SendBsq (SendBsqRequest) returns (SendBsqReply) { + } rpc GetFundingAddresses (GetFundingAddressesRequest) returns (GetFundingAddressesReply) { } rpc SetWalletPassword (SetWalletPasswordRequest) returns (SetWalletPasswordReply) { @@ -296,6 +309,27 @@ message GetBalanceReply { uint64 balance = 1; } +message GetBalancesRequest { +} + +message GetBalancesReply { + BalancesInfo balances = 1; +} + +message GetBsqBalancesRequest { +} + +message GetBsqBalancesReply { + BsqBalanceInfo bsqBalanceInfo = 1; +} + +message GetBtcBalancesRequest { +} + +message GetBtcBalancesReply { + BtcBalanceInfo btcBalanceInfo = 1; +} + message GetAddressBalanceRequest { string address = 1; } @@ -304,6 +338,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 +389,27 @@ message UnlockWalletRequest { message UnlockWalletReply { } +message BalancesInfo { + BsqBalanceInfo bsqBalanceInfo = 1; + BtcBalanceInfo btcBalanceInfo = 2; +} + +message BsqBalanceInfo { + uint64 availableConfirmedBalance = 1; + uint64 unverifiedBalance = 2; + uint64 unconfirmedChangeBalance = 3; + uint64 lockedForVotingBalance = 4; + uint64 lockupBondsBalance = 5; + uint64 unlockingBondsBalance = 6; +} + +message BtcBalanceInfo { + uint64 availableBalance = 1; + uint64 reservedBalance = 2; + uint64 totalAvailableBalance = 3; + uint64 lockedBalance = 4; +} + message AddressBalanceInfo { string address = 1; int64 balance = 2;