Move Node type of out Wallet API (#1708)

* Move Node type of out wallet api

* Remove extensions, add scaladocs
This commit is contained in:
Ben Carman 2020-07-29 05:57:48 -05:00 committed by GitHub
parent c3dfb369d7
commit 26d5a09532
12 changed files with 174 additions and 167 deletions

View file

@ -2,7 +2,7 @@ package org.bitcoins.wallet
import org.bitcoins.core.hd.AddressType
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.wallet.api.HDWalletApi
import org.bitcoins.wallet.api.AnyHDWalletApi
import org.bitcoins.wallet.models.AccountDb
import scala.concurrent.Future
@ -11,7 +11,7 @@ import scala.concurrent.Future
* ScalaMock cannot stub traits with protected methods,
* so we need to stub them manually.
*/
abstract class MockWalletApi extends HDWalletApi {
abstract class MockWalletApi extends AnyHDWalletApi {
override protected[wallet] def getNewChangeAddress(
account: AccountDb): Future[BitcoinAddress] = stub

View file

@ -19,7 +19,7 @@ import org.bitcoins.feeprovider.BitcoinerLiveFeeRateProvider
import org.bitcoins.node._
import org.bitcoins.node.config.NodeAppConfig
import org.bitcoins.node.models.Peer
import org.bitcoins.wallet.api._
import org.bitcoins.wallet.Wallet
import org.bitcoins.wallet.config.WalletAppConfig
import scala.concurrent.{ExecutionContext, Future, Promise}
@ -157,11 +157,11 @@ object Main extends App with BitcoinSLogger {
//start everything!
runMain()
private def createCallbacks(wallet: WalletApi)(implicit
private def createCallbacks(wallet: Wallet)(implicit
nodeConf: NodeAppConfig,
ec: ExecutionContext): Future[NodeCallbacks] = {
lazy val onTx: OnTxReceived = { tx =>
wallet.processTransaction(tx, blockHash = None).map(_ => ())
wallet.processTransaction(tx, blockHashOpt = None).map(_ => ())
}
lazy val onCompactFilters: OnCompactFiltersReceived = { blockFilters =>
wallet
@ -192,7 +192,7 @@ object Main extends App with BitcoinSLogger {
}
}
private def addCallbacksAndBloomFilterToNode(node: Node, wallet: WalletApi)(
private def addCallbacksAndBloomFilterToNode(node: Node, wallet: Wallet)(
implicit
nodeAppConfig: NodeAppConfig,
ec: ExecutionContext): Future[Node] = {
@ -233,7 +233,7 @@ object Main extends App with BitcoinSLogger {
private def startHttpServer(
node: Node,
wallet: HDWalletApi,
wallet: Wallet,
rpcPortOpt: Option[Int])(implicit
system: ActorSystem,
conf: BitcoinSAppConfig): Future[Http.ServerBinding] = {

View file

@ -6,12 +6,12 @@ import akka.http.scaladsl.server._
import org.bitcoins.commons.serializers.Picklers._
import org.bitcoins.core.currency._
import org.bitcoins.node.Node
import org.bitcoins.wallet.api.HDWalletApi
import org.bitcoins.wallet.api.AnyHDWalletApi
import scala.concurrent.Future
import scala.util.{Failure, Success}
case class WalletRoutes(wallet: HDWalletApi, node: Node)(implicit
case class WalletRoutes(wallet: AnyHDWalletApi, node: Node)(implicit
system: ActorSystem)
extends ServerRoute {
import system.dispatcher

View file

@ -14,7 +14,7 @@ import org.bitcoins.testkit.node.{
NodeUnitTest
}
import org.bitcoins.testkit.wallet.BitcoinSWalletTest
import org.bitcoins.wallet.api.WalletApi
import org.bitcoins.wallet.Wallet
import org.scalatest.FutureOutcome
import scala.concurrent.{Future, Promise}
@ -42,8 +42,8 @@ class NeutrinoNodeWithWalletTest extends NodeUnitTest {
}
}
private var walletP: Promise[WalletApi] = Promise()
private var walletF: Future[WalletApi] = walletP.future
private var walletP: Promise[Wallet] = Promise()
private var walletF: Future[Wallet] = walletP.future
after {
//reset assertion after a test runs, because we
//are doing mutation to work around our callback

View file

@ -10,7 +10,7 @@ import org.bitcoins.crypto.AesPassword
import org.bitcoins.keymanager.KeyManagerUnlockError.MnemonicNotFound
import org.bitcoins.keymanager.{KeyManagerUnlockError, WalletStorage}
import org.bitcoins.testkit.wallet.BitcoinSWalletTest
import org.bitcoins.wallet.api.WalletApi.BlockMatchingResponse
import org.bitcoins.wallet.api.NeutrinoWalletApi.BlockMatchingResponse
import org.bitcoins.wallet.models.AddressDb
import org.scalatest.FutureOutcome
import org.scalatest.compatible.Assertion

View file

@ -52,7 +52,7 @@ import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success}
abstract class Wallet
extends HDWalletApi
extends AnyHDWalletApi
with UtxoHandling
with AddressHandling
with AccountHandling

View file

@ -3,12 +3,12 @@ package org.bitcoins.wallet.api
import org.bitcoins.commons.jsonmodels.wallet.CoinSelectionAlgo
import org.bitcoins.core.currency.CurrencyUnit
import org.bitcoins.core.hd.{AddressType, HDAccount, HDChainType, HDPurpose}
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.transaction.{
Transaction,
TransactionOutPoint,
TransactionOutput
}
import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp}
import org.bitcoins.core.wallet.fee.FeeUnit
import org.bitcoins.core.wallet.utxo.{AddressTag, TxoState}
import org.bitcoins.keymanager.KeyManagerParams
@ -492,47 +492,6 @@ trait HDWalletApi extends WalletApi {
ec: ExecutionContext): Future[Vector[AccountDb]] =
listAccounts().map(_.filter(_.hdAccount.purpose == purpose))
def rescanNeutrinoWallet(
account: HDAccount,
startOpt: Option[BlockStamp],
endOpt: Option[BlockStamp],
addressBatchSize: Int,
useCreationTime: Boolean): Future[Unit]
override def rescanNeutrinoWallet(
startOpt: Option[BlockStamp],
endOpt: Option[BlockStamp],
addressBatchSize: Int,
useCreationTime: Boolean)(implicit ec: ExecutionContext): Future[Unit] = {
for {
account <- getDefaultAccount()
_ <- rescanNeutrinoWallet(account.hdAccount,
startOpt,
endOpt,
addressBatchSize,
useCreationTime)
} yield ()
}
def fullRescanNeutrinoWallet(
account: HDAccount,
addressBatchSize: Int): Future[Unit] = {
rescanNeutrinoWallet(account = account,
startOpt = None,
endOpt = None,
addressBatchSize = addressBatchSize,
useCreationTime = false)
}
/** Helper method to rescan the ENTIRE blockchain. */
override def fullRescanNeutrinoWallet(addressBatchSize: Int)(implicit
ec: ExecutionContext): Future[Unit] = {
for {
account <- getDefaultAccount()
_ <- fullRescanNeutrinoWallet(account.hdAccount, addressBatchSize)
} yield ()
}
def createNewAccount(keyManagerParams: KeyManagerParams): Future[HDWalletApi]
/**

View file

@ -0,0 +1,101 @@
package org.bitcoins.wallet.api
import org.bitcoins.core.gcs.GolombFilter
import org.bitcoins.core.protocol.BlockStamp
import org.bitcoins.core.protocol.blockchain.Block
import org.bitcoins.core.protocol.script.ScriptPubKey
import org.bitcoins.crypto.{DoubleSha256Digest, DoubleSha256DigestBE}
import org.bitcoins.wallet.api.NeutrinoWalletApi.BlockMatchingResponse
import scala.concurrent.{ExecutionContext, Future}
trait NeutrinoWalletApi { self: WalletApi =>
/**
* Processes the give block, updating our DB state if it's relevant to us.
* @param block The block we're processing
*/
def processBlock(block: Block): Future[WalletApi]
def processCompactFilter(
blockHash: DoubleSha256Digest,
blockFilter: GolombFilter): Future[WalletApi] =
processCompactFilters(Vector((blockHash, blockFilter)))
def processCompactFilters(
blockFilters: Vector[(DoubleSha256Digest, GolombFilter)]): Future[
WalletApi]
/**
* Iterates over the block filters in order to find filters that match to the given addresses
*
* I queries the filter database for [[batchSize]] filters a time
* and tries to run [[GolombFilter.matchesAny]] for each filter.
*
* It tries to match the filters in parallel using [[parallelismLevel]] threads.
* For best results use it with a separate execution context.
*
* @param scripts list of [[ScriptPubKey]]'s to watch
* @param startOpt start point (if empty it starts with the genesis block)
* @param endOpt end point (if empty it ends with the best tip)
* @param batchSize number of filters that can be matched in one batch
* @param parallelismLevel max number of threads required to perform matching
* (default [[Runtime.getRuntime.availableProcessors()]])
* @return a list of matching block hashes
*/
def getMatchingBlocks(
scripts: Vector[ScriptPubKey],
startOpt: Option[BlockStamp] = None,
endOpt: Option[BlockStamp] = None,
batchSize: Int = 100,
parallelismLevel: Int = Runtime.getRuntime.availableProcessors())(implicit
ec: ExecutionContext): Future[Vector[BlockMatchingResponse]]
/**
* Recreates the account using BIP-157 approach
*
* DANGER! This method removes all records from the wallet database
* and creates new ones while the account discovery process.
*
* The Wallet UI should check if the database is empty before calling
* this method and let the end users to decide whether they want to proceed or not.
*
* This method generates [[addressBatchSize]] of addresses, then matches them against the BIP-158 compact filters,
* and downloads and processes the matched blocks. This method keeps doing the steps until there are [[WalletConfig.addressGapLimit]]
* or more unused addresses in a row. In this case it considers the discovery process completed.
*
* [[addressBatchSize]] - the number of addresses we should generate from a keychain to attempt to match in in a rescan
* [[WalletConfig.addressGapLimit]] - the number of addresses required to go without a match before we determine that our wallet is "discovered".
* For instance, if addressBatchSize=100, and AddressGapLimit=20 we do a rescan and the last address we find containing
* funds is at index 75, we would not generate more addresses to try and rescan. However if the last index containing
* funds was 81, we would generate another 100 addresses from the keychain and attempt to rescan those.
*
* @param startOpt start block (if None it starts from the genesis block)
* @param endOpt end block (if None it ends at the current tip)
* @param addressBatchSize how many addresses to match in a single pass
*/
def rescanNeutrinoWallet(
startOpt: Option[BlockStamp],
endOpt: Option[BlockStamp],
addressBatchSize: Int,
useCreationTime: Boolean)(implicit ec: ExecutionContext): Future[Unit]
/** Helper method to rescan the ENTIRE blockchain. */
def fullRescanNeutrinoWallet(addressBatchSize: Int)(implicit
ec: ExecutionContext): Future[Unit] =
rescanNeutrinoWallet(startOpt = None,
endOpt = None,
addressBatchSize = addressBatchSize,
useCreationTime = false)
def discoveryBatchSize(): Int = 25
}
object NeutrinoWalletApi {
case class BlockMatchingResponse(
blockHash: DoubleSha256DigestBE,
blockHeight: Int)
}

View file

@ -0,0 +1,26 @@
package org.bitcoins.wallet.api
import org.bitcoins.core.bloom.BloomFilter
import scala.concurrent.Future
/**
* API for the wallet project.
*
* This wallet API is BIP44 compliant.
*
* @see [[https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki BIP44]]
*/
trait SpvWalletApi { self: WalletApi =>
/**
* Recreates the account using BIP-44 approach
*/
def rescanSPVWallet(): Future[Unit]
/**
* Retrieves a bloom filter that that can be sent to a P2P network node
* to get information about our transactions, pubkeys and scripts.
*/
def getBloomFilter(): Future[BloomFilter]
}

View file

@ -4,26 +4,22 @@ import java.time.Instant
import org.bitcoins.commons.jsonmodels.wallet.CoinSelectionAlgo
import org.bitcoins.core.api.{ChainQueryApi, FeeRateApi, NodeApi}
import org.bitcoins.core.bloom.BloomFilter
import org.bitcoins.core.config.NetworkParameters
import org.bitcoins.core.currency.CurrencyUnit
import org.bitcoins.core.gcs.GolombFilter
import org.bitcoins.core.hd.AddressType
import org.bitcoins.core.protocol.blockchain.{Block, BlockHeader}
import org.bitcoins.core.protocol.script.ScriptPubKey
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.blockchain.BlockHeader
import org.bitcoins.core.protocol.transaction.{
Transaction,
TransactionOutPoint,
TransactionOutput
}
import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp}
import org.bitcoins.core.util.FutureUtil
import org.bitcoins.core.wallet.fee.FeeUnit
import org.bitcoins.core.wallet.utxo.{AddressTag, TxoState}
import org.bitcoins.crypto.{DoubleSha256Digest, DoubleSha256DigestBE}
import org.bitcoins.crypto.DoubleSha256DigestBE
import org.bitcoins.keymanager._
import org.bitcoins.wallet.WalletLogger
import org.bitcoins.wallet.api.WalletApi.BlockMatchingResponse
import org.bitcoins.wallet.models.{AddressDb, SpendingInfoDb}
import scala.concurrent.{ExecutionContext, Future}
@ -50,12 +46,6 @@ trait WalletApi extends WalletLogger {
def stop(): Unit
/**
* Retrieves a bloom filter that that can be sent to a P2P network node
* to get information about our transactions, pubkeys and scripts.
*/
def getBloomFilter(): Future[BloomFilter]
/**
* Processes the given transaction, updating our DB state if it's relevant to us.
* @param transaction The transaction we're processing
@ -82,21 +72,6 @@ trait WalletApi extends WalletLogger {
def updateUtxoPendingStates(
blockHeader: BlockHeader): Future[Vector[SpendingInfoDb]]
/**
* Processes the give block, updating our DB state if it's relevant to us.
* @param block The block we're processing
*/
def processBlock(block: Block): Future[WalletApi]
def processCompactFilter(
blockHash: DoubleSha256Digest,
blockFilter: GolombFilter): Future[WalletApi] =
processCompactFilters(Vector((blockHash, blockFilter)))
def processCompactFilters(
blockFilters: Vector[(DoubleSha256Digest, GolombFilter)]): Future[
WalletApi]
/** Gets the sum of all UTXOs in this wallet */
def getBalance()(implicit ec: ExecutionContext): Future[CurrencyUnit] = {
val confirmedF = getConfirmedBalance()
@ -130,13 +105,6 @@ trait WalletApi extends WalletLogger {
def getUnconfirmedBalance(tag: AddressTag): Future[CurrencyUnit]
/**
* If a UTXO is spent outside of the wallet, we
* need to remove it from the database so it won't be
* attempted spent again by us.
*/
// def updateUtxo: Future[WalletApi]
/** Lists unspent transaction outputs in the wallet
* @return Vector[SpendingInfoDb]
*/
@ -237,72 +205,6 @@ trait WalletApi extends WalletLogger {
protected[wallet] def getNewChangeAddress()(implicit
ec: ExecutionContext): Future[BitcoinAddress]
/**
* Iterates over the block filters in order to find filters that match to the given addresses
*
* I queries the filter database for [[batchSize]] filters a time
* and tries to run [[GolombFilter.matchesAny]] for each filter.
*
* It tries to match the filters in parallel using [[parallelismLevel]] threads.
* For best results use it with a separate execution context.
*
* @param scripts list of [[ScriptPubKey]]'s to watch
* @param startOpt start point (if empty it starts with the genesis block)
* @param endOpt end point (if empty it ends with the best tip)
* @param batchSize number of filters that can be matched in one batch
* @param parallelismLevel max number of threads required to perform matching
* (default [[Runtime.getRuntime.availableProcessors()]])
* @return a list of matching block hashes
*/
def getMatchingBlocks(
scripts: Vector[ScriptPubKey],
startOpt: Option[BlockStamp] = None,
endOpt: Option[BlockStamp] = None,
batchSize: Int = 100,
parallelismLevel: Int = Runtime.getRuntime.availableProcessors())(implicit
ec: ExecutionContext): Future[Vector[BlockMatchingResponse]]
/**
* Recreates the account using BIP-157 approach
*
* DANGER! This method removes all records from the wallet database
* and creates new ones while the account discovery process.
*
* The Wallet UI should check if the database is empty before calling
* this method and let the end users to decide whether they want to proceed or not.
*
* This method generates [[addressBatchSize]] of addresses, then matches them against the BIP-158 compact filters,
* and downloads and processes the matched blocks. This method keeps doing the steps until there are [[WalletConfig.addressGapLimit]]
* or more unused addresses in a row. In this case it considers the discovery process completed.
*
* [[addressBatchSize]] - the number of addresses we should generate from a keychain to attempt to match in in a rescan
* [[WalletConfig.addressGapLimit]] - the number of addresses required to go without a match before we determine that our wallet is "discovered".
* For instance, if addressBatchSize=100, and AddressGapLimit=20 we do a rescan and the last address we find containing
* funds is at index 75, we would not generate more addresses to try and rescan. However if the last index containing
* funds was 81, we would generate another 100 addresses from the keychain and attempt to rescan those.
*
* @param startOpt start block (if None it starts from the genesis block)
* @param endOpt end block (if None it ends at the current tip)
* @param addressBatchSize how many addresses to match in a single pass
*/
def rescanNeutrinoWallet(
startOpt: Option[BlockStamp],
endOpt: Option[BlockStamp],
addressBatchSize: Int,
useCreationTime: Boolean)(implicit ec: ExecutionContext): Future[Unit]
/** Helper method to rescan the ENTIRE blockchain. */
def fullRescanNeutrinoWallet(addressBatchSize: Int)(implicit
ec: ExecutionContext): Future[Unit]
/**
* Recreates the account using BIP-44 approach
*/
def rescanSPVWallet(): Future[Unit]
def discoveryBatchSize(): Int = 25
def keyManager: KeyManager
protected def determineFeeRate(feeRateOpt: Option[FeeUnit]): Future[FeeUnit] =
@ -426,10 +328,14 @@ trait WalletApi extends WalletLogger {
}
object WalletApi {
/** An HDWallet that uses Neutrino to sync */
trait NeutrinoHDWalletApi extends HDWalletApi with NeutrinoWalletApi
case class BlockMatchingResponse(
blockHash: DoubleSha256DigestBE,
blockHeight: Int)
/** An HDWallet that uses SPV to sync */
trait SpvHDWalletApi extends HDWalletApi with SpvWalletApi
}
/** An HDWallet that supports both Neutrino and SPV methods of syncing */
trait AnyHDWalletApi
extends HDWalletApi
with NeutrinoWalletApi
with SpvWalletApi

View file

@ -14,7 +14,6 @@ import org.bitcoins.keymanager.{
KeyManagerParams,
WalletStorage
}
import org.bitcoins.wallet.api.HDWalletApi
import org.bitcoins.wallet.db.WalletDbManagement
import org.bitcoins.wallet.models.AccountDAO
import org.bitcoins.wallet.{Wallet, WalletLogger}
@ -162,7 +161,7 @@ case class WalletAppConfig(
chainQueryApi: ChainQueryApi,
feeRateApi: FeeRateApi,
bip39PasswordOpt: Option[String])(implicit
ec: ExecutionContext): Future[HDWalletApi] = {
ec: ExecutionContext): Future[Wallet] = {
WalletAppConfig.createHDWallet(
nodeApi = nodeApi,
chainQueryApi = chainQueryApi,
@ -194,7 +193,7 @@ object WalletAppConfig
feeRateApi: FeeRateApi,
bip39PasswordOpt: Option[String])(implicit
walletConf: WalletAppConfig,
ec: ExecutionContext): Future[HDWalletApi] = {
ec: ExecutionContext): Future[Wallet] = {
walletConf.hasWallet().flatMap { walletExists =>
if (walletExists) {
logger.info(s"Using pre-existing wallet")

View file

@ -9,9 +9,9 @@ import org.bitcoins.core.protocol.BlockStamp.BlockHeight
import org.bitcoins.core.protocol.script.ScriptPubKey
import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp}
import org.bitcoins.core.util.FutureUtil
import org.bitcoins.wallet.api.WalletApi.BlockMatchingResponse
import org.bitcoins.wallet.{Wallet, WalletLogger}
import org.bitcoins.crypto.DoubleSha256Digest
import org.bitcoins.wallet.api.NeutrinoWalletApi.BlockMatchingResponse
import org.bitcoins.wallet.{Wallet, WalletLogger}
import scala.concurrent.{ExecutionContext, Future}
@ -23,6 +23,22 @@ private[wallet] trait RescanHandling extends WalletLogger {
/** @inheritdoc */
override def rescanNeutrinoWallet(
startOpt: Option[BlockStamp],
endOpt: Option[BlockStamp],
addressBatchSize: Int,
useCreationTime: Boolean)(implicit ec: ExecutionContext): Future[Unit] = {
for {
account <- getDefaultAccount()
_ <- rescanNeutrinoWallet(account.hdAccount,
startOpt,
endOpt,
addressBatchSize,
useCreationTime)
} yield ()
}
/** @inheritdoc */
def rescanNeutrinoWallet(
account: HDAccount,
startOpt: Option[BlockStamp],
endOpt: Option[BlockStamp],