Replace incoming/outgoing TXs with TXOs

Rework the wallet DB model so that instead
of dealing with incoming and outgoing transactions
we deal with incoming and outgoing transaction
outputs.
This commit is contained in:
Torkel Rogstad 2019-06-26 16:56:12 +02:00
parent 454808abea
commit 86517c0a6d
13 changed files with 198 additions and 288 deletions

View File

@ -10,19 +10,19 @@ import slick.jdbc.SQLiteProfile
case class WalletDAOs(
accountDAO: AccountDAO,
addressDAO: AddressDAO,
incomingTxDAO: IncomingTransactionDAO,
outgoingTxDAO: OutgoingTransactionDAO,
utxoDAO: UTXOSpendingInfoDAO)
incomingTxoDAO: IncomingTxoDAO,
outgoingTxoDAO: OutgoingTxoDAO,
utxoDAO: SpendingInfoDAO)
trait WalletDAOFixture extends fixture.AsyncFlatSpec with BitcoinSWalletTest {
private lazy val daos: WalletDAOs = {
val account = AccountDAO()
val address = AddressDAO()
val inTx = IncomingTransactionDAO(SQLiteProfile)
val outTx = OutgoingTransactionDAO(SQLiteProfile)
val utxo = UTXOSpendingInfoDAO()
WalletDAOs(account, address, inTx, outTx, utxo)
val inTxo = IncomingTxoDAO(SQLiteProfile)
val outTxo = OutgoingTxoDAO(SQLiteProfile)
val utxo = SpendingInfoDAO()
WalletDAOs(account, address, inTxo, outTxo, utxo)
}
final override type FixtureParam = WalletDAOs

View File

@ -2,6 +2,7 @@ package org.bitcoins.testkit.wallet
import org.bitcoins.core.config.RegTest
import org.bitcoins.core.crypto._
import org.bitcoins.core.currency._
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.blockchain.{
ChainParams,
@ -17,11 +18,16 @@ import org.bitcoins.core.protocol.script.P2WPKHWitnessV0
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.testkit.core.gen.TransactionGenerators
import scala.concurrent.Future
import org.bitcoins.wallet.models.IncomingTransaction
import org.bitcoins.wallet.models.AddressDb
import org.bitcoins.wallet.models.AddressDbHelper
import org.bitcoins.testkit.fixtures.WalletDAOs
import scala.concurrent.ExecutionContext
import org.bitcoins.wallet.models.IncomingWalletTXO
import org.bitcoins.wallet.models.LegacySpendingInfo
import org.bitcoins.core.protocol.transaction.TransactionOutPoint
import org.bitcoins.core.protocol.transaction.TransactionOutput
import org.bitcoins.wallet.models.SegwitV0SpendingInfo
import org.bitcoins.wallet.models.SpendingInfoDb
object WalletTestUtil {
@ -75,15 +81,40 @@ object WalletTestUtil {
lazy val sampleScriptWitness: ScriptWitness = P2WPKHWitnessV0(freshXpub.key)
lazy val sampleSegwitUTXO: SegwitV0SpendingInfo = {
val outpoint =
TransactionOutPoint(WalletTestUtil.sampleTxid, WalletTestUtil.sampleVout)
val output = TransactionOutput(1.bitcoin, WalletTestUtil.sampleSPK)
val scriptWitness = WalletTestUtil.sampleScriptWitness
val privkeyPath = WalletTestUtil.sampleSegwitPath
SegwitV0SpendingInfo(outPoint = outpoint,
output = output,
privKeyPath = privkeyPath,
scriptWitness = scriptWitness)
}
lazy val sampleLegacyUTXO: LegacySpendingInfo = {
val outpoint =
TransactionOutPoint(WalletTestUtil.sampleTxid, WalletTestUtil.sampleVout)
val output = TransactionOutput(1.bitcoin, WalletTestUtil.sampleSPK)
val privKeyPath = WalletTestUtil.sampleLegacyPath
LegacySpendingInfo(outPoint = outpoint,
output = output,
privKeyPath = privKeyPath)
}
/**
* Inserts a incoming TX, and returns it with the address it was sent to
* Inserts a incoming TXO, and returns it with the address it was sent to
*
* This method also does some asserts on the result, to make sure what
* we're writing and reading matches up
*/
def insertIncomingTx(daos: WalletDAOs)(implicit ec: ExecutionContext): Future[
(IncomingTransaction, AddressDb)] = {
val WalletDAOs(accountDAO, addressDAO, txDAO, _, _) = daos
def insertIncomingTxo(daos: WalletDAOs, utxo: SpendingInfoDb)(
implicit ec: ExecutionContext): Future[(IncomingWalletTXO, AddressDb)] = {
require(utxo.id.isDefined)
val WalletDAOs(accountDAO, addressDAO, txoDAO, _, utxoDAO) = daos
/** Get a TX with outputs */
def getTx: Transaction =
@ -106,30 +137,31 @@ object WalletTestUtil {
}
val tx = getTx
val txDb = IncomingTransaction(tx,
confirmations = 3,
scriptPubKey = address.scriptPubKey,
voutIndex = 0)
val txoDb = IncomingWalletTXO(confirmations = 3,
txid = tx.txIdBE,
spent = false,
scriptPubKey = address.scriptPubKey,
spendingInfoID = utxo.id.get)
for {
_ <- accountDAO.create(account)
_ <- addressDAO.create(address)
createdTx <- txDAO.create(txDb)
txAndAddr <- txDAO.withAddress(createdTx.transaction)
_ <- utxoDAO.create(utxo)
createdTxo <- txoDAO.create(txoDb)
txAndAddrs <- txoDAO.withAddress(createdTxo.txid)
} yield
txAndAddr match {
case None =>
txAndAddrs match {
case Vector() =>
throw new org.scalatest.exceptions.TestFailedException(
s"Couldn't read back TX with address from DB!",
0)
case Some((foundTx, foundAddr)) =>
assert(foundTx.confirmations == txDb.confirmations)
assert(foundTx.scriptPubKey == txDb.scriptPubKey)
assert(foundTx.transaction == txDb.transaction)
case ((foundTxo, foundAddr)) +: _ =>
assert(foundTxo.confirmations == txoDb.confirmations)
assert(foundTxo.scriptPubKey == txoDb.scriptPubKey)
assert(foundTxo.txid == txoDb.txid)
assert(foundAddr == address)
(foundTx, foundAddr)
(foundTxo, foundAddr)
}
}
}

View File

@ -7,20 +7,23 @@ import scala.concurrent.Future
import org.scalatest.compatible.Assertion
import org.bitcoins.wallet.api.UnlockedWalletApi
import org.bitcoins.rpc.client.common.BitcoindRpcClient
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.testkit.core.gen.TransactionGenerators
import org.bitcoins.core.protocol.script.ScriptPubKey
import scala.annotation.tailrec
class ProcessTransactionTest extends BitcoinSWalletTest {
override type FixtureParam = WalletWithBitcoind
override type FixtureParam = UnlockedWalletApi
def withFixture(test: OneArgAsyncTest): FutureOutcome = {
withNewWalletAndBitcoind(test)
withNewWallet(test)
}
behavior of "Wallet.processTransaction"
/** Verifies that executing the given action doesn't change wallet state */
private def checkUtxosAndBalance(
wallet: UnlockedWalletApi,
bitcoind: BitcoindRpcClient)(action: => Future[_]): Future[Assertion] =
private def checkUtxosAndBalance(wallet: UnlockedWalletApi)(
action: => Future[_]): Future[Assertion] =
for {
oldUtxos <- wallet.listUtxos()
oldUnconfirmed <- wallet.getUnconfirmedBalance()
@ -36,49 +39,56 @@ class ProcessTransactionTest extends BitcoinSWalletTest {
assert(oldUtxos == newUtxos)
}
it must "not change state when processing the same transaction twice" in {
walletAndBitcoind =>
val WalletWithBitcoind(wallet, bitcoind) = walletAndBitcoind
/** Gets a TX which pays to the given SPK */
private def getTxFor(spk: ScriptPubKey): Transaction =
TransactionGenerators
.transactionTo(spk)
.sample
.getOrElse(getTxFor(spk))
private def getUnrelatedTx(): Transaction =
TransactionGenerators.transaction.sample.getOrElse(getUnrelatedTx())
it must "not change state when processing the same transaction twice" in {
wallet =>
for {
address <- wallet.getNewAddress()
tx <- bitcoind
.sendToAddress(address, 1.bitcoin)
.flatMap(bitcoind.getRawTransactionRaw(_))
tx = getTxFor(address.scriptPubKey)
_ = logger.info(s"tx: $tx")
_ <- wallet.processTransaction(tx, confirmations = 0)
oldBalance <- wallet.getBalance()
oldUnconfirmed <- wallet.getUnconfirmedBalance()
// repeating the action should not make a difference
_ <- checkUtxosAndBalance(wallet, bitcoind) {
_ <- checkUtxosAndBalance(wallet) {
wallet.processTransaction(tx, confirmations = 0)
}
_ <- wallet.processTransaction(tx, confirmations = 3)
newBalance <- wallet.getBalance()
newUnconfirmed <- wallet.getUnconfirmedBalance()
utxosPostAdd <- wallet.listUtxos()
// repeating the action should not make a difference
_ <- checkUtxosAndBalance(wallet, bitcoind) {
_ <- checkUtxosAndBalance(wallet) {
wallet.processTransaction(tx, confirmations = 3)
}
} yield {
val ourOutputs =
tx.outputs.filter(_.scriptPubKey == address.scriptPubKey)
assert(utxosPostAdd.length == ourOutputs.length)
assert(newBalance != oldBalance)
assert(newUnconfirmed != oldUnconfirmed)
}
}
it must "not change state when processing an unrelated transaction" in {
walletAndBitcoind =>
val WalletWithBitcoind(wallet, bitcoind) = walletAndBitcoind
wallet =>
val unrelated = getUnrelatedTx()
for {
unrelated <- bitcoind.getNewAddress
.flatMap(bitcoind.sendToAddress(_, 1.bitcoin))
.flatMap(bitcoind.getRawTransactionRaw(_))
_ <- checkUtxosAndBalance(wallet, bitcoind) {
_ <- checkUtxosAndBalance(wallet) {
wallet.processTransaction(unrelated, confirmations = 4)
}

View File

@ -12,19 +12,18 @@ import org.bouncycastle.crypto.tls.CertChainType
import org.bitcoins.core.hd.HDChainType
import org.bitcoins.core.hd.LegacyHDPath
import scala.concurrent.Future
import org.bitcoins.testkit.fixtures.WalletDAOs
class IncomingTransactionDAOTest
extends BitcoinSWalletTest
with WalletDAOFixture {
class IncomingTxoDAOTest extends BitcoinSWalletTest with WalletDAOFixture {
it must "insert a incoming transaction and read it back with its address" in {
daos =>
val txDAO = daos.incomingTxDAO
WalletTestUtil.insertIncomingTx(daos).flatMap {
case (tx, _) =>
txDAO.findTx(tx.transaction).map { txOpt =>
assert(txOpt.contains(tx))
}
}
val WalletDAOs(_, _, txoDAO, _, utxoDAO) = daos
for {
utxo <- utxoDAO.create(WalletTestUtil.sampleLegacyUTXO)
(txo, _) <- WalletTestUtil.insertIncomingTxo(daos, utxo)
foundTxos <- txoDAO.findTx(txo.txid)
} yield assert(foundTxos.contains(txo))
}
}

View File

@ -0,0 +1,36 @@
package org.bitcoins.wallet.models
import org.bitcoins.core.currency._
import org.bitcoins.core.protocol.transaction.{
TransactionOutPoint,
TransactionOutput
}
import org.bitcoins.testkit.fixtures.WalletDAOFixture
import org.bitcoins.wallet.Wallet
import org.bitcoins.testkit.wallet.WalletTestUtil
import org.bitcoins.testkit.wallet.BitcoinSWalletTest
class SpendingInfoDbDAOTest extends BitcoinSWalletTest with WalletDAOFixture {
behavior of "SpendingInfoDAO"
it should "insert a segwit UTXO and read it" in { daos =>
val utxoDAO = daos.utxoDAO
for {
created <- utxoDAO.create(WalletTestUtil.sampleSegwitUTXO)
read <- utxoDAO.read(created.id.get)
} yield assert(read.contains(created))
}
it should "insert a legacy UTXO and read it" in { daos =>
val utxoDAO = daos.utxoDAO
for {
created <- utxoDAO.create(WalletTestUtil.sampleLegacyUTXO)
read <- utxoDAO.read(created.id.get)
} yield assert(read.contains(created))
}
it should "insert a nested segwit UTXO and read it" ignore { _ =>
???
}
}

View File

@ -1,11 +1,11 @@
package org.bitcoins.wallet.api
sealed trait AddUtxoResult {
def flatMap(f: AddUtxoResult => AddUtxoResult) = ???
def map(success: AddUtxoSuccess => AddUtxoResult) = ???
}
import org.bitcoins.wallet.models._
case class AddUtxoSuccess(walletApi: WalletApi) extends AddUtxoResult
sealed trait AddUtxoResult
/** Contains the freshly added UTXO */
case class AddUtxoSuccess(spendingInfo: SpendingInfoDb) extends AddUtxoResult
/** Represents an error that might occur when adding an UTXO to the wallet */
sealed trait AddUtxoError extends Error with AddUtxoResult

View File

@ -9,7 +9,7 @@ import org.bitcoins.core.protocol.blockchain.ChainParams
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.core.wallet.fee.FeeUnit
import org.bitcoins.wallet.HDUtil
import org.bitcoins.wallet.models.{AccountDb, AddressDb, UTXOSpendingInfoDb}
import org.bitcoins.wallet.models.{AccountDb, AddressDb, SpendingInfoDb}
import scala.concurrent.Future
import scala.concurrent.ExecutionContext
@ -66,7 +66,7 @@ trait LockedWalletApi extends WalletApi {
*/
// def updateUtxo: Future[WalletApi]
def listUtxos(): Future[Vector[UTXOSpendingInfoDb]]
def listUtxos(): Future[Vector[SpendingInfoDb]]
def listAddresses(): Future[Vector[AddressDb]]

View File

@ -7,16 +7,16 @@ import org.bitcoins.wallet.models._
sealed abstract class WalletDbManagement extends DbManagement {
private val accountTable = TableQuery[AccountTable]
private val addressTable = TableQuery[AddressTable]
private val utxoTable = TableQuery[UTXOSpendingInfoTable]
private val incomingTxTable = TableQuery[IncomingTransactionTable]
private val outgoingTxTable = TableQuery[OutgoingTransactionTable]
private val utxoTable = TableQuery[SpendingInfoTable]
private val incomingTxoTable = TableQuery[IncomingTXOTable]
private val outgoingTxoTable = TableQuery[OutgoingTXOTable]
override val allTables: List[TableQuery[_ <: Table[_]]] =
List(accountTable,
addressTable,
utxoTable,
incomingTxTable,
outgoingTxTable)
incomingTxoTable,
outgoingTxoTable)
}

View File

@ -1,51 +0,0 @@
package org.bitcoins.wallet.models
import scala.concurrent.ExecutionContext
import org.bitcoins.wallet.config.WalletAppConfig
import org.bitcoins.db.CRUDAutoInc
import slick.jdbc.JdbcProfile
import scala.concurrent.Future
import org.bitcoins.core.protocol.transaction.Transaction
final case class IncomingTransactionDAO(profile: JdbcProfile)(
implicit val ec: ExecutionContext,
val appConfig: WalletAppConfig)
extends CRUDAutoInc[IncomingTransaction] {
import profile.api._
import org.bitcoins.db.DbCommonsColumnMappers._
override val table = TableQuery[IncomingTransactionTable]
val addrTable = TableQuery[AddressTable]
/**
* @param tx The transaction to look for
* @return If found, the DB representation of the given TX,
* along with the address it pays to
*/
def withAddress(
tx: Transaction): Future[Option[(IncomingTransaction, AddressDb)]] = {
withAddress(_ === tx)
}
/**
* @param rep A predicate to filter our incoming TXs on
* @return The first TX that meets the predicate, along with
* the address the transaction pays to
*/
def withAddress(pred: Rep[Transaction] => Rep[Boolean]): Future[
Option[(IncomingTransaction, AddressDb)]] = {
val query = {
val filtered = table.filter(dbTx => pred(dbTx.transaction))
filtered join addrTable on (_.scriptPubKey === _.scriptPubKey)
}
database.run(query.result.headOption)
}
def findTx(tx: Transaction): Future[Option[IncomingTransaction]] = {
val filtered = table.filter(_.transaction === tx)
database.run(filtered.result.headOption)
}
}

View File

@ -1,14 +1,19 @@
package org.bitcoins.wallet.models
import scala.concurrent.ExecutionContext
import slick.jdbc.JdbcProfile
import org.bitcoins.wallet.config.WalletAppConfig
import org.bitcoins.db.CRUDAutoInc
final case class OutgoingTransactionDAO(profile: JdbcProfile)(
import org.bitcoins.db.CRUDAutoInc
import org.bitcoins.wallet.config.WalletAppConfig
import slick.jdbc.JdbcProfile
import scala.concurrent.ExecutionContext
/**
* DAO for outgoing transaction outputs
*/
final case class OutgoingTxoDAO(profile: JdbcProfile)(
implicit val ec: ExecutionContext,
val appConfig: WalletAppConfig)
extends CRUDAutoInc[OutgoingTransaction] {
extends CRUDAutoInc[OutgoingWalletTXO] {
import profile.api._
override val table = TableQuery[OutgoingTransactionTable]
override val table = TableQuery[OutgoingTXOTable]
}

View File

@ -6,11 +6,11 @@ import slick.jdbc.SQLiteProfile.api._
import scala.concurrent.ExecutionContext
case class UTXOSpendingInfoDAO()(
case class SpendingInfoDAO()(
implicit val ec: ExecutionContext,
val appConfig: WalletAppConfig)
extends CRUDAutoInc[UTXOSpendingInfoDb] {
extends CRUDAutoInc[SpendingInfoDb] {
/** The table inside our database we are inserting into */
override val table = TableQuery[UTXOSpendingInfoTable]
override val table = TableQuery[SpendingInfoTable]
}

View File

@ -23,36 +23,37 @@ import org.bitcoins.core.hd.LegacyHDPath
* DB representation of a native V0
* SegWit UTXO
*/
case class NativeV0UTXOSpendingInfoDb(
case class SegwitV0SpendingInfo(
outPoint: TransactionOutPoint,
output: TransactionOutput,
privKeyPath: SegWitHDPath,
scriptWitness: ScriptWitness,
incomingTxId: Long,
id: Option[Long] = None
) extends UTXOSpendingInfoDb {
) extends SpendingInfoDb {
override val redeemScriptOpt: Option[ScriptPubKey] = None
override val scriptWitnessOpt: Option[ScriptWitness] = Some(scriptWitness)
override type PathType = SegWitHDPath
override def copyWithId(id: Long): NativeV0UTXOSpendingInfoDb =
override def copyWithId(id: Long): SegwitV0SpendingInfo =
copy(id = Some(id))
}
case class LegacyUTXOSpendingInfoDb(
/**
* DB representation of a legacy UTXO
*/
case class LegacySpendingInfo(
outPoint: TransactionOutPoint,
output: TransactionOutput,
privKeyPath: LegacyHDPath,
incomingTxId: Long,
id: Option[Long] = None
) extends UTXOSpendingInfoDb {
) extends SpendingInfoDb {
override val redeemScriptOpt: Option[ScriptPubKey] = None
override def scriptWitnessOpt: Option[ScriptWitness] = None
override type PathType = LegacyHDPath
override def copyWithId(id: Long): LegacyUTXOSpendingInfoDb =
override def copyWithId(id: Long): LegacySpendingInfo =
copy(id = Some(id))
}
@ -65,8 +66,8 @@ case class LegacyUTXOSpendingInfoDb(
* we need to derive the private keys, given
* the root wallet seed.
*/
sealed trait UTXOSpendingInfoDb
extends DbRowAutoInc[UTXOSpendingInfoDb]
sealed trait SpendingInfoDb
extends DbRowAutoInc[SpendingInfoDb]
with BitcoinSLogger {
protected type PathType <: HDPath
@ -82,9 +83,6 @@ sealed trait UTXOSpendingInfoDb
def value: CurrencyUnit = output.value
/** The ID of the transaction this UTXO was received in */
def incomingTxId: Long
/** Converts a non-sensitive DB representation of a UTXO into
* a signable (and sensitive) real-world UTXO
*/
@ -116,8 +114,19 @@ sealed trait UTXOSpendingInfoDb
}
case class UTXOSpendingInfoTable(tag: Tag)
extends TableAutoInc[UTXOSpendingInfoDb](tag, "utxos") {
/**
* This table stores the necessary information to spend
* a TXO at a later point in time.
*
* It does not contain informations about whether or not
* it is spent, how many (if any) confirmations it has
* or which block/transaction it was included in.
*
* That is rather handled by
* [[org.bitcoins.wallet.models.WalletTXOTable WalletTXOTable]].
*/
case class SpendingInfoTable(tag: Tag)
extends TableAutoInc[SpendingInfoDb](tag, "txo_spending_info") {
import org.bitcoins.db.DbCommonsColumnMappers._
def outPoint: Rep[TransactionOutPoint] =
@ -134,57 +143,40 @@ case class UTXOSpendingInfoTable(tag: Tag)
def scriptWitnessOpt: Rep[Option[ScriptWitness]] =
column[Option[ScriptWitness]]("script_witness")
/** The ID of the incoming transaction corresponding to this UTXO */
def incomingTxId: Rep[Long] = column("incoming_tx_id")
def fk_incomingTx =
foreignKey("fk_incoming_tx",
sourceColumns = incomingTxId,
targetTableQuery = TableQuery[IncomingTransactionTable]) {
_.id
}
private type UTXOTuple = (
Option[Long],
TransactionOutPoint,
TransactionOutput,
HDPath,
Option[ScriptPubKey],
Option[ScriptWitness],
Long // incoming TX ID
Option[ScriptWitness]
)
private val fromTuple: UTXOTuple => UTXOSpendingInfoDb = {
private val fromTuple: UTXOTuple => SpendingInfoDb = {
case (id,
outpoint,
output,
path: SegWitHDPath,
None, // ReedemScript
Some(scriptWitness),
txId) =>
NativeV0UTXOSpendingInfoDb(outpoint,
output,
path,
scriptWitness,
txId,
id)
Some(scriptWitness)) =>
SegwitV0SpendingInfo(outpoint, output, path, scriptWitness, id)
case (id,
outpoint,
output,
path: LegacyHDPath,
None, // RedeemScript
None, // ScriptWitness
txId) =>
LegacyUTXOSpendingInfoDb(outpoint, output, path, txId, id)
case (id, outpoint, output, path, spkOpt, swOpt, txId) =>
None // ScriptWitness
) =>
LegacySpendingInfo(outpoint, output, path, id)
case (id, outpoint, output, path, spkOpt, swOpt) =>
throw new IllegalArgumentException(
"Could not construct UtxoSpendingInfoDb from bad tuple:"
+ s" ($outpoint, $output, $path, $spkOpt, $swOpt, $txId, $id) . Note: Nested Segwit is not implemented")
+ s" ($outpoint, $output, $path, $spkOpt, $swOpt, $id) . Note: Nested Segwit is not implemented")
}
private val toTuple: UTXOSpendingInfoDb => Option[UTXOTuple] =
private val toTuple: SpendingInfoDb => Option[UTXOTuple] =
utxo =>
Some(
(utxo.id,
@ -192,15 +184,8 @@ case class UTXOSpendingInfoTable(tag: Tag)
utxo.output,
utxo.privKeyPath,
utxo.redeemScriptOpt,
utxo.scriptWitnessOpt,
utxo.incomingTxId))
utxo.scriptWitnessOpt))
def * : ProvenShape[UTXOSpendingInfoDb] =
(id.?,
outPoint,
output,
privKeyPath,
redeemScriptOpt,
scriptWitnessOpt,
incomingTxId) <> (fromTuple, toTuple)
def * : ProvenShape[SpendingInfoDb] =
(id.?, outPoint, output, privKeyPath, redeemScriptOpt, scriptWitnessOpt) <> (fromTuple, toTuple)
}

View File

@ -1,106 +0,0 @@
package org.bitcoins.wallet.models
import org.bitcoins.core.protocol.transaction.Transaction
import slick.jdbc.SQLiteProfile.api._
import slick.lifted.ProvenShape
import org.bitcoins.db.TableAutoInc
import org.bitcoins.core.crypto.DoubleSha256DigestBE
import org.bitcoins.db.DbRowAutoInc
import org.bitcoins.core.protocol.script.ScriptPubKey
import org.bitcoins.core.protocol.transaction.TransactionOutput
/**
* Database representation of transactions
* relevant to our wallet.
*/
sealed trait TransactionDb[T <: TransactionDb[_]] extends DbRowAutoInc[T] {
val transaction: Transaction
lazy val txid: DoubleSha256DigestBE = transaction.txIdBE
val confirmations: Int
}
/** Transactions our wallet has received */
final case class IncomingTransaction(
transaction: Transaction,
scriptPubKey: ScriptPubKey,
voutIndex: Int,
confirmations: Int,
id: Option[Long] = None
) extends TransactionDb[IncomingTransaction] {
require(voutIndex >= 0, s"voutIndex cannot be negative, got $voutIndex")
override def copyWithId(id: Long): IncomingTransaction = copy(id = Some(id))
/** The output we're interested in
*
* TODO: Concerns about TXs paying to multiple SPKs in our wallet, see note below
*/
lazy val output: TransactionOutput = transaction.outputs(voutIndex)
}
/** Transactions our wallet has sent */
final case class OutgoingTransaction(
transaction: Transaction,
confirmations: Int,
id: Option[Long] = None,
utxoId: Option[Long] = None
) extends TransactionDb[OutgoingTransaction] {
override def copyWithId(id: Long): OutgoingTransaction = copy(id = Some(id))
}
sealed abstract class TransactionTable[TxType <: TransactionDb[_]](
tag: Tag,
tableName: String)
extends TableAutoInc[TxType](tag, tableName) {
import org.bitcoins.db.DbCommonsColumnMappers._
def transaction: Rep[Transaction] = column("transaction")
def confirmations: Rep[Int] = column("confirmations")
}
final case class IncomingTransactionTable(tag: Tag)
extends TransactionTable[IncomingTransaction](tag, "incoming_transactions") {
import org.bitcoins.db.DbCommonsColumnMappers._
// TODO: What happens if we get paid to multiple SPKs in the same
// transaction? Need to make a table of SPKs, and map IDs in that
// table to TXs in this table...
/** The SPK that's relevant to us in this transaction. Foreign key into address table */
def scriptPubKey: Rep[ScriptPubKey] = column("our_script_pubkey")
// TODO: The same concerns as above
/** The output of this TX that's ours */
def voutIndex: Rep[Int] = column("vout_index")
def fk_scriptPubKey =
foreignKey("fk_script_pubkey",
sourceColumns = scriptPubKey,
targetTableQuery = TableQuery[AddressTable]) { addressTable =>
addressTable.scriptPubKey
}
override def * : ProvenShape[IncomingTransaction] =
(transaction, scriptPubKey, confirmations, voutIndex, id.?) <> (IncomingTransaction.tupled, IncomingTransaction.unapply)
}
final case class OutgoingTransactionTable(tag: Tag)
extends TransactionTable[OutgoingTransaction](tag, "outgoing_transactions") {
import org.bitcoins.db.DbCommonsColumnMappers._
def utxoId: Rep[Long] = column("utxo_id", O.Unique)
def fk_utxo = {
val utxoTable = TableQuery[UTXOSpendingInfoTable]
foreignKey("fk_utxo", sourceColumns = utxoId, targetTableQuery = utxoTable) {
_.id
}
}
override def * : ProvenShape[OutgoingTransaction] =
(transaction, confirmations, id.?, utxoId.?) <> (OutgoingTransaction.tupled, OutgoingTransaction.unapply)
}