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:
Ben Carman 2020-04-29 09:49:41 -05:00 committed by GitHub
parent 3c008ff82b
commit 6d7685b76e
33 changed files with 467 additions and 147 deletions

View file

@ -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

View file

@ -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`

View file

@ -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)

View file

@ -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"))

View file

@ -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(

View file

@ -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],

View file

@ -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)

View file

@ -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]] =

View file

@ -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

View file

@ -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 {

View 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
}
}

View file

@ -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

View file

@ -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)

View file

@ -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(

View file

@ -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)
}

View file

@ -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)
}
}

View file

@ -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

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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 {

View file

@ -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))
}

View file

@ -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]] = {

View file

@ -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)

View file

@ -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(),

View file

@ -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 {

View file

@ -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)
}
}
}

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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 */

View file

@ -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

View file

@ -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],