diff --git a/app/cli/src/main/scala/org/bitcoins/cli/Cli.scala b/app/cli/src/main/scala/org/bitcoins/cli/Cli.scala index ab034309d2..e9b51406e3 100644 --- a/app/cli/src/main/scala/org/bitcoins/cli/Cli.scala +++ b/app/cli/src/main/scala/org/bitcoins/cli/Cli.scala @@ -1,26 +1,17 @@ package org.bitcoins.cli -import org.bitcoins.picklers._ -import scopt.OParser -import org.bitcoins.core.config.NetworkParameters -import upickle.{default => up} -import CliReaders._ -import org.bitcoins.core.protocol._ -import org.bitcoins.core.currency._ -import org.bitcoins.cli.CliCommand.{ - GetBalance, - GetBestBlockHash, - GetBlockCount, - GetNewAddress, - GetPeers, - NoCommand, - Rescan, - SendToAddress -} import java.net.ConnectException import java.{util => ju} -import ujson.Num -import ujson.Str + +import org.bitcoins.cli.CliCommand._ +import org.bitcoins.cli.CliReaders._ +import org.bitcoins.core.config.NetworkParameters +import org.bitcoins.core.currency._ +import org.bitcoins.core.protocol._ +import org.bitcoins.picklers._ +import scopt.OParser +import ujson.{Num, Str} +import upickle.{default => up} case class Config( command: CliCommand = CliCommand.NoCommand, @@ -46,9 +37,10 @@ object CliCommand { case object GetBestBlockHash extends CliCommand case object GetBlockCount extends CliCommand case class Rescan( - addresses: Vector[BitcoinAddress], + addressBatchSize: Option[Int], startBlock: Option[BlockStamp], - endBlock: Option[BlockStamp]) + endBlock: Option[BlockStamp], + force: Boolean) extends CliCommand } @@ -80,17 +72,26 @@ object Cli extends App { .action( (_, conf) => conf.copy( - command = Rescan(addresses = Vector.empty, + command = Rescan(addressBatchSize = Option.empty, startBlock = Option.empty, - endBlock = Option.empty))) + endBlock = Option.empty, + force = false))) .text(s"Rescan UTXOs") .children( - opt[Seq[BitcoinAddress]]("addresses") - .required() - .action((addrs, conf) => + opt[Unit]("force") + .optional() + .action((_, conf) => conf.copy(command = conf.command match { case rescan: Rescan => - rescan.copy(addresses = addrs.toVector) + rescan.copy(force = true) + case other => other + })), + opt[Int]("batch-size") + .optional() + .action((batchSize, conf) => + conf.copy(command = conf.command match { + case rescan: Rescan => + rescan.copy(addressBatchSize = Option(batchSize)) case other => other })), opt[BlockStamp]("start") @@ -198,11 +199,12 @@ object Cli extends App { RequestParam("getbalance") case GetNewAddress => RequestParam("getnewaddress") - case Rescan(addresses, startBlock, endBlock) => + case Rescan(addressBatchSize, startBlock, endBlock, force) => RequestParam("rescan", - Seq(up.writeJs(addresses), + Seq(up.writeJs(addressBatchSize), up.writeJs(startBlock), - up.writeJs(endBlock))) + up.writeJs(endBlock), + up.writeJs(force))) case SendToAddress(address, bitcoins) => RequestParam("sendtoaddress", 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 2853425c2e..80d636b87b 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 @@ -23,7 +23,7 @@ import org.bitcoins.wallet.MockUnlockedWalletApi import org.scalamock.scalatest.MockFactory import org.scalatest.{Matchers, WordSpec} import ujson.Value.InvalidData -import ujson.{Arr, Null, Num, Str} +import ujson._ import scala.concurrent.Future @@ -181,80 +181,86 @@ class RoutesSpec "run wallet rescan" in { // positive cases - (mockWalletApi.rescan _) - .expects(Vector(testAddress.scriptPubKey), None, None) + (mockWalletApi.discoveryBatchSize: () => Int) + .expects() + .returning(100) + .atLeastOnce() + (mockWalletApi.isEmpty: () => Future[Boolean]) + .expects() + .returning(Future.successful(false)) + (mockWalletApi.rescanNeutrinoWallet _) + .expects(None, None, 100) .returning(FutureUtil.unit) val route1 = walletRoutes.handleCommand( - ServerCommand("rescan", Arr(Arr(Str(testAddressStr)), Null, Null))) + ServerCommand("rescan", Arr(Arr(), Null, Null, true))) Post() ~> route1 ~> check { contentType shouldEqual `application/json` - responseAs[String] shouldEqual """{"result":"ok","error":null}""" + responseAs[String] shouldEqual """{"result":"scheduled","error":null}""" } - (mockWalletApi.rescan _) + (mockWalletApi.isEmpty: () => Future[Boolean]) + .expects() + .returning(Future.successful(false)) + (mockWalletApi.rescanNeutrinoWallet _) .expects( - Vector(testAddress.scriptPubKey), Some(BlockTime( ZonedDateTime.of(2018, 10, 27, 12, 34, 56, 0, ZoneId.of("UTC")))), - None) + None, + 100) .returning(FutureUtil.unit) val route2 = walletRoutes.handleCommand( - ServerCommand( - "rescan", - Arr(Arr(Str(testAddressStr)), Str("2018-10-27T12:34:56Z"), Null))) + ServerCommand("rescan", + Arr(Arr(), Str("2018-10-27T12:34:56Z"), Null, true))) Post() ~> route2 ~> check { contentType shouldEqual `application/json` - responseAs[String] shouldEqual """{"result":"ok","error":null}""" + responseAs[String] shouldEqual """{"result":"scheduled","error":null}""" } - (mockWalletApi.rescan _) - .expects(Vector(testAddress.scriptPubKey), - None, - Some(BlockHash(DoubleSha256DigestBE.empty))) + (mockWalletApi.isEmpty: () => Future[Boolean]) + .expects() + .returning(Future.successful(false)) + (mockWalletApi.rescanNeutrinoWallet _) + .expects(None, Some(BlockHash(DoubleSha256DigestBE.empty)), 100) .returning(FutureUtil.unit) val route3 = walletRoutes.handleCommand( - ServerCommand("rescan", - Arr(Arr(Str(testAddressStr)), - Null, - Str(DoubleSha256DigestBE.empty.hex)))) + ServerCommand( + "rescan", + Arr(Null, Null, Str(DoubleSha256DigestBE.empty.hex), true))) Post() ~> route3 ~> check { contentType shouldEqual `application/json` - responseAs[String] shouldEqual """{"result":"ok","error":null}""" + responseAs[String] shouldEqual """{"result":"scheduled","error":null}""" } - (mockWalletApi.rescan _) - .expects(Vector(testAddress.scriptPubKey), - Some(BlockHeight(12345)), - Some(BlockHeight(67890))) + (mockWalletApi.isEmpty: () => Future[Boolean]) + .expects() + .returning(Future.successful(false)) + (mockWalletApi.rescanNeutrinoWallet _) + .expects(Some(BlockHeight(12345)), Some(BlockHeight(67890)), 100) .returning(FutureUtil.unit) val route4 = walletRoutes.handleCommand( - ServerCommand( - "rescan", - Arr(Arr(Str(testAddressStr)), Str("12345"), Num(67890)))) + ServerCommand("rescan", Arr(Arr(), Str("12345"), Num(67890), true))) Post() ~> route4 ~> check { contentType shouldEqual `application/json` - responseAs[String] shouldEqual """{"result":"ok","error":null}""" + responseAs[String] shouldEqual """{"result":"scheduled","error":null}""" } // negative cases val route5 = walletRoutes.handleCommand( - ServerCommand( - "rescan", - Arr(Arr(Str(testAddressStr)), Str("abcd"), Str("efgh")))) + ServerCommand("rescan", Arr(Null, Str("abcd"), Str("efgh"), true))) Post() ~> route5 ~> check { rejection shouldEqual ValidationRejection( @@ -264,9 +270,8 @@ class RoutesSpec val route6 = walletRoutes.handleCommand( - ServerCommand( - "rescan", - Arr(Arr(Str(testAddressStr)), Null, Str("2018-10-27T12:34:56")))) + ServerCommand("rescan", + Arr(Arr(55), Null, Str("2018-10-27T12:34:56"), true))) Post() ~> route6 ~> check { rejection shouldEqual ValidationRejection( @@ -276,7 +281,7 @@ class RoutesSpec val route7 = walletRoutes.handleCommand( - ServerCommand("rescan", Arr(Arr(Str(testAddressStr)), Num(-1), Null))) + ServerCommand("rescan", Arr(Null, Num(-1), Null, true))) Post() ~> route7 ~> check { rejection shouldEqual ValidationRejection( @@ -284,26 +289,21 @@ class RoutesSpec Some(InvalidData(Num(-1), "Expected a positive integer"))) } + (mockWalletApi.isEmpty: () => Future[Boolean]) + .expects() + .returning(Future.successful(false)) + (mockWalletApi.rescanNeutrinoWallet _) + .expects(None, None, 55) + .returning(FutureUtil.unit) + val route8 = walletRoutes.handleCommand( - ServerCommand("rescan", Arr(Arr(), Null, Null))) + ServerCommand("rescan", Arr(Arr(55), Arr(), Arr(), Bool(true)))) Post() ~> route8 ~> check { - rejection shouldEqual ValidationRejection( - "failure", - Some(InvalidData(Arr(), "Expected a non-empty address array"))) + contentType shouldEqual `application/json` + responseAs[String] shouldEqual """{"result":"scheduled","error":null}""" } - - val route9 = - walletRoutes.handleCommand( - ServerCommand("rescan", Arr(Arr("abcdefgh"), Null, Null))) - - Post() ~> route9 ~> check { - rejection shouldEqual ValidationRejection( - "failure", - Some(InvalidData("abcdefgh", "Expected a valid address"))) - } - } } diff --git a/app/server/src/main/resources/reference.conf b/app/server/src/main/resources/reference.conf index 4fa373e5d2..0d8acaed8f 100644 --- a/app/server/src/main/resources/reference.conf +++ b/app/server/src/main/resources/reference.conf @@ -57,6 +57,10 @@ bitcoin-s { defaultAccountType = legacy # legacy, segwit, nested-segwit bloomFalsePositiveRate = 0.0001 # percentage + + addressGapLimit = 20 + + discoveryBatchSize = 100 } } 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 1bdbfc4fd8..afabdcb51b 100644 --- a/app/server/src/main/scala/org/bitcoins/server/Main.scala +++ b/app/server/src/main/scala/org/bitcoins/server/Main.scala @@ -6,8 +6,8 @@ import java.nio.file.Files import akka.actor.ActorSystem import org.bitcoins.chain.config.ChainAppConfig import org.bitcoins.core.api.ChainQueryApi +import org.bitcoins.keymanager.KeyManagerInitializeError import org.bitcoins.keymanager.bip39.BIP39KeyManager -import org.bitcoins.keymanager.{KeyManager, KeyManagerInitializeError} import org.bitcoins.node.config.NodeAppConfig import org.bitcoins.node.models.Peer import org.bitcoins.node.networking.peer.DataMessageHandler @@ -101,8 +101,10 @@ object Main extends App { // TODO change me when we implement proper password handling locked.unlock(BIP39KeyManager.badPassphrase) match { - case Right(wallet) => Future.successful(wallet) - case Left(kmError) => error(kmError) + case Right(wallet) => + Future.successful(wallet) + case Left(kmError) => + error(kmError) } } else { logger.info(s"Initializing key manager") @@ -132,6 +134,7 @@ object Main extends App { lazy val onCompactFilter: OnCompactFilterReceived = { (blockHash, blockFilter) => wallet.processCompactFilter(blockHash, blockFilter) + () } lazy val onBlock: OnBlockReceived = { block => wallet.processBlock(block) diff --git a/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala b/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala index 9a31e8d719..a65760b0ee 100644 --- a/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala +++ b/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala @@ -3,7 +3,7 @@ package org.bitcoins.server import org.bitcoins.core.currency.Bitcoins import org.bitcoins.core.protocol.BlockStamp.BlockHeight import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp} -import ujson.{Arr, Null, Num, Str, Value} +import ujson._ import upickle.default._ import scala.util.{Failure, Try} @@ -16,27 +16,20 @@ object ServerCommand { } case class Rescan( - addresses: Vector[BitcoinAddress], + batchSize: Option[Int], startBlock: Option[BlockStamp], - endBlock: Option[BlockStamp]) + endBlock: Option[BlockStamp], + force: Boolean) object Rescan extends ServerJsonModels { def fromJsArr(jsArr: ujson.Arr): Try[Rescan] = { - def parseAddresses(value: Value): Vector[BitcoinAddress] = value match { - case Arr(arr) => - if (arr.isEmpty) - throw Value.InvalidData(value, "Expected a non-empty address array") - else - arr.toVector.map(jsToBitcoinAddress) - case _: Value => - throw Value.InvalidData(value, "Expected an Arr") - } - def nullToOpt(value: Value): Option[Value] = value match { - case Null => None - case _: Value => Some(value) + case Null => None + case Arr(arr) if arr.isEmpty => None + case Arr(arr) if arr.size == 1 => Some(arr.head) + case _: Value => Some(value) } def parseBlockStamp(value: Value): Option[BlockStamp] = @@ -51,13 +44,30 @@ object Rescan extends ServerJsonModels { throw Value.InvalidData(value, "Expected a Num or a Str") } + def parseInt(value: Value): Option[Int] = + nullToOpt(value).map { + case Str(value) => value.toInt + case Num(value) => value.toInt + case _: Value => + throw Value.InvalidData(value, "Expected a Num or a Str") + } + + def parseBoolean(value: Value): Boolean = value match { + case Bool(bool) => bool + case _: Value => throw Value.InvalidData(value, "Expected a Bool") + } + jsArr.arr.toList match { - case addrsJs :: startJs :: endJs :: Nil => + case batchSizeJs :: startJs :: endJs :: forceJs :: Nil => Try { - val addresses = parseAddresses(addrsJs) + val batchSize = parseInt(batchSizeJs) val start = parseBlockStamp(startJs) val end = parseBlockStamp(endJs) - Rescan(addresses = addresses, startBlock = start, endBlock = end) + val force = parseBoolean(forceJs) + Rescan(batchSize = batchSize, + startBlock = start, + endBlock = end, + force = force) } case Nil => Failure(new IllegalArgumentException("Missing addresses")) @@ -65,7 +75,7 @@ object Rescan extends ServerJsonModels { case other => Failure( new IllegalArgumentException( - s"Bad number of arguments: ${other.length}. Expected: 3")) + s"Bad number of arguments: ${other.length}. Expected: 4")) } } diff --git a/app/server/src/main/scala/org/bitcoins/server/WalletRoutes.scala b/app/server/src/main/scala/org/bitcoins/server/WalletRoutes.scala index ac61f1fde4..b50038c9d6 100644 --- a/app/server/src/main/scala/org/bitcoins/server/WalletRoutes.scala +++ b/app/server/src/main/scala/org/bitcoins/server/WalletRoutes.scala @@ -10,6 +10,7 @@ import org.bitcoins.node.Node import org.bitcoins.picklers._ import org.bitcoins.wallet.api.UnlockedWalletApi +import scala.concurrent.Future import scala.util.{Failure, Success} case class WalletRoutes(wallet: UnlockedWalletApi, node: Node)( @@ -54,11 +55,26 @@ case class WalletRoutes(wallet: UnlockedWalletApi, node: Node)( Rescan.fromJsArr(arr) match { case Failure(exception) => reject(ValidationRejection("failure", Some(exception))) - case Success(Rescan(addresses, startBlock, endBlock)) => + case Success(Rescan(batchSize, startBlock, endBlock, force)) => complete { - wallet - .rescan(addresses.map(_.scriptPubKey), startBlock, endBlock) - .map(_ => Server.httpSuccess("ok")) + val res = for { + empty <- wallet.isEmpty() + msg <- if (force || empty) { + wallet + .rescanNeutrinoWallet( + startBlock, + endBlock, + batchSize.getOrElse(wallet.discoveryBatchSize)) + .map(_ => "scheduled") + } else { + Future.successful( + "DANGER! The wallet is not empty, however the rescan " + + "process destroys all existing records and creates new ones. " + + "Use force option if you really want to proceed. " + + "Don't forget to backup the wallet database.") + } + } yield msg + res.map(msg => Server.httpSuccess(msg)) } } 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 ddfe489d6d..78a6d13807 100644 --- a/chain/src/main/scala/org/bitcoins/chain/blockchain/ChainHandler.scala +++ b/chain/src/main/scala/org/bitcoins/chain/blockchain/ChainHandler.scala @@ -4,8 +4,9 @@ import org.bitcoins.chain.ChainVerificationLogger import org.bitcoins.chain.api.ChainApi import org.bitcoins.chain.config.ChainAppConfig import org.bitcoins.chain.models._ +import org.bitcoins.core.api.ChainQueryApi.FilterResponse import org.bitcoins.core.crypto.{DoubleSha256Digest, DoubleSha256DigestBE} -import org.bitcoins.core.gcs.{FilterHeader, GolombFilter} +import org.bitcoins.core.gcs.FilterHeader import org.bitcoins.core.p2p.CompactFilterMessage import org.bitcoins.core.protocol.BlockStamp import org.bitcoins.core.protocol.blockchain.BlockHeader @@ -403,10 +404,12 @@ case class ChainHandler( override def getFiltersBetweenHeights( startHeight: Int, - endHeight: Int): Future[Vector[(GolombFilter, DoubleSha256DigestBE)]] = + endHeight: Int): Future[Vector[FilterResponse]] = filterDAO .getBetweenHeights(startHeight, endHeight) - .map(dbos => dbos.map(dbo => (dbo.golombFilter, dbo.blockHashBE))) + .map(dbos => + dbos.map(dbo => + FilterResponse(dbo.golombFilter, dbo.blockHashBE, dbo.height))) } object ChainHandler { 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 873e0c1edc..61248dfa95 100644 --- a/core/src/main/scala/org/bitcoins/core/api/ChainQueryApi.scala +++ b/core/src/main/scala/org/bitcoins/core/api/ChainQueryApi.scala @@ -3,7 +3,6 @@ package org.bitcoins.core.api import org.bitcoins.core.crypto.DoubleSha256DigestBE import org.bitcoins.core.gcs.GolombFilter import org.bitcoins.core.protocol.BlockStamp -import org.bitcoins.core.util.FutureUtil import scala.concurrent.Future @@ -12,6 +11,8 @@ import scala.concurrent.Future */ trait ChainQueryApi { + import org.bitcoins.core.api.ChainQueryApi._ + /** Gets the height of the given block */ def getBlockHeight(blockHash: DoubleSha256DigestBE): Future[Option[Int]] @@ -30,39 +31,15 @@ trait ChainQueryApi { def getFiltersBetweenHeights( startHeight: Int, - endHeight: Int): Future[Vector[(GolombFilter, DoubleSha256DigestBE)]] + endHeight: Int): Future[Vector[FilterResponse]] } object ChainQueryApi { - object NoOp extends ChainQueryApi { - - /** Gets the height of the given block */ - 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(): 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 - - /** Returns the block height of the given block stamp */ - override def getHeightByBlockStamp(blockStamp: BlockStamp): Future[Int] = - Future.successful(0) - - /** Gets the number of compact filters in the database */ - override def getFilterCount: Future[Int] = Future.successful(0) - - override def getFiltersBetweenHeights( - startHeight: Int, - endHeight: Int): Future[Vector[(GolombFilter, DoubleSha256DigestBE)]] = - Future.successful(Vector.empty) - } + case class FilterResponse( + compactFilter: GolombFilter, + blockHash: DoubleSha256DigestBE, + blockHeight: Int) sealed abstract class ChainException(message: String) extends RuntimeException(message) diff --git a/core/src/main/scala/org/bitcoins/core/api/NodeApi.scala b/core/src/main/scala/org/bitcoins/core/api/NodeApi.scala index 523ed95e5e..b25e5f2bfa 100644 --- a/core/src/main/scala/org/bitcoins/core/api/NodeApi.scala +++ b/core/src/main/scala/org/bitcoins/core/api/NodeApi.scala @@ -1,7 +1,6 @@ package org.bitcoins.core.api import org.bitcoins.core.crypto.DoubleSha256Digest -import org.bitcoins.core.util.FutureUtil import scala.concurrent.Future @@ -16,14 +15,3 @@ trait NodeApi { def downloadBlocks(blockHashes: Vector[DoubleSha256Digest]): Future[Unit] } - -object NodeApi { - - object NoOp extends NodeApi { - - override def downloadBlocks( - blockHashes: Vector[DoubleSha256Digest]): Future[Unit] = FutureUtil.unit - - } - -} diff --git a/db-commons/src/main/scala/org/bitcoins/db/CRUD.scala b/db-commons/src/main/scala/org/bitcoins/db/CRUD.scala index 3d3c5b5d01..ff05354136 100644 --- a/db-commons/src/main/scala/org/bitcoins/db/CRUD.scala +++ b/db-commons/src/main/scala/org/bitcoins/db/CRUD.scala @@ -1,10 +1,11 @@ package org.bitcoins.db +import java.sql.SQLException + +import org.bitcoins.core.config.MainNet import slick.jdbc.SQLiteProfile.api._ import scala.concurrent.{ExecutionContext, Future} -import java.sql.SQLException -import org.bitcoins.core.config.MainNet /** * Created by chris on 9/8/16. @@ -84,6 +85,12 @@ abstract class CRUD[T, PrimaryKeyType]( database.run(query.delete) } + /** + * delete all records from the table + */ + def deleteAll(): Future[Int] = + database.run(table.delete) + /** * insert the record if it does not exist, update it if it does * @@ -127,6 +134,8 @@ abstract class CRUD[T, PrimaryKeyType]( def findAll(): Future[Vector[T]] = database.run(table.result).map(_.toVector) + /** Returns number of rows in the table */ + def count(): Future[Int] = database.run(table.length.result) } case class SafeDatabase(config: AppConfig) extends DatabaseLogger { diff --git a/docs/applications/wallet.md b/docs/applications/wallet.md index 3b800cfdf8..7b33be660c 100644 --- a/docs/applications/wallet.md +++ b/docs/applications/wallet.md @@ -110,7 +110,6 @@ val syncF: Future[ChainApi] = configF.flatMap { _ => } //initialize our key manager, where we store our keys -import org.bitcoins.keymanager._ import org.bitcoins.keymanager.bip39._ val keyManager = BIP39KeyManager.initialize(walletConfig.kmParams).getOrElse { throw new RuntimeException(s"Failed to initalize key manager") @@ -122,9 +121,21 @@ val keyManager = BIP39KeyManager.initialize(walletConfig.kmParams).getOrElse { import org.bitcoins.wallet.api.LockedWalletApi import org.bitcoins.wallet.Wallet import org.bitcoins.core.api._ -val wallet = Wallet(keyManager,NodeApi.NoOp, ChainQueryApi.NoOp) +import org.bitcoins.core.crypto._ +import org.bitcoins.core.protocol._ +val wallet = Wallet(keyManager, new NodeApi { + override def downloadBlocks(blockHashes: Vector[DoubleSha256Digest]): Future[Unit] = Future.successful(()) + }, new ChainQueryApi { + import org.bitcoins.core.api.ChainQueryApi._ + override def getBlockHeight(blockHash: DoubleSha256DigestBE): Future[Option[Int]] = Future.successful(None) + override def getBestBlockHash(): Future[DoubleSha256DigestBE] = Future.successful(DoubleSha256DigestBE.empty) + override def getNumberOfConfirmations(blockHashOpt: DoubleSha256DigestBE): Future[Option[Int]] = Future.successful(None) + override def getFilterCount: Future[Int] = Future.successful(0) + override def getHeightByBlockStamp(blockStamp: BlockStamp): Future[Int] = Future.successful(0) + override def getFiltersBetweenHeights(startHeight: Int, endHeight: Int): Future[Vector[FilterResponse]] = Future.successful(Vector.empty) + }) val walletF: Future[LockedWalletApi] = configF.flatMap { _ => - Wallet.initialize(wallet) + Wallet.initialize(wallet) } // when this future completes, ww have sent a transaction diff --git a/key-manager/src/main/scala/org/bitcoins/keymanager/bip39/BIP39KeyManager.scala b/key-manager/src/main/scala/org/bitcoins/keymanager/bip39/BIP39KeyManager.scala index 8edf146f57..5791f607e9 100644 --- a/key-manager/src/main/scala/org/bitcoins/keymanager/bip39/BIP39KeyManager.scala +++ b/key-manager/src/main/scala/org/bitcoins/keymanager/bip39/BIP39KeyManager.scala @@ -56,7 +56,7 @@ case class BIP39KeyManager( object BIP39KeyManager extends KeyManagerCreateApi[BIP39KeyManager] with BitcoinSLogger { - val badPassphrase = AesPassword.fromString("bad-password").get + val badPassphrase = AesPassword.fromString("changeMe").get /** Initializes the mnemonic seed and saves it to file */ override def initializeWithEntropy( diff --git a/node-test/src/test/scala/org/bitcoins/node/NeutrinoNodeTest.scala b/node-test/src/test/scala/org/bitcoins/node/NeutrinoNodeTest.scala index 85dce3ef14..f4c6e49311 100644 --- a/node-test/src/test/scala/org/bitcoins/node/NeutrinoNodeTest.scala +++ b/node-test/src/test/scala/org/bitcoins/node/NeutrinoNodeTest.scala @@ -10,7 +10,6 @@ import org.bitcoins.testkit.BitcoinSTestAppConfig import org.bitcoins.testkit.fixtures.UsesExperimentalBitcoind import org.bitcoins.testkit.node.NodeUnitTest.NeutrinoNodeFundedWalletBitcoind import org.bitcoins.testkit.node.{NodeTestUtil, NodeUnitTest} -import org.scalatest.exceptions.TestFailedException import org.scalatest.{DoNotDiscover, FutureOutcome} import scala.concurrent.duration.DurationInt @@ -148,32 +147,4 @@ class NeutrinoNodeTest extends NodeUnitTest { } } - it must "download a block that matches a compact block filter" taggedAs (UsesExperimentalBitcoind) in { - nodeConnectedWithBitcoind: NeutrinoNodeFundedWalletBitcoind => - val node = nodeConnectedWithBitcoind.node - val wallet = nodeConnectedWithBitcoind.wallet - val bitcoind = nodeConnectedWithBitcoind.bitcoindRpc - - for { - walletUtxos <- wallet.listUtxos() - _ = { - assert(walletUtxos.nonEmpty) - utxos = walletUtxos.map(_.output.scriptPubKey).toSet - } - walletAddress <- wallet.getNewAddress() - _ <- node.sync() - _ <- NodeTestUtil.awaitCompactFiltersSync(node, bitcoind) - _ = system.scheduler.scheduleOnce(testTimeout) { - if (!assertionP.isCompleted) - assertionP.failure( - new TestFailedException( - s"Did not receive a block message after $testTimeout!", - failedCodeStackDepth = 0)) - } - _ <- wallet.rescan( - walletUtxos.map(_.output.scriptPubKey) :+ walletAddress.scriptPubKey) - result <- assertionP.future - } yield assert(result) - } - } 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 9a0799936c..c29c43c033 100644 --- a/node-test/src/test/scala/org/bitcoins/node/NeutrinoNodeWithWalletTest.scala +++ b/node-test/src/test/scala/org/bitcoins/node/NeutrinoNodeWithWalletTest.scala @@ -199,8 +199,9 @@ class NeutrinoNodeWithWalletTest extends NodeUnitTest { _ <- NodeTestUtil.awaitSync(node, bitcoind) _ <- NodeTestUtil.awaitCompactFiltersSync(node, bitcoind) - addresses <- wallet.listAddresses() - _ <- wallet.rescan(addresses.map(_.scriptPubKey)) + _ <- wallet.rescanNeutrinoWallet(startOpt = None, + endOpt = None, + addressBatchSize = 2) _ <- AsyncUtil.awaitConditionF(condition) } yield succeed diff --git a/node/src/main/scala/org/bitcoins/node/NeutrinoNode.scala b/node/src/main/scala/org/bitcoins/node/NeutrinoNode.scala index bae54ec393..43212985d5 100644 --- a/node/src/main/scala/org/bitcoins/node/NeutrinoNode.scala +++ b/node/src/main/scala/org/bitcoins/node/NeutrinoNode.scala @@ -2,8 +2,7 @@ package org.bitcoins.node import akka.actor.ActorSystem import org.bitcoins.chain.config.ChainAppConfig -import org.bitcoins.core.crypto.DoubleSha256DigestBE -import org.bitcoins.core.gcs.GolombFilter +import org.bitcoins.core.api.ChainQueryApi.FilterResponse import org.bitcoins.core.protocol.BlockStamp import org.bitcoins.node.config.NodeAppConfig import org.bitcoins.node.models.Peer @@ -55,7 +54,7 @@ case class NeutrinoNode( override def getFiltersBetweenHeights( startHeight: Int, - endHeight: Int): Future[Vector[(GolombFilter, DoubleSha256DigestBE)]] = + endHeight: Int): Future[Vector[FilterResponse]] = chainApiFromDb().flatMap(_.getFiltersBetweenHeights(startHeight, endHeight)) } diff --git a/node/src/main/scala/org/bitcoins/node/SpvNode.scala b/node/src/main/scala/org/bitcoins/node/SpvNode.scala index d72d852f7f..b48504c9c4 100644 --- a/node/src/main/scala/org/bitcoins/node/SpvNode.scala +++ b/node/src/main/scala/org/bitcoins/node/SpvNode.scala @@ -2,9 +2,8 @@ package org.bitcoins.node import akka.actor.ActorSystem import org.bitcoins.chain.config.ChainAppConfig +import org.bitcoins.core.api.ChainQueryApi.FilterResponse import org.bitcoins.core.bloom.BloomFilter -import org.bitcoins.core.crypto.DoubleSha256DigestBE -import org.bitcoins.core.gcs.GolombFilter import org.bitcoins.core.protocol.transaction.Transaction import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp} import org.bitcoins.node.config.NodeAppConfig @@ -97,6 +96,6 @@ case class SpvNode( override def getFiltersBetweenHeights( startHeight: Int, - endHeight: Int): Future[Vector[(GolombFilter, DoubleSha256DigestBE)]] = + endHeight: Int): Future[Vector[FilterResponse]] = Future.failed(new RuntimeException(cfErrMsg)) } diff --git a/testkit/src/main/resources/reference.conf b/testkit/src/main/resources/reference.conf index 566acb5360..00749ce651 100644 --- a/testkit/src/main/resources/reference.conf +++ b/testkit/src/main/resources/reference.conf @@ -53,6 +53,10 @@ bitcoin-s { defaultAccountType = legacy # legacy, segwit, nested-segwit bloomFalsePositiveRate = 0.0001 # percentage + + addressGapLimit = 20 + + discoveryBatchSize = 100 } } 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 dc22824684..f075945599 100644 --- a/testkit/src/main/scala/org/bitcoins/testkit/wallet/BitcoinSWalletTest.scala +++ b/testkit/src/main/scala/org/bitcoins/testkit/wallet/BitcoinSWalletTest.scala @@ -2,14 +2,14 @@ package org.bitcoins.testkit.wallet import akka.actor.ActorSystem import com.typesafe.config.{Config, ConfigFactory} +import org.bitcoins.core.api.ChainQueryApi.FilterResponse import org.bitcoins.core.api.{ChainQueryApi, NodeApi} -import org.bitcoins.core.crypto.DoubleSha256DigestBE +import org.bitcoins.core.crypto.{DoubleSha256Digest, DoubleSha256DigestBE} import org.bitcoins.core.currency._ -import org.bitcoins.core.gcs.{BlockFilter, GolombFilter} +import org.bitcoins.core.gcs.BlockFilter import org.bitcoins.core.protocol.BlockStamp import org.bitcoins.core.util.FutureUtil import org.bitcoins.db.AppConfig -import org.bitcoins.keymanager.KeyManager import org.bitcoins.keymanager.bip39.BIP39KeyManager import org.bitcoins.rpc.client.common.{BitcoindRpcClient, BitcoindVersion} import org.bitcoins.server.BitcoinSAppConfig @@ -36,7 +36,7 @@ trait BitcoinSWalletTest extends BitcoinSFixture with WalletLogger { AppConfig.throwIfDefaultDatadir(config.walletConf) } - def nodeApi: NodeApi = NodeApi.NoOp + def nodeApi: NodeApi = MockNodeApi // This is a random block on testnet val testBlockHash = DoubleSha256DigestBE.fromHex( @@ -71,7 +71,7 @@ trait BitcoinSWalletTest extends BitcoinSFixture with WalletLogger { override def getFiltersBetweenHeights( startHeight: Int, - endHeight: Int): Future[Vector[(GolombFilter, DoubleSha256DigestBE)]] = + endHeight: Int): Future[Vector[FilterResponse]] = Future.successful({ import scodec.bits._ @@ -104,8 +104,10 @@ trait BitcoinSWalletTest extends BitcoinSFixture with WalletLogger { hex"c14d41b2d5aefaf539e989f7fa097eac657c70b975c56e26b73fb9401ce3" ++ hex"81502f0883d52c6a3bcc956e0ea1787f0717d0205fecfe55b01edb1ac0" Vector( - (BlockFilter.fromBytes(filterBytes, testBlockHash.flip), - testBlockHash)) + FilterResponse(compactFilter = BlockFilter + .fromBytes(filterBytes, testBlockHash.flip), + blockHash = testBlockHash, + blockHeight = 1)) }) } @@ -188,6 +190,37 @@ object BitcoinSWalletTest extends WalletLogger { lazy val initialFunds = 25.bitcoins + object MockNodeApi extends NodeApi { + + override def downloadBlocks( + blockHashes: Vector[DoubleSha256Digest]): Future[Unit] = FutureUtil.unit + + } + + object MockChainQueryApi extends ChainQueryApi { + + override def getBlockHeight( + blockHash: DoubleSha256DigestBE): Future[Option[Int]] = + FutureUtil.none + + override def getBestBlockHash(): Future[DoubleSha256DigestBE] = + Future.successful(DoubleSha256DigestBE.empty) + + override def getNumberOfConfirmations( + blockHashOpt: DoubleSha256DigestBE): Future[Option[Int]] = + FutureUtil.none + + override def getHeightByBlockStamp(blockStamp: BlockStamp): Future[Int] = + Future.successful(0) + + override def getFilterCount: Future[Int] = Future.successful(0) + + override def getFiltersBetweenHeights( + startHeight: Int, + endHeight: Int): Future[Vector[FilterResponse]] = + Future.successful(Vector.empty) + } + case class WalletWithBitcoind( wallet: UnlockedWalletApi, bitcoind: BitcoindRpcClient) diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/TrezorAddressTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/TrezorAddressTest.scala index f5156e1dd6..4575d00d2f 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/TrezorAddressTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/TrezorAddressTest.scala @@ -1,18 +1,33 @@ package org.bitcoins.wallet -import com.typesafe.config.{Config, ConfigFactory} -import org.bitcoins.core.api.{ChainQueryApi, NodeApi} import org.bitcoins.core.crypto.{ExtPublicKey, MnemonicCode} -import org.bitcoins.core.hd.HDChainType.{Change, External} -import org.bitcoins.core.hd._ + +import play.api.libs.json.JsValue +import play.api.libs.json.Json +import org.bitcoins.core.hd.HDCoinType +import org.bitcoins.core.hd.HDPurpose +import org.bitcoins.core.hd.HDPath import org.bitcoins.core.protocol.BitcoinAddress -import org.bitcoins.core.util.FutureUtil -import org.bitcoins.keymanager.bip39.BIP39KeyManager -import org.bitcoins.keymanager.{KeyManager, KeyManagerParams} import org.bitcoins.rpc.serializers.JsonSerializers._ +import play.api.libs.json.Reads +import play.api.libs.json.JsResult +import play.api.libs.json.JsError +import play.api.libs.json.JsSuccess +import org.bitcoins.core.hd.HDChainType +import org.bitcoins.core.hd.HDPurposes +import com.typesafe.config.Config +import com.typesafe.config.ConfigFactory + +import org.bitcoins.core.util.FutureUtil +import org.bitcoins.keymanager.KeyManagerParams +import org.bitcoins.keymanager.bip39.BIP39KeyManager import org.bitcoins.testkit.BitcoinSTestAppConfig import org.bitcoins.testkit.fixtures.EmptyFixture import org.bitcoins.testkit.wallet.BitcoinSWalletTest +import org.bitcoins.testkit.wallet.BitcoinSWalletTest.{ + MockChainQueryApi, + MockNodeApi +} import org.bitcoins.wallet.config.WalletAppConfig import org.bitcoins.wallet.models.{AccountDb, AddressDb} import org.scalatest.compatible.Assertion @@ -135,18 +150,21 @@ class TrezorAddressTest extends BitcoinSWalletTest with EmptyFixture { ConfigFactory.parseString(confStr) } - private def getWallet(config: WalletAppConfig)(implicit ec: ExecutionContext): Future[Wallet] = { - val kmE = BIP39KeyManager.initializeWithEntropy(mnemonic.toEntropy, config.kmParams) + private def getWallet(config: WalletAppConfig)( + implicit ec: ExecutionContext): Future[Wallet] = { + val kmE = + BIP39KeyManager.initializeWithEntropy(mnemonic.toEntropy, config.kmParams) kmE match { - case Left(err) => Future.failed(new RuntimeException(s"Failed to initialize km with err=${err}")) + case Left(err) => + Future.failed( + new RuntimeException(s"Failed to initialize km with err=${err}")) case Right(km) => - val wallet = Wallet(km, NodeApi.NoOp, ChainQueryApi.NoOp)(config, ec) - val walletF = Wallet.initialize(wallet)(config,ec) + val wallet = Wallet(km, MockNodeApi, MockChainQueryApi)(config, ec) + val walletF = Wallet.initialize(wallet)(config, ec) walletF } } - case class AccountAndAddrsAndVector( account: AccountDb, addrs: Seq[AddressDb], @@ -167,12 +185,13 @@ class TrezorAddressTest extends BitcoinSWalletTest with EmptyFixture { assert(foundAddress.address == expectedAddress.address) } } + /** Creates the wallet accounts needed for this test */ private def createNeededAccounts( - wallet: Wallet, - existing: Vector[AccountDb], - keyManagerParams: KeyManagerParams, - testVectors: Vector[TestVector]): Future[Unit] = { + wallet: Wallet, + existing: Vector[AccountDb], + keyManagerParams: KeyManagerParams, + testVectors: Vector[TestVector]): Future[Unit] = { val accountsToCreate = existing.length until testVectors.length FutureUtil .sequentially(accountsToCreate) { _ => @@ -187,16 +206,16 @@ class TrezorAddressTest extends BitcoinSWalletTest with EmptyFixture { * addresses needed to verify the test vector */ def getAccountsWithAddressesAndVectors( - wallet: Wallet, - accountsWithVectors: Seq[(AccountDb, TestVector)]): Future[ + wallet: Wallet, + accountsWithVectors: Seq[(AccountDb, TestVector)]): Future[ Seq[AccountAndAddrsAndVector]] = { FutureUtil.sequentially(accountsWithVectors) { case (acc, vec) => val addrFutures: Future[Seq[AddressDb]] = FutureUtil.sequentially(vec.addresses) { vector => val addrFut = vector.chain match { - case Change => wallet.getNewChangeAddress(acc) - case External => + case HDChainType.Change => wallet.getNewChangeAddress(acc) + case HDChainType.External => wallet.getNewAddress(acc) } addrFut.flatMap(wallet.addressDAO.findAddress).map { @@ -224,7 +243,10 @@ class TrezorAddressTest extends BitcoinSWalletTest with EmptyFixture { for { wallet <- getWallet(conf) existingAccounts <- wallet.listAccounts(purpose) - _ <- createNeededAccounts(wallet, existingAccounts, conf.kmParams, testVectors) + _ <- createNeededAccounts(wallet, + existingAccounts, + conf.kmParams, + testVectors) accounts <- wallet.listAccounts(purpose) // we want to find all accounts for the given account type, // and match it with its corresponding test vector @@ -256,7 +278,7 @@ class TrezorAddressTest extends BitcoinSWalletTest with EmptyFixture { val assertions: Seq[Assertion] = { val nestedAssertions: Seq[Seq[Assertion]] = accountsWithAddrsWithVecs.map { - case AccountAndAddrsAndVector(account, addresses, vec) => + case AccountAndAddrsAndVector(account, addresses, _) => val acctIdx = account.hdAccount.index val vec = vectors.find(_.xpub == account.xpub) match { case None => diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/WalletUnitTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/WalletUnitTest.scala index 8f4dffb6a0..be17eb2530 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/WalletUnitTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/WalletUnitTest.scala @@ -7,9 +7,10 @@ import org.bitcoins.core.hd.HDChainType.{Change, External} import org.bitcoins.core.hd.{HDChainType, HDPurpose} import org.bitcoins.core.protocol.BitcoinAddress import org.bitcoins.core.util.FutureUtil +import org.bitcoins.keymanager.KeyManagerUnlockError.MnemonicNotFound import org.bitcoins.keymanager.{KeyManagerUnlockError, WalletStorage} -import org.bitcoins.keymanager.KeyManagerUnlockError.{BadPassword, JsonParsingError, MnemonicNotFound} import org.bitcoins.testkit.wallet.BitcoinSWalletTest +import org.bitcoins.wallet.api.LockedWalletApi.BlockMatchingResponse import org.bitcoins.wallet.api.UnlockedWalletApi import org.bitcoins.wallet.models.AddressDb import org.scalatest.FutureOutcome @@ -26,17 +27,18 @@ class WalletUnitTest extends BitcoinSWalletTest { behavior of "Wallet - unit test" - it must "write the mnemonic seed to the root datadir -- NOT A NETWORK sub directory" in { wallet: UnlockedWalletApi => - //since datadir has the path that relates it to a network ('mainnet'/'testnet'/'regtest') - //we need to get the parent of that to find where the encrypted seed should be - //this is where the bitcoin-s.conf should live too. - val datadir = wallet.walletConfig.baseDatadir + it must "write the mnemonic seed to the root datadir -- NOT A NETWORK sub directory" in { + wallet: UnlockedWalletApi => + //since datadir has the path that relates it to a network ('mainnet'/'testnet'/'regtest') + //we need to get the parent of that to find where the encrypted seed should be + //this is where the bitcoin-s.conf should live too. + val datadir = wallet.walletConfig.baseDatadir - assert(Files.exists(datadir.resolve(WalletStorage.ENCRYPTED_SEED_FILE_NAME))) + assert( + Files.exists(datadir.resolve(WalletStorage.ENCRYPTED_SEED_FILE_NAME))) } - it should "create a new wallet" in { wallet: UnlockedWalletApi => for { accounts <- wallet.listAccounts() @@ -145,7 +147,7 @@ class WalletUnitTest extends BitcoinSWalletTest { wallet: UnlockedWalletApi => val badpassphrase = AesPassword.fromNonEmptyString("bad") val errorType = wallet.unlock(badpassphrase) match { - case Right(_) => fail("Unlocked wallet with bad password!") + case Right(_) => fail("Unlocked wallet with bad password!") case Left(err) => err } errorType match { @@ -165,7 +167,9 @@ class WalletUnitTest extends BitcoinSWalletTest { endOpt = None )(system.dispatcher) } yield { - assert(Vector(testBlockHash) == matched) + assert( + Vector(BlockMatchingResponse(blockHash = testBlockHash, + blockHeight = 1)) == matched) } } diff --git a/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala b/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala index 7d67acdecc..f0226ca38a 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala @@ -10,9 +10,9 @@ import org.bitcoins.core.protocol.transaction._ import org.bitcoins.core.wallet.builder.BitcoinTxBuilder import org.bitcoins.core.wallet.fee.FeeUnit import org.bitcoins.core.wallet.utxo.BitcoinUTXOSpendingInfo +import org.bitcoins.keymanager.KeyManagerParams import org.bitcoins.keymanager.bip39.BIP39KeyManager import org.bitcoins.keymanager.util.HDUtil -import org.bitcoins.keymanager.{KeyManager, KeyManagerParams} import org.bitcoins.wallet.api._ import org.bitcoins.wallet.config.WalletAppConfig import org.bitcoins.wallet.models._ 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 6fdf33389f..bc19878c9e 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/api/WalletApi.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/api/WalletApi.scala @@ -1,12 +1,10 @@ package org.bitcoins.wallet.api -import java.util.concurrent.Executors - -import org.bitcoins.core.api.ChainQueryApi.InvalidBlockRange +import org.bitcoins.core.api.ChainQueryApi.{FilterResponse, InvalidBlockRange} import org.bitcoins.core.api.{ChainQueryApi, NodeApi} import org.bitcoins.core.bloom.BloomFilter import org.bitcoins.core.config.NetworkParameters -import org.bitcoins.core.crypto._ +import org.bitcoins.core.crypto.{DoubleSha256DigestBE, _} import org.bitcoins.core.currency.CurrencyUnit import org.bitcoins.core.gcs.{GolombFilter, SimpleFilterMatcher} import org.bitcoins.core.hd.{AddressType, HDPurpose} @@ -18,6 +16,7 @@ import org.bitcoins.core.wallet.fee.FeeUnit import org.bitcoins.keymanager._ import org.bitcoins.keymanager.bip39.{BIP39KeyManager, BIP39LockedKeyManager} import org.bitcoins.wallet.Wallet +import org.bitcoins.wallet.api.LockedWalletApi.BlockMatchingResponse import org.bitcoins.wallet.config.WalletAppConfig import org.bitcoins.wallet.models.{AccountDb, AddressDb, SpendingInfoDb} @@ -121,6 +120,9 @@ trait LockedWalletApi extends WalletApi { def listAddresses(): Future[Vector[AddressDb]] + /** Checks if the wallet contains any data */ + def isEmpty(): Future[Boolean] + /** * Gets a new external address with the specified * type. Calling this method multiple @@ -208,10 +210,159 @@ trait LockedWalletApi extends WalletApi { def listAccounts(purpose: HDPurpose): Future[Vector[AccountDb]] = listAccounts().map(_.filter(_.hdAccount.purpose == purpose)) + /** + * Iterates over the block filters in order to find filters that match to the given addresses + * + * I queries the filter database for [[batchSize]] filters a time + * and tries to run [[GolombFilter.matchesAny]] for each filter. + * + * It tries to match the filters in parallel using [[parallelismLevel]] threads. + * For best results use it with a separate execution context. + * + * @param scripts list of [[ScriptPubKey]]'s to watch + * @param startOpt start point (if empty it starts with the genesis block) + * @param endOpt end point (if empty it ends with the best tip) + * @param batchSize number of filters that can be matched in one batch + * @param parallelismLevel max number of threads required to perform matching + * (default [[Runtime.getRuntime.availableProcessors()]]) + * @return a list of matching block hashes + */ + def getMatchingBlocks( + scripts: Vector[ScriptPubKey], + startOpt: Option[BlockStamp] = None, + endOpt: Option[BlockStamp] = None, + batchSize: Int = 100, + parallelismLevel: Int = Runtime.getRuntime.availableProcessors())( + implicit ec: ExecutionContext): Future[Vector[BlockMatchingResponse]] = { + require(batchSize > 0, "batch size must be greater than zero") + require(parallelismLevel > 0, "parallelism level must be greater than zero") + + if (scripts.isEmpty) { + Future.successful(Vector.empty) + } else { + val bytes = scripts.map(_.asmBytes) + + /** Calculates group size to split a filter vector into [[parallelismLevel]] groups. + * It's needed to limit number of threads required to run the matching */ + def calcGroupSize(vectorSize: Int): Int = + if (vectorSize / parallelismLevel * parallelismLevel < vectorSize) + vectorSize / parallelismLevel + 1 + else vectorSize / parallelismLevel + + def findMatches(filters: Vector[FilterResponse]): Future[ + Iterator[BlockMatchingResponse]] = { + if (filters.isEmpty) + Future.successful(Iterator.empty) + else { + /* Iterates over the grouped vector of filters to find matches with the given [[bytes]]. */ + val groupSize = calcGroupSize(filters.size) + val filterGroups = filters.grouped(groupSize) + // Sequence on the filter groups making sure the number of threads doesn't exceed [[parallelismLevel]]. + Future + .sequence(filterGroups.map { filterGroup => + // We need to wrap in a future here to make sure we can + // potentially run these matches in parallel + Future { + // Find any matches in the group and add the corresponding block hashes into the result + filterGroup + .foldLeft(Vector.empty[BlockMatchingResponse]) { + (blocks, filter) => + val matcher = SimpleFilterMatcher(filter.compactFilter) + if (matcher.matchesAny(bytes)) { + blocks :+ BlockMatchingResponse(filter.blockHash, + filter.blockHeight) + } else { + blocks + } + } + } + }) + .map(_.flatten) + } + } + + /** Iterates over all filters in the range to find matches */ + @tailrec + def loop( + start: Int, + end: Int, + acc: Future[Vector[BlockMatchingResponse]]): Future[ + Vector[BlockMatchingResponse]] = { + if (end <= start) { + acc + } else { + val startHeight = end - (batchSize - 1) + val endHeight = end + val newAcc = for { + compactFilterDbs <- chainQueryApi.getFiltersBetweenHeights( + startHeight, + endHeight) + filtered <- findMatches(compactFilterDbs) + res <- acc + } yield { + res ++ filtered + } + val newEnd = Math.max(start, endHeight - batchSize) + loop(start, newEnd, newAcc) + } + } + + for { + startHeight <- startOpt.fold(Future.successful(0))( + chainQueryApi.getHeightByBlockStamp) + _ = if (startHeight < 0) + throw InvalidBlockRange(s"Start position cannot negative") + endHeight <- endOpt.fold(chainQueryApi.getFilterCount)( + chainQueryApi.getHeightByBlockStamp) + _ = if (startHeight > endHeight) + throw InvalidBlockRange( + s"End position cannot precede start: $startHeight:$endHeight") + matched <- loop(startHeight, endHeight, Future.successful(Vector.empty)) + } yield { + matched + } + } + } + + /** + * Recreates the account using BIP-157 approach + * + * DANGER! This method removes all records from the wallet database + * and creates new ones while the account discovery process. + * + * The Wallet UI should check if the database is empty before calling + * this method and let the end users to decide whether they want to proceed or not. + * + * This method generates [[addressBatchSize]] of addresses, then matches them against the BIP-158 compact filters, + * and downloads and processes the matched blocks. This method keeps doing the steps until there are [[WalletConfig.addressGapLimit]] + * or more unused addresses in a row. In this case it considers the discovery process completed. + * + * [[addressBatchSize]] - the number of addresses we should generate from a keychain to attempt to match in in a rescan + * [[WalletConfig.addressGapLimit]] - the number of addresses required to go without a match before we determine that our wallet is "discovered". + * For instance, if addressBatchSize=100, and AddressGapLimit=20 we do a rescan and the last address we find containing + * funds is at index 75, we would not generate more addresses to try and rescan. However if the last index containing + * funds was 81, we would generate another 100 addresses from the keychain and attempt to rescan those. + * + * @param startOpt start block (if None it starts from the genesis block) + * @param endOpt end block (if None it ends at the current tip) + * @param addressBatchSize how many addresses to match in a single pass + */ + def rescanNeutrinoWallet( + startOpt: Option[BlockStamp], + endOpt: Option[BlockStamp], + addressBatchSize: Int): Future[Unit] + + /** + * Recreates the account using BIP-44 approach + */ + def rescanSPVWallet(): Future[Unit] + } trait UnlockedWalletApi extends LockedWalletApi { + def discoveryBatchSize(): Int = walletConfig.discoveryBatchSize + def keyManager: BIP39KeyManager /** @@ -258,140 +409,11 @@ trait UnlockedWalletApi extends LockedWalletApi { */ def createNewAccount(keyManagerParams: KeyManagerParams): Future[WalletApi] - /** - * Iterates over the block filters in order to find filters that match to the given addresses - * - * I queries the filter database for [[batchSize]] filters a time - * and tries to run [[GolombFilter.matchesAny]] for each filter. - * - * It tries to match the filters in parallel using [[parallelismLevel]] threads. - * For best results use it with a separate execution context. - * - * @param scripts list of [[ScriptPubKey]]'s to watch - * @param startOpt start point (if empty it starts with the genesis block) - * @param endOpt end point (if empty it ends with the best tip) - * @param batchSize number of filters that can be matched in one batch - * @param parallelismLevel max number of threads required to perform matching - * (default [[Runtime.getRuntime.availableProcessors()]]) - * @return a list of matching block hashes - */ - def getMatchingBlocks( - scripts: Vector[ScriptPubKey], - startOpt: Option[BlockStamp] = None, - endOpt: Option[BlockStamp] = None, - batchSize: Int = 100, - parallelismLevel: Int = Runtime.getRuntime.availableProcessors())( - implicit 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") +} - if (scripts.isEmpty) { - Future.successful(Vector.empty) - } else { - val bytes = scripts.map(_.asmBytes) - - /** Calculates group size to split a filter vector into [[parallelismLevel]] groups. - * It's needed to limit number of threads required to run the matching */ - def calcGroupSize(vectorSize: Int): Int = - if (vectorSize / parallelismLevel * parallelismLevel < vectorSize) - vectorSize / parallelismLevel + 1 - else vectorSize / parallelismLevel - - def findMatches( - filters: Vector[(GolombFilter, DoubleSha256DigestBE)]): Future[ - Iterator[DoubleSha256DigestBE]] = { - if (filters.isEmpty) - Future.successful(Iterator.empty) - else { - /* Iterates over the grouped vector of filters to find matches with the given [[bytes]]. */ - val groupSize = calcGroupSize(filters.size) - val filterGroups = filters.grouped(groupSize) - // Sequence on the filter groups making sure the number of threads doesn't exceed [[parallelismLevel]]. - Future - .sequence(filterGroups.map { filterGroup => - // We need to wrap in a future here to make sure we can - // potentially run these matches in parallel - Future { - // Find any matches in the group and add the corresponding block hashes into the result - filterGroup.foldLeft(Vector.empty[DoubleSha256DigestBE]) { - (blocks, filter) => - val matcher = new SimpleFilterMatcher(filter._1) - if (matcher.matchesAny(bytes)) { - blocks :+ filter._2 - } else { - blocks - } - } - } - }) - .map(_.flatten) - } - } - - /** Iterates over all filters in the range to find matches */ - @tailrec - def loop( - start: Int, - end: Int, - acc: Future[Vector[DoubleSha256DigestBE]]): Future[ - Vector[DoubleSha256DigestBE]] = { - if (end <= start) { - acc - } else { - val startHeight = end - (batchSize - 1) - val endHeight = end - val newAcc = for { - compactFilterDbs <- chainQueryApi.getFiltersBetweenHeights( - startHeight, - endHeight) - filtered <- findMatches(compactFilterDbs) - res <- acc - } yield { - res ++ filtered - } - val newEnd = Math.max(start, endHeight - batchSize) - loop(start, newEnd, newAcc) - } - } - - for { - startHeight <- startOpt.fold(Future.successful(0))( - chainQueryApi.getHeightByBlockStamp) - _ = if (startHeight < 0) - throw InvalidBlockRange(s"Start position cannot negative") - endHeight <- endOpt.fold(chainQueryApi.getFilterCount)( - chainQueryApi.getHeightByBlockStamp) - _ = if (startHeight > endHeight) - throw InvalidBlockRange( - s"End position cannot precede start: $startHeight:$endHeight") - matched <- loop(startHeight, endHeight, Future.successful(Vector.empty)) - } yield { - matched - } - } - } - - def rescan( - scriptPubKeys: Vector[ScriptPubKey], - startOpt: Option[BlockStamp] = None, - endOpt: Option[BlockStamp] = None): Future[Unit] = { - val threadPool = - Executors.newFixedThreadPool(Runtime.getRuntime.availableProcessors() * 2) - - val res = for { - blockHashes <- getMatchingBlocks(scriptPubKeys, startOpt, endOpt)( - ExecutionContext.fromExecutor(threadPool)) - res <- nodeApi.downloadBlocks(blockHashes.map(_.flip)) - } yield { - res - } - - res.onComplete(_ => threadPool.shutdown()) - -// res.failed.foreach(logger.error("Cannot rescan", _)) - - res - - } +object LockedWalletApi { + case class BlockMatchingResponse( + blockHash: DoubleSha256DigestBE, + blockHeight: Int) } diff --git a/wallet/src/main/scala/org/bitcoins/wallet/config/WalletAppConfig.scala b/wallet/src/main/scala/org/bitcoins/wallet/config/WalletAppConfig.scala index 2d86455d6d..913da4c131 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/config/WalletAppConfig.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/config/WalletAppConfig.scala @@ -52,6 +52,10 @@ case class WalletAppConfig( lazy val bloomFalsePositiveRate: Double = config.getDouble("wallet.bloomFalsePositiveRate") + lazy val addressGapLimit: Int = config.getInt("wallet.addressGapLimit") + + lazy val discoveryBatchSize: Int = config.getInt("wallet.discoveryBatchSize") + override def initialize()(implicit ec: ExecutionContext): Future[Unit] = { logger.debug(s"Initializing wallet setup") diff --git a/wallet/src/main/scala/org/bitcoins/wallet/internal/AddressHandling.scala b/wallet/src/main/scala/org/bitcoins/wallet/internal/AddressHandling.scala index ad469cbfe5..c08dc00144 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/internal/AddressHandling.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/internal/AddressHandling.scala @@ -1,27 +1,21 @@ package org.bitcoins.wallet.internal -import org.bitcoins.wallet._ -import scala.concurrent.Future -import org.bitcoins.wallet.models.AddressDb import org.bitcoins.core.crypto.ECPublicKey -import org.bitcoins.wallet.models.AccountDb -import org.bitcoins.core.hd.HDChainType -import org.bitcoins.core.protocol.BitcoinAddress -import org.bitcoins.core.hd.HDPath -import org.bitcoins.core.hd.HDAddress -import scala.util.Failure -import scala.util.Success -import org.bitcoins.wallet.models.AddressDbHelper -import org.bitcoins.core.hd.SegWitHDPath -import org.bitcoins.core.hd.LegacyHDPath -import org.bitcoins.core.hd.NestedSegWitHDPath -import org.bitcoins.wallet.api.AddressInfo -import org.bitcoins.core.protocol.transaction.Transaction -import org.bitcoins.core.protocol.transaction.TransactionOutput -import org.bitcoins.core.protocol.script.ScriptPubKey -import org.bitcoins.core.protocol.transaction.TransactionOutPoint +import org.bitcoins.core.hd._ import org.bitcoins.core.number.UInt32 -import org.bitcoins.core.hd.AddressType +import org.bitcoins.core.protocol.BitcoinAddress +import org.bitcoins.core.protocol.script.ScriptPubKey +import org.bitcoins.core.protocol.transaction.{ + Transaction, + TransactionOutPoint, + TransactionOutput +} +import org.bitcoins.wallet._ +import org.bitcoins.wallet.api.AddressInfo +import org.bitcoins.wallet.models.{AccountDb, AddressDb, AddressDbHelper} + +import scala.concurrent.Future +import scala.util.{Failure, Success} /** * Provides functionality related to addresses. This includes diff --git a/wallet/src/main/scala/org/bitcoins/wallet/internal/RescanHandling.scala b/wallet/src/main/scala/org/bitcoins/wallet/internal/RescanHandling.scala new file mode 100644 index 0000000000..3a324b4e78 --- /dev/null +++ b/wallet/src/main/scala/org/bitcoins/wallet/internal/RescanHandling.scala @@ -0,0 +1,156 @@ +package org.bitcoins.wallet.internal + +import java.util.concurrent.Executors + +import org.bitcoins.core.crypto.DoubleSha256Digest +import org.bitcoins.core.hd.HDChainType +import org.bitcoins.core.protocol.script.ScriptPubKey +import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp} +import org.bitcoins.core.util.FutureUtil +import org.bitcoins.wallet.{LockedWallet, WalletLogger} + +import scala.concurrent.{ExecutionContext, Future} + +private[wallet] trait RescanHandling extends WalletLogger { + self: LockedWallet => + + ///////////////////// + // Public facing API + + /** @inheritdoc */ + override def rescanNeutrinoWallet( + startOpt: Option[BlockStamp], + endOpt: Option[BlockStamp], + addressBatchSize: Int): Future[Unit] = { + + logger.info(s"Starting rescanning the wallet.") + + val res = for { + _ <- spendingInfoDAO.deleteAll() + _ <- addressDAO.deleteAll() + _ <- doNeutrinoRescan(startOpt, endOpt, addressBatchSize) + } yield () + + res.onComplete(_ => logger.info("Finished rescanning the wallet")) + + res + } + + /** @inheritdoc */ + override def rescanSPVWallet(): Future[Unit] = + Future.failed(new RuntimeException("Rescan not implemented for SPV wallet")) + + ///////////////////// + // Private methods + + private def doNeutrinoRescan( + startOpt: Option[BlockStamp], + endOpt: Option[BlockStamp], + addressBatchSize: Int): Future[Unit] = { + for { + scriptPubKeys <- generateScriptPubKeys(addressBatchSize) + blocks <- matchBlocks(scriptPubKeys, endOpt, startOpt) + _ <- downloadAndProcessBlocks(blocks) + externalGap <- calcAddressGap(HDChainType.External) + changeGap <- calcAddressGap(HDChainType.Change) + res <- if (externalGap >= walletConfig.addressGapLimit && changeGap >= walletConfig.addressGapLimit) + pruneUnusedAddresses() + else doNeutrinoRescan(startOpt, endOpt, addressBatchSize) + } yield res + } + + private def pruneUnusedAddresses(): Future[Unit] = { + for { + addressDbs <- addressDAO.findAll() + _ <- addressDbs.foldLeft(FutureUtil.unit) { (prevF, addressDb) => + for { + _ <- prevF + spendingInfoDbs <- spendingInfoDAO.findByScriptPubKey( + addressDb.scriptPubKey) + _ <- if (spendingInfoDbs.isEmpty) addressDAO.delete(addressDb) + else FutureUtil.unit + } yield () + } + } yield () + } + + private def calcAddressGap(chainType: HDChainType): Future[Int] = { + for { + addressDbs <- addressDAO.findAll() + addressGap <- addressDbs + //make sure all addressDb are of the correct chainType + //and they are sorted according to their index so we can + //calculate the gap accurately + .filter(_.path.chain.chainType == chainType) + .sortBy(_.path.address.index) + .foldLeft(Future.successful(0)) { (prevNF, addressDb) => + for { + prevN <- prevNF + spendingInfoDbs <- spendingInfoDAO.findByScriptPubKey( + addressDb.scriptPubKey) + } yield { + if (spendingInfoDbs.isEmpty) prevN + 1 else 0 + } + } + } yield { + logger.debug(s"Address gap: $addressGap") + addressGap + } + } + + private def downloadAndProcessBlocks( + blocks: Vector[DoubleSha256Digest]): Future[Unit] = { + logger.debug(s"Requesting ${blocks.size} block(s)") + blocks.foldLeft(FutureUtil.unit) { (prevF, blockHash) => + val completedF = subscribeForBlockProcessingCompletionSignal(blockHash) + for { + _ <- prevF + _ <- nodeApi.downloadBlocks(Vector(blockHash)) + _ <- completedF + } yield () + } + } + + private def matchBlocks( + scriptPubKeys: Vector[ScriptPubKey], + endOpt: Option[BlockStamp], + startOpt: Option[BlockStamp]): Future[Vector[DoubleSha256Digest]] = { + val threadPool = + Executors.newFixedThreadPool(Runtime.getRuntime.availableProcessors() * 2) + + val blocksF = for { + blocks <- getMatchingBlocks(scriptPubKeys, startOpt, endOpt)( + ExecutionContext.fromExecutor(threadPool)) + } yield { + blocks.sortBy(_.blockHeight).map(_.blockHash.flip) + } + + blocksF.onComplete(_ => threadPool.shutdown()) + blocksF + } + + private def generateScriptPubKeys( + count: Int): Future[Vector[ScriptPubKey]] = { + for { + addresses <- 1 + .to(count) + .foldLeft(Future.successful(Vector.empty[BitcoinAddress])) { + (prevFuture, _) => + for { + prev <- prevFuture + address <- getNewAddress() + } yield prev :+ address + } + changeAddresses <- 1 + .to(count) + .foldLeft(Future.successful(Vector.empty[BitcoinAddress])) { + (prevFuture, _) => + for { + prev <- prevFuture + address <- getNewChangeAddress() + } yield prev :+ address + } + } yield addresses.map(_.scriptPubKey) ++ changeAddresses.map(_.scriptPubKey) + } + +} 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 3bab0e6c82..a6908b2ccd 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/internal/TransactionProcessing.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/internal/TransactionProcessing.scala @@ -1,6 +1,6 @@ package org.bitcoins.wallet.internal -import org.bitcoins.core.crypto.DoubleSha256DigestBE +import org.bitcoins.core.crypto.{DoubleSha256Digest, DoubleSha256DigestBE} import org.bitcoins.core.number.UInt32 import org.bitcoins.core.protocol.blockchain.Block import org.bitcoins.core.protocol.transaction.{Transaction, TransactionOutput} @@ -9,7 +9,8 @@ import org.bitcoins.wallet._ import org.bitcoins.wallet.api.{AddUtxoError, AddUtxoSuccess} import org.bitcoins.wallet.models._ -import scala.concurrent.Future +import scala.concurrent.{Future, Promise} +import scala.util.{Failure, Success, Try} /** Provides functionality for processing transactions. This * includes importing UTXOs spent to our wallet, updating @@ -29,13 +30,12 @@ private[wallet] trait TransactionProcessing extends WalletLogger { for { result <- processTransactionImpl(transaction, blockHashOpt) } yield { - logger.info( + logger.debug( s"Finished processing of transaction=${transaction.txIdBE}. Relevant incomingTXOs=${result.updatedIncoming.length}, outgoingTXOs=${result.updatedOutgoing.length}") this } } - /** @inheritdoc */ override def processBlock(block: Block): Future[LockedWallet] = { logger.info(s"Processing block=${block.blockHeader.hash.flip}") val res = block.transactions.foldLeft(Future.successful(this)) { @@ -48,6 +48,8 @@ private[wallet] trait TransactionProcessing extends WalletLogger { newWallet } } + res.onComplete(failure => + signalBlockProcessingCompletion(block.blockHeader.hash, failure)) res.foreach( _ => logger.info( @@ -89,6 +91,36 @@ private[wallet] trait TransactionProcessing extends WalletLogger { ///////////////////// // Private methods + private var blockProcessingSignals = + Map.empty[DoubleSha256Digest, Promise[DoubleSha256Digest]] + + private[wallet] def subscribeForBlockProcessingCompletionSignal( + blockHash: DoubleSha256Digest): Future[DoubleSha256Digest] = + synchronized { + blockProcessingSignals.get(blockHash) match { + case Some(existingSignal) => existingSignal.future + case None => + val newSignal = Promise[DoubleSha256Digest]() + blockProcessingSignals = + blockProcessingSignals.updated(blockHash, newSignal) + newSignal.future + } + } + + private def signalBlockProcessingCompletion( + blockHash: DoubleSha256Digest, + failure: Try[_]): Unit = + synchronized { + blockProcessingSignals.get(blockHash).foreach { signal => + blockProcessingSignals = + blockProcessingSignals.filterNot(_._1 == blockHash) + failure match { + case Success(_) => signal.success(blockHash) + case Failure(exception) => signal.failure(exception) + } + } + } + /** Does the grunt work of processing a TX. * This is called by either the internal or public TX * processing method, which logs and transforms the @@ -98,7 +130,7 @@ private[wallet] trait TransactionProcessing extends WalletLogger { transaction: Transaction, blockHashOpt: Option[DoubleSha256DigestBE]): Future[ProcessTxResult] = { - logger.info( + logger.debug( s"Processing transaction=${transaction.txIdBE} with blockHash=$blockHashOpt") for { aggregate <- { diff --git a/wallet/src/main/scala/org/bitcoins/wallet/models/AddressDAO.scala b/wallet/src/main/scala/org/bitcoins/wallet/models/AddressDAO.scala index 6f07e3074c..75a58992ef 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/models/AddressDAO.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/models/AddressDAO.scala @@ -1,18 +1,17 @@ package org.bitcoins.wallet.models +import org.bitcoins.core.crypto.ECPublicKey +import org.bitcoins.core.hd.{HDChainType, HDPurpose} import org.bitcoins.core.protocol.BitcoinAddress +import org.bitcoins.core.protocol.script.ScriptPubKey import org.bitcoins.db.{CRUD, SlickUtil} +import org.bitcoins.wallet.config.WalletAppConfig import slick.dbio.Effect import slick.jdbc.SQLiteProfile.api._ import slick.lifted.TableQuery import slick.sql.SqlAction import scala.concurrent.{ExecutionContext, Future} -import org.bitcoins.core.hd.HDChainType -import org.bitcoins.wallet.config.WalletAppConfig -import org.bitcoins.core.crypto.ECPublicKey -import org.bitcoins.core.protocol.script.ScriptPubKey -import org.bitcoins.core.hd.HDPurpose case class AddressDAO()( implicit ec: ExecutionContext, diff --git a/wallet/src/main/scala/org/bitcoins/wallet/models/SpendingInfoDAO.scala b/wallet/src/main/scala/org/bitcoins/wallet/models/SpendingInfoDAO.scala index 3490b2dcca..92b71b8a0c 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/models/SpendingInfoDAO.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/models/SpendingInfoDAO.scala @@ -1,15 +1,17 @@ package org.bitcoins.wallet.models +import org.bitcoins.core.crypto.DoubleSha256DigestBE +import org.bitcoins.core.protocol.script.ScriptPubKey +import org.bitcoins.core.protocol.transaction.{ + Transaction, + TransactionOutPoint, + TransactionOutput +} import org.bitcoins.db.CRUDAutoInc import org.bitcoins.wallet.config._ import slick.jdbc.SQLiteProfile.api._ -import scala.concurrent.ExecutionContext -import scala.concurrent.Future -import org.bitcoins.core.protocol.transaction.Transaction -import org.bitcoins.core.crypto.DoubleSha256DigestBE -import org.bitcoins.core.protocol.transaction.TransactionOutput -import org.bitcoins.core.protocol.transaction.TransactionOutPoint +import scala.concurrent.{ExecutionContext, Future} case class SpendingInfoDAO()( implicit val ec: ExecutionContext, @@ -91,6 +93,12 @@ case class SpendingInfoDAO()( database.runVec(filtered.result) } + def findByScriptPubKey( + scriptPubKey: ScriptPubKey): Future[Vector[SpendingInfoDb]] = { + val filtered = table.filter(_.scriptPubKey === scriptPubKey) + database.runVec(filtered.result) + } + /** Enumerates all unspent TX outputs in the wallet */ def findAllUnspent(): Future[Vector[SpendingInfoDb]] = { val query = table.filter(!_.spent) @@ -103,4 +111,5 @@ case class SpendingInfoDAO()( val query = table.map(_.outPoint) database.runVec(query.result).map(_.toVector) } + }