mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-02-23 14:50:42 +01:00
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:
parent
3b3d2414f7
commit
29eb6c2e05
23 changed files with 649 additions and 54 deletions
|
@ -7,7 +7,6 @@ import akka.http.scaladsl.server.ValidationRejection
|
|||
import akka.http.scaladsl.testkit.ScalatestRouteTest
|
||||
import org.bitcoins.chain.api.ChainApi
|
||||
import org.bitcoins.core.Core
|
||||
import org.bitcoins.core.api.CoreApi
|
||||
import org.bitcoins.core.crypto.DoubleSha256DigestBE
|
||||
import org.bitcoins.core.currency.{Bitcoins, CurrencyUnit}
|
||||
import org.bitcoins.core.protocol.BitcoinAddress
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package org.bitcoins.core
|
||||
|
||||
import scala.math.Ordering
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
// We extend AnyVal to avoid runtime allocation of new
|
||||
// objects. See the Scala documentation on value classes
|
||||
|
@ -42,4 +43,41 @@ package object currency {
|
|||
new Ordering[CurrencyUnit] {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
extends BitcoinFeeUnit {
|
||||
|
||||
|
@ -55,6 +59,7 @@ case class SatoshisPerVirtualByte(currencyUnit: CurrencyUnit)
|
|||
extends BitcoinFeeUnit
|
||||
|
||||
object SatoshisPerVirtualByte {
|
||||
|
||||
val zero: SatoshisPerVirtualByte = SatoshisPerVirtualByte(CurrencyUnits.zero)
|
||||
val one: SatoshisPerVirtualByte = SatoshisPerVirtualByte(Satoshis.one)
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ class DbManagementTest extends BitcoinSUnitTest {
|
|||
val walletAppConfig = WalletAppConfig(BitcoinSTestAppConfig.tmpDir(),
|
||||
dbConfig(ProjectType.Wallet))
|
||||
val result = WalletDbManagement.migrate(walletAppConfig)
|
||||
assert(result == 2)
|
||||
assert(result == 4)
|
||||
}
|
||||
|
||||
it must "run migrations for node db" in {
|
||||
|
|
|
@ -14,6 +14,7 @@ import org.bitcoins.core.protocol.transaction.{
|
|||
}
|
||||
import org.bitcoins.core.script.ScriptType
|
||||
import org.bitcoins.core.serializers.script.RawScriptWitnessParser
|
||||
import org.bitcoins.core.wallet.fee.SatoshisPerByte
|
||||
import org.bitcoins.core.wallet.utxo.TxoState
|
||||
import scodec.bits.ByteVector
|
||||
import slick.jdbc.GetResult
|
||||
|
@ -166,6 +167,11 @@ abstract class DbCommonsColumnMappers {
|
|||
MappedColumnType
|
||||
.base[TxoState, String](_.toString, TxoState.fromString(_).get)
|
||||
}
|
||||
|
||||
implicit val satoshisPerByteMapper: BaseColumnType[SatoshisPerByte] = {
|
||||
MappedColumnType
|
||||
.base[SatoshisPerByte, Long](_.toLong, SatoshisPerByte.fromLong)
|
||||
}
|
||||
}
|
||||
|
||||
object DbCommonsColumnMappers extends DbCommonsColumnMappers
|
||||
|
|
|
@ -10,7 +10,10 @@ import org.bitcoins.wallet.db.WalletDbManagement
|
|||
case class WalletDAOs(
|
||||
accountDAO: AccountDAO,
|
||||
addressDAO: AddressDAO,
|
||||
utxoDAO: SpendingInfoDAO)
|
||||
utxoDAO: SpendingInfoDAO,
|
||||
transactionDAO: TransactionDAO,
|
||||
incomingTxDAO: IncomingTransactionDAO,
|
||||
outgoingTxDAO: OutgoingTransactionDAO)
|
||||
|
||||
trait WalletDAOFixture extends FixtureAsyncFlatSpec with BitcoinSWalletTest {
|
||||
|
||||
|
@ -18,7 +21,10 @@ trait WalletDAOFixture extends FixtureAsyncFlatSpec with BitcoinSWalletTest {
|
|||
val account = AccountDAO()
|
||||
val address = AddressDAO()
|
||||
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
|
||||
|
|
|
@ -4,12 +4,15 @@ import org.bitcoins.core.config.RegTest
|
|||
import org.bitcoins.core.crypto._
|
||||
import org.bitcoins.core.currency._
|
||||
import org.bitcoins.core.hd._
|
||||
import org.bitcoins.core.number.UInt32
|
||||
import org.bitcoins.core.protocol.blockchain.{
|
||||
ChainParams,
|
||||
RegTestNetChainParams
|
||||
}
|
||||
import org.bitcoins.core.protocol.script._
|
||||
import org.bitcoins.core.protocol.transaction.{
|
||||
EmptyTransaction,
|
||||
Transaction,
|
||||
TransactionOutPoint,
|
||||
TransactionOutput
|
||||
}
|
||||
|
@ -31,6 +34,9 @@ object WalletTestUtil {
|
|||
|
||||
val hdCoinType: HDCoinType = HDCoinType.Testnet
|
||||
|
||||
lazy val sampleTransaction: Transaction = Transaction(
|
||||
"020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000")
|
||||
|
||||
/**
|
||||
* Useful if you want wallet test runs
|
||||
* To use the same key values each time
|
||||
|
@ -168,6 +174,26 @@ object WalletTestUtil {
|
|||
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 */
|
||||
def getNestedSegwitAddressDb(account: AccountDb): AddressDb = {
|
||||
val path = NestedSegWitHDPath(WalletTestUtil.hdCoinType,
|
||||
|
@ -195,8 +221,10 @@ object WalletTestUtil {
|
|||
for {
|
||||
account <- daos.accountDAO.create(WalletTestUtil.firstAccountDb)
|
||||
addr <- daos.addressDAO.create(getAddressDb(account))
|
||||
utxo <- daos.utxoDAO.create(sampleLegacyUTXO(addr.scriptPubKey))
|
||||
} yield utxo.asInstanceOf[LegacySpendingInfo]
|
||||
utxo = sampleLegacyUTXO(addr.scriptPubKey)
|
||||
_ <- insertDummyIncomingTransaction(daos, utxo)
|
||||
utxoDb <- daos.utxoDAO.create(utxo)
|
||||
} yield utxoDb.asInstanceOf[LegacySpendingInfo]
|
||||
}
|
||||
|
||||
/** Inserts an account, address and finally a UTXO */
|
||||
|
@ -205,8 +233,10 @@ object WalletTestUtil {
|
|||
for {
|
||||
account <- daos.accountDAO.create(WalletTestUtil.firstAccountDb)
|
||||
addr <- daos.addressDAO.create(getAddressDb(account))
|
||||
utxo <- daos.utxoDAO.create(sampleSegwitUTXO(addr.scriptPubKey))
|
||||
} yield utxo.asInstanceOf[SegwitV0SpendingInfo]
|
||||
utxo = sampleSegwitUTXO(addr.scriptPubKey)
|
||||
_ <- insertDummyIncomingTransaction(daos, utxo)
|
||||
utxoDb <- daos.utxoDAO.create(utxo)
|
||||
} yield utxoDb.asInstanceOf[SegwitV0SpendingInfo]
|
||||
}
|
||||
|
||||
/** Inserts an account, address and finally a UTXO */
|
||||
|
@ -215,7 +245,9 @@ object WalletTestUtil {
|
|||
for {
|
||||
account <- daos.accountDAO.create(WalletTestUtil.nestedSegWitAccountDb)
|
||||
addr <- daos.addressDAO.create(getNestedSegwitAddressDb(account))
|
||||
utxo <- daos.utxoDAO.create(sampleNestedSegwitUTXO(addr.ecPublicKey))
|
||||
} yield utxo.asInstanceOf[NestedSegwitV0SpendingInfo]
|
||||
utxo = sampleNestedSegwitUTXO(addr.ecPublicKey)
|
||||
_ <- insertDummyIncomingTransaction(daos, utxo)
|
||||
utxoDb <- daos.utxoDAO.create(utxo)
|
||||
} yield utxoDb.asInstanceOf[NestedSegwitV0SpendingInfo]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import org.bitcoins.core.currency.Satoshis
|
|||
import org.bitcoins.core.protocol.BitcoinAddress
|
||||
import org.bitcoins.core.protocol.script.EmptyScriptPubKey
|
||||
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.testkit.wallet.BitcoinSWalletTest
|
||||
import org.bitcoins.testkit.wallet.BitcoinSWalletTest.{
|
||||
|
@ -32,7 +32,7 @@ class UTXOLifeCycleTest extends BitcoinSWalletTest {
|
|||
for {
|
||||
tx <- wallet.sendToAddress(testAddr,
|
||||
Satoshis(3000),
|
||||
SatoshisPerVirtualByte(Satoshis(3)))
|
||||
SatoshisPerByte(Satoshis(3)))
|
||||
|
||||
updatedCoins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx)
|
||||
} yield {
|
||||
|
@ -48,7 +48,11 @@ class UTXOLifeCycleTest extends BitcoinSWalletTest {
|
|||
|
||||
txId <- bitcoind.sendToAddress(addr, Satoshis(3000))
|
||||
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(
|
||||
addr.scriptPubKey)
|
||||
|
|
|
@ -19,9 +19,9 @@ class WalletIntegrationTest extends BitcoinSWalletTest {
|
|||
|
||||
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(
|
||||
first: CurrencyUnit,
|
||||
second: CurrencyUnit,
|
||||
|
@ -65,15 +65,15 @@ class WalletIntegrationTest extends BitcoinSWalletTest {
|
|||
// it should not be confirmed
|
||||
utxosPostAdd <- wallet.listUtxos()
|
||||
_ = assert(utxosPostAdd.length == 1)
|
||||
_ <- wallet
|
||||
.getConfirmedBalance()
|
||||
.map(confirmed => assert(confirmed == 0.bitcoin))
|
||||
_ <- wallet
|
||||
.getConfirmedBalance()
|
||||
.map(confirmed => assert(confirmed == 0.bitcoin))
|
||||
_ <- wallet
|
||||
.getUnconfirmedBalance()
|
||||
.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, _))
|
||||
rawTx <- bitcoind.getRawTransaction(txId)
|
||||
|
@ -103,12 +103,27 @@ class WalletIntegrationTest extends BitcoinSWalletTest {
|
|||
_ <- bitcoind.getNewAddress.flatMap(bitcoind.generateToAddress(1, _))
|
||||
tx <- bitcoind.getRawTransaction(txid)
|
||||
|
||||
_ <- wallet.listUtxos().map {
|
||||
utxos <- wallet.listUtxos()
|
||||
_ = utxos match {
|
||||
case utxo +: Vector() =>
|
||||
assert(utxo.privKeyPath.chain.chainType == HDChainType.Change)
|
||||
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()
|
||||
_ = {
|
||||
// change UTXO should be smaller than what we had, but still have money in it
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@ import org.bitcoins.core.protocol.transaction.{
|
|||
import org.bitcoins.core.wallet.utxo.TxoState
|
||||
import org.bitcoins.testkit.Implicits._
|
||||
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}
|
||||
|
||||
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 =>
|
||||
val WalletDAOs(_, _, utxoDAO) = daos
|
||||
val utxoDAO = daos.utxoDAO
|
||||
|
||||
for {
|
||||
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 =>
|
||||
val WalletDAOs(_, _, spendingInfoDAO) = daos
|
||||
val spendingInfoDAO = daos.utxoDAO
|
||||
for {
|
||||
utxo <- WalletTestUtil.insertSegWitUTXO(daos)
|
||||
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 =>
|
||||
val WalletDAOs(_, _, spendingInfoDAO) = daos
|
||||
val spendingInfoDAO = daos.utxoDAO
|
||||
for {
|
||||
utxo <- WalletTestUtil.insertLegacyUTXO(daos)
|
||||
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 =>
|
||||
val WalletDAOs(_, _, spendingInfoDAO) = daos
|
||||
val spendingInfoDAO = daos.utxoDAO
|
||||
for {
|
||||
utxo <- WalletTestUtil.insertLegacyUTXO(daos)
|
||||
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 =>
|
||||
val WalletDAOs(_, _, spendingInfoDAO) = daos
|
||||
val spendingInfoDAO = daos.utxoDAO
|
||||
|
||||
for {
|
||||
utxo <- WalletTestUtil.insertLegacyUTXO(daos)
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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";
|
|
@ -46,7 +46,11 @@ sealed abstract class Wallet extends LockedWallet with UnlockedWalletApi {
|
|||
markAsReserved = false)
|
||||
signed <- txBuilder.sign
|
||||
ourOuts <- findOurOuts(signed)
|
||||
_ <- processOurTransaction(signed, blockHashOpt = None)
|
||||
_ <- processOurTransaction(transaction = signed,
|
||||
feeRate = feeRate,
|
||||
inputAmount = txBuilder.creditingAmount,
|
||||
sentAmount = txBuilder.destinationAmount,
|
||||
blockHashOpt = None)
|
||||
} yield {
|
||||
logger.debug(
|
||||
s"Signed transaction=${signed.txIdBE.hex} with outputs=${signed.outputs.length}, inputs=${signed.inputs.length}")
|
||||
|
|
|
@ -8,9 +8,17 @@ sealed abstract class WalletDbManagement extends DbManagement {
|
|||
private val accountTable = TableQuery[AccountTable]
|
||||
private val addressTable = TableQuery[AddressTable]
|
||||
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[_]]] =
|
||||
List(accountTable, addressTable, utxoTable)
|
||||
List(accountTable,
|
||||
addressTable,
|
||||
utxoTable,
|
||||
txTable,
|
||||
incomingTxTable,
|
||||
outgoingTxTable)
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
package org.bitcoins.wallet.internal
|
||||
|
||||
import org.bitcoins.core.crypto.{DoubleSha256Digest, DoubleSha256DigestBE}
|
||||
import org.bitcoins.core.currency.CurrencyUnit
|
||||
import org.bitcoins.core.number.UInt32
|
||||
import org.bitcoins.core.protocol.blockchain.Block
|
||||
import org.bitcoins.core.protocol.transaction.{Transaction, TransactionOutput}
|
||||
import org.bitcoins.core.util.FutureUtil
|
||||
import org.bitcoins.core.wallet.fee.FeeUnit
|
||||
import org.bitcoins.core.wallet.utxo.TxoState
|
||||
import org.bitcoins.wallet._
|
||||
import org.bitcoins.wallet.api.{AddUtxoError, AddUtxoSuccess}
|
||||
|
@ -68,6 +70,23 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
|
|||
/////////////////////
|
||||
// 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.
|
||||
* This is called right after we've signed a TX,
|
||||
|
@ -75,10 +94,19 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
|
|||
*/
|
||||
private[wallet] def processOurTransaction(
|
||||
transaction: Transaction,
|
||||
feeRate: FeeUnit,
|
||||
inputAmount: CurrencyUnit,
|
||||
sentAmount: CurrencyUnit,
|
||||
blockHashOpt: Option[DoubleSha256DigestBE]): Future[ProcessTxResult] = {
|
||||
logger.info(
|
||||
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 changeOutputs = result.updatedIncoming.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
|
||||
*
|
||||
|
@ -326,10 +384,11 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
|
|||
s"Found no outputs relevant to us in transaction${transaction.txIdBE}")
|
||||
Future.successful(Vector.empty)
|
||||
|
||||
case xs =>
|
||||
val count = xs.length
|
||||
case outputsWithIndex =>
|
||||
val count = outputsWithIndex.length
|
||||
val outputStr = {
|
||||
xs.map { elem =>
|
||||
outputsWithIndex
|
||||
.map { elem =>
|
||||
s"${transaction.txIdBE.hex}:${elem.index}"
|
||||
}
|
||||
.mkString(", ")
|
||||
|
@ -337,24 +396,12 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
|
|||
logger.trace(
|
||||
s"Found $count relevant output(s) in transaction=${transaction.txIdBE}: $outputStr")
|
||||
|
||||
val addUTXOsFut: Future[Seq[SpendingInfoDb]] =
|
||||
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
|
||||
val totalIncoming = outputsWithIndex.map(_.output.value).sum
|
||||
|
||||
for {
|
||||
_ <- insertIncomingTransaction(transaction, totalIncoming)
|
||||
utxos <- addUTXOsFut(outputsWithIndex, transaction, blockHashOpt)
|
||||
} yield utxos
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,7 +60,7 @@ private[wallet] trait UtxoHandling extends WalletLogger {
|
|||
|
||||
/** Constructs a DB level representation of the given UTXO, and persist it to disk */
|
||||
private def writeUtxo(
|
||||
txid: DoubleSha256DigestBE,
|
||||
tx: Transaction,
|
||||
state: TxoState,
|
||||
output: TransactionOutput,
|
||||
outPoint: TransactionOutPoint,
|
||||
|
@ -71,7 +71,7 @@ private[wallet] trait UtxoHandling extends WalletLogger {
|
|||
case segwitAddr: SegWitAddressDb =>
|
||||
SegwitV0SpendingInfo(
|
||||
state = state,
|
||||
txid = txid,
|
||||
txid = tx.txIdBE,
|
||||
outPoint = outPoint,
|
||||
output = output,
|
||||
privKeyPath = segwitAddr.path,
|
||||
|
@ -80,7 +80,7 @@ private[wallet] trait UtxoHandling extends WalletLogger {
|
|||
)
|
||||
case LegacyAddressDb(path, _, _, _, _) =>
|
||||
LegacySpendingInfo(state = state,
|
||||
txid = txid,
|
||||
txid = tx.txIdBE,
|
||||
outPoint = outPoint,
|
||||
output = output,
|
||||
privKeyPath = path,
|
||||
|
@ -92,14 +92,16 @@ private[wallet] trait UtxoHandling extends WalletLogger {
|
|||
privKeyPath = nested.path,
|
||||
redeemScript = P2WPKHWitnessSPKV0(nested.ecPublicKey),
|
||||
scriptWitness = P2WPKHWitnessV0(nested.ecPublicKey),
|
||||
txid = txid,
|
||||
txid = tx.txIdBE,
|
||||
state = state,
|
||||
id = None,
|
||||
blockHash = blockHash
|
||||
)
|
||||
}
|
||||
|
||||
spendingInfoDAO.create(utxo).map { written =>
|
||||
for {
|
||||
written <- spendingInfoDAO.create(utxo)
|
||||
} yield {
|
||||
val writtenOut = written.outPoint
|
||||
logger.info(
|
||||
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 =>
|
||||
val biasedE: CompatEither[AddUtxoError, Future[SpendingInfoDb]] = for {
|
||||
addressDb <- addressDbE
|
||||
} yield writeUtxo(txid = transaction.txIdBE,
|
||||
} yield writeUtxo(tx = transaction,
|
||||
state = state,
|
||||
output = output,
|
||||
outPoint = outPoint,
|
||||
|
|
|
@ -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]
|
||||
}
|
|
@ -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]
|
||||
}
|
|
@ -230,6 +230,14 @@ case class SpendingInfoTable(tag: Tag)
|
|||
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 = (
|
||||
Option[Long], // ID
|
||||
TransactionOutPoint,
|
||||
|
|
|
@ -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]
|
||||
}
|
|
@ -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)
|
||||
|
||||
}
|
Loading…
Add table
Reference in a new issue