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