mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-03-13 11:35:40 +01:00
Rescan from account (#1348)
This commit is contained in:
parent
5d3066b21f
commit
3d26ad8f34
8 changed files with 143 additions and 27 deletions
|
@ -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)
|
||||
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]] = {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue