diff --git a/app/server-routes/src/main/scala/org/bitcoins/server/routes/ServerRoute.scala b/app/server-routes/src/main/scala/org/bitcoins/server/routes/ServerRoute.scala index 7a680ee563..0495cf19c9 100644 --- a/app/server-routes/src/main/scala/org/bitcoins/server/routes/ServerRoute.scala +++ b/app/server-routes/src/main/scala/org/bitcoins/server/routes/ServerRoute.scala @@ -1,6 +1,6 @@ package org.bitcoins.server.routes -import org.apache.pekko.http.scaladsl.server.Directives.{complete, provide} +import org.apache.pekko.http.scaladsl.server.Directives import org.apache.pekko.http.scaladsl.server.{Directive1, Route} import scala.util.Try @@ -10,7 +10,7 @@ trait ServerRoute { def withValidServerCommand[R](validator: Try[R]): Directive1[R] = validator.fold( - e => complete(Server.httpBadRequest(e)), - provide + { (e: Throwable) => Directives.complete(Server.httpBadRequest(e)) }, + { (r: R) => Directives.provide(r) } ) } diff --git a/app/server-test/src/test/scala/org/bitcoins/server/CallBackUtilTest.scala b/app/server-test/src/test/scala/org/bitcoins/server/CallBackUtilTest.scala index 8afe136bc0..57d9aedd0e 100644 --- a/app/server-test/src/test/scala/org/bitcoins/server/CallBackUtilTest.scala +++ b/app/server-test/src/test/scala/org/bitcoins/server/CallBackUtilTest.scala @@ -24,7 +24,7 @@ class CallBackUtilTest extends BitcoinSWalletTest { it must "have the kill switch kill messages to the createBitcoindNodeCallbacksForWallet callback" in { fundedWallet => val wallet = fundedWallet.wallet - val addressF = wallet.getNewAddress() + val addressF = wallet.addressHandling.getNewAddress() val initBalanceF = wallet.getBalance() val tx1F = addressF.map { addr => TransactionGenerators @@ -65,7 +65,7 @@ class CallBackUtilTest extends BitcoinSWalletTest { it must "have the kill switch kill messages to the createNeutrinoNodeCallbacksForWallet callback" in { fundedWallet => val wallet = fundedWallet.wallet - val addressF = wallet.getNewAddress() + val addressF = wallet.addressHandling.getNewAddress() val initBalanceF = wallet.getBalance() val tx1F = addressF.map { addr => TransactionGenerators diff --git a/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala b/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala index db3f0ae5bf..5d8df37d8c 100644 --- a/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala +++ b/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala @@ -11,6 +11,7 @@ import org.bitcoins.core.api.chain.db.* import org.bitcoins.core.api.wallet.db.* import org.bitcoins.core.api.wallet.{ AccountHandlingApi, + AddressHandlingApi, AddressInfo, CoinSelectionAlgo, RescanHandlingApi @@ -86,6 +87,8 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory { val mockAccountHandlingApi: AccountHandlingApi = mock[AccountHandlingApi] + val mockAddressHandlingApi: AddressHandlingApi = mock[AddressHandlingApi] + val walletHolder = new WalletHolder(Some(mockWalletApi)) val feeRateApi = ConstantFeeRateProvider(SatoshisPerVirtualByte.one) @@ -553,7 +556,12 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory { testAddress.scriptPubKey ) - (() => mockWalletApi.listAddresses()) + (() => mockWalletApi.addressHandling) + .expects() + .returning(mockAddressHandlingApi) + .anyNumberOfTimes() + + (() => mockWalletApi.addressHandling.listAddresses()) .expects() .returning(Future.successful(Vector(addressDb))) @@ -579,7 +587,12 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory { testAddress.scriptPubKey ) - (() => mockWalletApi.listSpentAddresses()) + (() => mockWalletApi.addressHandling) + .expects() + .returning(mockAddressHandlingApi) + .anyNumberOfTimes() + + (() => mockWalletApi.addressHandling.listSpentAddresses()) .expects() .returning(Future.successful(Vector(addressDb))) @@ -605,7 +618,12 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory { testAddress.scriptPubKey ) - (() => mockWalletApi.listFundedAddresses()) + (() => mockWalletApi.addressHandling) + .expects() + .returning(mockAddressHandlingApi) + .anyNumberOfTimes() + + (() => mockWalletApi.addressHandling.listFundedAddresses()) .expects() .returning(Future.successful(Vector((addressDb, Satoshis.zero)))) @@ -630,7 +648,12 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory { testAddress.scriptPubKey ) - (() => mockWalletApi.listUnusedAddresses()) + (() => mockWalletApi.addressHandling) + .expects() + .returning(mockAddressHandlingApi) + .anyNumberOfTimes() + + (() => mockWalletApi.addressHandling.listUnusedAddresses()) .expects() .returning(Future.successful(Vector(addressDb))) @@ -682,7 +705,12 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory { } "return a new address" in { - (() => mockWalletApi.getNewAddress()) + (() => mockWalletApi.addressHandling) + .expects() + .returning(mockAddressHandlingApi) + .anyNumberOfTimes() + + (() => mockWalletApi.addressHandling.getNewAddress()) .expects() .returning(Future.successful(testAddress)) @@ -704,7 +732,12 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory { val key = ECPublicKey.freshPublicKey val hdPath = HDPath.fromString("m/84'/1'/0'/0/0") - (mockWalletApi + (() => mockWalletApi.addressHandling) + .expects() + .returning(mockAddressHandlingApi) + .anyNumberOfTimes() + + (mockWalletApi.addressHandling .getAddressInfo(_: BitcoinAddress)) .expects(testAddress) .returning(Future.successful(Some(AddressInfo(key, RegTest, hdPath)))) @@ -725,7 +758,13 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory { } "return a new address with a label" in { - (mockWalletApi + + (() => mockWalletApi.addressHandling) + .expects() + .returning(mockAddressHandlingApi) + .anyNumberOfTimes() + + (mockWalletApi.addressHandling .getNewAddress(_: Vector[AddressTag])) .expects(Vector(testLabel)) .returning(Future.successful(testAddress)) @@ -816,7 +855,12 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory { } "label an address" in { - (mockWalletApi + (() => mockWalletApi.addressHandling) + .expects() + .returning(mockAddressHandlingApi) + .anyNumberOfTimes() + + (mockWalletApi.addressHandling .tagAddress(_: BitcoinAddress, _: AddressTag)) .expects(testAddress, testLabel) .returning(Future.successful(AddressTagDb(testAddress, testLabel))) @@ -840,7 +884,12 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory { } "get address tags" in { - (mockWalletApi + (() => mockWalletApi.addressHandling) + .expects() + .returning(mockAddressHandlingApi) + .anyNumberOfTimes() + + (mockWalletApi.addressHandling .getAddressTags(_: BitcoinAddress)) .expects(testAddress) .returning( @@ -863,7 +912,12 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory { } "get address label" in { - (mockWalletApi + (() => mockWalletApi.addressHandling) + .expects() + .returning(mockAddressHandlingApi) + .anyNumberOfTimes() + + (mockWalletApi.addressHandling .getAddressTags(_: BitcoinAddress, _: AddressTagType)) .expects(testAddress, AddressLabelTagType) .returning( @@ -886,7 +940,12 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory { } "get address labels" in { - (() => mockWalletApi.getAddressTags()) + (() => mockWalletApi.addressHandling) + .expects() + .returning(mockAddressHandlingApi) + .anyNumberOfTimes() + + (() => mockWalletApi.addressHandling.getAddressTags()) .expects() .returning( Future.successful(Vector(AddressTagDb(testAddress, testLabel))) @@ -905,7 +964,13 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory { "drop address label" in { val labelName = "label" - (mockWalletApi + + (() => mockWalletApi.addressHandling) + .expects() + .returning(mockAddressHandlingApi) + .anyNumberOfTimes() + + (mockWalletApi.addressHandling .dropAddressTagName(_: BitcoinAddress, _: AddressTagName)) .expects(testAddress, AddressLabelTagName(labelName)) .returning(Future.successful(1)) @@ -929,7 +994,12 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory { } "drop address labels with no labels" in { - (mockWalletApi + (() => mockWalletApi.addressHandling) + .expects() + .returning(mockAddressHandlingApi) + .anyNumberOfTimes() + + (mockWalletApi.addressHandling .dropAddressTagType(_: BitcoinAddress, _: AddressTagType)) .expects(testAddress, AddressLabelTagType) .returning(Future.successful(0)) @@ -948,7 +1018,12 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory { } "drop address labels with 1 label" in { - (mockWalletApi + (() => mockWalletApi.addressHandling) + .expects() + .returning(mockAddressHandlingApi) + .anyNumberOfTimes() + + (mockWalletApi.addressHandling .dropAddressTagType(_: BitcoinAddress, _: AddressTagType)) .expects(testAddress, AddressLabelTagType) .returning(Future.successful(1)) @@ -969,7 +1044,12 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory { } "drop address labels with 2 labels" in { - (mockWalletApi + (() => mockWalletApi.addressHandling) + .expects() + .returning(mockAddressHandlingApi) + .anyNumberOfTimes() + + (mockWalletApi.addressHandling .dropAddressTagType(_: BitcoinAddress, _: AddressTagType)) .expects(testAddress, AddressLabelTagType) .returning(Future.successful(2)) diff --git a/app/server-test/src/test/scala/org/bitcoins/server/WalletRoutesSpec.scala b/app/server-test/src/test/scala/org/bitcoins/server/WalletRoutesSpec.scala index 1da7d38b78..7f003bd6ca 100644 --- a/app/server-test/src/test/scala/org/bitcoins/server/WalletRoutesSpec.scala +++ b/app/server-test/src/test/scala/org/bitcoins/server/WalletRoutesSpec.scala @@ -4,6 +4,7 @@ import org.apache.pekko.http.scaladsl.model.ContentTypes.* import org.apache.pekko.http.scaladsl.testkit.ScalatestRouteTest import org.bitcoins.commons.serializers.Picklers import org.bitcoins.core.api.chain.ChainApi +import org.bitcoins.core.api.wallet.AccountHandlingApi import org.bitcoins.core.api.wallet.db.AccountDb import org.bitcoins.core.crypto.ExtKeyVersion.SegWitMainNetPriv import org.bitcoins.core.crypto.ExtPrivateKey @@ -39,6 +40,7 @@ class WalletRoutesSpec val mockNode: Node = mock[Node] val mockWalletApi: MockWalletApi = mock[MockWalletApi] + val mockAccountHandlingApi: AccountHandlingApi = mock[AccountHandlingApi] val walletHolder = new WalletHolder(Some(mockWalletApi)) @@ -167,12 +169,18 @@ class WalletRoutesSpec val extPubKey = extPrivKey.extPublicKey val hdAccount = HDAccount.fromExtKeyVersion(version = keyVersion, idx = 0) val accountDb = AccountDb(extPubKey, hdAccount = hdAccount) - (mockWalletApi + + (() => mockWalletApi.accountHandling) + .expects() + .returning(mockAccountHandlingApi) + .anyNumberOfTimes() + + (mockWalletApi.accountHandling .createNewAccount(_: HDPurpose)) .expects(HDPurpose.default) - .returning(Future.successful(mockWalletApi)) + .returning(Future.successful(extPubKey)) - (() => mockWalletApi.listAccounts()) + (() => mockWalletApi.accountHandling.listAccounts()) .expects() .returning(Future.successful(Vector(accountDb))) diff --git a/app/server-test/src/test/scala/org/bitcoins/wallet/MockWalletApi.scala b/app/server-test/src/test/scala/org/bitcoins/wallet/MockWalletApi.scala index 9be23a73eb..1f2d05ccb6 100644 --- a/app/server-test/src/test/scala/org/bitcoins/wallet/MockWalletApi.scala +++ b/app/server-test/src/test/scala/org/bitcoins/wallet/MockWalletApi.scala @@ -3,7 +3,6 @@ package org.bitcoins.wallet import org.bitcoins.core.api.dlc.wallet.DLCNeutrinoHDWalletApi import org.bitcoins.core.api.wallet.db.AccountDb import org.bitcoins.core.hd.AddressType -import org.bitcoins.core.protocol.BitcoinAddress import scala.concurrent.Future @@ -12,9 +11,6 @@ import scala.concurrent.Future */ abstract class MockWalletApi extends DLCNeutrinoHDWalletApi { - override def getNewChangeAddress(account: AccountDb): Future[BitcoinAddress] = - stub - override def getDefaultAccount(): Future[AccountDb] = stub override def getDefaultAccountForType( diff --git a/app/server/src/main/scala/org/bitcoins/server/WalletRoutes.scala b/app/server/src/main/scala/org/bitcoins/server/WalletRoutes.scala index a02112a80e..b41d6bb3b6 100644 --- a/app/server/src/main/scala/org/bitcoins/server/WalletRoutes.scala +++ b/app/server/src/main/scala/org/bitcoins/server/WalletRoutes.scala @@ -177,9 +177,9 @@ case class WalletRoutes(loadWalletApi: DLCWalletLoaderApi)(implicit complete { val addressF = labelOpt match { case Some(label) => - wallet.getNewAddress(Vector(label)) + wallet.addressHandling.getNewAddress(Vector(label)) case None => - wallet.getNewAddress() + wallet.addressHandling.getNewAddress() } addressF.map(address => Server.httpSuccess(address)) } @@ -231,7 +231,7 @@ case class WalletRoutes(loadWalletApi: DLCWalletLoaderApi)(implicit withValidServerCommand(LabelAddress.fromJsArr(arr)) { case LabelAddress(address, label) => complete { - wallet.tagAddress(address, label).map { tagDb => + wallet.addressHandling.tagAddress(address, label).map { tagDb => Server.httpSuccess( s"Added label \'${tagDb.tagName.name}\' to ${tagDb.address.value}" ) @@ -243,7 +243,7 @@ case class WalletRoutes(loadWalletApi: DLCWalletLoaderApi)(implicit withValidServerCommand(GetAddressTags.fromJsArr(arr)) { case GetAddressTags(address) => complete { - wallet.getAddressTags(address).map { tagDbs => + wallet.addressHandling.getAddressTags(address).map { tagDbs => val retStr = tagDbs.map(_.tagName.name) Server.httpSuccess(retStr) } @@ -254,16 +254,18 @@ case class WalletRoutes(loadWalletApi: DLCWalletLoaderApi)(implicit withValidServerCommand(GetAddressLabel.fromJsArr(arr)) { case GetAddressLabel(address) => complete { - wallet.getAddressTags(address, AddressLabelTagType).map { tagDbs => - val retStr = tagDbs.map(_.tagName.name) - Server.httpSuccess(retStr) - } + wallet.addressHandling + .getAddressTags(address, AddressLabelTagType) + .map { tagDbs => + val retStr = tagDbs.map(_.tagName.name) + Server.httpSuccess(retStr) + } } } case ServerCommand("getaddresslabels", _) => complete { - val allTagsF = wallet.getAddressTags() + val allTagsF = wallet.addressHandling.getAddressTags() for { allTags <- allTagsF grouped = allTags.groupBy(_.address) @@ -284,7 +286,8 @@ case class WalletRoutes(loadWalletApi: DLCWalletLoaderApi)(implicit case DropAddressLabel(address, label) => complete { val tagName = AddressLabelTagName(label) - val droppedF = wallet.dropAddressTagName(address, tagName) + val droppedF = + wallet.addressHandling.dropAddressTagName(address, tagName) droppedF.map(handleTagResponse) } } @@ -293,7 +296,8 @@ case class WalletRoutes(loadWalletApi: DLCWalletLoaderApi)(implicit case DropAddressLabels(address) => complete { val droppedF = - wallet.dropAddressTagType(address, AddressLabelTagType) + wallet.addressHandling.dropAddressTagType(address, + AddressLabelTagType) droppedF.map(handleTagResponse) } } @@ -803,7 +807,7 @@ case class WalletRoutes(loadWalletApi: DLCWalletLoaderApi)(implicit case ServerCommand("getaddresses", _) => complete { - wallet.listAddresses().map { addressDbs => + wallet.addressHandling.listAddresses().map { addressDbs => val addresses = addressDbs.map(_.address) Server.httpSuccess(addresses) } @@ -811,7 +815,7 @@ case class WalletRoutes(loadWalletApi: DLCWalletLoaderApi)(implicit case ServerCommand("getspentaddresses", _) => complete { - wallet.listSpentAddresses().map { addressDbs => + wallet.addressHandling.listSpentAddresses().map { addressDbs => val addresses = addressDbs.map(_.address) Server.httpSuccess(addresses) } @@ -819,7 +823,7 @@ case class WalletRoutes(loadWalletApi: DLCWalletLoaderApi)(implicit case ServerCommand("getfundedaddresses", _) => complete { - wallet.listFundedAddresses().map { addressDbs => + wallet.addressHandling.listFundedAddresses().map { addressDbs => val addressAndValues = addressDbs.map { case (addressDb, value) => Obj( "address" -> Str(addressDb.address.value), @@ -833,7 +837,7 @@ case class WalletRoutes(loadWalletApi: DLCWalletLoaderApi)(implicit case ServerCommand("getunusedaddresses", _) => complete { - wallet.listUnusedAddresses().map { addressDbs => + wallet.addressHandling.listUnusedAddresses().map { addressDbs => val addresses = addressDbs.map(_.address) Server.httpSuccess(addresses) } @@ -851,7 +855,7 @@ case class WalletRoutes(loadWalletApi: DLCWalletLoaderApi)(implicit withValidServerCommand(GetAddressInfo.fromJsArr(arr)) { case GetAddressInfo(address) => complete { - wallet.getAddressInfo(address).map { + wallet.addressHandling.getAddressInfo(address).map { case Some(addressInfo) => val json = Obj( "pubkey" -> Str(addressInfo.pubkey.hex), @@ -869,8 +873,8 @@ case class WalletRoutes(loadWalletApi: DLCWalletLoaderApi)(implicit case CreateNewAccount(purpose) => complete { for { - newWallet <- wallet.createNewAccount(purpose) - accounts <- newWallet.listAccounts() + _ <- wallet.accountHandling.createNewAccount(purpose) + accounts <- wallet.accountHandling.listAccounts() } yield { val xpubs = accounts.map(_.xpub) val json = diff --git a/core/src/main/scala/org/bitcoins/core/api/wallet/AccountHandlingApi.scala b/core/src/main/scala/org/bitcoins/core/api/wallet/AccountHandlingApi.scala index ad35d5ffe7..a5df4802e7 100644 --- a/core/src/main/scala/org/bitcoins/core/api/wallet/AccountHandlingApi.scala +++ b/core/src/main/scala/org/bitcoins/core/api/wallet/AccountHandlingApi.scala @@ -1,19 +1,80 @@ package org.bitcoins.core.api.wallet -import org.bitcoins.core.api.wallet.db.AccountDb -import org.bitcoins.core.hd.{AddressType, HDAccount} +import org.bitcoins.core.api.wallet.db.{AccountDb, AddressDb} +import org.bitcoins.core.crypto.ExtPublicKey +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.script.ScriptPubKey -import scala.concurrent.Future +import scala.concurrent.{ExecutionContext, Future} trait AccountHandlingApi { + + /** Tries to create a new account in this wallet. Fails if the most recent + * account has no transaction history, as per BIP44 + * + * @see + * [[https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#account BIP44 account section]] + */ + def createNewAccount( + hdAccount: HDAccount + ): Future[ExtPublicKey] + + def createNewAccount(purpose: HDPurpose): Future[ExtPublicKey] + def getDefaultAccount(): Future[AccountDb] def listAccounts(): Future[Vector[AccountDb]] def getDefaultAccountForType(addressType: AddressType): Future[AccountDb] + def getNewAddress( + account: AccountDb, + chainType: HDChainType + ): Future[BitcoinAddress] + + final def getNewAddress(account: HDAccount)(implicit + ec: ExecutionContext): Future[BitcoinAddress] = { + val accountDbOptF = findAccount(account) + accountDbOptF.flatMap { + case Some(accountDb) => getNewAddress(accountDb) + case None => + Future.failed( + new RuntimeException( + s"No account found for given hdaccount=${account}" + ) + ) + } + } + def getNewAddress(accountDb: AccountDb): Future[BitcoinAddress] + + final def getNewChangeAddress(account: HDAccount)(implicit + ec: ExecutionContext): 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}" + ) + ) + } + } + def getNewChangeAddress(accountDb: AccountDb): Future[BitcoinAddress] def clearUtxos(account: HDAccount): Future[Unit] + def findAccount(account: HDAccount): Future[Option[AccountDb]] def generateScriptPubKeys( account: HDAccount, addressBatchSize: Int, forceGenerateSpks: Boolean ): Future[Vector[ScriptPubKey]] + def listUnusedAddresses( + account: HDAccount + ): Future[Vector[AddressDb]] + def listSpentAddresses( + account: HDAccount + ): Future[Vector[AddressDb]] + def listAddresses(account: HDAccount): Future[Vector[AddressDb]] + def listFundedAddresses( + account: HDAccount + ): Future[Vector[(AddressDb, CurrencyUnit)]] } diff --git a/core/src/main/scala/org/bitcoins/core/api/wallet/AddressHandlingApi.scala b/core/src/main/scala/org/bitcoins/core/api/wallet/AddressHandlingApi.scala new file mode 100644 index 0000000000..e631e029b0 --- /dev/null +++ b/core/src/main/scala/org/bitcoins/core/api/wallet/AddressHandlingApi.scala @@ -0,0 +1,74 @@ +package org.bitcoins.core.api.wallet + +import org.bitcoins.core.api.wallet.db.{AddressDb, AddressTagDb, ScriptPubKeyDb} +import org.bitcoins.core.currency.CurrencyUnit +import org.bitcoins.core.hd.AddressType +import org.bitcoins.core.protocol.BitcoinAddress +import org.bitcoins.core.protocol.script.ScriptPubKey +import org.bitcoins.core.protocol.transaction.{ + Transaction, + TransactionOutPoint, + TransactionOutput +} +import org.bitcoins.core.wallet.utxo.{ + AddressTag, + AddressTagName, + AddressTagType +} + +import scala.concurrent.Future + +trait AddressHandlingApi { + def dropAddressTag(addressTagDb: AddressTagDb): Future[Int] + def dropAddressTagType( + addressTagType: AddressTagType + ): Future[Int] + def dropAddressTagType( + address: BitcoinAddress, + addressTagType: AddressTagType + ): Future[Int] + def dropAddressTagName( + address: BitcoinAddress, + addressTagName: AddressTagName + ): Future[Int] + + /** Given a transaction, returns the outputs (with their corresponding + * outpoints) that pay to this wallet + */ + def findOurOutputs( + transaction: Transaction + ): Future[Vector[(TransactionOutput, TransactionOutPoint)]] + def getNewAddress(): Future[BitcoinAddress] + def getNewAddress( + tags: Vector[AddressTag] + ): Future[BitcoinAddress] + def getNewAddress( + addressType: AddressType + ): Future[BitcoinAddress] + def getNewChangeAddress(): Future[BitcoinAddress] + def getNewAddress( + addressType: AddressType, + tags: Vector[AddressTag] + ): Future[BitcoinAddress] + def getAddressInfo( + address: BitcoinAddress + ): Future[Option[AddressInfo]] + def getAddressTags(): Future[Vector[AddressTagDb]] + def getAddressTags(address: BitcoinAddress): Future[Vector[AddressTagDb]] + def getAddressTags( + address: BitcoinAddress, + tagType: AddressTagType + ): Future[Vector[AddressTagDb]] + def listAddresses(): Future[Vector[AddressDb]] + def listUnusedAddresses(): Future[Vector[AddressDb]] + def listScriptPubKeys(): Future[Vector[ScriptPubKeyDb]] + def listSpentAddresses(): Future[Vector[AddressDb]] + def listFundedAddresses(): Future[Vector[(AddressDb, CurrencyUnit)]] + def tagAddress( + address: BitcoinAddress, + tag: AddressTag + ): Future[AddressTagDb] + def watchScriptPubKey( + scriptPubKey: ScriptPubKey + ): Future[ScriptPubKeyDb] +} diff --git a/core/src/main/scala/org/bitcoins/core/api/wallet/HDWalletApi.scala b/core/src/main/scala/org/bitcoins/core/api/wallet/HDWalletApi.scala index 08274d891c..66857c722c 100644 --- a/core/src/main/scala/org/bitcoins/core/api/wallet/HDWalletApi.scala +++ b/core/src/main/scala/org/bitcoins/core/api/wallet/HDWalletApi.scala @@ -1,9 +1,9 @@ package org.bitcoins.core.api.wallet import org.bitcoins.core.api.keymanager.BIP39KeyManagerApi -import org.bitcoins.core.api.wallet.db.{AccountDb, AddressDb, SpendingInfoDb} +import org.bitcoins.core.api.wallet.db.{AccountDb, SpendingInfoDb} import org.bitcoins.core.currency.CurrencyUnit -import org.bitcoins.core.hd.{AddressType, HDAccount, HDChainType, HDPurpose} +import org.bitcoins.core.hd.{AddressType, HDAccount, HDPurpose} import org.bitcoins.core.protocol.BitcoinAddress import org.bitcoins.core.protocol.transaction.{ Transaction, @@ -33,6 +33,7 @@ trait HDWalletApi extends WalletApi { def accountHandling: AccountHandlingApi def fundTxHandling: FundTransactionHandlingApi def rescanHandling: RescanHandlingApi + def addressHandling: AddressHandlingApi /** Gets the balance of the given account */ def getBalance(account: HDAccount)(implicit @@ -51,21 +52,6 @@ trait HDWalletApi extends WalletApi { def getUnconfirmedBalance(account: HDAccount): Future[CurrencyUnit] - def getNewAddress(account: HDAccount): Future[BitcoinAddress] - - def getNewAddress(account: AccountDb): Future[BitcoinAddress] - - /** Generates a new change address */ - def getNewChangeAddress(account: AccountDb): Future[BitcoinAddress] - - override def getNewChangeAddress()(implicit - ec: ExecutionContext): Future[BitcoinAddress] = { - for { - account <- getDefaultAccount() - addr <- getNewChangeAddress(account) - } yield addr - } - /** Fetches the default account from the DB * @return * Future[AccountDb] @@ -443,40 +429,12 @@ trait HDWalletApi extends WalletApi { } yield tx } - def listAddresses(account: HDAccount): Future[Vector[AddressDb]] - - def listSpentAddresses(account: HDAccount): Future[Vector[AddressDb]] - - def listFundedAddresses( - account: HDAccount): Future[Vector[(AddressDb, CurrencyUnit)]] - - def listUnusedAddresses(account: HDAccount): Future[Vector[AddressDb]] - def listDefaultAccountUtxos(): Future[Vector[SpendingInfoDb]] def listUtxos(hdAccount: HDAccount): Future[Vector[SpendingInfoDb]] override def clearAllUtxos(): Future[HDWalletApi] - /** Gets the address associated with the pubkey at the resulting `BIP32Path` - * determined by the default account and the given chainType and addressIndex - */ - def getAddress(chainType: HDChainType, addressIndex: Int)(implicit - ec: ExecutionContext): Future[AddressDb] = { - for { - account <- getDefaultAccount() - address <- getAddress(account, chainType, addressIndex) - } yield address - } - - /** Gets the address associated with the pubkey at the resulting `BIP32Path` - * determined the given account, chainType, and addressIndex - */ - def getAddress( - account: AccountDb, - chainType: HDChainType, - addressIndex: Int): Future[AddressDb] - def listAccounts(): Future[Vector[AccountDb]] = { accountHandling.listAccounts() } @@ -491,19 +449,6 @@ trait HDWalletApi extends WalletApi { accountHandling.listAccounts().map(_.filter(_.hdAccount.purpose == purpose)) } - /** Creates a new account with the given purpose */ - def createNewAccount(purpose: HDPurpose): Future[HDWalletApi] - - /** Tries to create a new account in this wallet. Fails if the most recent - * account has no transaction history, as per BIP44 - * - * @see - * [[https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#account BIP44 account section]] - */ - def createNewAccount(hdAccount: HDAccount): Future[HDWalletApi] - - def findAccount(account: HDAccount): Future[Option[AccountDb]] - def fundRawTransaction( destinations: Vector[TransactionOutput], feeRate: FeeUnit, diff --git a/core/src/main/scala/org/bitcoins/core/api/wallet/WalletApi.scala b/core/src/main/scala/org/bitcoins/core/api/wallet/WalletApi.scala index 19d513dc15..71150c3e51 100644 --- a/core/src/main/scala/org/bitcoins/core/api/wallet/WalletApi.scala +++ b/core/src/main/scala/org/bitcoins/core/api/wallet/WalletApi.scala @@ -5,10 +5,9 @@ import org.bitcoins.core.api.feeprovider.FeeRateApi import org.bitcoins.core.api.keymanager.KeyManagerApi import org.bitcoins.core.api.node.NodeApi import org.bitcoins.core.api.wallet.db.* -import org.bitcoins.core.config.NetworkParameters import org.bitcoins.core.crypto.ExtPublicKey import org.bitcoins.core.currency.CurrencyUnit -import org.bitcoins.core.hd.{AddressType, HDAccount} +import org.bitcoins.core.hd.HDAccount import org.bitcoins.core.protocol.BitcoinAddress import org.bitcoins.core.protocol.blockchain.Block import org.bitcoins.core.protocol.script.ScriptPubKey @@ -17,19 +16,13 @@ import org.bitcoins.core.protocol.transaction.{ TransactionOutPoint, TransactionOutput } -import org.bitcoins.core.util.{FutureUtil, StartStopAsync} +import org.bitcoins.core.util.StartStopAsync import org.bitcoins.core.wallet.fee.FeeUnit -import org.bitcoins.core.wallet.utxo.{ - AddressTag, - AddressTagName, - AddressTagType, - TxoState -} +import org.bitcoins.core.wallet.utxo.{AddressTag, TxoState} import org.bitcoins.crypto.{DoubleSha256Digest, DoubleSha256DigestBE} import java.time.Instant import scala.concurrent.{ExecutionContext, Future} -import scala.util.{Failure, Success} /** API for the wallet project. * @@ -108,21 +101,15 @@ trait WalletApi extends StartStopAsync[WalletApi] { def getConfirmedBalance(tag: AddressTag): Future[CurrencyUnit] + def getNewAddress(): Future[BitcoinAddress] + + def getNewChangeAddress(): Future[BitcoinAddress] + /** Gets the sum of all unconfirmed UTXOs in this wallet */ def getUnconfirmedBalance(): Future[CurrencyUnit] def getUnconfirmedBalance(tag: AddressTag): Future[CurrencyUnit] - def listAddresses(): Future[Vector[AddressDb]] - - def listSpentAddresses(): Future[Vector[AddressDb]] - - def listFundedAddresses(): Future[Vector[(AddressDb, CurrencyUnit)]] - - def listUnusedAddresses(): Future[Vector[AddressDb]] - - def listScriptPubKeys(): Future[Vector[ScriptPubKeyDb]] - def listTransactions(): Future[Vector[TransactionDb]] def listUtxos(): Future[Vector[SpendingInfoDb]] @@ -131,8 +118,6 @@ trait WalletApi extends StartStopAsync[WalletApi] { def listUtxos(tag: AddressTag): Future[Vector[SpendingInfoDb]] - def watchScriptPubKey(scriptPubKey: ScriptPubKey): Future[ScriptPubKeyDb] - /** Checks if the wallet contains any data */ def isEmpty(): Future[Boolean] @@ -143,86 +128,6 @@ trait WalletApi extends StartStopAsync[WalletApi] { def clearAllAddresses(): Future[WalletApi] - /** Gets a new external address with the specified type. - * @param addressType - */ - def getNewAddress(addressType: AddressType): Future[BitcoinAddress] - - /** Gets a new external address Calling this method multiple times will return - * the same address, until it has received funds. - */ - def getNewAddress(): Future[BitcoinAddress] - - def getNewAddress( - addressType: AddressType, - tags: Vector[AddressTag]): Future[BitcoinAddress] - - def getNewAddress(tags: Vector[AddressTag]): Future[BitcoinAddress] - - /** Gets a external address the given AddressType. Calling this method - * multiple times will return the same address, until it has received funds. - */ - def getUnusedAddress(addressType: AddressType): Future[BitcoinAddress] - - /** Gets a external address. Calling this method multiple times will return - * the same address, until it has received funds. - */ - def getUnusedAddress: Future[BitcoinAddress] - - /** Mimics the `getaddressinfo` RPC call in Bitcoin Core - * - * @param address - * @return - * If the address is found in our database `Some(address)` is returned, - * otherwise `None` - */ - def getAddressInfo(address: BitcoinAddress): Future[Option[AddressInfo]] - - def getAddressInfo( - spendingInfoDb: SpendingInfoDb, - networkParameters: NetworkParameters): Future[Option[AddressInfo]] = { - val addressT = BitcoinAddress.fromScriptPubKeyT( - spk = spendingInfoDb.output.scriptPubKey, - np = networkParameters) - addressT match { - case Success(addr) => - getAddressInfo(addr) - case Failure(_) => - FutureUtil.none - } - } - - /** Tags the address with the address tag, updates the tag if one of tag's - * TagType already exists - */ - def tagAddress(address: BitcoinAddress, tag: AddressTag): Future[AddressTagDb] - - def getAddressTags(address: BitcoinAddress): Future[Vector[AddressTagDb]] - - def getAddressTags( - address: BitcoinAddress, - tagType: AddressTagType): Future[Vector[AddressTagDb]] - - def getAddressTags(): Future[Vector[AddressTagDb]] - - def getAddressTags(tagType: AddressTagType): Future[Vector[AddressTagDb]] - - def dropAddressTag(addressTagDb: AddressTagDb): Future[Int] - - def dropAddressTagType(addressTagType: AddressTagType): Future[Int] - - def dropAddressTagType( - address: BitcoinAddress, - addressTagType: AddressTagType): Future[Int] - - def dropAddressTagName( - address: BitcoinAddress, - tagName: AddressTagName): Future[Int] - - /** Generates a new change address */ - protected[wallet] def getNewChangeAddress()(implicit - ec: ExecutionContext): Future[BitcoinAddress] - def keyManager: KeyManagerApi protected def determineFeeRate(feeRateOpt: Option[FeeUnit]): Future[FeeUnit] = diff --git a/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/DLCWallet.scala b/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/DLCWallet.scala index 8e7918d5d7..e995988d49 100644 --- a/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/DLCWallet.scala +++ b/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/DLCWallet.scala @@ -301,8 +301,8 @@ abstract class DLCWallet index: Int ): Future[Vector[AddressDb]] = { for { - zero <- getAddress(account, chainType, index) - one <- getAddress(account, chainType, index + 1) + zero <- addressHandling.getAddress(account, chainType, index) + one <- addressHandling.getAddress(account, chainType, index + 1) } yield { logger.debug(s"Wrote DLC key addresses to database using index $index") Vector(zero, one) @@ -432,7 +432,7 @@ abstract class DLCWallet chainType = HDChainType.External account <- getDefaultAccountForType(AddressType.SegWit) - nextIndex <- getNextAvailableIndex(account, chainType) + nextIndex <- addressHandling.getNextAvailableIndex(account, chainType) _ <- writeDLCKeysToAddressDb(account, chainType, nextIndex) fundRawTxHelper <- fundTxHandling.fundRawTransactionInternal( @@ -636,7 +636,8 @@ abstract class DLCWallet Future.successful(initAccept) } case None => - val nextIndexF = getNextAvailableIndex(account, chainType) + val nextIndexF = + addressHandling.getNextAvailableIndex(account, chainType) val acceptWithoutSigsWithKeysF : Future[(DLCAcceptWithoutSigs, DLCPublicKeys)] = nextIndexF.map { nextIndex => diff --git a/node-test/src/test/scala/org/bitcoins/node/NeutrinoNodeWithWalletTest.scala b/node-test/src/test/scala/org/bitcoins/node/NeutrinoNodeWithWalletTest.scala index 698683fe54..d13434b090 100644 --- a/node-test/src/test/scala/org/bitcoins/node/NeutrinoNodeWithWalletTest.scala +++ b/node-test/src/test/scala/org/bitcoins/node/NeutrinoNodeWithWalletTest.scala @@ -59,7 +59,7 @@ class NeutrinoNodeWithWalletTest extends NodeTestWithCachedBitcoindNewest { ): Future[Boolean] = { for { balance <- wallet.getBalance() - addresses <- wallet.listAddresses() + addresses <- wallet.addressHandling.listAddresses() utxos <- wallet.listDefaultAccountUtxos() } yield { // +- fee rate because signatures could vary in size @@ -108,7 +108,7 @@ class NeutrinoNodeWithWalletTest extends NodeTestWithCachedBitcoindNewest { _ <- NodeTestUtil.awaitSync(node, bitcoind) _ <- AsyncUtil.awaitConditionF(condition1, maxTries = 100) // 10 seconds // receive - address <- wallet.getNewAddress() + address <- wallet.addressHandling.getNewAddress() txId <- bitcoind.sendToAddress(address, TestAmount) expectedTx <- bitcoind.getRawTransactionRaw(txId) @@ -145,7 +145,7 @@ class NeutrinoNodeWithWalletTest extends NodeTestWithCachedBitcoindNewest { for { // start watching - _ <- wallet.watchScriptPubKey(spk) + _ <- wallet.addressHandling.watchScriptPubKey(spk) // send txSent <- wallet.sendToOutputs(Vector(output), FeeRate) @@ -168,7 +168,7 @@ class NeutrinoNodeWithWalletTest extends NodeTestWithCachedBitcoindNewest { for { rescan <- wallet.isRescanning() balance <- wallet.getBalance() - addresses <- wallet.listAddresses() + addresses <- wallet.addressHandling.listAddresses() utxos <- wallet.listDefaultAccountUtxos() spks = utxos .map(_.output.scriptPubKey) @@ -181,7 +181,7 @@ class NeutrinoNodeWithWalletTest extends NodeTestWithCachedBitcoindNewest { } for { - addresses <- wallet.listAddresses() + addresses <- wallet.addressHandling.listAddresses() utxos <- wallet.listDefaultAccountUtxos() _ = assert(addresses.size == 6) _ = assert(utxos.size == 3) @@ -191,7 +191,7 @@ class NeutrinoNodeWithWalletTest extends NodeTestWithCachedBitcoindNewest { bitcoind .sendToAddress(address, TestAmount) - addresses <- wallet.listAddresses() + addresses <- wallet.addressHandling.listAddresses() utxos <- wallet.listDefaultAccountUtxos() _ = assert(addresses.size == 7) _ = assert(utxos.size == 3) @@ -207,7 +207,7 @@ class NeutrinoNodeWithWalletTest extends NodeTestWithCachedBitcoindNewest { .map(_.get.height == bitcoindHeight) }) _ <- wallet.clearAllUtxos() - addresses <- wallet.listAddresses() + addresses <- wallet.addressHandling.listAddresses() utxos <- wallet.listDefaultAccountUtxos() _ = assert(addresses.nonEmpty) _ = assert(utxos.isEmpty) diff --git a/testkit/src/main/scala/org/bitcoins/testkit/wallet/BitcoinSWalletTest.scala b/testkit/src/main/scala/org/bitcoins/testkit/wallet/BitcoinSWalletTest.scala index 59d35d7cad..fd430a19a7 100644 --- a/testkit/src/main/scala/org/bitcoins/testkit/wallet/BitcoinSWalletTest.scala +++ b/testkit/src/main/scala/org/bitcoins/testkit/wallet/BitcoinSWalletTest.scala @@ -392,10 +392,10 @@ object BitcoinSWalletTest extends WalletLogger { for { wallet <- defaultWalletF account1 = WalletTestUtil.getHdAccount1(wallet.walletConfig) - newAccountWallet <- wallet.createNewAccount( + _ <- wallet.accountHandling.createNewAccount( hdAccount = account1 ) - } yield newAccountWallet + } yield wallet } @@ -411,10 +411,10 @@ object BitcoinSWalletTest extends WalletLogger { chainQueryApi = chainQueryApi ) account1 = WalletTestUtil.getHdAccount1(wallet.walletConfig) - newAccountWallet <- wallet.createNewAccount( + _ <- wallet.accountHandling.createNewAccount( hdAccount = account1 ) - } yield newAccountWallet.asInstanceOf[DLCWallet] + } yield wallet } /** Pairs the given wallet with a bitcoind instance that has money in the diff --git a/testkit/src/main/scala/org/bitcoins/testkit/wallet/FundWalletUtil.scala b/testkit/src/main/scala/org/bitcoins/testkit/wallet/FundWalletUtil.scala index a6652d40a9..2b19bd8aef 100644 --- a/testkit/src/main/scala/org/bitcoins/testkit/wallet/FundWalletUtil.scala +++ b/testkit/src/main/scala/org/bitcoins/testkit/wallet/FundWalletUtil.scala @@ -68,7 +68,7 @@ trait FundWalletUtil extends BitcoinSLogger { )(implicit ec: ExecutionContext): Future[Wallet] = { val addressesF: Future[Vector[BitcoinAddress]] = Future.sequence { - Vector.fill(3)(wallet.getNewAddress(account)) + Vector.fill(3)(wallet.accountHandling.getNewAddress(account)) } // construct three txs that send money to these addresses @@ -100,7 +100,7 @@ trait FundWalletUtil extends BitcoinSLogger { )(implicit ec: ExecutionContext): Future[HDWalletApi] = { val addressesF: Future[Vector[BitcoinAddress]] = Future.sequence { - Vector.fill(3)(wallet.getNewAddress(account)) + Vector.fill(3)(wallet.accountHandling.getNewAddress(account)) } val txAndHashF = for { diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/AddressHandlingTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/AddressHandlingTest.scala index 6dfc6ad759..123562dec5 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/AddressHandlingTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/AddressHandlingTest.scala @@ -27,11 +27,11 @@ class AddressHandlingTest extends BitcoinSWalletTest { it must "generate a new address for the default account and then find it" in { (fundedWallet: FundedWallet) => val wallet = fundedWallet.wallet - val addressF = wallet.getNewAddress() + val addressF = wallet.addressHandling.getNewAddress() for { address <- addressF - exists <- wallet.contains(address, None) + exists <- wallet.addressHandling.contains(address, None) } yield { assert(exists, s"Wallet must contain address after generating it") } @@ -41,12 +41,14 @@ class AddressHandlingTest extends BitcoinSWalletTest { (fundedWallet: FundedWallet) => val wallet = fundedWallet.wallet val account1 = WalletTestUtil.getHdAccount1(wallet.walletConfig) - val addressF = wallet.getNewAddress(account1) + val addressF = wallet.accountHandling.getNewAddress(account1) for { address <- addressF - listAddressesForAcct <- wallet.listAddresses(account1) - exists <- wallet.contains(address, Some(account1)) - doesNotExist <- wallet.contains(address, None) + listAddressesForAcct <- wallet.accountHandling.listAddresses(account1) + exists <- wallet.addressHandling.contains( + address, + Some((wallet.accountHandling, account1))) + doesNotExist <- wallet.addressHandling.contains(address, None) } yield { assert(listAddressesForAcct.nonEmpty) assert(listAddressesForAcct.map(_.address).contains(address)) @@ -66,13 +68,13 @@ class AddressHandlingTest extends BitcoinSWalletTest { val wallet = fundedWallet.wallet for { - address1 <- wallet.getUnusedAddress - exists <- wallet.contains(address1, None) + address1 <- wallet.addressHandling.getUnusedAddress + exists <- wallet.addressHandling.contains(address1, None) _ = assert(exists, s"Wallet must contain address after generating it") - address2 <- wallet.getUnusedAddress + address2 <- wallet.addressHandling.getUnusedAddress _ = assert(address1 == address2, "Must generate same address") _ <- wallet.sendToAddress(address1, Satoshis(10000), None) - address3 <- wallet.getUnusedAddress + address3 <- wallet.addressHandling.getUnusedAddress } yield { assert(address1 != address3, "Must generate a new address") assert(address2 != address3, "Must generate a new address") @@ -83,7 +85,7 @@ class AddressHandlingTest extends BitcoinSWalletTest { (fundedWallet: FundedWallet) => val wallet = fundedWallet.wallet val addressesF = Future.sequence { - Vector.fill(10)(wallet.getNewAddress()) + Vector.fill(10)(wallet.addressHandling.getNewAddress()) } for { @@ -102,16 +104,16 @@ class AddressHandlingTest extends BitcoinSWalletTest { val wallet = fundedWallet.wallet for { - emptySpentAddresses <- wallet.listSpentAddresses() + emptySpentAddresses <- wallet.addressHandling.listSpentAddresses() _ = assert( emptySpentAddresses.isEmpty, s"Wallet did not start with empty spent addresses, got $emptySpentAddresses" ) - tempAddress <- wallet.getNewAddress() + tempAddress <- wallet.addressHandling.getNewAddress() tx <- wallet.sendToAddress(tempAddress, Bitcoins(1), None) spentDbs <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx) - spentAddresses <- wallet.listSpentAddresses() + spentAddresses <- wallet.addressHandling.listSpentAddresses() } yield { val diff = spentDbs .map(_.output.scriptPubKey) @@ -126,7 +128,7 @@ class AddressHandlingTest extends BitcoinSWalletTest { for { unspentDbs <- wallet.spendingInfoDAO.findAllUnspent() - fundedAddresses <- wallet.listFundedAddresses() + fundedAddresses <- wallet.addressHandling.listFundedAddresses() } yield { val diff = unspentDbs .map(_.output) @@ -144,7 +146,7 @@ class AddressHandlingTest extends BitcoinSWalletTest { for { addrDbs <- wallet.spendingInfoDAO.findAllSpendingInfos() - fundedAddresses <- wallet.listUnusedAddresses() + fundedAddresses <- wallet.addressHandling.listUnusedAddresses() } yield { val intersect = addrDbs .map(_.output.scriptPubKey) @@ -157,13 +159,13 @@ class AddressHandlingTest extends BitcoinSWalletTest { val wallet = fundedWallet.wallet for { - addr <- wallet.getNewAddress() - initTags <- wallet.getAddressTags(addr) + addr <- wallet.addressHandling.getNewAddress() + initTags <- wallet.addressHandling.getAddressTags(addr) _ = assert(initTags.isEmpty) tag = AddressLabelTag("for test") - _ <- wallet.tagAddress(addr, tag) - tags <- wallet.getAddressTags(addr) + _ <- wallet.addressHandling.tagAddress(addr, tag) + tags <- wallet.addressHandling.getAddressTags(addr) } yield { assert(tags.size == 1) val tagDb = tags.head @@ -176,19 +178,19 @@ class AddressHandlingTest extends BitcoinSWalletTest { val wallet = fundedWallet.wallet for { - addr <- wallet.getNewAddress() - initTags <- wallet.getAddressTags(addr) + addr <- wallet.addressHandling.getNewAddress() + initTags <- wallet.addressHandling.getAddressTags(addr) _ = assert(initTags.isEmpty) tag = AddressLabelTag("no one knows the supply of eth") - _ <- wallet.tagAddress(addr, tag) - tags <- wallet.getAddressTags(addr) + _ <- wallet.addressHandling.tagAddress(addr, tag) + tags <- wallet.addressHandling.getAddressTags(addr) _ = assert(tags.size == 1) tagDb = tags.head _ = assert(tagDb.address == addr) _ = assert(tagDb.addressTag == tag) - num <- wallet.dropAddressTag(tagDb) + num <- wallet.addressHandling.dropAddressTag(tagDb) } yield assert(num == 1) } @@ -198,16 +200,16 @@ class AddressHandlingTest extends BitcoinSWalletTest { for { addr <- wallet.getNewAddress() addr1 <- wallet.getNewAddress() - initTags <- wallet.getAddressTags(addr) - initTags1 <- wallet.getAddressTags(addr1) + initTags <- wallet.addressHandling.getAddressTags(addr) + initTags1 <- wallet.addressHandling.getAddressTags(addr1) _ = assert(initTags.isEmpty) _ = assert(initTags1.isEmpty) tag = AddressLabelTag("no one knows the supply of eth") - _ <- wallet.tagAddress(addr, tag) - _ <- wallet.tagAddress(addr, HotStorage) - _ <- wallet.tagAddress(addr1, tag) - tags <- wallet.getAddressTags(AddressLabelTagType) + _ <- wallet.addressHandling.tagAddress(addr, tag) + _ <- wallet.addressHandling.tagAddress(addr, HotStorage) + _ <- wallet.addressHandling.tagAddress(addr1, tag) + tags <- wallet.addressHandling.getAddressTags(AddressLabelTagType) _ = assert(tags.size == 2) tagDb = tags.head _ = assert(tagDb.address == addr) @@ -216,8 +218,10 @@ class AddressHandlingTest extends BitcoinSWalletTest { _ = assert(tagDb1.address == addr1) _ = assert(tagDb1.addressTag == tag) - numDropped <- wallet.dropAddressTagType(AddressLabelTagType) - hotStorageTags <- wallet.getAddressTags(StorageLocationTagType) + numDropped <- wallet.addressHandling.dropAddressTagType( + AddressLabelTagType) + hotStorageTags <- wallet.addressHandling.getAddressTags( + StorageLocationTagType) } yield { assert(numDropped == 2) assert(hotStorageTags.size == 1) @@ -229,9 +233,9 @@ class AddressHandlingTest extends BitcoinSWalletTest { val wallet = fundedWallet.wallet val spk = EmptyScriptPubKey for { - before <- wallet.listScriptPubKeys() - spkDb <- wallet.watchScriptPubKey(spk) - after <- wallet.listScriptPubKeys() + before <- wallet.addressHandling.listScriptPubKeys() + spkDb <- wallet.addressHandling.watchScriptPubKey(spk) + after <- wallet.addressHandling.listScriptPubKeys() } yield { assert(before.size + 1 == after.size) assert(spkDb.scriptPubKey == spk) diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/AddressLabelTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/AddressLabelTest.scala index f54f723d2a..01bc407be0 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/AddressLabelTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/AddressLabelTest.scala @@ -21,10 +21,10 @@ class AddressLabelTest extends BitcoinSWalletTest { val tag1 = UnknownAddressTag("test_tag_name_1", "test_tag_type_1") val tag2 = UnknownAddressTag("test_tag_name_2", "test_tag_type_2") val addressF = for { - address <- wallet.getNewAddress(Vector(tag1)) + address <- wallet.addressHandling.getNewAddress(Vector(tag1)) // add another tag to address - tagDb1 <- wallet.getAddressTags(address) - tagDb2 <- wallet.tagAddress(address, tag2) + tagDb1 <- wallet.addressHandling.getAddressTags(address) + tagDb2 <- wallet.addressHandling.tagAddress(address, tag2) } yield { assert(tagDb1.head.address == address) assert(tagDb1.head.tagName == tag1.tagName) @@ -50,8 +50,8 @@ class AddressLabelTest extends BitcoinSWalletTest { val resultF = for { address <- wallet.getNewAddress() // add another tag to address - _ <- wallet.tagAddress(address, tag1) - _ <- wallet.tagAddress(address, tag2) + _ <- wallet.addressHandling.tagAddress(address, tag1) + _ <- wallet.addressHandling.tagAddress(address, tag2) } yield () recoverToSucceededIf[SQLException](resultF) diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/AddressTagIntegrationTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/AddressTagIntegrationTest.scala index 2a581f054f..a08d715bee 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/AddressTagIntegrationTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/AddressTagIntegrationTest.scala @@ -41,7 +41,7 @@ class AddressTagIntegrationTest extends BitcoinSWalletTest { IncomingTransactionDAO()(system.dispatcher, walletConfig) for { addr <- wallet.getNewAddress() - taggedAddr <- wallet.getNewAddress(Vector(exampleTag)) + taggedAddr <- wallet.addressHandling.getNewAddress(Vector(exampleTag)) txId <- bitcoind.sendMany( Map(addr -> valueFromBitcoind, taggedAddr -> valueFromBitcoind) ) @@ -132,7 +132,7 @@ class AddressTagIntegrationTest extends BitcoinSWalletTest { val bitcoindAddrF = bitcoind.getNewAddress val walletAddr1F = wallet.getNewAddress() - val taggedAddrF = wallet.getNewAddress(Vector(exampleTag)) + val taggedAddrF = wallet.addressHandling.getNewAddress(Vector(exampleTag)) for { walletAddr1 <- walletAddr1F txid <- bitcoind.sendToAddress(walletAddr1, Bitcoins.two) diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/FundTransactionHandlingTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/FundTransactionHandlingTest.scala index 7894c5cb84..5086ea6856 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/FundTransactionHandlingTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/FundTransactionHandlingTest.scala @@ -169,7 +169,7 @@ class FundTransactionHandlingTest val wallet = fundedWallet.wallet val walletConfig = fundedWallet.walletConfig val account1 = WalletTestUtil.getHdAccount1(walletConfig) - val account1DbF = wallet.findAccount(account1) + val account1DbF = wallet.accountHandling.findAccount(account1) for { feeRate <- wallet.getFeeRate() account1DbOpt <- account1DbF @@ -195,7 +195,7 @@ class FundTransactionHandlingTest val wallet = fundedWallet.wallet val walletConfig = fundedWallet.walletConfig val account1 = WalletTestUtil.getHdAccount1(walletConfig) - val account1DbF = wallet.findAccount(account1) + val account1DbF = wallet.accountHandling.findAccount(account1) val fundedTxF = for { feeRate <- wallet.getFeeRate() account1DbOpt <- account1DbF @@ -218,11 +218,12 @@ class FundTransactionHandlingTest val bitcoind = fundedWallet.bitcoind val fundedTxF = for { feeRate <- wallet.getFeeRate() - _ <- wallet.createNewAccount(wallet.keyManager.kmParams.purpose) + _ <- wallet.accountHandling.createNewAccount( + wallet.keyManager.kmParams.purpose) accounts <- wallet.listAccounts() account2 = accounts.find(_.hdAccount.index == 2).get - addr <- wallet.getNewAddress(account2) + addr <- wallet.accountHandling.getNewAddress(account2) hash <- bitcoind.generateToAddress(1, addr).map(_.head) block <- bitcoind.getBlockRaw(hash) @@ -273,7 +274,7 @@ class FundTransactionHandlingTest ): Future[Assertion] = { for { feeRate <- wallet.getFeeRate() - taggedAddr <- wallet.getNewAddress(Vector(tag)) + taggedAddr <- wallet.addressHandling.getNewAddress(Vector(tag)) _ <- wallet.sendToAddress(taggedAddr, destination.value * 2, Some(feeRate)) taggedBalance <- wallet.getBalance(tag) diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/LegacyWalletTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/LegacyWalletTest.scala index feb9e22f19..d772721e5f 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/LegacyWalletTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/LegacyWalletTest.scala @@ -17,8 +17,8 @@ class LegacyWalletTest extends BitcoinSWalletTest { addr <- wallet.getNewAddress() account <- wallet.getDefaultAccount() otherAddr <- wallet.getNewAddress() - thirdAddr <- wallet.getNewAddress(AddressType.Legacy) - allAddrs <- wallet.listAddresses() + thirdAddr <- wallet.addressHandling.getNewAddress(AddressType.Legacy) + allAddrs <- wallet.addressHandling.listAddresses() } yield { assert(account.hdAccount.purpose == HDPurpose.Legacy) assert(allAddrs.forall(_.address.isInstanceOf[P2PKHAddress])) @@ -32,7 +32,7 @@ class LegacyWalletTest extends BitcoinSWalletTest { it should "generate segwit addresses" in { wallet => for { account <- wallet.getDefaultAccountForType(AddressType.SegWit) - addr <- wallet.getNewAddress(AddressType.SegWit) + addr <- wallet.addressHandling.getNewAddress(AddressType.SegWit) } yield { assert(account.hdAccount.purpose == HDPurpose.SegWit) assert(addr.isInstanceOf[Bech32Address]) @@ -41,9 +41,9 @@ class LegacyWalletTest extends BitcoinSWalletTest { it should "generate mixed addresses" in { wallet => for { - segwit <- wallet.getNewAddress(AddressType.SegWit) - legacy <- wallet.getNewAddress(AddressType.Legacy) - nested <- wallet.getNewAddress(AddressType.NestedSegWit) + segwit <- wallet.addressHandling.getNewAddress(AddressType.SegWit) + legacy <- wallet.addressHandling.getNewAddress(AddressType.Legacy) + nested <- wallet.addressHandling.getNewAddress(AddressType.NestedSegWit) } yield { assert(segwit.isInstanceOf[Bech32Address]) assert(legacy.isInstanceOf[P2PKHAddress]) diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/ProcessBlockTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/ProcessBlockTest.scala index 00f66688de..05024eae4e 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/ProcessBlockTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/ProcessBlockTest.scala @@ -184,7 +184,7 @@ class ProcessBlockTest extends BitcoinSWalletTestCachedBitcoindNewest { UInt32.zero ) - addrDb <- wallet.getAddressInfo(recvAddr).map(_.get) + addrDb <- wallet.addressHandling.getAddressInfo(recvAddr).map(_.get) path = addrDb.path coin = path.coin accountDb <- accountDAO diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/RescanHandlingTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/RescanHandlingTest.scala index 5093941437..107f1f8ac6 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/RescanHandlingTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/RescanHandlingTest.scala @@ -29,13 +29,13 @@ class RescanHandlingTest extends BitcoinSWalletTestCachedBitcoindNewest { utxos <- wallet.listUtxos(account) _ = assert(utxos.nonEmpty) - addresses <- wallet.listAddresses(account) + addresses <- wallet.accountHandling.listAddresses(account) _ = assert(addresses.nonEmpty) _ <- wallet.accountHandling.clearUtxos(account) clearedUtxos <- wallet.listUtxos(account) - clearedAddresses <- wallet.listAddresses(account) + clearedAddresses <- wallet.accountHandling.listAddresses(account) } yield { assert(clearedUtxos.isEmpty) assert(clearedAddresses.nonEmpty) @@ -52,13 +52,13 @@ class RescanHandlingTest extends BitcoinSWalletTestCachedBitcoindNewest { utxos <- wallet.listUtxos() _ = assert(utxos.nonEmpty) - addresses <- wallet.listAddresses() + addresses <- wallet.addressHandling.listAddresses() _ = assert(addresses.nonEmpty) _ <- wallet.clearAllUtxos() clearedUtxos <- wallet.listUtxos() - clearedAddresses <- wallet.listAddresses() + clearedAddresses <- wallet.addressHandling.listAddresses() } yield { assert(clearedUtxos.isEmpty) assert(clearedAddresses.nonEmpty) @@ -362,7 +362,7 @@ class RescanHandlingTest extends BitcoinSWalletTestCachedBitcoindNewest { force = false ) - usedAddresses <- wallet.listFundedAddresses() + usedAddresses <- wallet.addressHandling.listFundedAddresses() _ = assert( !usedAddresses.exists(_._1.address == address), @@ -372,7 +372,7 @@ class RescanHandlingTest extends BitcoinSWalletTestCachedBitcoindNewest { hashes <- bitcoind.generateToAddress(1, address) block <- bitcoind.getBlockRaw(hashes.head) _ <- wallet.processBlock(block) - fundedAddresses <- wallet.listFundedAddresses() + fundedAddresses <- wallet.addressHandling.listFundedAddresses() utxos <- wallet.listUtxos(TxoState.ImmatureCoinbase) } yield { // note 25 bitcoin reward from coinbase tx here @@ -580,7 +580,7 @@ class RescanHandlingTest extends BitcoinSWalletTestCachedBitcoindNewest { DEFAULT_ADDR_BATCH_SIZE) _ = assert(rescanState.isInstanceOf[RescanState.RescanStarted]) _ <- RescanState.awaitRescanDone(rescanState) - addresses <- wallet.listAddresses() + addresses <- wallet.addressHandling.listAddresses() } yield { assert(addresses.exists(_.isChange)) assert(addresses.exists(!_.isChange)) diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/SegwitWalletTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/SegwitWalletTest.scala index 166f9804fa..bfc154f092 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/SegwitWalletTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/SegwitWalletTest.scala @@ -18,8 +18,8 @@ class SegwitWalletTest extends BitcoinSWalletTest { addr <- wallet.getNewAddress() account <- wallet.getDefaultAccount() otherAddr <- wallet.getNewAddress() - thirdAddr <- wallet.getNewAddress(AddressType.SegWit) - allAddrs <- wallet.listAddresses() + thirdAddr <- wallet.addressHandling.getNewAddress(AddressType.SegWit) + allAddrs <- wallet.addressHandling.listAddresses() } yield { assert(account.hdAccount.purpose == HDPurpose.SegWit) assert(allAddrs.forall(_.address.isInstanceOf[Bech32Address])) @@ -33,7 +33,7 @@ class SegwitWalletTest extends BitcoinSWalletTest { it should "generate legacy addresses" in { wallet => for { account <- wallet.getDefaultAccountForType(AddressType.Legacy) - addr <- wallet.getNewAddress(AddressType.Legacy) + addr <- wallet.addressHandling.getNewAddress(AddressType.Legacy) } yield { assert(account.hdAccount.purpose == HDPurpose.Legacy) assert(addr.isInstanceOf[P2PKHAddress]) @@ -42,9 +42,9 @@ class SegwitWalletTest extends BitcoinSWalletTest { it should "generate mixed addresses" in { wallet => for { - segwit <- wallet.getNewAddress(AddressType.SegWit) - legacy <- wallet.getNewAddress(AddressType.Legacy) - nested <- wallet.getNewAddress(AddressType.NestedSegWit) + segwit <- wallet.addressHandling.getNewAddress(AddressType.SegWit) + legacy <- wallet.addressHandling.getNewAddress(AddressType.Legacy) + nested <- wallet.addressHandling.getNewAddress(AddressType.NestedSegWit) } yield { assert(segwit.isInstanceOf[Bech32Address]) assert(legacy.isInstanceOf[P2PKHAddress]) diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/TrezorAddressTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/TrezorAddressTest.scala index 736ee9628e..f14beaaf0d 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/TrezorAddressTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/TrezorAddressTest.scala @@ -196,7 +196,7 @@ class TrezorAddressTest extends BitcoinSWalletTest with EmptyFixture { val accountsToCreate = existing.length until testVectors.length FutureUtil .sequentially(accountsToCreate) { _ => - wallet.createNewAccount(keyManagerParams.purpose) + wallet.accountHandling.createNewAccount(keyManagerParams.purpose) } .map(_ => ()) } @@ -212,9 +212,10 @@ class TrezorAddressTest extends BitcoinSWalletTest with EmptyFixture { val addrFutures: Future[Seq[AddressDb]] = FutureUtil.sequentially(vec.addresses) { vector => val addrFut = vector.chain match { - case HDChainType.Change => wallet.getNewChangeAddress(acc) + case HDChainType.Change => + wallet.accountHandling.getNewChangeAddress(acc) case HDChainType.External => - wallet.getNewAddress(acc) + wallet.accountHandling.getNewAddress(acc) } addrFut.flatMap(wallet.addressDAO.findAddress).map { case Some(addr) => addr diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/WalletCallbackTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/WalletCallbackTest.scala index 8a27b1d86d..3af28b035d 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/WalletCallbackTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/WalletCallbackTest.scala @@ -49,7 +49,7 @@ class WalletCallbackTest extends BitcoinSWalletTest { for { address <- wallet.getNewAddress() - exists <- wallet.contains(address, None) + exists <- wallet.addressHandling.contains(address, None) _ = assert(exists, "Wallet must contain address after generating it") result <- resultP.future } yield assert(result == address) diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/WalletUnitTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/WalletUnitTest.scala index 87ef35f1c9..a4bd6fe964 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/WalletUnitTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/WalletUnitTest.scala @@ -37,7 +37,7 @@ class WalletUnitTest extends BitcoinSWalletTest { it should "create a new wallet" in { (wallet: Wallet) => for { accounts <- wallet.listAccounts() - addresses <- wallet.listAddresses() + addresses <- wallet.addressHandling.listAddresses() } yield { assert(accounts.length == 3) // legacy, segwit and nested segwit assert(addresses.isEmpty) @@ -48,7 +48,7 @@ class WalletUnitTest extends BitcoinSWalletTest { for { addr <- wallet.getNewAddress() otherAddr <- wallet.getNewAddress() - allAddrs <- wallet.listAddresses() + allAddrs <- wallet.addressHandling.listAddresses() } yield { assert(allAddrs.length == 2) assert(allAddrs.exists(_.address == addr)) @@ -221,7 +221,7 @@ class WalletUnitTest extends BitcoinSWalletTest { for { accountDb <- wallet.accountDAO.findAll().map(_.head) - addr <- wallet.getNewAddress(accountDb) + addr <- wallet.accountHandling.getNewAddress(accountDb) addrDb <- wallet.addressDAO.findAddress(addr).map(_.get) walletKey = addrDb.ecPublicKey walletPath = addrDb.path @@ -247,7 +247,7 @@ class WalletUnitTest extends BitcoinSWalletTest { it must "be able to sign a psbt with our own p2pkh utxo" in { (wallet: Wallet) => for { - addr <- wallet.getNewAddress(AddressType.Legacy) + addr <- wallet.addressHandling.getNewAddress(AddressType.Legacy) addrDb <- wallet.addressDAO.findAddress(addr).map(_.get) walletKey = addrDb.ecPublicKey @@ -271,7 +271,7 @@ class WalletUnitTest extends BitcoinSWalletTest { it must "be able to sign a psbt with our own p2sh segwit utxo" in { (wallet: Wallet) => for { - addr <- wallet.getNewAddress(AddressType.NestedSegWit) + addr <- wallet.addressHandling.getNewAddress(AddressType.NestedSegWit) addrDb <- wallet.addressDAO.findAddress(addr).map(_.get) walletKey = addrDb.ecPublicKey @@ -295,7 +295,7 @@ class WalletUnitTest extends BitcoinSWalletTest { it must "be able to sign a psbt with our own p2wpkh utxo" in { (wallet: Wallet) => for { - addr <- wallet.getNewAddress(AddressType.SegWit) + addr <- wallet.addressHandling.getNewAddress(AddressType.SegWit) addrDb <- wallet.addressDAO.findAddress(addr).map(_.get) walletKey = addrDb.ecPublicKey @@ -326,7 +326,7 @@ class WalletUnitTest extends BitcoinSWalletTest { it must "get correct txs to broadcast" in { (wallet: Wallet) => for { - addr <- wallet.getNewAddress(AddressType.SegWit) + addr <- wallet.addressHandling.getNewAddress(AddressType.SegWit) addrDb <- wallet.addressDAO.findAddress(addr).map(_.get) walletKey = addrDb.ecPublicKey diff --git a/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala b/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala index d8a0b10d4c..7108e3332b 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala @@ -44,12 +44,9 @@ import java.time.temporal.ChronoUnit import java.util.concurrent.TimeUnit import scala.concurrent.{ExecutionContext, Future} import scala.util.control.NonFatal -import scala.util.{Failure, Random, Success} +import scala.util.Random -abstract class Wallet - extends NeutrinoHDWalletApi - with AddressHandling - with WalletLogger { +abstract class Wallet extends NeutrinoHDWalletApi with WalletLogger { override def keyManager: BIP39KeyManager = { walletConfig.kmConf.toBip39KeyManager @@ -104,13 +101,16 @@ abstract class Wallet def fundTxHandling: FundTransactionHandling = FundTransactionHandling( accountHandling = accountHandling, utxoHandling = utxoHandling, - addressHandling = this, + addressHandling = addressHandling, spendingInfoDAO = spendingInfoDAO, transactionDAO = transactionDAO, keyManager = keyManager ) - def accountHandling: AccountHandlingApi = - AccountHandling(this, walletDAOs) + def accountHandling: AccountHandling = + AccountHandling(walletDAOs, keyManager) + + def addressHandling: AddressHandling = + AddressHandling(accountHandling, walletDAOs) protected lazy val transactionProcessing: TransactionProcessingApi = { TransactionProcessing( @@ -124,7 +124,7 @@ abstract class Wallet RescanHandling( transactionProcessing = transactionProcessing, accountHandling = accountHandling, - addressHandling = this, + addressHandling = addressHandling, chainQueryApi = chainQueryApi, nodeApi = nodeApi, walletDAOs = walletDAOs @@ -173,6 +173,14 @@ abstract class Wallet Future.successful(this) } + override def getNewAddress(): Future[BitcoinAddress] = { + addressHandling.getNewAddress() + } + + override def getNewChangeAddress(): Future[BitcoinAddress] = { + addressHandling.getNewChangeAddress() + } + override def getSyncDescriptorOpt(): Future[Option[SyncHeightDescriptor]] = { stateDescriptorDAO.getSyncHeight() } @@ -219,7 +227,7 @@ abstract class Wallet blockFilters: Vector[(DoubleSha256DigestBE, GolombFilter)] ): Future[Wallet] = { val utxosF = utxoHandling.listUtxos() - val spksF = listScriptPubKeys() + val spksF = addressHandling.listScriptPubKeys() val blockHashOpt = blockFilters.lastOption.map(_._1) val heightOptF = blockHashOpt match { case Some(blockHash) => @@ -411,7 +419,7 @@ abstract class Wallet val signed = rawTxHelper.signedTx val processedTxF = for { - ourOuts <- findOurOuts(signed) + ourOuts <- addressHandling.findOurOutputs(signed) creditingAmount = rawTxHelper.scriptSigParams.foldLeft( CurrencyUnits.zero )(_ + _.amount) @@ -540,7 +548,7 @@ abstract class Wallet .zip(prevTxs) .map(info => info._1.toUTXOInfo(keyManager, info._2)) - changeAddr <- getNewChangeAddress(fromAccount.hdAccount) + changeAddr <- accountHandling.getNewChangeAddress(fromAccount.hdAccount) output = TransactionOutput(amount, address.scriptPubKey) txBuilder = ShufflingNonInteractiveFinalizer.txBuilderFrom( @@ -862,7 +870,7 @@ abstract class Wallet Random.shuffle(spendingInfos).head } - addr <- getNewChangeAddress() + addr <- addressHandling.getNewChangeAddress() childTx <- sendFromOutPoints(Vector(spendingInfo.outPoint), addr, feeRate) } yield childTx } @@ -942,62 +950,6 @@ abstract class Wallet } } - protected def getLastAccountOpt( - purpose: HDPurpose - ): Future[Option[AccountDb]] = { - accountDAO - .findAll() - .map(_.filter(_.hdAccount.purpose == purpose)) - .map(_.sortBy(_.hdAccount.index)) - // we want to the most recently created account, - // to know what the index of our new account - // should be. - .map(_.lastOption) - } - - /** Creates a new account my reading from our account database, finding the - * last account, and then incrementing the account index by one, and then - * creating that account - * - * @return - */ - override def createNewAccount(purpose: HDPurpose): Future[Wallet] = { - getLastAccountOpt(purpose).flatMap { - case Some(accountDb) => - val hdAccount = accountDb.hdAccount - val newAccount = hdAccount.copy(index = hdAccount.index + 1) - createNewAccount(newAccount) - case None => - createNewAccount(walletConfig.defaultAccount) - } - } - - // todo: check if there's addresses in the most recent - // account before creating new - override def createNewAccount( - hdAccount: HDAccount - ): Future[Wallet] = { - logger.info( - s"Creating new account at index ${hdAccount.index} for purpose ${hdAccount.purpose}" - ) - - val xpub: ExtPublicKey = { - keyManager.deriveXPub(hdAccount) match { - case Failure(exception) => - // this won't happen, because we're deriving from a privkey - // this should really be changed in the method signature - logger.error(s"Unexpected error when deriving xpub: $exception") - throw exception - case Success(xpub) => xpub - } - } - val newAccountDb = AccountDb(xpub, hdAccount) - accountDAO.create(newAccountDb).map { created => - logger.debug(s"Created new account ${created.hdAccount}") - this - } - } - override def getWalletName(): Future[String] = { Future.successful(walletConfig.walletName) } diff --git a/wallet/src/main/scala/org/bitcoins/wallet/WalletHolder.scala b/wallet/src/main/scala/org/bitcoins/wallet/WalletHolder.scala index 5c29a36f35..38cac1893c 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/WalletHolder.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/WalletHolder.scala @@ -13,11 +13,10 @@ import org.bitcoins.core.api.keymanager.BIP39KeyManagerApi import org.bitcoins.core.api.node.NodeApi import org.bitcoins.core.api.wallet.* import org.bitcoins.core.api.wallet.db.* -import org.bitcoins.core.config.NetworkParameters import org.bitcoins.core.currency.{CurrencyUnit, Satoshis} import org.bitcoins.core.dlc.accounting.DLCWalletAccounting import org.bitcoins.core.gcs.GolombFilter -import org.bitcoins.core.hd.{AddressType, HDAccount, HDChainType, HDPurpose} +import org.bitcoins.core.hd.{AddressType, HDAccount, HDPurpose} import org.bitcoins.core.number.UInt32 import org.bitcoins.core.protocol.blockchain.Block import org.bitcoins.core.protocol.dlc.models.* @@ -35,12 +34,7 @@ import org.bitcoins.core.wallet.builder.{ ShufflingNonInteractiveFinalizer } import org.bitcoins.core.wallet.fee.{FeeUnit, SatoshisPerVirtualByte} -import org.bitcoins.core.wallet.utxo.{ - AddressTag, - AddressTagName, - AddressTagType, - TxoState -} +import org.bitcoins.core.wallet.utxo.{AddressTag, TxoState} import org.bitcoins.crypto.{DoubleSha256DigestBE, Sha256Digest} import scodec.bits.ByteVector @@ -72,6 +66,8 @@ class WalletHolder(initWalletOpt: Option[DLCNeutrinoHDWalletApi])(implicit override def fundTxHandling: FundTransactionHandlingApi = wallet.fundTxHandling + + override def addressHandling: AddressHandlingApi = wallet.addressHandling def isInitialized: Boolean = synchronized { walletOpt.isDefined } @@ -107,6 +103,11 @@ class WalletHolder(initWalletOpt: Option[DLCNeutrinoHDWalletApi])(implicit Future(wallet).flatMap[T](_) } + override def getNewAddress(): Future[BitcoinAddress] = delegate( + _.getNewAddress()) + + override def getNewChangeAddress(): Future[BitcoinAddress] = delegate( + _.getNewChangeAddress()) override def processBlock(block: Block): Future[Unit] = delegate(_.processBlock(block)) @@ -214,31 +215,6 @@ class WalletHolder(initWalletOpt: Option[DLCNeutrinoHDWalletApi])(implicit delegate(_.listUtxos(tag)) } - override def listAddresses(): Future[Vector[AddressDb]] = delegate( - _.listAddresses() - ) - - override def listSpentAddresses(): Future[Vector[AddressDb]] = delegate( - _.listSpentAddresses() - ) - - override def listFundedAddresses() - : Future[Vector[(AddressDb, CurrencyUnit)]] = delegate( - _.listFundedAddresses() - ) - - override def listUnusedAddresses(): Future[Vector[AddressDb]] = delegate( - _.listUnusedAddresses() - ) - - override def listScriptPubKeys(): Future[Vector[ScriptPubKeyDb]] = delegate( - _.listScriptPubKeys() - ) - - override def watchScriptPubKey( - scriptPubKey: ScriptPubKey - ): Future[ScriptPubKeyDb] = delegate(_.watchScriptPubKey(scriptPubKey)) - override def markUTXOsAsReserved( utxos: Vector[SpendingInfoDb] ): Future[Vector[SpendingInfoDb]] = delegate(_.markUTXOsAsReserved(utxos)) @@ -257,75 +233,6 @@ class WalletHolder(initWalletOpt: Option[DLCNeutrinoHDWalletApi])(implicit override def isEmpty(): Future[Boolean] = delegate(_.isEmpty()) - override def getNewAddress(addressType: AddressType): Future[BitcoinAddress] = - delegate(_.getNewAddress(addressType)) - - override def getNewAddress(account: HDAccount): Future[BitcoinAddress] = { - delegate(_.getNewAddress(account)) - } - - override def getNewAddress(): Future[BitcoinAddress] = delegate( - _.getNewAddress() - ) - - override def getNewAddress( - addressType: AddressType, - tags: Vector[AddressTag] - ): Future[BitcoinAddress] = delegate(_.getNewAddress(addressType, tags)) - - override def getNewAddress(tags: Vector[AddressTag]): Future[BitcoinAddress] = - delegate(_.getNewAddress(tags)) - - override def getUnusedAddress( - addressType: AddressType - ): Future[BitcoinAddress] = delegate(_.getUnusedAddress(addressType)) - - override def getUnusedAddress: Future[BitcoinAddress] = delegate( - _.getUnusedAddress - ) - - override def getAddressInfo( - address: BitcoinAddress - ): Future[Option[AddressInfo]] = delegate(_.getAddressInfo(address)) - - override def tagAddress( - address: BitcoinAddress, - tag: AddressTag - ): Future[AddressTagDb] = delegate(_.tagAddress(address, tag)) - - override def getAddressTags( - address: BitcoinAddress - ): Future[Vector[AddressTagDb]] = delegate(_.getAddressTags(address)) - - override def getAddressTags( - address: BitcoinAddress, - tagType: AddressTagType - ): Future[Vector[AddressTagDb]] = delegate(_.getAddressTags(address, tagType)) - - override def getAddressTags(): Future[Vector[AddressTagDb]] = delegate( - _.getAddressTags() - ) - - override def getAddressTags( - tagType: AddressTagType - ): Future[Vector[AddressTagDb]] = delegate(_.getAddressTags(tagType)) - - override def dropAddressTag(addressTagDb: AddressTagDb): Future[Int] = - delegate(_.dropAddressTag(addressTagDb)) - - override def dropAddressTagType(addressTagType: AddressTagType): Future[Int] = - delegate(_.dropAddressTagType(addressTagType)) - - override def dropAddressTagType( - address: BitcoinAddress, - addressTagType: AddressTagType - ): Future[Int] = delegate(_.dropAddressTagType(address, addressTagType)) - - override def dropAddressTagName( - address: BitcoinAddress, - tagName: AddressTagName - ): Future[Int] = delegate(_.dropAddressTagName(address, tagName)) - override def sendFromOutPoints( outPoints: Vector[TransactionOutPoint], address: BitcoinAddress, @@ -528,9 +435,6 @@ class WalletHolder(initWalletOpt: Option[DLCNeutrinoHDWalletApi])(implicit override def getUnconfirmedBalance(account: HDAccount): Future[CurrencyUnit] = delegate(_.getUnconfirmedBalance(account)) - override def getNewChangeAddress(account: AccountDb): Future[BitcoinAddress] = - delegate(_.getNewChangeAddress(account)) - override def getDefaultAccount(): Future[AccountDb] = delegate( _.getDefaultAccount() ) @@ -610,23 +514,6 @@ class WalletHolder(initWalletOpt: Option[DLCNeutrinoHDWalletApi])(implicit _.makeOpReturnCommitment(message, hashMessage, feeRate, fromAccount) ) - override def listAddresses(account: HDAccount): Future[Vector[AddressDb]] = - delegate(_.listAddresses(account)) - - override def listSpentAddresses( - account: HDAccount - ): Future[Vector[AddressDb]] = delegate(_.listSpentAddresses(account)) - - override def listFundedAddresses( - account: HDAccount - ): Future[Vector[(AddressDb, CurrencyUnit)]] = delegate( - _.listFundedAddresses(account) - ) - - override def listUnusedAddresses( - account: HDAccount - ): Future[Vector[AddressDb]] = delegate(_.listUnusedAddresses(account)) - override def clearAllUtxos(): Future[HDWalletApi] = delegate( _.clearAllUtxos() ) @@ -635,23 +522,6 @@ class WalletHolder(initWalletOpt: Option[DLCNeutrinoHDWalletApi])(implicit delegate(_.clearAllAddresses()) } - override def getAddress( - account: AccountDb, - chainType: HDChainType, - addressIndex: Int - ): Future[AddressDb] = delegate( - _.getAddress(account, chainType, addressIndex) - ) - - override def createNewAccount( - purpose: HDPurpose - ): Future[HDWalletApi] = delegate(_.createNewAccount(purpose)) - - override def createNewAccount(hdAccount: HDAccount): Future[HDWalletApi] = - delegate( - _.createNewAccount(hdAccount) - ) - override def getSyncDescriptorOpt(): Future[Option[SyncHeightDescriptor]] = delegate(_.getSyncDescriptorOpt()) @@ -676,12 +546,6 @@ class WalletHolder(initWalletOpt: Option[DLCNeutrinoHDWalletApi])(implicit ec: ExecutionContext ): Future[CurrencyUnit] = delegate(_.getBalance(tag)) - override def getAddressInfo( - spendingInfoDb: SpendingInfoDb, - networkParameters: NetworkParameters - ): Future[Option[AddressInfo]] = - delegate(_.getAddressInfo(spendingInfoDb, networkParameters)) - override def sendFromOutPoints( outPoints: Vector[TransactionOutPoint], address: BitcoinAddress, @@ -760,11 +624,6 @@ class WalletHolder(initWalletOpt: Option[DLCNeutrinoHDWalletApi])(implicit ): Future[NeutrinoHDWalletApi] = delegate(_.processCompactFilter(blockHash, blockFilter)) - override def getNewChangeAddress()(implicit - ec: ExecutionContext - ): Future[BitcoinAddress] = - delegate(_.getNewChangeAddress()) - override def sendWithAlgo( address: BitcoinAddress, amount: CurrencyUnit, @@ -945,11 +804,6 @@ class WalletHolder(initWalletOpt: Option[DLCNeutrinoHDWalletApi])(implicit )(implicit ec: ExecutionContext): Future[Transaction] = delegate(_.makeOpReturnCommitment(message, hashMessage, feeRate)) - override def getAddress(chainType: HDChainType, addressIndex: Int)(implicit - ec: ExecutionContext - ): Future[AddressDb] = - delegate(_.getAddress(chainType, addressIndex)) - override def listAccounts(purpose: HDPurpose)(implicit ec: ExecutionContext ): Future[Vector[AccountDb]] = @@ -1041,14 +895,6 @@ class WalletHolder(initWalletOpt: Option[DLCNeutrinoHDWalletApi])(implicit delegate(_.findOutputsBeingSpent(tx)) } - override def findAccount(account: HDAccount): Future[Option[AccountDb]] = { - delegate(_.findAccount(account)) - } - - override def getNewAddress(account: AccountDb): Future[BitcoinAddress] = { - delegate(_.getNewAddress(account)) - } - override def findByScriptPubKey( scriptPubKey: ScriptPubKey ): Future[Vector[SpendingInfoDb]] = { diff --git a/wallet/src/main/scala/org/bitcoins/wallet/internal/AccountHandling.scala b/wallet/src/main/scala/org/bitcoins/wallet/internal/AccountHandling.scala index 0e0d0bd2e2..7989f76ad9 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/internal/AccountHandling.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/internal/AccountHandling.scala @@ -1,8 +1,12 @@ package org.bitcoins.wallet.internal import org.bitcoins.commons.util.BitcoinSLogger +import org.bitcoins.core.api.keymanager.BIP39KeyManagerApi import org.bitcoins.core.api.wallet.AccountHandlingApi -import org.bitcoins.core.api.wallet.db.AccountDb +import org.bitcoins.core.api.wallet.db.{AccountDb, AddressDb, AddressDbHelper} +import org.bitcoins.core.config.NetworkParameters +import org.bitcoins.core.crypto.ExtPublicKey +import org.bitcoins.core.currency.CurrencyUnit import org.bitcoins.core.hd.AddressType.* import org.bitcoins.core.hd.* import org.bitcoins.core.protocol.BitcoinAddress @@ -15,6 +19,7 @@ import org.bitcoins.core.protocol.blockchain.{ } import org.bitcoins.core.protocol.script.ScriptPubKey import org.bitcoins.db.SafeDatabase +import org.bitcoins.wallet.callback.WalletCallbacks import org.bitcoins.wallet.config.WalletAppConfig import org.bitcoins.wallet.models.{ AccountDAO, @@ -26,13 +31,14 @@ import org.bitcoins.wallet.models.{ import slick.dbio.{DBIOAction, Effect, NoStream} import scala.concurrent.{ExecutionContext, Future} +import scala.util.{Failure, Success} /** Provides functionality related enumerating accounts. Account creation does * not happen here, as that requires an unlocked wallet. */ case class AccountHandling( - addressHandling: AddressHandling, - walletDAOs: WalletDAOs)(implicit + walletDAOs: WalletDAOs, + keyManager: BIP39KeyManagerApi)(implicit walletConfig: WalletAppConfig, ec: ExecutionContext) extends AccountHandlingApi @@ -43,6 +49,49 @@ case class AccountHandling( private val scriptPubKeyDAO: ScriptPubKeyDAO = walletDAOs.scriptPubKeyDAO private val safeDatabase: SafeDatabase = spendingInfoDAO.safeDatabase private val chainParams: ChainParams = walletConfig.chain + private val networkParameters: NetworkParameters = walletConfig.network + private def walletCallbacks: WalletCallbacks = walletConfig.callBacks + + override def createNewAccount( + hdAccount: HDAccount + ): Future[ExtPublicKey] = { + logger.info( + s"Creating new account at index ${hdAccount.index} for purpose ${hdAccount.purpose}" + ) + + val xpub: ExtPublicKey = { + keyManager.deriveXPub(hdAccount) match { + case Failure(exception) => + // this won't happen, because we're deriving from a privkey + // this should really be changed in the method signature + logger.error(s"Unexpected error when deriving xpub: $exception") + throw exception + case Success(xpub) => xpub + } + } + val newAccountDb = AccountDb(xpub, hdAccount) + accountDAO.create(newAccountDb).map { created => + logger.debug(s"Created new account ${created.hdAccount}") + created.xpub + } + } + + /** Creates a new account my reading from our account database, finding the + * last account, and then incrementing the account index by one, and then + * creating that account + * + * @return + */ + override def createNewAccount(purpose: HDPurpose): Future[ExtPublicKey] = { + getLastAccountOpt(purpose).flatMap { + case Some(accountDb) => + val hdAccount = accountDb.hdAccount + val newAccount = hdAccount.copy(index = hdAccount.index + 1) + createNewAccount(newAccount) + case None => + createNewAccount(walletConfig.defaultAccount) + } + } /** @inheritdoc */ override def listAccounts(): Future[Vector[AccountDb]] = @@ -168,7 +217,7 @@ case class AccountHandling( Effect.Read with Effect.Write with Effect.Transactional] = { DBIOAction.sequence { 1.to(addressBatchSize) - .map(_ => addressHandling.getNewAddressAction(account)) + .map(_ => getNewAddressAction(account)) } }.map(_.toVector) @@ -180,7 +229,7 @@ case class AccountHandling( Effect.Read with Effect.Write with Effect.Transactional] = { DBIOAction.sequence { 1.to(addressBatchSize) - .map(_ => addressHandling.getNewChangeAddressAction(account)) + .map(_ => getNewChangeAddressAction(account)) } }.map(_.toVector) @@ -190,6 +239,247 @@ case class AccountHandling( } yield receiveAddresses ++ changeAddresses } + override def getNewChangeAddress( + account: AccountDb): Future[BitcoinAddress] = { + val action = getNewChangeAddressAction(account) + safeDatabase.run(action) + } + + /** Queues a request to generate an address and returns a Future that will be + * completed when the request is processed in the queue. If the queue is full + * it throws an exception. + * @throws IllegalStateException + */ + override def getNewAddress( + account: AccountDb, + chainType: HDChainType + ): Future[BitcoinAddress] = { + val action = getNewAddressHelperAction(account, chainType) + safeDatabase.run(action) + } + + def getNewAddressAction(account: HDAccount): DBIOAction[ + BitcoinAddress, + NoStream, + Effect.Read with Effect.Write with Effect.Transactional + ] = { + val accountDbOptA = findAccountAction(account) + accountDbOptA.flatMap { + case Some(accountDb) => getNewAddressAction(accountDb) + case None => + DBIOAction.failed( + new RuntimeException( + s"No account found for given hdaccount=${account}" + ) + ) + } + } + + def getNewChangeAddressAction(account: HDAccount): DBIOAction[ + BitcoinAddress, + NoStream, + Effect.Read with Effect.Write with Effect.Transactional + ] = { + val accountDbOptA = findAccountAction(account) + accountDbOptA.flatMap { + case Some(accountDb) => getNewChangeAddressAction(accountDb) + case None => + DBIOAction.failed( + new RuntimeException( + s"No account found for given hdaccount=${account}" + ) + ) + } + } + + override def getNewAddress(account: AccountDb): Future[BitcoinAddress] = { + val action = getNewAddressAction(account) + safeDatabase.run(action) + } + + def getNewAddressAction(account: AccountDb): DBIOAction[ + BitcoinAddress, + NoStream, + Effect.Read with Effect.Write with Effect.Transactional + ] = { + getNewAddressHelperAction(account, HDChainType.External) + } + + def getNewChangeAddressAction(account: AccountDb): DBIOAction[ + BitcoinAddress, + NoStream, + Effect.Read with Effect.Write with Effect.Transactional + ] = { + getNewAddressHelperAction(account, HDChainType.Change) + } + + private def findAccountAction( + account: HDAccount + ): DBIOAction[Option[AccountDb], NoStream, Effect.Read] = { + accountDAO.findByAccountAction(account) + } + + override def findAccount(account: HDAccount): Future[Option[AccountDb]] = { + safeDatabase.run(findAccountAction(account)) + } + + override def listUnusedAddresses( + account: HDAccount): Future[Vector[AddressDb]] = { + val unusedAddressesF = addressDAO.getUnusedAddresses + unusedAddressesF.map { unusedAddresses => + unusedAddresses.filter(addr => + HDAccount.isSameAccount(addr.path, account)) + } + } + + override def listAddresses(account: HDAccount): Future[Vector[AddressDb]] = { + val allAddressesF: Future[Vector[AddressDb]] = addressDAO.findAllAddresses() + + val accountAddressesF = { + allAddressesF.map { addresses => + addresses.filter { a => + logger.debug(s"a.path=${a.path} account=${account}") + HDAccount.isSameAccount(a.path, account) + } + } + } + + accountAddressesF + } + + override def listSpentAddresses( + account: HDAccount + ): Future[Vector[AddressDb]] = { + val spentAddressesF = addressDAO.getSpentAddresses + + spentAddressesF.map { spentAddresses => + spentAddresses.filter(addr => HDAccount.isSameAccount(addr.path, account)) + } + } + + override def listFundedAddresses( + account: HDAccount + ): Future[Vector[(AddressDb, CurrencyUnit)]] = { + val spentAddressesF = addressDAO.getFundedAddresses + + spentAddressesF.map { spentAddresses => + spentAddresses.filter(addr => + HDAccount.isSameAccount(addr._1.path, account)) + } + } + + private def getNewAddressHelperAction( + account: AccountDb, + chainType: HDChainType + ): DBIOAction[ + BitcoinAddress, + NoStream, + Effect.Read with Effect.Write with Effect.Transactional + ] = { + logger.debug(s"Processing $account $chainType in our address request queue") + val resultA: DBIOAction[ + BitcoinAddress, + NoStream, + Effect.Read with Effect.Write with Effect.Transactional + ] = for { + addressDb <- getNewAddressDbAction(account, chainType) + writtenAddressDb <- addressDAO.createAction(addressDb) + } yield { + logger.info( + s"Generated new address=${addressDb.address} path=${addressDb.path} isChange=${addressDb.isChange}" + ) + writtenAddressDb.address + } + + val callbackExecuted = resultA.flatMap { address => + val executedF = + walletCallbacks.executeOnNewAddressGenerated(address) + DBIOAction + .from(executedF) + .map(_ => address) + } + + callbackExecuted + } + + private def getNewAddressDbAction( + account: AccountDb, + chainType: HDChainType + ): DBIOAction[AddressDb, NoStream, Effect.Read] = { + logger.debug(s"Getting new $chainType adddress for ${account.hdAccount}") + + val lastAddrOptA = chainType match { + case HDChainType.External => + addressDAO.findMostRecentExternalAction(account.hdAccount) + case HDChainType.Change => + addressDAO.findMostRecentChangeAction(account.hdAccount) + } + + lastAddrOptA.map { lastAddrOpt => + val addrPath: HDPath = lastAddrOpt match { + case Some(addr) => + val next = addr.path.next + logger.debug( + s"Found previous address at path=${addr.path}, next=$next" + ) + next + case None => + val address = account.hdAccount + .toChain(chainType) + .toHDAddress(0) + + val path = address.toPath + logger.debug(s"Did not find previous address, next=$path") + path + } + + val pathDiff = + account.hdAccount.diff(addrPath) match { + case Some(value) => value + case None => + throw new RuntimeException( + s"Could not diff ${account.hdAccount} and $addrPath" + ) + } + + val pubkey = account.xpub.deriveChildPubKey(pathDiff) match { + case Failure(exception) => throw exception + case Success(value) => value.key + } + + addrPath match { + case segwitPath: SegWitHDPath => + AddressDbHelper + .getSegwitAddress(pubkey, segwitPath, networkParameters) + case legacyPath: LegacyHDPath => + AddressDbHelper.getLegacyAddress( + pubkey, + legacyPath, + networkParameters + ) + case nestedPath: NestedSegWitHDPath => + AddressDbHelper.getNestedSegwitAddress( + pubkey, + nestedPath, + networkParameters + ) + } + } + } + + protected def getLastAccountOpt( + purpose: HDPurpose + ): Future[Option[AccountDb]] = { + accountDAO + .findAll() + .map(_.filter(_.hdAccount.purpose == purpose)) + .map(_.sortBy(_.hdAccount.index)) + // we want to the most recently created account, + // to know what the index of our new account + // should be. + .map(_.lastOption) + } + /** The default HD coin for this wallet, read from config */ protected[wallet] lazy val DEFAULT_HD_COIN: HDCoin = { val coinType = DEFAULT_HD_COIN_TYPE diff --git a/wallet/src/main/scala/org/bitcoins/wallet/internal/AddressHandling.scala b/wallet/src/main/scala/org/bitcoins/wallet/internal/AddressHandling.scala index 8054ba85d9..be30cc8a54 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/internal/AddressHandling.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/internal/AddressHandling.scala @@ -1,10 +1,15 @@ package org.bitcoins.wallet.internal import org.bitcoins.core.api.wallet -import org.bitcoins.core.api.wallet.AddressInfo -import org.bitcoins.core.api.wallet.db._ +import org.bitcoins.core.api.wallet.{ + AccountHandlingApi, + AddressHandlingApi, + AddressInfo +} +import org.bitcoins.core.api.wallet.db.* +import org.bitcoins.core.config.NetworkParameters import org.bitcoins.core.currency.CurrencyUnit -import org.bitcoins.core.hd._ +import org.bitcoins.core.hd.* import org.bitcoins.core.number.UInt32 import org.bitcoins.core.protocol.BitcoinAddress import org.bitcoins.core.protocol.script.ScriptPubKey @@ -18,26 +23,40 @@ import org.bitcoins.core.wallet.utxo.{ AddressTagName, AddressTagType } -import org.bitcoins.crypto.ECPublicKey -import org.bitcoins.wallet._ -import slick.dbio.{DBIOAction, Effect, NoStream} +import org.bitcoins.wallet.* +import org.bitcoins.wallet.config.WalletAppConfig +import org.bitcoins.wallet.models.{ + AddressDAO, + AddressTagDAO, + ScriptPubKeyDAO, + WalletDAOs +} -import scala.concurrent.Future +import scala.concurrent.{ExecutionContext, Future} import scala.util.{Failure, Success} /** Provides functionality related to addresses. This includes enumeratng and * creating them, primarily. */ -private[wallet] trait AddressHandling extends WalletLogger { - self: Wallet => +case class AddressHandling( + accountHandling: AccountHandlingApi, + walletDAOs: WalletDAOs)(implicit + walletConfig: WalletAppConfig, + ec: ExecutionContext) + extends AddressHandlingApi + with WalletLogger { + private val addressDAO: AddressDAO = walletDAOs.addressDAO + private val addressTagDAO: AddressTagDAO = walletDAOs.addressTagDAO + private val scriptPubKeyDAO: ScriptPubKeyDAO = walletDAOs.scriptPubKeyDAO + private val networkParameters: NetworkParameters = walletConfig.network def contains( address: BitcoinAddress, - accountOpt: Option[HDAccount] + accountOpt: Option[(AccountHandlingApi, HDAccount)] ): Future[Boolean] = { val possibleAddressesF = accountOpt match { - case Some(account) => - listAddresses(account) + case Some((ah, account)) => + ah.listAddresses(account) case None => listAddresses() } @@ -50,66 +69,19 @@ private[wallet] trait AddressHandling extends WalletLogger { override def listAddresses(): Future[Vector[AddressDb]] = addressDAO.findAllAddresses() - override def listAddresses(account: HDAccount): Future[Vector[AddressDb]] = { - val allAddressesF: Future[Vector[AddressDb]] = listAddresses() - - val accountAddressesF = { - allAddressesF.map { addresses => - addresses.filter { a => - logger.info(s"a.path=${a.path} account=${account}") - HDAccount.isSameAccount(a.path, account) - } - } - } - - accountAddressesF - } - override def listSpentAddresses(): Future[Vector[AddressDb]] = { addressDAO.getSpentAddresses } - override def listSpentAddresses( - account: HDAccount - ): Future[Vector[AddressDb]] = { - val spentAddressesF = addressDAO.getSpentAddresses - - spentAddressesF.map { spentAddresses => - spentAddresses.filter(addr => HDAccount.isSameAccount(addr.path, account)) - } - } - override def listFundedAddresses() : Future[Vector[(AddressDb, CurrencyUnit)]] = { addressDAO.getFundedAddresses } - override def listFundedAddresses( - account: HDAccount - ): Future[Vector[(AddressDb, CurrencyUnit)]] = { - val spentAddressesF = addressDAO.getFundedAddresses - - spentAddressesF.map { spentAddresses => - spentAddresses.filter(addr => - HDAccount.isSameAccount(addr._1.path, account)) - } - } - override def listUnusedAddresses(): Future[Vector[AddressDb]] = { addressDAO.getUnusedAddresses } - override def listUnusedAddresses( - account: HDAccount - ): Future[Vector[AddressDb]] = { - val unusedAddressesF = addressDAO.getUnusedAddresses - - unusedAddressesF.map { unusedAddresses => - unusedAddresses.filter(addr => - HDAccount.isSameAccount(addr.path, account)) - } - } - override def listScriptPubKeys(): Future[Vector[ScriptPubKeyDb]] = scriptPubKeyDAO.findAll() @@ -118,92 +90,20 @@ private[wallet] trait AddressHandling extends WalletLogger { ): Future[ScriptPubKeyDb] = scriptPubKeyDAO.createIfNotExists(ScriptPubKeyDb(scriptPubKey)) - /** Enumerates the public keys in this wallet */ - protected[wallet] def listPubkeys(): Future[Vector[ECPublicKey]] = - addressDAO.findAllPubkeys() - - /** Enumerates the scriptPubKeys in this wallet */ - protected[wallet] def listSPKs(): Future[Vector[ScriptPubKey]] = - addressDAO.findAllSPKs() - /** Given a transaction, returns the outputs (with their corresponding * outpoints) that pay to this wallet */ - def findOurOuts( + def findOurOutputs( transaction: Transaction ): Future[Vector[(TransactionOutput, TransactionOutPoint)]] = for { - spks <- listSPKs() + spks <- listScriptPubKeys() } yield transaction.outputs.zipWithIndex.collect { - case (out, index) if spks.contains(out.scriptPubKey) => + case (out, index) + if spks.map(_.scriptPubKey).contains(out.scriptPubKey) => (out, TransactionOutPoint(transaction.txId, UInt32(index))) }.toVector - private def getNewAddressDbAction( - account: AccountDb, - chainType: HDChainType - ): DBIOAction[AddressDb, NoStream, Effect.Read] = { - logger.debug(s"Getting new $chainType adddress for ${account.hdAccount}") - - val lastAddrOptA = chainType match { - case HDChainType.External => - addressDAO.findMostRecentExternalAction(account.hdAccount) - case HDChainType.Change => - addressDAO.findMostRecentChangeAction(account.hdAccount) - } - - lastAddrOptA.map { lastAddrOpt => - val addrPath: HDPath = lastAddrOpt match { - case Some(addr) => - val next = addr.path.next - logger.debug( - s"Found previous address at path=${addr.path}, next=$next" - ) - next - case None => - val address = account.hdAccount - .toChain(chainType) - .toHDAddress(0) - - val path = address.toPath - logger.debug(s"Did not find previous address, next=$path") - path - } - - val pathDiff = - account.hdAccount.diff(addrPath) match { - case Some(value) => value - case None => - throw new RuntimeException( - s"Could not diff ${account.hdAccount} and $addrPath" - ) - } - - val pubkey = account.xpub.deriveChildPubKey(pathDiff) match { - case Failure(exception) => throw exception - case Success(value) => value.key - } - - addrPath match { - case segwitPath: SegWitHDPath => - AddressDbHelper - .getSegwitAddress(pubkey, segwitPath, networkParameters) - case legacyPath: LegacyHDPath => - AddressDbHelper.getLegacyAddress( - pubkey, - legacyPath, - networkParameters - ) - case nestedPath: NestedSegWitHDPath => - AddressDbHelper.getNestedSegwitAddress( - pubkey, - nestedPath, - networkParameters - ) - } - } - } - /** Derives a new address in the wallet for the given account and chain type * (change/external). After deriving the address it inserts it into our table * of addresses. @@ -220,55 +120,10 @@ private[wallet] trait AddressHandling extends WalletLogger { account: AccountDb, chainType: HDChainType ): Future[AddressDb] = { - val action = getNewAddressDbAction(account, chainType) - safeDatabase.run(action) - } - - private def getNewAddressHelperAction( - account: AccountDb, - chainType: HDChainType - ): DBIOAction[ - BitcoinAddress, - NoStream, - Effect.Read with Effect.Write with Effect.Transactional - ] = { - logger.debug(s"Processing $account $chainType in our address request queue") - val resultA: DBIOAction[ - BitcoinAddress, - NoStream, - Effect.Read with Effect.Write with Effect.Transactional - ] = for { - addressDb <- getNewAddressDbAction(account, chainType) - writtenAddressDb <- addressDAO.createAction(addressDb) - } yield { - logger.info( - s"Generated new address=${addressDb.address} path=${addressDb.path} isChange=${addressDb.isChange}" - ) - writtenAddressDb.address - } - - val callbackExecuted = resultA.flatMap { address => - val executedF = - walletCallbacks.executeOnNewAddressGenerated(address) - DBIOAction - .from(executedF) - .map(_ => address) - } - - callbackExecuted - } - - /** Queues a request to generate an address and returns a Future that will be - * completed when the request is processed in the queue. If the queue is full - * it throws an exception. - * @throws IllegalStateException - */ - private def getNewAddressHelper( - account: AccountDb, - chainType: HDChainType - ): Future[BitcoinAddress] = { - val action = getNewAddressHelperAction(account, chainType) - safeDatabase.run(action) + accountHandling + .getNewAddress(account, chainType) + .flatMap(addr => addressDAO.findAddress(addr)) + .map(_.get) } def getNextAvailableIndex( @@ -278,73 +133,6 @@ private[wallet] trait AddressHandling extends WalletLogger { getNewAddressDb(accountDb, chainType).map(_.path.path.last.index) } - def getNewAddressAction(account: HDAccount): DBIOAction[ - BitcoinAddress, - NoStream, - Effect.Read with Effect.Write with Effect.Transactional - ] = { - val accountDbOptA = findAccountAction(account) - accountDbOptA.flatMap { - case Some(accountDb) => getNewAddressAction(accountDb) - case None => - DBIOAction.failed( - new RuntimeException( - s"No account found for given hdaccount=${account}" - ) - ) - } - } - - def getNewChangeAddressAction(account: HDAccount): DBIOAction[ - BitcoinAddress, - NoStream, - Effect.Read with Effect.Write with Effect.Transactional - ] = { - val accountDbOptA = findAccountAction(account) - accountDbOptA.flatMap { - case Some(accountDb) => getNewChangeAddressAction(accountDb) - case None => - DBIOAction.failed( - new RuntimeException( - s"No account found for given hdaccount=${account}" - ) - ) - } - } - - def getNewAddress(account: HDAccount): Future[BitcoinAddress] = { - val accountDbOptF = findAccount(account) - accountDbOptF.flatMap { - case Some(accountDb) => getNewAddress(accountDb) - case None => - Future.failed( - new RuntimeException( - s"No account found for given hdaccount=${account}" - ) - ) - } - } - - def getNewAddressAction(account: AccountDb): DBIOAction[ - BitcoinAddress, - NoStream, - Effect.Read with Effect.Write with Effect.Transactional - ] = { - getNewAddressHelperAction(account, HDChainType.External) - } - - def getNewChangeAddressAction(account: AccountDb): DBIOAction[ - BitcoinAddress, - NoStream, - Effect.Read with Effect.Write with Effect.Transactional - ] = { - getNewAddressHelperAction(account, HDChainType.Change) - } - - def getNewAddress(account: AccountDb): Future[BitcoinAddress] = { - safeDatabase.run(getNewAddressAction(account)) - } - /** @inheritdoc */ override def getNewAddress(): Future[BitcoinAddress] = { getNewAddress(walletConfig.defaultAddressType) @@ -357,6 +145,13 @@ private[wallet] trait AddressHandling extends WalletLogger { getNewAddress(walletConfig.defaultAddressType, tags) } + override def getNewChangeAddress(): Future[BitcoinAddress] = { + for { + account <- accountHandling.getDefaultAccount() + addr <- accountHandling.getNewChangeAddress(account) + } yield addr + } + /** @inheritdoc */ def getAddress( account: AccountDb, @@ -434,11 +229,11 @@ private[wallet] trait AddressHandling extends WalletLogger { /** @inheritdoc */ def getUnusedAddress(addressType: AddressType): Future[BitcoinAddress] = { for { - account <- getDefaultAccountForType(addressType) + account <- accountHandling.getDefaultAccountForType(addressType) addresses <- addressDAO.getUnusedAddresses(account.hdAccount) address <- if (addresses.isEmpty) { - getNewAddress(account.hdAccount) + accountHandling.getNewAddress(account.hdAccount) } else { Future.successful(addresses.head.address) } @@ -448,34 +243,24 @@ private[wallet] trait AddressHandling extends WalletLogger { /** @inheritdoc */ def getUnusedAddress: Future[BitcoinAddress] = { for { - account <- getDefaultAccount() + account <- accountHandling.getDefaultAccount() addresses <- addressDAO.getUnusedAddresses(account.hdAccount) address <- if (addresses.isEmpty) { - getNewAddress(account.hdAccount) + accountHandling.getNewAddress(account.hdAccount) } else { Future.successful(addresses.head.address) } } yield address } - def findAccountAction( - account: HDAccount - ): DBIOAction[Option[AccountDb], NoStream, Effect.Read] = { - accountDAO.findByAccountAction(account) - } - - override def findAccount(account: HDAccount): Future[Option[AccountDb]] = { - safeDatabase.run(findAccountAction(account)) - } - /** @inheritdoc */ override def getNewAddress( addressType: AddressType ): Future[BitcoinAddress] = { for { - account <- getDefaultAccountForType(addressType) - address <- getNewAddressHelper(account, HDChainType.External) + account <- accountHandling.getDefaultAccountForType(addressType) + address <- accountHandling.getNewAddress(account, HDChainType.External) } yield address } @@ -485,32 +270,14 @@ private[wallet] trait AddressHandling extends WalletLogger { tags: Vector[AddressTag] ): Future[BitcoinAddress] = { for { - account <- getDefaultAccountForType(addressType) - address <- getNewAddressHelper(account, HDChainType.External) + account <- accountHandling.getDefaultAccountForType(addressType) + address <- accountHandling.getNewAddress(account, HDChainType.External) tagDbs = tags.map(tag => AddressTagDb(address, tag)) _ <- addressTagDAO.createAll(tagDbs) } yield address } - /** Generates a new change address */ - override def getNewChangeAddress( - account: AccountDb - ): Future[BitcoinAddress] = { - 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 diff --git a/wallet/src/main/scala/org/bitcoins/wallet/internal/FundTransactionHandling.scala b/wallet/src/main/scala/org/bitcoins/wallet/internal/FundTransactionHandling.scala index 4b0a4724a7..e6e47fec72 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/internal/FundTransactionHandling.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/internal/FundTransactionHandling.scala @@ -19,9 +19,9 @@ import slick.dbio.{DBIO, DBIOAction, Effect, NoStream} import scala.concurrent.Future case class FundTransactionHandling( - accountHandling: AccountHandlingApi, + accountHandling: AccountHandling, utxoHandling: UtxoHandling, - addressHandling: AddressHandling, + addressHandling: AddressHandlingApi, spendingInfoDAO: SpendingInfoDAO, transactionDAO: TransactionDAO, keyManager: BIP39KeyManagerApi)(implicit @@ -191,7 +191,7 @@ case class FundTransactionHandling( for { (selectedUtxos, callbackF) <- selectedUtxosA - change <- addressHandling.getNewChangeAddressAction(fromAccount) + change <- accountHandling.getNewChangeAddressAction(fromAccount) utxoSpendingInfos = { selectedUtxos.map { case (utxo, prevTx) => utxo.toUTXOInfo(keyManager = keyManager, prevTx) diff --git a/wallet/src/main/scala/org/bitcoins/wallet/internal/RescanHandling.scala b/wallet/src/main/scala/org/bitcoins/wallet/internal/RescanHandling.scala index f9c8e71d2d..dca064da63 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/internal/RescanHandling.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/internal/RescanHandling.scala @@ -12,6 +12,7 @@ import org.bitcoins.core.api.node.NodeApi import org.bitcoins.core.api.wallet.NeutrinoWalletApi.BlockMatchingResponse import org.bitcoins.core.api.wallet.{ AccountHandlingApi, + AddressHandlingApi, RescanHandlingApi, TransactionProcessingApi } @@ -41,7 +42,7 @@ import scala.util.{Failure, Success} case class RescanHandling( transactionProcessing: TransactionProcessingApi, accountHandling: AccountHandlingApi, - addressHandling: AddressHandling, + addressHandling: AddressHandlingApi, chainQueryApi: ChainQueryApi, nodeApi: NodeApi, walletDAOs: WalletDAOs)(implicit