Skip to content

Commit

Permalink
core: Implement sendBsqTest
Browse files Browse the repository at this point in the history
First, the sendBsq test creates one BTC and two BSQ wallets. Afterward,
it funds the BTC and one BSQ wallet with 1 BTC. Next, the funded BSQ
wallet sends 100 BSQ to the second BSQ wallet.
  • Loading branch information
alvasw committed Nov 14, 2024
1 parent 3263a91 commit abcc9f8
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 2 deletions.
8 changes: 8 additions & 0 deletions core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ dependencies {
integrationTestImplementation('bitcoind:regtest') {
exclude(module: 'kotlin-stdlib-jdk8')
}
integrationTestImplementation libs.hamcrest
integrationTestImplementation libs.mockito.core
integrationTestImplementation libs.mockito.junit.jupiter

integrationTestImplementation libs.junit.jupiter.api
integrationTestImplementation libs.junit.jupiter.params

integrationTestRuntimeOnly libs.junit.jupiter.engine
}

test {
Expand Down
148 changes: 148 additions & 0 deletions core/src/integrationTest/java/bisq/core/BitcoinjBsqTests.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package bisq.core;

import bisq.core.btc.exceptions.BsqChangeBelowDustException;
import bisq.core.btc.wallet.BisqDefaultCoinSelector;
import bisq.core.btc.wallet.BsqCoinSelector;
import bisq.core.btc.wallet.BsqWalletV2;
import bisq.core.btc.wallet.BtcWalletV2;
import bisq.core.btc.wallet.WalletFactory;
import bisq.core.dao.state.DaoStateService;
import bisq.core.dao.state.model.blockchain.TxOutputKey;
import bisq.core.dao.state.unconfirmed.UnconfirmedBsqChangeOutputListService;

import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.InsufficientMoneyException;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.kits.WalletAppKit;
import org.bitcoinj.params.RegTestParams;
import org.bitcoinj.wallet.Wallet;

import java.nio.file.Path;

import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import lombok.extern.slf4j.Slf4j;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;



import bisq.wallets.regtest.BitcoindExtension;
import bisq.wallets.regtest.bitcoind.BitcoindRegtestSetup;

@ExtendWith(BitcoindExtension.class)
@Slf4j
public class BitcoinjBsqTests {

private static class BisqRegtestNetworkParams extends RegTestParams {
public void setPort(int port) {
this.port = port;
}
}

private final BitcoindRegtestSetup regtestSetup;
private final BisqRegtestNetworkParams networkParams;

public BitcoinjBsqTests(BitcoindRegtestSetup regtestSetup) {
this.regtestSetup = regtestSetup;
networkParams = new BisqRegtestNetworkParams();
networkParams.setPort(regtestSetup.getP2pPort());
}

@Test
void sendBsqTest(@TempDir Path tempDir) throws InterruptedException, InsufficientMoneyException, BsqChangeBelowDustException {
var walletFactory = new WalletFactory(networkParams);
Wallet btcWallet = walletFactory.createBtcWallet();
Wallet secondBsqWallet = walletFactory.createBsqWallet();

var wallets = List.of(btcWallet, secondBsqWallet);
var regtestWalletAppKit = new RegtestWalletAppKit(networkParams, tempDir, wallets);
regtestWalletAppKit.initialize();

WalletAppKit walletAppKit = regtestWalletAppKit.getWalletAppKit();
Wallet bsqWallet = walletAppKit.wallet();

var bsqWalletReceivedLatch = new CountDownLatch(1);
bsqWallet.addCoinsReceivedEventListener((wallet, tx, prevBalance, newBalance) ->
bsqWalletReceivedLatch.countDown());

var btcWalletReceivedLatch = new CountDownLatch(1);
btcWallet.addCoinsReceivedEventListener((wallet, tx, prevBalance, newBalance) ->
btcWalletReceivedLatch.countDown());

Address currentReceiveAddress = bsqWallet.currentReceiveAddress();
String address = currentReceiveAddress.toString();
regtestSetup.fundAddress(address, 1.0);

currentReceiveAddress = btcWallet.currentReceiveAddress();
address = currentReceiveAddress.toString();
regtestSetup.fundAddress(address, 1.0);

regtestSetup.mineOneBlock();

boolean isSuccess = bsqWalletReceivedLatch.await(30, TimeUnit.SECONDS);
assertThat("BSQ wallet not funded after 30 seconds.", isSuccess);

Coin balance = bsqWallet.getBalance();
assertThat("BitcoinJ BSQ wallet balance should equal 1 BTC.", balance.equals(Coin.COIN));

isSuccess = btcWalletReceivedLatch.await(30, TimeUnit.SECONDS);
assertThat("BTC wallet not funded after 30 seconds.", isSuccess);

balance = btcWallet.getBalance();
assertThat("BitcoinJ BTC wallet balance should equal 1 BTC.", balance.equals(Coin.COIN));

DaoStateService daoStateService = mock(DaoStateService.class);
doReturn(true).when(daoStateService)
.isTxOutputSpendable(any(TxOutputKey.class));

var bsqCoinSelector = new BsqCoinSelector(daoStateService, mock(UnconfirmedBsqChangeOutputListService.class));
var btcCoinSelector = new BisqDefaultCoinSelector(true) {
@Override
protected boolean isDustAttackUtxo(TransactionOutput output) {
return false;
}

@Override
protected boolean isTxOutputSpendable(TransactionOutput output) {
return true;
}
};

var btcWalletV2 = new BtcWalletV2(btcCoinSelector, btcWallet);
var bsqWalletV2 = new BsqWalletV2(networkParams,
walletAppKit.peerGroup(),
btcWalletV2,
bsqWallet,
bsqCoinSelector);

var secondBsqWalletReceivedLatch = new CountDownLatch(1);
secondBsqWallet.addCoinsReceivedEventListener((wallet, tx, prevBalance, newBalance) ->
secondBsqWalletReceivedLatch.countDown());

// Send 100 BSQ (1 BSQ = 100 Satoshis)
Address receiverAddress = secondBsqWallet.currentReceiveAddress();
Coin receiverAmount = Coin.ofSat(100 * 100);
bsqWalletV2.sendBsq(receiverAddress, receiverAmount, Coin.ofSat(10));

regtestSetup.mineOneBlock();

isSuccess = secondBsqWalletReceivedLatch.await(30, TimeUnit.SECONDS);
assertThat("Didn't receive BSQ after 30 seconds.", isSuccess);

assertEquals(bsqWallet.getBalance(), Coin.ofSat(99990000));
assertEquals(btcWallet.getBalance(), Coin.ofSat(99999747));
assertEquals(secondBsqWallet.getBalance(), Coin.ofSat(10000));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ protected boolean isTxSpendable(Transaction tx) {
return isConfirmed || (isPending && (permitForeignPendingTx || isOwnTx));
}

abstract boolean isTxOutputSpendable(TransactionOutput output);
protected abstract boolean isTxOutputSpendable(TransactionOutput output);

// TODO Why it uses coin age and not try to minimize number of inputs as the highest priority?
// Asked Oscar and he also don't knows why coin age is used. Should be changed so that min. number of inputs is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
* We lookup for spendable outputs which matches any of our addresses.
*/
@Slf4j
class BtcCoinSelector extends BisqDefaultCoinSelector {
public class BtcCoinSelector extends BisqDefaultCoinSelector {
private final Set<Address> addresses;
private final long ignoreDustThreshold;

Expand Down

0 comments on commit abcc9f8

Please sign in to comment.