Rescan from account (#1348)

This commit is contained in:
Ben Carman 2020-04-24 09:37:12 -05:00 committed by GitHub
parent 5d3066b21f
commit 3d26ad8f34
8 changed files with 143 additions and 27 deletions

View file

@ -28,7 +28,7 @@ import org.bitcoins.core.protocol.transaction.{
EmptyTransactionOutput,
Transaction
}
import org.bitcoins.core.protocol.{BitcoinAddress, P2PKHAddress}
import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp, P2PKHAddress}
import org.bitcoins.core.psbt.PSBT
import org.bitcoins.core.util.FutureUtil
import org.bitcoins.core.wallet.fee.FeeUnit
@ -496,7 +496,10 @@ class RoutesSpec
(mockWalletApi.isEmpty: () => Future[Boolean])
.expects()
.returning(Future.successful(false))
(mockWalletApi.rescanNeutrinoWallet _)
(mockWalletApi
.rescanNeutrinoWallet(_: Option[BlockStamp],
_: Option[BlockStamp],
_: Int))
.expects(None, None, 100)
.returning(FutureUtil.unit)
@ -512,7 +515,10 @@ class RoutesSpec
(mockWalletApi.isEmpty: () => Future[Boolean])
.expects()
.returning(Future.successful(false))
(mockWalletApi.rescanNeutrinoWallet _)
(mockWalletApi
.rescanNeutrinoWallet(_: Option[BlockStamp],
_: Option[BlockStamp],
_: Int))
.expects(
Some(BlockTime(
ZonedDateTime.of(2018, 10, 27, 12, 34, 56, 0, ZoneId.of("UTC")))),
@ -533,7 +539,10 @@ class RoutesSpec
(mockWalletApi.isEmpty: () => Future[Boolean])
.expects()
.returning(Future.successful(false))
(mockWalletApi.rescanNeutrinoWallet _)
(mockWalletApi
.rescanNeutrinoWallet(_: Option[BlockStamp],
_: Option[BlockStamp],
_: Int))
.expects(None, Some(BlockHash(DoubleSha256DigestBE.empty)), 100)
.returning(FutureUtil.unit)
@ -551,7 +560,10 @@ class RoutesSpec
(mockWalletApi.isEmpty: () => Future[Boolean])
.expects()
.returning(Future.successful(false))
(mockWalletApi.rescanNeutrinoWallet _)
(mockWalletApi
.rescanNeutrinoWallet(_: Option[BlockStamp],
_: Option[BlockStamp],
_: Int))
.expects(Some(BlockHeight(12345)), Some(BlockHeight(67890)), 100)
.returning(FutureUtil.unit)
@ -600,7 +612,10 @@ class RoutesSpec
(mockWalletApi.isEmpty: () => Future[Boolean])
.expects()
.returning(Future.successful(false))
(mockWalletApi.rescanNeutrinoWallet _)
(mockWalletApi
.rescanNeutrinoWallet(_: Option[BlockStamp],
_: Option[BlockStamp],
_: Int))
.expects(None, None, 55)
.returning(FutureUtil.unit)

View file

@ -55,7 +55,7 @@ val initBalanceF = for {
val clearedWalletF = for {
w <- walletF
_ <- initBalanceF
clearedWallet <- w.clearUtxosAndAddresses()
clearedWallet <- w.clearAllUtxosAndAddresses()
zeroBalance <- clearedWallet.getBalance()
} yield {
println(s"Balance after clearing utxos: ${zeroBalance}")
@ -71,7 +71,7 @@ val addrBatchSize = 100
//ok now that we have a cleared wallet, we need to rescan and find our fudns again!
val rescannedBalanceF = for {
w <- clearedWalletF
_ <- w.fullRescanNeurinoWallet(addrBatchSize)
_ <- w.fullRescanNeutrinoWallet(addrBatchSize)
balanceAfterRescan <- w.getBalance()
} yield {
println(s"Wallet balance after rescan: ${balanceAfterRescan}")

View file

@ -26,6 +26,50 @@ class RescanHandlingTest extends BitcoinSWalletTest {
behavior of "Wallet rescans"
it must "properly clear utxos and address for an account" in {
fixture: WalletWithBitcoind =>
val wallet = fixture.wallet
for {
accountDb <- wallet.getDefaultAccount()
account = accountDb.hdAccount
utxos <- wallet.spendingInfoDAO.findAllForAccount(account)
_ = assert(utxos.nonEmpty)
addresses <- wallet.addressDAO.findAllForAccount(account)
_ = assert(addresses.nonEmpty)
_ <- wallet.clearUtxosAndAddresses(account)
clearedUtxos <- wallet.spendingInfoDAO.findAllForAccount(account)
clearedAddresses <- wallet.addressDAO.findAllForAccount(account)
} yield {
assert(clearedUtxos.isEmpty)
assert(clearedAddresses.isEmpty)
}
}
it must "properly clear all utxos and address" in {
fixture: WalletWithBitcoind =>
val wallet = fixture.wallet
for {
utxos <- wallet.spendingInfoDAO.findAll()
_ = assert(utxos.nonEmpty)
addresses <- wallet.addressDAO.findAll()
_ = assert(addresses.nonEmpty)
_ <- wallet.clearAllUtxosAndAddresses()
clearedUtxos <- wallet.spendingInfoDAO.findAll()
clearedAddresses <- wallet.addressDAO.findAll()
} yield {
assert(clearedUtxos.isEmpty)
assert(clearedAddresses.isEmpty)
}
}
val DEFAULT_ADDR_BATCH_SIZE = 10
it must "be able to discover funds that belong to the wallet using WalletApi.rescanNeutrinoWallet" in {
fixture: WalletWithBitcoind =>
@ -38,7 +82,7 @@ class RescanHandlingTest extends BitcoinSWalletTest {
_ = assert(
initBalance > CurrencyUnits.zero,
s"Cannot run rescan test if our init wallet balance is zero!")
_ <- wallet.fullRescanNeurinoWallet(DEFAULT_ADDR_BATCH_SIZE)
_ <- wallet.fullRescanNeutrinoWallet(DEFAULT_ADDR_BATCH_SIZE)
balanceAfterRescan <- wallet.getBalance()
} yield {
assert(balanceAfterRescan == initBalance)

View file

@ -167,11 +167,20 @@ trait LockedWalletApi extends WalletApi with WalletLogger {
/** Checks if the wallet contains any data */
def isEmpty(): Future[Boolean]
/** Removes all utxos and addresses from the wallet account.
* Don't call this unless you are sure you can recover
* your wallet
*/
def clearUtxosAndAddresses(account: HDAccount): Future[WalletApi]
def clearUtxosAndAddresses(): Future[WalletApi] =
clearUtxosAndAddresses(walletConfig.defaultAccount)
/** Removes all utxos and addresses from the wallet.
* Don't call this unless you are sure you can recover
* your wallet
* */
def clearUtxosAndAddresses(): Future[WalletApi]
*/
def clearAllUtxosAndAddresses(): Future[WalletApi]
/**
* Gets a new external address with the specified
@ -358,16 +367,32 @@ trait LockedWalletApi extends WalletApi with WalletLogger {
* @param addressBatchSize how many addresses to match in a single pass
*/
def rescanNeutrinoWallet(
account: HDAccount,
startOpt: Option[BlockStamp],
endOpt: Option[BlockStamp],
addressBatchSize: Int): Future[Unit]
def rescanNeutrinoWallet(
startOpt: Option[BlockStamp],
endOpt: Option[BlockStamp],
addressBatchSize: Int): Future[Unit] =
rescanNeutrinoWallet(account = walletConfig.defaultAccount,
startOpt = startOpt,
endOpt = endOpt,
addressBatchSize = addressBatchSize)
/** Helper method to rescan the ENTIRE blockchain. */
def fullRescanNeurinoWallet(addressBatchSize: Int): Future[Unit] = {
rescanNeutrinoWallet(startOpt = None,
def fullRescanNeutrinoWallet(addressBatchSize: Int): Future[Unit] =
fullRescanNeutrinoWallet(account = walletConfig.defaultAccount,
addressBatchSize = addressBatchSize)
def fullRescanNeutrinoWallet(
account: HDAccount,
addressBatchSize: Int): Future[Unit] =
rescanNeutrinoWallet(account = account,
startOpt = None,
endOpt = None,
addressBatchSize = addressBatchSize)
}
/**
* Recreates the account using BIP-44 approach

View file

@ -298,6 +298,17 @@ private[wallet] trait AddressHandling extends WalletLogger {
getNewAddressHelper(account, HDChainType.Change)
}
def getNewChangeAddress(account: HDAccount): Future[BitcoinAddress] = {
val accountDbOptF = findAccount(account)
accountDbOptF.flatMap {
case Some(accountDb) => getNewChangeAddress(accountDb)
case None =>
Future.failed(
new RuntimeException(
s"No account found for given hdaccount=$account"))
}
}
/** @inheritdoc */
override def getAddressInfo(
address: BitcoinAddress): Future[Option[AddressInfo]] = {

View file

@ -5,7 +5,7 @@ import java.util.concurrent.Executors
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.HDChainType
import org.bitcoins.core.hd.{HDAccount, HDChainType}
import org.bitcoins.core.protocol.script.ScriptPubKey
import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp}
import org.bitcoins.core.util.FutureUtil
@ -22,6 +22,7 @@ private[wallet] trait RescanHandling extends WalletLogger {
/** @inheritdoc */
override def rescanNeutrinoWallet(
account: HDAccount,
startOpt: Option[BlockStamp],
endOpt: Option[BlockStamp],
addressBatchSize: Int): Future[Unit] = {
@ -29,8 +30,8 @@ private[wallet] trait RescanHandling extends WalletLogger {
logger.info(s"Starting rescanning the wallet from ${startOpt} to ${endOpt}")
val res = for {
_ <- clearUtxosAndAddresses()
_ <- doNeutrinoRescan(startOpt, endOpt, addressBatchSize)
_ <- clearUtxosAndAddresses(account)
_ <- doNeutrinoRescan(account, startOpt, endOpt, addressBatchSize)
} yield ()
res.onComplete(_ => logger.info("Finished rescanning the wallet"))
@ -85,11 +86,12 @@ private[wallet] trait RescanHandling extends WalletLogger {
// Private methods
private def doNeutrinoRescan(
account: HDAccount,
startOpt: Option[BlockStamp],
endOpt: Option[BlockStamp],
addressBatchSize: Int): Future[Unit] = {
for {
scriptPubKeys <- generateScriptPubKeys(addressBatchSize)
scriptPubKeys <- generateScriptPubKeys(account, addressBatchSize)
blocks <- matchBlocks(scriptPubKeys = scriptPubKeys,
endOpt = endOpt,
startOpt = startOpt)
@ -102,7 +104,7 @@ private[wallet] trait RescanHandling extends WalletLogger {
logger.info(
s"Attempting rescan again with fresh pool of addresses as we had a " +
s"match within our address gap limit of ${walletConfig.addressGapLimit}")
doNeutrinoRescan(startOpt, endOpt, addressBatchSize)
doNeutrinoRescan(account, startOpt, endOpt, addressBatchSize)
}
} yield res
}
@ -180,6 +182,7 @@ private[wallet] trait RescanHandling extends WalletLogger {
}
private def generateScriptPubKeys(
account: HDAccount,
count: Int): Future[Vector[ScriptPubKey]] = {
for {
addresses <- 1
@ -188,7 +191,7 @@ private[wallet] trait RescanHandling extends WalletLogger {
(prevFuture, _) =>
for {
prev <- prevFuture
address <- getNewAddress()
address <- getNewAddress(account)
} yield prev :+ address
}
changeAddresses <- 1
@ -197,7 +200,7 @@ private[wallet] trait RescanHandling extends WalletLogger {
(prevFuture, _) =>
for {
prev <- prevFuture
address <- getNewChangeAddress()
address <- getNewChangeAddress(account)
} yield prev :+ address
}
} yield addresses.map(_.scriptPubKey) ++ changeAddresses.map(_.scriptPubKey)

View file

@ -42,6 +42,14 @@ case class AddressDAO()(
accountIndex: Int): Query[AddressTable, AddressDb, Seq] =
table.filter(_.accountIndex === accountIndex)
def findAllForAccount(account: HDAccount): Future[Vector[AddressDb]] = {
val query = table
.filter(_.accountIndex === account.index)
.filter(_.accountCoin === account.coin.coinType)
database.run(query.result).map(_.toVector)
}
/**
* Finds the most recent change address in the wallet, if any
*/

View file

@ -113,16 +113,26 @@ case class SpendingInfoDAO()(
database.run(query.result).map(_.toVector)
}
private def filterUtxosByAccount(
utxos: Vector[SpendingInfoDb],
hdAccount: HDAccount): Vector[SpendingInfoDb] = {
utxos.filter(
utxo =>
HDAccount.isSameAccount(bip32Path = utxo.privKeyPath,
account = hdAccount))
}
/** Finds all utxos for a given account */
def findAllUnspentForAccount(
hdAccount: HDAccount): Future[Vector[SpendingInfoDb]] = {
val allUtxosF = findAllUnspent()
allUtxosF.map { allUtxos =>
allUtxos.filter(
utxo =>
HDAccount.isSameAccount(bip32Path = utxo.privKeyPath,
account = hdAccount))
}
allUtxosF.map(filterUtxosByAccount(_, hdAccount))
}
def findAllForAccount(
hdAccount: HDAccount): Future[Vector[SpendingInfoDb]] = {
val allUtxosF = findAll()
allUtxosF.map(filterUtxosByAccount(_, hdAccount))
}
/** Enumerates all TX outputs in the wallet with the state