diff --git a/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala b/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala index 9f9c4baff5..9ffa787216 100644 --- a/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala +++ b/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala @@ -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 diff --git a/core/src/main/scala/org/bitcoins/core/currency/package.scala b/core/src/main/scala/org/bitcoins/core/currency/package.scala index 0c46468feb..ae41ad99ff 100644 --- a/core/src/main/scala/org/bitcoins/core/currency/package.scala +++ b/core/src/main/scala/org/bitcoins/core/currency/package.scala @@ -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 + } + } + } + } } diff --git a/core/src/main/scala/org/bitcoins/core/wallet/fee/FeeUnit.scala b/core/src/main/scala/org/bitcoins/core/wallet/fee/FeeUnit.scala index f3c3f1949e..877300b792 100644 --- a/core/src/main/scala/org/bitcoins/core/wallet/fee/FeeUnit.scala +++ b/core/src/main/scala/org/bitcoins/core/wallet/fee/FeeUnit.scala @@ -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) } diff --git a/db-commons-test/src/test/scala/org/bitcoins/db/DbManagementTest.scala b/db-commons-test/src/test/scala/org/bitcoins/db/DbManagementTest.scala index d40e52525e..d11bdc8a51 100644 --- a/db-commons-test/src/test/scala/org/bitcoins/db/DbManagementTest.scala +++ b/db-commons-test/src/test/scala/org/bitcoins/db/DbManagementTest.scala @@ -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 { diff --git a/db-commons/src/main/scala/org/bitcoins/db/DbCommonsColumnMappers.scala b/db-commons/src/main/scala/org/bitcoins/db/DbCommonsColumnMappers.scala index ba4b9aacdf..ca13b977f0 100644 --- a/db-commons/src/main/scala/org/bitcoins/db/DbCommonsColumnMappers.scala +++ b/db-commons/src/main/scala/org/bitcoins/db/DbCommonsColumnMappers.scala @@ -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 diff --git a/testkit/src/main/scala/org/bitcoins/testkit/fixtures/WalletDAOFixture.scala b/testkit/src/main/scala/org/bitcoins/testkit/fixtures/WalletDAOFixture.scala index 8bd8d8b65b..ee940326b6 100644 --- a/testkit/src/main/scala/org/bitcoins/testkit/fixtures/WalletDAOFixture.scala +++ b/testkit/src/main/scala/org/bitcoins/testkit/fixtures/WalletDAOFixture.scala @@ -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 diff --git a/testkit/src/main/scala/org/bitcoins/testkit/wallet/WalletTestUtil.scala b/testkit/src/main/scala/org/bitcoins/testkit/wallet/WalletTestUtil.scala index bfa9ee3249..9024e68cd0 100644 --- a/testkit/src/main/scala/org/bitcoins/testkit/wallet/WalletTestUtil.scala +++ b/testkit/src/main/scala/org/bitcoins/testkit/wallet/WalletTestUtil.scala @@ -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] } } diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/UTXOLifeCycleTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/UTXOLifeCycleTest.scala index ba4d06f8f3..95e5eb8640 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/UTXOLifeCycleTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/UTXOLifeCycleTest.scala @@ -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) diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/WalletIntegrationTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/WalletIntegrationTest.scala index 6e90c57031..6328c62313 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/WalletIntegrationTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/WalletIntegrationTest.scala @@ -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 diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/models/IncomingTransactionDAOTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/models/IncomingTransactionDAOTest.scala new file mode 100644 index 0000000000..eeed5a372c --- /dev/null +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/models/IncomingTransactionDAOTest.scala @@ -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)) + } +} diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/models/OutgoingTransactionDAOTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/models/OutgoingTransactionDAOTest.scala new file mode 100644 index 0000000000..148c5b4a9f --- /dev/null +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/models/OutgoingTransactionDAOTest.scala @@ -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)) + } +} diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/models/SpendingInfoDAOTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/models/SpendingInfoDAOTest.scala index 7c70cf3690..468a7ff78f 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/models/SpendingInfoDAOTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/models/SpendingInfoDAOTest.scala @@ -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) diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/models/TransactionDAOTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/models/TransactionDAOTest.scala new file mode 100644 index 0000000000..43218fc1e5 --- /dev/null +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/models/TransactionDAOTest.scala @@ -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)) + } +} diff --git a/wallet/src/main/resources/walletdb/migration/V4__wallet_db_add_transaction_table.sql b/wallet/src/main/resources/walletdb/migration/V4__wallet_db_add_transaction_table.sql new file mode 100644 index 0000000000..9a27664d75 --- /dev/null +++ b/wallet/src/main/resources/walletdb/migration/V4__wallet_db_add_transaction_table.sql @@ -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"; \ No newline at end of file diff --git a/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala b/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala index df085bd009..d804106e87 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala @@ -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}") diff --git a/wallet/src/main/scala/org/bitcoins/wallet/db/WalletDbManagement.scala b/wallet/src/main/scala/org/bitcoins/wallet/db/WalletDbManagement.scala index a55d9d6882..bb70e94177 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/db/WalletDbManagement.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/db/WalletDbManagement.scala @@ -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) } diff --git a/wallet/src/main/scala/org/bitcoins/wallet/internal/TransactionProcessing.scala b/wallet/src/main/scala/org/bitcoins/wallet/internal/TransactionProcessing.scala index 59ad7bedb6..2f5222dcd5 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/internal/TransactionProcessing.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/internal/TransactionProcessing.scala @@ -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 } } } diff --git a/wallet/src/main/scala/org/bitcoins/wallet/internal/UtxoHandling.scala b/wallet/src/main/scala/org/bitcoins/wallet/internal/UtxoHandling.scala index 33ab2c4af8..e7b6013461 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/internal/UtxoHandling.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/internal/UtxoHandling.scala @@ -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, diff --git a/wallet/src/main/scala/org/bitcoins/wallet/models/IncomingTransactionDAO.scala b/wallet/src/main/scala/org/bitcoins/wallet/models/IncomingTransactionDAO.scala new file mode 100644 index 0000000000..0059588262 --- /dev/null +++ b/wallet/src/main/scala/org/bitcoins/wallet/models/IncomingTransactionDAO.scala @@ -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] +} diff --git a/wallet/src/main/scala/org/bitcoins/wallet/models/OutgoingTransactionDAO.scala b/wallet/src/main/scala/org/bitcoins/wallet/models/OutgoingTransactionDAO.scala new file mode 100644 index 0000000000..8b8564813c --- /dev/null +++ b/wallet/src/main/scala/org/bitcoins/wallet/models/OutgoingTransactionDAO.scala @@ -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] +} diff --git a/wallet/src/main/scala/org/bitcoins/wallet/models/SpendingInfoTable.scala b/wallet/src/main/scala/org/bitcoins/wallet/models/SpendingInfoTable.scala index 9ba74f2bf7..cef8ebf278 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/models/SpendingInfoTable.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/models/SpendingInfoTable.scala @@ -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, diff --git a/wallet/src/main/scala/org/bitcoins/wallet/models/TransactionDAO.scala b/wallet/src/main/scala/org/bitcoins/wallet/models/TransactionDAO.scala new file mode 100644 index 0000000000..40c97d10ba --- /dev/null +++ b/wallet/src/main/scala/org/bitcoins/wallet/models/TransactionDAO.scala @@ -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] +} diff --git a/wallet/src/main/scala/org/bitcoins/wallet/models/TransactionTable.scala b/wallet/src/main/scala/org/bitcoins/wallet/models/TransactionTable.scala new file mode 100644 index 0000000000..72759d28d1 --- /dev/null +++ b/wallet/src/main/scala/org/bitcoins/wallet/models/TransactionTable.scala @@ -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) + +}