mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-02-22 14:33:06 +01:00
Add wallet creation time for rescans (#1353)
* Add wallet creation time for rescans * Fix docs * Clean up and add test * Remove account bday * Fix compile issue and docs * Add more chain handler tests * Use Instant over Long, add docs * Fix docs
This commit is contained in:
parent
3c008ff82b
commit
6d7685b76e
33 changed files with 467 additions and 147 deletions
|
@ -53,7 +53,8 @@ object ConsoleCli {
|
|||
command = Rescan(addressBatchSize = Option.empty,
|
||||
startBlock = Option.empty,
|
||||
endBlock = Option.empty,
|
||||
force = false)))
|
||||
force = false,
|
||||
ignoreCreationTime = false)))
|
||||
.text(s"Rescan for wallet UTXOs")
|
||||
.children(
|
||||
opt[Unit]("force")
|
||||
|
@ -80,7 +81,11 @@ object ConsoleCli {
|
|||
.action((start, conf) =>
|
||||
conf.copy(command = conf.command match {
|
||||
case rescan: Rescan =>
|
||||
rescan.copy(startBlock = Option(start))
|
||||
// Need to ignoreCreationTime so we try to call
|
||||
// rescan with rescanNeutrinoWallet with a block
|
||||
// and a creation time
|
||||
rescan.copy(startBlock = Option(start),
|
||||
ignoreCreationTime = true)
|
||||
case other => other
|
||||
})),
|
||||
opt[BlockStamp]("end")
|
||||
|
@ -91,6 +96,15 @@ object ConsoleCli {
|
|||
case rescan: Rescan =>
|
||||
rescan.copy(endBlock = Option(end))
|
||||
case other => other
|
||||
})),
|
||||
opt[Unit]("ignorecreationtime")
|
||||
.text("Ignores the wallet creation date and will instead do a full rescan")
|
||||
.optional()
|
||||
.action((_, conf) =>
|
||||
conf.copy(command = conf.command match {
|
||||
case rescan: Rescan =>
|
||||
rescan.copy(ignoreCreationTime = true)
|
||||
case other => other
|
||||
}))
|
||||
),
|
||||
cmd("isempty")
|
||||
|
@ -348,12 +362,17 @@ object ConsoleCli {
|
|||
RequestParam("getaddressinfo", Seq(up.writeJs(address)))
|
||||
case GetNewAddress =>
|
||||
RequestParam("getnewaddress")
|
||||
case Rescan(addressBatchSize, startBlock, endBlock, force) =>
|
||||
case Rescan(addressBatchSize,
|
||||
startBlock,
|
||||
endBlock,
|
||||
force,
|
||||
ignoreCreationTime) =>
|
||||
RequestParam("rescan",
|
||||
Seq(up.writeJs(addressBatchSize),
|
||||
up.writeJs(startBlock),
|
||||
up.writeJs(endBlock),
|
||||
up.writeJs(force)))
|
||||
up.writeJs(force),
|
||||
up.writeJs(ignoreCreationTime)))
|
||||
|
||||
case SendToAddress(address, bitcoins, satoshisPerVirtualByte) =>
|
||||
RequestParam("sendtoaddress",
|
||||
|
@ -505,7 +524,8 @@ object CliCommand {
|
|||
addressBatchSize: Option[Int],
|
||||
startBlock: Option[BlockStamp],
|
||||
endBlock: Option[BlockStamp],
|
||||
force: Boolean)
|
||||
force: Boolean,
|
||||
ignoreCreationTime: Boolean)
|
||||
extends CliCommand
|
||||
|
||||
// PSBT
|
||||
|
|
|
@ -363,8 +363,9 @@ class RoutesSpec
|
|||
.get
|
||||
|
||||
val accountDb =
|
||||
AccountDb(xpub,
|
||||
HDAccount(HDCoin(HDPurposes.Legacy, HDCoinType.Testnet), 0))
|
||||
AccountDb(xpub = xpub,
|
||||
hdAccount =
|
||||
HDAccount(HDCoin(HDPurposes.Legacy, HDCoinType.Testnet), 0))
|
||||
|
||||
(mockWalletApi.listAccounts: () => Future[Vector[AccountDb]])
|
||||
.expects()
|
||||
|
@ -499,13 +500,14 @@ class RoutesSpec
|
|||
(mockWalletApi
|
||||
.rescanNeutrinoWallet(_: Option[BlockStamp],
|
||||
_: Option[BlockStamp],
|
||||
_: Int))
|
||||
.expects(None, None, 100)
|
||||
_: Int,
|
||||
_: Boolean))
|
||||
.expects(None, None, 100, false)
|
||||
.returning(FutureUtil.unit)
|
||||
|
||||
val route1 =
|
||||
walletRoutes.handleCommand(
|
||||
ServerCommand("rescan", Arr(Arr(), Null, Null, true)))
|
||||
ServerCommand("rescan", Arr(Arr(), Null, Null, true, true)))
|
||||
|
||||
Post() ~> route1 ~> check {
|
||||
contentType shouldEqual `application/json`
|
||||
|
@ -518,18 +520,21 @@ class RoutesSpec
|
|||
(mockWalletApi
|
||||
.rescanNeutrinoWallet(_: Option[BlockStamp],
|
||||
_: Option[BlockStamp],
|
||||
_: Int))
|
||||
_: Int,
|
||||
_: Boolean))
|
||||
.expects(
|
||||
Some(BlockTime(
|
||||
ZonedDateTime.of(2018, 10, 27, 12, 34, 56, 0, ZoneId.of("UTC")))),
|
||||
None,
|
||||
100)
|
||||
100,
|
||||
false)
|
||||
.returning(FutureUtil.unit)
|
||||
|
||||
val route2 =
|
||||
walletRoutes.handleCommand(
|
||||
ServerCommand("rescan",
|
||||
Arr(Arr(), Str("2018-10-27T12:34:56Z"), Null, true)))
|
||||
ServerCommand(
|
||||
"rescan",
|
||||
Arr(Arr(), Str("2018-10-27T12:34:56Z"), Null, true, true)))
|
||||
|
||||
Post() ~> route2 ~> check {
|
||||
contentType shouldEqual `application/json`
|
||||
|
@ -542,15 +547,16 @@ class RoutesSpec
|
|||
(mockWalletApi
|
||||
.rescanNeutrinoWallet(_: Option[BlockStamp],
|
||||
_: Option[BlockStamp],
|
||||
_: Int))
|
||||
.expects(None, Some(BlockHash(DoubleSha256DigestBE.empty)), 100)
|
||||
_: Int,
|
||||
_: Boolean))
|
||||
.expects(None, Some(BlockHash(DoubleSha256DigestBE.empty)), 100, false)
|
||||
.returning(FutureUtil.unit)
|
||||
|
||||
val route3 =
|
||||
walletRoutes.handleCommand(
|
||||
ServerCommand(
|
||||
"rescan",
|
||||
Arr(Null, Null, Str(DoubleSha256DigestBE.empty.hex), true)))
|
||||
Arr(Null, Null, Str(DoubleSha256DigestBE.empty.hex), true, true)))
|
||||
|
||||
Post() ~> route3 ~> check {
|
||||
contentType shouldEqual `application/json`
|
||||
|
@ -563,13 +569,15 @@ class RoutesSpec
|
|||
(mockWalletApi
|
||||
.rescanNeutrinoWallet(_: Option[BlockStamp],
|
||||
_: Option[BlockStamp],
|
||||
_: Int))
|
||||
.expects(Some(BlockHeight(12345)), Some(BlockHeight(67890)), 100)
|
||||
_: Int,
|
||||
_: Boolean))
|
||||
.expects(Some(BlockHeight(12345)), Some(BlockHeight(67890)), 100, false)
|
||||
.returning(FutureUtil.unit)
|
||||
|
||||
val route4 =
|
||||
walletRoutes.handleCommand(
|
||||
ServerCommand("rescan", Arr(Arr(), Str("12345"), Num(67890), true)))
|
||||
ServerCommand("rescan",
|
||||
Arr(Arr(), Str("12345"), Num(67890), true, true)))
|
||||
|
||||
Post() ~> route4 ~> check {
|
||||
contentType shouldEqual `application/json`
|
||||
|
@ -580,7 +588,8 @@ class RoutesSpec
|
|||
|
||||
val route5 =
|
||||
walletRoutes.handleCommand(
|
||||
ServerCommand("rescan", Arr(Null, Str("abcd"), Str("efgh"), true)))
|
||||
ServerCommand("rescan",
|
||||
Arr(Null, Str("abcd"), Str("efgh"), true, true)))
|
||||
|
||||
Post() ~> route5 ~> check {
|
||||
rejection shouldEqual ValidationRejection(
|
||||
|
@ -590,8 +599,9 @@ class RoutesSpec
|
|||
|
||||
val route6 =
|
||||
walletRoutes.handleCommand(
|
||||
ServerCommand("rescan",
|
||||
Arr(Arr(55), Null, Str("2018-10-27T12:34:56"), true)))
|
||||
ServerCommand(
|
||||
"rescan",
|
||||
Arr(Arr(55), Null, Str("2018-10-27T12:34:56"), true, true)))
|
||||
|
||||
Post() ~> route6 ~> check {
|
||||
rejection shouldEqual ValidationRejection(
|
||||
|
@ -601,7 +611,7 @@ class RoutesSpec
|
|||
|
||||
val route7 =
|
||||
walletRoutes.handleCommand(
|
||||
ServerCommand("rescan", Arr(Null, Num(-1), Null, true)))
|
||||
ServerCommand("rescan", Arr(Null, Num(-1), Null, true, false)))
|
||||
|
||||
Post() ~> route7 ~> check {
|
||||
rejection shouldEqual ValidationRejection(
|
||||
|
@ -615,13 +625,15 @@ class RoutesSpec
|
|||
(mockWalletApi
|
||||
.rescanNeutrinoWallet(_: Option[BlockStamp],
|
||||
_: Option[BlockStamp],
|
||||
_: Int))
|
||||
.expects(None, None, 55)
|
||||
_: Int,
|
||||
_: Boolean))
|
||||
.expects(None, None, 55, false)
|
||||
.returning(FutureUtil.unit)
|
||||
|
||||
val route8 =
|
||||
walletRoutes.handleCommand(
|
||||
ServerCommand("rescan", Arr(Arr(55), Arr(), Arr(), Bool(true))))
|
||||
ServerCommand("rescan",
|
||||
Arr(Arr(55), Arr(), Arr(), Bool(true), Bool(true))))
|
||||
|
||||
Post() ~> route8 ~> check {
|
||||
contentType shouldEqual `application/json`
|
||||
|
|
|
@ -125,7 +125,7 @@ object Main extends App {
|
|||
bip39PasswordOpt,
|
||||
walletConf.kmParams) match {
|
||||
case Right(km) =>
|
||||
val wallet = Wallet(km, nodeApi, chainQueryApi)
|
||||
val wallet = Wallet(km, nodeApi, chainQueryApi, km.creationTime)
|
||||
Future.successful(wallet)
|
||||
case Left(err) =>
|
||||
error(err)
|
||||
|
@ -144,7 +144,8 @@ object Main extends App {
|
|||
}
|
||||
|
||||
logger.info(s"Creating new wallet")
|
||||
val unInitializedWallet = Wallet(keyManager, nodeApi, chainQueryApi)
|
||||
val unInitializedWallet =
|
||||
Wallet(keyManager, nodeApi, chainQueryApi, keyManager.creationTime)
|
||||
|
||||
Wallet.initialize(wallet = unInitializedWallet,
|
||||
bip39PasswordOpt = bip39PasswordOpt)
|
||||
|
|
|
@ -143,7 +143,8 @@ case class Rescan(
|
|||
batchSize: Option[Int],
|
||||
startBlock: Option[BlockStamp],
|
||||
endBlock: Option[BlockStamp],
|
||||
force: Boolean)
|
||||
force: Boolean,
|
||||
ignoreCreationTime: Boolean)
|
||||
|
||||
object Rescan extends ServerJsonModels {
|
||||
|
||||
|
@ -175,16 +176,18 @@ object Rescan extends ServerJsonModels {
|
|||
}
|
||||
|
||||
jsArr.arr.toList match {
|
||||
case batchSizeJs :: startJs :: endJs :: forceJs :: Nil =>
|
||||
case batchSizeJs :: startJs :: endJs :: forceJs :: ignoreCreationTimeJs :: Nil =>
|
||||
Try {
|
||||
val batchSize = parseInt(batchSizeJs)
|
||||
val start = parseBlockStamp(startJs)
|
||||
val end = parseBlockStamp(endJs)
|
||||
val force = parseBoolean(forceJs)
|
||||
val ignoreCreationTime = parseBoolean(ignoreCreationTimeJs)
|
||||
Rescan(batchSize = batchSize,
|
||||
startBlock = start,
|
||||
endBlock = end,
|
||||
force = force)
|
||||
force = force,
|
||||
ignoreCreationTime = ignoreCreationTime)
|
||||
}
|
||||
case Nil =>
|
||||
Failure(new IllegalArgumentException("Missing addresses"))
|
||||
|
|
|
@ -111,16 +111,22 @@ case class WalletRoutes(wallet: WalletApi, node: Node)(
|
|||
Rescan.fromJsArr(arr) match {
|
||||
case Failure(exception) =>
|
||||
reject(ValidationRejection("failure", Some(exception)))
|
||||
case Success(Rescan(batchSize, startBlock, endBlock, force)) =>
|
||||
case Success(
|
||||
Rescan(batchSize,
|
||||
startBlock,
|
||||
endBlock,
|
||||
force,
|
||||
ignoreCreationTime)) =>
|
||||
complete {
|
||||
val res = for {
|
||||
empty <- wallet.isEmpty()
|
||||
msg <- if (force || empty) {
|
||||
wallet
|
||||
.rescanNeutrinoWallet(
|
||||
startBlock,
|
||||
endBlock,
|
||||
batchSize.getOrElse(wallet.discoveryBatchSize))
|
||||
.rescanNeutrinoWallet(startOpt = startBlock,
|
||||
endOpt = endBlock,
|
||||
addressBatchSize = batchSize.getOrElse(
|
||||
wallet.discoveryBatchSize),
|
||||
useCreationTime = !ignoreCreationTime)
|
||||
.map(_ => "scheduled")
|
||||
} else {
|
||||
Future.successful(
|
||||
|
|
|
@ -3,22 +3,18 @@ package org.bitcoins.chain.blockchain
|
|||
import akka.actor.ActorSystem
|
||||
import org.bitcoins.chain.api.ChainApi
|
||||
import org.bitcoins.chain.config.ChainAppConfig
|
||||
import org.bitcoins.chain.models.{
|
||||
BlockHeaderDb,
|
||||
BlockHeaderDbHelper,
|
||||
CompactFilterDb
|
||||
}
|
||||
import org.bitcoins.chain.models.{BlockHeaderDb, BlockHeaderDbHelper}
|
||||
import org.bitcoins.core.crypto.{
|
||||
DoubleSha256Digest,
|
||||
DoubleSha256DigestBE,
|
||||
ECPrivateKey
|
||||
}
|
||||
import org.bitcoins.core.gcs.{BlockFilter, FilterHeader, FilterType}
|
||||
import org.bitcoins.core.gcs.{BlockFilter, FilterHeader}
|
||||
import org.bitcoins.core.number.{Int32, UInt32}
|
||||
import org.bitcoins.core.p2p.CompactFilterMessage
|
||||
import org.bitcoins.core.protocol.BlockStamp
|
||||
import org.bitcoins.core.protocol.blockchain.BlockHeader
|
||||
import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp}
|
||||
import org.bitcoins.core.util.CryptoUtil
|
||||
import org.bitcoins.core.util.TimeUtil
|
||||
import org.bitcoins.testkit.BitcoinSTestAppConfig
|
||||
import org.bitcoins.testkit.chain.fixture.ChainFixtureTag
|
||||
import org.bitcoins.testkit.chain.{
|
||||
|
@ -31,6 +27,7 @@ import org.scalatest.{Assertion, FutureOutcome}
|
|||
import play.api.libs.json.Json
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.io.BufferedSource
|
||||
|
||||
class ChainHandlerTest extends ChainUnitTest {
|
||||
|
||||
|
@ -47,8 +44,8 @@ class ChainHandlerTest extends ChainUnitTest {
|
|||
mainnetAppConfig.withOverrides(memoryDb)
|
||||
}
|
||||
|
||||
val source = FileUtil.getFileAsSource("block_headers.json")
|
||||
val arrStr = source.getLines.next
|
||||
val source: BufferedSource = FileUtil.getFileAsSource("block_headers.json")
|
||||
val arrStr: String = source.getLines.next
|
||||
source.close()
|
||||
|
||||
import org.bitcoins.commons.serializers.JsonReaders.BlockHeaderReads
|
||||
|
@ -61,9 +58,19 @@ class ChainHandlerTest extends ChainUnitTest {
|
|||
override def withFixture(test: OneArgAsyncTest): FutureOutcome =
|
||||
withChainHandler(test)
|
||||
|
||||
val genesis = ChainUnitTest.genesisHeaderDb
|
||||
val genesis: BlockHeaderDb = ChainUnitTest.genesisHeaderDb
|
||||
behavior of "ChainHandler"
|
||||
|
||||
val nextBlockHeader: BlockHeader =
|
||||
BlockHeader(
|
||||
version = Int32(1),
|
||||
previousBlockHash = ChainUnitTest.genesisHeaderDb.hashBE.flip,
|
||||
merkleRootHash = DoubleSha256Digest.empty,
|
||||
time = UInt32(1231006505),
|
||||
nBits = UInt32(545259519),
|
||||
nonce = UInt32(2083236893)
|
||||
)
|
||||
|
||||
it must "process a new valid block header, and then be able to fetch that header" in {
|
||||
chainHandler: ChainHandler =>
|
||||
val newValidHeader =
|
||||
|
@ -322,17 +329,8 @@ class ChainHandlerTest extends ChainUnitTest {
|
|||
|
||||
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 {
|
||||
_ <- chainHandler.processHeader(blockHeader)
|
||||
_ <- chainHandler.processHeader(nextBlockHeader)
|
||||
blockHashBE <- chainHandler.getHeadersAtHeight(1).map(_.head.hashBE)
|
||||
golombFilter = BlockFilter.fromHex("017fa880", blockHashBE.flip)
|
||||
firstFilter = CompactFilterMessage(blockHash = blockHashBE.flip,
|
||||
|
@ -458,6 +456,16 @@ class ChainHandlerTest extends ChainUnitTest {
|
|||
}
|
||||
}
|
||||
|
||||
it must "get the correct height from an epoch second" in {
|
||||
chainHandler: ChainHandler =>
|
||||
for {
|
||||
height <- chainHandler.epochSecondToBlockHeight(
|
||||
TimeUtil.currentEpochSecond)
|
||||
} yield {
|
||||
assert(height == 0)
|
||||
}
|
||||
}
|
||||
|
||||
final def processHeaders(
|
||||
processorF: Future[ChainApi],
|
||||
headers: Vector[BlockHeader],
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
package org.bitcoins.chain.models
|
||||
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import org.bitcoins.core.crypto.DoubleSha256DigestBE
|
||||
import org.bitcoins.core.number.UInt32
|
||||
import org.bitcoins.core.util.TimeUtil
|
||||
import org.bitcoins.testkit.chain.{BlockHeaderHelper, ChainUnitTest}
|
||||
import org.scalatest.FutureOutcome
|
||||
|
||||
|
@ -61,6 +65,56 @@ class BlockHeaderDAOTest extends ChainUnitTest {
|
|||
|
||||
}
|
||||
|
||||
it must "fail to find a header closest to a epoch second" in {
|
||||
blockHeaderDAO: BlockHeaderDAO =>
|
||||
val blockHeader = BlockHeaderHelper.buildNextHeader(genesisHeaderDb)
|
||||
val createdF = blockHeaderDAO.create(blockHeader)
|
||||
|
||||
val headerDbF =
|
||||
createdF.flatMap(_ => blockHeaderDAO.findClosestToTime(UInt32.zero))
|
||||
|
||||
recoverToSucceededIf[IllegalArgumentException](headerDbF)
|
||||
}
|
||||
|
||||
it must "find the closest block to the given epoch second" in {
|
||||
blockHeaderDAO: BlockHeaderDAO =>
|
||||
val blockHeader = BlockHeaderHelper.buildNextHeader(genesisHeaderDb)
|
||||
val createdF = blockHeaderDAO.create(blockHeader)
|
||||
|
||||
val headerDbF = createdF.flatMap(_ =>
|
||||
blockHeaderDAO.findClosestToTime(UInt32(TimeUtil.currentEpochSecond)))
|
||||
|
||||
headerDbF.map { headerDb =>
|
||||
assert(headerDb == blockHeader)
|
||||
}
|
||||
}
|
||||
|
||||
it must "find the block at given epoch second" in {
|
||||
blockHeaderDAO: BlockHeaderDAO =>
|
||||
val blockHeader = BlockHeaderHelper.buildNextHeader(genesisHeaderDb)
|
||||
val createdF = blockHeaderDAO.create(blockHeader)
|
||||
|
||||
val headerDbF = createdF.flatMap(_ =>
|
||||
blockHeaderDAO.findClosestToTime(blockHeader.time))
|
||||
|
||||
headerDbF.map { headerDb =>
|
||||
assert(headerDb == blockHeader)
|
||||
}
|
||||
}
|
||||
|
||||
it must "find all the headers before to the given epoch second" in {
|
||||
blockHeaderDAO: BlockHeaderDAO =>
|
||||
val blockHeader = BlockHeaderHelper.buildNextHeader(genesisHeaderDb)
|
||||
val createdF = blockHeaderDAO.create(blockHeader)
|
||||
|
||||
val headerDbsF = createdF.flatMap(_ =>
|
||||
blockHeaderDAO.findAllBeforeTime(UInt32(TimeUtil.currentEpochSecond)))
|
||||
|
||||
headerDbsF.map { headerDbs =>
|
||||
assert(headerDbs.size == 2)
|
||||
}
|
||||
}
|
||||
|
||||
it must "retrieve the chain tip saved in the database" in {
|
||||
blockHeaderDAO: BlockHeaderDAO =>
|
||||
val blockHeader = BlockHeaderHelper.buildNextHeader(genesisHeaderDb)
|
||||
|
|
|
@ -7,6 +7,7 @@ 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
|
||||
import org.bitcoins.core.number.UInt32
|
||||
import org.bitcoins.core.p2p.CompactFilterMessage
|
||||
import org.bitcoins.core.protocol.BlockStamp
|
||||
import org.bitcoins.core.protocol.blockchain.BlockHeader
|
||||
|
@ -418,6 +419,9 @@ case class ChainHandler(
|
|||
Future.failed(new RuntimeException(s"Not implemented: $blockTime"))
|
||||
}
|
||||
|
||||
override def epochSecondToBlockHeight(time: Long): Future[Int] =
|
||||
blockHeaderDAO.findClosestToTime(time = UInt32(time)).map(_.height)
|
||||
|
||||
/** @inheritdoc */
|
||||
override def getBlockHeight(
|
||||
blockHash: DoubleSha256DigestBE): Future[Option[Int]] =
|
||||
|
|
|
@ -185,6 +185,28 @@ case class BlockHeaderDAO()(
|
|||
table.filter(header => header.height >= from && header.height <= to).result
|
||||
}
|
||||
|
||||
def findAllBeforeTime(time: UInt32): Future[Vector[BlockHeaderDb]] = {
|
||||
val query = table.filter(_.time < time)
|
||||
|
||||
database.run(query.result).map(_.toVector)
|
||||
}
|
||||
|
||||
def findClosestToTime(time: UInt32): Future[BlockHeaderDb] = {
|
||||
require(time >= UInt32(1231006505),
|
||||
s"Time must be after the genesis block (1231006505), got $time")
|
||||
|
||||
val query = table.filter(_.time === time)
|
||||
|
||||
val opt = database.run(query.result).map(_.headOption)
|
||||
|
||||
opt.flatMap {
|
||||
case None =>
|
||||
findAllBeforeTime(time).map(_.maxBy(_.time))
|
||||
case Some(header) =>
|
||||
Future.successful(header)
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the maximum block height from our database */
|
||||
def maxHeight: Future[Int] = {
|
||||
val query = maxHeightQuery
|
||||
|
|
|
@ -41,6 +41,9 @@ trait ChainQueryApi {
|
|||
startHeight: Int,
|
||||
endHeight: Int): Future[Vector[FilterResponse]]
|
||||
|
||||
/** Gets the block height of the closest block to the given time */
|
||||
def epochSecondToBlockHeight(time: Long): Future[Int]
|
||||
|
||||
}
|
||||
|
||||
object ChainQueryApi {
|
||||
|
|
16
core/src/main/scala/org/bitcoins/core/util/TimeUtil.scala
Normal file
16
core/src/main/scala/org/bitcoins/core/util/TimeUtil.scala
Normal file
|
@ -0,0 +1,16 @@
|
|||
package org.bitcoins.core.util
|
||||
|
||||
import java.time.Instant
|
||||
|
||||
object TimeUtil {
|
||||
|
||||
def now: Instant = Instant.now
|
||||
|
||||
/** Returns the current timestamp in milliseconds */
|
||||
def currentEpochMs: Long = now.toEpochMilli
|
||||
|
||||
/** Returns the current timestamp in seconds */
|
||||
def currentEpochSecond: Long = {
|
||||
now.getEpochSecond
|
||||
}
|
||||
}
|
|
@ -64,6 +64,8 @@ This controls how the root key is defined. The combination of `purpose` and `net
|
|||
|
||||
Now we can construct a native segwit key manager for the regtest network!
|
||||
```scala mdoc:invisible
|
||||
import java.time.Instant
|
||||
|
||||
import org.bitcoins.core.crypto._
|
||||
|
||||
import org.bitcoins.core.config._
|
||||
|
@ -114,7 +116,7 @@ again after initializing it once. You can use the same `mnemonic` for different
|
|||
val mainnetKmParams = KeyManagerParams(seedPath, HDPurposes.SegWit, MainNet)
|
||||
|
||||
//we do not need to all `initializeWithMnemonic()` again as we have saved the seed to dis
|
||||
val mainnetKeyManager = BIP39KeyManager(mnemonic, mainnetKmParams, None)
|
||||
val mainnetKeyManager = BIP39KeyManager(mnemonic, mainnetKmParams, None, Instant.now)
|
||||
|
||||
val mainnetXpub = mainnetKeyManager.getRootXPub
|
||||
|
||||
|
|
|
@ -22,6 +22,8 @@ import org.bitcoins.testkit.wallet.BitcoinSWalletTest
|
|||
import org.bitcoins.wallet.Wallet
|
||||
import org.bitcoins.wallet.config.WalletAppConfig
|
||||
|
||||
import java.time.Instant
|
||||
|
||||
import scala.concurrent.{ExecutionContextExecutor, Future}
|
||||
```
|
||||
|
||||
|
@ -128,6 +130,9 @@ val exampleCallbacks =
|
|||
// but for the examples sake we will keep it small.
|
||||
val chainApi = new ChainQueryApi {
|
||||
|
||||
override def epochSecondToBlockHeight(time: Long): Future[Int] =
|
||||
Future.successful(0)
|
||||
|
||||
/** Gets the height of the given block */
|
||||
override def getBlockHeight(
|
||||
blockHash: DoubleSha256DigestBE): Future[Option[Int]] = {
|
||||
|
@ -190,7 +195,7 @@ val chainApi = new ChainQueryApi {
|
|||
|
||||
// Finally, we can initialize our wallet with our own node api
|
||||
val wallet =
|
||||
Wallet(keyManager = keyManager, nodeApi = nodeApi, chainQueryApi = chainApi)
|
||||
Wallet(keyManager = keyManager, nodeApi = nodeApi, chainQueryApi = chainApi, creationTime = Instant.now)
|
||||
|
||||
// Then to trigger one of the events we can run
|
||||
wallet.chainQueryApi.getFiltersBetweenHeights(100, 150)
|
||||
|
|
|
@ -19,6 +19,8 @@ import org.bitcoins.testkit.wallet.BitcoinSWalletTest
|
|||
import org.bitcoins.wallet.Wallet
|
||||
import org.bitcoins.wallet.config.WalletAppConfig
|
||||
|
||||
import java.time.Instant
|
||||
|
||||
import scala.concurrent.{ExecutionContextExecutor, Future}
|
||||
```
|
||||
|
||||
|
@ -100,7 +102,7 @@ val exampleCallback = createCallback(exampleProcessBlock)
|
|||
|
||||
// Finally, we can initialize our wallet with our own node api
|
||||
val wallet =
|
||||
Wallet(keyManager = keyManager, nodeApi = nodeApi, chainQueryApi = chainApi)
|
||||
Wallet(keyManager = keyManager, nodeApi = nodeApi, chainQueryApi = chainApi, creationTime = Instant.now)
|
||||
|
||||
// Then to trigger the event we can run
|
||||
val exampleBlock = DoubleSha256Digest(
|
||||
|
|
|
@ -67,6 +67,7 @@ import org.bitcoins.wallet.Wallet
|
|||
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import java.nio.file.Files
|
||||
import java.time.Instant
|
||||
import scala.concurrent._
|
||||
```
|
||||
|
||||
|
@ -144,13 +145,14 @@ val wallet = Wallet(keyManager, new NodeApi {
|
|||
override def broadcastTransaction(tx: Transaction): Future[Unit] = Future.successful(())
|
||||
override def downloadBlocks(blockHashes: Vector[DoubleSha256Digest]): Future[Unit] = Future.successful(())
|
||||
}, new ChainQueryApi {
|
||||
override def epochSecondToBlockHeight(time: Long): Future[Int] = Future.successful(0)
|
||||
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)
|
||||
})
|
||||
}, creationTime = Instant.now)
|
||||
val walletF: Future[WalletApi] = configF.flatMap { _ =>
|
||||
Wallet.initialize(wallet,bip39PasswordOpt)
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package org.bitcoins.keymanager
|
|||
import java.nio.file.{Files, Path}
|
||||
|
||||
import org.bitcoins.core.crypto.{AesPassword, MnemonicCode}
|
||||
import org.bitcoins.core.util.TimeUtil
|
||||
import org.bitcoins.keymanager.ReadMnemonicError.{
|
||||
DecryptionError,
|
||||
JsonParsingError
|
||||
|
@ -29,13 +30,15 @@ class WalletStorageTest extends BitcoinSWalletTest with BeforeAndAfterEach {
|
|||
val passphrase = AesPassword.fromNonEmptyString("this_is_secret")
|
||||
val badPassphrase = AesPassword.fromNonEmptyString("this_is_also_secret")
|
||||
|
||||
def getAndWriteMnemonic(walletConf: WalletAppConfig): MnemonicCode = {
|
||||
val mnemonic = CryptoGenerators.mnemonicCode.sampleSome
|
||||
val encrypted = EncryptedMnemonicHelper.encrypt(mnemonic, passphrase)
|
||||
def getAndWriteMnemonic(walletConf: WalletAppConfig): DecryptedMnemonic = {
|
||||
val mnemonicCode = CryptoGenerators.mnemonicCode.sampleSome
|
||||
val decryptedMnemonic = DecryptedMnemonic(mnemonicCode, TimeUtil.now)
|
||||
val encrypted =
|
||||
EncryptedMnemonicHelper.encrypt(decryptedMnemonic, passphrase)
|
||||
val seedPath = getSeedPath(walletConf)
|
||||
val _ =
|
||||
WalletStorage.writeMnemonicToDisk(seedPath, encrypted)
|
||||
mnemonic
|
||||
decryptedMnemonic
|
||||
}
|
||||
|
||||
it must "write and read a mnemonic to disk" in {
|
||||
|
@ -51,7 +54,11 @@ class WalletStorageTest extends BitcoinSWalletTest with BeforeAndAfterEach {
|
|||
WalletStorage.decryptMnemonicFromDisk(seedPath, passphrase)
|
||||
read match {
|
||||
case Right(readMnemonic) =>
|
||||
assert(writtenMnemonic == readMnemonic)
|
||||
assert(writtenMnemonic.mnemonicCode == readMnemonic.mnemonicCode)
|
||||
// Need to compare using getEpochSecond because when reading an epoch second
|
||||
// it will not include the milliseconds that writtenMnemonic will have
|
||||
assert(
|
||||
writtenMnemonic.creationTime.getEpochSecond == readMnemonic.creationTime.getEpochSecond)
|
||||
case Left(err) => fail(err.toString)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import java.nio.file.Files
|
|||
import org.bitcoins.core.config.{MainNet, RegTest}
|
||||
import org.bitcoins.core.crypto.{DoubleSha256DigestBE, MnemonicCode}
|
||||
import org.bitcoins.core.hd._
|
||||
import org.bitcoins.core.util.TimeUtil
|
||||
import org.bitcoins.keymanager._
|
||||
import org.bitcoins.testkit.keymanager.{KeyManagerTestUtil, KeyManagerUnitTest}
|
||||
import scodec.bits.BitVector
|
||||
|
@ -42,7 +43,7 @@ class BIP39KeyManagerTest extends KeyManagerUnitTest {
|
|||
s"Failed to read mnemonic that was written by key manager with err=${err}")
|
||||
}
|
||||
|
||||
assert(mnemonic.toEntropy == entropy,
|
||||
assert(mnemonic.mnemonicCode.toEntropy == entropy,
|
||||
s"We did not read the same entropy that we wrote!")
|
||||
}
|
||||
|
||||
|
@ -60,7 +61,7 @@ class BIP39KeyManagerTest extends KeyManagerUnitTest {
|
|||
|
||||
it must "initialize a key manager to the same xpub if we call constructor directly or use CreateKeyManagerApi" in {
|
||||
val kmParams = buildParams()
|
||||
val direct = BIP39KeyManager(mnemonic, kmParams, None)
|
||||
val direct = BIP39KeyManager(mnemonic, kmParams, None, TimeUtil.now)
|
||||
|
||||
val directXpub = direct.getRootXPub
|
||||
|
||||
|
@ -83,7 +84,8 @@ class BIP39KeyManagerTest extends KeyManagerUnitTest {
|
|||
it must "initialize a key manager with a bip39 password to the same xpub if we call constructor directly or use CreateKeyManagerApi" in {
|
||||
val kmParams = buildParams()
|
||||
val bip39Pw = KeyManagerTestUtil.bip39Password
|
||||
val direct = BIP39KeyManager(mnemonic, kmParams, Some(bip39Pw))
|
||||
val direct =
|
||||
BIP39KeyManager(mnemonic, kmParams, Some(bip39Pw), TimeUtil.now)
|
||||
|
||||
val directXpub = direct.getRootXPub
|
||||
|
||||
|
@ -105,10 +107,12 @@ class BIP39KeyManagerTest extends KeyManagerUnitTest {
|
|||
val kmParams = buildParams()
|
||||
val bip39Pw = KeyManagerTestUtil.bip39PasswordNonEmpty
|
||||
|
||||
val withPassword = BIP39KeyManager(mnemonic, kmParams, Some(bip39Pw))
|
||||
val withPassword =
|
||||
BIP39KeyManager(mnemonic, kmParams, Some(bip39Pw), TimeUtil.now)
|
||||
val withPasswordXpub = withPassword.getRootXPub
|
||||
|
||||
val noPassword = BIP39KeyManager(mnemonic, kmParams, None)
|
||||
val noPassword =
|
||||
BIP39KeyManager(mnemonic, kmParams, None, TimeUtil.now)
|
||||
|
||||
val noPwXpub = noPassword.getRootXPub
|
||||
|
||||
|
|
|
@ -1,12 +1,25 @@
|
|||
package org.bitcoins.keymanager
|
||||
|
||||
import java.time.Instant
|
||||
|
||||
import org.bitcoins.core.compat.CompatEither
|
||||
import org.bitcoins.core.crypto._
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
case class EncryptedMnemonic(value: AesEncryptedData, salt: AesSalt) {
|
||||
case class DecryptedMnemonic(
|
||||
mnemonicCode: MnemonicCode,
|
||||
creationTime: Instant) {
|
||||
|
||||
def encrypt(password: AesPassword): EncryptedMnemonic =
|
||||
EncryptedMnemonicHelper.encrypt(this, password)
|
||||
}
|
||||
|
||||
case class EncryptedMnemonic(
|
||||
value: AesEncryptedData,
|
||||
salt: AesSalt,
|
||||
creationTime: Instant) {
|
||||
|
||||
def toMnemonic(password: AesPassword): Try[MnemonicCode] = {
|
||||
val key = password.toKey(salt)
|
||||
|
@ -29,9 +42,9 @@ case class EncryptedMnemonic(value: AesEncryptedData, salt: AesSalt) {
|
|||
object EncryptedMnemonicHelper {
|
||||
|
||||
def encrypt(
|
||||
mnemonicCode: MnemonicCode,
|
||||
mnemonic: DecryptedMnemonic,
|
||||
password: AesPassword): EncryptedMnemonic = {
|
||||
val wordsStr = mnemonicCode.words.mkString(" ")
|
||||
val wordsStr = mnemonic.mnemonicCode.words.mkString(" ")
|
||||
val Right(clearText) = ByteVector.encodeUtf8(wordsStr)
|
||||
|
||||
val (key, salt) = password.toKey
|
||||
|
@ -39,6 +52,6 @@ object EncryptedMnemonicHelper {
|
|||
val encryted = AesCrypt
|
||||
.encrypt(clearText, key)
|
||||
|
||||
EncryptedMnemonic(encryted, salt)
|
||||
EncryptedMnemonic(encryted, salt, mnemonic.creationTime)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package org.bitcoins.keymanager
|
||||
|
||||
import java.nio.file.{Files, Path}
|
||||
import java.time.Instant
|
||||
import java.util.NoSuchElementException
|
||||
|
||||
import org.bitcoins.core.compat._
|
||||
import org.bitcoins.core.crypto._
|
||||
|
@ -17,6 +19,9 @@ object WalletStorage {
|
|||
|
||||
import org.bitcoins.core.compat.JavaConverters._
|
||||
|
||||
/** Start of bitcoin-s wallet project, Block 555,990 block time on 2018-12-28 */
|
||||
val FIRST_BITCOIN_S_WALLET_TIME = 1546042867L
|
||||
|
||||
private val logger = LoggerFactory.getLogger(getClass)
|
||||
|
||||
/** Checks if a wallet seed exists in datadir */
|
||||
|
@ -28,6 +33,7 @@ object WalletStorage {
|
|||
val IV = "iv"
|
||||
val CIPHER_TEXT = "cipherText"
|
||||
val SALT = "salt"
|
||||
val CREATION_TIME = "creationTime"
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -44,7 +50,9 @@ object WalletStorage {
|
|||
ujson.Obj(
|
||||
IV -> encrypted.iv.hex,
|
||||
CIPHER_TEXT -> encrypted.cipherText.toHex,
|
||||
SALT -> mnemonic.salt.bytes.toHex
|
||||
SALT -> mnemonic.salt.bytes.toHex,
|
||||
CREATION_TIME -> ujson.Num(
|
||||
mnemonic.creationTime.getEpochSecond.toDouble)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -111,13 +119,22 @@ object WalletStorage {
|
|||
|
||||
val readJsonTupleEither: CompatEither[
|
||||
ReadMnemonicError,
|
||||
(String, String, String)] = jsonE.flatMap { json =>
|
||||
(String, String, String, Long)] = jsonE.flatMap { json =>
|
||||
logger.trace(s"Read encrypted mnemonic JSON: $json")
|
||||
val creationTimeNum = Try(json(CREATION_TIME).num.toLong) match {
|
||||
case Success(value) =>
|
||||
value
|
||||
case Failure(err) if err.isInstanceOf[NoSuchElementException] =>
|
||||
// If no CREATION_TIME is set, we set date to start of bitcoin-s wallet project
|
||||
// default is Block 555,990 block time on 2018-12-28
|
||||
FIRST_BITCOIN_S_WALLET_TIME
|
||||
case Failure(exception) => throw exception
|
||||
}
|
||||
Try {
|
||||
val ivString = json(IV).str
|
||||
val cipherTextString = json(CIPHER_TEXT).str
|
||||
val rawSaltString = json(SALT).str
|
||||
(ivString, cipherTextString, rawSaltString)
|
||||
(ivString, cipherTextString, rawSaltString, creationTimeNum)
|
||||
} match {
|
||||
case Success(value) => CompatRight(value)
|
||||
case Failure(exception) => throw exception
|
||||
|
@ -126,15 +143,17 @@ object WalletStorage {
|
|||
|
||||
val encryptedEither: CompatEither[ReadMnemonicError, EncryptedMnemonic] =
|
||||
readJsonTupleEither.flatMap {
|
||||
case (rawIv, rawCipherText, rawSalt) =>
|
||||
case (rawIv, rawCipherText, rawSalt, rawCreationTime) =>
|
||||
val encryptedOpt = for {
|
||||
iv <- ByteVector.fromHex(rawIv).map(AesIV.fromValidBytes(_))
|
||||
iv <- ByteVector.fromHex(rawIv).map(AesIV.fromValidBytes)
|
||||
cipherText <- ByteVector.fromHex(rawCipherText)
|
||||
salt <- ByteVector.fromHex(rawSalt).map(AesSalt(_))
|
||||
} yield {
|
||||
logger.debug(
|
||||
s"Parsed contents of $seedPath into an EncryptedMnemonic")
|
||||
EncryptedMnemonic(AesEncryptedData(cipherText, iv), salt)
|
||||
EncryptedMnemonic(AesEncryptedData(cipherText, iv),
|
||||
salt,
|
||||
Instant.ofEpochSecond(rawCreationTime))
|
||||
}
|
||||
val toRight: Option[
|
||||
CompatRight[ReadMnemonicError, EncryptedMnemonic]] = encryptedOpt
|
||||
|
@ -147,24 +166,26 @@ object WalletStorage {
|
|||
}
|
||||
|
||||
/**
|
||||
* Reads the wallet mmemonic from disk and tries to parse and
|
||||
* Reads the wallet mnemonic from disk and tries to parse and
|
||||
* decrypt it
|
||||
*/
|
||||
def decryptMnemonicFromDisk(
|
||||
seedPath: Path,
|
||||
passphrase: AesPassword): Either[ReadMnemonicError, MnemonicCode] = {
|
||||
passphrase: AesPassword): Either[ReadMnemonicError, DecryptedMnemonic] = {
|
||||
|
||||
val encryptedEither = readEncryptedMnemonicFromDisk(seedPath)
|
||||
|
||||
val decryptedEither: CompatEither[ReadMnemonicError, MnemonicCode] =
|
||||
val decryptedEither: CompatEither[ReadMnemonicError, DecryptedMnemonic] =
|
||||
encryptedEither.flatMap { encrypted =>
|
||||
encrypted.toMnemonic(passphrase) match {
|
||||
case Failure(exc) =>
|
||||
logger.error(s"Error when decrypting $encrypted: $exc")
|
||||
CompatLeft(ReadMnemonicError.DecryptionError)
|
||||
case Success(value) =>
|
||||
case Success(mnemonic) =>
|
||||
logger.debug(s"Decrypted $encrypted successfully")
|
||||
CompatRight(value)
|
||||
val decryptedMnemonic =
|
||||
DecryptedMnemonic(mnemonic, encrypted.creationTime)
|
||||
CompatRight(decryptedMnemonic)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
package org.bitcoins.keymanager.bip39
|
||||
|
||||
import java.nio.file.Files
|
||||
import java.time.Instant
|
||||
|
||||
import org.bitcoins.core.compat.{CompatEither, CompatLeft, CompatRight}
|
||||
import org.bitcoins.core.crypto._
|
||||
import org.bitcoins.core.hd.{HDAccount, HDPath}
|
||||
import org.bitcoins.core.util.BitcoinSLogger
|
||||
import org.bitcoins.keymanager.util.HDUtil
|
||||
import org.bitcoins.core.util.{BitcoinSLogger, TimeUtil}
|
||||
import org.bitcoins.keymanager._
|
||||
import org.bitcoins.keymanager.util.HDUtil
|
||||
import scodec.bits.BitVector
|
||||
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
@ -23,9 +24,9 @@ import scala.util.{Failure, Success, Try}
|
|||
case class BIP39KeyManager(
|
||||
private val mnemonic: MnemonicCode,
|
||||
kmParams: KeyManagerParams,
|
||||
private val bip39PasswordOpt: Option[String])
|
||||
private val bip39PasswordOpt: Option[String],
|
||||
creationTime: Instant)
|
||||
extends KeyManager {
|
||||
|
||||
private val seed = bip39PasswordOpt match {
|
||||
case Some(pw) =>
|
||||
BIP39Seed.fromMnemonic(mnemonic = mnemonic, password = pw)
|
||||
|
@ -34,6 +35,17 @@ case class BIP39KeyManager(
|
|||
password = BIP39Seed.EMPTY_PASSWORD)
|
||||
}
|
||||
|
||||
def ==(km: KeyManager): Boolean =
|
||||
km match {
|
||||
case bip39Km: BIP39KeyManager =>
|
||||
mnemonic == bip39Km.mnemonic &&
|
||||
kmParams == bip39Km.kmParams &&
|
||||
bip39PasswordOpt == bip39Km.bip39PasswordOpt &&
|
||||
creationTime.getEpochSecond == bip39Km.creationTime.getEpochSecond
|
||||
case _: KeyManager =>
|
||||
km == this
|
||||
}
|
||||
|
||||
private val privVersion: ExtKeyPrivVersion =
|
||||
HDUtil.getXprivVersion(kmParams.purpose, kmParams.network)
|
||||
|
||||
|
@ -72,6 +84,8 @@ object BIP39KeyManager extends BIP39KeyManagerCreateApi with BitcoinSLogger {
|
|||
val seedPath = kmParams.seedPath
|
||||
logger.info(s"Initializing wallet with seedPath=${seedPath}")
|
||||
|
||||
val time = TimeUtil.now
|
||||
|
||||
val writtenToDiskE: CompatEither[KeyManagerInitializeError, KeyManager] =
|
||||
if (Files.notExists(seedPath)) {
|
||||
logger.info(
|
||||
|
@ -92,8 +106,9 @@ object BIP39KeyManager extends BIP39KeyManagerCreateApi with BitcoinSLogger {
|
|||
val encryptedMnemonicE: CompatEither[
|
||||
KeyManagerInitializeError,
|
||||
EncryptedMnemonic] =
|
||||
mnemonicE.map {
|
||||
EncryptedMnemonicHelper.encrypt(_, badPassphrase)
|
||||
mnemonicE.map { mnemonic =>
|
||||
EncryptedMnemonicHelper.encrypt(DecryptedMnemonic(mnemonic, time),
|
||||
badPassphrase)
|
||||
}
|
||||
|
||||
for {
|
||||
|
@ -108,7 +123,8 @@ object BIP39KeyManager extends BIP39KeyManagerCreateApi with BitcoinSLogger {
|
|||
} yield {
|
||||
BIP39KeyManager(mnemonic = mnemonic,
|
||||
kmParams = kmParams,
|
||||
bip39PasswordOpt = bip39PasswordOpt)
|
||||
bip39PasswordOpt = bip39PasswordOpt,
|
||||
creationTime = time)
|
||||
}
|
||||
} else {
|
||||
logger.info(
|
||||
|
@ -117,9 +133,10 @@ object BIP39KeyManager extends BIP39KeyManagerCreateApi with BitcoinSLogger {
|
|||
WalletStorage.decryptMnemonicFromDisk(kmParams.seedPath, badPassphrase) match {
|
||||
case Right(mnemonic) =>
|
||||
CompatRight(
|
||||
BIP39KeyManager(mnemonic = mnemonic,
|
||||
BIP39KeyManager(mnemonic = mnemonic.mnemonicCode,
|
||||
kmParams = kmParams,
|
||||
bip39PasswordOpt = bip39PasswordOpt))
|
||||
bip39PasswordOpt = bip39PasswordOpt,
|
||||
creationTime = mnemonic.creationTime))
|
||||
case Left(err) =>
|
||||
CompatLeft(
|
||||
InitializeKeyManagerError.FailedToReadWrittenSeed(
|
||||
|
@ -138,8 +155,10 @@ object BIP39KeyManager extends BIP39KeyManagerCreateApi with BitcoinSLogger {
|
|||
kmBeforeWrite <- writtenToDiskE
|
||||
invariant <- unlocked match {
|
||||
case Right(unlockedKeyManager) =>
|
||||
require(kmBeforeWrite == unlockedKeyManager,
|
||||
s"We could not read the key manager we just wrote!")
|
||||
require(
|
||||
unlockedKeyManager == kmBeforeWrite,
|
||||
s"We could not read the key manager we just wrote! $kmBeforeWrite != $unlockedKeyManager"
|
||||
)
|
||||
CompatRight(unlockedKeyManager)
|
||||
|
||||
case Left(err) =>
|
||||
|
@ -171,7 +190,11 @@ object BIP39KeyManager extends BIP39KeyManagerCreateApi with BitcoinSLogger {
|
|||
|
||||
mnemonicCodeE match {
|
||||
case Right(mnemonic) =>
|
||||
Right(new BIP39KeyManager(mnemonic, kmParams, bip39PasswordOpt))
|
||||
Right(
|
||||
new BIP39KeyManager(mnemonic.mnemonicCode,
|
||||
kmParams,
|
||||
bip39PasswordOpt,
|
||||
mnemonic.creationTime))
|
||||
case Left(v) => Left(v)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,8 +27,12 @@ object BIP39LockedKeyManager extends BitcoinSLogger {
|
|||
val resultE =
|
||||
WalletStorage.decryptMnemonicFromDisk(kmParams.seedPath, passphrase)
|
||||
resultE match {
|
||||
case Right(mnemonicCode) =>
|
||||
Right(BIP39KeyManager(mnemonicCode, kmParams, bip39PasswordOpt))
|
||||
case Right(mnemonic) =>
|
||||
Right(
|
||||
BIP39KeyManager(mnemonic.mnemonicCode,
|
||||
kmParams,
|
||||
bip39PasswordOpt,
|
||||
mnemonic.creationTime))
|
||||
|
||||
case Left(result) =>
|
||||
result match {
|
||||
|
|
|
@ -229,4 +229,7 @@ trait Node extends NodeApi with ChainQueryApi with P2PLogger {
|
|||
blockHashOpt: DoubleSha256DigestBE): Future[Option[Int]] =
|
||||
chainApiFromDb().flatMap(_.getNumberOfConfirmations(blockHashOpt))
|
||||
|
||||
override def epochSecondToBlockHeight(time: Long): Future[Int] =
|
||||
chainApiFromDb().flatMap(_.epochSecondToBlockHeight(time))
|
||||
|
||||
}
|
||||
|
|
|
@ -84,10 +84,13 @@ abstract class SyncUtil extends BitcoinSLogger {
|
|||
case BlockStamp.BlockHeight(height) =>
|
||||
Future.successful(height)
|
||||
case BlockStamp.BlockTime(_) =>
|
||||
Future.failed(new RuntimeException(s"Cannot query by block time"))
|
||||
throw new RuntimeException("Cannot query by block time")
|
||||
}
|
||||
}
|
||||
|
||||
override def epochSecondToBlockHeight(time: Long): Future[Int] =
|
||||
Future.successful(0)
|
||||
|
||||
override def getFiltersBetweenHeights(
|
||||
startHeight: Int,
|
||||
endHeight: Int): Future[Vector[FilterResponse]] = {
|
||||
|
|
|
@ -123,6 +123,9 @@ trait BitcoinSWalletTest extends BitcoinSFixture with WalletLogger {
|
|||
blockHash = testBlockHash,
|
||||
blockHeight = 1))
|
||||
})
|
||||
|
||||
override def epochSecondToBlockHeight(time: Long): Future[Int] =
|
||||
Future.successful(0)
|
||||
}
|
||||
|
||||
/** Lets you customize the parameters for the created wallet */
|
||||
|
@ -289,6 +292,9 @@ object BitcoinSWalletTest extends WalletLogger {
|
|||
startHeight: Int,
|
||||
endHeight: Int): Future[Vector[FilterResponse]] =
|
||||
Future.successful(Vector.empty)
|
||||
|
||||
override def epochSecondToBlockHeight(time: Long): Future[Int] =
|
||||
Future.successful(0)
|
||||
}
|
||||
|
||||
sealed trait WalletWithBitcoind {
|
||||
|
@ -342,7 +348,9 @@ object BitcoinSWalletTest extends WalletLogger {
|
|||
|
||||
walletConfig.initialize().flatMap { _ =>
|
||||
val wallet =
|
||||
Wallet(keyManager, nodeApi, chainQueryApi)(walletConfig, ec)
|
||||
Wallet(keyManager, nodeApi, chainQueryApi, keyManager.creationTime)(
|
||||
walletConfig,
|
||||
ec)
|
||||
Wallet.initialize(wallet, bip39PasswordOpt)
|
||||
}
|
||||
}
|
||||
|
@ -403,11 +411,12 @@ object BitcoinSWalletTest extends WalletLogger {
|
|||
|
||||
//create the wallet with the appropriate callbacks now that
|
||||
//we have them
|
||||
walletWithCallback = Wallet(keyManager = wallet.keyManager,
|
||||
nodeApi = apiCallback.nodeApi,
|
||||
chainQueryApi = apiCallback.chainQueryApi)(
|
||||
wallet.walletConfig,
|
||||
wallet.ec)
|
||||
walletWithCallback = Wallet(
|
||||
keyManager = wallet.keyManager,
|
||||
nodeApi = apiCallback.nodeApi,
|
||||
chainQueryApi = apiCallback.chainQueryApi,
|
||||
creationTime = wallet.keyManager.creationTime)(wallet.walletConfig,
|
||||
wallet.ec)
|
||||
//complete the walletCallbackP so we can handle the callbacks when they are
|
||||
//called without hanging forever.
|
||||
_ = walletCallbackP.success(walletWithCallback)
|
||||
|
|
|
@ -84,7 +84,8 @@ object WalletTestUtil {
|
|||
HDAccount(coin = HDCoin(purpose, HDCoinType.Testnet), index = 1)
|
||||
}
|
||||
|
||||
def firstAccountDb = AccountDb(freshXpub(), defaultHdAccount)
|
||||
def firstAccountDb: AccountDb =
|
||||
AccountDb(freshXpub(), defaultHdAccount)
|
||||
|
||||
def nestedSegWitAccountDb: AccountDb =
|
||||
AccountDb(freshXpub(),
|
||||
|
|
|
@ -1,19 +1,9 @@
|
|||
package org.bitcoins.testkit.db
|
||||
|
||||
import java.nio.file.Files
|
||||
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import org.bitcoins.chain.models.BlockHeaderDAO
|
||||
import org.bitcoins.core.config.TestNet3
|
||||
import org.bitcoins.core.hd.{HDAccount, HDCoin, HDCoinType, HDPurposes}
|
||||
import org.bitcoins.db.{CRUD, SQLiteTableInfo}
|
||||
import org.bitcoins.server.BitcoinSAppConfig._
|
||||
import org.bitcoins.testkit.BitcoinSTestAppConfig
|
||||
import org.bitcoins.testkit.Implicits._
|
||||
import org.bitcoins.testkit.chain.ChainTestUtil
|
||||
import org.bitcoins.testkit.core.gen.CryptoGenerators
|
||||
import org.bitcoins.testkit.util.BitcoinSAsyncTest
|
||||
import org.bitcoins.wallet.models.{AccountDAO, AccountDb}
|
||||
|
||||
class AppConfigTest extends BitcoinSAsyncTest {
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
package org.bitcoins.wallet
|
||||
|
||||
import org.bitcoins.core.crypto.{AesPassword, MnemonicCode}
|
||||
import org.bitcoins.keymanager.EncryptedMnemonicHelper
|
||||
import org.bitcoins.core.util.TimeUtil
|
||||
import org.bitcoins.keymanager.{DecryptedMnemonic, EncryptedMnemonicHelper}
|
||||
import org.bitcoins.testkit.core.gen.CryptoGenerators
|
||||
import org.bitcoins.testkit.util.BitcoinSUnitTest
|
||||
import org.bitcoins.testkit.Implicits._
|
||||
|
@ -15,7 +16,8 @@ class EncryptedMnemonicTest extends BitcoinSUnitTest {
|
|||
val password = AesPassword.fromNonEmptyString("good")
|
||||
val badPassword = AesPassword.fromNonEmptyString("bad")
|
||||
|
||||
val mnemonic = CryptoGenerators.mnemonicCode.sampleSome
|
||||
val mnemonicCode = CryptoGenerators.mnemonicCode.sampleSome
|
||||
val mnemonic = DecryptedMnemonic(mnemonicCode, TimeUtil.now)
|
||||
val encrypted = EncryptedMnemonicHelper.encrypt(mnemonic, password)
|
||||
|
||||
val decrypted = encrypted.toMnemonic(badPassword)
|
||||
|
@ -26,13 +28,14 @@ class EncryptedMnemonicTest extends BitcoinSUnitTest {
|
|||
|
||||
it must "have encryption/decryption symmetry" in {
|
||||
forAll(CryptoGenerators.mnemonicCode, CryptoGenerators.aesPassword) {
|
||||
(code, password) =>
|
||||
val encrypted = EncryptedMnemonicHelper.encrypt(code, password)
|
||||
(mnemonicCode, password) =>
|
||||
val mnemonic = DecryptedMnemonic(mnemonicCode, TimeUtil.now)
|
||||
val encrypted = EncryptedMnemonicHelper.encrypt(mnemonic, password)
|
||||
val decrypted = encrypted.toMnemonic(password) match {
|
||||
case Success(clear) => clear
|
||||
case Failure(exc) => fail(exc)
|
||||
}
|
||||
assert(decrypted == code)
|
||||
assert(decrypted == mnemonicCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -132,13 +132,57 @@ class RescanHandlingTest extends BitcoinSWalletTest {
|
|||
_ <- newTxWallet.rescanNeutrinoWallet(startOpt = txInBlockHeightOpt,
|
||||
endOpt = None,
|
||||
addressBatchSize =
|
||||
DEFAULT_ADDR_BATCH_SIZE)
|
||||
DEFAULT_ADDR_BATCH_SIZE,
|
||||
useCreationTime = false)
|
||||
balance <- newTxWallet.getBalance()
|
||||
} yield {
|
||||
assert(balance == amt)
|
||||
}
|
||||
}
|
||||
|
||||
it must "be able to discover funds that occurred from the wallet creation time" in {
|
||||
fixture: WalletWithBitcoind =>
|
||||
val WalletWithBitcoindV19(wallet, bitcoind) = fixture
|
||||
|
||||
val amt = Bitcoins.one
|
||||
val numBlocks = 1
|
||||
|
||||
//send funds to a fresh wallet address
|
||||
val addrF = wallet.getNewAddress()
|
||||
val bitcoindAddrF = bitcoind.getNewAddress
|
||||
val newTxWalletF = for {
|
||||
addr <- addrF
|
||||
txid <- bitcoind.sendToAddress(addr, amt)
|
||||
tx <- bitcoind.getRawTransactionRaw(txid)
|
||||
bitcoindAddr <- bitcoindAddrF
|
||||
blockHashes <- bitcoind.generateToAddress(blocks = numBlocks,
|
||||
address = bitcoindAddr)
|
||||
newTxWallet <- wallet.processTransaction(transaction = tx,
|
||||
blockHashOpt =
|
||||
blockHashes.headOption)
|
||||
balance <- newTxWallet.getBalance()
|
||||
unconfirmedBalance <- newTxWallet.getUnconfirmedBalance()
|
||||
} yield {
|
||||
//balance doesn't have to exactly equal, as there was money in the
|
||||
//wallet before hand.
|
||||
assert(balance >= amt)
|
||||
assert(balance == unconfirmedBalance)
|
||||
newTxWallet
|
||||
}
|
||||
|
||||
for {
|
||||
newTxWallet <- newTxWalletF
|
||||
_ <- newTxWallet.rescanNeutrinoWallet(startOpt = None,
|
||||
endOpt = None,
|
||||
addressBatchSize =
|
||||
DEFAULT_ADDR_BATCH_SIZE,
|
||||
useCreationTime = true)
|
||||
balance <- newTxWallet.getBalance()
|
||||
} yield {
|
||||
assert(balance == Bitcoins(7))
|
||||
}
|
||||
}
|
||||
|
||||
it must "NOT discover funds that happened OUTSIDE of a certain range of block hashes" in {
|
||||
fixture: WalletWithBitcoind =>
|
||||
val WalletWithBitcoindV19(wallet, _) = fixture
|
||||
|
@ -167,7 +211,8 @@ class RescanHandlingTest extends BitcoinSWalletTest {
|
|||
_ <- wallet.rescanNeutrinoWallet(startOpt = BlockStamp.height0Opt,
|
||||
endOpt = end,
|
||||
addressBatchSize =
|
||||
DEFAULT_ADDR_BATCH_SIZE)
|
||||
DEFAULT_ADDR_BATCH_SIZE,
|
||||
useCreationTime = false)
|
||||
balanceAfterRescan <- wallet.getBalance()
|
||||
} yield {
|
||||
assert(balanceAfterRescan == CurrencyUnits.zero)
|
||||
|
|
|
@ -5,7 +5,7 @@ import org.bitcoins.commons.serializers.JsonSerializers._
|
|||
import org.bitcoins.core.crypto.{ExtPublicKey, MnemonicCode}
|
||||
import org.bitcoins.core.hd._
|
||||
import org.bitcoins.core.protocol.BitcoinAddress
|
||||
import org.bitcoins.core.util.FutureUtil
|
||||
import org.bitcoins.core.util.{FutureUtil, TimeUtil}
|
||||
import org.bitcoins.keymanager.KeyManagerParams
|
||||
import org.bitcoins.keymanager.bip39.BIP39KeyManager
|
||||
import org.bitcoins.testkit.BitcoinSTestAppConfig
|
||||
|
@ -149,7 +149,8 @@ class TrezorAddressTest extends BitcoinSWalletTest with EmptyFixture {
|
|||
Future.failed(
|
||||
new RuntimeException(s"Failed to initialize km with err=${err}"))
|
||||
case Right(km) =>
|
||||
val wallet = Wallet(km, MockNodeApi, MockChainQueryApi)(config, ec)
|
||||
val wallet =
|
||||
Wallet(km, MockNodeApi, MockChainQueryApi, TimeUtil.now)(config, ec)
|
||||
val walletF =
|
||||
Wallet.initialize(wallet = wallet,
|
||||
bip39PasswordOpt = bip39PasswordOpt)(config, ec)
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
package org.bitcoins.wallet.models
|
||||
|
||||
import org.bitcoins.testkit.Implicits._
|
||||
import org.bitcoins.testkit.core.gen.CryptoGenerators
|
||||
import org.bitcoins.testkit.fixtures.WalletDAOFixture
|
||||
import org.bitcoins.testkit.wallet.{BitcoinSWalletTest, WalletTestUtil}
|
||||
import org.bitcoins.testkit.Implicits._
|
||||
|
||||
class AccountDAOTest extends BitcoinSWalletTest with WalletDAOFixture {
|
||||
|
||||
|
@ -15,7 +15,8 @@ class AccountDAOTest extends BitcoinSWalletTest with WalletDAOFixture {
|
|||
|
||||
val xpub = CryptoGenerators.extPublicKey.sampleSome
|
||||
|
||||
val accountDb = AccountDb(xpub, account)
|
||||
val accountDb =
|
||||
AccountDb(xpub, account)
|
||||
accountDAO.create(accountDb)
|
||||
}
|
||||
found <- accountDAO.read(
|
||||
|
@ -32,7 +33,8 @@ class AccountDAOTest extends BitcoinSWalletTest with WalletDAOFixture {
|
|||
|
||||
val xpub = CryptoGenerators.extPublicKey.sampleSome
|
||||
|
||||
val accountDb = AccountDb(xpub, account)
|
||||
val accountDb =
|
||||
AccountDb(xpub, account)
|
||||
accountDAO.create(accountDb)
|
||||
}
|
||||
found <- accountDAO.findByAccount(account)
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package org.bitcoins.wallet
|
||||
|
||||
import java.time.Instant
|
||||
|
||||
import org.bitcoins.core.api.{ChainQueryApi, NodeApi}
|
||||
import org.bitcoins.core.bloom.{BloomFilter, BloomUpdateAll}
|
||||
import org.bitcoins.core.crypto._
|
||||
|
@ -7,11 +9,7 @@ import org.bitcoins.core.currency._
|
|||
import org.bitcoins.core.hd.{HDAccount, HDCoin, HDPurposes}
|
||||
import org.bitcoins.core.protocol.BitcoinAddress
|
||||
import org.bitcoins.core.protocol.blockchain.BlockHeader
|
||||
import org.bitcoins.core.protocol.transaction.{
|
||||
Transaction,
|
||||
TransactionOutPoint,
|
||||
TransactionOutput
|
||||
}
|
||||
import org.bitcoins.core.protocol.transaction._
|
||||
import org.bitcoins.core.wallet.fee.FeeUnit
|
||||
import org.bitcoins.core.wallet.utxo.TxoState
|
||||
import org.bitcoins.core.wallet.utxo.TxoState.{
|
||||
|
@ -47,6 +45,10 @@ abstract class Wallet
|
|||
private[wallet] val outgoingTxDAO: OutgoingTransactionDAO =
|
||||
OutgoingTransactionDAO()
|
||||
|
||||
val nodeApi: NodeApi
|
||||
val chainQueryApi: ChainQueryApi
|
||||
val creationTime: Instant = keyManager.creationTime
|
||||
|
||||
override def isEmpty(): Future[Boolean] =
|
||||
for {
|
||||
addressCount <- addressDAO.count()
|
||||
|
@ -337,7 +339,8 @@ abstract class Wallet
|
|||
accountCreationF.map(created =>
|
||||
logger.debug(s"Created new account ${created.hdAccount}"))
|
||||
accountCreationF
|
||||
.map(_ => Wallet(keyManager, nodeApi, chainQueryApi))
|
||||
.map(_ =>
|
||||
Wallet(keyManager, nodeApi, chainQueryApi, keyManager.creationTime))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -347,7 +350,8 @@ object Wallet extends WalletLogger {
|
|||
private case class WalletImpl(
|
||||
override val keyManager: BIP39KeyManager,
|
||||
override val nodeApi: NodeApi,
|
||||
override val chainQueryApi: ChainQueryApi
|
||||
override val chainQueryApi: ChainQueryApi,
|
||||
override val creationTime: Instant
|
||||
)(
|
||||
implicit override val walletConfig: WalletAppConfig,
|
||||
override val ec: ExecutionContext
|
||||
|
@ -356,10 +360,11 @@ object Wallet extends WalletLogger {
|
|||
def apply(
|
||||
keyManager: BIP39KeyManager,
|
||||
nodeApi: NodeApi,
|
||||
chainQueryApi: ChainQueryApi)(
|
||||
chainQueryApi: ChainQueryApi,
|
||||
creationTime: Instant)(
|
||||
implicit config: WalletAppConfig,
|
||||
ec: ExecutionContext): Wallet = {
|
||||
WalletImpl(keyManager, nodeApi, chainQueryApi)
|
||||
WalletImpl(keyManager, nodeApi, chainQueryApi, creationTime)
|
||||
}
|
||||
|
||||
/** Creates the level 0 account for the given HD purpose */
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package org.bitcoins.wallet.api
|
||||
|
||||
import java.time.Instant
|
||||
|
||||
import org.bitcoins.core.api.{ChainQueryApi, NodeApi}
|
||||
import org.bitcoins.core.bloom.BloomFilter
|
||||
import org.bitcoins.core.config.NetworkParameters
|
||||
|
@ -37,6 +39,7 @@ trait WalletApi extends WalletLogger {
|
|||
|
||||
val nodeApi: NodeApi
|
||||
val chainQueryApi: ChainQueryApi
|
||||
val creationTime: Instant
|
||||
|
||||
def chainParams: ChainParams = walletConfig.chain
|
||||
|
||||
|
@ -303,7 +306,8 @@ trait WalletApi extends WalletLogger {
|
|||
case Right(km) =>
|
||||
val w = Wallet(keyManager = km,
|
||||
nodeApi = nodeApi,
|
||||
chainQueryApi = chainQueryApi)
|
||||
chainQueryApi = chainQueryApi,
|
||||
creationTime = km.creationTime)
|
||||
Right(w)
|
||||
case Left(err) => Left(err)
|
||||
}
|
||||
|
@ -370,16 +374,19 @@ trait WalletApi extends WalletLogger {
|
|||
account: HDAccount,
|
||||
startOpt: Option[BlockStamp],
|
||||
endOpt: Option[BlockStamp],
|
||||
addressBatchSize: Int): Future[Unit]
|
||||
addressBatchSize: Int,
|
||||
useCreationTime: Boolean): Future[Unit]
|
||||
|
||||
def rescanNeutrinoWallet(
|
||||
startOpt: Option[BlockStamp],
|
||||
endOpt: Option[BlockStamp],
|
||||
addressBatchSize: Int): Future[Unit] =
|
||||
addressBatchSize: Int,
|
||||
useCreationTime: Boolean): Future[Unit] =
|
||||
rescanNeutrinoWallet(account = walletConfig.defaultAccount,
|
||||
startOpt = startOpt,
|
||||
endOpt = endOpt,
|
||||
addressBatchSize = addressBatchSize)
|
||||
addressBatchSize = addressBatchSize,
|
||||
useCreationTime = useCreationTime)
|
||||
|
||||
/** Helper method to rescan the ENTIRE blockchain. */
|
||||
def fullRescanNeutrinoWallet(addressBatchSize: Int): Future[Unit] =
|
||||
|
@ -392,7 +399,8 @@ trait WalletApi extends WalletLogger {
|
|||
rescanNeutrinoWallet(account = account,
|
||||
startOpt = None,
|
||||
endOpt = None,
|
||||
addressBatchSize = addressBatchSize)
|
||||
addressBatchSize = addressBatchSize,
|
||||
useCreationTime = false)
|
||||
|
||||
/**
|
||||
* Recreates the account using BIP-44 approach
|
||||
|
|
|
@ -6,6 +6,7 @@ import org.bitcoins.core.api.ChainQueryApi.{FilterResponse, InvalidBlockRange}
|
|||
import org.bitcoins.core.crypto.DoubleSha256Digest
|
||||
import org.bitcoins.core.gcs.SimpleFilterMatcher
|
||||
import org.bitcoins.core.hd.{HDAccount, HDChainType}
|
||||
import org.bitcoins.core.protocol.BlockStamp.BlockHeight
|
||||
import org.bitcoins.core.protocol.script.ScriptPubKey
|
||||
import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp}
|
||||
import org.bitcoins.core.util.FutureUtil
|
||||
|
@ -25,13 +26,25 @@ private[wallet] trait RescanHandling extends WalletLogger {
|
|||
account: HDAccount,
|
||||
startOpt: Option[BlockStamp],
|
||||
endOpt: Option[BlockStamp],
|
||||
addressBatchSize: Int): Future[Unit] = {
|
||||
addressBatchSize: Int,
|
||||
useCreationTime: Boolean = true): Future[Unit] = {
|
||||
|
||||
logger.info(s"Starting rescanning the wallet from ${startOpt} to ${endOpt}")
|
||||
|
||||
val res = for {
|
||||
start <- (startOpt, useCreationTime) match {
|
||||
case (Some(_), true) =>
|
||||
Future.failed(new IllegalArgumentException(
|
||||
"Cannot define a starting block and use the wallet creation time"))
|
||||
case (Some(value), false) =>
|
||||
Future.successful(Some(value))
|
||||
case (None, true) =>
|
||||
walletCreationBlockHeight.map(Some(_))
|
||||
case (None, false) =>
|
||||
Future.successful(None)
|
||||
}
|
||||
_ <- clearUtxosAndAddresses(account)
|
||||
_ <- doNeutrinoRescan(account, startOpt, endOpt, addressBatchSize)
|
||||
_ <- doNeutrinoRescan(account, start, endOpt, addressBatchSize)
|
||||
} yield ()
|
||||
|
||||
res.onComplete(_ => logger.info("Finished rescanning the wallet"))
|
||||
|
@ -43,6 +56,11 @@ private[wallet] trait RescanHandling extends WalletLogger {
|
|||
override def rescanSPVWallet(): Future[Unit] =
|
||||
Future.failed(new RuntimeException("Rescan not implemented for SPV wallet"))
|
||||
|
||||
lazy val walletCreationBlockHeight: Future[BlockHeight] =
|
||||
chainQueryApi
|
||||
.epochSecondToBlockHeight(creationTime.getEpochSecond)
|
||||
.map(BlockHeight)
|
||||
|
||||
/** @inheritdoc */
|
||||
override def getMatchingBlocks(
|
||||
scripts: Vector[ScriptPubKey],
|
||||
|
|
Loading…
Add table
Reference in a new issue