diff --git a/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala b/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala index aa48343819..c80dd63ddf 100644 --- a/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala +++ b/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala @@ -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)) diff --git a/app/server/src/main/scala/org/bitcoins/server/BitcoindRpcBackendUtil.scala b/app/server/src/main/scala/org/bitcoins/server/BitcoindRpcBackendUtil.scala index f1cf04a196..4655b764b4 100644 --- a/app/server/src/main/scala/org/bitcoins/server/BitcoindRpcBackendUtil.scala +++ b/app/server/src/main/scala/org/bitcoins/server/BitcoindRpcBackendUtil.scala @@ -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 } diff --git a/core/src/main/scala/org/bitcoins/core/api/wallet/db/SpendingInfoDb.scala b/core/src/main/scala/org/bitcoins/core/api/wallet/db/SpendingInfoDb.scala index d7ebbde038..6596369ebf 100644 --- a/core/src/main/scala/org/bitcoins/core/api/wallet/db/SpendingInfoDb.scala +++ b/core/src/main/scala/org/bitcoins/core/api/wallet/db/SpendingInfoDb.scala @@ -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 diff --git a/core/src/main/scala/org/bitcoins/core/api/wallet/db/TransactionDb.scala b/core/src/main/scala/org/bitcoins/core/api/wallet/db/TransactionDb.scala index fb556f7070..f266ae0610 100644 --- a/core/src/main/scala/org/bitcoins/core/api/wallet/db/TransactionDb.scala +++ b/core/src/main/scala/org/bitcoins/core/api/wallet/db/TransactionDb.scala @@ -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) } } diff --git a/core/src/main/scala/org/bitcoins/core/api/wallet/db/UTXORecord.scala b/core/src/main/scala/org/bitcoins/core/api/wallet/db/UTXORecord.scala index db235a7313..c6894f36c3 100644 --- a/core/src/main/scala/org/bitcoins/core/api/wallet/db/UTXORecord.scala +++ b/core/src/main/scala/org/bitcoins/core/api/wallet/db/UTXORecord.scala @@ -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 ) } diff --git a/core/src/main/scala/org/bitcoins/core/wallet/utxo/TxoState.scala b/core/src/main/scala/org/bitcoins/core/wallet/utxo/TxoState.scala index e787e0bf52..dbb15c30f0 100644 --- a/core/src/main/scala/org/bitcoins/core/wallet/utxo/TxoState.scala +++ b/core/src/main/scala/org/bitcoins/core/wallet/utxo/TxoState.scala @@ -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] = diff --git a/db-commons-test/src/test/scala/org/bitcoins/db/DbManagementTest.scala b/db-commons-test/src/test/scala/org/bitcoins/db/DbManagementTest.scala index 48afd7ace1..6f2f3d0ae4 100644 --- a/db-commons-test/src/test/scala/org/bitcoins/db/DbManagementTest.scala +++ b/db-commons-test/src/test/scala/org/bitcoins/db/DbManagementTest.scala @@ -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() diff --git a/testkit/src/main/scala/org/bitcoins/testkit/wallet/BitcoinSWalletTest.scala b/testkit/src/main/scala/org/bitcoins/testkit/wallet/BitcoinSWalletTest.scala index 781cb5974d..d97e9014b1 100644 --- a/testkit/src/main/scala/org/bitcoins/testkit/wallet/BitcoinSWalletTest.scala +++ b/testkit/src/main/scala/org/bitcoins/testkit/wallet/BitcoinSWalletTest.scala @@ -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") diff --git a/testkit/src/main/scala/org/bitcoins/testkit/wallet/WalletTestUtil.scala b/testkit/src/main/scala/org/bitcoins/testkit/wallet/WalletTestUtil.scala index ad5aa5d1e1..aa812405b0 100644 --- a/testkit/src/main/scala/org/bitcoins/testkit/wallet/WalletTestUtil.scala +++ b/testkit/src/main/scala/org/bitcoins/testkit/wallet/WalletTestUtil.scala @@ -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 { diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/CoinSelectorTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/CoinSelectorTest.scala index 9931f7ea49..33aaf06cfc 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/CoinSelectorTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/CoinSelectorTest.scala @@ -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)) diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/ProcessBlockTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/ProcessBlockTest.scala index b1d7393a44..1bdc1dab23 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/ProcessBlockTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/ProcessBlockTest.scala @@ -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))) diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/ProcessTransactionTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/ProcessTransactionTest.scala index 1543515cdb..41365fbd92 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/ProcessTransactionTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/ProcessTransactionTest.scala @@ -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) } diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/RescanHandlingTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/RescanHandlingTest.scala index 68644e734d..c3b9d9083e 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/RescanHandlingTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/RescanHandlingTest.scala @@ -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 diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/UTXOHandlingTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/UTXOHandlingTest.scala new file mode 100644 index 0000000000..95e1f62603 --- /dev/null +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/UTXOHandlingTest.scala @@ -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) + } +} diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/UTXOLifeCycleTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/UTXOLifeCycleTest.scala index dfda304404..a87ed91060 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/UTXOLifeCycleTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/UTXOLifeCycleTest.scala @@ -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 { diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/WalletIntegrationTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/WalletIntegrationTest.scala index 0a942ed356..da47e3c090 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/WalletIntegrationTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/WalletIntegrationTest.scala @@ -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)) + } } diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/WalletUnitTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/WalletUnitTest.scala index ec22a716c9..87f66a7b31 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/WalletUnitTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/WalletUnitTest.scala @@ -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) diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/models/IncomingTransactionDAOTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/models/IncomingTransactionDAOTest.scala index 4ccf23ab7a..573e120d8e 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/models/IncomingTransactionDAOTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/models/IncomingTransactionDAOTest.scala @@ -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, diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/models/OutgoingTransactionDAOTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/models/OutgoingTransactionDAOTest.scala index 3b59db1c1b..cca146ab0b 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/models/OutgoingTransactionDAOTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/models/OutgoingTransactionDAOTest.scala @@ -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, diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/models/TransactionDAOTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/models/TransactionDAOTest.scala index f4527b0596..904e84e1a7 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/models/TransactionDAOTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/models/TransactionDAOTest.scala @@ -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 diff --git a/wallet/src/main/resources/postgresql/wallet/migration/V11__blockhash_to_transaction_table.sql b/wallet/src/main/resources/postgresql/wallet/migration/V11__blockhash_to_transaction_table.sql new file mode 100644 index 0000000000..d43864c396 --- /dev/null +++ b/wallet/src/main/resources/postgresql/wallet/migration/V11__blockhash_to_transaction_table.sql @@ -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; diff --git a/wallet/src/main/resources/sqlite/wallet/migration/V10__blockhash_to_transaction_table.sql b/wallet/src/main/resources/sqlite/wallet/migration/V10__blockhash_to_transaction_table.sql new file mode 100644 index 0000000000..a44bc759c1 --- /dev/null +++ b/wallet/src/main/resources/sqlite/wallet/migration/V10__blockhash_to_transaction_table.sql @@ -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"; diff --git a/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala b/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala index 06444f839c..8d102673bd 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala @@ -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) { diff --git a/wallet/src/main/scala/org/bitcoins/wallet/internal/FundTransactionHandling.scala b/wallet/src/main/scala/org/bitcoins/wallet/internal/FundTransactionHandling.scala index ffdeed62dd..d7b3622655 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/internal/FundTransactionHandling.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/internal/FundTransactionHandling.scala @@ -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)) diff --git a/wallet/src/main/scala/org/bitcoins/wallet/internal/TransactionProcessing.scala b/wallet/src/main/scala/org/bitcoins/wallet/internal/TransactionProcessing.scala index 6c14b94f70..44b4cd9eae 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/internal/TransactionProcessing.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/internal/TransactionProcessing.scala @@ -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 diff --git a/wallet/src/main/scala/org/bitcoins/wallet/internal/UtxoHandling.scala b/wallet/src/main/scala/org/bitcoins/wallet/internal/UtxoHandling.scala index 55105c4a95..8b345238eb 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/internal/UtxoHandling.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/internal/UtxoHandling.scala @@ -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 } diff --git a/wallet/src/main/scala/org/bitcoins/wallet/models/SpendingInfoDAO.scala b/wallet/src/main/scala/org/bitcoins/wallet/models/SpendingInfoDAO.scala index 3293b6a8ea..a0f8fea875 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/models/SpendingInfoDAO.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/models/SpendingInfoDAO.scala @@ -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) } } diff --git a/wallet/src/main/scala/org/bitcoins/wallet/models/TransactionDAO.scala b/wallet/src/main/scala/org/bitcoins/wallet/models/TransactionDAO.scala index d3788a37c0..c8214be63c 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/models/TransactionDAO.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/models/TransactionDAO.scala @@ -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)