Reserved TxoState (#1111)

* Reserved TxoState

* Allow fund transaction handling to mark utxos as Reserved

* Add test, fix overloading default arg issue
This commit is contained in:
Ben Carman 2020-02-13 06:51:51 -06:00 committed by GitHub
parent bc34e2d327
commit cf3dd113b5
8 changed files with 64 additions and 21 deletions

View file

@ -15,6 +15,8 @@ class TxoStateTest extends BitcoinSUnitTest {
TxoState.fromString("ConfirmedReceived").get must be(
TxoState.ConfirmedReceived)
TxoState.fromString("Reserved").get must be(TxoState.Reserved)
TxoState.fromString("PendingConfirmationsSpent").get must be(
TxoState.PendingConfirmationsSpent)

View file

@ -18,6 +18,9 @@ object TxoState {
/** Means we have received funds and they are fully confirmed for this utxo */
final case object ConfirmedReceived extends ReceivedState
/** Means we have not spent this utxo yet, but will be used in a future transaction */
final case object Reserved extends SpentState
/** Means we have spent this utxo, but it is not fully confirmed */
final case object PendingConfirmationsSpent extends SpentState
@ -27,6 +30,7 @@ object TxoState {
val all: Vector[TxoState] = Vector(DoesNotExist,
PendingConfirmationsReceived,
ConfirmedReceived,
Reserved,
PendingConfirmationsSpent,
ConfirmedSpent)

View file

@ -27,11 +27,14 @@ trait BitcoinSFixture extends BitcoinSAsyncFixtureTest {
destroy: T => Future[Any])(test: OneArgAsyncTest): FutureOutcome = {
val fixtureF = build()
val outcomeF: Future[Outcome] = fixtureF.flatMap { fixture =>
test(fixture.asInstanceOf[FixtureParam]).toFuture
}.recoverWith { case err =>
FutureOutcome.failed(err).toFuture
}
val outcomeF: Future[Outcome] = fixtureF
.flatMap { fixture =>
test(fixture.asInstanceOf[FixtureParam]).toFuture
}
.recoverWith {
case err =>
FutureOutcome.failed(err).toFuture
}
val futOutcome: FutureOutcome = new FutureOutcome(outcomeF)

View file

@ -3,6 +3,7 @@ package org.bitcoins.wallet
import org.bitcoins.core.currency.Bitcoins
import org.bitcoins.core.protocol.transaction.TransactionOutput
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.core.wallet.utxo.TxoState
import org.bitcoins.testkit.util.TestUtil
import org.bitcoins.testkit.wallet.{BitcoinSWalletTest, WalletTestUtil}
import org.bitcoins.testkit.wallet.FundWalletUtil.FundedWallet
@ -23,7 +24,8 @@ class FundTransactionHandlingTest extends BitcoinSWalletTest {
val wallet = fundedWallet.wallet
val fundedTxF = wallet.fundRawTransaction(destinations =
Vector(destination),
feeRate = feeRate)
feeRate = feeRate,
false)
for {
fundedTx <- fundedTxF
} yield {
@ -42,7 +44,8 @@ class FundTransactionHandlingTest extends BitcoinSWalletTest {
val wallet = fundedWallet.wallet
val fundedTxF = wallet.fundRawTransaction(destinations =
Vector(newDestination),
feeRate = feeRate)
feeRate = feeRate,
false)
for {
fundedTx <- fundedTxF
} yield {
@ -60,7 +63,8 @@ class FundTransactionHandlingTest extends BitcoinSWalletTest {
val wallet = fundedWallet.wallet
val fundedTxF = wallet.fundRawTransaction(destinations = destinations,
feeRate = feeRate)
feeRate = feeRate,
false)
for {
fundedTx <- fundedTxF
} yield {
@ -82,7 +86,8 @@ class FundTransactionHandlingTest extends BitcoinSWalletTest {
val wallet = fundedWallet.wallet
val fundedTxF = wallet.fundRawTransaction(destinations =
Vector(tooBigOutput),
feeRate = feeRate)
feeRate = feeRate,
false)
recoverToSucceededIf[RuntimeException] {
fundedTxF
@ -100,7 +105,8 @@ class FundTransactionHandlingTest extends BitcoinSWalletTest {
//not have enough money for this
val fundedTxF = wallet.fundRawTransaction(destinations =
Vector(tooBigOutput),
feeRate = feeRate)
feeRate = feeRate,
false)
recoverToSucceededIf[RuntimeException] {
fundedTxF
@ -120,7 +126,8 @@ class FundTransactionHandlingTest extends BitcoinSWalletTest {
account1DbOpt <- account1DbF
fundedTx <- wallet.fundRawTransaction(Vector(newDestination),
feeRate,
account1DbOpt.get)
account1DbOpt.get,
false)
} yield {
assert(fundedTx.inputs.nonEmpty)
assert(fundedTx.outputs.contains(newDestination))
@ -141,11 +148,27 @@ class FundTransactionHandlingTest extends BitcoinSWalletTest {
account1DbOpt <- account1DbF
fundedTx <- wallet.fundRawTransaction(Vector(newDestination),
feeRate,
account1DbOpt.get)
account1DbOpt.get,
false)
} yield fundedTx
recoverToSucceededIf[RuntimeException] {
fundedTxF
}
}
it must "mark utxos as reserved after building a transaction" in {
fundedWallet: FundedWallet =>
val wallet = fundedWallet.wallet
val fundedTxF = wallet.fundRawTransaction(destinations =
Vector(destination),
feeRate = feeRate,
markAsReserved = true)
for {
fundedTx <- fundedTxF
spendingInfos <- wallet.spendingInfoDAO.findOutputsBeingSpent(fundedTx)
} yield {
assert(spendingInfos.exists(_.state == TxoState.Reserved))
}
}
}

View file

@ -42,7 +42,8 @@ sealed abstract class Wallet extends LockedWallet with UnlockedWalletApi {
destinations = Vector(destination),
feeRate = feeRate,
fromAccount = fromAccount,
keyManagerOpt = Some(keyManager))
keyManagerOpt = Some(keyManager),
markAsReserved = false)
signed <- txBuilder.sign
ourOuts <- findOurOuts(signed)
_ <- processOurTransaction(signed, blockHashOpt = None)

View file

@ -151,6 +151,9 @@ trait LockedWalletApi extends WalletApi {
def listAddresses(account: HDAccount): Future[Vector[AddressDb]]
def markUTXOsAsReserved(
utxos: Vector[SpendingInfoDb]): Future[Vector[SpendingInfoDb]]
/** Checks if the wallet contains any data */
def isEmpty(): Future[Boolean]

View file

@ -16,24 +16,28 @@ trait FundTransactionHandling extends WalletLogger { self: LockedWalletApi =>
def fundRawTransaction(
destinations: Vector[TransactionOutput],
feeRate: FeeUnit): Future[Transaction] = {
feeRate: FeeUnit,
markAsReserved: Boolean): Future[Transaction] = {
for {
account <- getDefaultAccount()
funded <- fundRawTransaction(destinations = destinations,
feeRate = feeRate,
fromAccount = account)
fromAccount = account,
markAsReserved = markAsReserved)
} yield funded
}
def fundRawTransaction(
destinations: Vector[TransactionOutput],
feeRate: FeeUnit,
fromAccount: AccountDb): Future[Transaction] = {
fromAccount: AccountDb,
markAsReserved: Boolean = false): Future[Transaction] = {
val txBuilderF =
fundRawTransactionInternal(destinations = destinations,
feeRate = feeRate,
fromAccount = fromAccount,
keyManagerOpt = None)
keyManagerOpt = None,
markAsReserved = markAsReserved)
txBuilderF.flatMap(_.unsignedTx)
}
@ -52,14 +56,17 @@ trait FundTransactionHandling extends WalletLogger { self: LockedWalletApi =>
destinations: Vector[TransactionOutput],
feeRate: FeeUnit,
fromAccount: AccountDb,
keyManagerOpt: Option[BIP39KeyManager]): Future[BitcoinTxBuilder] = {
keyManagerOpt: Option[BIP39KeyManager],
markAsReserved: Boolean = false): Future[BitcoinTxBuilder] = {
val utxosF = listUtxos(fromAccount.hdAccount)
val changeAddrF = getNewChangeAddress(fromAccount)
val selectedUtxosF = for {
walletUtxos <- utxosF
//currently just grab the biggest utxos
selectedUtxos = CoinSelector
utxos = CoinSelector
.accumulateLargest(walletUtxos, destinations, feeRate)
selectedUtxos <- if (markAsReserved) markUTXOsAsReserved(utxos)
else Future.successful(utxos)
} yield selectedUtxos
val addrInfosWithUtxoF: Future[Vector[(SpendingInfoDb, AddressInfo)]] =

View file

@ -203,8 +203,8 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
s"Marked utxo=${updated.toHumanReadableString} as state=${updated.state}")
)
updatedF.map(Some(_))
case TxoState.ConfirmedSpent | TxoState.PendingConfirmationsSpent |
TxoState.DoesNotExist =>
case TxoState.Reserved | TxoState.ConfirmedSpent |
TxoState.PendingConfirmationsSpent | TxoState.DoesNotExist =>
FutureUtil.none
}
}