Wallet Transaction Tracking (#1197)

* Incoming and Outgoing Transaction Tables

* Remove script sigs for witness txs

* Create parent tx_table for incoming and outgoing txs

* Response to review

* Use isCloseEnough

* Fix test

* Fix rebase error

* Test that tx is tracking all sats correctly
This commit is contained in:
Ben Carman 2020-04-02 06:55:09 -05:00 committed by GitHub
parent 3b3d2414f7
commit 29eb6c2e05
23 changed files with 649 additions and 54 deletions

View file

@ -7,7 +7,6 @@ import akka.http.scaladsl.server.ValidationRejection
import akka.http.scaladsl.testkit.ScalatestRouteTest import akka.http.scaladsl.testkit.ScalatestRouteTest
import org.bitcoins.chain.api.ChainApi import org.bitcoins.chain.api.ChainApi
import org.bitcoins.core.Core import org.bitcoins.core.Core
import org.bitcoins.core.api.CoreApi
import org.bitcoins.core.crypto.DoubleSha256DigestBE import org.bitcoins.core.crypto.DoubleSha256DigestBE
import org.bitcoins.core.currency.{Bitcoins, CurrencyUnit} import org.bitcoins.core.currency.{Bitcoins, CurrencyUnit}
import org.bitcoins.core.protocol.BitcoinAddress import org.bitcoins.core.protocol.BitcoinAddress

View file

@ -1,6 +1,7 @@
package org.bitcoins.core package org.bitcoins.core
import scala.math.Ordering import scala.math.Ordering
import scala.util.{Failure, Success, Try}
// We extend AnyVal to avoid runtime allocation of new // We extend AnyVal to avoid runtime allocation of new
// objects. See the Scala documentation on value classes // objects. See the Scala documentation on value classes
@ -42,4 +43,41 @@ package object currency {
new Ordering[CurrencyUnit] { new Ordering[CurrencyUnit] {
override def compare(x: CurrencyUnit, y: CurrencyUnit): Int = x.compare(y) override def compare(x: CurrencyUnit, y: CurrencyUnit): Int = x.compare(y)
} }
implicit val currencyUnitNumeric: Numeric[CurrencyUnit] =
new Numeric[CurrencyUnit] {
override def plus(x: CurrencyUnit, y: CurrencyUnit): CurrencyUnit = x + y
override def minus(x: CurrencyUnit, y: CurrencyUnit): CurrencyUnit = x - y
override def times(x: CurrencyUnit, y: CurrencyUnit): CurrencyUnit = x * y
override def negate(x: CurrencyUnit): CurrencyUnit = -x
override def fromInt(x: Int): CurrencyUnit = Satoshis(x.toLong)
override def toInt(x: CurrencyUnit): Int = x.satoshis.toLong.toInt
override def toLong(x: CurrencyUnit): Long = x.satoshis.toLong
override def toFloat(x: CurrencyUnit): Float = x.satoshis.toBigInt.toFloat
override def toDouble(x: CurrencyUnit): Double =
x.satoshis.toBigInt.toDouble
override def compare(x: CurrencyUnit, y: CurrencyUnit): Int =
x.satoshis compare y.satoshis
// Cannot use the override modifier because this method was added in scala version 2.13
def parseString(str: String): Option[CurrencyUnit] = {
if (str.isEmpty) {
None
} else {
Try(str.toLong) match {
case Success(num) => Some(Satoshis(num))
case Failure(_) => None
}
}
}
}
} }

View file

@ -27,6 +27,10 @@ case class SatoshisPerByte(currencyUnit: CurrencyUnit) extends BitcoinFeeUnit {
} }
} }
object SatoshisPerByte {
def fromLong(sats: Long): SatoshisPerByte = SatoshisPerByte(Satoshis(sats))
}
case class SatoshisPerKiloByte(currencyUnit: CurrencyUnit) case class SatoshisPerKiloByte(currencyUnit: CurrencyUnit)
extends BitcoinFeeUnit { extends BitcoinFeeUnit {
@ -55,6 +59,7 @@ case class SatoshisPerVirtualByte(currencyUnit: CurrencyUnit)
extends BitcoinFeeUnit extends BitcoinFeeUnit
object SatoshisPerVirtualByte { object SatoshisPerVirtualByte {
val zero: SatoshisPerVirtualByte = SatoshisPerVirtualByte(CurrencyUnits.zero) val zero: SatoshisPerVirtualByte = SatoshisPerVirtualByte(CurrencyUnits.zero)
val one: SatoshisPerVirtualByte = SatoshisPerVirtualByte(Satoshis.one) val one: SatoshisPerVirtualByte = SatoshisPerVirtualByte(Satoshis.one)
} }

View file

@ -27,7 +27,7 @@ class DbManagementTest extends BitcoinSUnitTest {
val walletAppConfig = WalletAppConfig(BitcoinSTestAppConfig.tmpDir(), val walletAppConfig = WalletAppConfig(BitcoinSTestAppConfig.tmpDir(),
dbConfig(ProjectType.Wallet)) dbConfig(ProjectType.Wallet))
val result = WalletDbManagement.migrate(walletAppConfig) val result = WalletDbManagement.migrate(walletAppConfig)
assert(result == 2) assert(result == 4)
} }
it must "run migrations for node db" in { it must "run migrations for node db" in {

View file

@ -14,6 +14,7 @@ import org.bitcoins.core.protocol.transaction.{
} }
import org.bitcoins.core.script.ScriptType import org.bitcoins.core.script.ScriptType
import org.bitcoins.core.serializers.script.RawScriptWitnessParser import org.bitcoins.core.serializers.script.RawScriptWitnessParser
import org.bitcoins.core.wallet.fee.SatoshisPerByte
import org.bitcoins.core.wallet.utxo.TxoState import org.bitcoins.core.wallet.utxo.TxoState
import scodec.bits.ByteVector import scodec.bits.ByteVector
import slick.jdbc.GetResult import slick.jdbc.GetResult
@ -166,6 +167,11 @@ abstract class DbCommonsColumnMappers {
MappedColumnType MappedColumnType
.base[TxoState, String](_.toString, TxoState.fromString(_).get) .base[TxoState, String](_.toString, TxoState.fromString(_).get)
} }
implicit val satoshisPerByteMapper: BaseColumnType[SatoshisPerByte] = {
MappedColumnType
.base[SatoshisPerByte, Long](_.toLong, SatoshisPerByte.fromLong)
}
} }
object DbCommonsColumnMappers extends DbCommonsColumnMappers object DbCommonsColumnMappers extends DbCommonsColumnMappers

View file

@ -10,7 +10,10 @@ import org.bitcoins.wallet.db.WalletDbManagement
case class WalletDAOs( case class WalletDAOs(
accountDAO: AccountDAO, accountDAO: AccountDAO,
addressDAO: AddressDAO, addressDAO: AddressDAO,
utxoDAO: SpendingInfoDAO) utxoDAO: SpendingInfoDAO,
transactionDAO: TransactionDAO,
incomingTxDAO: IncomingTransactionDAO,
outgoingTxDAO: OutgoingTransactionDAO)
trait WalletDAOFixture extends FixtureAsyncFlatSpec with BitcoinSWalletTest { trait WalletDAOFixture extends FixtureAsyncFlatSpec with BitcoinSWalletTest {
@ -18,7 +21,10 @@ trait WalletDAOFixture extends FixtureAsyncFlatSpec with BitcoinSWalletTest {
val account = AccountDAO() val account = AccountDAO()
val address = AddressDAO() val address = AddressDAO()
val utxo = SpendingInfoDAO() val utxo = SpendingInfoDAO()
WalletDAOs(account, address, utxo) val tx = TransactionDAO()
val incomingTx = IncomingTransactionDAO()
val outgoingTx = OutgoingTransactionDAO()
WalletDAOs(account, address, utxo, tx, incomingTx, outgoingTx)
} }
final override type FixtureParam = WalletDAOs final override type FixtureParam = WalletDAOs

View file

@ -4,12 +4,15 @@ import org.bitcoins.core.config.RegTest
import org.bitcoins.core.crypto._ import org.bitcoins.core.crypto._
import org.bitcoins.core.currency._ import org.bitcoins.core.currency._
import org.bitcoins.core.hd._ import org.bitcoins.core.hd._
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.blockchain.{ import org.bitcoins.core.protocol.blockchain.{
ChainParams, ChainParams,
RegTestNetChainParams RegTestNetChainParams
} }
import org.bitcoins.core.protocol.script._ import org.bitcoins.core.protocol.script._
import org.bitcoins.core.protocol.transaction.{ import org.bitcoins.core.protocol.transaction.{
EmptyTransaction,
Transaction,
TransactionOutPoint, TransactionOutPoint,
TransactionOutput TransactionOutput
} }
@ -31,6 +34,9 @@ object WalletTestUtil {
val hdCoinType: HDCoinType = HDCoinType.Testnet val hdCoinType: HDCoinType = HDCoinType.Testnet
lazy val sampleTransaction: Transaction = Transaction(
"020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000")
/** /**
* Useful if you want wallet test runs * Useful if you want wallet test runs
* To use the same key values each time * To use the same key values each time
@ -168,6 +174,26 @@ object WalletTestUtil {
scriptPubKey = wspk) scriptPubKey = wspk)
} }
def insertDummyIncomingTransaction(daos: WalletDAOs, utxo: SpendingInfoDb)(
implicit ec: ExecutionContext): Future[IncomingTransactionDb] = {
val txDb = TransactionDb(
txIdBE = utxo.txid,
transaction = EmptyTransaction,
unsignedTxIdBE = utxo.txid,
unsignedTx = EmptyTransaction,
wTxIdBEOpt = None,
totalOutput = Satoshis.zero,
numInputs = 1,
numOutputs = 1,
lockTime = UInt32.zero
)
val incomingDb = IncomingTransactionDb(utxo.txid, utxo.output.value)
for {
_ <- daos.transactionDAO.upsert(txDb)
written <- daos.incomingTxDAO.upsert(incomingDb)
} yield written
}
/** Given an account returns a sample address */ /** Given an account returns a sample address */
def getNestedSegwitAddressDb(account: AccountDb): AddressDb = { def getNestedSegwitAddressDb(account: AccountDb): AddressDb = {
val path = NestedSegWitHDPath(WalletTestUtil.hdCoinType, val path = NestedSegWitHDPath(WalletTestUtil.hdCoinType,
@ -195,8 +221,10 @@ object WalletTestUtil {
for { for {
account <- daos.accountDAO.create(WalletTestUtil.firstAccountDb) account <- daos.accountDAO.create(WalletTestUtil.firstAccountDb)
addr <- daos.addressDAO.create(getAddressDb(account)) addr <- daos.addressDAO.create(getAddressDb(account))
utxo <- daos.utxoDAO.create(sampleLegacyUTXO(addr.scriptPubKey)) utxo = sampleLegacyUTXO(addr.scriptPubKey)
} yield utxo.asInstanceOf[LegacySpendingInfo] _ <- insertDummyIncomingTransaction(daos, utxo)
utxoDb <- daos.utxoDAO.create(utxo)
} yield utxoDb.asInstanceOf[LegacySpendingInfo]
} }
/** Inserts an account, address and finally a UTXO */ /** Inserts an account, address and finally a UTXO */
@ -205,8 +233,10 @@ object WalletTestUtil {
for { for {
account <- daos.accountDAO.create(WalletTestUtil.firstAccountDb) account <- daos.accountDAO.create(WalletTestUtil.firstAccountDb)
addr <- daos.addressDAO.create(getAddressDb(account)) addr <- daos.addressDAO.create(getAddressDb(account))
utxo <- daos.utxoDAO.create(sampleSegwitUTXO(addr.scriptPubKey)) utxo = sampleSegwitUTXO(addr.scriptPubKey)
} yield utxo.asInstanceOf[SegwitV0SpendingInfo] _ <- insertDummyIncomingTransaction(daos, utxo)
utxoDb <- daos.utxoDAO.create(utxo)
} yield utxoDb.asInstanceOf[SegwitV0SpendingInfo]
} }
/** Inserts an account, address and finally a UTXO */ /** Inserts an account, address and finally a UTXO */
@ -215,7 +245,9 @@ object WalletTestUtil {
for { for {
account <- daos.accountDAO.create(WalletTestUtil.nestedSegWitAccountDb) account <- daos.accountDAO.create(WalletTestUtil.nestedSegWitAccountDb)
addr <- daos.addressDAO.create(getNestedSegwitAddressDb(account)) addr <- daos.addressDAO.create(getNestedSegwitAddressDb(account))
utxo <- daos.utxoDAO.create(sampleNestedSegwitUTXO(addr.ecPublicKey)) utxo = sampleNestedSegwitUTXO(addr.ecPublicKey)
} yield utxo.asInstanceOf[NestedSegwitV0SpendingInfo] _ <- insertDummyIncomingTransaction(daos, utxo)
utxoDb <- daos.utxoDAO.create(utxo)
} yield utxoDb.asInstanceOf[NestedSegwitV0SpendingInfo]
} }
} }

View file

@ -4,7 +4,7 @@ import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.protocol.BitcoinAddress import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.script.EmptyScriptPubKey import org.bitcoins.core.protocol.script.EmptyScriptPubKey
import org.bitcoins.core.protocol.transaction.TransactionOutput import org.bitcoins.core.protocol.transaction.TransactionOutput
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte import org.bitcoins.core.wallet.fee.{SatoshisPerByte, SatoshisPerVirtualByte}
import org.bitcoins.core.wallet.utxo.TxoState import org.bitcoins.core.wallet.utxo.TxoState
import org.bitcoins.testkit.wallet.BitcoinSWalletTest import org.bitcoins.testkit.wallet.BitcoinSWalletTest
import org.bitcoins.testkit.wallet.BitcoinSWalletTest.{ import org.bitcoins.testkit.wallet.BitcoinSWalletTest.{
@ -32,7 +32,7 @@ class UTXOLifeCycleTest extends BitcoinSWalletTest {
for { for {
tx <- wallet.sendToAddress(testAddr, tx <- wallet.sendToAddress(testAddr,
Satoshis(3000), Satoshis(3000),
SatoshisPerVirtualByte(Satoshis(3))) SatoshisPerByte(Satoshis(3)))
updatedCoins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx) updatedCoins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx)
} yield { } yield {
@ -48,7 +48,11 @@ class UTXOLifeCycleTest extends BitcoinSWalletTest {
txId <- bitcoind.sendToAddress(addr, Satoshis(3000)) txId <- bitcoind.sendToAddress(addr, Satoshis(3000))
tx <- bitcoind.getRawTransactionRaw(txId) tx <- bitcoind.getRawTransactionRaw(txId)
_ <- wallet.processOurTransaction(tx, None) _ <- wallet.processOurTransaction(transaction = tx,
feeRate = SatoshisPerByte(Satoshis(3)),
inputAmount = Satoshis(4000),
sentAmount = Satoshis(3000),
blockHashOpt = None)
updatedCoin <- wallet.spendingInfoDAO.findByScriptPubKey( updatedCoin <- wallet.spendingInfoDAO.findByScriptPubKey(
addr.scriptPubKey) addr.scriptPubKey)

View file

@ -19,9 +19,9 @@ class WalletIntegrationTest extends BitcoinSWalletTest {
behavior of "Wallet - integration test" behavior of "Wallet - integration test"
val feeRate = SatoshisPerByte(Satoshis.one) val feeRate: SatoshisPerByte = SatoshisPerByte(Satoshis.one)
/** Checks that the given vaues are the same-ish, save for fee-level deviations */ /** Checks that the given values are the same-ish, save for fee-level deviations */
private def isCloseEnough( private def isCloseEnough(
first: CurrencyUnit, first: CurrencyUnit,
second: CurrencyUnit, second: CurrencyUnit,
@ -65,15 +65,15 @@ class WalletIntegrationTest extends BitcoinSWalletTest {
// it should not be confirmed // it should not be confirmed
utxosPostAdd <- wallet.listUtxos() utxosPostAdd <- wallet.listUtxos()
_ = assert(utxosPostAdd.length == 1) _ = assert(utxosPostAdd.length == 1)
_ <- wallet
.getConfirmedBalance()
.map(confirmed => assert(confirmed == 0.bitcoin))
_ <- wallet _ <- wallet
.getConfirmedBalance() .getConfirmedBalance()
.map(confirmed => assert(confirmed == 0.bitcoin)) .map(confirmed => assert(confirmed == 0.bitcoin))
_ <- wallet _ <- wallet
.getUnconfirmedBalance() .getUnconfirmedBalance()
.map(unconfirmed => assert(unconfirmed == valueFromBitcoind)) .map(unconfirmed => assert(unconfirmed == valueFromBitcoind))
incomingTx <- wallet.incomingTxDAO.findByTxId(tx.txIdBE)
_ = assert(incomingTx.isDefined)
_ = assert(incomingTx.get.incomingAmount == valueFromBitcoind)
_ <- bitcoind.getNewAddress.flatMap(bitcoind.generateToAddress(6, _)) _ <- bitcoind.getNewAddress.flatMap(bitcoind.generateToAddress(6, _))
rawTx <- bitcoind.getRawTransaction(txId) rawTx <- bitcoind.getRawTransaction(txId)
@ -103,12 +103,27 @@ class WalletIntegrationTest extends BitcoinSWalletTest {
_ <- bitcoind.getNewAddress.flatMap(bitcoind.generateToAddress(1, _)) _ <- bitcoind.getNewAddress.flatMap(bitcoind.generateToAddress(1, _))
tx <- bitcoind.getRawTransaction(txid) tx <- bitcoind.getRawTransaction(txid)
_ <- wallet.listUtxos().map { utxos <- wallet.listUtxos()
_ = utxos match {
case utxo +: Vector() => case utxo +: Vector() =>
assert(utxo.privKeyPath.chain.chainType == HDChainType.Change) assert(utxo.privKeyPath.chain.chainType == HDChainType.Change)
case other => fail(s"Found ${other.length} utxos!") case other => fail(s"Found ${other.length} utxos!")
} }
outgoingTx <- wallet.outgoingTxDAO.findByTxId(txid)
_ = assert(outgoingTx.isDefined)
_ = assert(outgoingTx.get.inputAmount == valueFromBitcoind)
_ = assert(outgoingTx.get.sentAmount == valueToBitcoind)
_ = assert(outgoingTx.get.feeRate == feeRate)
_ = assert(outgoingTx.get.expectedFee == feeRate.calc(signedTx))
_ = assert(
isCloseEnough(feeRate.calc(signedTx),
outgoingTx.get.actualFee,
3.satoshi))
// Safe to use utxos.head because we've already asserted that we only have our change output
_ = assert(
outgoingTx.get.actualFee + outgoingTx.get.sentAmount == outgoingTx.get.inputAmount - utxos.head.output.value)
balancePostSend <- wallet.getBalance() balancePostSend <- wallet.getBalance()
_ = { _ = {
// change UTXO should be smaller than what we had, but still have money in it // change UTXO should be smaller than what we had, but still have money in it

View file

@ -0,0 +1,50 @@
package org.bitcoins.wallet.models
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.testkit.fixtures.WalletDAOFixture
import org.bitcoins.testkit.wallet.{BitcoinSWalletTest, WalletTestUtil}
class IncomingTransactionDAOTest
extends BitcoinSWalletTest
with WalletDAOFixture {
val txDb: TransactionDb =
TransactionDb.fromTransaction(WalletTestUtil.sampleTransaction)
val incoming: IncomingTransactionDb =
IncomingTransactionDb(WalletTestUtil.sampleTransaction.txIdBE,
Satoshis(10000))
it should "insert and read an transaction into the database" in { daos =>
val txDAO = daos.transactionDAO
val incomingTxDAO = daos.incomingTxDAO
for {
_ <- txDAO.create(txDb)
created <- incomingTxDAO.create(incoming)
found <- incomingTxDAO.read(incoming.txIdBE)
} yield assert(found.contains(created))
}
it must "find a transaction by txIdBE" in { daos =>
val txDAO = daos.transactionDAO
val incomingTxDAO = daos.incomingTxDAO
for {
_ <- txDAO.create(txDb)
created <- incomingTxDAO.create(incoming)
found <- incomingTxDAO.findByTxId(incoming.txIdBE)
} yield assert(found.contains(created))
}
it must "find a transaction by txId" in { daos =>
val txDAO = daos.transactionDAO
val incomingTxDAO = daos.incomingTxDAO
for {
_ <- txDAO.create(txDb)
created <- incomingTxDAO.create(incoming)
found <- incomingTxDAO.findByTxId(incoming.txId)
} yield assert(found.contains(created))
}
}

View file

@ -0,0 +1,53 @@
package org.bitcoins.wallet.models
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.wallet.fee.SatoshisPerByte
import org.bitcoins.testkit.fixtures.WalletDAOFixture
import org.bitcoins.testkit.wallet.{BitcoinSWalletTest, WalletTestUtil}
class OutgoingTransactionDAOTest
extends BitcoinSWalletTest
with WalletDAOFixture {
val txDb: TransactionDb =
TransactionDb.fromTransaction(WalletTestUtil.sampleTransaction)
val outgoing: OutgoingTransactionDb = OutgoingTransactionDb.fromTransaction(
WalletTestUtil.sampleTransaction,
Satoshis(250000000),
WalletTestUtil.sampleTransaction.outputs.head.value,
Satoshis(10000))
it should "insert and read an transaction into the database" in { daos =>
val txDAO = daos.transactionDAO
val outgoingTxDAO = daos.outgoingTxDAO
for {
_ <- txDAO.create(txDb)
created <- outgoingTxDAO.create(outgoing)
found <- outgoingTxDAO.read(outgoing.txIdBE)
} yield assert(found.contains(created))
}
it must "find a transaction by txIdBE" in { daos =>
val txDAO = daos.transactionDAO
val outgoingTxDAO = daos.outgoingTxDAO
for {
_ <- txDAO.create(txDb)
created <- outgoingTxDAO.create(outgoing)
found <- outgoingTxDAO.findByTxId(outgoing.txIdBE)
} yield assert(found.contains(created))
}
it must "find a transaction by txId" in { daos =>
val txDAO = daos.transactionDAO
val outgoingTxDAO = daos.outgoingTxDAO
for {
_ <- txDAO.create(txDb)
created <- outgoingTxDAO.create(outgoing)
found <- outgoingTxDAO.findByTxId(outgoing.txId)
} yield assert(found.contains(created))
}
}

View file

@ -8,7 +8,7 @@ import org.bitcoins.core.protocol.transaction.{
import org.bitcoins.core.wallet.utxo.TxoState import org.bitcoins.core.wallet.utxo.TxoState
import org.bitcoins.testkit.Implicits._ import org.bitcoins.testkit.Implicits._
import org.bitcoins.testkit.core.gen.TransactionGenerators import org.bitcoins.testkit.core.gen.TransactionGenerators
import org.bitcoins.testkit.fixtures.{WalletDAOFixture, WalletDAOs} import org.bitcoins.testkit.fixtures.WalletDAOFixture
import org.bitcoins.testkit.wallet.{BitcoinSWalletTest, WalletTestUtil} import org.bitcoins.testkit.wallet.{BitcoinSWalletTest, WalletTestUtil}
class SpendingInfoDAOTest extends BitcoinSWalletTest with WalletDAOFixture { class SpendingInfoDAOTest extends BitcoinSWalletTest with WalletDAOFixture {
@ -40,7 +40,7 @@ class SpendingInfoDAOTest extends BitcoinSWalletTest with WalletDAOFixture {
} }
it should "find incoming outputs being spent, given a TX" in { daos => it should "find incoming outputs being spent, given a TX" in { daos =>
val WalletDAOs(_, _, utxoDAO) = daos val utxoDAO = daos.utxoDAO
for { for {
utxo <- WalletTestUtil.insertLegacyUTXO(daos) utxo <- WalletTestUtil.insertLegacyUTXO(daos)
@ -75,7 +75,7 @@ class SpendingInfoDAOTest extends BitcoinSWalletTest with WalletDAOFixture {
} }
it must "insert an unspent TXO and then mark it as spent" in { daos => it must "insert an unspent TXO and then mark it as spent" in { daos =>
val WalletDAOs(_, _, spendingInfoDAO) = daos val spendingInfoDAO = daos.utxoDAO
for { for {
utxo <- WalletTestUtil.insertSegWitUTXO(daos) utxo <- WalletTestUtil.insertSegWitUTXO(daos)
updated <- spendingInfoDAO.update( updated <- spendingInfoDAO.update(
@ -93,7 +93,7 @@ class SpendingInfoDAOTest extends BitcoinSWalletTest with WalletDAOFixture {
} }
it must "insert an unspent TXO and find it as unspent" in { daos => it must "insert an unspent TXO and find it as unspent" in { daos =>
val WalletDAOs(_, _, spendingInfoDAO) = daos val spendingInfoDAO = daos.utxoDAO
for { for {
utxo <- WalletTestUtil.insertLegacyUTXO(daos) utxo <- WalletTestUtil.insertLegacyUTXO(daos)
state = utxo.copy(state = TxoState.PendingConfirmationsReceived) state = utxo.copy(state = TxoState.PendingConfirmationsReceived)
@ -108,7 +108,7 @@ class SpendingInfoDAOTest extends BitcoinSWalletTest with WalletDAOFixture {
} }
it must "insert a spent TXO and NOT find it as unspent" in { daos => it must "insert a spent TXO and NOT find it as unspent" in { daos =>
val WalletDAOs(_, _, spendingInfoDAO) = daos val spendingInfoDAO = daos.utxoDAO
for { for {
utxo <- WalletTestUtil.insertLegacyUTXO(daos) utxo <- WalletTestUtil.insertLegacyUTXO(daos)
state = utxo.copy(state = TxoState.PendingConfirmationsSpent) state = utxo.copy(state = TxoState.PendingConfirmationsSpent)
@ -118,7 +118,7 @@ class SpendingInfoDAOTest extends BitcoinSWalletTest with WalletDAOFixture {
} }
it must "insert a TXO and read it back with through a TXID " in { daos => it must "insert a TXO and read it back with through a TXID " in { daos =>
val WalletDAOs(_, _, spendingInfoDAO) = daos val spendingInfoDAO = daos.utxoDAO
for { for {
utxo <- WalletTestUtil.insertLegacyUTXO(daos) utxo <- WalletTestUtil.insertLegacyUTXO(daos)

View file

@ -0,0 +1,37 @@
package org.bitcoins.wallet.models
import org.bitcoins.testkit.fixtures.WalletDAOFixture
import org.bitcoins.testkit.wallet.{BitcoinSWalletTest, WalletTestUtil}
class TransactionDAOTest extends BitcoinSWalletTest with WalletDAOFixture {
val txDb: TransactionDb =
TransactionDb.fromTransaction(WalletTestUtil.sampleTransaction)
it should "insert and read an transaction into the database" in { daos =>
val txDAO = daos.transactionDAO
for {
created <- txDAO.create(txDb)
found <- txDAO.read(txDb.txIdBE)
} yield assert(found.contains(created))
}
it must "find a transaction by txIdBE" in { daos =>
val txDAO = daos.transactionDAO
for {
created <- txDAO.create(txDb)
found <- txDAO.findByTxId(txDb.txIdBE)
} yield assert(found.contains(created))
}
it must "find a transaction by txId" in { daos =>
val txDAO = daos.transactionDAO
for {
created <- txDAO.create(txDb)
found <- txDAO.findByTxId(txDb.txId)
} yield assert(found.contains(created))
}
}

View file

@ -0,0 +1,11 @@
CREATE TABLE "tx_table" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,"txIdBE" VARCHAR(254) NOT NULL UNIQUE,"transaction" VARCHAR(254) NOT NULL,"unsignedTxIdBE" VARCHAR(254) NOT NULL,"unsignedTx" VARCHAR(254) NOT NULL,"wTxIdBE" VARCHAR(254),"totalOutput" INTEGER NOT NULL,"numInputs" INTEGER NOT NULL,"numOutputs" INTEGER NOT NULL,"locktime" INTEGER NOT NULL);
CREATE TABLE "wallet_incoming_txs" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,"txIdBE" VARCHAR(254) NOT NULL UNIQUE,"incomingAmount" INTEGER NOT NULL,constraint "fk_underlying_tx" foreign key("txIdBE") references "tx_table"("txIdBE") on update NO ACTION on delete NO ACTION);
CREATE TABLE "wallet_outgoing_txs" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,"txIdBE" VARCHAR(254) NOT NULL UNIQUE,"inputAmount" INTEGER NOT NULL,"sentAmount" INTEGER NOT NULL,"actualFee" INTEGER NOT NULL,"expectedFee" INTEGER NOT NULL,"feeRate" INTEGER NOT NULL,constraint "fk_underlying_tx" foreign key("txIdBE") references "tx_table"("txIdBE") on update NO ACTION on delete NO ACTION);
-- This adds the foreign key constraint for txid to table txo_spending_info
CREATE TEMPORARY TABLE "txo_spending_info_backup" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,"tx_outpoint" VARCHAR(254) NOT NULL, "script_pub_key" VARCHAR(254) NOT NULL,"value" INTEGER NOT NULL,"hd_privkey_path" VARCHAR(254) NOT NULL,"redeem_script" VARCHAR(254),"script_witness" VARCHAR(254),"confirmations" INTEGER,"txid" VARCHAR(254) NOT NULL,"block_hash" VARCHAR(254), "txo_state" VARCHAR(254) NOT NULL, constraint "fk_scriptPubKey" foreign key("script_pub_key") references "addresses"("script_pub_key") on update NO ACTION on delete NO ACTION);
INSERT INTO "txo_spending_info_backup" SELECT "id", "tx_outpoint", "script_pub_key", "value", "hd_privkey_path", "redeem_script", "script_witness", "confirmations", "txid","block_hash", "txo_state" FROM "txo_spending_info";
DROP TABLE "txo_spending_info";
CREATE TABLE "txo_spending_info" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,"tx_outpoint" VARCHAR(254) NOT NULL, "script_pub_key" VARCHAR(254) 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, constraint "fk_scriptPubKey" foreign key("script_pub_key") references "addresses"("script_pub_key"), 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" SELECT "id", "tx_outpoint", "script_pub_key", "value", "hd_privkey_path", "redeem_script", "script_witness", "txid", "block_hash", "txo_state" FROM "txo_spending_info_backup";
DROP TABLE "txo_spending_info_backup";

View file

@ -46,7 +46,11 @@ sealed abstract class Wallet extends LockedWallet with UnlockedWalletApi {
markAsReserved = false) markAsReserved = false)
signed <- txBuilder.sign signed <- txBuilder.sign
ourOuts <- findOurOuts(signed) ourOuts <- findOurOuts(signed)
_ <- processOurTransaction(signed, blockHashOpt = None) _ <- processOurTransaction(transaction = signed,
feeRate = feeRate,
inputAmount = txBuilder.creditingAmount,
sentAmount = txBuilder.destinationAmount,
blockHashOpt = None)
} yield { } yield {
logger.debug( logger.debug(
s"Signed transaction=${signed.txIdBE.hex} with outputs=${signed.outputs.length}, inputs=${signed.inputs.length}") s"Signed transaction=${signed.txIdBE.hex} with outputs=${signed.outputs.length}, inputs=${signed.inputs.length}")

View file

@ -8,9 +8,17 @@ sealed abstract class WalletDbManagement extends DbManagement {
private val accountTable = TableQuery[AccountTable] private val accountTable = TableQuery[AccountTable]
private val addressTable = TableQuery[AddressTable] private val addressTable = TableQuery[AddressTable]
private val utxoTable = TableQuery[SpendingInfoTable] private val utxoTable = TableQuery[SpendingInfoTable]
private val txTable = TableQuery[TransactionTable]
private val incomingTxTable = TableQuery[IncomingTransactionTable]
private val outgoingTxTable = TableQuery[OutgoingTransactionTable]
override val allTables: List[TableQuery[_ <: Table[_]]] = override val allTables: List[TableQuery[_ <: Table[_]]] =
List(accountTable, addressTable, utxoTable) List(accountTable,
addressTable,
utxoTable,
txTable,
incomingTxTable,
outgoingTxTable)
} }

View file

@ -1,10 +1,12 @@
package org.bitcoins.wallet.internal package org.bitcoins.wallet.internal
import org.bitcoins.core.crypto.{DoubleSha256Digest, DoubleSha256DigestBE} import org.bitcoins.core.crypto.{DoubleSha256Digest, DoubleSha256DigestBE}
import org.bitcoins.core.currency.CurrencyUnit
import org.bitcoins.core.number.UInt32 import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.blockchain.Block import org.bitcoins.core.protocol.blockchain.Block
import org.bitcoins.core.protocol.transaction.{Transaction, TransactionOutput} import org.bitcoins.core.protocol.transaction.{Transaction, TransactionOutput}
import org.bitcoins.core.util.FutureUtil import org.bitcoins.core.util.FutureUtil
import org.bitcoins.core.wallet.fee.FeeUnit
import org.bitcoins.core.wallet.utxo.TxoState import org.bitcoins.core.wallet.utxo.TxoState
import org.bitcoins.wallet._ import org.bitcoins.wallet._
import org.bitcoins.wallet.api.{AddUtxoError, AddUtxoSuccess} import org.bitcoins.wallet.api.{AddUtxoError, AddUtxoSuccess}
@ -68,6 +70,23 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
///////////////////// /////////////////////
// Internal wallet API // Internal wallet API
private[wallet] def insertOutgoingTransaction(
transaction: Transaction,
feeRate: FeeUnit,
inputAmount: CurrencyUnit,
sentAmount: CurrencyUnit): Future[OutgoingTransactionDb] = {
val txDb = TransactionDb.fromTransaction(transaction)
val outgoingDb =
OutgoingTransactionDb.fromTransaction(transaction,
inputAmount,
sentAmount,
feeRate.calc(transaction))
for {
_ <- transactionDAO.upsert(txDb)
written <- outgoingTxDAO.upsert(outgoingDb)
} yield written
}
/** /**
* Processes TXs originating from our wallet. * Processes TXs originating from our wallet.
* This is called right after we've signed a TX, * This is called right after we've signed a TX,
@ -75,10 +94,19 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
*/ */
private[wallet] def processOurTransaction( private[wallet] def processOurTransaction(
transaction: Transaction, transaction: Transaction,
feeRate: FeeUnit,
inputAmount: CurrencyUnit,
sentAmount: CurrencyUnit,
blockHashOpt: Option[DoubleSha256DigestBE]): Future[ProcessTxResult] = { blockHashOpt: Option[DoubleSha256DigestBE]): Future[ProcessTxResult] = {
logger.info( logger.info(
s"Processing TX from our wallet, transaction=${transaction.txIdBE} with blockHash=$blockHashOpt") s"Processing TX from our wallet, transaction=${transaction.txIdBE} with blockHash=$blockHashOpt")
processTransactionImpl(transaction, blockHashOpt).map { result => for {
_ <- insertOutgoingTransaction(transaction,
feeRate,
inputAmount,
sentAmount)
result <- processTransactionImpl(transaction, blockHashOpt)
} yield {
val txid = transaction.txIdBE val txid = transaction.txIdBE
val changeOutputs = result.updatedIncoming.length val changeOutputs = result.updatedIncoming.length
val spentOutputs = result.updatedOutgoing.length val spentOutputs = result.updatedOutgoing.length
@ -301,6 +329,36 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
} }
} }
private def addUTXOsFut(
outputsWithIndex: Seq[OutputWithIndex],
transaction: Transaction,
blockHashOpt: Option[DoubleSha256DigestBE]): Future[Seq[SpendingInfoDb]] =
Future
.sequence {
outputsWithIndex.map(
out =>
processUtxo(
transaction,
out.index,
// TODO is this correct?
//we probably need to incorporate what
//what our wallet's desired confirmation number is
state = TxoState.PendingConfirmationsReceived,
blockHash = blockHashOpt
))
}
private[wallet] def insertIncomingTransaction(
transaction: Transaction,
incomingAmount: CurrencyUnit): Future[IncomingTransactionDb] = {
val txDb = TransactionDb.fromTransaction(transaction)
val incomingDb = IncomingTransactionDb(transaction.txIdBE, incomingAmount)
for {
_ <- transactionDAO.upsert(txDb)
written <- incomingTxDAO.upsert(incomingDb)
} yield written
}
/** /**
* Processes an incoming transaction that's new to us * Processes an incoming transaction that's new to us
* *
@ -326,10 +384,11 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
s"Found no outputs relevant to us in transaction${transaction.txIdBE}") s"Found no outputs relevant to us in transaction${transaction.txIdBE}")
Future.successful(Vector.empty) Future.successful(Vector.empty)
case xs => case outputsWithIndex =>
val count = xs.length val count = outputsWithIndex.length
val outputStr = { val outputStr = {
xs.map { elem => outputsWithIndex
.map { elem =>
s"${transaction.txIdBE.hex}:${elem.index}" s"${transaction.txIdBE.hex}:${elem.index}"
} }
.mkString(", ") .mkString(", ")
@ -337,24 +396,12 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
logger.trace( logger.trace(
s"Found $count relevant output(s) in transaction=${transaction.txIdBE}: $outputStr") s"Found $count relevant output(s) in transaction=${transaction.txIdBE}: $outputStr")
val addUTXOsFut: Future[Seq[SpendingInfoDb]] = val totalIncoming = outputsWithIndex.map(_.output.value).sum
Future
.sequence {
xs.map(
out =>
processUtxo(
transaction,
out.index,
// TODO is this correct?
//we probably need to incorporate what
//what our wallet's desired confirmation number is
state = TxoState.PendingConfirmationsReceived,
blockHash = blockHashOpt
))
}
addUTXOsFut
for {
_ <- insertIncomingTransaction(transaction, totalIncoming)
utxos <- addUTXOsFut(outputsWithIndex, transaction, blockHashOpt)
} yield utxos
} }
} }
} }

View file

@ -60,7 +60,7 @@ private[wallet] trait UtxoHandling extends WalletLogger {
/** Constructs a DB level representation of the given UTXO, and persist it to disk */ /** Constructs a DB level representation of the given UTXO, and persist it to disk */
private def writeUtxo( private def writeUtxo(
txid: DoubleSha256DigestBE, tx: Transaction,
state: TxoState, state: TxoState,
output: TransactionOutput, output: TransactionOutput,
outPoint: TransactionOutPoint, outPoint: TransactionOutPoint,
@ -71,7 +71,7 @@ private[wallet] trait UtxoHandling extends WalletLogger {
case segwitAddr: SegWitAddressDb => case segwitAddr: SegWitAddressDb =>
SegwitV0SpendingInfo( SegwitV0SpendingInfo(
state = state, state = state,
txid = txid, txid = tx.txIdBE,
outPoint = outPoint, outPoint = outPoint,
output = output, output = output,
privKeyPath = segwitAddr.path, privKeyPath = segwitAddr.path,
@ -80,7 +80,7 @@ private[wallet] trait UtxoHandling extends WalletLogger {
) )
case LegacyAddressDb(path, _, _, _, _) => case LegacyAddressDb(path, _, _, _, _) =>
LegacySpendingInfo(state = state, LegacySpendingInfo(state = state,
txid = txid, txid = tx.txIdBE,
outPoint = outPoint, outPoint = outPoint,
output = output, output = output,
privKeyPath = path, privKeyPath = path,
@ -92,14 +92,16 @@ private[wallet] trait UtxoHandling extends WalletLogger {
privKeyPath = nested.path, privKeyPath = nested.path,
redeemScript = P2WPKHWitnessSPKV0(nested.ecPublicKey), redeemScript = P2WPKHWitnessSPKV0(nested.ecPublicKey),
scriptWitness = P2WPKHWitnessV0(nested.ecPublicKey), scriptWitness = P2WPKHWitnessV0(nested.ecPublicKey),
txid = txid, txid = tx.txIdBE,
state = state, state = state,
id = None, id = None,
blockHash = blockHash blockHash = blockHash
) )
} }
spendingInfoDAO.create(utxo).map { written => for {
written <- spendingInfoDAO.create(utxo)
} yield {
val writtenOut = written.outPoint val writtenOut = written.outPoint
logger.info( logger.info(
s"Successfully inserted UTXO ${writtenOut.txId.hex}:${writtenOut.vout.toInt} into DB") s"Successfully inserted UTXO ${writtenOut.txId.hex}:${writtenOut.vout.toInt} into DB")
@ -147,7 +149,7 @@ private[wallet] trait UtxoHandling extends WalletLogger {
addressDbEitherF.flatMap { addressDbE => addressDbEitherF.flatMap { addressDbE =>
val biasedE: CompatEither[AddUtxoError, Future[SpendingInfoDb]] = for { val biasedE: CompatEither[AddUtxoError, Future[SpendingInfoDb]] = for {
addressDb <- addressDbE addressDb <- addressDbE
} yield writeUtxo(txid = transaction.txIdBE, } yield writeUtxo(tx = transaction,
state = state, state = state,
output = output, output = output,
outPoint = outPoint, outPoint = outPoint,

View file

@ -0,0 +1,13 @@
package org.bitcoins.wallet.models
import org.bitcoins.wallet.config._
import slick.lifted.TableQuery
import scala.concurrent.ExecutionContext
case class IncomingTransactionDAO()(
implicit val ec: ExecutionContext,
val appConfig: WalletAppConfig)
extends TxDAO[IncomingTransactionDb, IncomingTransactionTable] {
override val table = TableQuery[IncomingTransactionTable]
}

View file

@ -0,0 +1,13 @@
package org.bitcoins.wallet.models
import org.bitcoins.wallet.config._
import slick.lifted.TableQuery
import scala.concurrent.ExecutionContext
case class OutgoingTransactionDAO()(
implicit val ec: ExecutionContext,
val appConfig: WalletAppConfig)
extends TxDAO[OutgoingTransactionDb, OutgoingTransactionTable] {
override val table = TableQuery[OutgoingTransactionTable]
}

View file

@ -230,6 +230,14 @@ case class SpendingInfoTable(tag: Tag)
targetTableQuery = addressTable)(_.scriptPubKey) targetTableQuery = addressTable)(_.scriptPubKey)
} }
/** All UTXOs must have a corresponding transaction in the wallet */
def fk_incoming_txId = {
val txTable = TableQuery[IncomingTransactionTable]
foreignKey("fk_incoming_txId",
sourceColumns = txid,
targetTableQuery = txTable)(_.txIdBE)
}
private type UTXOTuple = ( private type UTXOTuple = (
Option[Long], // ID Option[Long], // ID
TransactionOutPoint, TransactionOutPoint,

View file

@ -0,0 +1,62 @@
package org.bitcoins.wallet.models
import org.bitcoins.core.crypto.{DoubleSha256Digest, DoubleSha256DigestBE}
import org.bitcoins.db.{CRUD, SlickUtil}
import org.bitcoins.wallet.config._
import slick.jdbc.SQLiteProfile.api._
import scala.concurrent.{ExecutionContext, Future}
trait TxDAO[DbEntryType <: TxDB, DbTable <: TxTable[DbEntryType]]
extends CRUD[DbEntryType, DoubleSha256DigestBE] {
implicit val ec: ExecutionContext
val appConfig: WalletAppConfig
import org.bitcoins.db.DbCommonsColumnMappers._
override val table: TableQuery[DbTable]
override def createAll(ts: Vector[DbEntryType]): Future[Vector[DbEntryType]] =
SlickUtil.createAllNoAutoInc(ts, database, table)
override protected def findByPrimaryKeys(txIdBEs: Vector[
DoubleSha256DigestBE]): Query[Table[_], DbEntryType, Seq] =
table.filter(_.txIdBE.inSet(txIdBEs))
override def findByPrimaryKey(
txIdBE: DoubleSha256DigestBE): Query[Table[_], DbEntryType, Seq] = {
table.filter(_.txIdBE === txIdBE)
}
override def findAll(
txs: Vector[DbEntryType]): Query[Table[_], DbEntryType, Seq] =
findByPrimaryKeys(txs.map(_.txIdBE))
def findByTxId(txIdBE: DoubleSha256DigestBE): Future[Option[DbEntryType]] = {
val q = table
.filter(_.txIdBE === txIdBE)
database.run(q.result).map {
case h +: Vector() =>
Some(h)
case Vector() =>
None
case txs: Vector[DbEntryType] =>
// yikes, we should not have more the one transaction per id
throw new RuntimeException(
s"More than one transaction per id=${txIdBE.hex}, got=$txs")
}
}
def findByTxId(txId: DoubleSha256Digest): Future[Option[DbEntryType]] =
findByTxId(txId.flip)
}
case class TransactionDAO()(
implicit val ec: ExecutionContext,
val appConfig: WalletAppConfig)
extends TxDAO[TransactionDb, TransactionTable] {
override val table = TableQuery[TransactionTable]
}

View file

@ -0,0 +1,182 @@
package org.bitcoins.wallet.models
import org.bitcoins.core.crypto._
import org.bitcoins.core.currency.CurrencyUnit
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.script.EmptyScriptSignature
import org.bitcoins.core.protocol.transaction.{
BaseTransaction,
EmptyWitness,
Transaction,
TransactionInput,
WitnessTransaction
}
import slick.jdbc.SQLiteProfile.api._
import slick.lifted.{PrimaryKey, ProvenShape}
trait TxDB {
def txIdBE: DoubleSha256DigestBE
}
trait TxTable[DbEntryType <: TxDB] extends Table[DbEntryType] {
def txIdBE: Rep[DoubleSha256DigestBE]
}
/**
* Represents a relevant transaction for the wallet that we should be keeping track of
* @param txIdBE Transaction ID
* @param transaction Serialized Transaction
* @param unsignedTxIdBE Transaction ID of the unsigned transaction
* @param unsignedTx Unsigned Transaction. This is useful so we can reconcile what our estimated
* fees were against our actual fees in the case of ECDSA signature size variability
* @param wTxIdBEOpt Witness Transaction ID
* @param numInputs Number of inputs in the transaction
* @param numOutputs Number of outputs in the transaction
* @param lockTime locktime of the transaction
*/
case class TransactionDb(
txIdBE: DoubleSha256DigestBE,
transaction: Transaction,
unsignedTxIdBE: DoubleSha256DigestBE,
unsignedTx: Transaction,
wTxIdBEOpt: Option[DoubleSha256DigestBE],
totalOutput: CurrencyUnit,
numInputs: Int,
numOutputs: Int,
lockTime: UInt32)
extends TxDB {
require(unsignedTx.inputs.forall(_.scriptSignature == EmptyScriptSignature),
s"All ScriptSignatures must be empty, got $unsignedTx")
lazy val txId: DoubleSha256Digest = txIdBE.flip
lazy val unsignedTxId: DoubleSha256Digest = unsignedTxIdBE.flip
lazy val wTxIdOpt: Option[DoubleSha256Digest] = wTxIdBEOpt.map(_.flip)
}
object TransactionDb {
def fromTransaction(tx: Transaction): TransactionDb = {
val (unsignedTx, wTxIdBEOpt) = tx match {
case btx: BaseTransaction =>
val unsignedInputs = btx.inputs.map(
input =>
TransactionInput(input.previousOutput,
EmptyScriptSignature,
input.sequence))
(BaseTransaction(btx.version,
unsignedInputs,
btx.outputs,
btx.lockTime),
None)
case wtx: WitnessTransaction =>
val unsignedInputs = wtx.inputs.map(
input =>
TransactionInput(input.previousOutput,
EmptyScriptSignature,
input.sequence))
val uwtx = WitnessTransaction(wtx.version,
unsignedInputs,
wtx.outputs,
wtx.lockTime,
EmptyWitness.fromInputs(unsignedInputs))
(uwtx, Some(uwtx.wTxIdBE))
}
val totalOutput = tx.outputs.map(_.value).sum
TransactionDb(tx.txIdBE,
tx,
unsignedTx.txIdBE,
unsignedTx,
wTxIdBEOpt,
totalOutput,
tx.inputs.size,
tx.outputs.size,
tx.lockTime)
}
}
class TransactionTable(tag: Tag)
extends Table[TransactionDb](tag, "tx_table")
with TxTable[TransactionDb] {
import org.bitcoins.db.DbCommonsColumnMappers._
def txIdBE: Rep[DoubleSha256DigestBE] = column("txIdBE", O.Unique)
def transaction: Rep[Transaction] = column("transaction")
def unsignedTxIdBE: Rep[DoubleSha256DigestBE] = column("unsignedTxIdBE")
def unsignedTx: Rep[Transaction] = column("unsignedTx")
def wTxIdBEOpt: Rep[Option[DoubleSha256DigestBE]] =
column("wTxIdBE")
def totalOutput: Rep[CurrencyUnit] = column("totalOutput")
def numInputs: Rep[Int] = column("numInputs")
def numOutputs: Rep[Int] = column("numOutputs")
def locktime: Rep[UInt32] = column("locktime")
private type TransactionTuple =
(
DoubleSha256DigestBE,
Transaction,
DoubleSha256DigestBE,
Transaction,
Option[DoubleSha256DigestBE],
CurrencyUnit,
Int,
Int,
UInt32)
private val fromTuple: TransactionTuple => TransactionDb = {
case (txId,
transaction,
unsignedTxIdBE,
unsignedTx,
wTxIdBEOpt,
totalOutput,
numInputs,
numOutputs,
locktime) =>
TransactionDb(txId,
transaction,
unsignedTxIdBE,
unsignedTx,
wTxIdBEOpt,
totalOutput,
numInputs,
numOutputs,
locktime)
}
private val toTuple: TransactionDb => Option[TransactionTuple] = tx =>
Some(
(tx.txIdBE,
tx.transaction,
tx.unsignedTxIdBE,
tx.unsignedTx,
tx.wTxIdBEOpt,
tx.totalOutput,
tx.numInputs,
tx.numOutputs,
tx.lockTime))
def * : ProvenShape[TransactionDb] =
(txIdBE,
transaction,
unsignedTxIdBE,
unsignedTx,
wTxIdBEOpt,
totalOutput,
numInputs,
numOutputs,
locktime) <> (fromTuple, toTuple)
def primaryKey: PrimaryKey =
primaryKey("pk_tx", sourceColumns = txIdBE)
}