Testkit wallet with bitcoind uses bitcoind as api (#1499)

* Testkit wallet with bitcoind uses bitcoind as api

* Fix docs
This commit is contained in:
Ben Carman 2020-06-03 14:05:22 -05:00 committed by GitHub
parent 29c667c18b
commit 1dd6025b9d
6 changed files with 135 additions and 138 deletions

View file

@ -15,6 +15,7 @@ import org.bitcoins.core.api.ChainQueryApi.FilterResponse
import org.bitcoins.core.gcs.FilterType
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.core.script.crypto.HashType
import org.bitcoins.core.util.FutureUtil
import org.bitcoins.crypto.ECPrivateKey
import org.bitcoins.rpc.client.common.{
BitcoindRpcClient,
@ -42,16 +43,24 @@ class BitcoindV19RpcClient(override val instance: BitcoindInstance)(
override def getFiltersBetweenHeights(
startHeight: Int,
endHeight: Int): Future[Vector[ChainQueryApi.FilterResponse]] = {
Future.sequence(
startHeight
.until(endHeight)
.map { height =>
val allHeights = startHeight.to(endHeight)
def f(range: Vector[Int]): Future[Vector[FilterResponse]] = {
val filterFs = range.map { height =>
for {
hash <- getBlockHash(height)
filter <- getBlockFilter(hash, FilterType.Basic)
} yield FilterResponse(filter.filter, hash, height)
} yield {
FilterResponse(filter.filter, hash, height)
}
.toVector)
}
Future.sequence(filterFs)
}
FutureUtil.batchExecute(elements = allHeights.toVector,
f = f,
init = Vector.empty,
batchSize = 25)
}
override def getFilterCount: Future[Int] = getBlockCount

View file

@ -17,6 +17,7 @@ To run this example you need to make sure you have access to a bitcoind binary.
```scala mdoc:invisible
import org.bitcoins.testkit.BitcoinSTestAppConfig
import org.bitcoins.testkit.fixtures._
import org.bitcoins.testkit.wallet._
import org.bitcoins.server.BitcoinSAppConfig
import akka.actor.ActorSystem
@ -33,8 +34,9 @@ implicit val appConfig: BitcoinSAppConfig = BitcoinSTestAppConfig.getNeutrinoTes
//ok now let's spin up a bitcoind and a bitcoin-s wallet with funds in it
val walletWithBitcoindF = for {
w <- BitcoinSWalletTest.createWalletBitcoindNodeChainQueryApi()
} yield w
bitcoind <- BitcoinSFixture.createBitcoindWithFunds()
walletWithBitcoind <- BitcoinSWalletTest.createWalletWithBitcoindCallbacks(bitcoind)
} yield walletWithBitcoind
val walletF = walletWithBitcoindF.map(_.wallet)

View file

@ -1,17 +1,17 @@
package org.bitcoins.testkit.chain
import org.bitcoins.chain.blockchain.sync.FilterWithHeaderHash
import org.bitcoins.commons.jsonmodels.bitcoind.GetBlockFilterResult
import org.bitcoins.core.api.ChainQueryApi.FilterResponse
import org.bitcoins.core.api.{ChainQueryApi, NodeApi, NodeChainQueryApi}
import org.bitcoins.core.gcs.FilterType
import org.bitcoins.core.protocol.BlockStamp
import org.bitcoins.core.protocol.blockchain.{Block, BlockHeader}
import org.bitcoins.core.protocol.blockchain.BlockHeader
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.core.util.{BitcoinSLogger, FutureUtil}
import org.bitcoins.crypto.{DoubleSha256Digest, DoubleSha256DigestBE}
import org.bitcoins.rpc.client.common.BitcoindRpcClient
import org.bitcoins.rpc.client.v19.BitcoindV19RpcClient
import org.bitcoins.commons.jsonmodels.bitcoind.GetBlockFilterResult
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.crypto.{DoubleSha256Digest, DoubleSha256DigestBE}
import org.bitcoins.wallet.Wallet
import scala.concurrent.{ExecutionContext, Future}
@ -45,75 +45,41 @@ abstract class SyncUtil extends BitcoinSLogger {
}
}
def getChainQueryApi(bitcoindV19RpcClient: BitcoindV19RpcClient)(
implicit ec: ExecutionContext): ChainQueryApi = {
def getTestChainQueryApi(bitcoind: BitcoindRpcClient): ChainQueryApi = {
new ChainQueryApi {
/** Gets the height of the given block */
override def getBlockHeight(
blockHash: DoubleSha256DigestBE): Future[Option[Int]] = {
bitcoindV19RpcClient
.getBlockHeader(blockHash)
.map(b => Some(b.height))
}
blockHash: DoubleSha256DigestBE): Future[Option[Int]] =
bitcoind.getBlockHeight(blockHash)
/** Gets the hash of the block that is what we consider "best" */
override def getBestBlockHash(): Future[DoubleSha256DigestBE] = {
bitcoindV19RpcClient.getBestBlockHash
bitcoind.getBestBlockHash
}
/** Gets number of confirmations for the given block hash */
override def getNumberOfConfirmations(
blockHashOpt: DoubleSha256DigestBE): Future[Option[Int]] = {
bitcoindV19RpcClient.getBlock(blockHashOpt).map { b =>
Some(b.confirmations)
}
bitcoind.getNumberOfConfirmations(blockHashOpt)
}
/** Gets the number of compact filters in the database */
override def getFilterCount: Future[Int] = {
//filter count should be same as block height?
bitcoindV19RpcClient.getBlockCount
bitcoind.getFilterCount
}
/** Returns the block height of the given block stamp */
override def getHeightByBlockStamp(
blockStamp: BlockStamp): Future[Int] = {
blockStamp match {
case BlockStamp.BlockHash(hash) => getBlockHeight(hash).map(_.get)
case BlockStamp.BlockHeight(height) =>
Future.successful(height)
case BlockStamp.BlockTime(_) =>
throw new RuntimeException("Cannot query by block time")
}
}
override def getHeightByBlockStamp(blockStamp: BlockStamp): Future[Int] =
bitcoind.getHeightByBlockStamp(blockStamp)
override def epochSecondToBlockHeight(time: Long): Future[Int] =
Future.successful(0)
override def getFiltersBetweenHeights(
startHeight: Int,
endHeight: Int): Future[Vector[FilterResponse]] = {
val allHeights = startHeight.to(endHeight)
def f(range: Vector[Int]): Future[Vector[FilterResponse]] = {
val filterFs = range.map { height =>
for {
hash <- bitcoindV19RpcClient.getBlockHash(height)
filter <- bitcoindV19RpcClient.getBlockFilter(hash,
FilterType.Basic)
} yield {
FilterResponse(filter.filter, hash, height)
}
}
Future.sequence(filterFs)
}
FutureUtil.batchExecute(elements = allHeights.toVector,
f = f,
init = Vector.empty,
batchSize = 25)
}
endHeight: Int): Future[Vector[FilterResponse]] =
bitcoind.getFiltersBetweenHeights(startHeight, endHeight)
}
}
@ -168,13 +134,9 @@ abstract class SyncUtil extends BitcoinSLogger {
override def downloadBlocks(
blockHashes: Vector[DoubleSha256Digest]): Future[Unit] = {
logger.info(s"Fetching ${blockHashes.length} hashes from bitcoind")
val f: Vector[DoubleSha256Digest] => Future[Wallet] = {
case hashes =>
val fetchedBlocks: Vector[Future[Block]] = hashes.map {
bitcoindRpcClient
.getBlockRaw(_)
}
val blocksF = Future.sequence(fetchedBlocks)
val f: Vector[DoubleSha256Digest] => Future[Wallet] = { hashes =>
val blocksF =
FutureUtil.sequentially(hashes)(bitcoindRpcClient.getBlockRaw)
val updatedWalletF = for {
blocks <- blocksF
@ -182,7 +144,7 @@ abstract class SyncUtil extends BitcoinSLogger {
processedWallet <- {
FutureUtil.foldLeftAsync(wallet, blocks) {
case (wallet, block) =>
wallet.processBlock(block).map(_.asInstanceOf[Wallet])
wallet.processBlock(block)
}
}
} yield processedWallet
@ -213,20 +175,20 @@ abstract class SyncUtil extends BitcoinSLogger {
}
}
def getNodeChainQueryApi(bitcoindV19RpcClient: BitcoindV19RpcClient)(
def getNodeChainQueryApi(bitcoind: BitcoindRpcClient)(
implicit ec: ExecutionContext): NodeChainQueryApi = {
val chainQuery = SyncUtil.getChainQueryApi(bitcoindV19RpcClient)
val nodeApi = SyncUtil.getNodeApi(bitcoindV19RpcClient)
val chainQuery = SyncUtil.getTestChainQueryApi(bitcoind)
val nodeApi = SyncUtil.getNodeApi(bitcoind)
NodeChainQueryApi(nodeApi, chainQuery)
}
def getNodeChainQueryApiWalletCallback(
bitcoindV19RpcClient: BitcoindV19RpcClient,
bitcoind: BitcoindRpcClient,
walletF: Future[Wallet])(
implicit ec: ExecutionContext): NodeChainQueryApi = {
val chainQuery = SyncUtil.getChainQueryApi(bitcoindV19RpcClient)
val chainQuery = SyncUtil.getTestChainQueryApi(bitcoind)
val nodeApi =
SyncUtil.getNodeApiWalletCallback(bitcoindV19RpcClient, walletF)
SyncUtil.getNodeApiWalletCallback(bitcoind, walletF)
NodeChainQueryApi(nodeApi, chainQuery)
}
}

View file

@ -12,7 +12,6 @@ import org.bitcoins.core.util.{FutureUtil, TimeUtil}
import org.bitcoins.core.wallet.fee.{FeeUnit, SatoshisPerVirtualByte}
import org.bitcoins.crypto.{DoubleSha256Digest, DoubleSha256DigestBE}
import org.bitcoins.db.AppConfig
import org.bitcoins.feeprovider.ConstantFeeRateProvider
import org.bitcoins.keymanager.bip39.BIP39KeyManager
import org.bitcoins.rpc.client.common.{BitcoindRpcClient, BitcoindVersion}
import org.bitcoins.rpc.client.v19.BitcoindV19RpcClient
@ -203,12 +202,30 @@ trait BitcoinSWalletTest
def withNewWalletAndBitcoind(test: OneArgAsyncTest): FutureOutcome = {
val builder: () => Future[WalletWithBitcoind] = composeBuildersAndWrap(
builder = { () =>
createDefaultWallet(nodeApi, chainQueryApi)
BitcoinSFixture.createBitcoindWithFunds()
},
dependentBuilder = { (wallet: Wallet) =>
createWalletWithBitcoind(wallet)
dependentBuilder = { (bitcoind: BitcoindRpcClient) =>
createWalletWithBitcoind(bitcoind)
},
wrap = (_: WalletApi, walletWithBitcoind: WalletWithBitcoind) =>
wrap = (_: BitcoindRpcClient, walletWithBitcoind: WalletWithBitcoind) =>
walletWithBitcoind
)
makeDependentFixture(builder, destroy = destroyWalletWithBitcoind)(test)
}
def withNewWalletAndBitcoindV19(test: OneArgAsyncTest): FutureOutcome = {
val builder: () => Future[WalletWithBitcoind] = composeBuildersAndWrap(
builder = { () =>
BitcoinSFixture
.createBitcoindWithFunds(Some(BitcoindVersion.V19))
.map(_.asInstanceOf[BitcoindV19RpcClient])
},
dependentBuilder = { (bitcoind: BitcoindV19RpcClient) =>
createWalletWithBitcoindV19(bitcoind)
},
wrap =
(_: BitcoindV19RpcClient, walletWithBitcoind: WalletWithBitcoindV19) =>
walletWithBitcoind
)
@ -216,17 +233,14 @@ trait BitcoinSWalletTest
}
def withFundedWalletAndBitcoind(test: OneArgAsyncTest): FutureOutcome = {
val builder: () => Future[WalletWithBitcoind] =
composeBuildersAndWrapFuture(
builder = { () =>
BitcoinSWalletTest.createWallet2Accounts(nodeApi, chainQueryApi)
},
dependentBuilder = { (wallet: Wallet) =>
createWalletWithBitcoind(wallet)
},
processResult = (_: WalletApi, pair: WalletWithBitcoind) =>
fundWalletWithBitcoind(pair)
)
val builder: () => Future[WalletWithBitcoind] = { () =>
for {
bitcoind <- BitcoinSFixture
.createBitcoindWithFunds(None)
wallet <- createWalletWithBitcoindCallbacks(bitcoind)
fundedWallet <- fundWalletWithBitcoind(wallet)
} yield fundedWallet
}
makeDependentFixture(builder, destroy = destroyWalletWithBitcoind)(test)
}
@ -234,9 +248,14 @@ trait BitcoinSWalletTest
def withFundedWalletAndBitcoindV19(test: OneArgAsyncTest): FutureOutcome = {
val builder: () => Future[WalletWithBitcoindV19] = { () =>
for {
walletBitcoind <- createWalletBitcoindNodeChainQueryApi()
fundedWallet <- fundWalletWithBitcoind(walletBitcoind)
} yield fundedWallet
bitcoind <- BitcoinSFixture
.createBitcoindWithFunds(Some(BitcoindVersion.V19))
.map(_.asInstanceOf[BitcoindV19RpcClient])
wallet <- createWalletWithBitcoindCallbacks(bitcoind)
fundedWallet <- fundWalletWithBitcoind(wallet)
} yield {
WalletWithBitcoindV19(fundedWallet.wallet, bitcoind)
}
}
makeDependentFixture(builder, destroy = destroyWalletWithBitcoind)(test)
@ -406,50 +425,39 @@ object BitcoinSWalletTest extends WalletLogger {
/** Creates a default wallet with bitcoind where the [[ChainQueryApi]] fed to the wallet
* is implemented by bitcoind */
def createWalletBitcoindNodeChainQueryApi(extraConfig: Option[Config] = None)(
def createWalletWithBitcoindCallbacks(
bitcoind: BitcoindRpcClient,
extraConfig: Option[Config] = None)(
implicit config: BitcoinSAppConfig,
system: ActorSystem): Future[WalletWithBitcoindV19] = {
system: ActorSystem): Future[WalletWithBitcoind] = {
import system.dispatcher
val bitcoindF = BitcoinSFixture
.createBitcoindWithFunds(Some(BitcoindVersion.V19))
.map(_.asInstanceOf[BitcoindV19RpcClient])
val nodeChainQueryApiF =
bitcoindF.map(b => SyncUtil.getNodeChainQueryApi(b))
val walletCallbackP = Promise[Wallet]()
val walletWithBitcoindV19F = for {
bitcoind <- bitcoindF
api <- nodeChainQueryApiF
wallet <- BitcoinSWalletTest.createWallet2Accounts(api.nodeApi,
api.chainQueryApi,
extraConfig)
//we need to create a promise so we can inject the wallet with the callback
//after we have created it into SyncUtil.getNodeChainQueryApiWalletCallback
//after we have created it into SyncUtil.getNodeApiWalletCallback
//so we don't lose the internal state of the wallet
//now unfortunately we have to create _another_ wallet that has the correct callback
//setup for our wallet so we can receive block updates from bitcoind
apiCallback = SyncUtil.getNodeChainQueryApiWalletCallback(
bitcoindV19RpcClient = bitcoind,
walletF = walletCallbackP.future)
val walletCallbackP = Promise[Wallet]()
val walletWithBitcoindF = for {
wallet <- BitcoinSWalletTest.createWallet2Accounts(bitcoind,
bitcoind,
extraConfig)
//create the wallet with the appropriate callbacks now that
//we have them
walletWithCallback = Wallet(
keyManager = wallet.keyManager,
nodeApi = apiCallback.nodeApi,
chainQueryApi = apiCallback.chainQueryApi,
feeRateApi = ConstantFeeRateProvider(SatoshisPerVirtualByte.one),
nodeApi =
SyncUtil.getNodeApiWalletCallback(bitcoind, walletCallbackP.future),
chainQueryApi = SyncUtil.getTestChainQueryApi(bitcoind),
feeRateApi = bitcoind,
creationTime = wallet.keyManager.creationTime
)(wallet.walletConfig, wallet.ec)
//complete the walletCallbackP so we can handle the callbacks when they are
//called without hanging forever.
_ = walletCallbackP.success(walletWithCallback)
} yield WalletWithBitcoindV19(walletWithCallback, bitcoind)
} yield WalletWithBitcoindRpc(walletWithCallback, bitcoind)
walletWithBitcoindV19F.failed.foreach(err => walletCallbackP.failure(err))
walletWithBitcoindF.failed.foreach(err => walletCallbackP.failure(err))
walletWithBitcoindV19F
walletWithBitcoindF
}
def createWallet2Accounts(
@ -490,6 +498,12 @@ object BitcoinSWalletTest extends WalletLogger {
bitcoindF.map(WalletWithBitcoindRpc(wallet, _))
}
def createWalletWithBitcoind(bitcoind: BitcoindRpcClient)(
implicit system: ActorSystem,
config: BitcoinSAppConfig): Future[WalletWithBitcoind] = {
createWalletWithBitcoindCallbacks(bitcoind, None)
}
def createWalletWithBitcoindV19(wallet: Wallet)(
implicit system: ActorSystem): Future[WalletWithBitcoindV19] = {
import system.dispatcher
@ -502,6 +516,16 @@ object BitcoinSWalletTest extends WalletLogger {
created.bitcoind.asInstanceOf[BitcoindV19RpcClient])
}
def createWalletWithBitcoindV19(bitcoind: BitcoindV19RpcClient)(
implicit system: ActorSystem,
config: BitcoinSAppConfig): Future[WalletWithBitcoindV19] = {
import system.dispatcher
for {
created <- createWalletWithBitcoindCallbacks(bitcoind)
} yield WalletWithBitcoindV19(created.wallet, bitcoind)
}
def createWalletWithBitcoind(
wallet: Wallet,
bitcoindRpcClient: BitcoindRpcClient

View file

@ -1,7 +1,8 @@
package org.bitcoins.wallet
import org.bitcoins.core.currency.{Bitcoins, CurrencyUnits}
import org.bitcoins.core.currency.{Bitcoins, CurrencyUnits, Satoshis}
import org.bitcoins.core.protocol.BlockStamp
import org.bitcoins.core.util.FutureUtil
import org.bitcoins.server.BitcoinSAppConfig
import org.bitcoins.testkit.BitcoinSTestAppConfig
import org.bitcoins.testkit.wallet.BitcoinSWalletTest
@ -11,8 +12,6 @@ import org.bitcoins.testkit.wallet.BitcoinSWalletTest.{
}
import org.scalatest.FutureOutcome
import scala.concurrent.Future
class RescanHandlingTest extends BitcoinSWalletTest {
/** Wallet config with data directory set to user temp directory */
@ -54,6 +53,8 @@ class RescanHandlingTest extends BitcoinSWalletTest {
val wallet = fixture.wallet
for {
balance <- wallet.getBalance()
_ = assert(balance != Satoshis.zero)
utxos <- wallet.spendingInfoDAO.findAll()
_ = assert(utxos.nonEmpty)
@ -194,11 +195,10 @@ class RescanHandlingTest extends BitcoinSWalletTest {
val oldestHeightF = for {
utxos <- utxosF
blockhashes = utxos.map(_.blockHash)
heights <- Future.sequence {
blockhashes.map(h =>
wallet.chainQueryApi.getBlockHeight(h.get).map(_.get))
heights <- FutureUtil.sequentially(blockhashes) { hash =>
wallet.chainQueryApi.getBlockHeight(hash.get)
}
} yield heights.min
} yield heights.min.get
//ok now that we have the height of the oldest utxo, let's rescan up to then
val rescanF = for {

View file

@ -93,10 +93,10 @@ class WalletIntegrationTest extends BitcoinSWalletTest {
.map(balance => assert(balance == valueFromBitcoind))
_ <- wallet
.getConfirmedBalance()
.map(confirmed => assert(confirmed == 0.bitcoin))
.map(confirmed => assert(confirmed == valueFromBitcoind))
_ <- wallet
.getUnconfirmedBalance()
.map(unconfirmed => assert(unconfirmed == valueFromBitcoind))
.map(unconfirmed => assert(unconfirmed == 0.satoshis))
signedTx <- bitcoind.getNewAddress.flatMap {
wallet.sendToAddress(_, valueToBitcoind, Some(feeRate))