diff --git a/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala b/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala index 20e9ad3b4f..11c466e1f4 100644 --- a/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala +++ b/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala @@ -25,7 +25,7 @@ import org.scalatest.{Matchers, WordSpec} import ujson.Value.InvalidData import ujson.{Arr, Null, Num, Str} -import scala.concurrent.{ExecutionContext, Future} +import scala.concurrent.Future class RoutesSpec extends WordSpec @@ -52,9 +52,8 @@ class RoutesSpec "The server" should { "return the block count" in { - (mockChainApi - .getBlockCount(_: ExecutionContext)) - .expects(*) + (mockChainApi.getBlockCount: () => Future[Int]) + .expects() .returning(Future.successful(1234567890)) val route = @@ -67,9 +66,8 @@ class RoutesSpec } "return the best block hash" in { - (mockChainApi - .getBestBlockHash(_: ExecutionContext)) - .expects(*) + (mockChainApi.getBestBlockHash: () => Future[DoubleSha256DigestBE]) + .expects() .returning(Future.successful(DoubleSha256DigestBE.empty)) val route = diff --git a/app/server/src/main/scala/org/bitcoins/server/Main.scala b/app/server/src/main/scala/org/bitcoins/server/Main.scala index 9ed8148d96..5497ccdc3c 100644 --- a/app/server/src/main/scala/org/bitcoins/server/Main.scala +++ b/app/server/src/main/scala/org/bitcoins/server/Main.scala @@ -5,7 +5,8 @@ import java.nio.file.Files import akka.actor.ActorSystem import org.bitcoins.chain.config.ChainAppConfig -import org.bitcoins.core.api.{ChainQueryApi, NodeApi} +import org.bitcoins.core.api.ChainQueryApi +import org.bitcoins.core.crypto.DoubleSha256Digest import org.bitcoins.node.config.NodeAppConfig import org.bitcoins.node.models.Peer import org.bitcoins.node.networking.peer.DataMessageHandler @@ -115,7 +116,7 @@ object Main extends App { wallet: UnlockedWalletApi): Future[NodeCallbacks] = { import DataMessageHandler._ lazy val onTx: OnTxReceived = { tx => - wallet.processTransaction(tx, confirmations = 0) + wallet.processTransaction(tx, blockHash = None) () } lazy val onCompactFilter: OnCompactFilterReceived = { @@ -123,7 +124,7 @@ object Main extends App { wallet.processCompactFilter(blockHash, blockFilter) } lazy val onBlock: OnBlockReceived = { block => - wallet.processBlock(block, 0) + wallet.processBlock(block) () } if (nodeConf.isSPVEnabled) { diff --git a/chain-test/src/test/scala/org/bitcoins/chain/blockchain/ChainHandlerTest.scala b/chain-test/src/test/scala/org/bitcoins/chain/blockchain/ChainHandlerTest.scala index 1f0bd47a8b..49c4deb8e4 100644 --- a/chain-test/src/test/scala/org/bitcoins/chain/blockchain/ChainHandlerTest.scala +++ b/chain-test/src/test/scala/org/bitcoins/chain/blockchain/ChainHandlerTest.scala @@ -14,9 +14,10 @@ import org.bitcoins.core.crypto.{ ECPrivateKey } import org.bitcoins.core.gcs.{BlockFilter, FilterHeader, FilterType} +import org.bitcoins.core.number.{Int32, UInt32} import org.bitcoins.core.p2p.CompactFilterMessage -import org.bitcoins.core.protocol.BitcoinAddress import org.bitcoins.core.protocol.blockchain.BlockHeader +import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp} import org.bitcoins.core.util.CryptoUtil import org.bitcoins.testkit.BitcoinSTestAppConfig import org.bitcoins.testkit.chain.fixture.ChainFixtureTag @@ -51,7 +52,9 @@ class ChainHandlerTest extends ChainUnitTest { source.close() import org.bitcoins.rpc.serializers.JsonReaders.BlockHeaderReads - val headersResult = Json.parse(arrStr).validate[Vector[BlockHeader]].get + + val headersResult: Vector[BlockHeader] = + Json.parse(arrStr).validate[Vector[BlockHeader]].get override val defaultTag: ChainFixtureTag = ChainFixtureTag.GenisisChainHandler @@ -268,30 +271,14 @@ class ChainHandlerTest extends ChainUnitTest { it must "get the highest filter header" in { chainHandler: ChainHandler => { - val firstFilterHeader = FilterHeader( - filterHash = - DoubleSha256Digest.fromBytes(ECPrivateKey.freshPrivateKey.bytes), - prevHeaderHash = DoubleSha256Digest.empty) for { - empty <- chainHandler.getFilterHeadersAtHeight(0) - block <- chainHandler.getHeadersAtHeight(0) - _ <- chainHandler.processFilterHeader(firstFilterHeader, - block.head.hashBE) count <- chainHandler.getFilterHeaderCount - first <- chainHandler.getFilterHeader(block.head.hashBE) - vec <- chainHandler.getFilterHeadersAtHeight(count) + genesisFilterHeader <- chainHandler.getFilterHeadersAtHeight(count) } yield { - assert(empty.isEmpty) - assert(first.nonEmpty) - assert(vec.nonEmpty) - assert(Vector(first.get) == vec) - assert(first.get.hashBE == firstFilterHeader.hash.flip) - assert(first.get.filterHashBE == firstFilterHeader.filterHash.flip) + assert(genesisFilterHeader.size == 1) assert( - first.get.previousFilterHeaderBE == firstFilterHeader.prevHeaderHash.flip) - assert(first.get.blockHashBE == block.head.hashBE) - assert(first.get.height == 0) - assert(first.get.filterHeader == firstFilterHeader) + genesisFilterHeader.contains(ChainUnitTest.genesisFilterHeaderDb)) + assert(count == 0) } } } @@ -313,42 +300,36 @@ class ChainHandlerTest extends ChainUnitTest { it must "get the highest filter" in { chainHandler: ChainHandler => { for { - empty <- chainHandler.getFilterCount - blockHashBE <- chainHandler.getHeadersAtHeight(0).map(_.head.hashBE) - golombFilter = BlockFilter.fromHex("017fa880", blockHashBE.flip) - firstFilter = CompactFilterMessage(blockHash = blockHashBE.flip, - filter = golombFilter) - firstFilterHeader = FilterHeader(filterHash = golombFilter.hash, - prevHeaderHash = - DoubleSha256Digest.empty) - newChainHandler <- chainHandler.processFilterHeader(firstFilterHeader, - blockHashBE) - _ <- chainHandler.processFilter(firstFilter) - count <- newChainHandler.getFilterCount - first <- newChainHandler.getFiltersAtHeight(count).map(_.headOption) + count <- chainHandler.getFilterCount + genesisFilter <- chainHandler.getFiltersAtHeight(count) } yield { - assert(empty == 0) - assert(first.nonEmpty) - assert(first.get.hashBE == golombFilter.hash.flip) - assert(first.get.height == 0) - assert(first.get.blockHashBE == blockHashBE) - assert(first.get.filterType == FilterType.Basic) - assert(first.get.golombFilter == golombFilter) + assert(count == 0) + assert(genesisFilter.contains(ChainUnitTest.genesisFilterDb)) } } } it must "NOT create an unknown filter" in { chainHandler: ChainHandler => { + val blockHeader = + BlockHeader( + version = Int32(1), + previousBlockHash = ChainUnitTest.genesisHeaderDb.hashBE.flip, + merkleRootHash = DoubleSha256Digest.empty, + time = UInt32(1231006505), + nBits = UInt32(545259519), + nonce = UInt32(2083236893) + ) val unknownHashF = for { - blockHashBE <- chainHandler.getHeadersAtHeight(0).map(_.head.hashBE) + _ <- chainHandler.processHeader(blockHeader) + blockHashBE <- chainHandler.getHeadersAtHeight(1).map(_.head.hashBE) golombFilter = BlockFilter.fromHex("017fa880", blockHashBE.flip) firstFilter = CompactFilterMessage(blockHash = blockHashBE.flip, filter = golombFilter) firstFilterHeader = FilterHeader( filterHash = DoubleSha256Digest.fromBytes(ECPrivateKey.freshPrivateKey.bytes), - prevHeaderHash = DoubleSha256Digest.empty) + prevHeaderHash = ChainUnitTest.genesisFilterHeaderDb.hashBE.flip) newChainHandler <- chainHandler.processFilterHeader(firstFilterHeader, blockHashBE) process <- newChainHandler.processFilter(firstFilter) @@ -400,7 +381,7 @@ class ChainHandlerTest extends ChainUnitTest { it must "generate a range for a block filter query" in { chainHandler: ChainHandler => for { - bestBlock <- chainHandler.getBestBlockHeader + bestBlock <- chainHandler.getBestBlockHeader() bestBlockHashBE = bestBlock.hashBE rangeOpt <- chainHandler.nextHeaderBatchRange( DoubleSha256DigestBE.empty, @@ -412,6 +393,47 @@ class ChainHandlerTest extends ChainUnitTest { } } + it must "generate a range for a block filter header query" in { + chainHandler: ChainHandler => + for { + bestBlock <- chainHandler.getBestBlockHeader() + bestBlockHashBE = bestBlock.hashBE + rangeOpt <- chainHandler.nextFilterHeaderBatchRange( + DoubleSha256DigestBE.empty, + 1) + } yield { + assert(rangeOpt.nonEmpty) + assert(rangeOpt.get._1 == 0) + assert(rangeOpt.get._2 == bestBlockHashBE.flip) + } + } + + it must "return the number of confirmations" in { + chainHandler: ChainHandler => + for { + bestBlockHashBE <- chainHandler.getBestBlockHash() + confirmations <- chainHandler.getNumberOfConfirmations(bestBlockHashBE) + } yield { + assert(confirmations == Some(1)) + } + } + + it must "return the height by block stamp" in { chainHandler: ChainHandler => + for { + bestBlock <- chainHandler.getBestBlockHeader() + stamp1 = BlockStamp.BlockHash(bestBlock.hashBE) + stamp2 = BlockStamp.BlockHeight(bestBlock.height) + stamp3 = BlockStamp.BlockTime(bestBlock.time) + height1 <- chainHandler.getHeightByBlockStamp(stamp1) + height2 <- chainHandler.getHeightByBlockStamp(stamp2) + // TODO implement BlockTime +// height3 <- chainHandler.getHeightByBlockStamp(stamp3) + } yield { + assert(height1 == height2) +// assert(height1 == height3) + } + } + it must "match block filters" in { chainHandler: ChainHandler => import scodec.bits._ @@ -462,7 +484,7 @@ class ChainHandlerTest extends ChainUnitTest { BitcoinAddress("n1RH2x3b3ah4TGQtgrmNAHfmad9wr8U2QY").get.scriptPubKey), startOpt = None, endOpt = None - ) + )(system.dispatcher) } yield { assert(Vector(created.blockHashBE) == matched) } @@ -524,7 +546,7 @@ class ChainHandlerTest extends ChainUnitTest { private def buildChainHandlerCompetingHeaders( chainHandler: ChainHandler): Future[ReorgFixture] = { for { - oldBestTip <- chainHandler.getBestBlockHeader + oldBestTip <- chainHandler.getBestBlockHeader() (newHeaderB, newHeaderC) = buildCompetingHeaders(oldBestTip) newChainApi <- chainHandler.processHeaders(Vector(newHeaderB, newHeaderC)) newHeaderDbB <- newChainApi.getHeader(newHeaderB.hashBE) diff --git a/chain/src/main/scala/org/bitcoins/chain/api/ChainApi.scala b/chain/src/main/scala/org/bitcoins/chain/api/ChainApi.scala index 00d8b1e3ed..a66ae6b995 100644 --- a/chain/src/main/scala/org/bitcoins/chain/api/ChainApi.scala +++ b/chain/src/main/scala/org/bitcoins/chain/api/ChainApi.scala @@ -25,8 +25,7 @@ trait ChainApi extends ChainQueryApi { * @param header * @return */ - def processHeader(header: BlockHeader)( - implicit ec: ExecutionContext): Future[ChainApi] = { + def processHeader(header: BlockHeader): Future[ChainApi] = { processHeaders(Vector(header)) } @@ -36,37 +35,22 @@ trait ChainApi extends ChainQueryApi { * @param headers * @return */ - def processHeaders(headers: Vector[BlockHeader])( - implicit ec: ExecutionContext): Future[ChainApi] + def processHeaders(headers: Vector[BlockHeader]): Future[ChainApi] /** Gets a [[org.bitcoins.chain.models.BlockHeaderDb]] from the chain's database */ - def getHeader(hash: DoubleSha256DigestBE)( - implicit ec: ExecutionContext): Future[Option[BlockHeaderDb]] + def getHeader(hash: DoubleSha256DigestBE): Future[Option[BlockHeaderDb]] /** Gets all [[org.bitcoins.chain.models.BlockHeaderDb]]s at a given height */ - def getHeadersAtHeight(height: Int)( - implicit ec: ExecutionContext): Future[Vector[BlockHeaderDb]] + def getHeadersAtHeight(height: Int): Future[Vector[BlockHeaderDb]] /** Gets the number of blocks in the database */ - def getBlockCount(implicit ec: ExecutionContext): Future[Int] + def getBlockCount(): Future[Int] - /** Gets the hash of the block that is what we consider "best" */ - override def getBestBlockHash( - implicit ec: ExecutionContext): Future[DoubleSha256DigestBE] +// /** Gets the hash of the block that is what we consider "best" */ +// override def getBestBlockHash: Future[DoubleSha256DigestBE] /** Gets the best block header we have */ - def getBestBlockHeader( - implicit ec: ExecutionContext): Future[BlockHeaderDb] = { - for { - hash <- getBestBlockHash - headerOpt <- getHeader(hash) - } yield headerOpt match { - case None => - throw new RuntimeException( - s"We found best hash=${hash.hex} but could not retrieve the full header!!!") - case Some(header) => header - } - } + def getBestBlockHeader(): Future[BlockHeaderDb] /** * Adds a compact filter header into the filter header chain and returns a new [[ChainApi chain api]] @@ -74,8 +58,7 @@ trait ChainApi extends ChainQueryApi { */ def processFilterHeader( filterHeader: FilterHeader, - blockHash: DoubleSha256DigestBE)( - implicit ec: ExecutionContext): Future[ChainApi] = { + blockHash: DoubleSha256DigestBE): Future[ChainApi] = { processFilterHeaders(Vector(filterHeader), blockHash) } @@ -85,44 +68,40 @@ trait ChainApi extends ChainQueryApi { */ def processFilterHeaders( filterHeaders: Vector[FilterHeader], - stopHash: DoubleSha256DigestBE)( - implicit ec: ExecutionContext): Future[ChainApi] + stopHash: DoubleSha256DigestBE): Future[ChainApi] /** * Generates a block range in form of (startHeight, stopHash) by the given stop hash. */ - def nextHeaderBatchRange(stopHash: DoubleSha256DigestBE, batchSize: Int)( - implicit ec: ExecutionContext): Future[Option[(Int, DoubleSha256Digest)]] + def nextHeaderBatchRange( + stopHash: DoubleSha256DigestBE, + batchSize: Int): Future[Option[(Int, DoubleSha256Digest)]] /** * Generates a filter header range in form of (startHeight, stopHash) by the given stop hash. */ def nextFilterHeaderBatchRange( stopHash: DoubleSha256DigestBE, - batchSize: Int)( - implicit ec: ExecutionContext): Future[Option[(Int, DoubleSha256Digest)]] + batchSize: Int): Future[Option[(Int, DoubleSha256Digest)]] /** * Adds a compact filter into the filter database. */ - def processFilter(message: CompactFilterMessage)( - implicit ec: ExecutionContext): Future[ChainApi] = + def processFilter(message: CompactFilterMessage): Future[ChainApi] = processFilters(Vector(message)) /** * Process all of the given compact filters and returns a new [[ChainApi chain api]] * that contains these headers. */ - def processFilters(message: Vector[CompactFilterMessage])( - implicit ec: ExecutionContext): Future[ChainApi] + def processFilters(message: Vector[CompactFilterMessage]): Future[ChainApi] /** * Adds a compact filter header check point into the list of check points. */ def processCheckpoint( filterHeaderHash: DoubleSha256DigestBE, - blockHash: DoubleSha256DigestBE)( - implicit ec: ExecutionContext): Future[ChainApi] = { + blockHash: DoubleSha256DigestBE): Future[ChainApi] = { processCheckpoints(Vector(filterHeaderHash), blockHash) } @@ -131,38 +110,35 @@ trait ChainApi extends ChainQueryApi { */ def processCheckpoints( checkpoints: Vector[DoubleSha256DigestBE], - blockHash: DoubleSha256DigestBE)( - implicit ec: ExecutionContext): Future[ChainApi] + blockHash: DoubleSha256DigestBE): Future[ChainApi] /** Gets the number of compact filter headers in the database */ - def getFilterHeaderCount(implicit ec: ExecutionContext): Future[Int] + def getFilterHeaderCount: Future[Int] /** * Looks up a compact filter header by its height. */ - def getFilterHeadersAtHeight(height: Int)( - implicit ec: ExecutionContext): Future[Vector[CompactFilterHeaderDb]] + def getFilterHeadersAtHeight( + height: Int): Future[Vector[CompactFilterHeaderDb]] /** * Looks up a compact filter header by its hash. */ - def getFilterHeader(blockHash: DoubleSha256DigestBE)( - implicit ec: ExecutionContext): Future[Option[CompactFilterHeaderDb]] + def getFilterHeader( + blockHash: DoubleSha256DigestBE): Future[Option[CompactFilterHeaderDb]] /** * Looks up a compact filter by its hash. */ - def getFilter(hash: DoubleSha256DigestBE)( - implicit ec: ExecutionContext): Future[Option[CompactFilterDb]] + def getFilter(hash: DoubleSha256DigestBE): Future[Option[CompactFilterDb]] /** Gets the number of compact filters in the database */ - def getFilterCount(implicit ec: ExecutionContext): Future[Int] + def getFilterCount: Future[Int] /** * Looks up a compact filter by its height. */ - def getFiltersAtHeight(height: Int)( - implicit ec: ExecutionContext): Future[Vector[CompactFilterDb]] + def getFiltersAtHeight(height: Int): Future[Vector[CompactFilterDb]] /** * Iterates over the block filters in order to find filters that match to the given addresses @@ -182,15 +158,8 @@ trait ChainApi extends ChainQueryApi { endOpt: Option[BlockStamp], batchSize: Int, parallelismLevel: Int)( - implicit ec: ExecutionContext): Future[Vector[DoubleSha256DigestBE]] + ec: ExecutionContext): Future[Vector[DoubleSha256DigestBE]] /** Returns the block height of the given block stamp */ - def getHeightByBlockStamp(blockStamp: BlockStamp)( - implicit ec: ExecutionContext): Future[Int] - - /** @inheritdoc */ - override def getBlockHeight(blockHash: DoubleSha256DigestBE)( - implicit ec: ExecutionContext): Future[Option[Int]] = - getHeader(blockHash).map(_.map(_.height)) - + def getHeightByBlockStamp(blockStamp: BlockStamp): Future[Int] } diff --git a/chain/src/main/scala/org/bitcoins/chain/blockchain/ChainHandler.scala b/chain/src/main/scala/org/bitcoins/chain/blockchain/ChainHandler.scala index b07f46870d..ba546a8cc4 100644 --- a/chain/src/main/scala/org/bitcoins/chain/blockchain/ChainHandler.scala +++ b/chain/src/main/scala/org/bitcoins/chain/blockchain/ChainHandler.scala @@ -33,12 +33,13 @@ case class ChainHandler( filterDAO: CompactFilterDAO, blockchains: Vector[Blockchain], blockFilterCheckpoints: Map[DoubleSha256DigestBE, DoubleSha256DigestBE])( - implicit val chainConfig: ChainAppConfig) + implicit val chainConfig: ChainAppConfig, + executionContext: ExecutionContext) extends ChainApi with ChainVerificationLogger { /** @inheritdoc */ - override def getBlockCount(implicit ec: ExecutionContext): Future[Int] = { + override def getBlockCount(): Future[Int] = { logger.debug(s"Querying for block count") blockHeaderDAO.maxHeight.map { height => logger.debug(s"getBlockCount result: count=$height") @@ -46,9 +47,21 @@ case class ChainHandler( } } + override def getBestBlockHeader(): Future[BlockHeaderDb] = { + for { + hash <- getBestBlockHash() + headerOpt <- getHeader(hash) + } yield headerOpt match { + case None => + throw new RuntimeException( + s"We found best hash=${hash.hex} but could not retrieve the full header!!!") + case Some(header) => header + } + } + /** @inheritdoc */ - override def getHeader(hash: DoubleSha256DigestBE)( - implicit ec: ExecutionContext): Future[Option[BlockHeaderDb]] = { + override def getHeader( + hash: DoubleSha256DigestBE): Future[Option[BlockHeaderDb]] = { blockHeaderDAO.findByHash(hash).map { header => logger.debug(s"Looking for header by hash=$hash") val resultStr = header @@ -60,8 +73,8 @@ case class ChainHandler( } /** @inheritdoc */ - override def processHeaders(headers: Vector[BlockHeader])( - implicit ec: ExecutionContext): Future[ChainApi] = { + override def processHeaders( + headers: Vector[BlockHeader]): Future[ChainApi] = { if (headers.isEmpty) { Future.successful(this) } else { @@ -92,8 +105,7 @@ case class ChainHandler( /** * @inheritdoc */ - override def getBestBlockHash( - implicit ec: ExecutionContext): Future[DoubleSha256DigestBE] = { + override def getBestBlockHash(): Future[DoubleSha256DigestBE] = { logger.debug(s"Querying for best block hash") //naive implementation, this is looking for the tip with the _most_ proof of work //this does _not_ mean that it is on the chain that has the most work @@ -121,8 +133,7 @@ case class ChainHandler( /** @inheritdoc */ override def nextHeaderBatchRange( prevStopHash: DoubleSha256DigestBE, - batchSize: Int)(implicit ec: ExecutionContext): Future[ - Option[(Int, DoubleSha256Digest)]] = { + batchSize: Int): Future[Option[(Int, DoubleSha256Digest)]] = { val startHeightF = if (prevStopHash == DoubleSha256DigestBE.empty) { Future.successful(0) } else { @@ -151,8 +162,7 @@ case class ChainHandler( /** @inheritdoc */ override def nextFilterHeaderBatchRange( prevStopHash: DoubleSha256DigestBE, - batchSize: Int)(implicit ec: ExecutionContext): Future[ - Option[(Int, DoubleSha256Digest)]] = { + batchSize: Int): Future[Option[(Int, DoubleSha256Digest)]] = { val startHeightF = if (prevStopHash == DoubleSha256DigestBE.empty) { Future.successful(0) } else { @@ -182,8 +192,7 @@ case class ChainHandler( /** @inheritdoc */ override def processFilterHeaders( filterHeaders: Vector[FilterHeader], - stopHash: DoubleSha256DigestBE)( - implicit ec: ExecutionContext): Future[ChainApi] = { + stopHash: DoubleSha256DigestBE): Future[ChainApi] = { val filterHeadersToCreateF = for { blockHeaders <- blockHeaderDAO @@ -223,8 +232,8 @@ case class ChainHandler( } /** @inheritdoc */ - override def processFilters(messages: Vector[CompactFilterMessage])( - implicit ec: ExecutionContext): Future[ChainApi] = { + override def processFilters( + messages: Vector[CompactFilterMessage]): Future[ChainApi] = { logger.debug(s"processFilters: messages=${messages}") val filterHeadersF = filterHeaderDAO @@ -291,8 +300,7 @@ case class ChainHandler( /** @inheritdoc */ override def processCheckpoints( checkpoints: Vector[DoubleSha256DigestBE], - blockHash: DoubleSha256DigestBE)( - implicit ec: ExecutionContext): Future[ChainApi] = { + blockHash: DoubleSha256DigestBE): Future[ChainApi] = { val blockHeadersF: Future[Seq[BlockHeaderDb]] = Future .traverse(checkpoints.indices.toVector) { i => @@ -317,19 +325,17 @@ case class ChainHandler( } /** @inheritdoc */ - override def getFilter(blockHash: DoubleSha256DigestBE)( - implicit ec: ExecutionContext): Future[Option[CompactFilterDb]] = { + override def getFilter( + blockHash: DoubleSha256DigestBE): Future[Option[CompactFilterDb]] = { filterDAO.findByBlockHash(blockHash) } /** @inheritdoc */ - override def getHeadersAtHeight(height: Int)( - implicit ec: ExecutionContext): Future[Vector[BlockHeaderDb]] = + override def getHeadersAtHeight(height: Int): Future[Vector[BlockHeaderDb]] = blockHeaderDAO.getAtHeight(height) /** @inheritdoc */ - override def getFilterHeaderCount( - implicit ec: ExecutionContext): Future[Int] = { + override def getFilterHeaderCount: Future[Int] = { logger.debug(s"Querying for filter header count") filterHeaderDAO.maxHeight.map { height => logger.debug(s"getFilterHeaderCount result: count=$height") @@ -338,17 +344,17 @@ case class ChainHandler( } /** @inheritdoc */ - override def getFilterHeadersAtHeight(height: Int)( - implicit ec: ExecutionContext): Future[Vector[CompactFilterHeaderDb]] = + override def getFilterHeadersAtHeight( + height: Int): Future[Vector[CompactFilterHeaderDb]] = filterHeaderDAO.getAtHeight(height) /** @inheritdoc */ - override def getFilterHeader(blockHash: DoubleSha256DigestBE)( - implicit ec: ExecutionContext): Future[Option[CompactFilterHeaderDb]] = + override def getFilterHeader( + blockHash: DoubleSha256DigestBE): Future[Option[CompactFilterHeaderDb]] = filterHeaderDAO.findByBlockHash(blockHash) /** @inheritdoc */ - override def getFilterCount(implicit ec: ExecutionContext): Future[Int] = { + override def getFilterCount: Future[Int] = { logger.debug(s"Querying for filter count") filterDAO.maxHeight.map { height => logger.debug(s"getFilterCount result: count=$height") @@ -357,8 +363,8 @@ case class ChainHandler( } /** @inheritdoc */ - override def getFiltersAtHeight(height: Int)( - implicit ec: ExecutionContext): Future[Vector[CompactFilterDb]] = + override def getFiltersAtHeight( + height: Int): Future[Vector[CompactFilterDb]] = filterDAO.getAtHeight(height) /** Implements [[ChainApi.getMatchingBlocks()]]. @@ -384,7 +390,7 @@ case class ChainHandler( endOpt: Option[BlockStamp] = None, batchSize: Int = chainConfig.filterBatchSize, parallelismLevel: Int = Runtime.getRuntime.availableProcessors())( - implicit ec: ExecutionContext): Future[Vector[DoubleSha256DigestBE]] = { + ec: ExecutionContext): Future[Vector[DoubleSha256DigestBE]] = { require(batchSize > 0, "batch size must be greater than zero") require(parallelismLevel > 0, "parallelism level must be greater than zero") @@ -485,8 +491,7 @@ case class ChainHandler( } /** @inheritdoc */ - override def getHeightByBlockStamp(blockStamp: BlockStamp)( - implicit ec: ExecutionContext): Future[Int] = + override def getHeightByBlockStamp(blockStamp: BlockStamp): Future[Int] = blockStamp match { case blockHeight: BlockStamp.BlockHeight => Future.successful(blockHeight.height) @@ -501,6 +506,25 @@ case class ChainHandler( Future.failed(new RuntimeException(s"Not implemented: $blockTime")) } + /** @inheritdoc */ + override def getBlockHeight( + blockHash: DoubleSha256DigestBE): Future[Option[Int]] = + getHeader(blockHash).map(_.map(_.height)) + + /** @inheritdoc */ + override def getNumberOfConfirmations( + blockHash: DoubleSha256DigestBE): Future[Option[Int]] = { + getBlockHeight(blockHash).flatMap { + case None => FutureUtil.none + case Some(blockHeight) => + for { + tipHash <- getBestBlockHash() + tipHeightOpt <- getBlockHeight(tipHash) + } yield { + tipHeightOpt.map(tipHeight => tipHeight - blockHeight + 1) + } + } + } } object ChainHandler { @@ -530,7 +554,8 @@ object ChainHandler { filterHeaderDAO: CompactFilterHeaderDAO, filterDAO: CompactFilterDAO, blockchains: Blockchain)( - implicit chainConfig: ChainAppConfig): ChainHandler = { + implicit ec: ExecutionContext, + chainConfig: ChainAppConfig): ChainHandler = { new ChainHandler(blockHeaderDAO = blockHeaderDAO, filterHeaderDAO = filterHeaderDAO, filterDAO = filterDAO, diff --git a/core/src/main/scala/org/bitcoins/core/api/ChainQueryApi.scala b/core/src/main/scala/org/bitcoins/core/api/ChainQueryApi.scala index 4ee47f3e50..a9febb7050 100644 --- a/core/src/main/scala/org/bitcoins/core/api/ChainQueryApi.scala +++ b/core/src/main/scala/org/bitcoins/core/api/ChainQueryApi.scala @@ -1,8 +1,9 @@ package org.bitcoins.core.api import org.bitcoins.core.crypto.DoubleSha256DigestBE +import org.bitcoins.core.util.FutureUtil -import scala.concurrent.{ExecutionContext, Future} +import scala.concurrent.Future /** * This trait provides methods to query various types of blockchain data. @@ -10,12 +11,14 @@ import scala.concurrent.{ExecutionContext, Future} trait ChainQueryApi { /** Gets the height of the given block */ - def getBlockHeight(blockHash: DoubleSha256DigestBE)( - implicit ec: ExecutionContext): Future[Option[Int]] + def getBlockHeight(blockHash: DoubleSha256DigestBE): Future[Option[Int]] /** Gets the hash of the block that is what we consider "best" */ - def getBestBlockHash( - implicit ec: ExecutionContext): Future[DoubleSha256DigestBE] + def getBestBlockHash(): Future[DoubleSha256DigestBE] + + /** Gets number of confirmations for the given block hash*/ + def getNumberOfConfirmations( + blockHashOpt: DoubleSha256DigestBE): Future[Option[Int]] } @@ -24,13 +27,17 @@ object ChainQueryApi { object NoOp extends ChainQueryApi { /** Gets the height of the given block */ - override def getBlockHeight(blockHash: DoubleSha256DigestBE)( - implicit ec: ExecutionContext): Future[Option[Int]] = - Future.successful(None) + override def getBlockHeight( + blockHash: DoubleSha256DigestBE): Future[Option[Int]] = + FutureUtil.none /** Gets the hash of the block that is what we consider "best" */ - override def getBestBlockHash( - implicit ec: ExecutionContext): Future[DoubleSha256DigestBE] = + override def getBestBlockHash(): Future[DoubleSha256DigestBE] = Future.successful(DoubleSha256DigestBE.empty) + + /** Gets number of confirmations for the given block hash. It returns None of no block found */ + override def getNumberOfConfirmations( + blockHashOpt: DoubleSha256DigestBE): Future[Option[Int]] = + FutureUtil.none } } diff --git a/core/src/main/scala/org/bitcoins/core/util/FutureUtil.scala b/core/src/main/scala/org/bitcoins/core/util/FutureUtil.scala index a4dcf11047..4de5e759a6 100644 --- a/core/src/main/scala/org/bitcoins/core/util/FutureUtil.scala +++ b/core/src/main/scala/org/bitcoins/core/util/FutureUtil.scala @@ -1,7 +1,6 @@ package org.bitcoins.core.util -import scala.concurrent.Future -import scala.concurrent.ExecutionContext +import scala.concurrent.{ExecutionContext, Future} object FutureUtil { @@ -24,6 +23,8 @@ object FutureUtil { val unit: Future[Unit] = Future.successful(()) + def none[T]: Future[Option[T]] = Future.successful(Option.empty[T]) + /** * Folds over the given elements sequentially in a non-blocking async way * @param init the initialized value for the accumulator diff --git a/docs/applications/wallet.md b/docs/applications/wallet.md index 297392a425..76d74cb9cf 100644 --- a/docs/applications/wallet.md +++ b/docs/applications/wallet.md @@ -110,22 +110,23 @@ val walletF: Future[LockedWalletApi] = configF.flatMap { _ => // when this future completes, ww have sent a transaction // from bitcoind to the Bitcoin-S wallet -import org.bitcoins.core.protocol.transaction.Transaction +import org.bitcoins.core.crypto._ +import org.bitcoins.core.protocol.transaction._ import org.bitcoins.core.currency._ -val transactionF: Future[Transaction] = for { +val transactionF: Future[(Transaction, Option[DoubleSha256DigestBE])] = for { wallet <- walletF address <- wallet.getNewAddress() txid <- bitcoind.sendToAddress(address, 3.bitcoin) transaction <- bitcoind.getRawTransaction(txid) -} yield transaction.hex +} yield (transaction.hex, transaction.blockhash) // when this future completes, we have processed // the transaction from bitcoind, and we have // queried our balance for the current balance val balanceF: Future[CurrencyUnit] = for { wallet <- walletF - tx <- transactionF - _ <- wallet.processTransaction(tx, confirmations = 0) + (tx, blockhash) <- transactionF + _ <- wallet.processTransaction(tx, blockhash) balance <- wallet.getBalance } yield balance diff --git a/node-test/src/test/scala/org/bitcoins/node/NeutrinoNodeWithWalletTest.scala b/node-test/src/test/scala/org/bitcoins/node/NeutrinoNodeWithWalletTest.scala index c74c918e06..3c3fccb5b8 100644 --- a/node-test/src/test/scala/org/bitcoins/node/NeutrinoNodeWithWalletTest.scala +++ b/node-test/src/test/scala/org/bitcoins/node/NeutrinoNodeWithWalletTest.scala @@ -53,7 +53,7 @@ class NeutrinoNodeWithWalletTest extends NodeUnitTest { val onBlock: DataMessageHandler.OnBlockReceived = { block => for { wallet <- walletF - _ <- wallet.processBlock(block, confirmations = 6) + _ <- wallet.processBlock(block) } yield () } val onCompactFilter: OnCompactFilterReceived = { (blockHash, blockFilter) => @@ -98,14 +98,16 @@ class NeutrinoNodeWithWalletTest extends NodeUnitTest { expectedConfirmedAmount = 0.sats, expectedUnconfirmedAmount = BitcoinSWalletTest.initialFunds - TestAmount - TestFees, expectedUtxos = 1, - expectedAddresses = 2) + expectedAddresses = 2 + ) } val condition2 = { () => condition( expectedConfirmedAmount = TestAmount, expectedUnconfirmedAmount = BitcoinSWalletTest.initialFunds - TestAmount - TestFees, expectedUtxos = 2, - expectedAddresses = 3) + expectedAddresses = 3 + ) } for { diff --git a/node-test/src/test/scala/org/bitcoins/node/SpvNodeWithWalletTest.scala b/node-test/src/test/scala/org/bitcoins/node/SpvNodeWithWalletTest.scala index ef3927c9dc..44e4eaf88c 100644 --- a/node-test/src/test/scala/org/bitcoins/node/SpvNodeWithWalletTest.scala +++ b/node-test/src/test/scala/org/bitcoins/node/SpvNodeWithWalletTest.scala @@ -46,7 +46,7 @@ class SpvNodeWithWalletTest extends NodeUnitTest { if (expectedTxId == tx.txId) { for { prevBalance <- wallet.getUnconfirmedBalance() - _ <- wallet.processTransaction(tx, confirmations = 0) + _ <- wallet.processTransaction(tx, None) balance <- wallet.getUnconfirmedBalance() } yield { val result = balance == prevBalance + amountFromBitcoind diff --git a/node/src/main/scala/org/bitcoins/node/NeutrinoNode.scala b/node/src/main/scala/org/bitcoins/node/NeutrinoNode.scala index 123656624e..37f04287d6 100644 --- a/node/src/main/scala/org/bitcoins/node/NeutrinoNode.scala +++ b/node/src/main/scala/org/bitcoins/node/NeutrinoNode.scala @@ -33,7 +33,7 @@ case class NeutrinoNode( val res = for { node <- super.start() chainApi <- chainApiFromDb() - bestHash <- chainApi.getBestBlockHash + bestHash <- chainApi.getBestBlockHash() peerMsgSender <- peerMsgSenderF _ <- peerMsgSender.sendGetCompactFilterCheckPointMessage( stopHash = bestHash.flip) diff --git a/node/src/main/scala/org/bitcoins/node/Node.scala b/node/src/main/scala/org/bitcoins/node/Node.scala index 891efcf0ad..d3fd4dc878 100644 --- a/node/src/main/scala/org/bitcoins/node/Node.scala +++ b/node/src/main/scala/org/bitcoins/node/Node.scala @@ -8,8 +8,8 @@ import org.bitcoins.chain.models.{ CompactFilterDAO, CompactFilterHeaderDAO } -import org.bitcoins.core.api.NodeApi -import org.bitcoins.core.crypto.DoubleSha256Digest +import org.bitcoins.core.api.{ChainQueryApi, NodeApi} +import org.bitcoins.core.crypto.{DoubleSha256Digest, DoubleSha256DigestBE} import org.bitcoins.core.p2p.{NetworkPayload, TypeIdentifier} import org.bitcoins.core.protocol.BlockStamp import org.bitcoins.core.protocol.script.ScriptPubKey @@ -36,7 +36,7 @@ import scala.util.{Failure, Success} /** This a base trait for various kinds of nodes. It contains house keeping methods required for all nodes. */ -trait Node extends NodeApi with P2PLogger { +trait Node extends NodeApi with ChainQueryApi with P2PLogger { implicit def system: ActorSystem @@ -180,7 +180,7 @@ trait Node extends NodeApi with P2PLogger { def sync(): Future[Unit] = { for { chainApi <- chainApiFromDb() - hash <- chainApi.getBestBlockHash + hash <- chainApi.getBestBlockHash() header <- chainApi .getHeader(hash) .map(_.get) // .get is safe since this is an internal call @@ -223,4 +223,18 @@ trait Node extends NodeApi with P2PLogger { } yield () } + /** Gets the height of the given block */ + override def getBlockHeight( + blockHash: DoubleSha256DigestBE): Future[Option[Int]] = + chainApiFromDb().flatMap(_.getBlockHeight(blockHash)) + + /** Gets the hash of the block that is what we consider "best" */ + override def getBestBlockHash(): Future[DoubleSha256DigestBE] = + chainApiFromDb().flatMap(_.getBestBlockHash()) + + /** Gets number of confirmations for the given block hash*/ + def getNumberOfConfirmations( + blockHashOpt: DoubleSha256DigestBE): Future[Option[Int]] = + chainApiFromDb().flatMap(_.getNumberOfConfirmations(blockHashOpt)) + } diff --git a/testkit/src/main/scala/org/bitcoins/testkit/chain/ChainTestUtil.scala b/testkit/src/main/scala/org/bitcoins/testkit/chain/ChainTestUtil.scala index 72274ef90c..0db1eb4217 100644 --- a/testkit/src/main/scala/org/bitcoins/testkit/chain/ChainTestUtil.scala +++ b/testkit/src/main/scala/org/bitcoins/testkit/chain/ChainTestUtil.scala @@ -1,8 +1,9 @@ package org.bitcoins.testkit.chain -import org.bitcoins.chain.models.{BlockHeaderDb, BlockHeaderDbHelper} +import org.bitcoins.chain.models._ import org.bitcoins.core.crypto -import org.bitcoins.core.crypto.DoubleSha256DigestBE +import org.bitcoins.core.crypto.{DoubleSha256Digest, DoubleSha256DigestBE} +import org.bitcoins.core.gcs.{BlockFilter, FilterHeader, GolombFilter} import org.bitcoins.core.protocol.blockchain.{ BlockHeader, MainNetChainParams, @@ -20,6 +21,20 @@ sealed abstract class ChainTestUtil { lazy val regTestGenesisHeaderDb: BlockHeaderDb = { BlockHeaderDbHelper.fromBlockHeader(height = 0, bh = regTestHeader) } + lazy val regTestGenesisHeaderCompactFilter: GolombFilter = + BlockFilter.apply(regTestChainParams.genesisBlock, Vector.empty) + lazy val regTestGenesisHeaderCompactFilterDb: CompactFilterDb = + CompactFilterDbHelper.fromGolombFilter(regTestGenesisHeaderCompactFilter, + regTestHeader.hashBE, + 0) + lazy val regTestGenesisHeaderCompactFilterHeader: FilterHeader = FilterHeader( + regTestGenesisHeaderCompactFilter.hash, + DoubleSha256Digest.empty) + lazy val regTestGenesisHeaderCompactFilterHeaderDb: CompactFilterHeaderDb = + CompactFilterHeaderDbHelper.fromFilterHeader( + regTestGenesisHeaderCompactFilterHeader, + regTestHeader.hashBE, + 0) lazy val mainnetChainParam: MainNetChainParams.type = MainNetChainParams diff --git a/testkit/src/main/scala/org/bitcoins/testkit/chain/ChainUnitTest.scala b/testkit/src/main/scala/org/bitcoins/testkit/chain/ChainUnitTest.scala index 8aa0ecc724..0e31d71ee2 100644 --- a/testkit/src/main/scala/org/bitcoins/testkit/chain/ChainUnitTest.scala +++ b/testkit/src/main/scala/org/bitcoins/testkit/chain/ChainUnitTest.scala @@ -12,10 +12,10 @@ import org.bitcoins.chain.models._ import org.bitcoins.core.protocol.blockchain.{Block, BlockHeader} import org.bitcoins.db.AppConfig import org.bitcoins.rpc.client.common.BitcoindRpcClient -import org.bitcoins.testkit.{chain, BitcoinSTestAppConfig} import org.bitcoins.testkit.chain.fixture._ import org.bitcoins.testkit.fixtures.BitcoinSFixture import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil +import org.bitcoins.testkit.{chain, BitcoinSTestAppConfig} import org.bitcoins.zmq.ZMQSubscriber import org.scalatest._ import play.api.libs.json.{JsError, JsSuccess, Json} @@ -289,6 +289,12 @@ object ChainUnitTest extends ChainVerificationLogger { val genesisHeaderDb: BlockHeaderDb = ChainTestUtil.regTestGenesisHeaderDb + val genesisFilterDb: CompactFilterDb = + ChainTestUtil.regTestGenesisHeaderCompactFilterDb + + val genesisFilterHeaderDb: CompactFilterHeaderDb = + ChainTestUtil.regTestGenesisHeaderCompactFilterHeaderDb + def createChainHandler()( implicit ec: ExecutionContext, appConfig: ChainAppConfig): Future[ChainHandler] = { @@ -449,6 +455,9 @@ object ChainUnitTest extends ChainVerificationLogger { for { chainHandler <- chainHandlerF genHeader <- chainHandler.blockHeaderDAO.create(genesisHeaderDb) + genFilterHeader <- chainHandler.filterHeaderDAO.create( + genesisFilterHeaderDb) + genFilter <- chainHandler.filterDAO.create(genesisFilterDb) } yield genHeader } diff --git a/testkit/src/main/scala/org/bitcoins/testkit/node/NodeUnitTest.scala b/testkit/src/main/scala/org/bitcoins/testkit/node/NodeUnitTest.scala index 2c537b176a..b2b935f4a3 100644 --- a/testkit/src/main/scala/org/bitcoins/testkit/node/NodeUnitTest.scala +++ b/testkit/src/main/scala/org/bitcoins/testkit/node/NodeUnitTest.scala @@ -5,7 +5,9 @@ import java.net.InetSocketAddress import akka.actor.ActorSystem import org.bitcoins.chain.api.ChainApi import org.bitcoins.chain.config.ChainAppConfig +import org.bitcoins.core.api.ChainQueryApi import org.bitcoins.core.config.NetworkParameters +import org.bitcoins.core.crypto.DoubleSha256DigestBE import org.bitcoins.db.AppConfig import org.bitcoins.node._ import org.bitcoins.node.config.NodeAppConfig @@ -252,10 +254,9 @@ object NodeUnitTest extends P2PLogger { for { bitcoind <- BitcoinSFixture.createBitcoindWithFunds(versionOpt) node <- createSpvNode(bitcoind, callbacks) - chain <- node.chainApiFromDb() fundedWallet <- BitcoinSWalletTest.fundedWalletAndBitcoind(bitcoind, node, - chain) + node) } yield { SpvNodeFundedWalletBitcoind(node = node, wallet = fundedWallet.wallet, @@ -274,10 +275,9 @@ object NodeUnitTest extends P2PLogger { for { bitcoind <- BitcoinSFixture.createBitcoindWithFunds(versionOpt) node <- createNeutrinoNode(bitcoind, callbacks) - chain <- node.chainApiFromDb() fundedWallet <- BitcoinSWalletTest.fundedWalletAndBitcoind(bitcoind, node, - chain) + node) } yield { NeutrinoNodeFundedWalletBitcoind(node = node, wallet = fundedWallet.wallet, diff --git a/testkit/src/main/scala/org/bitcoins/testkit/wallet/BitcoinSWalletTest.scala b/testkit/src/main/scala/org/bitcoins/testkit/wallet/BitcoinSWalletTest.scala index b755901bf4..b4c6d63e3f 100644 --- a/testkit/src/main/scala/org/bitcoins/testkit/wallet/BitcoinSWalletTest.scala +++ b/testkit/src/main/scala/org/bitcoins/testkit/wallet/BitcoinSWalletTest.scala @@ -3,7 +3,7 @@ package org.bitcoins.testkit.wallet import akka.actor.ActorSystem import com.typesafe.config.{Config, ConfigFactory} import org.bitcoins.core.api.{ChainQueryApi, NodeApi} -import org.bitcoins.core.crypto.DoubleSha256Digest +import org.bitcoins.core.crypto.DoubleSha256DigestBE import org.bitcoins.core.currency._ import org.bitcoins.core.util.FutureUtil import org.bitcoins.db.AppConfig @@ -227,12 +227,10 @@ object BitcoinSWalletTest extends WalletLogger { val WalletWithBitcoind(wallet, bitcoind) = pair for { addr <- wallet.getNewAddress() - tx <- bitcoind - .sendToAddress(addr, initialFunds) - .flatMap(bitcoind.getRawTransaction(_)) - + txId <- bitcoind.sendToAddress(addr, initialFunds) _ <- bitcoind.getNewAddress.flatMap(bitcoind.generateToAddress(6, _)) - _ <- wallet.processTransaction(tx.hex, 6) + tx <- bitcoind.getRawTransaction(txId) + _ <- wallet.processTransaction(tx.hex, tx.blockhash) balance <- wallet.getBalance() } yield { diff --git a/testkit/src/main/scala/org/bitcoins/testkit/wallet/WalletTestUtil.scala b/testkit/src/main/scala/org/bitcoins/testkit/wallet/WalletTestUtil.scala index f1fbb207eb..11610d2b9e 100644 --- a/testkit/src/main/scala/org/bitcoins/testkit/wallet/WalletTestUtil.scala +++ b/testkit/src/main/scala/org/bitcoins/testkit/wallet/WalletTestUtil.scala @@ -1,33 +1,31 @@ package org.bitcoins.testkit.wallet -import org.bitcoins.testkit.Implicits._ import org.bitcoins.core.config.RegTest import org.bitcoins.core.crypto._ import org.bitcoins.core.currency._ +import org.bitcoins.core.hd._ +import org.bitcoins.core.protocol.Bech32Address import org.bitcoins.core.protocol.blockchain.{ ChainParams, RegTestNetChainParams } -import org.bitcoins.testkit.core.gen.CryptoGenerators -import org.bitcoins.wallet.models.AccountDb -import org.bitcoins.core.hd._ -import org.bitcoins.core.protocol.script.ScriptWitness -import org.bitcoins.core.protocol.script.P2WPKHWitnessV0 -import org.bitcoins.wallet.models.LegacySpendingInfo -import org.bitcoins.core.protocol.transaction.TransactionOutPoint -import org.bitcoins.core.protocol.transaction.TransactionOutput -import org.bitcoins.wallet.models.SegwitV0SpendingInfo -import org.bitcoins.testkit.core.gen.NumberGenerator -import org.scalacheck.Gen -import org.bitcoins.core.protocol.script.ScriptPubKey -import org.bitcoins.testkit.fixtures.WalletDAOs -import scala.concurrent.Future -import scala.concurrent.ExecutionContext -import org.bitcoins.wallet.models.SegWitAddressDb -import org.bitcoins.core.protocol.Bech32Address -import org.bitcoins.core.protocol.script.P2WPKHWitnessSPKV0 +import org.bitcoins.core.protocol.script.{ + P2WPKHWitnessSPKV0, + P2WPKHWitnessV0, + ScriptPubKey, + ScriptWitness +} +import org.bitcoins.core.protocol.transaction.{ + TransactionOutPoint, + TransactionOutput +} import org.bitcoins.core.util.CryptoUtil -import org.bitcoins.wallet.models.AddressDb +import org.bitcoins.testkit.Implicits._ +import org.bitcoins.testkit.core.gen.{CryptoGenerators, NumberGenerator} +import org.bitcoins.testkit.fixtures.WalletDAOs +import org.bitcoins.wallet.models._ + +import scala.concurrent.{ExecutionContext, Future} object WalletTestUtil { @@ -78,9 +76,8 @@ object WalletTestUtil { private def randomTXID = CryptoGenerators.doubleSha256Digest.sampleSome.flip private def randomVout = NumberGenerator.uInt32s.sampleSome - - /** Between 0 and 10 confirmations */ - private def randomConfs: Int = Gen.choose(0, 10).sampleSome + private def randomBlockHash = + CryptoGenerators.doubleSha256Digest.sampleSome.flip private def randomSpent: Boolean = math.random > 0.5 @@ -90,13 +87,15 @@ object WalletTestUtil { TransactionOutput(1.bitcoin, spk) val scriptWitness = randomScriptWitness val privkeyPath = WalletTestUtil.sampleSegwitPath - SegwitV0SpendingInfo(confirmations = randomConfs, - spent = randomSpent, - txid = randomTXID, - outPoint = outpoint, - output = output, - privKeyPath = privkeyPath, - scriptWitness = scriptWitness) + SegwitV0SpendingInfo( + spent = randomSpent, + txid = randomTXID, + outPoint = outpoint, + output = output, + privKeyPath = privkeyPath, + scriptWitness = scriptWitness, + blockHash = Some(randomBlockHash) + ) } def sampleLegacyUTXO(spk: ScriptPubKey): LegacySpendingInfo = { @@ -105,12 +104,12 @@ object WalletTestUtil { val output = TransactionOutput(1.bitcoin, spk) val privKeyPath = WalletTestUtil.sampleLegacyPath - LegacySpendingInfo(confirmations = randomConfs, - spent = randomSpent, + LegacySpendingInfo(spent = randomSpent, txid = randomTXID, outPoint = outpoint, output = output, - privKeyPath = privKeyPath) + privKeyPath = privKeyPath, + blockHash = Some(randomBlockHash)) } /** Given an account returns a sample address */ diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/ProcessTransactionTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/ProcessTransactionTest.scala index e7bbf0f59c..d8ce37536b 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/ProcessTransactionTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/ProcessTransactionTest.scala @@ -1,17 +1,15 @@ package org.bitcoins.wallet -import org.bitcoins.testkit.wallet.BitcoinSWalletTest -import org.bitcoins.testkit.Implicits._ -import org.scalatest.FutureOutcome +import org.bitcoins.core.crypto.{DoubleSha256DigestBE, ECPrivateKey} import org.bitcoins.core.currency._ -import scala.concurrent.Future -import org.scalatest.compatible.Assertion -import org.bitcoins.wallet.api.UnlockedWalletApi -import org.bitcoins.rpc.client.common.BitcoindRpcClient -import org.bitcoins.core.protocol.transaction.Transaction +import org.bitcoins.testkit.Implicits._ import org.bitcoins.testkit.core.gen.TransactionGenerators -import org.bitcoins.core.protocol.script.ScriptPubKey -import scala.annotation.tailrec +import org.bitcoins.testkit.wallet.BitcoinSWalletTest +import org.bitcoins.wallet.api.UnlockedWalletApi +import org.scalatest.FutureOutcome +import org.scalatest.compatible.Assertion + +import scala.concurrent.Future class ProcessTransactionTest extends BitcoinSWalletTest { override type FixtureParam = UnlockedWalletApi @@ -47,24 +45,26 @@ class ProcessTransactionTest extends BitcoinSWalletTest { tx = TransactionGenerators .transactionTo(address.scriptPubKey) .sampleSome + blockHash = DoubleSha256DigestBE.fromBytes( + ECPrivateKey.freshPrivateKey.bytes) - _ <- wallet.processTransaction(tx, confirmations = 0) + _ <- wallet.processTransaction(tx, None) oldConfirmed <- wallet.getConfirmedBalance() oldUnconfirmed <- wallet.getUnconfirmedBalance() // repeating the action should not make a difference _ <- checkUtxosAndBalance(wallet) { - wallet.processTransaction(tx, confirmations = 0) + wallet.processTransaction(tx, None) } - _ <- wallet.processTransaction(tx, confirmations = 3) + _ <- wallet.processTransaction(tx, Some(blockHash)) newConfirmed <- wallet.getConfirmedBalance() newUnconfirmed <- wallet.getUnconfirmedBalance() utxosPostAdd <- wallet.listUtxos() // repeating the action should not make a difference _ <- checkUtxosAndBalance(wallet) { - wallet.processTransaction(tx, confirmations = 3) + wallet.processTransaction(tx, Some(blockHash)) } } yield { val ourOutputs = @@ -81,7 +81,7 @@ class ProcessTransactionTest extends BitcoinSWalletTest { val unrelated = TransactionGenerators.transaction.sampleSome for { _ <- checkUtxosAndBalance(wallet) { - wallet.processTransaction(unrelated, confirmations = 4) + wallet.processTransaction(unrelated, None) } balance <- wallet.getBalance() diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/WalletIntegrationTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/WalletIntegrationTest.scala index 5de4296309..9e3d8b0363 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/WalletIntegrationTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/WalletIntegrationTest.scala @@ -1,16 +1,11 @@ package org.bitcoins.wallet import org.bitcoins.core.currency._ -import org.bitcoins.core.number.UInt32 -import org.bitcoins.core.wallet.fee.SatoshisPerByte -import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil -import org.bitcoins.wallet.api.{AddUtxoError, AddUtxoSuccess, WalletApi} -import org.bitcoins.testkit.wallet.BitcoinSWalletTest -import org.scalatest.FutureOutcome - -import scala.concurrent.Future import org.bitcoins.core.hd.HDChainType +import org.bitcoins.core.wallet.fee.SatoshisPerByte +import org.bitcoins.testkit.wallet.BitcoinSWalletTest import org.bitcoins.testkit.wallet.BitcoinSWalletTest.WalletWithBitcoind +import org.scalatest.FutureOutcome class WalletIntegrationTest extends BitcoinSWalletTest { @@ -50,10 +45,8 @@ class WalletIntegrationTest extends BitcoinSWalletTest { for { addr <- wallet.getNewAddress() - - tx <- bitcoind - .sendToAddress(addr, valueFromBitcoind) - .flatMap(bitcoind.getRawTransactionRaw(_)) + txId <- bitcoind.sendToAddress(addr, valueFromBitcoind) + tx <- bitcoind.getRawTransactionRaw(txId) // before processing TX, wallet should be completely empty _ <- wallet.listUtxos().map(utxos => assert(utxos.isEmpty)) @@ -63,7 +56,7 @@ class WalletIntegrationTest extends BitcoinSWalletTest { .map(unconfirmed => assert(unconfirmed == 0.bitcoin)) // after this, tx is unconfirmed in wallet - _ <- wallet.processTransaction(tx, confirmations = 0) + _ <- wallet.processTransaction(tx, None) // we should now have one UTXO in the wallet // it should not be confirmed @@ -79,8 +72,11 @@ class WalletIntegrationTest extends BitcoinSWalletTest { .getUnconfirmedBalance() .map(unconfirmed => assert(unconfirmed == valueFromBitcoind)) + _ <- bitcoind.getNewAddress.flatMap(bitcoind.generateToAddress(6, _)) + rawTx <- bitcoind.getRawTransaction(txId) + // after this, tx should be confirmed - _ <- wallet.processTransaction(tx, confirmations = 6) + _ <- wallet.processTransaction(tx, rawTx.blockhash) _ <- wallet .listUtxos() .map { utxos => diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/api/CoinSelectorTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/api/CoinSelectorTest.scala index 7bb68d8363..84c7fb5f9b 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/api/CoinSelectorTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/api/CoinSelectorTest.scala @@ -1,17 +1,18 @@ package org.bitcoins.wallet.api import org.bitcoins.core.currency._ -import org.bitcoins.core.number.Int64 import org.bitcoins.core.protocol.script.ScriptPubKey import org.bitcoins.core.protocol.transaction.TransactionOutput import org.bitcoins.core.wallet.fee.{FeeUnit, SatoshisPerByte} -import org.bitcoins.testkit.core.gen.{TransactionGenerators, WitnessGenerators} -import org.bitcoins.testkit.wallet.{BitcoinSWalletTest, WalletTestUtil} -import org.scalatest.FutureOutcome -import org.bitcoins.wallet.models.SpendingInfoDb -import org.bitcoins.wallet.models.SegwitV0SpendingInfo import org.bitcoins.testkit.Implicits._ -import org.bitcoins.testkit.core.gen.CryptoGenerators +import org.bitcoins.testkit.core.gen.{ + CryptoGenerators, + TransactionGenerators, + WitnessGenerators +} +import org.bitcoins.testkit.wallet.{BitcoinSWalletTest, WalletTestUtil} +import org.bitcoins.wallet.models.{SegwitV0SpendingInfo, SpendingInfoDb} +import org.scalatest.FutureOutcome class CoinSelectorTest extends BitcoinSWalletTest { case class CoinSelectionFixture( @@ -30,34 +31,34 @@ class CoinSelectorTest extends BitcoinSWalletTest { val feeRate = SatoshisPerByte(CurrencyUnits.zero) val utxo1 = SegwitV0SpendingInfo( - confirmations = 0, txid = CryptoGenerators.doubleSha256Digest.sampleSome.flip, spent = false, id = Some(1), outPoint = TransactionGenerators.outPoint.sampleSome, output = TransactionOutput(10.sats, ScriptPubKey.empty), privKeyPath = WalletTestUtil.sampleSegwitPath, - scriptWitness = WitnessGenerators.scriptWitness.sampleSome + scriptWitness = WitnessGenerators.scriptWitness.sampleSome, + blockHash = None ) val utxo2 = SegwitV0SpendingInfo( - confirmations = 0, txid = CryptoGenerators.doubleSha256Digest.sampleSome.flip, spent = false, id = Some(2), outPoint = TransactionGenerators.outPoint.sampleSome, output = TransactionOutput(90.sats, ScriptPubKey.empty), privKeyPath = WalletTestUtil.sampleSegwitPath, - scriptWitness = WitnessGenerators.scriptWitness.sampleSome + scriptWitness = WitnessGenerators.scriptWitness.sampleSome, + blockHash = None ) val utxo3 = SegwitV0SpendingInfo( - confirmations = 0, txid = CryptoGenerators.doubleSha256Digest.sampleSome.flip, spent = false, id = Some(3), outPoint = TransactionGenerators.outPoint.sampleSome, output = TransactionOutput(20.sats, ScriptPubKey.empty), privKeyPath = WalletTestUtil.sampleSegwitPath, - scriptWitness = WitnessGenerators.scriptWitness.sampleSome + scriptWitness = WitnessGenerators.scriptWitness.sampleSome, + blockHash = None ) test(CoinSelectionFixture(output, feeRate, utxo1, utxo2, utxo3)) diff --git a/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala b/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala index 903cc073a8..e1c1db564d 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala @@ -84,7 +84,7 @@ sealed abstract class Wallet extends LockedWallet with UnlockedWalletApi { signed <- txBuilder.sign ourOuts <- findOurOuts(signed) // TODO internal - _ <- processOurTransaction(signed, confirmations = 0) + _ <- processOurTransaction(signed, blockHashOpt = None) } yield { logger.debug( s"Signed transaction=${signed.txIdBE.hex} with outputs=${signed.outputs.length}, inputs=${signed.inputs.length}") diff --git a/wallet/src/main/scala/org/bitcoins/wallet/api/WalletApi.scala b/wallet/src/main/scala/org/bitcoins/wallet/api/WalletApi.scala index 2272bcb53a..0cb98f7f15 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/api/WalletApi.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/api/WalletApi.scala @@ -51,17 +51,17 @@ trait LockedWalletApi extends WalletApi { /** * Processes the given transaction, updating our DB state if it's relevant to us. * @param transaction The transaction we're processing - * @param confirmations How many confirmations the TX has + * @param blockHash Containing block hash */ def processTransaction( transaction: Transaction, - confirmations: Int): Future[LockedWalletApi] + blockHash: Option[DoubleSha256DigestBE]): Future[LockedWalletApi] /** * Processes the give block, updating our DB state if it's relevant to us. * @param block The block we're processing */ - def processBlock(block: Block, confirmations: Int): Future[LockedWalletApi] + def processBlock(block: Block): Future[LockedWalletApi] def processCompactFilter( blockHash: DoubleSha256Digest, diff --git a/wallet/src/main/scala/org/bitcoins/wallet/internal/TransactionProcessing.scala b/wallet/src/main/scala/org/bitcoins/wallet/internal/TransactionProcessing.scala index abdc0ad216..3bab0e6c82 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/internal/TransactionProcessing.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/internal/TransactionProcessing.scala @@ -1,5 +1,6 @@ package org.bitcoins.wallet.internal +import org.bitcoins.core.crypto.DoubleSha256DigestBE import org.bitcoins.core.number.UInt32 import org.bitcoins.core.protocol.blockchain.Block import org.bitcoins.core.protocol.transaction.{Transaction, TransactionOutput} @@ -23,29 +24,26 @@ private[wallet] trait TransactionProcessing extends WalletLogger { /** @inheritdoc */ override def processTransaction( transaction: Transaction, - confirmations: Int): Future[LockedWallet] = { - logger.info( - s"Processing transaction=${transaction.txIdBE} with confirmations=$confirmations") - processTransactionImpl(transaction, confirmations).map { - case ProcessTxResult(incoming, outgoing) => - logger.info( - s"Finished processing of transaction=${transaction.txIdBE}. Relevant incomingTXOs=${incoming.length}, outgoingTXOs=${outgoing.length}") - this + blockHashOpt: Option[DoubleSha256DigestBE] + ): Future[LockedWallet] = { + for { + result <- processTransactionImpl(transaction, blockHashOpt) + } yield { + logger.info( + s"Finished processing of transaction=${transaction.txIdBE}. Relevant incomingTXOs=${result.updatedIncoming.length}, outgoingTXOs=${result.updatedOutgoing.length}") + this } - } /** @inheritdoc */ - override def processBlock( - block: Block, - confirmations: Int): Future[LockedWallet] = { - logger.info( - s"Processing block=${block.blockHeader.hash.flip} with confirmations=$confirmations") + override def processBlock(block: Block): Future[LockedWallet] = { + logger.info(s"Processing block=${block.blockHeader.hash.flip}") val res = block.transactions.foldLeft(Future.successful(this)) { (acc, transaction) => for { _ <- acc - newWallet <- processTransaction(transaction, confirmations) + newWallet <- processTransaction(transaction, + Some(block.blockHeader.hash.flip)) } yield { newWallet } @@ -74,10 +72,10 @@ private[wallet] trait TransactionProcessing extends WalletLogger { */ private[wallet] def processOurTransaction( transaction: Transaction, - confirmations: Int): Future[ProcessTxResult] = { + blockHashOpt: Option[DoubleSha256DigestBE]): Future[ProcessTxResult] = { logger.info( - s"Processing TX from our wallet, transaction=${transaction.txIdBE} with confirmations=$confirmations") - processTransactionImpl(transaction, confirmations).map { result => + s"Processing TX from our wallet, transaction=${transaction.txIdBE} with blockHash=$blockHashOpt") + processTransactionImpl(transaction, blockHashOpt).map { result => val txid = transaction.txIdBE val changeOutputs = result.updatedIncoming.length val spentOutputs = result.updatedOutgoing.length @@ -98,49 +96,61 @@ private[wallet] trait TransactionProcessing extends WalletLogger { */ private def processTransactionImpl( transaction: Transaction, - confirmations: Int): Future[ProcessTxResult] = { + blockHashOpt: Option[DoubleSha256DigestBE]): Future[ProcessTxResult] = { - val incomingTxoFut: Future[Vector[SpendingInfoDb]] = - spendingInfoDAO - .findTx(transaction) - .flatMap { - // no existing elements found - case Vector() => - processNewIncomingTx(transaction, confirmations).map(_.toVector) + logger.info( + s"Processing transaction=${transaction.txIdBE} with blockHash=$blockHashOpt") + for { + aggregate <- { - case txos: Vector[SpendingInfoDb] => - val txoProcessingFutures = - txos - .map(processExistingIncomingTxo(transaction, confirmations, _)) + val incomingTxoFut: Future[Vector[SpendingInfoDb]] = + spendingInfoDAO + .findTx(transaction) + .flatMap { + // no existing elements found + case Vector() => + processNewIncomingTx(transaction, blockHashOpt) + .map(_.toVector) - Future - .sequence(txoProcessingFutures) + case txos: Vector[SpendingInfoDb] => + val txoProcessingFutures = + txos + .map( + processExistingIncomingTxo(transaction, blockHashOpt, _)) + + Future + .sequence(txoProcessingFutures) + + } + + val outgoingTxFut: Future[Vector[SpendingInfoDb]] = { + for { + outputsBeingSpent <- spendingInfoDAO.findOutputsBeingSpent( + transaction) + processed <- FutureUtil.sequentially(outputsBeingSpent)( + markAsSpentIfUnspent) + } yield processed.flatten.toVector } - val outgoingTxFut: Future[Vector[SpendingInfoDb]] = { - for { - outputsBeingSpent <- spendingInfoDAO.findOutputsBeingSpent(transaction) - processed <- FutureUtil.sequentially(outputsBeingSpent)( - markAsSpentIfUnspent) - } yield processed.flatten.toVector + val aggregateFut = + for { + incoming <- incomingTxoFut + outgoing <- outgoingTxFut + } yield { + ProcessTxResult(incoming.toList, outgoing.toList) + } - } + aggregateFut.failed.foreach { err => + val msg = s"Error when processing transaction=${transaction.txIdBE}" + logger.error(msg, err) + } - val aggregateFut = - for { - incoming <- incomingTxoFut - outgoing <- outgoingTxFut - } yield { - ProcessTxResult(incoming.toList, outgoing.toList) + aggregateFut } - - aggregateFut.failed.foreach { err => - val msg = s"Error when processing transaction=${transaction.txIdBE}" - logger.error(msg, err) + } yield { + aggregate } - - aggregateFut } /** If the given UTXO is marked as unspent, updates @@ -149,7 +159,7 @@ private[wallet] trait TransactionProcessing extends WalletLogger { private val markAsSpentIfUnspent: SpendingInfoDb => Future[ Option[SpendingInfoDb]] = { out => if (out.spent) { - Future.successful(None) + FutureUtil.none } else { val updatedF = spendingInfoDAO.update(out.copyWithSpent(spent = true)) @@ -171,11 +181,8 @@ private[wallet] trait TransactionProcessing extends WalletLogger { transaction: Transaction, index: Int, spent: Boolean, - confirmations: Int): Future[SpendingInfoDb] = - addUtxo(transaction, - UInt32(index), - spent = spent, - confirmations = confirmations) + blockHash: Option[DoubleSha256DigestBE]): Future[SpendingInfoDb] = + addUtxo(transaction, UInt32(index), spent = spent, blockHash = blockHash) .flatMap { case AddUtxoSuccess(utxo) => Future.successful(utxo) case err: AddUtxoError => @@ -192,48 +199,61 @@ private[wallet] trait TransactionProcessing extends WalletLogger { */ private def processExistingIncomingTxo( transaction: Transaction, - confirmations: Int, + blockHashOpt: Option[DoubleSha256DigestBE], foundTxo: SpendingInfoDb): Future[SpendingInfoDb] = { - if (foundTxo.confirmations < confirmations) { - // TODO The assumption here is that double-spends never occur. That's not - // the case. This must be fixed when double-spend logic is implemented. - logger.debug( - s"Increasing confirmation count of txo=${transaction.txIdBE}, old=${foundTxo.confirmations} new=${confirmations}") - val updateF = - spendingInfoDAO.update( - foundTxo.copyWithConfirmations(confirmations = confirmations)) - - updateF.foreach(tx => - logger.debug( - s"Updated confirmation count=${tx.confirmations} of output=${foundTxo}")) - updateF.failed.foreach(err => - logger.error( - s"Failed to update confirmation count of transaction=${transaction.txIdBE}", - err)) - - updateF - } else if (foundTxo.confirmations > confirmations) { - val msg = - List( - s"Incoming transaction=${transaction.txIdBE} has fewer confirmations=$confirmations", - s"than what we already have registered=${foundTxo.confirmations}! I don't know how", - s"to handle this." + if (foundTxo.txid != transaction.txIdBE) { + val errMsg = + Seq( + s"Found TXO has txid=${foundTxo.txid}, tx we were given has txid=${transaction.txIdBE}.", + "This is either a reorg or a double spent, which is not implemented yet" ).mkString(" ") - logger.warn(msg) - Future.failed(new RuntimeException(msg)) + logger.error(errMsg) + Future.failed(new RuntimeException(errMsg)) } else { - if (foundTxo.txid == transaction.txIdBE) { - logger.debug( - s"Skipping further processing of transaction=${transaction.txIdBE}, already processed.") - Future.successful(foundTxo) - } else { - val errMsg = - Seq( - s"Found TXO has txid=${foundTxo.txid}, tx we were given has txid=${transaction.txIdBE}.", - "This is either a reorg or a double spent, which is not implemented yet" - ).mkString(" ") - logger.error(errMsg) - Future.failed(new RuntimeException(errMsg)) + (foundTxo.blockHash, blockHashOpt) match { + case (None, Some(blockHash)) => + logger.debug( + s"Updating block_hash of txo=${transaction.txIdBE}, new block hash=${blockHash}") + val updateF = + spendingInfoDAO.update( + foundTxo + .copyWithBlockHash(blockHash = blockHash)) + + updateF.foreach(tx => + logger.debug( + s"Updated block_hash of txo=${tx.txid} new block hash=${blockHash}")) + updateF.failed.foreach(err => + logger.error( + s"Failed to update confirmation count of transaction=${transaction.txIdBE}", + err)) + + updateF + case (Some(oldBlockHash), Some(newBlockHash)) => + if (oldBlockHash == newBlockHash) { + logger.debug( + s"Skipping further processing of transaction=${transaction.txIdBE}, already processed.") + Future.successful(foundTxo) + } else { + val errMsg = + Seq( + s"Found TXO has block hash=${oldBlockHash}, tx we were given has block hash=${newBlockHash}.", + "This is either a reorg or a double spent, which is not implemented yet" + ).mkString(" ") + logger.error(errMsg) + Future.failed(new RuntimeException(errMsg)) + } + case (Some(blockHash), None) => + val msg = + List( + s"Incoming transaction=${transaction.txIdBE} already has block hash=$blockHash! assigned", + s" I don't know how to handle this." + ).mkString(" ") + logger.warn(msg) + Future.failed(new RuntimeException(msg)) + case (None, None) => + logger.debug( + s"Skipping further processing of transaction=${transaction.txIdBE}, already processed.") + Future.successful(foundTxo) } } } @@ -245,7 +265,7 @@ private[wallet] trait TransactionProcessing extends WalletLogger { */ private def processNewIncomingTx( transaction: Transaction, - confirmations: Int): Future[Seq[SpendingInfoDb]] = { + blockHashOpt: Option[DoubleSha256DigestBE]): Future[Seq[SpendingInfoDb]] = { addressDAO.findAll().flatMap { addrs => val relevantOutsWithIdx: Seq[OutputWithIndex] = { val withIndex = @@ -281,9 +301,9 @@ private[wallet] trait TransactionProcessing extends WalletLogger { out => processUtxo(transaction, out.index, - confirmations = confirmations, // TODO is this correct? - spent = false)) + spent = false, + blockHash = blockHashOpt)) } addUTXOsFut diff --git a/wallet/src/main/scala/org/bitcoins/wallet/internal/UtxoHandling.scala b/wallet/src/main/scala/org/bitcoins/wallet/internal/UtxoHandling.scala index 8ce8943728..32adfe7c20 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/internal/UtxoHandling.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/internal/UtxoHandling.scala @@ -48,30 +48,30 @@ private[wallet] trait UtxoHandling extends WalletLogger { /** Constructs a DB level representation of the given UTXO, and persist it to disk */ private def writeUtxo( txid: DoubleSha256DigestBE, - confirmations: Int, spent: Boolean, output: TransactionOutput, outPoint: TransactionOutPoint, - addressDb: AddressDb): Future[SpendingInfoDb] = { + addressDb: AddressDb, + blockHash: Option[DoubleSha256DigestBE]): Future[SpendingInfoDb] = { val utxo: SpendingInfoDb = addressDb match { case segwitAddr: SegWitAddressDb => SegwitV0SpendingInfo( - confirmations = confirmations, spent = spent, txid = txid, outPoint = outPoint, output = output, privKeyPath = segwitAddr.path, - scriptWitness = segwitAddr.witnessScript + scriptWitness = segwitAddr.witnessScript, + blockHash = blockHash ) case LegacyAddressDb(path, _, _, _, _) => - LegacySpendingInfo(confirmations = confirmations, - spent = spent, + LegacySpendingInfo(spent = spent, txid = txid, outPoint = outPoint, output = output, - privKeyPath = path) + privKeyPath = path, + blockHash = blockHash) case nested: NestedSegWitAddressDb => throw new IllegalArgumentException( s"Bad utxo $nested. Note: nested segwit is not implemented") @@ -93,8 +93,8 @@ private[wallet] trait UtxoHandling extends WalletLogger { protected def addUtxo( transaction: Transaction, vout: UInt32, - confirmations: Int, - spent: Boolean): Future[AddUtxoResult] = { + spent: Boolean, + blockHash: Option[DoubleSha256DigestBE]): Future[AddUtxoResult] = { import AddUtxoError._ logger.info(s"Adding UTXO to wallet: ${transaction.txId.hex}:${vout.toInt}") @@ -127,11 +127,11 @@ private[wallet] trait UtxoHandling extends WalletLogger { val biasedE: CompatEither[AddUtxoError, Future[SpendingInfoDb]] = for { addressDb <- addressDbE } yield writeUtxo(txid = transaction.txIdBE, - confirmations = confirmations, spent = spent, output, outPoint, - addressDb) + addressDb, + blockHash) EitherUtil.liftRightBiasedFutureE(biasedE) } map { diff --git a/wallet/src/main/scala/org/bitcoins/wallet/models/SpendingInfoTable.scala b/wallet/src/main/scala/org/bitcoins/wallet/models/SpendingInfoTable.scala index fd6f5da4b8..bfed234fe9 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/models/SpendingInfoTable.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/models/SpendingInfoTable.scala @@ -1,7 +1,8 @@ package org.bitcoins.wallet.models -import org.bitcoins.core.crypto.Sign +import org.bitcoins.core.crypto.{BIP39Seed, DoubleSha256DigestBE, Sign} import org.bitcoins.core.currency.CurrencyUnit +import org.bitcoins.core.hd.{HDPath, LegacyHDPath, SegWitHDPath} import org.bitcoins.core.protocol.script.{ScriptPubKey, ScriptWitness} import org.bitcoins.core.protocol.transaction.{ TransactionOutPoint, @@ -12,11 +13,6 @@ import org.bitcoins.core.wallet.utxo.{BitcoinUTXOSpendingInfo, ConditionalPath} import org.bitcoins.db.{DbRowAutoInc, TableAutoInc} import slick.jdbc.SQLiteProfile.api._ import slick.lifted.ProvenShape -import org.bitcoins.core.hd.HDPath -import org.bitcoins.core.hd.SegWitHDPath -import org.bitcoins.core.crypto.BIP39Seed -import org.bitcoins.core.hd.LegacyHDPath -import org.bitcoins.core.crypto.DoubleSha256DigestBE /** * DB representation of a native V0 @@ -29,8 +25,8 @@ case class SegwitV0SpendingInfo( scriptWitness: ScriptWitness, txid: DoubleSha256DigestBE, spent: Boolean, - confirmations: Int, - id: Option[Long] = None + id: Option[Long] = None, + blockHash: Option[DoubleSha256DigestBE] ) extends SpendingInfoDb { override val redeemScriptOpt: Option[ScriptPubKey] = None override val scriptWitnessOpt: Option[ScriptWitness] = Some(scriptWitness) @@ -38,13 +34,16 @@ case class SegwitV0SpendingInfo( override type PathType = SegWitHDPath override type SpendingInfoType = SegwitV0SpendingInfo - def copyWithSpent(spent: Boolean): SegwitV0SpendingInfo = copy(spent = spent) - - def copyWithConfirmations(confirmations: Int): SegwitV0SpendingInfo = - copy(confirmations = confirmations) + override def copyWithSpent(spent: Boolean): SegwitV0SpendingInfo = + copy(spent = spent) override def copyWithId(id: Long): SegwitV0SpendingInfo = copy(id = Some(id)) + + /** Updates the `blockHash` field */ + override def copyWithBlockHash( + blockHash: DoubleSha256DigestBE): SegwitV0SpendingInfo = + copy(blockHash = Some(blockHash)) } /** @@ -54,9 +53,9 @@ case class LegacySpendingInfo( outPoint: TransactionOutPoint, output: TransactionOutput, privKeyPath: LegacyHDPath, - confirmations: Int, spent: Boolean, txid: DoubleSha256DigestBE, + blockHash: Option[DoubleSha256DigestBE], id: Option[Long] = None ) extends SpendingInfoDb { override val redeemScriptOpt: Option[ScriptPubKey] = None @@ -68,10 +67,12 @@ case class LegacySpendingInfo( override def copyWithId(id: Long): LegacySpendingInfo = copy(id = Some(id)) - def copyWithSpent(spent: Boolean): LegacySpendingInfo = copy(spent = spent) + override def copyWithSpent(spent: Boolean): LegacySpendingInfo = + copy(spent = spent) - def copyWithConfirmations(confirmations: Int): LegacySpendingInfo = - copy(confirmations = confirmations) + override def copyWithBlockHash( + blockHash: DoubleSha256DigestBE): LegacySpendingInfo = + copy(blockHash = Some(blockHash)) } // TODO add case for nested segwit @@ -101,18 +102,15 @@ sealed trait SpendingInfoDb extends DbRowAutoInc[SpendingInfoDb] { val hashType: HashType = HashType.sigHashAll - /** How many confirmations this output has */ - // MOVE ME - require(confirmations >= 0, - s"Confirmations cannot be negative! Got: $confirmations") - def confirmations: Int - /** Whether or not this TXO is spent from our wallet */ def spent: Boolean /** The TXID of the transaction this output was received in */ def txid: DoubleSha256DigestBE + /** The hash of the block in which the transaction was included */ + def blockHash: Option[DoubleSha256DigestBE] + /** Converts the UTXO to the canonical `txid:vout` format */ def toHumanReadableString: String = s"${outPoint.txId.flip.hex}:${outPoint.vout.toInt}" @@ -120,8 +118,8 @@ sealed trait SpendingInfoDb extends DbRowAutoInc[SpendingInfoDb] { /** Updates the `spent` field */ def copyWithSpent(spent: Boolean): SpendingInfoType - /** Updates the `confirmations` field */ - def copyWithConfirmations(confirmations: Int): SpendingInfoType + /** Updates the `blockHash` field */ + def copyWithBlockHash(blockHash: DoubleSha256DigestBE): SpendingInfoType /** Converts a non-sensitive DB representation of a UTXO into * a signable (and sensitive) real-world UTXO @@ -165,8 +163,6 @@ case class SpendingInfoTable(tag: Tag) def txid: Rep[DoubleSha256DigestBE] = column("txid") - def confirmations: Rep[Int] = column("confirmations") - def spent: Rep[Boolean] = column("spent") def scriptPubKey: Rep[ScriptPubKey] = column("script_pub_key") @@ -180,6 +176,8 @@ case class SpendingInfoTable(tag: Tag) def scriptWitnessOpt: Rep[Option[ScriptWitness]] = column("script_witness") + def blockHash: Rep[Option[DoubleSha256DigestBE]] = column("block_hash") + /** All UTXOs must have a SPK in the wallet that gets spent to */ def fk_scriptPubKey = { val addressTable = TableQuery[AddressTable] @@ -196,9 +194,9 @@ case class SpendingInfoTable(tag: Tag) HDPath, Option[ScriptPubKey], // ReedemScript Option[ScriptWitness], - Int, // confirmations Boolean, // spent - DoubleSha256DigestBE // TXID + DoubleSha256DigestBE, // TXID + Option[DoubleSha256DigestBE] // block hash ) private val fromTuple: UTXOTuple => SpendingInfoDb = { @@ -209,18 +207,18 @@ case class SpendingInfoTable(tag: Tag) path: SegWitHDPath, None, // ReedemScript Some(scriptWitness), - confirmations, spent, - txid) => + txid, + blockHash) => SegwitV0SpendingInfo( outPoint = outpoint, output = TransactionOutput(value, spk), privKeyPath = path, scriptWitness = scriptWitness, id = id, - confirmations = confirmations, spent = spent, - txid = txid + txid = txid, + blockHash = blockHash ) case (id, @@ -230,20 +228,29 @@ case class SpendingInfoTable(tag: Tag) path: LegacyHDPath, None, // RedeemScript None, // ScriptWitness - confirmations, spent, - txid) => + txid, + blockHash) => LegacySpendingInfo(outPoint = outpoint, output = TransactionOutput(value, spk), privKeyPath = path, id = id, - confirmations = confirmations, spent = spent, - txid = txid) - case (id, outpoint, spk, value, path, spkOpt, swOpt, confs, spent, txid) => + txid = txid, + blockHash = blockHash) + case (id, + outpoint, + spk, + value, + path, + spkOpt, + swOpt, + spent, + txid, + blockHash) => throw new IllegalArgumentException( "Could not construct UtxoSpendingInfoDb from bad tuple:" - + s" ($id, $outpoint, $spk, $value, $path, $spkOpt, $swOpt, $confs, $spent, $txid)." + + s" ($id, $outpoint, $spk, $value, $path, $spkOpt, $swOpt, $spent, $txid, $blockHash)." + " Note: Nested Segwit is not implemented") } @@ -258,9 +265,9 @@ case class SpendingInfoTable(tag: Tag) utxo.privKeyPath, utxo.redeemScriptOpt, utxo.scriptWitnessOpt, - utxo.confirmations, utxo.spent, - utxo.txid)) + utxo.txid, + utxo.blockHash)) def * : ProvenShape[SpendingInfoDb] = (id.?, @@ -270,7 +277,7 @@ case class SpendingInfoTable(tag: Tag) privKeyPath, redeemScriptOpt, scriptWitnessOpt, - confirmations, spent, - txid) <> (fromTuple, toTuple) + txid, + blockHash) <> (fromTuple, toTuple) }