Implement and test new getbalance(s) api methods

- Added three new methods to CLI:

      getbalances   ...	returns complete bsq and btc balance info
      getbsqbalance ...	returns complete bsq balance info
      getbtcbalance ...	returns complete btc balance info

      The old getbalance method is deprecated and will be removed
      if there is agreement to do that.

- Made the needed changes in the CLI's output formatting classes.

- Added new tests to existing BsqWalletTest, added new BtcWalletTest
  and WalletBalancesTest.

- Added disabled tests for funding a bsq wallet (todo in next PR).
This commit is contained in:
ghubstan 2020-11-13 14:21:26 -03:00
parent 8dc1a74c8b
commit 208a37b339
No known key found for this signature in database
GPG key ID: E35592D6800A861E
13 changed files with 707 additions and 55 deletions

View file

@ -17,11 +17,19 @@
package bisq.apitest.method;
import bisq.proto.grpc.AddressBalanceInfo;
import bisq.proto.grpc.BalancesInfo;
import bisq.proto.grpc.BsqBalanceInfo;
import bisq.proto.grpc.BtcBalanceInfo;
import bisq.proto.grpc.CancelOfferRequest;
import bisq.proto.grpc.ConfirmPaymentReceivedRequest;
import bisq.proto.grpc.ConfirmPaymentStartedRequest;
import bisq.proto.grpc.CreatePaymentAccountRequest;
import bisq.proto.grpc.GetAddressBalanceRequest;
import bisq.proto.grpc.GetBalanceRequest;
import bisq.proto.grpc.GetBalancesRequest;
import bisq.proto.grpc.GetBsqBalancesRequest;
import bisq.proto.grpc.GetBtcBalancesRequest;
import bisq.proto.grpc.GetFundingAddressesRequest;
import bisq.proto.grpc.GetOfferRequest;
import bisq.proto.grpc.GetPaymentAccountsRequest;
@ -33,6 +41,7 @@ import bisq.proto.grpc.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;
@ -104,12 +113,25 @@ public class MethodTest extends ApiTestCase {
// Convenience methods for building gRPC request objects
@Deprecated
protected final GetBalanceRequest createBalanceRequest() {
return GetBalanceRequest.newBuilder().build();
}
protected final GetUnusedBsqAddressRequest createGetUnusedBsqAddressRequest() {
return GetUnusedBsqAddressRequest.newBuilder().build();
protected final GetBalancesRequest createGetBalancesRequest() {
return GetBalancesRequest.newBuilder().build();
}
protected final GetAddressBalanceRequest createGetAddressBalanceRequest(String address) {
return GetAddressBalanceRequest.newBuilder().setAddress(address).build();
}
protected final GetBsqBalancesRequest createGetBsqBalancesRequest() {
return GetBsqBalancesRequest.newBuilder().build();
}
protected final GetBtcBalancesRequest createBtcBalancesRequest() {
return GetBtcBalancesRequest.newBuilder().build();
}
protected final SetWalletPasswordRequest createSetWalletPasswordRequest(String password) {
@ -132,6 +154,14 @@ public class MethodTest extends ApiTestCase {
return LockWalletRequest.newBuilder().build();
}
protected final GetUnusedBsqAddressRequest createGetUnusedBsqAddressRequest() {
return GetUnusedBsqAddressRequest.newBuilder().build();
}
protected final SendBsqRequest createSendBsqRequest(String address, double amount) {
return SendBsqRequest.newBuilder().setAddress(address).setAmount(amount).build();
}
protected final GetFundingAddressesRequest createGetFundingAddressesRequest() {
return GetFundingAddressesRequest.newBuilder().build();
}
@ -148,8 +178,12 @@ public class MethodTest extends ApiTestCase {
return CancelOfferRequest.newBuilder().setId(offerId).build();
}
protected final TakeOfferRequest createTakeOfferRequest(String offerId, String paymentAccountId) {
return TakeOfferRequest.newBuilder().setOfferId(offerId).setPaymentAccountId(paymentAccountId).build();
protected final TakeOfferRequest createTakeOfferRequest(String offerId,
String paymentAccountId) {
return TakeOfferRequest.newBuilder()
.setOfferId(offerId)
.setPaymentAccountId(paymentAccountId)
.build();
}
protected final GetTradeRequest createGetTradeRequest(String tradeId) {
@ -179,10 +213,27 @@ public class MethodTest extends ApiTestCase {
// Convenience methods for calling frequently used & thoroughly tested gRPC services.
@Deprecated
protected final long getBalance(BisqAppConfig bisqAppConfig) {
return grpcStubs(bisqAppConfig).walletsService.getBalance(createBalanceRequest()).getBalance();
}
protected final BalancesInfo getBalances(BisqAppConfig bisqAppConfig) {
return grpcStubs(bisqAppConfig).walletsService.getBalances(createGetBalancesRequest()).getBalances();
}
protected final BsqBalanceInfo getBsqBalances(BisqAppConfig bisqAppConfig) {
return grpcStubs(bisqAppConfig).walletsService.getBsqBalances(createGetBsqBalancesRequest()).getBsqBalanceInfo();
}
protected final BtcBalanceInfo getBtcBalances(BisqAppConfig bisqAppConfig) {
return grpcStubs(bisqAppConfig).walletsService.getBtcBalances(createBtcBalancesRequest()).getBtcBalanceInfo();
}
protected final AddressBalanceInfo getAddressBalance(BisqAppConfig bisqAppConfig, String address) {
return grpcStubs(bisqAppConfig).walletsService.getAddressBalance(createGetAddressBalanceRequest(address)).getAddressBalanceInfo();
}
protected final void unlockWallet(BisqAppConfig bisqAppConfig, String password, long timeout) {
//noinspection ResultOfMethodCallIgnored
grpcStubs(bisqAppConfig).walletsService.unlockWallet(createUnlockWalletRequest(password, timeout));
@ -193,6 +244,14 @@ public class MethodTest extends ApiTestCase {
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())

View file

@ -1,5 +1,7 @@
package bisq.apitest.method.wallet;
import bisq.proto.grpc.BsqBalanceInfo;
import org.bitcoinj.core.LegacyAddress;
import org.bitcoinj.core.NetworkParameters;
@ -7,16 +9,22 @@ import 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;
@ -24,20 +32,43 @@ import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import bisq.apitest.config.BisqAppConfig;
import bisq.apitest.method.MethodTest;
// @Disabled
@Disabled
@Slf4j
@TestMethodOrder(OrderAnnotation.class)
public class BsqWalletTest extends MethodTest {
// Alice's regtest BSQ wallet is initialized with 1,000,000 BSQ.
private static final bisq.core.api.model.BsqBalanceInfo ALICES_INITIAL_BSQ_BALANCES =
expectedBsqBalanceModel(100000000,
0,
0,
0,
0,
0);
// Bob's regtest BSQ wallet is initialized with 1,500,000 BSQ.
private static final bisq.core.api.model.BsqBalanceInfo BOBS_INITIAL_BSQ_BALANCES =
expectedBsqBalanceModel(150000000,
0,
0,
0,
0,
0);
private static final double SEND_BSQ_AMOUNT = 25000.50;
@BeforeAll
public static void setUp() {
startSupportingApps(false,
true,
bitcoind,
seednode,
alicedaemon);
arbdaemon,
alicedaemon,
bobdaemon);
}
@Test
@ -52,13 +83,164 @@ public class BsqWalletTest extends MethodTest {
NetworkParameters networkParameters = LegacyAddress.getParametersFromAddress(address.substring(1));
String addressNetwork = networkParameters.getPaymentProtocolId();
assertNotEquals(PAYMENT_PROTOCOL_ID_MAINNET, addressNetwork);
// TODO Fix bug(?) causing the regtest bsq address network to be evaluated as 'testnet' here.
// TODO Fix bug causing the regtest bsq address network to be evaluated as 'testnet' here.
assertTrue(addressNetwork.equals(PAYMENT_PROTOCOL_ID_TESTNET)
|| addressNetwork.equals(PAYMENT_PROTOCOL_ID_REGTEST));
}
@Test
@Order(2)
public void testInitialBsqBalances(final TestInfo testInfo) {
BsqBalanceInfo alicesBsqBalances = getBsqBalances(alicedaemon);
log.info("{} -> Alice's BSQ Initial Balances -> \n{}",
testName(testInfo),
formatBsqBalanceInfoTbl(alicesBsqBalances));
verifyBsqBalances(ALICES_INITIAL_BSQ_BALANCES, alicesBsqBalances);
BsqBalanceInfo bobsBsqBalances = getBsqBalances(bobdaemon);
log.info("{} -> Bob's BSQ Initial Balances -> \n{}",
testName(testInfo),
formatBsqBalanceInfoTbl(bobsBsqBalances));
verifyBsqBalances(BOBS_INITIAL_BSQ_BALANCES, bobsBsqBalances);
}
@Disabled // TODO
@Test
@Order(3)
public void testSendBsqAndCheckBalancesBeforeGeneratingBtcBlock(final TestInfo testInfo) {
String bobsBsqAddress = getUnusedBsqAddress(bobdaemon);
sendBsq(alicedaemon, bobsBsqAddress, SEND_BSQ_AMOUNT);
sleep(2000);
BsqBalanceInfo alicesBsqBalances = getBsqBalances(alicedaemon);
BsqBalanceInfo bobsBsqBalances = waitForNonZeroUnverifiedBalance(bobdaemon);
log.info("BSQ Balances Before BTC Block Gen...");
printBobAndAliceBsqBalances(testInfo,
bobsBsqBalances,
alicesBsqBalances,
alicedaemon);
verifyBsqBalances(expectedBsqBalanceModel(150000000,
2500050,
0,
0,
0,
0),
bobsBsqBalances);
verifyBsqBalances(expectedBsqBalanceModel(97499950,
97499950,
97499950,
0,
0,
0),
alicesBsqBalances);
}
@Disabled // TODO
@Test
@Order(4)
public void testBalancesAfterSendingBsqAndGeneratingBtcBlock(final TestInfo testInfo) {
// There is a wallet persist delay; we have to
// wait for both wallets to be saved to disk.
genBtcBlocksThenWait(1, 4000);
BsqBalanceInfo alicesBsqBalances = getBsqBalances(alicedaemon);
BsqBalanceInfo bobsBsqBalances = waitForNewAvailableConfirmedBalance(bobdaemon, 150000000);
log.info("See Available Confirmed BSQ Balances...");
printBobAndAliceBsqBalances(testInfo,
bobsBsqBalances,
alicesBsqBalances,
alicedaemon);
verifyBsqBalances(expectedBsqBalanceModel(152500050,
0,
0,
0,
0,
0),
bobsBsqBalances);
verifyBsqBalances(expectedBsqBalanceModel(97499950,
0,
0,
0,
0,
0),
alicesBsqBalances);
}
@AfterAll
public static void tearDown() {
tearDownScaffold();
}
private void verifyBsqBalances(bisq.core.api.model.BsqBalanceInfo expected,
bisq.proto.grpc.BsqBalanceInfo actual) {
assertEquals(expected.getAvailableConfirmedBalance(), actual.getAvailableConfirmedBalance());
assertEquals(expected.getUnverifiedBalance(), actual.getUnverifiedBalance());
assertEquals(expected.getUnconfirmedChangeBalance(), actual.getUnconfirmedChangeBalance());
assertEquals(expected.getLockedForVotingBalance(), actual.getLockedForVotingBalance());
assertEquals(expected.getLockupBondsBalance(), actual.getLockupBondsBalance());
assertEquals(expected.getUnlockingBondsBalance(), actual.getUnlockingBondsBalance());
}
private BsqBalanceInfo waitForNonZeroUnverifiedBalance(BisqAppConfig daemon) {
// A BSQ recipient needs to wait for her daemon to detect a new tx.
// Loop here until her unverifiedBalance != 0, or give up after 15 seconds.
// A slow test is preferred over a flaky test.
BsqBalanceInfo bsqBalance = getBsqBalances(daemon);
for (int numRequests = 1; numRequests <= 15 && bsqBalance.getUnverifiedBalance() == 0; numRequests++) {
sleep(1000);
bsqBalance = getBsqBalances(daemon);
}
return bsqBalance;
}
private BsqBalanceInfo waitForNewAvailableConfirmedBalance(BisqAppConfig daemon,
long staleBalance) {
BsqBalanceInfo bsqBalance = getBsqBalances(daemon);
for (int numRequests = 1;
numRequests <= 15 && bsqBalance.getAvailableConfirmedBalance() == staleBalance;
numRequests++) {
sleep(1000);
bsqBalance = getBsqBalances(daemon);
}
return bsqBalance;
}
@SuppressWarnings("SameParameterValue")
private void printBobAndAliceBsqBalances(final TestInfo testInfo,
BsqBalanceInfo bobsBsqBalances,
BsqBalanceInfo alicesBsqBalances,
BisqAppConfig senderApp) {
log.info("{} -> Bob's BSQ Balances After {} {} BSQ-> \n{}",
testName(testInfo),
senderApp.equals(bobdaemon) ? "Sending" : "Receiving",
SEND_BSQ_AMOUNT,
formatBsqBalanceInfoTbl(bobsBsqBalances));
log.info("{} -> Alice's Balances After {} {} BSQ-> \n{}",
testName(testInfo),
senderApp.equals(alicedaemon) ? "Sending" : "Receiving",
SEND_BSQ_AMOUNT,
formatBsqBalanceInfoTbl(alicesBsqBalances));
}
@SuppressWarnings("SameParameterValue")
private static bisq.core.api.model.BsqBalanceInfo expectedBsqBalanceModel(long availableConfirmedBalance,
long unverifiedBalance,
long unconfirmedChangeBalance,
long lockedForVotingBalance,
long lockupBondsBalance,
long unlockingBondsBalance) {
return bisq.core.api.model.BsqBalanceInfo.valueOf(availableConfirmedBalance,
unverifiedBalance,
unconfirmedChangeBalance,
lockedForVotingBalance,
lockupBondsBalance,
unlockingBondsBalance);
}
}

View file

@ -0,0 +1,102 @@
package bisq.apitest.method.wallet;
import bisq.proto.grpc.BtcBalanceInfo;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind;
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
import static bisq.apitest.config.BisqAppConfig.bobdaemon;
import static bisq.apitest.config.BisqAppConfig.seednode;
import static bisq.cli.TableFormat.formatAddressBalanceTbl;
import static bisq.cli.TableFormat.formatBtcBalanceInfoTbl;
import static java.util.Collections.singletonList;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import bisq.apitest.method.MethodTest;
@Disabled
@Slf4j
@TestMethodOrder(OrderAnnotation.class)
public class BtcWalletTest extends MethodTest {
// All api tests depend on the DAO / regtest environment, and Bob & Alice's wallets
// are initialized with 10 BTC during the scaffolding setup.
private static final bisq.core.api.model.BtcBalanceInfo INITIAL_BTC_BALANCES =
bisq.core.api.model.BtcBalanceInfo.valueOf(1000000000,
0,
1000000000,
0);
@BeforeAll
public static void setUp() {
startSupportingApps(false,
true,
bitcoind,
seednode,
alicedaemon,
bobdaemon);
}
@Test
@Order(1)
public void testDeprecatedAvailableBtcBalance() {
// Alice's regtest Bisq wallet was initialized with 10 BTC.
long balance = getBalance(alicedaemon); // @Deprecated method
assertEquals(INITIAL_BTC_BALANCES.getAvailableBalance(), balance);
// Bob's regtest Bisq wallet was initialized with 10 BTC.
balance = getBalance(bobdaemon); // @Deprecated method
assertEquals(INITIAL_BTC_BALANCES.getAvailableBalance(), balance);
}
@Test
@Order(2)
public void testFundAlicesBtcWallet(final TestInfo testInfo) {
String newAddress = getUnusedBtcAddress(alicedaemon);
bitcoinCli.sendToAddress(newAddress, "2.5");
genBtcBlocksThenWait(1, 1500);
long balance = getBalance(alicedaemon); // @Deprecated method
assertEquals(1250000000, balance); // new balance is 12.5 btc
log.info("{} -> Alice's Funded Address Balance -> \n{}",
testName(testInfo),
formatAddressBalanceTbl(singletonList(getAddressBalance(alicedaemon, newAddress))));
BtcBalanceInfo btcBalanceInfo = getBtcBalances(alicedaemon); // new balance is 12.5 btc
bisq.core.api.model.BtcBalanceInfo alicesExpectedBalances =
bisq.core.api.model.BtcBalanceInfo.valueOf(1250000000,
0,
1250000000,
0);
verifyBtcBalances(alicesExpectedBalances, btcBalanceInfo);
log.info("{} -> Alice's BTC Balances After Sending 2.5 BTC -> \n{}",
testName(testInfo),
formatBtcBalanceInfoTbl(btcBalanceInfo));
}
@AfterAll
public static void tearDown() {
tearDownScaffold();
}
private void verifyBtcBalances(bisq.core.api.model.BtcBalanceInfo expected,
bisq.proto.grpc.BtcBalanceInfo actual) {
assertEquals(expected.getAvailableBalance(), actual.getAvailableBalance());
assertEquals(expected.getReservedBalance(), actual.getReservedBalance());
assertEquals(expected.getTotalAvailableBalance(), actual.getTotalAvailableBalance());
assertEquals(expected.getLockedBalance(), actual.getLockedBalance());
}
}

View file

@ -40,6 +40,7 @@ import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import bisq.apitest.method.MethodTest;
@Deprecated
@Disabled
@Slf4j
@TestMethodOrder(OrderAnnotation.class)

View file

@ -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();
}
}

View file

@ -24,55 +24,69 @@ 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.arbdaemon;
import static bisq.apitest.config.BisqAppConfig.bobdaemon;
import static bisq.apitest.config.BisqAppConfig.seednode;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
import bisq.apitest.method.MethodTest;
import bisq.apitest.method.wallet.BsqWalletTest;
import bisq.apitest.method.wallet.BtcWalletTest;
import bisq.apitest.method.wallet.WalletBalancesTest;
import bisq.apitest.method.wallet.WalletProtectionTest;
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class WalletTest extends MethodTest {
// All tests depend on the DAO / regtest environment, and Alice's wallet is
// initialized with 10 BTC during the scaffolding setup.
@BeforeAll
public static void setUp() {
try {
setUpScaffold(bitcoind, seednode, alicedaemon);
genBtcBlocksThenWait(1, 1500);
} catch (Exception ex) {
fail(ex);
}
startSupportingApps(true,
true,
bitcoind,
seednode,
arbdaemon,
alicedaemon,
bobdaemon);
}
@Test
@Order(1)
public void testFundWallet() {
// The regtest Bisq wallet was initialized with 10 BTC.
long balance = getBalance(alicedaemon);
assertEquals(1000000000, balance);
public void testGetWalletBalances(final TestInfo testInfo) {
WalletBalancesTest btcWalletTest = new WalletBalancesTest();
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
btcWalletTest.testDeprecatedAvailableBtcBalance();
btcWalletTest.testNewGetBalances(testInfo);
}
@Test
@Order(2)
public void testBtcWalletFunding(final TestInfo testInfo) {
BtcWalletTest btcWalletTest = new BtcWalletTest();
btcWalletTest.testDeprecatedAvailableBtcBalance();
btcWalletTest.testFundAlicesBtcWallet(testInfo);
}
@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(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

View file

@ -24,6 +24,9 @@ import bisq.proto.grpc.CreateOfferRequest;
import bisq.proto.grpc.CreatePaymentAccountRequest;
import bisq.proto.grpc.GetAddressBalanceRequest;
import bisq.proto.grpc.GetBalanceRequest;
import bisq.proto.grpc.GetBalancesRequest;
import bisq.proto.grpc.GetBsqBalancesRequest;
import bisq.proto.grpc.GetBtcBalancesRequest;
import bisq.proto.grpc.GetFundingAddressesRequest;
import bisq.proto.grpc.GetOfferRequest;
import bisq.proto.grpc.GetOffersRequest;
@ -58,6 +61,7 @@ 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.formatBalancesTbls;
import static bisq.cli.TableFormat.formatOfferTable;
import static bisq.cli.TableFormat.formatPaymentAcctTbl;
import static java.lang.String.format;
@ -88,7 +92,10 @@ public class CliMain {
createpaymentacct,
getpaymentaccts,
getversion,
getbalance,
@Deprecated getbalance, // Use getbalances, return bsq and btc balance info
getbalances,
getbsqbalance,
getbtcbalance,
getaddressbalance,
getfundingaddresses,
getunusedbsqaddress,
@ -185,12 +192,31 @@ public class CliMain {
return;
}
case getbalance: {
// Deprecated, use getbalances.
var request = GetBalanceRequest.newBuilder().build();
var reply = walletsService.getBalance(request);
var btcBalance = formatSatoshis(reply.getBalance());
out.println(btcBalance);
return;
}
case getbalances: {
var request = GetBalancesRequest.newBuilder().build();
var reply = walletsService.getBalances(request);
out.println(formatBalancesTbls(reply.getBalances()));
return;
}
case getbsqbalance: {
var request = GetBsqBalancesRequest.newBuilder().build();
var reply = walletsService.getBsqBalances(request);
out.println(reply.getBsqBalanceInfo());
return;
}
case getbtcbalance: {
var request = GetBtcBalancesRequest.newBuilder().build();
var reply = walletsService.getBtcBalances(request);
out.println(reply.getBtcBalanceInfo());
return;
}
case getaddressbalance: {
if (nonOptionArgs.size() < 2)
throw new IllegalArgumentException("no address specified");
@ -494,7 +520,10 @@ public class CliMain {
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");

View file

@ -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";

View file

@ -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)

View file

@ -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.TimeZone;
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> 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> 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 @@ class TableFormat {
.collect(Collectors.joining("\n"));
}
static String formatOfferTable(List<OfferInfo> 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> 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

View file

@ -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 class CoreApi {
// 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);
}

View file

@ -18,6 +18,9 @@
package bisq.core.api;
import bisq.core.api.model.AddressBalanceInfo;
import bisq.core.api.model.BalancesInfo;
import bisq.core.api.model.BsqBalanceInfo;
import bisq.core.api.model.BtcBalanceInfo;
import bisq.core.btc.Balances;
import bisq.core.btc.model.AddressEntry;
import bisq.core.btc.wallet.BsqWalletService;
@ -82,6 +85,7 @@ class CoreWalletsService {
return tempAesKey;
}
@Deprecated
long getAvailableBalance() {
verifyWalletsAreAvailable();
verifyEncryptedWalletIsUnlocked();
@ -93,6 +97,56 @@ class CoreWalletsService {
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;

View file

@ -19,11 +19,19 @@ package bisq.daemon.grpc;
import bisq.core.api.CoreApi;
import bisq.core.api.model.AddressBalanceInfo;
import bisq.core.api.model.BsqBalanceInfo;
import bisq.core.api.model.BtcBalanceInfo;
import bisq.proto.grpc.GetAddressBalanceReply;
import bisq.proto.grpc.GetAddressBalanceRequest;
import bisq.proto.grpc.GetBalanceReply;
import bisq.proto.grpc.GetBalanceRequest;
import bisq.proto.grpc.GetBalancesReply;
import bisq.proto.grpc.GetBalancesRequest;
import bisq.proto.grpc.GetBsqBalancesReply;
import bisq.proto.grpc.GetBsqBalancesRequest;
import bisq.proto.grpc.GetBtcBalancesReply;
import bisq.proto.grpc.GetBtcBalancesRequest;
import bisq.proto.grpc.GetFundingAddressesReply;
import bisq.proto.grpc.GetFundingAddressesRequest;
import bisq.proto.grpc.GetUnusedBsqAddressReply;
@ -56,12 +64,8 @@ class GrpcWalletsService extends WalletsGrpc.WalletsImplBase {
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<GetBalanceReply> responseObserver) {
try {
@ -76,6 +80,54 @@ class GrpcWalletsService extends WalletsGrpc.WalletsImplBase {
}
}
@Override
public void getBalances(GetBalancesRequest req, StreamObserver<GetBalancesReply> 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<GetBsqBalancesReply> 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<GetBtcBalancesReply> 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<GetAddressBalanceReply> responseObserver) {