bitcoin-s/docs/wallet/chain-query-api.md
Nadav Kohen a503441619 Crypto Project Refactor (#1380)
* Added crypto project and decoupled BitcoinSLogger from NetworkElement

Decoupled BitcoinSLogger from Factory

Moved NetworkElement into crypto project

Moved Factory and BitcoinSUtil (renamed to BytesUtil) to crypto project

Moved MaskedToString to crypto project

Added BytesUtil to imports and cleaned up CryptoUtil.recoverPoint

Moved the rest of crypto stuff to the crypto project

Moved crypto tests to crypto-test project

* Added documentation for crypto project
2020-04-30 12:34:53 -05:00

7.4 KiB

id title
chain-query-api Chain Query API
import akka.actor.ActorSystem
import org.bitcoins.core.api.ChainQueryApi
import org.bitcoins.core.api.ChainQueryApi.FilterResponse
import org.bitcoins.crypto.{DoubleSha256Digest, DoubleSha256DigestBE}
import org.bitcoins.core.gcs.{FilterType, GolombFilter}
import org.bitcoins.core.protocol.BlockStamp
import org.bitcoins.core.protocol.blockchain.Block
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.keymanager.bip39.BIP39KeyManager
import org.bitcoins.node.NodeCallbacks
import org.bitcoins.node.networking.peer.DataMessageHandler._
import org.bitcoins.rpc.client.v19.BitcoindV19RpcClient
import org.bitcoins.rpc.config.BitcoindInstance
import org.bitcoins.testkit.BitcoinSTestAppConfig
import org.bitcoins.testkit.wallet.BitcoinSWalletTest
import org.bitcoins.wallet.Wallet
import org.bitcoins.wallet.config.WalletAppConfig

import java.time.Instant

import scala.concurrent.{ExecutionContextExecutor, Future}

ChainQueryAPI

The ChainQueryApi is how the wallet project stays aware of the current best chain. This allows the wallet for example to calculate the number of confirmations for a transaction, get the current chain tip, or even retrieve block filters for a given set of blocks.

Since this is an API it can be hooked up to the chain module of bitcoin-s but it can also be linked to any other implementation of your choosing. This allows you to use the bitcoin-s wallet in any schema that you want.

The functions that the ChainQueryApi supports are:

trait ChainQueryApi {

  /** Gets the height of the given block */
  def getBlockHeight(blockHash: DoubleSha256DigestBE): Future[Option[Int]]

  /** Gets the hash of the block that is what we consider "best" */
  def getBestBlockHash(): Future[DoubleSha256DigestBE]

  /** Gets number of confirmations for the given block hash*/
  def getNumberOfConfirmations(
      blockHashOpt: DoubleSha256DigestBE): Future[Option[Int]]

  /** Gets the number of compact filters in the database */
  def getFilterCount: Future[Int]

  /** Returns the block height of the given block stamp */
  def getHeightByBlockStamp(blockStamp: BlockStamp): Future[Int]

  def getFiltersBetweenHeights(
      startHeight: Int,
      endHeight: Int): Future[Vector[FilterResponse]]
}

Chain query with bitcoind

As an example, we will show you how to use the ChainQueryApi and bitcoind to query chain data.

implicit val system: ActorSystem = ActorSystem(s"node-api-example")
implicit val ec: ExecutionContextExecutor = system.dispatcher
implicit val walletConf: WalletAppConfig =
    BitcoinSTestAppConfig.getSpvTestConfig().walletConf

// let's use a helper method to get a v19 bitcoind
// and a ChainApi
val bitcoind = BitcoindV19RpcClient(BitcoindInstance.fromConfigFile())
val nodeApi = BitcoinSWalletTest.MockNodeApi

// Create our key manager
val keyManagerE = BIP39KeyManager.initialize(kmParams = walletConf.kmParams,
                                               bip39PasswordOpt = None)

val keyManager = keyManagerE match {
    case Right(keyManager) => keyManager
    case Left(err) =>
      throw new RuntimeException(s"Cannot initialize key manager err=$err")
  }

// This function can be used to create a callback for when our chain api receives a transaction, block, or
// a block filter, the returned NodeCallbacks will contain the necessary items to initialize the callbacks
def createCallbacks(
      processTransaction: Transaction => Future[Unit],
      processCompactFilters: (Vector[(DoubleSha256Digest, GolombFilter)]) => Future[Unit],
      processBlock: Block => Future[Unit]): NodeCallbacks = {
    lazy val onTx: OnTxReceived = { tx =>
      processTransaction(tx)
    }
    lazy val onCompactFilters: OnCompactFiltersReceived = {
      blockFilters =>
        processCompactFilters(blockFilters)
    }
    lazy val onBlock: OnBlockReceived = { block =>
      processBlock(block)
    }
    NodeCallbacks(onTxReceived = Seq(onTx),
                  onBlockReceived = Seq(onBlock),
                  onCompactFiltersReceived = Seq(onCompactFilters))
  }

// Here is a super simple example of a callback, this could be replaced with anything, from
// relaying the block on the network, finding relevant wallet transactions, verifying the block,
// or writing it to disk
val exampleProcessTx = (tx: Transaction) =>
    Future.successful(println(s"Received tx: ${tx.txIdBE}"))

val exampleProcessBlock = (block: Block) =>
    Future.successful(println(s"Received block: ${block.blockHeader.hashBE}"))

val exampleProcessFilters =
    (filters: Vector[(DoubleSha256Digest, GolombFilter)]) =>
      Future.successful(println(s"Received filter: ${filters.head._1.flip.hex} ${filters.head._2.hash.flip.hex}"))

val exampleCallbacks =
    createCallbacks(exampleProcessTx, exampleProcessFilters, exampleProcessBlock)

// Here is where we are defining our actual chain api, Ideally this could be it's own class
// but for the examples sake we will keep it small.
val chainApi = new ChainQueryApi {

    override def epochSecondToBlockHeight(time: Long): Future[Int] =
        Future.successful(0)

    /** Gets the height of the given block */
    override def getBlockHeight(
        blockHash: DoubleSha256DigestBE): Future[Option[Int]] = {
      bitcoind.getBlock(blockHash).map(block => Some(block.height))
    }

    /** Gets the hash of the block that is what we consider "best" */
    override def getBestBlockHash(): Future[DoubleSha256DigestBE] = {
      bitcoind.getBestBlockHash
    }

    /** Gets number of confirmations for the given block hash */
    override def getNumberOfConfirmations(
        blockHash: DoubleSha256DigestBE): Future[Option[Int]] = {
      for {
        tip <- bitcoind.getBlockCount
        block <- bitcoind.getBlock(blockHash)
      } yield {
        Some(tip - block.height + 1)
      }
    }

    /** Gets the number of compact filters in the database */
    override def getFilterCount: Future[Int] = {
      // since bitcoind should have the filter for
      // every block we can just return the block height
      bitcoind.getBlockCount
    }

    /** Returns the block height of the given block stamp */
    override def getHeightByBlockStamp(blockStamp: BlockStamp): Future[Int] = {
      blockStamp match {
        case blockHeight: BlockStamp.BlockHeight =>
          Future.successful(blockHeight.height)
        case blockHash: BlockStamp.BlockHash =>
          getBlockHeight(blockHash.hash).map(_.get)
        case blockTime: BlockStamp.BlockTime =>
          Future.failed(new RuntimeException(s"Not implemented: $blockTime"))
      }
    }

    override def getFiltersBetweenHeights(
        startHeight: Int,
        endHeight: Int): Future[Vector[FilterResponse]] = {
      val filterFs = startHeight
        .until(endHeight)
        .map { height =>
          for {
            hash <- bitcoind.getBlockHash(height)
            filter <- bitcoind.getBlockFilter(hash, FilterType.Basic)
          } yield {
            FilterResponse(filter.filter, hash, height)
          }
        }
        .toVector

      Future.sequence(filterFs)
    }
  }

// Finally, we can initialize our wallet with our own node api
val wallet =
    Wallet(keyManager = keyManager, nodeApi = nodeApi, chainQueryApi = chainApi, creationTime = Instant.now)

// Then to trigger one of the events we can run
wallet.chainQueryApi.getFiltersBetweenHeights(100, 150)