mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-01-18 21:34:39 +01:00
Move blockhash to tx table from spending info table (#2744)
* Move blockhash to tx table from spending info table * Add test, fix spending tx id col name * Scaladocs, add test * Add more unit test * Make id not comparable in process tx test * Fix tests * attempt to fix * Add mempool comment to scaladoc * Deparallelize process inputs & outputs
This commit is contained in:
parent
7e23eecb20
commit
9494eec1b8
@ -374,7 +374,6 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory {
|
||||
EmptyScriptWitness,
|
||||
DoubleSha256DigestBE.empty,
|
||||
TxoState.PendingConfirmationsSpent,
|
||||
None,
|
||||
None
|
||||
)
|
||||
|
||||
@ -775,7 +774,7 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory {
|
||||
val tx = Transaction(
|
||||
"020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000")
|
||||
|
||||
val txDb = TransactionDbHelper.fromTransaction(tx)
|
||||
val txDb = TransactionDbHelper.fromTransaction(tx, None)
|
||||
|
||||
(mockWalletApi
|
||||
.findTransaction(_: DoubleSha256DigestBE))
|
||||
|
@ -58,17 +58,17 @@ object BitcoindRpcBackendUtil extends BitcoinSLogger {
|
||||
_ <- walletStateOpt match {
|
||||
case None =>
|
||||
for {
|
||||
utxos <- wallet.listUtxos()
|
||||
lastConfirmedOpt = utxos.filter(_.blockHash.isDefined).lastOption
|
||||
txDbs <- wallet.listTransactions()
|
||||
lastConfirmedOpt = txDbs.filter(_.blockHashOpt.isDefined).lastOption
|
||||
_ <- lastConfirmedOpt match {
|
||||
case None => Future.unit
|
||||
case Some(utxo) =>
|
||||
case Some(txDb) =>
|
||||
for {
|
||||
heightOpt <- bitcoind.getBlockHeight(utxo.blockHash.get)
|
||||
heightOpt <- bitcoind.getBlockHeight(txDb.blockHashOpt.get)
|
||||
_ <- heightOpt match {
|
||||
case Some(height) =>
|
||||
logger.info(
|
||||
s"Last utxo occurred at block $height, syncing from there")
|
||||
s"Last tx occurred at block $height, syncing from there")
|
||||
doSync(height, bitcoindHeight)
|
||||
case None => Future.unit
|
||||
}
|
||||
|
@ -2,13 +2,7 @@ package org.bitcoins.core.api.wallet.db
|
||||
|
||||
import org.bitcoins.core.api.db.DbRowAutoInc
|
||||
import org.bitcoins.core.api.keymanager.BIP39KeyManagerApi
|
||||
import org.bitcoins.core.hd.{
|
||||
HDChainType,
|
||||
HDPath,
|
||||
LegacyHDPath,
|
||||
NestedSegWitHDPath,
|
||||
SegWitHDPath
|
||||
}
|
||||
import org.bitcoins.core.hd._
|
||||
import org.bitcoins.core.protocol.script.{ScriptPubKey, ScriptWitness}
|
||||
import org.bitcoins.core.protocol.transaction.{
|
||||
Transaction,
|
||||
@ -34,8 +28,8 @@ case class SegwitV0SpendingInfo(
|
||||
scriptWitness: ScriptWitness,
|
||||
txid: DoubleSha256DigestBE,
|
||||
state: TxoState,
|
||||
id: Option[Long] = None,
|
||||
blockHash: Option[DoubleSha256DigestBE]
|
||||
spendingTxIdOpt: Option[DoubleSha256DigestBE],
|
||||
id: Option[Long] = None
|
||||
) extends SpendingInfoDb {
|
||||
override val redeemScriptOpt: Option[ScriptPubKey] = None
|
||||
override val scriptWitnessOpt: Option[ScriptWitness] = Some(scriptWitness)
|
||||
@ -49,10 +43,10 @@ case class SegwitV0SpendingInfo(
|
||||
override def copyWithId(id: Long): SegwitV0SpendingInfo =
|
||||
copy(id = Some(id))
|
||||
|
||||
/** Updates the `blockHash` field */
|
||||
override def copyWithBlockHash(
|
||||
blockHash: DoubleSha256DigestBE): SegwitV0SpendingInfo =
|
||||
copy(blockHash = Some(blockHash))
|
||||
/** Updates the `spendingTxId` field */
|
||||
override def copyWithSpendingTxId(
|
||||
txId: DoubleSha256DigestBE): SegwitV0SpendingInfo =
|
||||
copy(spendingTxIdOpt = Some(txId))
|
||||
}
|
||||
|
||||
/** DB representation of a legacy UTXO
|
||||
@ -63,10 +57,11 @@ case class LegacySpendingInfo(
|
||||
privKeyPath: LegacyHDPath,
|
||||
state: TxoState,
|
||||
txid: DoubleSha256DigestBE,
|
||||
blockHash: Option[DoubleSha256DigestBE],
|
||||
spendingTxIdOpt: Option[DoubleSha256DigestBE],
|
||||
id: Option[Long] = None
|
||||
) extends SpendingInfoDb {
|
||||
override val redeemScriptOpt: Option[ScriptPubKey] = None
|
||||
|
||||
override def scriptWitnessOpt: Option[ScriptWitness] = None
|
||||
|
||||
override type PathType = LegacyHDPath
|
||||
@ -78,9 +73,10 @@ case class LegacySpendingInfo(
|
||||
override def copyWithState(state: TxoState): LegacySpendingInfo =
|
||||
copy(state = state)
|
||||
|
||||
override def copyWithBlockHash(
|
||||
blockHash: DoubleSha256DigestBE): LegacySpendingInfo =
|
||||
copy(blockHash = Some(blockHash))
|
||||
/** Updates the `spendingTxId` field */
|
||||
override def copyWithSpendingTxId(
|
||||
txId: DoubleSha256DigestBE): LegacySpendingInfo =
|
||||
copy(spendingTxIdOpt = Some(txId))
|
||||
}
|
||||
|
||||
/** DB representation of a nested segwit V0
|
||||
@ -94,7 +90,7 @@ case class NestedSegwitV0SpendingInfo(
|
||||
scriptWitness: ScriptWitness,
|
||||
txid: DoubleSha256DigestBE,
|
||||
state: TxoState,
|
||||
blockHash: Option[DoubleSha256DigestBE],
|
||||
spendingTxIdOpt: Option[DoubleSha256DigestBE],
|
||||
id: Option[Long] = None
|
||||
) extends SpendingInfoDb {
|
||||
override val redeemScriptOpt: Option[ScriptPubKey] = Some(redeemScript)
|
||||
@ -109,10 +105,10 @@ case class NestedSegwitV0SpendingInfo(
|
||||
override def copyWithId(id: Long): NestedSegwitV0SpendingInfo =
|
||||
copy(id = Some(id))
|
||||
|
||||
/** Updates the `blockHash` field */
|
||||
override def copyWithBlockHash(
|
||||
blockHash: DoubleSha256DigestBE): NestedSegwitV0SpendingInfo =
|
||||
copy(blockHash = Some(blockHash))
|
||||
/** Updates the `spendingTxId` field */
|
||||
override def copyWithSpendingTxId(
|
||||
txId: DoubleSha256DigestBE): NestedSegwitV0SpendingInfo =
|
||||
copy(spendingTxIdOpt = Some(txId))
|
||||
}
|
||||
|
||||
/** The database level representation of a UTXO.
|
||||
@ -124,16 +120,6 @@ case class NestedSegwitV0SpendingInfo(
|
||||
*/
|
||||
sealed trait SpendingInfoDb extends DbRowAutoInc[SpendingInfoDb] {
|
||||
|
||||
state match {
|
||||
case TxoState.ConfirmedSpent | TxoState.ConfirmedReceived |
|
||||
TxoState.ImmatureCoinbase =>
|
||||
require(blockHash.isDefined,
|
||||
"Transaction cannot be confirmed without a blockHash")
|
||||
case TxoState.DoesNotExist | TxoState.PendingConfirmationsSpent |
|
||||
TxoState.PendingConfirmationsReceived | TxoState.Reserved =>
|
||||
()
|
||||
}
|
||||
|
||||
protected type PathType <: HDPath
|
||||
|
||||
/** This type is here to ensure copyWithSpent returns the same
|
||||
@ -158,8 +144,8 @@ sealed trait SpendingInfoDb extends DbRowAutoInc[SpendingInfoDb] {
|
||||
/** The TXID of the transaction this output was received in */
|
||||
def txid: DoubleSha256DigestBE
|
||||
|
||||
/** The hash of the block in which the transaction was included */
|
||||
def blockHash: Option[DoubleSha256DigestBE]
|
||||
/** TxId of the transaction that this output was spent by */
|
||||
def spendingTxIdOpt: Option[DoubleSha256DigestBE]
|
||||
|
||||
/** Converts the UTXO to the canonical `txid:vout` format */
|
||||
def toHumanReadableString: String =
|
||||
@ -168,8 +154,8 @@ sealed trait SpendingInfoDb extends DbRowAutoInc[SpendingInfoDb] {
|
||||
/** Updates the `spent` field */
|
||||
def copyWithState(state: TxoState): SpendingInfoType
|
||||
|
||||
/** Updates the `blockHash` field */
|
||||
def copyWithBlockHash(blockHash: DoubleSha256DigestBE): SpendingInfoType
|
||||
/** Updates the `spendingTxId` field */
|
||||
def copyWithSpendingTxId(txId: DoubleSha256DigestBE): SpendingInfoType
|
||||
|
||||
/** Converts a non-sensitive DB representation of a UTXO into
|
||||
* a signable (and sensitive) real-world UTXO
|
||||
|
@ -30,7 +30,8 @@ case class TransactionDb(
|
||||
totalOutput: CurrencyUnit,
|
||||
numInputs: Int,
|
||||
numOutputs: Int,
|
||||
lockTime: UInt32)
|
||||
lockTime: UInt32,
|
||||
blockHashOpt: Option[DoubleSha256DigestBE])
|
||||
extends TxDB {
|
||||
require(unsignedTx.inputs.forall(_.scriptSignature == EmptyScriptSignature),
|
||||
s"All ScriptSignatures must be empty, got $unsignedTx")
|
||||
@ -42,7 +43,9 @@ case class TransactionDb(
|
||||
|
||||
object TransactionDbHelper {
|
||||
|
||||
def fromTransaction(tx: Transaction): TransactionDb = {
|
||||
def fromTransaction(
|
||||
tx: Transaction,
|
||||
blockHashOpt: Option[DoubleSha256DigestBE]): TransactionDb = {
|
||||
val (unsignedTx, wTxIdBEOpt) = tx match {
|
||||
case btx: NonWitnessTransaction =>
|
||||
val unsignedInputs = btx.inputs.map(input =>
|
||||
@ -76,6 +79,7 @@ object TransactionDbHelper {
|
||||
totalOutput,
|
||||
tx.inputs.size,
|
||||
tx.outputs.size,
|
||||
tx.lockTime)
|
||||
tx.lockTime,
|
||||
blockHashOpt)
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ case class UTXORecord(
|
||||
path: HDPath,
|
||||
redeemScript: Option[ScriptPubKey], // RedeemScript
|
||||
scriptWitness: Option[ScriptWitness],
|
||||
blockHash: Option[DoubleSha256DigestBE], // block hash
|
||||
spendingTxIdOpt: Option[DoubleSha256DigestBE],
|
||||
id: Option[Long] = None
|
||||
) extends DbRowAutoInc[UTXORecord] {
|
||||
override def copyWithId(id: Long): UTXORecord = copy(id = Option(id))
|
||||
@ -47,7 +47,7 @@ case class UTXORecord(
|
||||
id = id,
|
||||
state = state,
|
||||
txid = txid,
|
||||
blockHash = blockHash
|
||||
spendingTxIdOpt = spendingTxIdOpt
|
||||
)
|
||||
|
||||
case (path: LegacyHDPath, None, None) =>
|
||||
@ -56,20 +56,22 @@ case class UTXORecord(
|
||||
privKeyPath = path,
|
||||
id = id,
|
||||
state = state,
|
||||
txid = txid,
|
||||
blockHash = blockHash)
|
||||
spendingTxIdOpt = spendingTxIdOpt,
|
||||
txid = txid)
|
||||
|
||||
case (path: NestedSegWitHDPath, Some(redeemScript), Some(scriptWitness))
|
||||
if WitnessScriptPubKey.isValidAsm(redeemScript.asm) =>
|
||||
NestedSegwitV0SpendingInfo(outpoint,
|
||||
TransactionOutput(value, scriptPubKey),
|
||||
path,
|
||||
redeemScript,
|
||||
scriptWitness,
|
||||
txid,
|
||||
state,
|
||||
blockHash,
|
||||
id)
|
||||
NestedSegwitV0SpendingInfo(
|
||||
outPoint = outpoint,
|
||||
output = TransactionOutput(value, scriptPubKey),
|
||||
privKeyPath = path,
|
||||
redeemScript = redeemScript,
|
||||
scriptWitness = scriptWitness,
|
||||
txid = txid,
|
||||
state = state,
|
||||
spendingTxIdOpt = spendingTxIdOpt,
|
||||
id = id
|
||||
)
|
||||
|
||||
case _ =>
|
||||
throw new IllegalArgumentException(
|
||||
@ -91,7 +93,7 @@ object UTXORecord {
|
||||
spendingInfoDb.privKeyPath,
|
||||
spendingInfoDb.redeemScriptOpt, // ReedemScript
|
||||
spendingInfoDb.scriptWitnessOpt,
|
||||
spendingInfoDb.blockHash, // block hash
|
||||
spendingInfoDb.spendingTxIdOpt,
|
||||
spendingInfoDb.id
|
||||
)
|
||||
}
|
||||
|
@ -35,7 +35,8 @@ object TxoState extends StringFactory[TxoState] {
|
||||
final case object ConfirmedSpent extends SpentState
|
||||
|
||||
val pendingConfStates: Set[TxoState] =
|
||||
Set(TxoState.PendingConfirmationsReceived,
|
||||
Set(TxoState.ImmatureCoinbase,
|
||||
TxoState.PendingConfirmationsReceived,
|
||||
TxoState.PendingConfirmationsSpent)
|
||||
|
||||
val confirmedStates: Set[TxoState] =
|
||||
|
@ -74,13 +74,13 @@ class DbManagementTest extends BitcoinSAsyncTest with EmbeddedPg {
|
||||
val result = walletDbManagement.migrate()
|
||||
walletAppConfig.driver match {
|
||||
case SQLite =>
|
||||
val expected = 9
|
||||
val expected = 10
|
||||
assert(result == expected)
|
||||
val flywayInfo = walletDbManagement.info()
|
||||
assert(flywayInfo.applied().length == expected)
|
||||
assert(flywayInfo.pending().length == 0)
|
||||
case PostgreSQL =>
|
||||
val expected = 7
|
||||
val expected = 8
|
||||
assert(result == expected)
|
||||
val flywayInfo = walletDbManagement.info()
|
||||
|
||||
|
@ -9,7 +9,7 @@ import org.bitcoins.core.api.feeprovider.FeeRateApi
|
||||
import org.bitcoins.core.api.node.NodeApi
|
||||
import org.bitcoins.core.currency._
|
||||
import org.bitcoins.core.gcs.BlockFilter
|
||||
import org.bitcoins.core.protocol.BlockStamp
|
||||
import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp}
|
||||
import org.bitcoins.core.protocol.transaction.Transaction
|
||||
import org.bitcoins.core.util.FutureUtil
|
||||
import org.bitcoins.core.wallet.fee._
|
||||
@ -66,6 +66,9 @@ trait BitcoinSWalletTest extends BitcoinSFixture with EmbeddedPg {
|
||||
|
||||
def nodeApi: NodeApi = MockNodeApi
|
||||
|
||||
val testAddr: BitcoinAddress =
|
||||
BitcoinAddress.fromString("bcrt1qlhctylgvdsvaanv539rg7hyn0sjkdm23y70kgq")
|
||||
|
||||
val legacyWalletConf: Config =
|
||||
ConfigFactory.parseString("bitcoin-s.wallet.defaultAccountType = legacy")
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
package org.bitcoins.testkit.wallet
|
||||
|
||||
import org.bitcoins.core.api.wallet.db
|
||||
import org.bitcoins.core.api.wallet.db._
|
||||
import org.bitcoins.core.config.RegTest
|
||||
import org.bitcoins.core.crypto._
|
||||
@ -125,14 +124,14 @@ object WalletTestUtil {
|
||||
TransactionOutput(1.bitcoin, spk)
|
||||
val scriptWitness = randomScriptWitness
|
||||
val privkeyPath = WalletTestUtil.sampleSegwitPath
|
||||
db.SegwitV0SpendingInfo(
|
||||
SegwitV0SpendingInfo(
|
||||
state = randomState,
|
||||
txid = randomTXID,
|
||||
outPoint = outpoint,
|
||||
output = output,
|
||||
privKeyPath = privkeyPath,
|
||||
scriptWitness = scriptWitness,
|
||||
blockHash = Some(randomBlockHash)
|
||||
spendingTxIdOpt = None
|
||||
)
|
||||
}
|
||||
|
||||
@ -142,12 +141,12 @@ object WalletTestUtil {
|
||||
val output =
|
||||
TransactionOutput(1.bitcoin, spk)
|
||||
val privKeyPath = WalletTestUtil.sampleLegacyPath
|
||||
db.LegacySpendingInfo(state = randomState,
|
||||
txid = randomTXID,
|
||||
outPoint = outpoint,
|
||||
output = output,
|
||||
privKeyPath = privKeyPath,
|
||||
blockHash = Some(randomBlockHash))
|
||||
LegacySpendingInfo(state = randomState,
|
||||
txid = randomTXID,
|
||||
outPoint = outpoint,
|
||||
output = output,
|
||||
privKeyPath = privKeyPath,
|
||||
spendingTxIdOpt = None)
|
||||
}
|
||||
|
||||
def sampleNestedSegwitUTXO(
|
||||
@ -158,7 +157,7 @@ object WalletTestUtil {
|
||||
TransactionOutput(1.bitcoin, P2SHScriptPubKey(wpkh))
|
||||
val scriptWitness = randomScriptWitness
|
||||
val privkeyPath = WalletTestUtil.sampleNestedSegwitPath
|
||||
db.NestedSegwitV0SpendingInfo(
|
||||
NestedSegwitV0SpendingInfo(
|
||||
state = randomState,
|
||||
txid = randomTXID,
|
||||
outPoint = outpoint,
|
||||
@ -166,7 +165,7 @@ object WalletTestUtil {
|
||||
privKeyPath = privkeyPath,
|
||||
redeemScript = wpkh,
|
||||
scriptWitness = scriptWitness,
|
||||
blockHash = Some(randomBlockHash)
|
||||
spendingTxIdOpt = None
|
||||
)
|
||||
}
|
||||
|
||||
@ -201,7 +200,8 @@ object WalletTestUtil {
|
||||
totalOutput = Satoshis.zero,
|
||||
numInputs = 1,
|
||||
numOutputs = 1,
|
||||
lockTime = UInt32.zero
|
||||
lockTime = UInt32.zero,
|
||||
blockHashOpt = Some(randomBlockHash)
|
||||
)
|
||||
val incomingDb = IncomingTransactionDb(utxo.txid, utxo.output.value)
|
||||
for {
|
||||
|
@ -41,7 +41,7 @@ class CoinSelectorTest extends BitcoinSWalletTest {
|
||||
output = TransactionOutput(10.sats, ScriptPubKey.empty),
|
||||
privKeyPath = WalletTestUtil.sampleSegwitPath,
|
||||
scriptWitness = WitnessGenerators.scriptWitness.sampleSome,
|
||||
blockHash = None
|
||||
spendingTxIdOpt = None
|
||||
)
|
||||
val utxo2 = SegwitV0SpendingInfo(
|
||||
txid = CryptoGenerators.doubleSha256Digest.sampleSome.flip,
|
||||
@ -51,7 +51,7 @@ class CoinSelectorTest extends BitcoinSWalletTest {
|
||||
output = TransactionOutput(90.sats, ScriptPubKey.empty),
|
||||
privKeyPath = WalletTestUtil.sampleSegwitPath,
|
||||
scriptWitness = WitnessGenerators.scriptWitness.sampleSome,
|
||||
blockHash = None
|
||||
spendingTxIdOpt = None
|
||||
)
|
||||
val utxo3 = SegwitV0SpendingInfo(
|
||||
txid = CryptoGenerators.doubleSha256Digest.sampleSome.flip,
|
||||
@ -61,7 +61,7 @@ class CoinSelectorTest extends BitcoinSWalletTest {
|
||||
output = TransactionOutput(20.sats, ScriptPubKey.empty),
|
||||
privKeyPath = WalletTestUtil.sampleSegwitPath,
|
||||
scriptWitness = WitnessGenerators.scriptWitness.sampleSome,
|
||||
blockHash = None
|
||||
spendingTxIdOpt = None
|
||||
)
|
||||
|
||||
test(CoinSelectionFixture(output, feeRate, utxo1, utxo2, utxo3))
|
||||
|
@ -36,11 +36,13 @@ class ProcessBlockTest extends BitcoinSWalletTest {
|
||||
height <- bitcoind.getBlockCount
|
||||
bestHash <- bitcoind.getBestBlockHash
|
||||
syncHeightOpt <- wallet.getSyncDescriptorOpt()
|
||||
txDbOpt <- wallet.transactionDAO.findByTxId(txId)
|
||||
} yield {
|
||||
assert(txDbOpt.isDefined)
|
||||
assert(txDbOpt.get.blockHashOpt.contains(hash))
|
||||
assert(utxos.size == 1)
|
||||
assert(utxos.head.output.scriptPubKey == addr.scriptPubKey)
|
||||
assert(utxos.head.output.value == 1.bitcoin)
|
||||
assert(utxos.head.blockHash.contains(hash))
|
||||
assert(utxos.head.txid == txId)
|
||||
|
||||
assert(syncHeightOpt.contains(SyncHeightDescriptor(bestHash, height)))
|
||||
|
@ -2,9 +2,9 @@ package org.bitcoins.wallet
|
||||
|
||||
import org.bitcoins.core.api.wallet.WalletApi
|
||||
import org.bitcoins.core.currency._
|
||||
import org.bitcoins.testkit.wallet.BitcoinSWalletTest
|
||||
import org.bitcoins.testkitcore.Implicits._
|
||||
import org.bitcoins.testkitcore.gen.TransactionGenerators
|
||||
import org.bitcoins.testkit.wallet.BitcoinSWalletTest
|
||||
import org.scalatest.FutureOutcome
|
||||
import org.scalatest.compatible.Assertion
|
||||
|
||||
@ -36,7 +36,12 @@ class ProcessTransactionTest extends BitcoinSWalletTest {
|
||||
} yield {
|
||||
assert(oldConfirmed == newConfirmed)
|
||||
assert(oldUnconfirmed == newUnconfirmed)
|
||||
assert(oldUtxos == newUtxos)
|
||||
// make utxos comparable
|
||||
val comparableOldUtxos =
|
||||
oldUtxos.map(_.copyWithId(0)).sortBy(_.outPoint.hex)
|
||||
val comparableNewUtxos =
|
||||
newUtxos.map(_.copyWithId(0)).sortBy(_.outPoint.hex)
|
||||
assert(comparableOldUtxos == comparableNewUtxos)
|
||||
assert(oldTransactions == newTransactions)
|
||||
}
|
||||
|
||||
|
@ -184,10 +184,13 @@ class RescanHandlingTest extends BitcoinSWalletTest {
|
||||
newTxWallet <- newTxWalletF
|
||||
|
||||
account <- newTxWallet.getDefaultAccount()
|
||||
blocks <-
|
||||
txIds <-
|
||||
newTxWallet.spendingInfoDAO
|
||||
.findAllForAccount(account.hdAccount)
|
||||
.map(_.flatMap(_.blockHash).distinct)
|
||||
.map(_.map(_.txid))
|
||||
blocks <- newTxWallet.transactionDAO
|
||||
.findByTxIdBEs(txIds)
|
||||
.map(_.flatMap(_.blockHashOpt))
|
||||
|
||||
_ <- newTxWallet.clearAllUtxosAndAddresses()
|
||||
scriptPubKeys <-
|
||||
@ -265,9 +268,11 @@ class RescanHandlingTest extends BitcoinSWalletTest {
|
||||
val utxosF = wallet.listUtxos()
|
||||
val oldestHeightF = for {
|
||||
utxos <- utxosF
|
||||
blockhashes = utxos.map(_.blockHash)
|
||||
blockhashes <- wallet.transactionDAO
|
||||
.findByTxIdBEs(utxos.map(_.txid))
|
||||
.map(_.flatMap(_.blockHashOpt))
|
||||
heights <- FutureUtil.sequentially(blockhashes) { hash =>
|
||||
wallet.chainQueryApi.getBlockHeight(hash.get)
|
||||
wallet.chainQueryApi.getBlockHeight(hash)
|
||||
}
|
||||
} yield heights.min.get
|
||||
|
||||
|
@ -0,0 +1,57 @@
|
||||
package org.bitcoins.wallet
|
||||
|
||||
import org.bitcoins.core.protocol.script.EmptyScriptPubKey
|
||||
import org.bitcoins.core.wallet.utxo.TxoState._
|
||||
import org.bitcoins.testkit.wallet.BitcoinSWalletTest
|
||||
import org.bitcoins.testkit.wallet.WalletTestUtil._
|
||||
import org.scalatest.FutureOutcome
|
||||
|
||||
class UTXOHandlingTest extends BitcoinSWalletTest {
|
||||
|
||||
behavior of "UTXOHandling"
|
||||
|
||||
override type FixtureParam = Wallet
|
||||
|
||||
override def withFixture(test: OneArgAsyncTest): FutureOutcome = {
|
||||
withNewWallet(test, getBIP39PasswordOpt())
|
||||
}
|
||||
|
||||
it must "correctly update txo state based on confirmations" in { wallet =>
|
||||
val utxo = sampleSegwitUTXO(EmptyScriptPubKey)
|
||||
val requiredConfs = 6
|
||||
assert(wallet.walletConfig.requiredConfirmations == requiredConfs)
|
||||
|
||||
val immatureCoinbase = utxo.copyWithState(ImmatureCoinbase)
|
||||
val pendingConfReceived = utxo.copyWithState(PendingConfirmationsReceived)
|
||||
val pendingConfSpent = utxo.copyWithState(PendingConfirmationsSpent)
|
||||
val confReceived = utxo.copyWithState(ConfirmedReceived)
|
||||
val confSpent = utxo.copyWithState(ConfirmedSpent)
|
||||
val reserved = utxo.copyWithState(Reserved)
|
||||
val dne = utxo.copyWithState(DoesNotExist)
|
||||
|
||||
assert(wallet.updateTxoWithConfs(reserved, 1) == reserved)
|
||||
|
||||
assert(wallet.updateTxoWithConfs(immatureCoinbase, 10) == immatureCoinbase)
|
||||
assert(wallet.updateTxoWithConfs(immatureCoinbase, 101) == confReceived)
|
||||
|
||||
assert(
|
||||
wallet.updateTxoWithConfs(pendingConfReceived, 1) == pendingConfReceived)
|
||||
assert(
|
||||
wallet.updateTxoWithConfs(pendingConfReceived,
|
||||
requiredConfs) == confReceived)
|
||||
|
||||
assert(wallet.updateTxoWithConfs(pendingConfSpent, 1) == pendingConfSpent)
|
||||
assert(
|
||||
wallet.updateTxoWithConfs(pendingConfSpent, requiredConfs) == confSpent)
|
||||
|
||||
assert(wallet.updateTxoWithConfs(dne, 1) == dne)
|
||||
assert(wallet.updateTxoWithConfs(dne, requiredConfs) == dne)
|
||||
|
||||
assert(wallet.updateTxoWithConfs(confSpent, 1) == confSpent)
|
||||
assert(wallet.updateTxoWithConfs(confSpent, requiredConfs) == confSpent)
|
||||
|
||||
assert(wallet.updateTxoWithConfs(confReceived, 1) == confReceived)
|
||||
assert(
|
||||
wallet.updateTxoWithConfs(confReceived, requiredConfs) == confReceived)
|
||||
}
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
package org.bitcoins.wallet
|
||||
|
||||
import org.bitcoins.core.currency.Satoshis
|
||||
import org.bitcoins.core.protocol.BitcoinAddress
|
||||
import org.bitcoins.core.protocol.script.{EmptyScriptPubKey, P2PKHScriptPubKey}
|
||||
import org.bitcoins.core.protocol.transaction.TransactionOutput
|
||||
import org.bitcoins.core.number._
|
||||
import org.bitcoins.core.protocol.script._
|
||||
import org.bitcoins.core.protocol.transaction._
|
||||
import org.bitcoins.core.wallet.fee.SatoshisPerByte
|
||||
import org.bitcoins.core.wallet.utxo.TxoState
|
||||
import org.bitcoins.core.wallet.utxo.TxoState._
|
||||
@ -21,10 +21,6 @@ class UTXOLifeCycleTest extends BitcoinSWalletTest {
|
||||
|
||||
override type FixtureParam = WalletWithBitcoind
|
||||
|
||||
val testAddr: BitcoinAddress =
|
||||
BitcoinAddress
|
||||
.fromString("bcrt1qlhctylgvdsvaanv539rg7hyn0sjkdm23y70kgq")
|
||||
|
||||
override def withFixture(test: OneArgAsyncTest): FutureOutcome = {
|
||||
withFundedWalletAndBitcoind(test, getBIP39PasswordOpt())
|
||||
}
|
||||
@ -40,6 +36,7 @@ class UTXOLifeCycleTest extends BitcoinSWalletTest {
|
||||
newTransactions <- wallet.listTransactions()
|
||||
} yield {
|
||||
assert(updatedCoins.forall(_.state == TxoState.PendingConfirmationsSpent))
|
||||
assert(updatedCoins.forall(_.spendingTxIdOpt.contains(tx.txIdBE)))
|
||||
assert(!oldTransactions.map(_.transaction).contains(tx))
|
||||
assert(newTransactions.map(_.transaction).contains(tx))
|
||||
}
|
||||
@ -60,8 +57,11 @@ class UTXOLifeCycleTest extends BitcoinSWalletTest {
|
||||
|
||||
// Give tx a fake hash so it can appear as it's in a block
|
||||
hash <- bitcoind.getBestBlockHash
|
||||
_ <- wallet.spendingInfoDAO.upsertAllSpendingInfoDb(
|
||||
updatedCoins.map(_.copyWithBlockHash(hash)).toVector)
|
||||
_ <- wallet.processTransaction(tx, Some(hash))
|
||||
|
||||
pendingCoins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx)
|
||||
_ <- wallet.updateUtxoPendingStates()
|
||||
_ = assert(pendingCoins.forall(_.state == PendingConfirmationsSpent))
|
||||
|
||||
// Put confirmations on top of the tx's block
|
||||
_ <- bitcoind.getNewAddress.flatMap(
|
||||
@ -70,10 +70,128 @@ class UTXOLifeCycleTest extends BitcoinSWalletTest {
|
||||
// Need to call this to actually update the state, normally a node callback would do this
|
||||
_ <- wallet.updateUtxoPendingStates()
|
||||
confirmedCoins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx)
|
||||
} yield assert(confirmedCoins.forall(_.state == ConfirmedSpent))
|
||||
} yield {
|
||||
assert(confirmedCoins.forall(_.state == ConfirmedSpent))
|
||||
assert(confirmedCoins.forall(_.spendingTxIdOpt.contains(tx.txIdBE)))
|
||||
}
|
||||
}
|
||||
|
||||
it should "track a utxo state change to pending recieved" in { param =>
|
||||
it should "handle an RBF transaction on unconfirmed coins" in { param =>
|
||||
val WalletWithBitcoindRpc(wallet, _) = param
|
||||
|
||||
for {
|
||||
tx <- wallet.sendToAddress(testAddr,
|
||||
Satoshis(3000),
|
||||
Some(SatoshisPerByte.one))
|
||||
|
||||
coins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx)
|
||||
_ = assert(coins.forall(_.state == PendingConfirmationsSpent))
|
||||
_ = assert(coins.forall(_.spendingTxIdOpt.contains(tx.txIdBE)))
|
||||
|
||||
rbf <- wallet.bumpFeeRBF(tx.txIdBE, SatoshisPerByte.fromLong(3))
|
||||
_ <- wallet.processTransaction(rbf, None)
|
||||
rbfCoins <- wallet.spendingInfoDAO.findOutputsBeingSpent(rbf)
|
||||
} yield {
|
||||
assert(rbfCoins.forall(_.state == PendingConfirmationsSpent))
|
||||
assert(rbfCoins.forall(_.spendingTxIdOpt.contains(rbf.txIdBE)))
|
||||
}
|
||||
}
|
||||
|
||||
it should "handle attempting to spend an immature coinbase" in { param =>
|
||||
val WalletWithBitcoindRpc(wallet, _) = param
|
||||
|
||||
for {
|
||||
tx <- wallet.sendToAddress(testAddr, Satoshis(3000), None)
|
||||
|
||||
coins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx)
|
||||
|
||||
updatedCoins = coins.map(_.copyWithState(TxoState.ImmatureCoinbase))
|
||||
_ <- wallet.spendingInfoDAO.updateAllSpendingInfoDb(updatedCoins.toVector)
|
||||
|
||||
// Create tx to spend immature coinbase utxos
|
||||
newTx = {
|
||||
val inputs = coins.map { db =>
|
||||
TransactionInput(db.outPoint, EmptyScriptSignature, UInt32.zero)
|
||||
}
|
||||
BaseTransaction(Int32.zero, inputs, Vector.empty, UInt32.zero)
|
||||
}
|
||||
|
||||
res <- recoverToSucceededIf[RuntimeException](
|
||||
wallet.processTransaction(newTx, None))
|
||||
} yield res
|
||||
}
|
||||
|
||||
it should "handle processing a new spending tx for a spent utxo" in { param =>
|
||||
val WalletWithBitcoindRpc(wallet, bitcoind) = param
|
||||
|
||||
for {
|
||||
oldTransactions <- wallet.listTransactions()
|
||||
tx <- wallet.sendToAddress(testAddr, Satoshis(3000), None)
|
||||
|
||||
updatedCoins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx)
|
||||
newTransactions <- wallet.listTransactions()
|
||||
_ = assert(updatedCoins.forall(_.state == PendingConfirmationsSpent))
|
||||
_ = assert(!oldTransactions.map(_.transaction).contains(tx))
|
||||
_ = assert(newTransactions.map(_.transaction).contains(tx))
|
||||
|
||||
// Give tx a fake hash so it can appear as it's in a block
|
||||
hash <- bitcoind.getBestBlockHash
|
||||
_ <- wallet.processTransaction(tx, Some(hash))
|
||||
|
||||
pendingCoins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx)
|
||||
_ <- wallet.updateUtxoPendingStates()
|
||||
_ = assert(pendingCoins.forall(_.state == PendingConfirmationsSpent))
|
||||
|
||||
// Put confirmations on top of the tx's block
|
||||
_ <- bitcoind.getNewAddress.flatMap(
|
||||
bitcoind.generateToAddress(wallet.walletConfig.requiredConfirmations,
|
||||
_))
|
||||
// Need to call this to actually update the state, normally a node callback would do this
|
||||
_ <- wallet.updateUtxoPendingStates()
|
||||
confirmedCoins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx)
|
||||
|
||||
// Assert tx is confirmed
|
||||
_ = assert(confirmedCoins.forall(_.state == ConfirmedSpent))
|
||||
_ = assert(confirmedCoins.forall(_.spendingTxIdOpt.contains(tx.txIdBE)))
|
||||
|
||||
// Create tx to spend same utxos
|
||||
newTx = {
|
||||
val inputs = updatedCoins.map { db =>
|
||||
TransactionInput(db.outPoint, EmptyScriptSignature, UInt32.zero)
|
||||
}
|
||||
BaseTransaction(Int32.zero, inputs, Vector.empty, UInt32.zero)
|
||||
}
|
||||
|
||||
res <- recoverToSucceededIf[RuntimeException](
|
||||
wallet.processTransaction(newTx, None))
|
||||
} yield res
|
||||
}
|
||||
|
||||
it should "handle processing a new spending tx for a DNE utxo" in { param =>
|
||||
val WalletWithBitcoindRpc(wallet, _) = param
|
||||
|
||||
for {
|
||||
tx <- wallet.sendToAddress(testAddr, Satoshis(3000), None)
|
||||
|
||||
coins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx)
|
||||
|
||||
dneCoins = coins.map(_.copyWithState(TxoState.DoesNotExist))
|
||||
_ <- wallet.spendingInfoDAO.updateAllSpendingInfoDb(dneCoins.toVector)
|
||||
|
||||
// Create tx to spend dne utxos
|
||||
newTx = {
|
||||
val inputs = coins.map { db =>
|
||||
TransactionInput(db.outPoint, EmptyScriptSignature, UInt32.zero)
|
||||
}
|
||||
BaseTransaction(Int32.zero, inputs, Vector.empty, UInt32.zero)
|
||||
}
|
||||
|
||||
res <- recoverToSucceededIf[RuntimeException](
|
||||
wallet.processTransaction(newTx, None))
|
||||
} yield res
|
||||
}
|
||||
|
||||
it should "track a utxo state change to pending received" in { param =>
|
||||
val WalletWithBitcoindRpc(wallet, bitcoind) = param
|
||||
|
||||
for {
|
||||
|
@ -2,15 +2,15 @@ package org.bitcoins.wallet
|
||||
|
||||
import org.bitcoins.core.currency._
|
||||
import org.bitcoins.core.hd.HDChainType
|
||||
import org.bitcoins.core.number._
|
||||
import org.bitcoins.core.protocol.script.EmptyScriptSignature
|
||||
import org.bitcoins.core.protocol.transaction._
|
||||
import org.bitcoins.core.psbt.PSBT
|
||||
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
|
||||
import org.bitcoins.core.wallet.utxo.TxoState
|
||||
import org.bitcoins.rpc.BitcoindException.InvalidAddressOrKey
|
||||
import org.bitcoins.testkit.wallet.BitcoinSWalletTest.RandomFeeProvider
|
||||
import org.bitcoins.testkit.wallet.{
|
||||
BitcoinSWalletTest,
|
||||
WalletTestUtil,
|
||||
WalletWithBitcoind,
|
||||
WalletWithBitcoindRpc
|
||||
}
|
||||
import org.bitcoins.testkit.wallet._
|
||||
import org.scalatest.FutureOutcome
|
||||
|
||||
class WalletIntegrationTest extends BitcoinSWalletTest {
|
||||
@ -192,7 +192,10 @@ class WalletIntegrationTest extends BitcoinSWalletTest {
|
||||
_ <- bitcoind.getNewAddress.flatMap(bitcoind.generateToAddress(6, _))
|
||||
|
||||
replacementInfo <- bitcoind.getRawTransaction(replacementTx.txIdBE)
|
||||
|
||||
utxos <- wallet.spendingInfoDAO.findOutputsBeingSpent(replacementTx)
|
||||
} yield {
|
||||
assert(utxos.forall(_.spendingTxIdOpt.contains(replacementTx.txIdBE)))
|
||||
// Check correct one was confirmed
|
||||
assert(replacementInfo.blockhash.isDefined)
|
||||
}
|
||||
@ -271,4 +274,76 @@ class WalletIntegrationTest extends BitcoinSWalletTest {
|
||||
walletBal2 <- wallet.getBalance()
|
||||
} yield assert(walletBal1 > walletBal2)
|
||||
}
|
||||
|
||||
it should "correctly handle spending coinbase utxos" in {
|
||||
walletWithBitcoind =>
|
||||
val WalletWithBitcoindRpc(wallet, bitcoind) = walletWithBitcoind
|
||||
|
||||
val amountToSend = Bitcoins(49.99)
|
||||
|
||||
for {
|
||||
// Mine to wallet
|
||||
addr <- wallet.getNewAddress()
|
||||
hash <- bitcoind.generateToAddress(1, addr).map(_.head)
|
||||
block <- bitcoind.getBlockRaw(hash)
|
||||
|
||||
// Assert we mined to our address
|
||||
coinbaseTx = block.transactions.head
|
||||
_ = assert(
|
||||
coinbaseTx.outputs.exists(_.scriptPubKey == addr.scriptPubKey))
|
||||
|
||||
_ <- wallet.processBlock(block)
|
||||
|
||||
// Verify we funded the wallet
|
||||
allUtxos <- wallet.spendingInfoDAO.findAllSpendingInfos()
|
||||
_ = assert(allUtxos.size == 1)
|
||||
utxos <- wallet.listUtxos(TxoState.ImmatureCoinbase)
|
||||
_ = assert(utxos.size == 1)
|
||||
|
||||
bitcoindAddr <- bitcoind.getNewAddress
|
||||
|
||||
// Attempt to spend utxo
|
||||
_ <- recoverToSucceededIf[RuntimeException](
|
||||
wallet.sendToAddress(bitcoindAddr, valueToBitcoind, None))
|
||||
|
||||
spendingTx = {
|
||||
val inputs = utxos.map { db =>
|
||||
TransactionInput(db.outPoint, EmptyScriptSignature, UInt32.zero)
|
||||
}
|
||||
val outputs =
|
||||
Vector(TransactionOutput(amountToSend, bitcoindAddr.scriptPubKey))
|
||||
BaseTransaction(Int32.two, inputs, outputs, UInt32.zero)
|
||||
}
|
||||
|
||||
_ <- recoverToSucceededIf[RuntimeException](
|
||||
wallet.processTransaction(spendingTx, None))
|
||||
|
||||
// Make coinbase mature
|
||||
_ <- bitcoind.generateToAddress(101, bitcoindAddr)
|
||||
_ <- wallet.updateUtxoPendingStates()
|
||||
|
||||
// Create valid spending tx
|
||||
psbt = PSBT.fromUnsignedTx(spendingTx)
|
||||
signedPSBT <- wallet.signPSBT(psbt)
|
||||
signedTx = signedPSBT.finalizePSBT
|
||||
.flatMap(_.extractTransactionAndValidate)
|
||||
.get
|
||||
|
||||
// Process tx, validate correctly moved to
|
||||
_ <- wallet.processTransaction(signedTx, None)
|
||||
newCoinbaseUtxos <- wallet.listUtxos(TxoState.ImmatureCoinbase)
|
||||
_ = assert(newCoinbaseUtxos.isEmpty)
|
||||
spentUtxos <- wallet.listUtxos(TxoState.PendingConfirmationsSpent)
|
||||
_ = assert(spentUtxos.size == 1)
|
||||
|
||||
// Assert spending tx valid to bitcoind
|
||||
oldBalance <- bitcoind.getBalance
|
||||
_ = assert(oldBalance == Satoshis(510000000000L))
|
||||
|
||||
_ <- bitcoind.sendRawTransaction(signedTx)
|
||||
_ <- bitcoind.generateToAddress(1, bitcoindAddr)
|
||||
|
||||
newBalance <- bitcoind.getBalance
|
||||
} yield assert(newBalance == oldBalance + amountToSend + Bitcoins(50))
|
||||
}
|
||||
}
|
||||
|
@ -200,7 +200,7 @@ class WalletUnitTest extends BitcoinSWalletTest {
|
||||
|
||||
spk = MultiSignatureScriptPubKey(2, Vector(dummyKey, walletKey))
|
||||
dummyPrevTx = dummyTx(spk = spk)
|
||||
prevTxDb = TransactionDbHelper.fromTransaction(dummyPrevTx)
|
||||
prevTxDb = TransactionDbHelper.fromTransaction(dummyPrevTx, None)
|
||||
_ <- wallet.transactionDAO.create(prevTxDb)
|
||||
|
||||
psbt = dummyPSBT(prevTxId = dummyPrevTx.txId)
|
||||
|
@ -1,18 +1,16 @@
|
||||
package org.bitcoins.wallet.models
|
||||
|
||||
import org.bitcoins.core.api.wallet.db.{
|
||||
IncomingTransactionDb,
|
||||
TransactionDb,
|
||||
TransactionDbHelper
|
||||
}
|
||||
import org.bitcoins.core.api.wallet.db._
|
||||
import org.bitcoins.core.currency.Satoshis
|
||||
import org.bitcoins.crypto.DoubleSha256DigestBE
|
||||
import org.bitcoins.testkit.fixtures.WalletDAOFixture
|
||||
import org.bitcoins.testkit.wallet.WalletTestUtil
|
||||
|
||||
class IncomingTransactionDAOTest extends WalletDAOFixture {
|
||||
|
||||
val txDb: TransactionDb =
|
||||
TransactionDbHelper.fromTransaction(WalletTestUtil.sampleTransaction)
|
||||
TransactionDbHelper.fromTransaction(WalletTestUtil.sampleTransaction,
|
||||
Some(DoubleSha256DigestBE.empty))
|
||||
|
||||
val incoming: IncomingTransactionDb =
|
||||
IncomingTransactionDb(WalletTestUtil.sampleTransaction.txIdBE,
|
||||
|
@ -1,18 +1,16 @@
|
||||
package org.bitcoins.wallet.models
|
||||
|
||||
import org.bitcoins.core.api.wallet.db.{
|
||||
OutgoingTransactionDb,
|
||||
TransactionDb,
|
||||
TransactionDbHelper
|
||||
}
|
||||
import org.bitcoins.core.api.wallet.db._
|
||||
import org.bitcoins.core.currency.Satoshis
|
||||
import org.bitcoins.crypto.DoubleSha256DigestBE
|
||||
import org.bitcoins.testkit.fixtures.WalletDAOFixture
|
||||
import org.bitcoins.testkit.wallet.WalletTestUtil
|
||||
|
||||
class OutgoingTransactionDAOTest extends WalletDAOFixture {
|
||||
|
||||
val txDb: TransactionDb =
|
||||
TransactionDbHelper.fromTransaction(WalletTestUtil.sampleTransaction)
|
||||
TransactionDbHelper.fromTransaction(WalletTestUtil.sampleTransaction,
|
||||
Some(DoubleSha256DigestBE.empty))
|
||||
|
||||
val outgoing: OutgoingTransactionDb = OutgoingTransactionDb.fromTransaction(
|
||||
WalletTestUtil.sampleTransaction,
|
||||
|
@ -7,7 +7,7 @@ import org.bitcoins.testkit.wallet.WalletTestUtil
|
||||
class TransactionDAOTest extends WalletDAOFixture {
|
||||
|
||||
val txDb: TransactionDb =
|
||||
TransactionDbHelper.fromTransaction(WalletTestUtil.sampleTransaction)
|
||||
TransactionDbHelper.fromTransaction(WalletTestUtil.sampleTransaction, None)
|
||||
|
||||
it should "insert and read an transaction into the database" in { daos =>
|
||||
val txDAO = daos.transactionDAO
|
||||
|
@ -0,0 +1,16 @@
|
||||
ALTER TABLE "tx_table"
|
||||
ADD COLUMN "block_hash" TEXT;
|
||||
ALTER TABLE "tx_table"
|
||||
Drop COLUMN "id";
|
||||
|
||||
|
||||
UPDATE "tx_table"
|
||||
set "block_hash" = (
|
||||
select "txo_spending_info"."block_hash"
|
||||
from "txo_spending_info"
|
||||
where "txo_spending_info"."txid" = "tx_table"."txIdBE"
|
||||
);
|
||||
|
||||
-- Delete block_hash column, add spending_txid column
|
||||
ALTER TABLE "txo_spending_info" DROP COLUMN "block_hash";
|
||||
ALTER TABLE "txo_spending_info" ADD COLUMN "spending_txid" TEXT;
|
@ -0,0 +1,19 @@
|
||||
ALTER TABLE "tx_table"
|
||||
ADD COLUMN "block_hash" VARCHAR(254);
|
||||
|
||||
|
||||
UPDATE "tx_table"
|
||||
set "block_hash" = (
|
||||
select "txo_spending_info"."block_hash"
|
||||
from "txo_spending_info"
|
||||
where "txo_spending_info"."txid" = "tx_table"."txIdBE"
|
||||
);
|
||||
|
||||
-- Delete block_hash column, add spending_txid column
|
||||
CREATE TABLE IF NOT EXISTS "txo_spending_info_backup" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,"tx_outpoint" VARCHAR(254) NOT NULL, "script_pub_key_id" INT NOT NULL,"value" INTEGER NOT NULL,"hd_privkey_path" VARCHAR(254) NOT NULL,"redeem_script" VARCHAR(254),"script_witness" VARCHAR(254),"txid" VARCHAR(254) NOT NULL,"block_hash" VARCHAR(254), "txo_state" VARCHAR(254) NOT NULL);
|
||||
INSERT INTO "txo_spending_info_backup" SELECT * FROM "txo_spending_info";
|
||||
DROP TABLE "txo_spending_info";
|
||||
CREATE TABLE IF NOT EXISTS "txo_spending_info" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,"tx_outpoint" VARCHAR(254) NOT NULL, "script_pub_key_id" INT NOT NULL,"value" INTEGER NOT NULL,"hd_privkey_path" VARCHAR(254) NOT NULL,"redeem_script" VARCHAR(254),"script_witness" VARCHAR(254),"txid" VARCHAR(254) NOT NULL, "txo_state" VARCHAR(254) NOT NULL, spending_txid VARCHAR(254), constraint "fk_scriptPubKey" foreign key("script_pub_key_id") references "pub_key_scripts"("id"), constraint "fk_incoming_txId" foreign key("txid") references "wallet_incoming_txs"("txIdBE") on update NO ACTION on delete NO ACTION);
|
||||
INSERT INTO "txo_spending_info" ("id","tx_outpoint","script_pub_key_id","value","hd_privkey_path","redeem_script","script_witness","txid","txo_state") SELECT t."id",t."tx_outpoint",t."script_pub_key_id",t.value,t."hd_privkey_path",t."redeem_script",t."script_witness",t."txid",t."txo_state" FROM "txo_spending_info_backup" t;
|
||||
CREATE INDEX "txo_spending_info_spk_idx" ON "txo_spending_info"("script_pub_key_id");
|
||||
DROP TABLE "txo_spending_info_backup";
|
@ -107,7 +107,8 @@ abstract class Wallet
|
||||
protected def downloadMissingUtxos: Future[Unit] =
|
||||
for {
|
||||
utxos <- utxosWithMissingTx
|
||||
blockHashes = utxos.flatMap(_.blockHash.map(_.flip))
|
||||
txDbs <- transactionDAO.findByTxIdBEs(utxos.map(_.txid))
|
||||
blockHashes = txDbs.flatMap(_.blockHashOpt.map(_.flip))
|
||||
// Download the block the tx is from so we process the block and subsequent txs
|
||||
_ <-
|
||||
if (blockHashes.nonEmpty) {
|
||||
@ -512,12 +513,13 @@ abstract class Wallet
|
||||
newFeeRate: FeeUnit): Future[Transaction] = {
|
||||
for {
|
||||
txDbOpt <- transactionDAO.findByTxId(txId)
|
||||
tx <- txDbOpt match {
|
||||
case Some(db) => Future.successful(db.transaction)
|
||||
txDb <- txDbOpt match {
|
||||
case Some(db) => Future.successful(db)
|
||||
case None =>
|
||||
Future.failed(
|
||||
new RuntimeException(s"Unable to find transaction ${txId.hex}"))
|
||||
}
|
||||
tx = txDb.transaction
|
||||
|
||||
_ = require(TxUtil.isRBFEnabled(tx), "Transaction is not signaling RBF")
|
||||
|
||||
@ -529,11 +531,9 @@ abstract class Wallet
|
||||
_ = require(utxos.size == tx.inputs.size,
|
||||
"Can only bump fee for a transaction we own all the inputs")
|
||||
|
||||
oldOutputs <- spendingInfoDAO.findDbsForTx(txId)
|
||||
blockHashes = oldOutputs.flatMap(_.blockHash).distinct
|
||||
_ = require(
|
||||
blockHashes.isEmpty,
|
||||
s"Cannot replace a confirmed transaction, ${blockHashes.map(_.hex)}")
|
||||
txDb.blockHashOpt.isEmpty,
|
||||
s"Cannot replace a confirmed transaction, ${txDb.blockHashOpt.get.hex}")
|
||||
|
||||
spendingInfos <- FutureUtil.sequentially(utxos) { utxo =>
|
||||
transactionDAO
|
||||
@ -579,6 +579,7 @@ abstract class Wallet
|
||||
Random.shuffle(myAddrs.map(_.scriptPubKey)).head
|
||||
}
|
||||
|
||||
oldOutputs <- spendingInfoDAO.findDbsForTx(txId)
|
||||
// Mark old outputs as replaced
|
||||
_ <- spendingInfoDAO.updateAll(
|
||||
oldOutputs.map(_.copyWithState(TxoState.DoesNotExist)))
|
||||
@ -738,22 +739,21 @@ abstract class Wallet
|
||||
feeRate: FeeUnit): Future[Transaction] = {
|
||||
for {
|
||||
txDbOpt <- transactionDAO.findByTxId(txId)
|
||||
tx <- txDbOpt match {
|
||||
case Some(db) => Future.successful(db.transaction)
|
||||
txDb <- txDbOpt match {
|
||||
case Some(db) => Future.successful(db)
|
||||
case None =>
|
||||
Future.failed(
|
||||
new RuntimeException(s"Unable to find transaction ${txId.hex}"))
|
||||
}
|
||||
tx = txDb.transaction
|
||||
|
||||
spendingInfos <- spendingInfoDAO.findTx(tx)
|
||||
_ = require(spendingInfos.nonEmpty,
|
||||
s"Transaction ${txId.hex} must have an output we own")
|
||||
|
||||
oldOutputs <- spendingInfoDAO.findDbsForTx(txId)
|
||||
blockHashes = oldOutputs.flatMap(_.blockHash).distinct
|
||||
_ = require(
|
||||
blockHashes.isEmpty,
|
||||
s"No need to fee bump a confirmed transaction, ${blockHashes.map(_.hex)}")
|
||||
txDb.blockHashOpt.isEmpty,
|
||||
s"Cannot replace a confirmed transaction, ${txDb.blockHashOpt.get.hex}")
|
||||
|
||||
changeSpendingInfos = spendingInfos.flatMap { db =>
|
||||
if (db.isChange) {
|
||||
|
@ -2,7 +2,6 @@ package org.bitcoins.wallet.internal
|
||||
|
||||
import org.bitcoins.core.api.wallet.db.{AccountDb, SpendingInfoDb}
|
||||
import org.bitcoins.core.api.wallet.{CoinSelectionAlgo, CoinSelector}
|
||||
import org.bitcoins.core.consensus.Consensus
|
||||
import org.bitcoins.core.protocol.transaction._
|
||||
import org.bitcoins.core.util.FutureUtil
|
||||
import org.bitcoins.core.wallet.builder._
|
||||
@ -75,18 +74,8 @@ trait FundTransactionHandling extends WalletLogger { self: Wallet =>
|
||||
}
|
||||
|
||||
// Need to remove immature coinbase inputs
|
||||
coinbaseUtxos = utxoWithTxs.filter(_._2.isCoinbase)
|
||||
confFs = coinbaseUtxos.map(utxo =>
|
||||
chainQueryApi
|
||||
.getNumberOfConfirmations(utxo._1.blockHash.get)
|
||||
.map((utxo, _)))
|
||||
confs <- FutureUtil.collect(confFs)
|
||||
immatureCoinbases =
|
||||
confs
|
||||
.filter { case (_, confsOpt) =>
|
||||
confsOpt.isDefined && confsOpt.get < Consensus.coinbaseMaturity
|
||||
}
|
||||
.map(_._1)
|
||||
immatureCoinbases = utxoWithTxs.filter(
|
||||
_._1.state == TxoState.ImmatureCoinbase)
|
||||
} yield utxoWithTxs.filter(utxo =>
|
||||
!immatureCoinbases.exists(_._1 == utxo._1))
|
||||
|
||||
|
@ -1,14 +1,14 @@
|
||||
package org.bitcoins.wallet.internal
|
||||
|
||||
import org.bitcoins.core.api.wallet.{AddUtxoError, AddUtxoSuccess}
|
||||
import org.bitcoins.core.api.wallet.db._
|
||||
import org.bitcoins.core.api.wallet.{AddUtxoError, AddUtxoSuccess}
|
||||
import org.bitcoins.core.consensus.Consensus
|
||||
import org.bitcoins.core.currency.CurrencyUnit
|
||||
import org.bitcoins.core.number.UInt32
|
||||
import org.bitcoins.core.protocol.BitcoinAddress
|
||||
import org.bitcoins.core.protocol.blockchain.Block
|
||||
import org.bitcoins.core.protocol.transaction.{Transaction, TransactionOutput}
|
||||
import org.bitcoins.core.util.{FutureUtil, TimeUtil}
|
||||
import org.bitcoins.core.util.TimeUtil
|
||||
import org.bitcoins.core.wallet.fee.FeeUnit
|
||||
import org.bitcoins.core.wallet.utxo.{AddressTag, TxoState}
|
||||
import org.bitcoins.crypto.{DoubleSha256Digest, DoubleSha256DigestBE}
|
||||
@ -94,8 +94,10 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
|
||||
/////////////////////
|
||||
// Internal wallet API
|
||||
|
||||
protected def insertTransaction(tx: Transaction): Future[TransactionDb] = {
|
||||
val txDb = TransactionDbHelper.fromTransaction(tx)
|
||||
protected def insertTransaction(
|
||||
tx: Transaction,
|
||||
blockHashOpt: Option[DoubleSha256DigestBE]): Future[TransactionDb] = {
|
||||
val txDb = TransactionDbHelper.fromTransaction(tx, blockHashOpt)
|
||||
transactionDAO.upsert(txDb)
|
||||
}
|
||||
|
||||
@ -103,7 +105,8 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
|
||||
transaction: Transaction,
|
||||
feeRate: FeeUnit,
|
||||
inputAmount: CurrencyUnit,
|
||||
sentAmount: CurrencyUnit): Future[
|
||||
sentAmount: CurrencyUnit,
|
||||
blockHashOpt: Option[DoubleSha256DigestBE]): Future[
|
||||
(TransactionDb, OutgoingTransactionDb)] = {
|
||||
val outgoingDb =
|
||||
OutgoingTransactionDb.fromTransaction(transaction,
|
||||
@ -111,7 +114,7 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
|
||||
sentAmount,
|
||||
feeRate.calc(transaction))
|
||||
for {
|
||||
txDb <- insertTransaction(transaction)
|
||||
txDb <- insertTransaction(transaction, blockHashOpt)
|
||||
written <- outgoingTxDAO.upsert(outgoingDb)
|
||||
} yield (txDb, written)
|
||||
}
|
||||
@ -131,7 +134,11 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
|
||||
s"Processing TX from our wallet, transaction=${transaction.txIdBE} with blockHash=$blockHashOpt")
|
||||
for {
|
||||
(txDb, _) <-
|
||||
insertOutgoingTransaction(transaction, feeRate, inputAmount, sentAmount)
|
||||
insertOutgoingTransaction(transaction,
|
||||
feeRate,
|
||||
inputAmount,
|
||||
sentAmount,
|
||||
blockHashOpt)
|
||||
result <- processTransactionImpl(txDb.transaction, blockHashOpt, newTags)
|
||||
} yield {
|
||||
val txid = txDb.transaction.txIdBE
|
||||
@ -205,6 +212,11 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
|
||||
for {
|
||||
outputsBeingSpent <- spendingInfoDAO.findOutputsBeingSpent(transaction)
|
||||
|
||||
_ <-
|
||||
if (outputsBeingSpent.nonEmpty)
|
||||
insertTransaction(transaction, blockHashOpt)
|
||||
else Future.unit
|
||||
|
||||
// unreserved outputs now they are in a block
|
||||
outputsToUse = blockHashOpt match {
|
||||
case Some(_) =>
|
||||
@ -219,7 +231,7 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
|
||||
|
||||
processed <- Future
|
||||
.sequence {
|
||||
outputsToUse.map(markAsPendingSpent)
|
||||
outputsToUse.map(markAsSpent(_, transaction.txIdBE))
|
||||
}
|
||||
.map(_.toVector)
|
||||
} yield processed.flatten
|
||||
@ -239,11 +251,9 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
|
||||
logger.debug(
|
||||
s"Processing transaction=${transaction.txIdBE} with blockHash=$blockHashOpt")
|
||||
|
||||
val incomingF = processIncomingUtxos(transaction, blockHashOpt, newTags)
|
||||
val outgoingF = processOutgoingUtxos(transaction, blockHashOpt)
|
||||
for {
|
||||
incoming <- incomingF
|
||||
outgoing <- outgoingF
|
||||
incoming <- processIncomingUtxos(transaction, blockHashOpt, newTags)
|
||||
outgoing <- processOutgoingUtxos(transaction, blockHashOpt)
|
||||
_ <- walletCallbacks.executeOnTransactionProcessed(logger, transaction)
|
||||
} yield {
|
||||
ProcessTxResult(incoming, outgoing)
|
||||
@ -251,24 +261,38 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
|
||||
}
|
||||
|
||||
/** If the given UTXO is marked as unspent, updates
|
||||
* its spending status. Otherwise returns `None`.
|
||||
* its spending status. Otherwise returns an error.
|
||||
*/
|
||||
private def markAsPendingSpent(
|
||||
out: SpendingInfoDb): Future[Option[SpendingInfoDb]] = {
|
||||
private def markAsSpent(
|
||||
out: SpendingInfoDb,
|
||||
spendingTxId: DoubleSha256DigestBE): Future[Option[SpendingInfoDb]] = {
|
||||
out.state match {
|
||||
case TxoState.ConfirmedReceived | TxoState.PendingConfirmationsReceived |
|
||||
TxoState.ImmatureCoinbase =>
|
||||
case TxoState.ConfirmedReceived | TxoState.PendingConfirmationsReceived =>
|
||||
val updated =
|
||||
out.copyWithState(state = TxoState.PendingConfirmationsSpent)
|
||||
out
|
||||
.copyWithState(state = TxoState.PendingConfirmationsSpent)
|
||||
.copyWithSpendingTxId(spendingTxId)
|
||||
val updatedF =
|
||||
spendingInfoDAO.update(updated)
|
||||
updatedF.foreach(updated =>
|
||||
logger.debug(
|
||||
s"Marked utxo=${updated.toHumanReadableString} as state=${updated.state}"))
|
||||
updatedF.map(Some(_))
|
||||
case TxoState.Reserved | TxoState.ConfirmedSpent |
|
||||
TxoState.PendingConfirmationsSpent | TxoState.DoesNotExist =>
|
||||
FutureUtil.none
|
||||
case TxoState.Reserved | TxoState.PendingConfirmationsSpent =>
|
||||
val updated =
|
||||
out.copyWithSpendingTxId(spendingTxId)
|
||||
val updatedF =
|
||||
spendingInfoDAO.update(updated)
|
||||
updatedF.map(Some(_))
|
||||
case TxoState.ImmatureCoinbase =>
|
||||
Future.failed(new RuntimeException(
|
||||
s"Attempting to spend an ImmatureCoinbase ${out.outPoint.hex}, this should not be possible until it is confirmed."))
|
||||
case TxoState.ConfirmedSpent =>
|
||||
Future.failed(new RuntimeException(
|
||||
s"Attempted to mark an already spent utxo ${out.outPoint.hex} with a new spending tx ${spendingTxId.hex}"))
|
||||
case TxoState.DoesNotExist =>
|
||||
Future.failed(new RuntimeException(
|
||||
s"Attempted to process a transaction for a utxo that does not exist ${out.outPoint.hex} with a new spending tx ${spendingTxId.hex}"))
|
||||
}
|
||||
}
|
||||
|
||||
@ -279,17 +303,14 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
|
||||
private def processUtxo(
|
||||
transaction: Transaction,
|
||||
index: Int,
|
||||
state: TxoState,
|
||||
blockHash: Option[DoubleSha256DigestBE]): Future[SpendingInfoDb] = {
|
||||
addUtxo(transaction = transaction,
|
||||
vout = UInt32(index),
|
||||
state = state,
|
||||
blockHash = blockHash).flatMap {
|
||||
case AddUtxoSuccess(utxo) => Future.successful(utxo)
|
||||
case err: AddUtxoError =>
|
||||
logger.error(s"Could not add UTXO", err)
|
||||
Future.failed(err)
|
||||
}
|
||||
state: TxoState): Future[SpendingInfoDb] = {
|
||||
addUtxo(transaction = transaction, vout = UInt32(index), state = state)
|
||||
.flatMap {
|
||||
case AddUtxoSuccess(utxo) => Future.successful(utxo)
|
||||
case err: AddUtxoError =>
|
||||
logger.error(s"Could not add UTXO", err)
|
||||
Future.failed(err)
|
||||
}
|
||||
}
|
||||
|
||||
private case class OutputWithIndex(output: TransactionOutput, index: Int)
|
||||
@ -311,65 +332,39 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
|
||||
logger.error(errMsg)
|
||||
Future.failed(new RuntimeException(errMsg))
|
||||
} else {
|
||||
(foundTxo.blockHash, blockHashOpt) match {
|
||||
case (None, Some(blockHash)) =>
|
||||
blockHashOpt match {
|
||||
case Some(blockHash) =>
|
||||
logger.debug(
|
||||
s"Updating block_hash of txo=${transaction.txIdBE}, new block hash=$blockHash")
|
||||
// update block hash
|
||||
val txoWithHash = foundTxo.copyWithBlockHash(blockHash = blockHash)
|
||||
|
||||
// If the utxo was marked reserved we want to update it to spent now
|
||||
// since it has been included in a block
|
||||
val unreservedTxo = txoWithHash.state match {
|
||||
val unreservedTxo = foundTxo.state match {
|
||||
case TxoState.Reserved =>
|
||||
txoWithHash.copyWithState(TxoState.PendingConfirmationsSpent)
|
||||
foundTxo.copyWithState(TxoState.PendingConfirmationsSpent)
|
||||
case TxoState.PendingConfirmationsReceived |
|
||||
TxoState.ConfirmedReceived |
|
||||
TxoState.PendingConfirmationsSpent | TxoState.ConfirmedSpent |
|
||||
TxoState.DoesNotExist | TxoState.ImmatureCoinbase =>
|
||||
txoWithHash
|
||||
foundTxo
|
||||
}
|
||||
|
||||
val updateTxDbF = insertTransaction(transaction, blockHashOpt)
|
||||
|
||||
// Update Txo State
|
||||
updateUtxoConfirmedState(unreservedTxo).flatMap {
|
||||
case Some(txo) =>
|
||||
logger.debug(
|
||||
s"Updated block_hash of txo=${txo.txid.hex} new block hash=${blockHash.hex}")
|
||||
Future.successful(txo)
|
||||
case None =>
|
||||
// State was not updated so we need to update it so it's block hash is in the database
|
||||
spendingInfoDAO.update(unreservedTxo)
|
||||
}
|
||||
case (Some(oldBlockHash), Some(newBlockHash)) =>
|
||||
if (oldBlockHash == newBlockHash) {
|
||||
logger.debug(
|
||||
s"Skipping further processing of transaction=${transaction.txIdBE}, already processed.")
|
||||
|
||||
for {
|
||||
relevantOuts <- getRelevantOutputs(transaction)
|
||||
totalIncoming = relevantOuts.map(_.output.value).sum
|
||||
_ <- insertIncomingTransaction(transaction, totalIncoming)
|
||||
} yield foundTxo
|
||||
} else {
|
||||
val errMsg =
|
||||
Seq(
|
||||
s"Found TXO has block hash=${oldBlockHash}, tx we were given has block hash=${newBlockHash}.",
|
||||
"This is either a reorg or a double spent, which is not implemented yet"
|
||||
).mkString(" ")
|
||||
logger.error(errMsg)
|
||||
Future.failed(new RuntimeException(errMsg))
|
||||
}
|
||||
case (Some(blockHash), None) =>
|
||||
val msg =
|
||||
List(
|
||||
s"Incoming transaction=${transaction.txIdBE} already has block hash=$blockHash! assigned",
|
||||
s" I don't know how to handle this."
|
||||
).mkString(" ")
|
||||
logger.warn(msg)
|
||||
Future.failed(new RuntimeException(msg))
|
||||
case (None, None) =>
|
||||
updateTxDbF.flatMap(_ =>
|
||||
updateUtxoConfirmedState(unreservedTxo).flatMap {
|
||||
case Some(txo) =>
|
||||
logger.debug(
|
||||
s"Updated block_hash of txo=${txo.txid.hex} new block hash=${blockHash.hex}")
|
||||
Future.successful(txo)
|
||||
case None =>
|
||||
// State was not updated so we need to update it so it's block hash is in the database
|
||||
spendingInfoDAO.update(unreservedTxo)
|
||||
})
|
||||
case None =>
|
||||
logger.debug(
|
||||
s"Skipping further processing of transaction=${transaction.txIdBE}, already processed.")
|
||||
s"Skipping further processing of transaction=${transaction.txIdBE.hex}, already processed.")
|
||||
Future.successful(foundTxo)
|
||||
}
|
||||
}
|
||||
@ -403,8 +398,7 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
|
||||
processUtxo(
|
||||
transaction,
|
||||
out.index,
|
||||
state = state,
|
||||
blockHash = blockHashOpt
|
||||
state = state
|
||||
)
|
||||
}
|
||||
Future.sequence(outputsVec)
|
||||
@ -413,11 +407,12 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
|
||||
|
||||
private[wallet] def insertIncomingTransaction(
|
||||
transaction: Transaction,
|
||||
incomingAmount: CurrencyUnit): Future[
|
||||
incomingAmount: CurrencyUnit,
|
||||
blockHashOpt: Option[DoubleSha256DigestBE]): Future[
|
||||
(TransactionDb, IncomingTransactionDb)] = {
|
||||
val incomingDb = IncomingTransactionDb(transaction.txIdBE, incomingAmount)
|
||||
for {
|
||||
txDb <- insertTransaction(transaction)
|
||||
txDb <- insertTransaction(transaction, blockHashOpt)
|
||||
written <- incomingTxDAO.upsert(incomingDb)
|
||||
} yield (txDb, written)
|
||||
}
|
||||
@ -478,7 +473,7 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
|
||||
}
|
||||
|
||||
val txDbF: Future[(TransactionDb, IncomingTransactionDb)] =
|
||||
insertIncomingTransaction(transaction, totalIncoming)
|
||||
insertIncomingTransaction(transaction, totalIncoming, blockHashOpt)
|
||||
|
||||
val prevTagsDbF = for {
|
||||
(txDb, _) <- txDbF
|
||||
|
@ -22,12 +22,13 @@ import org.bitcoins.core.protocol.transaction.{
|
||||
TransactionOutput
|
||||
}
|
||||
import org.bitcoins.core.util.FutureUtil
|
||||
import org.bitcoins.core.wallet.utxo.TxoState._
|
||||
import org.bitcoins.core.wallet.utxo._
|
||||
import org.bitcoins.crypto.DoubleSha256DigestBE
|
||||
import org.bitcoins.wallet.{Wallet, WalletLogger}
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.util.{Failure, Success, Try}
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
/** Provides functionality related to handling UTXOs in our wallet.
|
||||
* The most notable examples of functionality here are enumerating
|
||||
@ -87,68 +88,107 @@ private[wallet] trait UtxoHandling extends WalletLogger {
|
||||
}
|
||||
}
|
||||
|
||||
protected def updateUtxoConfirmedState(
|
||||
private[wallet] def updateUtxoConfirmedState(
|
||||
txo: SpendingInfoDb): Future[Option[SpendingInfoDb]] = {
|
||||
updateUtxoConfirmedStates(Vector(txo)).map(_.headOption)
|
||||
}
|
||||
|
||||
protected def updateUtxoConfirmedStates(
|
||||
/** Returns a map of the SpendingInfoDbs with their relevant block.
|
||||
* If the block hash is None, then it is a mempool transaction.
|
||||
* The relevant block is determined by if the utxo has been spent or not.
|
||||
* If it has been spent it uses the block that included the spending transaction,
|
||||
* otherwise it uses the block that included the receiving transaction.
|
||||
*/
|
||||
private[wallet] def getDbsByRelevantBlock(
|
||||
spendingInfoDbs: Vector[SpendingInfoDb]): Future[
|
||||
Map[Option[DoubleSha256DigestBE], Vector[SpendingInfoDb]]] = {
|
||||
val txIds =
|
||||
spendingInfoDbs.map { db =>
|
||||
db.spendingTxIdOpt match {
|
||||
case Some(spendingTxId) =>
|
||||
spendingTxId
|
||||
case None =>
|
||||
db.txid
|
||||
}
|
||||
}
|
||||
|
||||
transactionDAO.findByTxIdBEs(txIds).map { txDbs =>
|
||||
val blockHashMap = txDbs.map(db => db.txIdBE -> db.blockHashOpt).toMap
|
||||
val blockHashAndDb = spendingInfoDbs.map { txo =>
|
||||
val txToUse = txo.state match {
|
||||
case _: ReceivedState | DoesNotExist | ImmatureCoinbase | Reserved =>
|
||||
txo.txid
|
||||
case PendingConfirmationsSpent | ConfirmedSpent =>
|
||||
txo.spendingTxIdOpt.get
|
||||
}
|
||||
(blockHashMap(txToUse), txo)
|
||||
}
|
||||
blockHashAndDb.groupBy(_._1).map { case (blockHashOpt, vec) =>
|
||||
blockHashOpt -> vec.map(_._2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Updates the SpendingInfoDb to the correct state based
|
||||
* on the number of confirmations it has received
|
||||
*/
|
||||
private[wallet] def updateTxoWithConfs(
|
||||
txo: SpendingInfoDb,
|
||||
confs: Int): SpendingInfoDb = {
|
||||
txo.state match {
|
||||
case TxoState.ImmatureCoinbase =>
|
||||
if (confs > Consensus.coinbaseMaturity) {
|
||||
if (confs >= walletConfig.requiredConfirmations)
|
||||
txo.copyWithState(TxoState.ConfirmedReceived)
|
||||
else
|
||||
txo.copyWithState(TxoState.PendingConfirmationsReceived)
|
||||
} else txo
|
||||
case TxoState.PendingConfirmationsReceived =>
|
||||
if (confs >= walletConfig.requiredConfirmations)
|
||||
txo.copyWithState(TxoState.ConfirmedReceived)
|
||||
else txo
|
||||
case TxoState.PendingConfirmationsSpent =>
|
||||
if (confs >= walletConfig.requiredConfirmations)
|
||||
txo.copyWithState(TxoState.ConfirmedSpent)
|
||||
else txo
|
||||
case TxoState.Reserved =>
|
||||
// We should keep the utxo as reserved so it is not used in
|
||||
// a future transaction that it should not be in
|
||||
txo
|
||||
case TxoState.DoesNotExist | TxoState.ConfirmedReceived |
|
||||
TxoState.ConfirmedSpent =>
|
||||
txo
|
||||
}
|
||||
}
|
||||
|
||||
/** Updates all the given SpendingInfoDbs to the correct state
|
||||
* based on how many confirmations they have received
|
||||
*/
|
||||
private[wallet] def updateUtxoConfirmedStates(
|
||||
spendingInfoDbs: Vector[SpendingInfoDb]): Future[
|
||||
Vector[SpendingInfoDb]] = {
|
||||
|
||||
val byBlock = spendingInfoDbs.groupBy(_.blockHash)
|
||||
|
||||
val toUpdateFs = byBlock.map {
|
||||
case (Some(blockHash), txos) =>
|
||||
chainQueryApi.getNumberOfConfirmations(blockHash).map {
|
||||
case None =>
|
||||
logger.warn(
|
||||
s"Given txos exist in block (${blockHash.hex}) that we do not have or that has been reorged! $txos")
|
||||
Vector.empty
|
||||
case Some(confs) =>
|
||||
txos.flatMap { txo =>
|
||||
txo.state match {
|
||||
case TxoState.ImmatureCoinbase =>
|
||||
if (confs > Consensus.coinbaseMaturity) {
|
||||
if (confs >= walletConfig.requiredConfirmations) {
|
||||
Some(txo.copyWithState(TxoState.ConfirmedReceived))
|
||||
} else {
|
||||
Some(
|
||||
txo.copyWithState(
|
||||
TxoState.PendingConfirmationsReceived))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
case TxoState.PendingConfirmationsReceived =>
|
||||
if (confs >= walletConfig.requiredConfirmations) {
|
||||
Some(txo.copyWithState(TxoState.ConfirmedReceived))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
case TxoState.PendingConfirmationsSpent =>
|
||||
if (confs >= walletConfig.requiredConfirmations) {
|
||||
Some(txo.copyWithState(TxoState.ConfirmedSpent))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
case TxoState.Reserved =>
|
||||
// We should keep the utxo as reserved so it is not used in
|
||||
// a future transaction that it should not be in
|
||||
None
|
||||
case TxoState.DoesNotExist | TxoState.ConfirmedReceived |
|
||||
TxoState.ConfirmedSpent =>
|
||||
None
|
||||
}
|
||||
val toUpdateF = getDbsByRelevantBlock(spendingInfoDbs).flatMap {
|
||||
txsByBlock =>
|
||||
val toUpdateFs = txsByBlock.map {
|
||||
case (Some(blockHash), txos) =>
|
||||
chainQueryApi.getNumberOfConfirmations(blockHash).map {
|
||||
case None =>
|
||||
logger.warn(
|
||||
s"Given txos exist in block (${blockHash.hex}) that we do not have or that has been reorged! $txos")
|
||||
Vector.empty
|
||||
case Some(confs) => txos.map(updateTxoWithConfs(_, confs))
|
||||
}
|
||||
case (None, txos) =>
|
||||
logger.debug(
|
||||
s"Currently have ${txos.size} transactions in the mempool")
|
||||
Future.successful(Vector.empty)
|
||||
}
|
||||
case (None, txos) =>
|
||||
logger.debug(s"Currently have ${txos.size} transactions in the mempool")
|
||||
Future.successful(Vector.empty)
|
||||
FutureUtil.collect(toUpdateFs)
|
||||
}
|
||||
|
||||
for {
|
||||
toUpdate <- FutureUtil.collect(toUpdateFs)
|
||||
toUpdate <- toUpdateF
|
||||
_ =
|
||||
if (toUpdate.nonEmpty)
|
||||
logger.info(s"${toUpdate.size} txos are now confirmed!")
|
||||
@ -177,8 +217,7 @@ private[wallet] trait UtxoHandling extends WalletLogger {
|
||||
state: TxoState,
|
||||
output: TransactionOutput,
|
||||
outPoint: TransactionOutPoint,
|
||||
addressDb: AddressDb,
|
||||
blockHash: Option[DoubleSha256DigestBE]): Future[SpendingInfoDb] = {
|
||||
addressDb: AddressDb): Future[SpendingInfoDb] = {
|
||||
|
||||
val utxo: SpendingInfoDb = addressDb match {
|
||||
case segwitAddr: SegWitAddressDb =>
|
||||
@ -189,7 +228,7 @@ private[wallet] trait UtxoHandling extends WalletLogger {
|
||||
output = output,
|
||||
privKeyPath = segwitAddr.path,
|
||||
scriptWitness = segwitAddr.witnessScript,
|
||||
blockHash = blockHash
|
||||
spendingTxIdOpt = None
|
||||
)
|
||||
case LegacyAddressDb(path, _, _, _, _) =>
|
||||
LegacySpendingInfo(state = state,
|
||||
@ -197,7 +236,7 @@ private[wallet] trait UtxoHandling extends WalletLogger {
|
||||
outPoint = outPoint,
|
||||
output = output,
|
||||
privKeyPath = path,
|
||||
blockHash = blockHash)
|
||||
spendingTxIdOpt = None)
|
||||
case nested: NestedSegWitAddressDb =>
|
||||
NestedSegwitV0SpendingInfo(
|
||||
outPoint = outPoint,
|
||||
@ -207,8 +246,8 @@ private[wallet] trait UtxoHandling extends WalletLogger {
|
||||
scriptWitness = P2WPKHWitnessV0(nested.ecPublicKey),
|
||||
txid = tx.txIdBE,
|
||||
state = state,
|
||||
id = None,
|
||||
blockHash = blockHash
|
||||
spendingTxIdOpt = None,
|
||||
id = None
|
||||
)
|
||||
}
|
||||
|
||||
@ -228,8 +267,7 @@ private[wallet] trait UtxoHandling extends WalletLogger {
|
||||
protected def addUtxo(
|
||||
transaction: Transaction,
|
||||
vout: UInt32,
|
||||
state: TxoState,
|
||||
blockHash: Option[DoubleSha256DigestBE]): Future[AddUtxoResult] = {
|
||||
state: TxoState): Future[AddUtxoResult] = {
|
||||
|
||||
logger.info(s"Adding UTXO to wallet: ${transaction.txId.hex}:${vout.toInt}")
|
||||
|
||||
@ -266,11 +304,10 @@ private[wallet] trait UtxoHandling extends WalletLogger {
|
||||
state = state,
|
||||
output = output,
|
||||
outPoint = outPoint,
|
||||
addressDb = addressDb,
|
||||
blockHash = blockHash)
|
||||
addressDb = addressDb)
|
||||
}
|
||||
.flatMap {
|
||||
case Right(utxoF) => utxoF.map(AddUtxoSuccess(_))
|
||||
case Right(utxoF) => utxoF.map(AddUtxoSuccess)
|
||||
case Left(e) => Future.successful(e)
|
||||
}
|
||||
}
|
||||
@ -300,35 +337,19 @@ private[wallet] trait UtxoHandling extends WalletLogger {
|
||||
require(unreserved.isEmpty, s"Some utxos are not reserved, got $unreserved")
|
||||
|
||||
// unmark all utxos are reserved
|
||||
val groupedUtxos = utxos
|
||||
val updatedUtxos = utxos
|
||||
.map(_.copyWithState(TxoState.PendingConfirmationsReceived))
|
||||
.groupBy(_.blockHash)
|
||||
|
||||
val mempoolUtxos = Try(groupedUtxos(None)).getOrElse(Vector.empty)
|
||||
|
||||
// get the ones in blocks
|
||||
val utxosInBlocks = groupedUtxos.flatMap {
|
||||
case (Some(_), utxos) =>
|
||||
utxos
|
||||
case (None, _) =>
|
||||
None
|
||||
}.toVector
|
||||
|
||||
for {
|
||||
// update utxos in the mempool
|
||||
updatedMempoolUtxos <-
|
||||
spendingInfoDAO.updateAllSpendingInfoDb(mempoolUtxos)
|
||||
|
||||
// update the confirmed utxos
|
||||
updatedConfirmed <- updateUtxoConfirmedStates(utxosInBlocks)
|
||||
updatedConfirmed <- updateUtxoConfirmedStates(updatedUtxos)
|
||||
|
||||
// update the utxos that are in blocks but not considered confirmed yet
|
||||
pendingConf = utxosInBlocks.filterNot(utxo =>
|
||||
pendingConf = updatedUtxos.filterNot(utxo =>
|
||||
updatedConfirmed.exists(_.outPoint == utxo.outPoint))
|
||||
updatedPendingConf <- spendingInfoDAO.updateAllSpendingInfoDb(pendingConf)
|
||||
updated <- spendingInfoDAO.updateAllSpendingInfoDb(
|
||||
pendingConf ++ updatedConfirmed)
|
||||
|
||||
// Return all utxos that were updated
|
||||
updated = updatedMempoolUtxos ++ updatedConfirmed ++ updatedPendingConf
|
||||
_ <- walletCallbacks.executeOnReservedUtxos(logger, updated)
|
||||
} yield updated
|
||||
}
|
||||
|
@ -1,7 +1,5 @@
|
||||
package org.bitcoins.wallet.models
|
||||
|
||||
import java.sql.SQLException
|
||||
|
||||
import org.bitcoins.core.api.wallet.db._
|
||||
import org.bitcoins.core.currency.CurrencyUnit
|
||||
import org.bitcoins.core.hd._
|
||||
@ -17,6 +15,7 @@ import org.bitcoins.crypto.DoubleSha256DigestBE
|
||||
import org.bitcoins.db.CRUDAutoInc
|
||||
import org.bitcoins.wallet.config._
|
||||
|
||||
import java.sql.SQLException
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
case class SpendingInfoDAO()(implicit
|
||||
@ -483,7 +482,8 @@ case class SpendingInfoDAO()(implicit
|
||||
|
||||
def scriptWitnessOpt: Rep[Option[ScriptWitness]] = column("script_witness")
|
||||
|
||||
def blockHash: Rep[Option[DoubleSha256DigestBE]] = column("block_hash")
|
||||
def spendingTxIdOpt: Rep[Option[DoubleSha256DigestBE]] = column(
|
||||
"spending_txid")
|
||||
|
||||
/** All UTXOs must have a SPK in the wallet that gets spent to */
|
||||
def fk_scriptPubKeyId: slick.lifted.ForeignKeyQuery[_, ScriptPubKeyDb] = {
|
||||
@ -511,7 +511,7 @@ case class SpendingInfoDAO()(implicit
|
||||
privKeyPath,
|
||||
redeemScriptOpt,
|
||||
scriptWitnessOpt,
|
||||
blockHash,
|
||||
spendingTxIdOpt,
|
||||
id.?).<>((UTXORecord.apply _).tupled, UTXORecord.unapply)
|
||||
}
|
||||
}
|
||||
|
@ -101,10 +101,22 @@ case class TransactionDAO()(implicit
|
||||
|
||||
override val table = TableQuery[TransactionTable]
|
||||
|
||||
def findAllUnconfirmed(): Future[Vector[TransactionDb]] = {
|
||||
val query = table.filter(_.blockHash === Rep.None[DoubleSha256DigestBE])
|
||||
|
||||
safeDatabase.runVec(query.result)
|
||||
}
|
||||
|
||||
def findAllConfirmed(): Future[Vector[TransactionDb]] = {
|
||||
val query = table.filterNot(_.blockHash === Rep.None[DoubleSha256DigestBE])
|
||||
|
||||
safeDatabase.runVec(query.result)
|
||||
}
|
||||
|
||||
class TransactionTable(tag: Tag)
|
||||
extends TxTable[TransactionDb](tag, schemaName, "tx_table") {
|
||||
|
||||
def txIdBE: Rep[DoubleSha256DigestBE] = column("txIdBE", O.Unique)
|
||||
def txIdBE: Rep[DoubleSha256DigestBE] = column("txIdBE", O.PrimaryKey)
|
||||
|
||||
def transaction: Rep[Transaction] = column("transaction")
|
||||
|
||||
@ -123,6 +135,8 @@ case class TransactionDAO()(implicit
|
||||
|
||||
def locktime: Rep[UInt32] = column("locktime")
|
||||
|
||||
def blockHash: Rep[Option[DoubleSha256DigestBE]] = column("block_hash")
|
||||
|
||||
def * : ProvenShape[TransactionDb] =
|
||||
(txIdBE,
|
||||
transaction,
|
||||
@ -132,7 +146,8 @@ case class TransactionDAO()(implicit
|
||||
totalOutput,
|
||||
numInputs,
|
||||
numOutputs,
|
||||
locktime).<>(TransactionDb.tupled, TransactionDb.unapply)
|
||||
locktime,
|
||||
blockHash).<>(TransactionDb.tupled, TransactionDb.unapply)
|
||||
|
||||
def primaryKey: PrimaryKey =
|
||||
primaryKey("pk_tx", sourceColumns = txIdBE)
|
||||
|
Loading…
Reference in New Issue
Block a user