mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2024-11-19 09:52:09 +01:00
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:
parent
454808abea
commit
86517c0a6d
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
@ -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 { _ =>
|
||||
???
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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]]
|
||||
|
||||
|
@ -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)
|
||||
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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]
|
||||
}
|
@ -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]
|
||||
}
|
@ -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)
|
||||
}
|
@ -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)
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user