diff --git a/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala b/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala index 2e7ed17fd5..ce499f278c 100644 --- a/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala +++ b/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala @@ -169,6 +169,9 @@ object ConsoleCli { .action((_, conf) => conf.copy(command = GetSpentAddresses)) .text( "Returns list of all wallet addresses that have received funds and been spent"), + cmd("getfundedaddresses") + .action((_, conf) => conf.copy(command = GetFundedAddresses)) + .text("Returns list of all wallet addresses that are holding funds"), cmd("getaccounts") .action((_, conf) => conf.copy(command = GetAccounts)) .text("Returns list of all wallet accounts"), @@ -402,6 +405,8 @@ object ConsoleCli { RequestParam("getaddresses") case GetSpentAddresses => RequestParam("getspentaddresses") + case GetFundedAddresses => + RequestParam("getfundedaddresses") case GetAccounts => RequestParam("getaccounts") case CreateNewAccount => @@ -577,6 +582,7 @@ object CliCommand { case object GetUtxos extends CliCommand case object GetAddresses extends CliCommand case object GetSpentAddresses extends CliCommand + case object GetFundedAddresses extends CliCommand case object GetAccounts extends CliCommand case object CreateNewAccount extends CliCommand case object IsEmpty extends CliCommand 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 8c6297cbef..d07b22fcec 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 @@ -8,7 +8,7 @@ import akka.http.scaladsl.testkit.ScalatestRouteTest import org.bitcoins.chain.api.ChainApi import org.bitcoins.core.Core import org.bitcoins.core.crypto.ExtPublicKey -import org.bitcoins.core.currency.{Bitcoins, CurrencyUnit} +import org.bitcoins.core.currency.{Bitcoins, CurrencyUnit, Satoshis} import org.bitcoins.core.hd._ import org.bitcoins.core.protocol.BlockStamp.{ BlockHash, @@ -373,6 +373,31 @@ class RoutesSpec } } + "return the wallet's funded addresses" in { + val addressDb = LegacyAddressDb( + LegacyHDPath(HDCoinType.Testnet, 0, HDChainType.External, 0), + ECPublicKey.freshPublicKey, + Sha256Hash160Digest.fromBytes(ByteVector.low(20)), + testAddress.asInstanceOf[P2PKHAddress], + testAddress.scriptPubKey + ) + + (mockWalletApi.listFundedAddresses: () => Future[Vector[( + AddressDb, + CurrencyUnit)]]) + .expects() + .returning(Future.successful(Vector((addressDb, Satoshis.zero)))) + + val route = + walletRoutes.handleCommand(ServerCommand("getfundedaddresses", Arr())) + + Get() ~> route ~> check { + contentType shouldEqual `application/json` + responseAs[String] shouldEqual + s"""{"result":["$testAddressStr ${Satoshis.zero}"],"error":null}""".stripMargin + } + } + "return the wallet accounts" in { val xpub = ExtPublicKey .fromString( diff --git a/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala b/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala index 9f9a5bc1c6..587895d71c 100644 --- a/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala +++ b/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala @@ -266,7 +266,7 @@ object SendFromOutpoints extends ServerJsonModels { case other => Failure( new IllegalArgumentException( - s"Bad number of arguments: ${other.length}. Expected: 3")) + s"Bad number of arguments: ${other.length}. Expected: 4")) } } 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 ed8087aaa0..86ad698d12 100644 --- a/app/server/src/main/scala/org/bitcoins/server/WalletRoutes.scala +++ b/app/server/src/main/scala/org/bitcoins/server/WalletRoutes.scala @@ -189,6 +189,17 @@ case class WalletRoutes(wallet: WalletApi, node: Node)( } } + case ServerCommand("getfundedaddresses", _) => + complete { + wallet.listFundedAddresses().map { addressDbs => + val addressAndValues = addressDbs.map { + case (addressDb, value) => s"${addressDb.address} $value" + } + + Server.httpSuccess(addressAndValues) + } + } + case ServerCommand("getaccounts", _) => complete { wallet.listAccounts().map { accounts => 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 44d606451d..59a06e2a1c 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/AddressHandlingTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/AddressHandlingTest.scala @@ -1,6 +1,7 @@ package org.bitcoins.wallet import org.bitcoins.core.currency.{Bitcoins, Satoshis} +import org.bitcoins.core.protocol.transaction.TransactionOutput import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte import org.bitcoins.testkit.wallet.FundWalletUtil.FundedWallet import org.bitcoins.rpc.util.AsyncUtil @@ -136,4 +137,19 @@ class AddressHandlingTest extends BitcoinSWalletTest { assert(diff.isEmpty, s"Extra spent addresses $diff") } } + + it must "get the correct funded addresses" in { fundedWallet: FundedWallet => + val wallet = fundedWallet.wallet + + for { + unspentDbs <- wallet.spendingInfoDAO.findAllUnspent() + fundedAddresses <- wallet.listFundedAddresses() + } yield { + val diff = unspentDbs + .map(_.output) + .diff(fundedAddresses.map(tuple => + TransactionOutput(tuple._2, tuple._1.scriptPubKey))) + assert(diff.isEmpty, s"Extra funded addresses $diff") + } + } } diff --git a/wallet/src/main/scala/org/bitcoins/wallet/api/WalletApi.scala b/wallet/src/main/scala/org/bitcoins/wallet/api/WalletApi.scala index 77b6ea9d72..75dfc74882 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/api/WalletApi.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/api/WalletApi.scala @@ -176,6 +176,11 @@ trait WalletApi extends WalletLogger { def listSpentAddresses(account: HDAccount): Future[Vector[AddressDb]] + def listFundedAddresses(): Future[Vector[(AddressDb, CurrencyUnit)]] + + def listFundedAddresses( + account: HDAccount): Future[Vector[(AddressDb, CurrencyUnit)]] + def markUTXOsAsReserved( utxos: Vector[SpendingInfoDb]): Future[Vector[SpendingInfoDb]] 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 a9c645fb06..8d718a4fa3 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/internal/AddressHandling.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/internal/AddressHandling.scala @@ -1,5 +1,6 @@ package org.bitcoins.wallet.internal +import org.bitcoins.core.currency.CurrencyUnit import org.bitcoins.core.hd._ import org.bitcoins.core.number.UInt32 import org.bitcoins.core.protocol.BitcoinAddress @@ -70,6 +71,21 @@ private[wallet] trait AddressHandling extends WalletLogger { } } + 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)) + } + } + /** Enumerates the public keys in this wallet */ protected[wallet] def listPubkeys(): Future[Vector[ECPublicKey]] = addressDAO.findAllPubkeys() diff --git a/wallet/src/main/scala/org/bitcoins/wallet/models/AddressDAO.scala b/wallet/src/main/scala/org/bitcoins/wallet/models/AddressDAO.scala index a5d02d6d42..0c78e5b80a 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/models/AddressDAO.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/models/AddressDAO.scala @@ -1,5 +1,6 @@ package org.bitcoins.wallet.models +import org.bitcoins.core.currency.CurrencyUnit import org.bitcoins.core.hd.{ HDAccount, HDChainType, @@ -126,6 +127,19 @@ case class AddressDAO()( safeDatabase.runVec(query.result) } + def getFundedAddresses: Future[Vector[(AddressDb, CurrencyUnit)]] = { + val query = table + .join(spendingInfoTable) + .on(_.scriptPubKey === _.scriptPubKey) + .filter(_._2.state.inSet(TxoState.receivedStates)) + + safeDatabase + .runVec(query.result) + .map(_.map { + case (addrDb, utxoDb) => (addrDb, utxoDb.output.value) + }) + } + private def findMostRecentForChain( account: HDAccount, chain: HDChainType): slick.sql.SqlAction[