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 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

View file

@ -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
}
}
}
}
}

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)
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)
}

View file

@ -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 {

View file

@ -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

View file

@ -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

View file

@ -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]
}
}

View file

@ -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)

View file

@ -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

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.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)

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)
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}")

View file

@ -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)
}

View file

@ -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
}
}
}

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 */
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,

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)
}
/** 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,

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)
}