mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2024-11-19 09:52:09 +01:00
2020 1 11 accounts (#1022)
* Create 'fundRawTransaction' and 'fundRawTransactionInternal' * Move TransactionTestUtil to testkit, begin writing tests for fundrawtransaction * Add FundTransactionTests * Move things to 'FundTransactionHandling' trait * Start segregating things by HDAccount in the wallet, this gives us the ability to query for balances based on account, fund a transaction based on an account, generate a new address based on an account etc. All old api calls are now based on the default account, i.e. getBalance() returns the balance for account 0, fundRawTransaction funds the transaction from account 0 etc. * Fix compile issue with 2.11.x * Bump address generation Thread.sleep() to 500ms * Address ben code review * Address code review
This commit is contained in:
parent
603951ea53
commit
b8c59b4c93
@ -80,7 +80,7 @@ class RoutesSpec
|
||||
}
|
||||
|
||||
"return the wallet's balance" in {
|
||||
(mockWalletApi.getBalance _)
|
||||
(mockWalletApi.getBalance: () => Future[CurrencyUnit])
|
||||
.expects()
|
||||
.returning(Future.successful(Bitcoins(50)))
|
||||
|
||||
|
@ -0,0 +1,65 @@
|
||||
package org.bitcoins.core.hd
|
||||
|
||||
import org.bitcoins.testkit.core.gen.HDGenerators
|
||||
import org.bitcoins.testkit.util.BitcoinSUnitTest
|
||||
import org.bitcoins.testkit.wallet.WalletTestUtil
|
||||
|
||||
class HDAccountTest extends BitcoinSUnitTest {
|
||||
|
||||
behavior of "HDAccount"
|
||||
|
||||
override implicit val generatorDrivenConfig: PropertyCheckConfiguration = {
|
||||
generatorDrivenConfigNewCode
|
||||
}
|
||||
|
||||
val defaultAcct = WalletTestUtil.defaultHdAccount
|
||||
val defaultPath = defaultAcct.path
|
||||
it must "determine if a bip32 path is the same as a given HDAccount" in {
|
||||
val isSameDefault = HDAccount.isSameAccount(defaultPath, defaultAcct)
|
||||
assert(isSameDefault, s"Must have symmetry")
|
||||
}
|
||||
|
||||
it must "fail if the given path is shorter than how BIP44 defines accounts" in {
|
||||
val missingLast = defaultPath.dropRight(1)
|
||||
|
||||
val isNotSame = !HDAccount.isSameAccount(missingLast, defaultAcct)
|
||||
|
||||
assert(isNotSame, s"If we drop the last element from the defualt path, we are not in the same account anymore")
|
||||
}
|
||||
|
||||
it must "fail if we modify the last element in the path" in {
|
||||
val newLast = defaultPath.last.copy(index = Int.MaxValue)
|
||||
val modifiedLast = defaultPath.updated(defaultPath.length - 1, newLast)
|
||||
|
||||
val isNotSame = !HDAccount.isSameAccount(modifiedLast, defaultAcct)
|
||||
|
||||
assert(isNotSame, s"We should have the same account if we modify the account index")
|
||||
}
|
||||
|
||||
it must "succeed if we add an arbitrary element onto the end of the path" in {
|
||||
val extraNode = defaultPath.:+(BIP32Node(0, true))
|
||||
|
||||
val isSame = HDAccount.isSameAccount(extraNode, defaultAcct)
|
||||
|
||||
assert(isSame, s"If we add an extra element onto the path, we are still in the same account")
|
||||
}
|
||||
|
||||
it must "fail with the empty path" in {
|
||||
val empty = BIP32Path.empty
|
||||
val isNotSame = !HDAccount.isSameAccount(empty, defaultAcct)
|
||||
assert(isNotSame)
|
||||
}
|
||||
|
||||
it must "have symmetry for isSameAccount with all hdaccount" in {
|
||||
forAll(HDGenerators.hdAccount) { acct =>
|
||||
val path = acct.path
|
||||
assert(HDAccount.isSameAccount(path, acct))
|
||||
}
|
||||
}
|
||||
|
||||
it must "not taken an arbitrary path and arbitrary account and find them in the same account" in {
|
||||
forAll(HDGenerators.hdAccount, HDGenerators.bip32Path) { case (acct, path) =>
|
||||
assert(!HDAccount.isSameAccount(path, acct))
|
||||
}
|
||||
}
|
||||
}
|
@ -7,6 +7,8 @@ package org.bitcoins.core.hd
|
||||
* and
|
||||
* [[https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki BIP49]]
|
||||
* account
|
||||
*
|
||||
* m / purpose' / coin_type' / account'
|
||||
*/
|
||||
case class HDAccount(
|
||||
coin: HDCoin,
|
||||
@ -23,3 +25,34 @@ case class HDAccount(
|
||||
def toChain(chainType: HDChainType): HDChain =
|
||||
HDChain(chainType = chainType, account = this)
|
||||
}
|
||||
|
||||
object HDAccount {
|
||||
|
||||
/** This method is meant to take in an arbitrary bip32 path and see
|
||||
* if it has the same account as the given account
|
||||
*
|
||||
* This is tricky as an account is defined as
|
||||
* m / purpose' / cointype' / account'
|
||||
*
|
||||
* whereas a bip32 path can be arbitrarily deep.
|
||||
*
|
||||
* We want to just check the first 4 elements of the path
|
||||
* and see if they are the same, which indicates we are in
|
||||
* the same account
|
||||
* */
|
||||
def isSameAccount(path: Vector[BIP32Node], account: HDAccount): Boolean = {
|
||||
if (account.path.length > path.length) {
|
||||
false
|
||||
} else {
|
||||
val zipped = path.zip(account.path)
|
||||
zipped.foldLeft(true) {
|
||||
case (past, (bip32Node, accountNode)) =>
|
||||
past && bip32Node == accountNode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def isSameAccount(bip32Path: BIP32Path, account: HDAccount): Boolean = {
|
||||
isSameAccount(bip32Path.path, account)
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,10 @@ package org.bitcoins.core.hd
|
||||
|
||||
/**
|
||||
* Address chain (external vs. change) used by
|
||||
*
|
||||
* Format:
|
||||
* m / purpose' / coin_type' / account' / change
|
||||
*
|
||||
* [[https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#change BIP44]],
|
||||
* [[https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki BIP84]]
|
||||
* and
|
||||
|
@ -5,6 +5,10 @@ package org.bitcoins.core.hd
|
||||
*
|
||||
* This has been used for deploying keychains that are compatible with
|
||||
* raw segwit, p2sh wrapped segwit, and raw scripts.
|
||||
*
|
||||
* Format:
|
||||
* m / purpose'
|
||||
*
|
||||
* @see [[https://github.com/bitcoin/bips/blob/master/bip-0043.mediawiki BIP43]]
|
||||
* @see [[https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#Purpose BIP44]]
|
||||
* @see [[https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki BIP84]]
|
||||
|
@ -8,7 +8,6 @@ import org.bitcoins.core.crypto.{DoubleSha256Digest, DoubleSha256DigestBE}
|
||||
import org.bitcoins.core.currency._
|
||||
import org.bitcoins.core.gcs.BlockFilter
|
||||
import org.bitcoins.core.protocol.BlockStamp
|
||||
import org.bitcoins.core.protocol.transaction.TransactionOutput
|
||||
import org.bitcoins.core.util.FutureUtil
|
||||
import org.bitcoins.db.AppConfig
|
||||
import org.bitcoins.keymanager.KeyManagerTestUtil
|
||||
@ -18,11 +17,12 @@ import org.bitcoins.server.BitcoinSAppConfig
|
||||
import org.bitcoins.server.BitcoinSAppConfig._
|
||||
import org.bitcoins.testkit.BitcoinSTestAppConfig
|
||||
import org.bitcoins.testkit.fixtures.BitcoinSFixture
|
||||
import org.bitcoins.testkit.util.{FileUtil, TransactionTestUtil}
|
||||
import org.bitcoins.testkit.util.FileUtil
|
||||
import org.bitcoins.testkit.wallet.FundWalletUtil.FundedWallet
|
||||
import org.bitcoins.wallet.api.{LockedWalletApi, UnlockedWalletApi}
|
||||
import org.bitcoins.wallet.config.WalletAppConfig
|
||||
import org.bitcoins.wallet.db.WalletDbManagement
|
||||
import org.bitcoins.wallet.{LockedWallet, Wallet, WalletLogger}
|
||||
import org.bitcoins.wallet.{Wallet, WalletLogger}
|
||||
import org.scalatest._
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
@ -34,6 +34,10 @@ trait BitcoinSWalletTest extends BitcoinSFixture with WalletLogger {
|
||||
implicit protected def config: BitcoinSAppConfig =
|
||||
BitcoinSTestAppConfig.getSpvTestConfig()
|
||||
|
||||
implicit protected def walletAppConfig: WalletAppConfig = {
|
||||
config.walletConf
|
||||
}
|
||||
|
||||
override def beforeAll(): Unit = {
|
||||
AppConfig.throwIfDefaultDatadir(config.walletConf)
|
||||
}
|
||||
@ -133,7 +137,7 @@ trait BitcoinSWalletTest extends BitcoinSFixture with WalletLogger {
|
||||
* underlying blockchain */
|
||||
def withFundedWallet(test: OneArgAsyncTest): FutureOutcome = {
|
||||
makeDependentFixture(
|
||||
build = () => createFundedWallet(nodeApi, chainQueryApi),
|
||||
build = () => FundWalletUtil.createFundedWallet(nodeApi, chainQueryApi),
|
||||
destroy = { funded: FundedWallet =>
|
||||
destroyWallet(funded.wallet)
|
||||
}
|
||||
@ -159,6 +163,12 @@ trait BitcoinSWalletTest extends BitcoinSFixture with WalletLogger {
|
||||
createDefaultWallet(nodeApi, chainQueryApi)
|
||||
}, destroy = destroyWallet)(test)
|
||||
|
||||
def withNewWallet2Accounts(test: OneArgAsyncTest): FutureOutcome = {
|
||||
makeDependentFixture(build = { () =>
|
||||
createWallet2Accounts(nodeApi, chainQueryApi)
|
||||
}, destroy = destroyWallet)(test)
|
||||
}
|
||||
|
||||
def withNewWalletAndBitcoind(test: OneArgAsyncTest): FutureOutcome = {
|
||||
val builder: () => Future[WalletWithBitcoind] = composeBuildersAndWrap(
|
||||
builder = { () =>
|
||||
@ -271,7 +281,7 @@ object BitcoinSWalletTest extends WalletLogger {
|
||||
nodeApi: NodeApi,
|
||||
chainQueryApi: ChainQueryApi)(
|
||||
implicit config: BitcoinSAppConfig,
|
||||
ec: ExecutionContext): () => Future[UnlockedWalletApi] =
|
||||
ec: ExecutionContext): () => Future[Wallet] =
|
||||
() => {
|
||||
val defaultConf = config.walletConf
|
||||
val walletConfig = extraConfig match {
|
||||
@ -291,11 +301,9 @@ object BitcoinSWalletTest extends WalletLogger {
|
||||
}
|
||||
|
||||
/** Creates a wallet with the default configuration */
|
||||
private def createDefaultWallet(
|
||||
nodeApi: NodeApi,
|
||||
chainQueryApi: ChainQueryApi)(
|
||||
def createDefaultWallet(nodeApi: NodeApi, chainQueryApi: ChainQueryApi)(
|
||||
implicit config: BitcoinSAppConfig,
|
||||
ec: ExecutionContext): Future[UnlockedWalletApi] = {
|
||||
ec: ExecutionContext): Future[Wallet] = {
|
||||
val bip39PasswordOpt = KeyManagerTestUtil.bip39PasswordOpt
|
||||
val km = createNewKeyManager(bip39PasswordOpt = bip39PasswordOpt)
|
||||
createNewWallet(
|
||||
@ -306,20 +314,18 @@ object BitcoinSWalletTest extends WalletLogger {
|
||||
chainQueryApi = chainQueryApi)(config, ec)() // get the standard config
|
||||
}
|
||||
|
||||
/** This wallet should have a total of 6 bitcoin in it
|
||||
* spread across 3 utxos that have values 1, 2, 3 bitcoins */
|
||||
case class FundedWallet(wallet: LockedWallet)
|
||||
|
||||
/** This creates a wallet that is funded that is not paired to a bitcoind instance. */
|
||||
def createFundedWallet(nodeApi: NodeApi, chainQueryApi: ChainQueryApi)(
|
||||
def createWallet2Accounts(nodeApi: NodeApi, chainQueryApi: ChainQueryApi)(
|
||||
implicit config: BitcoinSAppConfig,
|
||||
system: ActorSystem): Future[FundedWallet] = {
|
||||
|
||||
import system.dispatcher
|
||||
ec: ExecutionContext): Future[Wallet] = {
|
||||
val defaultWalletF = createDefaultWallet(nodeApi, chainQueryApi)
|
||||
for {
|
||||
wallet <- createDefaultWallet(nodeApi, chainQueryApi)
|
||||
funded <- fundWallet(wallet)
|
||||
} yield funded
|
||||
wallet <- defaultWalletF
|
||||
account1 = WalletTestUtil.getHdAccount1(wallet.walletConfig)
|
||||
newAccountWallet <- wallet.createNewAccount(hdAccount = account1,
|
||||
kmParams =
|
||||
wallet.keyManager.kmParams)
|
||||
} yield newAccountWallet
|
||||
|
||||
}
|
||||
|
||||
/** Pairs the given wallet with a bitcoind instance that has money in the bitcoind wallet */
|
||||
@ -376,48 +382,6 @@ object BitcoinSWalletTest extends WalletLogger {
|
||||
} yield funded
|
||||
}
|
||||
|
||||
/** Funds a bitcoin-s wallet with 3 utxos with 1, 2 and 3 bitcoin in the utxos */
|
||||
def fundWallet(wallet: UnlockedWalletApi)(
|
||||
implicit ec: ExecutionContext): Future[FundedWallet] = {
|
||||
//get three addresses
|
||||
val addressesF = Future.sequence(Vector.fill(3) {
|
||||
//this Thread.sleep is needed because of
|
||||
//https://github.com/bitcoin-s/bitcoin-s/issues/1009
|
||||
//once that is resolved we should be able to remove this
|
||||
Thread.sleep(500)
|
||||
wallet.getNewAddress()
|
||||
})
|
||||
|
||||
//construct three txs that send money to these addresses
|
||||
//these are "fictional" transactions in the sense that the
|
||||
//outpoints do not exist on a blockchain anywhere
|
||||
val amounts = Vector(1.bitcoin, 2.bitcoin, 3.bitcoin)
|
||||
val expectedAmt = amounts.fold(CurrencyUnits.zero)(_ + _)
|
||||
val txsF = for {
|
||||
addresses <- addressesF
|
||||
} yield {
|
||||
addresses.zip(amounts).map {
|
||||
case (addr, amt) =>
|
||||
val output =
|
||||
TransactionOutput(value = amt, scriptPubKey = addr.scriptPubKey)
|
||||
TransactionTestUtil.buildTransactionTo(output)
|
||||
}
|
||||
}
|
||||
|
||||
val fundedWalletF =
|
||||
txsF.flatMap(txs =>
|
||||
wallet.processTransactions(transactions = txs, blockHash = None))
|
||||
|
||||
//sanity check to make sure we have money
|
||||
for {
|
||||
fundedWallet <- fundedWalletF
|
||||
balance <- fundedWallet.getBalance()
|
||||
_ = require(
|
||||
balance == 6.bitcoin,
|
||||
s"Funding wallet fixture failed ot fund the wallet, got balance=${balance} expected=${expectedAmt}")
|
||||
} yield FundedWallet(fundedWallet.asInstanceOf[LockedWallet])
|
||||
}
|
||||
|
||||
/** Funds the given wallet with money from the given bitcoind */
|
||||
def fundWalletWithBitcoind(pair: WalletWithBitcoind)(
|
||||
implicit ec: ExecutionContext): Future[WalletWithBitcoind] = {
|
||||
|
@ -0,0 +1,124 @@
|
||||
package org.bitcoins.testkit.wallet
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import org.bitcoins.core.api.{ChainQueryApi, NodeApi}
|
||||
import org.bitcoins.core.currency.{Bitcoins, CurrencyUnit, CurrencyUnits, _}
|
||||
import org.bitcoins.core.hd.HDAccount
|
||||
import org.bitcoins.core.protocol.BitcoinAddress
|
||||
import org.bitcoins.core.protocol.transaction.TransactionOutput
|
||||
import org.bitcoins.server.BitcoinSAppConfig
|
||||
import org.bitcoins.testkit.util.TransactionTestUtil
|
||||
import org.bitcoins.testkit.wallet.FundWalletUtil.FundedWallet
|
||||
import org.bitcoins.wallet.{LockedWallet, Wallet}
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
trait FundWalletUtil {
|
||||
|
||||
def fundAccountForWallet(
|
||||
amts: Vector[CurrencyUnit],
|
||||
account: HDAccount,
|
||||
wallet: Wallet)(implicit ec: ExecutionContext): Future[Wallet] = {
|
||||
|
||||
val init = Future.successful(Vector.empty[BitcoinAddress])
|
||||
val addressesF: Future[Vector[BitcoinAddress]] = 0.until(3).foldLeft(init) {
|
||||
case (accumF, _) =>
|
||||
//this Thread.sleep is needed because of
|
||||
//https://github.com/bitcoin-s/bitcoin-s/issues/1009
|
||||
//once that is resolved we should be able to remove this
|
||||
for {
|
||||
accum <- accumF
|
||||
address <- wallet.getNewAddress(account)
|
||||
} yield {
|
||||
accum.:+(address)
|
||||
}
|
||||
}
|
||||
|
||||
//construct three txs that send money to these addresses
|
||||
//these are "fictional" transactions in the sense that the
|
||||
//outpoints do not exist on a blockchain anywhere
|
||||
val txsF = for {
|
||||
addresses <- addressesF
|
||||
} yield {
|
||||
addresses.zip(amts).map {
|
||||
case (addr, amt) =>
|
||||
val output =
|
||||
TransactionOutput(value = amt, scriptPubKey = addr.scriptPubKey)
|
||||
TransactionTestUtil.buildTransactionTo(output)
|
||||
}
|
||||
}
|
||||
|
||||
val fundedWalletF =
|
||||
txsF.flatMap(txs => wallet.processTransactions(txs, None))
|
||||
|
||||
fundedWalletF.map(_.asInstanceOf[Wallet])
|
||||
}
|
||||
|
||||
/** Funds a bitcoin-s wallet with 3 utxos with 1, 2 and 3 bitcoin in the utxos */
|
||||
def fundWallet(wallet: Wallet)(
|
||||
implicit ec: ExecutionContext): Future[FundedWallet] = {
|
||||
|
||||
val defaultAcctAmts = Vector(1.bitcoin, 2.bitcoin, 3.bitcoin)
|
||||
val expectedDefaultAmt = defaultAcctAmts.fold(CurrencyUnits.zero)(_ + _)
|
||||
val account1Amt = Vector(Bitcoins(0.2), Bitcoins(0.3), Bitcoins(0.5))
|
||||
val expectedAccount1Amt = account1Amt.fold(CurrencyUnits.zero)(_ + _)
|
||||
|
||||
val defaultAccount = wallet.walletConfig.defaultAccount
|
||||
val fundedDefaultAccountWalletF = FundWalletUtil.fundAccountForWallet(
|
||||
amts = defaultAcctAmts,
|
||||
account = defaultAccount,
|
||||
wallet = wallet
|
||||
)
|
||||
|
||||
val hdAccount1 = WalletTestUtil.getHdAccount1(wallet.walletConfig)
|
||||
val fundedAccount1WalletF = for {
|
||||
fundedDefaultAcct <- fundedDefaultAccountWalletF
|
||||
fundedAcct1 <- FundWalletUtil.fundAccountForWallet(
|
||||
amts = account1Amt,
|
||||
account = hdAccount1,
|
||||
fundedDefaultAcct
|
||||
)
|
||||
} yield fundedAcct1
|
||||
|
||||
//sanity check to make sure we have money
|
||||
for {
|
||||
fundedWallet <- fundedAccount1WalletF
|
||||
balance <- fundedWallet.getBalance(defaultAccount)
|
||||
_ = require(
|
||||
balance == expectedDefaultAmt,
|
||||
s"Funding wallet fixture failed to fund the wallet, got balance=${balance} expected=${expectedDefaultAmt}")
|
||||
|
||||
account1Balance <- fundedWallet.getBalance(hdAccount1)
|
||||
_ = require(
|
||||
account1Balance == expectedAccount1Amt,
|
||||
s"Funding wallet fixture failed to fund account 1, " +
|
||||
s"got balance=${hdAccount1} expected=${expectedAccount1Amt}"
|
||||
)
|
||||
|
||||
} yield FundedWallet(fundedWallet.asInstanceOf[LockedWallet])
|
||||
}
|
||||
}
|
||||
|
||||
object FundWalletUtil extends FundWalletUtil {
|
||||
|
||||
/** This is a wallet that was two funded accounts
|
||||
* Account 0 (default account) has utxos of 1,2,3 bitcoin in it (6 btc total)
|
||||
* Account 1 has a utxos of 0.2,0.3,0.5 bitcoin in it (0.6 total)
|
||||
* */
|
||||
case class FundedWallet(wallet: LockedWallet)
|
||||
|
||||
/** This creates a wallet that was two funded accounts
|
||||
* Account 0 (default account) has utxos of 1,2,3 bitcoin in it (6 btc total)
|
||||
* Account 1 has a utxos of 0.2,0.3,0.5 bitcoin in it (1 btc total)
|
||||
* */
|
||||
def createFundedWallet(nodeApi: NodeApi, chainQueryApi: ChainQueryApi)(
|
||||
implicit config: BitcoinSAppConfig,
|
||||
system: ActorSystem): Future[FundedWallet] = {
|
||||
|
||||
import system.dispatcher
|
||||
for {
|
||||
wallet <- BitcoinSWalletTest.createWallet2Accounts(nodeApi, chainQueryApi)
|
||||
funded <- FundWalletUtil.fundWallet(wallet)
|
||||
} yield funded
|
||||
}
|
||||
}
|
@ -24,6 +24,7 @@ import org.bitcoins.core.wallet.utxo.TxoState
|
||||
import org.bitcoins.testkit.Implicits._
|
||||
import org.bitcoins.testkit.core.gen.{CryptoGenerators, NumberGenerator}
|
||||
import org.bitcoins.testkit.fixtures.WalletDAOs
|
||||
import org.bitcoins.wallet.config.WalletAppConfig
|
||||
import org.bitcoins.wallet.models._
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
@ -69,8 +70,14 @@ object WalletTestUtil {
|
||||
private def freshXpub(): ExtPublicKey =
|
||||
CryptoGenerators.extPublicKey.sampleSome
|
||||
|
||||
val firstAccount = HDAccount(HDCoin(HDPurposes.SegWit, hdCoinType), 0)
|
||||
def firstAccountDb = AccountDb(freshXpub(), firstAccount)
|
||||
val defaultHdAccount = HDAccount(HDCoin(HDPurposes.SegWit, hdCoinType), 0)
|
||||
|
||||
def getHdAccount1(walletAppConfig: WalletAppConfig): HDAccount = {
|
||||
val purpose = walletAppConfig.defaultAccountKind
|
||||
HDAccount(coin = HDCoin(purpose, HDCoinType.Testnet), index = 1)
|
||||
}
|
||||
|
||||
def firstAccountDb = AccountDb(freshXpub(), defaultHdAccount)
|
||||
|
||||
private def randomScriptWitness: ScriptWitness =
|
||||
P2WPKHWitnessV0(freshXpub().key)
|
||||
|
@ -0,0 +1,42 @@
|
||||
package org.bitcoins.wallet
|
||||
|
||||
import org.bitcoins.testkit.wallet.{BitcoinSWalletTest, WalletTestUtil}
|
||||
import org.scalatest.FutureOutcome
|
||||
|
||||
class AddressHandlingTest extends BitcoinSWalletTest {
|
||||
type FixtureParam = Wallet
|
||||
|
||||
override def withFixture(test: OneArgAsyncTest): FutureOutcome = {
|
||||
withNewWallet2Accounts(test)
|
||||
}
|
||||
|
||||
behavior of "AddressHandling"
|
||||
|
||||
it must "generate a new address for the default account and then find it" in { wallet: Wallet =>
|
||||
val addressF = wallet.getNewAddress()
|
||||
|
||||
for {
|
||||
address <- addressF
|
||||
exists <- wallet.contains(address, None)
|
||||
} yield {
|
||||
assert(exists, s"Wallet must contain address after generating it")
|
||||
}
|
||||
}
|
||||
|
||||
it must "generate an address for a non default account and then find it" in { wallet: Wallet =>
|
||||
val account1 = WalletTestUtil.getHdAccount1(wallet.walletConfig)
|
||||
val addressF = wallet.getNewAddress(account1)
|
||||
for {
|
||||
address <- addressF
|
||||
listAddressesForAcct <- wallet.listAddresses(account1)
|
||||
exists <- wallet.contains(address, Some(account1))
|
||||
doesNotExist <- wallet.contains(address, None)
|
||||
} yield {
|
||||
assert(listAddressesForAcct.nonEmpty)
|
||||
assert(listAddressesForAcct.map(_.address).contains(address))
|
||||
assert(exists, s"Wallet must contain address in specific after generating it")
|
||||
assert(doesNotExist, s"Wallet must NOT contain address in default account when address is specified")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -4,8 +4,8 @@ import org.bitcoins.core.currency.Bitcoins
|
||||
import org.bitcoins.core.protocol.transaction.TransactionOutput
|
||||
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
|
||||
import org.bitcoins.testkit.util.TestUtil
|
||||
import org.bitcoins.testkit.wallet.BitcoinSWalletTest
|
||||
import org.bitcoins.testkit.wallet.BitcoinSWalletTest.FundedWallet
|
||||
import org.bitcoins.testkit.wallet.{BitcoinSWalletTest, WalletTestUtil}
|
||||
import org.bitcoins.testkit.wallet.FundWalletUtil.FundedWallet
|
||||
import org.scalatest.FutureOutcome
|
||||
|
||||
class FundTransactionHandlingTest extends BitcoinSWalletTest {
|
||||
@ -83,7 +83,7 @@ class FundTransactionHandlingTest extends BitcoinSWalletTest {
|
||||
}
|
||||
}
|
||||
|
||||
it must "fail to fund a raw transaction if we have the _exact_ amount of money in the wallet because of the fee" in {fundedWallet: FundedWallet =>
|
||||
it must "fail to fund a raw transaction if we have the _exact_ amount of money in the wallet because of the fee" in { fundedWallet: FundedWallet =>
|
||||
//our wallet should only have 6 bitcoin in it
|
||||
val tooMuchMoney = Bitcoins(6)
|
||||
val tooBigOutput = destination.copy(value = tooMuchMoney)
|
||||
@ -100,8 +100,46 @@ class FundTransactionHandlingTest extends BitcoinSWalletTest {
|
||||
}
|
||||
}
|
||||
|
||||
it must "fund from a specific account" ignore { _: FundedWallet =>
|
||||
assert(false)
|
||||
it must "fund from a specific account" in { fundedWallet: FundedWallet =>
|
||||
//we want to fund from account 1, not hte default account
|
||||
//account 1 has 1 btc in it
|
||||
val amt = Bitcoins(0.1)
|
||||
|
||||
val newDestination = destination.copy(value = amt)
|
||||
val wallet = fundedWallet.wallet
|
||||
val account1 = WalletTestUtil.getHdAccount1(wallet.walletConfig)
|
||||
val account1DbF = wallet.accountDAO.findByAccount(account1)
|
||||
for {
|
||||
account1DbOpt <- account1DbF
|
||||
fundedTx <- wallet.fundRawTransaction(
|
||||
Vector(newDestination),
|
||||
feeRate,
|
||||
account1DbOpt.get)
|
||||
} yield {
|
||||
assert(fundedTx.inputs.nonEmpty)
|
||||
assert(fundedTx.outputs.contains(newDestination))
|
||||
assert(fundedTx.outputs.length == 2)
|
||||
}
|
||||
}
|
||||
|
||||
it must "fail to fund from an account that does not have the funds" in { fundedWallet: FundedWallet =>
|
||||
//account 1 should only have 1 btc in it
|
||||
val amt = Bitcoins(1.1)
|
||||
|
||||
val newDestination = destination.copy(value = amt)
|
||||
val wallet = fundedWallet.wallet
|
||||
val account1 = WalletTestUtil.getHdAccount1(wallet.walletConfig)
|
||||
val account1DbF = wallet.accountDAO.findByAccount(account1)
|
||||
val fundedTxF = for {
|
||||
account1DbOpt <- account1DbF
|
||||
fundedTx <- wallet.fundRawTransaction(
|
||||
Vector(newDestination),
|
||||
feeRate,
|
||||
account1DbOpt.get)
|
||||
} yield fundedTx
|
||||
|
||||
recoverToSucceededIf[RuntimeException] {
|
||||
fundedTxF
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import java.nio.file.Files
|
||||
|
||||
import org.bitcoins.core.crypto.AesPassword
|
||||
import org.bitcoins.core.hd.HDChainType.{Change, External}
|
||||
import org.bitcoins.core.hd.{HDChainType, HDPurpose}
|
||||
import org.bitcoins.core.hd.{HDAccount, HDChainType, HDPurpose}
|
||||
import org.bitcoins.core.protocol.BitcoinAddress
|
||||
import org.bitcoins.core.util.FutureUtil
|
||||
import org.bitcoins.keymanager.KeyManagerUnlockError.MnemonicNotFound
|
||||
@ -64,16 +64,13 @@ class WalletUnitTest extends BitcoinSWalletTest {
|
||||
it should "know what the last address index is" in { walletApi =>
|
||||
val wallet = walletApi.asInstanceOf[Wallet]
|
||||
|
||||
def getMostRecent(
|
||||
purpose: HDPurpose,
|
||||
chain: HDChainType,
|
||||
acctIndex: Int
|
||||
): Future[AddressDb] = {
|
||||
def getMostRecent(hdAccount: HDAccount,
|
||||
chain: HDChainType): Future[AddressDb] = {
|
||||
val recentOptFut: Future[Option[AddressDb]] = chain match {
|
||||
case Change =>
|
||||
wallet.addressDAO.findMostRecentChange(purpose, acctIndex)
|
||||
wallet.addressDAO.findMostRecentChange(hdAccount)
|
||||
case External =>
|
||||
wallet.addressDAO.findMostRecentExternal(purpose, acctIndex)
|
||||
wallet.addressDAO.findMostRecentExternal(hdAccount)
|
||||
}
|
||||
|
||||
recentOptFut.map {
|
||||
@ -83,11 +80,10 @@ class WalletUnitTest extends BitcoinSWalletTest {
|
||||
}
|
||||
|
||||
def assertIndexIs(
|
||||
purpose: HDPurpose,
|
||||
hdAccount: HDAccount,
|
||||
chain: HDChainType,
|
||||
addrIndex: Int,
|
||||
accountIndex: Int): Future[Assertion] = {
|
||||
getMostRecent(purpose, chain, accountIndex) map { addr =>
|
||||
addrIndex: Int): Future[Assertion] = {
|
||||
getMostRecent(hdAccount, chain) map { addr =>
|
||||
assert(addr.path.address.index == addrIndex)
|
||||
}
|
||||
}
|
||||
@ -100,9 +96,8 @@ class WalletUnitTest extends BitcoinSWalletTest {
|
||||
* being reported
|
||||
*/
|
||||
def testChain(
|
||||
purpose: HDPurpose,
|
||||
chain: HDChainType,
|
||||
accIdx: Int): Future[Assertion] = {
|
||||
hdAccount: HDAccount,
|
||||
chain: HDChainType): Future[Assertion] = {
|
||||
val getAddrFunc: () => Future[BitcoinAddress] = chain match {
|
||||
case Change => wallet.getNewChangeAddress _
|
||||
case External => wallet.getNewAddress _
|
||||
@ -111,9 +106,9 @@ class WalletUnitTest extends BitcoinSWalletTest {
|
||||
_ <- {
|
||||
val addrF = chain match {
|
||||
case Change =>
|
||||
wallet.addressDAO.findMostRecentChange(purpose, accIdx)
|
||||
wallet.addressDAO.findMostRecentChange(hdAccount)
|
||||
case External =>
|
||||
wallet.addressDAO.findMostRecentExternal(purpose, accIdx)
|
||||
wallet.addressDAO.findMostRecentExternal(hdAccount)
|
||||
}
|
||||
addrF.map {
|
||||
case Some(addr) =>
|
||||
@ -123,12 +118,11 @@ class WalletUnitTest extends BitcoinSWalletTest {
|
||||
}
|
||||
}
|
||||
_ <- FutureUtil.sequentially(addrRange)(_ => getAddrFunc())
|
||||
_ <- assertIndexIs(purpose,
|
||||
_ <- assertIndexIs(hdAccount,
|
||||
chain,
|
||||
accountIndex = accIdx,
|
||||
addrIndex = addressesToGenerate)
|
||||
newest <- getAddrFunc()
|
||||
res <- getMostRecent(purpose, chain, accIdx).map { found =>
|
||||
res <- getMostRecent(hdAccount, chain).map { found =>
|
||||
assert(found.address == newest)
|
||||
assert(found.path.address.index == addressesToGenerate + 1)
|
||||
}
|
||||
@ -137,9 +131,8 @@ class WalletUnitTest extends BitcoinSWalletTest {
|
||||
|
||||
for {
|
||||
account <- wallet.getDefaultAccount()
|
||||
accIdx = account.hdAccount.index
|
||||
_ <- testChain(wallet.DEFAULT_HD_PURPOSE, External, accIdx)
|
||||
res <- testChain(wallet.DEFAULT_HD_PURPOSE, Change, accIdx)
|
||||
_ <- testChain(hdAccount = account.hdAccount, External)
|
||||
res <- testChain(hdAccount = account.hdAccount, Change)
|
||||
} yield res
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,7 @@ class AccountDAOTest extends BitcoinSWalletTest with WalletDAOFixture {
|
||||
val accountDAO = daos.accountDAO
|
||||
for {
|
||||
created <- {
|
||||
val account = WalletTestUtil.firstAccount
|
||||
val account = WalletTestUtil.defaultHdAccount
|
||||
|
||||
val xpub = CryptoGenerators.extPublicKey.sampleSome
|
||||
|
||||
@ -22,4 +22,21 @@ class AccountDAOTest extends BitcoinSWalletTest with WalletDAOFixture {
|
||||
(created.hdAccount.coin, created.hdAccount.index))
|
||||
} yield assert(found.contains(created))
|
||||
}
|
||||
|
||||
|
||||
it must "find an account by HdAccount" in { daos =>
|
||||
val accountDAO = daos.accountDAO
|
||||
val account = WalletTestUtil.getHdAccount1(walletAppConfig = walletAppConfig)
|
||||
for {
|
||||
created <- {
|
||||
|
||||
val xpub = CryptoGenerators.extPublicKey.sampleSome
|
||||
|
||||
val accountDb = AccountDb(xpub, account)
|
||||
accountDAO.create(accountDb)
|
||||
}
|
||||
found <- accountDAO.findByAccount(account)
|
||||
} yield assert(found.contains(created))
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -58,10 +58,14 @@ sealed abstract class Wallet extends LockedWallet with UnlockedWalletApi {
|
||||
}
|
||||
}
|
||||
|
||||
// todo: check if there's addresses in the most recent
|
||||
// account before creating new
|
||||
/** 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
|
||||
*
|
||||
* @param kmParams
|
||||
* @return
|
||||
*/
|
||||
override def createNewAccount(kmParams: KeyManagerParams): Future[Wallet] = {
|
||||
accountDAO
|
||||
val lastAccountOptF = accountDAO
|
||||
.findAll()
|
||||
.map(_.filter(_.hdAccount.purpose == kmParams.purpose))
|
||||
.map(_.sortBy(_.hdAccount.index))
|
||||
@ -69,33 +73,44 @@ sealed abstract class Wallet extends LockedWallet with UnlockedWalletApi {
|
||||
// to know what the index of our new account
|
||||
// should be.
|
||||
.map(_.lastOption)
|
||||
.flatMap { mostRecentOpt =>
|
||||
val accountIndex = mostRecentOpt match {
|
||||
case None => 0 // no accounts present in wallet
|
||||
case Some(account) => account.hdAccount.index + 1
|
||||
}
|
||||
logger.info(
|
||||
s"Creating new account at index $accountIndex for purpose ${kmParams.purpose}")
|
||||
val hdCoin =
|
||||
HDCoin(purpose = keyManager.kmParams.purpose,
|
||||
coinType = HDUtil.getCoinType(keyManager.kmParams.network))
|
||||
val newAccount = HDAccount(hdCoin, accountIndex)
|
||||
val xpub: ExtPublicKey = {
|
||||
keyManager.deriveXPub(newAccount) 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, newAccount)
|
||||
val accountCreationF = accountDAO.create(newAccountDb)
|
||||
accountCreationF.map(created =>
|
||||
logger.debug(s"Created new account ${created.hdAccount}"))
|
||||
accountCreationF
|
||||
|
||||
lastAccountOptF.flatMap {
|
||||
case Some(accountDb) =>
|
||||
val hdAccount = accountDb.hdAccount
|
||||
val newAccount = hdAccount.copy(index = hdAccount.index + 1)
|
||||
createNewAccount(newAccount, kmParams)
|
||||
case None =>
|
||||
createNewAccount(walletConfig.defaultAccount, kmParams)
|
||||
}
|
||||
}
|
||||
|
||||
// todo: check if there's addresses in the most recent
|
||||
// account before creating new
|
||||
override def createNewAccount(
|
||||
hdAccount: HDAccount,
|
||||
kmParams: KeyManagerParams): Future[Wallet] = {
|
||||
val accountIndex = hdAccount.index
|
||||
logger.info(
|
||||
s"Creating new account at index $accountIndex for purpose ${kmParams.purpose}")
|
||||
val hdCoin =
|
||||
HDCoin(purpose = keyManager.kmParams.purpose,
|
||||
coinType = HDUtil.getCoinType(keyManager.kmParams.network))
|
||||
val newAccount = HDAccount(hdCoin, accountIndex)
|
||||
val xpub: ExtPublicKey = {
|
||||
keyManager.deriveXPub(newAccount) 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, newAccount)
|
||||
val accountCreationF = accountDAO.create(newAccountDb)
|
||||
accountCreationF.map(created =>
|
||||
logger.debug(s"Created new account ${created.hdAccount}"))
|
||||
accountCreationF
|
||||
.map(_ => Wallet(keyManager, nodeApi, chainQueryApi))
|
||||
}
|
||||
}
|
||||
|
@ -7,10 +7,10 @@ import org.bitcoins.core.config.NetworkParameters
|
||||
import org.bitcoins.core.crypto.{DoubleSha256DigestBE, _}
|
||||
import org.bitcoins.core.currency.CurrencyUnit
|
||||
import org.bitcoins.core.gcs.{GolombFilter, SimpleFilterMatcher}
|
||||
import org.bitcoins.core.hd.{AddressType, HDPurpose}
|
||||
import org.bitcoins.core.hd.{AddressType, HDAccount, HDPurpose}
|
||||
import org.bitcoins.core.protocol.blockchain.{Block, ChainParams}
|
||||
import org.bitcoins.core.protocol.script.ScriptPubKey
|
||||
import org.bitcoins.core.protocol.transaction.{Transaction, TransactionOutput}
|
||||
import org.bitcoins.core.protocol.transaction.Transaction
|
||||
import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp}
|
||||
import org.bitcoins.core.util.FutureUtil
|
||||
import org.bitcoins.core.wallet.fee.FeeUnit
|
||||
@ -111,12 +111,28 @@ trait LockedWalletApi extends WalletApi {
|
||||
} yield confirmed + unconfirmed
|
||||
}
|
||||
|
||||
/** Gets the balance of the given account */
|
||||
def getBalance(account: HDAccount): Future[CurrencyUnit] = {
|
||||
val confirmedF = getConfirmedBalance(account)
|
||||
val unconfirmedF = getUnconfirmedBalance(account)
|
||||
for {
|
||||
confirmed <- confirmedF
|
||||
unconfirmed <- unconfirmedF
|
||||
} yield {
|
||||
confirmed + unconfirmed
|
||||
}
|
||||
}
|
||||
|
||||
/** Gets the sum of all confirmed UTXOs in this wallet */
|
||||
def getConfirmedBalance(): Future[CurrencyUnit]
|
||||
|
||||
def getConfirmedBalance(account: HDAccount): Future[CurrencyUnit]
|
||||
|
||||
/** Gets the sum of all unconfirmed UTXOs in this wallet */
|
||||
def getUnconfirmedBalance(): Future[CurrencyUnit]
|
||||
|
||||
def getUnconfirmedBalance(account: HDAccount): Future[CurrencyUnit]
|
||||
|
||||
/**
|
||||
* If a UTXO is spent outside of the wallet, we
|
||||
* need to remove it from the database so it won't be
|
||||
@ -129,8 +145,12 @@ trait LockedWalletApi extends WalletApi {
|
||||
* */
|
||||
def listUtxos(): Future[Vector[SpendingInfoDb]]
|
||||
|
||||
def listUtxos(account: HDAccount): Future[Vector[SpendingInfoDb]]
|
||||
|
||||
def listAddresses(): Future[Vector[AddressDb]]
|
||||
|
||||
def listAddresses(account: HDAccount): Future[Vector[AddressDb]]
|
||||
|
||||
/** Checks if the wallet contains any data */
|
||||
def isEmpty(): Future[Boolean]
|
||||
|
||||
@ -426,6 +446,8 @@ trait UnlockedWalletApi extends LockedWalletApi {
|
||||
} yield tx
|
||||
}
|
||||
|
||||
def createNewAccount(keyManagerParams: KeyManagerParams): Future[Wallet]
|
||||
|
||||
/**
|
||||
* Tries to create a new account in this wallet. Fails if the
|
||||
* most recent account has no transaction history, as per
|
||||
@ -433,7 +455,9 @@ trait UnlockedWalletApi extends LockedWalletApi {
|
||||
*
|
||||
* @see [[https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#account BIP44 account section]]
|
||||
*/
|
||||
def createNewAccount(keyManagerParams: KeyManagerParams): Future[WalletApi]
|
||||
def createNewAccount(
|
||||
hdAccount: HDAccount,
|
||||
keyManagerParams: KeyManagerParams): Future[Wallet]
|
||||
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,14 @@ package org.bitcoins.wallet.config
|
||||
import java.nio.file.{Files, Path}
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import org.bitcoins.core.hd.{AddressType, HDPurpose, HDPurposes}
|
||||
import org.bitcoins.core.hd.{
|
||||
AddressType,
|
||||
HDAccount,
|
||||
HDCoin,
|
||||
HDCoinType,
|
||||
HDPurpose,
|
||||
HDPurposes
|
||||
}
|
||||
import org.bitcoins.db.AppConfig
|
||||
import org.bitcoins.keymanager.{KeyManagerParams, WalletStorage}
|
||||
import org.bitcoins.wallet.db.WalletDbManagement
|
||||
@ -49,6 +56,11 @@ case class WalletAppConfig(
|
||||
}
|
||||
}
|
||||
|
||||
lazy val defaultAccount: HDAccount = {
|
||||
val purpose = defaultAccountKind
|
||||
HDAccount(coin = HDCoin(purpose, HDCoinType.Testnet), index = 0)
|
||||
}
|
||||
|
||||
lazy val bloomFalsePositiveRate: Double =
|
||||
config.getDouble("wallet.bloomFalsePositiveRate")
|
||||
|
||||
|
@ -33,7 +33,16 @@ private[wallet] trait AccountHandling { self: LockedWallet =>
|
||||
override protected[wallet] def getDefaultAccount(): Future[AccountDb] = {
|
||||
for {
|
||||
account <- accountDAO.read((DEFAULT_HD_COIN, 0))
|
||||
} yield getOrThrowAccount(account)
|
||||
} yield {
|
||||
|
||||
val acct = getOrThrowAccount(account)
|
||||
require(
|
||||
acct.hdAccount == walletConfig.defaultAccount,
|
||||
s"Divergence between configured default account and " +
|
||||
s"database default account walletConfig=${walletConfig.defaultAccount} database=${acct.hdAccount}"
|
||||
)
|
||||
acct
|
||||
}
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
|
@ -24,9 +24,39 @@ import scala.util.{Failure, Success}
|
||||
private[wallet] trait AddressHandling extends WalletLogger {
|
||||
self: LockedWallet =>
|
||||
|
||||
def contains(
|
||||
address: BitcoinAddress,
|
||||
accountOpt: Option[HDAccount]): Future[Boolean] = {
|
||||
val possibleAddressesF = accountOpt match {
|
||||
case Some(account) =>
|
||||
listAddresses(account)
|
||||
case None =>
|
||||
listAddresses()
|
||||
}
|
||||
|
||||
possibleAddressesF.map { possibleAddresses =>
|
||||
possibleAddresses.exists(_.address == address)
|
||||
}
|
||||
}
|
||||
|
||||
override def listAddresses(): Future[Vector[AddressDb]] =
|
||||
addressDAO.findAll()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/** Enumerates the public keys in this wallet */
|
||||
protected[wallet] def listPubkeys(): Future[Vector[ECPublicKey]] =
|
||||
addressDAO.findAllPubkeys()
|
||||
@ -65,14 +95,11 @@ private[wallet] trait AddressHandling extends WalletLogger {
|
||||
): Future[BitcoinAddress] = {
|
||||
logger.debug(s"Getting new $chainType adddress for ${account.hdAccount}")
|
||||
|
||||
val accountIndex = account.hdAccount.index
|
||||
|
||||
val lastAddrOptF = chainType match {
|
||||
case HDChainType.External =>
|
||||
addressDAO.findMostRecentExternal(account.hdAccount.purpose,
|
||||
accountIndex)
|
||||
addressDAO.findMostRecentExternal(account.hdAccount)
|
||||
case HDChainType.Change =>
|
||||
addressDAO.findMostRecentChange(account.hdAccount.purpose, accountIndex)
|
||||
addressDAO.findMostRecentChange(account.hdAccount)
|
||||
}
|
||||
|
||||
lastAddrOptF.flatMap { lastAddrOpt =>
|
||||
@ -129,12 +156,27 @@ private[wallet] trait AddressHandling extends WalletLogger {
|
||||
}
|
||||
}
|
||||
|
||||
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 getNewAddress(account: AccountDb): Future[BitcoinAddress] = {
|
||||
val addrF =
|
||||
getNewAddressHelper(account, HDChainType.External)
|
||||
addrF
|
||||
}
|
||||
|
||||
def findAccount(account: HDAccount): Future[Option[AccountDb]] = {
|
||||
accountDAO.findByAccount(account)
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
override def getNewAddress(
|
||||
addressType: AddressType): Future[BitcoinAddress] = {
|
||||
|
@ -53,11 +53,10 @@ trait FundTransactionHandling extends WalletLogger { self: LockedWalletApi =>
|
||||
feeRate: FeeUnit,
|
||||
fromAccount: AccountDb,
|
||||
keyManagerOpt: Option[BIP39KeyManager]): Future[BitcoinTxBuilder] = {
|
||||
val utxosF = listUtxos()
|
||||
val utxosF = listUtxos(fromAccount.hdAccount)
|
||||
val changeAddrF = getNewChangeAddress(fromAccount)
|
||||
val selectedUtxosF = for {
|
||||
walletUtxos <- utxosF
|
||||
change <- changeAddrF
|
||||
//currently just grab the biggest utxos
|
||||
selectedUtxos = CoinSelector
|
||||
.accumulateLargest(walletUtxos, destinations, feeRate)
|
||||
|
@ -2,6 +2,7 @@ package org.bitcoins.wallet.internal
|
||||
|
||||
import org.bitcoins.core.compat._
|
||||
import org.bitcoins.core.crypto.DoubleSha256DigestBE
|
||||
import org.bitcoins.core.hd.HDAccount
|
||||
import org.bitcoins.core.number.UInt32
|
||||
import org.bitcoins.core.protocol.BitcoinAddress
|
||||
import org.bitcoins.core.protocol.script.ScriptPubKey
|
||||
@ -29,8 +30,14 @@ private[wallet] trait UtxoHandling extends WalletLogger {
|
||||
self: LockedWallet =>
|
||||
|
||||
/** @inheritdoc */
|
||||
override def listUtxos(): Future[Vector[SpendingInfoDb]] =
|
||||
spendingInfoDAO.findAllUnspent()
|
||||
override def listUtxos(): Future[Vector[SpendingInfoDb]] = {
|
||||
listUtxos(walletConfig.defaultAccount)
|
||||
}
|
||||
|
||||
override def listUtxos(
|
||||
hdAccount: HDAccount): Future[Vector[SpendingInfoDb]] = {
|
||||
spendingInfoDAO.findAllUnspentForAccount(hdAccount)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to convert the provided spk to an address, and then checks if we have
|
||||
|
@ -1,13 +1,11 @@
|
||||
package org.bitcoins.wallet.models
|
||||
|
||||
import org.bitcoins.core.hd._
|
||||
import org.bitcoins.db.{CRUD, SlickUtil}
|
||||
import org.bitcoins.wallet.config._
|
||||
import slick.jdbc.SQLiteProfile.api._
|
||||
|
||||
import scala.concurrent.{Future}
|
||||
import org.bitcoins.db.CRUD
|
||||
import org.bitcoins.db.SlickUtil
|
||||
import scala.concurrent.ExecutionContext
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
case class AccountDAO()(
|
||||
implicit val ec: ExecutionContext,
|
||||
@ -38,4 +36,21 @@ case class AccountDAO()(
|
||||
findByPrimaryKeys(
|
||||
accounts.map(acc => (acc.hdAccount.coin, acc.hdAccount.index)))
|
||||
|
||||
def findByAccount(account: HDAccount): Future[Option[AccountDb]] = {
|
||||
val q = table
|
||||
.filter(_.coinType === account.coin.coinType)
|
||||
.filter(_.purpose === account.purpose)
|
||||
.filter(_.index === account.index)
|
||||
|
||||
database.run(q.result).map {
|
||||
case h +: Vector() =>
|
||||
Some(h)
|
||||
case Vector() =>
|
||||
None
|
||||
case accounts: Vector[AccountDb] =>
|
||||
//yikes, we should not have more the one account per coin type/purpose
|
||||
throw new RuntimeException(
|
||||
s"More than one account per account=${account}, got=${accounts}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
package org.bitcoins.wallet.models
|
||||
|
||||
import org.bitcoins.core.crypto.ECPublicKey
|
||||
import org.bitcoins.core.hd.{HDChainType, HDPurpose}
|
||||
import org.bitcoins.core.hd.{HDAccount, HDChainType}
|
||||
import org.bitcoins.core.protocol.BitcoinAddress
|
||||
import org.bitcoins.core.protocol.script.ScriptPubKey
|
||||
import org.bitcoins.db.{CRUD, SlickUtil}
|
||||
@ -44,11 +44,9 @@ case class AddressDAO()(
|
||||
/**
|
||||
* Finds the most recent change address in the wallet, if any
|
||||
*/
|
||||
def findMostRecentChange(
|
||||
purpose: HDPurpose,
|
||||
accountIndex: Int): Future[Option[AddressDb]] = {
|
||||
def findMostRecentChange(hdAccount: HDAccount): Future[Option[AddressDb]] = {
|
||||
val query =
|
||||
findMostRecentForChain(purpose, accountIndex, HDChainType.Change)
|
||||
findMostRecentForChain(hdAccount, HDChainType.Change)
|
||||
|
||||
database.run(query)
|
||||
}
|
||||
@ -66,11 +64,11 @@ case class AddressDAO()(
|
||||
}
|
||||
|
||||
private def findMostRecentForChain(
|
||||
purpose: HDPurpose,
|
||||
accountIndex: Int,
|
||||
account: HDAccount,
|
||||
chain: HDChainType): SqlAction[Option[AddressDb], NoStream, Effect.Read] = {
|
||||
addressesForAccountQuery(accountIndex)
|
||||
.filter(_.purpose === purpose)
|
||||
addressesForAccountQuery(account.index)
|
||||
.filter(_.purpose === account.purpose)
|
||||
.filter(_.accountCoin === account.coin.coinType)
|
||||
.filter(_.accountChainType === chain)
|
||||
.sortBy(_.addressIndex.desc)
|
||||
.take(1)
|
||||
@ -82,10 +80,9 @@ case class AddressDAO()(
|
||||
* Finds the most recent external address in the wallet, if any
|
||||
*/
|
||||
def findMostRecentExternal(
|
||||
purpose: HDPurpose,
|
||||
accountIndex: Int): Future[Option[AddressDb]] = {
|
||||
hdAccount: HDAccount): Future[Option[AddressDb]] = {
|
||||
val query =
|
||||
findMostRecentForChain(purpose, accountIndex, HDChainType.External)
|
||||
findMostRecentForChain(hdAccount, HDChainType.External)
|
||||
database.run(query)
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package org.bitcoins.wallet.models
|
||||
|
||||
import org.bitcoins.core.crypto.DoubleSha256DigestBE
|
||||
import org.bitcoins.core.hd.HDAccount
|
||||
import org.bitcoins.core.protocol.script.ScriptPubKey
|
||||
import org.bitcoins.core.protocol.transaction.{
|
||||
Transaction,
|
||||
@ -112,10 +113,21 @@ case class SpendingInfoDAO()(
|
||||
database.run(query.result).map(_.toVector)
|
||||
}
|
||||
|
||||
/** Finds all utxos for a given account */
|
||||
def findAllUnspentForAccount(
|
||||
hdAccount: HDAccount): Future[Vector[SpendingInfoDb]] = {
|
||||
val allUtxosF = findAllUnspent()
|
||||
allUtxosF.map { allUtxos =>
|
||||
allUtxos.filter(
|
||||
utxo =>
|
||||
HDAccount.isSameAccount(bip32Path = utxo.privKeyPath,
|
||||
account = hdAccount))
|
||||
}
|
||||
}
|
||||
|
||||
/** Enumerates all TX outpoints in the wallet */
|
||||
def findAllOutpoints(): Future[Vector[TransactionOutPoint]] = {
|
||||
val query = table.map(_.outPoint)
|
||||
database.runVec(query.result).map(_.toVector)
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user