Merge pull request #534 from torkelrogstad/2019-06-17-wallet-transactions

Process incoming transactions
This commit is contained in:
Torkel Rogstad 2019-07-02 11:19:57 +02:00 committed by GitHub
commit d5a7b7aa0f
32 changed files with 642 additions and 154 deletions

View file

@ -29,6 +29,15 @@ class HDPathTest extends BitcoinSUnitTest {
}
}
behavior of "HDChain"
it must "be convertable to an address" in {
forAll(HDGenerators.hdChain, NumberGenerator.positiveInts) { (chain, i) =>
val addr = chain.toHDAddress(i)
assert(addr.chain == chain)
}
}
behavior of "HDAddress"
it must "fail to make addresses with neagtives indices" in {

View file

@ -4,6 +4,7 @@ import org.bitcoins.testkit.core.gen.p2p.DataMessageGenerator
import org.bitcoins.testkit.util.BitcoinSUnitTest
import org.bitcoins.core.crypto.DoubleSha256Digest
import org.bitcoins.testkit.core.gen.CryptoGenerators
import org.bitcoins.testkit.Implicits._
class GetHeadersMessageTest extends BitcoinSUnitTest {
@ -21,12 +22,13 @@ class GetHeadersMessageTest extends BitcoinSUnitTest {
}
it must "be constructable without a stop" in {
def getHash: DoubleSha256Digest =
CryptoGenerators.doubleSha256Digest.sample.getOrElse(getHash)
val msg = GetHeadersMessage(List.fill(10)(getHash))
def getHash(): DoubleSha256Digest =
CryptoGenerators.doubleSha256Digest.sampleSome
val msg = GetHeadersMessage(List.fill(10)(getHash()))
assert(msg.hashStop == DoubleSha256Digest.empty)
val hash = getHash
val hash = getHash()
val otherMsg = GetHeadersMessage(hash)
assert(otherMsg == GetHeadersMessage(Vector(hash)))
}

View file

@ -1,15 +1,13 @@
package org.bitcoins.core.protocol.script
import org.bitcoins.testkit.core.gen.CryptoGenerators
import org.bitcoins.testkit.Implicits._
import org.scalatest.{FlatSpec, MustMatchers}
/**
* Created by chris on 9/22/16.
*/
class P2PKHScriptPubKeyTest extends FlatSpec with MustMatchers {
"P2PKHScriptPubKey" must "return the pubkeyhash" in {
val hash = CryptoGenerators.sha256Hash160Digest.sample.get
val hash = CryptoGenerators.sha256Hash160Digest.sampleSome
val p2pkhScriptPubKey = P2PKHScriptPubKey(hash)
p2pkhScriptPubKey.pubKeyHash must be(hash)
}

View file

@ -1,6 +1,7 @@
package org.bitcoins.core.protocol.script
import org.bitcoins.testkit.core.gen.CryptoGenerators
import org.bitcoins.testkit.Implicits._
import org.bitcoins.core.script.bitwise.OP_EQUALVERIFY
import org.bitcoins.core.script.constant._
import org.bitcoins.core.script.crypto.{OP_CHECKSIG, OP_HASH160}
@ -30,7 +31,7 @@ class ScriptPubKeyTest extends FlatSpec with MustMatchers {
it must "determine if we have a witness program inside of the scriptPubKey" in {
val pubKeyHash =
CryptoUtil.sha256Hash160(CryptoGenerators.publicKey.sample.get.bytes)
CryptoUtil.sha256Hash160(CryptoGenerators.publicKey.sampleSome.bytes)
val witnessProgram = Seq(ScriptConstant(pubKeyHash.bytes))
val asm = OP_0 +: BytesToPushOntoStack(20) +: witnessProgram
val witnessScriptPubKey = WitnessScriptPubKey(asm)

View file

@ -20,6 +20,9 @@ sealed abstract class HDChain extends BIP32Path {
def toInt: Int = chainType.index
/** Given a index, creates a HD address */
def toHDAddress(index: Int): HDAddress = HDAddress(this, index = index)
}
object HDChain {

View file

@ -38,6 +38,11 @@
<logger name="org.bitcoins.node.config" level="INFO"/>
<logger name="org.bitcoins.wallet.config" level="INFO"/>
<!-- inspect table creation, etc -->
<logger name="org.bitcoins.chain.db" level="INFO" />
<logger name="org.bitcoins.node.db" level="INFO" />
<logger name="org.bitcoins.wallet.db" level="INFO" />
<!-- ╔═════════════════╗ -->
<!-- ║ Node module ║ -->
<!-- ╚═════════════════╝ -->

View file

@ -19,6 +19,8 @@ import org.bitcoins.core.hd.HDPurpose
import org.bitcoins.core.hd.HDPurposes
import org.bitcoins.core.hd.SegWitHDPath
import slick.jdbc.GetResult
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.core.currency.Satoshis
abstract class DbCommonsColumnMappers {
@ -159,6 +161,9 @@ abstract class DbCommonsColumnMappers {
MappedColumnType
.base[ScriptType, String](_.toString, ScriptType.fromStringExn)
implicit val txMapper: BaseColumnType[Transaction] =
MappedColumnType.base[Transaction, String](_.hex, Transaction.fromHex)
}
object DbCommonsColumnMappers extends DbCommonsColumnMappers

View file

@ -20,10 +20,22 @@ abstract class DbManagement extends BitcoinSLogger {
def listTables(db: SafeDatabase): Future[Vector[SQLiteTableInfo]] =
listTables(db.config.database)
/** Creates all tables in our table list, in one SQL transaction */
def createAll()(
implicit config: AppConfig,
ec: ExecutionContext): Future[Unit] = {
Future.sequence(allTables.map(createTable(_))).map(_ => ())
ec: ExecutionContext
): Future[Unit] = {
val tables = allTables.map(_.baseTableRow.tableName).mkString(", ")
logger.debug(s"Creating tables: $tables")
val query = {
val querySeq =
allTables.map(createTableQuery(_, createIfNotExists = true))
DBIO.seq(querySeq: _*).transactionally
}
import config.database
database.run(query).map(_ => logger.debug(s"Created tables: $tables"))
}
def dropAll()(
@ -32,6 +44,18 @@ abstract class DbManagement extends BitcoinSLogger {
Future.sequence(allTables.reverse.map(dropTable(_))).map(_ => ())
}
/** The query needed to create the given table */
private def createTableQuery(
table: TableQuery[_ <: Table[_]],
createIfNotExists: Boolean) = {
if (createIfNotExists) {
table.schema.createIfNotExists
} else {
table.schema.create
}
}
/** Creates the given table */
def createTable(
table: TableQuery[_ <: Table[_]],
createIfNotExists: Boolean = true)(
@ -42,11 +66,7 @@ abstract class DbManagement extends BitcoinSLogger {
s"Creating table $tableName with DB config: ${config.dbConfig.config} ")
import config.database
val query = if (createIfNotExists) {
table.schema.createIfNotExists
} else {
table.schema.create
}
val query = createTableQuery(table, createIfNotExists)
database.run(query).map(_ => logger.debug(s"Created table $tableName"))
}

View file

@ -23,6 +23,9 @@ import org.bitcoins.testkit.node.NodeTestUtil
import akka.actor.Cancellable
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.core.crypto.DoubleSha256DigestBE
import scala.util.Try
import scala.util.Failure
import scala.util.Success
class NodeWithWalletTest extends BitcoinSWalletTest {
@ -48,6 +51,8 @@ class NodeWithWalletTest extends BitcoinSWalletTest {
val completionP = Promise[Assertion]
val amountFromBitcoind = 1.bitcoin
val callbacks = {
val onBlock: DataMessageHandler.OnBlockReceived = { block =>
completionP.failure(
@ -58,8 +63,21 @@ class NodeWithWalletTest extends BitcoinSWalletTest {
val onTx: DataMessageHandler.OnTxReceived = { tx =>
if (expectedTxId.contains(tx.txId)) {
logger.debug(s"Cancelling timeout we set earlier")
cancellable.map(_.cancel())
completionP.success(succeed)
for {
prevBalance <- wallet.getUnconfirmedBalance()
_ <- wallet.processTransaction(tx, confirmations = 0)
balance <- wallet.getUnconfirmedBalance()
} {
completionP.complete {
Try {
assert(balance == prevBalance + amountFromBitcoind)
}
}
}
} else if (unexpectedTxId.contains(tx.txId)) {
completionP.failure(
new TestFailedException(
@ -89,6 +107,7 @@ class NodeWithWalletTest extends BitcoinSWalletTest {
}
}
logger.debug(s"Setting timeout for receiving TX in thru node")
cancellable = Some(actorSystem.scheduler.scheduleOnce(delay, runnable))
tx
}
@ -116,7 +135,9 @@ class NodeWithWalletTest extends BitcoinSWalletTest {
_ <- spv.sync()
_ <- NodeTestUtil.awaitSync(spv, rpc)
ourTxid <- rpc.sendToAddress(address, 1.bitcoin).map(processWalletTx)
ourTxid <- rpc
.sendToAddress(address, amountFromBitcoind)
.map(processWalletTx)
notOurTxid <- rpc.getNewAddress
.flatMap(rpc.sendToAddress(_, 1.bitcoin))

View file

@ -0,0 +1,34 @@
package org.bitcoins.testkit
import org.scalacheck.Gen
import scala.annotation.tailrec
/**
* Provides extension methods, syntax
* and other handy implicit values that
* aid in testing.
*/
object Implicits {
/** Extension methods for Scalacheck generatos */
implicit class GeneratorOps[T](private val gen: Gen[T]) extends AnyVal {
/** Gets a sample from this generator that's not `None` */
def sampleSome: T = {
val max = 10
@tailrec
def loop(counter: Int): T =
if (counter > max) {
sys.error(
s"Could not get a sample from generator after $max attempts")
} else {
gen.sample match {
case None => loop(counter + 1)
case Some(sample) => sample
}
}
loop(0)
}
}
}

View file

@ -1,5 +1,6 @@
package org.bitcoins.testkit.core.gen
import org.bitcoins.testkit.Implicits._
import org.bitcoins.core.consensus.Merkle
import org.bitcoins.core.crypto.DoubleSha256Digest
import org.bitcoins.core.number.{Int32, UInt32}
@ -119,7 +120,7 @@ sealed abstract class BlockchainElementsGenerator {
prevBlockHash: DoubleSha256Digest,
nBits: UInt32): BlockHeader = {
//nonce for the unique hash
val nonce = NumberGenerator.uInt32s.sample.get
val nonce = NumberGenerator.uInt32s.sampleSome
BlockHeader(Int32.one,
prevBlockHash,
EmptyTransaction.txId,

View file

@ -1,7 +1,7 @@
package org.bitcoins.testkit.core.gen
import org.bitcoins.core.crypto._
import org.bitcoins.core.currency.{CurrencyUnit, CurrencyUnits, Satoshis}
import org.bitcoins.core.currency._
import org.bitcoins.core.number.{Int32, Int64, UInt32}
import org.bitcoins.core.policy.Policy
import org.bitcoins.core.protocol.script._
@ -14,14 +14,12 @@ import org.bitcoins.core.protocol.transaction.{
import org.bitcoins.core.script.constant.ScriptNumber
import org.bitcoins.core.script.locktime.LockTimeInterpreter
import org.bitcoins.core.util.BitcoinSLogger
import org.bitcoins.testkit.Implicits._
import org.scalacheck.Gen
import scala.annotation.tailrec
/**
* Created by chris on 6/21/16.
*/
trait TransactionGenerators extends BitcoinSLogger {
object TransactionGenerators extends BitcoinSLogger {
/** Responsible for generating [[org.bitcoins.core.protocol.transaction.TransactionOutPoint TransactionOutPoint]] */
def outPoint: Gen[TransactionOutPoint] =
@ -30,6 +28,12 @@ trait TransactionGenerators extends BitcoinSLogger {
vout <- NumberGenerator.uInt32s
} yield TransactionOutPoint(txId, vout)
/** Generates a random TX output paying to the given SPK */
def outputTo(spk: ScriptPubKey): Gen[TransactionOutput] =
for {
satoshis <- CurrencyUnitGenerator.positiveRealistic
} yield TransactionOutput(satoshis, spk)
/** Generates a random [[org.bitcoins.core.protocol.transaction.TransactionOutput TransactionOutput]] */
def output: Gen[TransactionOutput] =
for {
@ -51,13 +55,17 @@ trait TransactionGenerators extends BitcoinSLogger {
def realisticOutputs: Gen[Seq[TransactionOutput]] =
Gen.choose(0, 5).flatMap(n => Gen.listOfN(n, realisticOutput))
/** Generates a small list of TX outputs paying to the given SPK */
def smallOutputsTo(spk: ScriptPubKey): Gen[Seq[TransactionOutput]] =
Gen.choose(1, 5).flatMap(i => Gen.listOfN(i, outputTo(spk)))
/** Generates a small list of [[org.bitcoins.core.protocol.transaction.TransactionOutput TransactionOutput]] */
def smallOutputs: Gen[Seq[TransactionOutput]] =
Gen.choose(0, 5).flatMap(i => Gen.listOfN(i, output))
/** Creates a small sequence of outputs whose total sum is <= totalAmount */
def smallOutputs(totalAmount: CurrencyUnit): Gen[Seq[TransactionOutput]] = {
val numOutputs = Gen.choose(0, 5).sample.get
val numOutputs = Gen.choose(0, 5).sampleSome
@tailrec
def loop(
remaining: Int,
@ -69,8 +77,7 @@ trait TransactionGenerators extends BitcoinSLogger {
val amt = Gen
.choose(100, remainingAmount.toBigDecimal.toLongExact)
.map(n => Satoshis(Int64(n)))
.sample
.get
.sampleSome
loop(remaining - 1, remainingAmount - amt, amt +: accum)
}
}
@ -124,6 +131,14 @@ trait TransactionGenerators extends BitcoinSLogger {
def transaction: Gen[Transaction] =
Gen.oneOf(baseTransaction, witnessTransaction)
/** Generates a transaction where at least one output pays to the given SPK */
def transactionTo(spk: ScriptPubKey) =
Gen.oneOf(baseTransactionTo(spk), witnessTransactionTo(spk))
/** Generates a transaction with at least one output */
def nonEmptyOutputTransaction: Gen[Transaction] =
TransactionGenerators.transaction.suchThat(_.outputs.nonEmpty)
def baseTransaction: Gen[BaseTransaction] =
for {
version <- NumberGenerator.int32s
@ -132,14 +147,23 @@ trait TransactionGenerators extends BitcoinSLogger {
lockTime <- NumberGenerator.uInt32s
} yield BaseTransaction(version, is, os, lockTime)
/** Generates a random [[org.bitcoins.core.protocol.transaction.WitnessTransaction WitnessTransaction]] */
def witnessTransaction: Gen[WitnessTransaction] =
/** Generates a legacy transaction with at least one output paying to the given SPK */
def baseTransactionTo(spk: ScriptPubKey): Gen[BaseTransaction] =
for {
version <- NumberGenerator.int32s
is <- smallInputs
os <- smallOutputsTo(spk)
lockTime <- NumberGenerator.uInt32s
} yield BaseTransaction(version, is, os, lockTime)
/** To avoid duplicating logic */
private def witnessTxHelper(
outputs: Seq[TransactionOutput]): Gen[WitnessTransaction] = {
for {
version <- NumberGenerator.int32s
//we cannot have zero witnesses on a WitnessTx
//https://github.com/bitcoin/bitcoin/blob/e8cfe1ee2d01c493b758a67ad14707dca15792ea/src/primitives/transaction.h#L276-L281
is <- smallInputsNonEmpty
os <- smallOutputs
lockTime <- NumberGenerator.uInt32s
//we have to have atleast one NON `EmptyScriptWitness` for a tx to be a valid WitnessTransaction, otherwise we
//revert to using the `BaseTransaction` serialization format
@ -148,7 +172,23 @@ trait TransactionGenerators extends BitcoinSLogger {
witness <- WitnessGenerators
.transactionWitness(is.size)
.suchThat(_.witnesses.exists(_ != EmptyScriptWitness))
} yield WitnessTransaction(version, is, os, lockTime, witness)
} yield WitnessTransaction(version, is, outputs, lockTime, witness)
}
/** Generates a random [[org.bitcoins.core.protocol.transaction.WitnessTransaction WitnessTransaction]] */
def witnessTransaction: Gen[WitnessTransaction] =
for {
os <- smallOutputs
tx <- witnessTxHelper(os)
} yield tx
/** Generates a SegWit TX where at least one output pays to the given SPK */
def witnessTransactionTo(spk: ScriptPubKey): Gen[WitnessTransaction] = {
for {
os <- smallOutputsTo(spk)
tx <- witnessTxHelper(os)
} yield tx
}
/**
* Creates a [[org.bitcoins.core.crypto.ECPrivateKey ECPrivateKey]], then creates a
@ -769,5 +809,3 @@ trait TransactionGenerators extends BitcoinSLogger {
}
}
}
object TransactionGenerators extends TransactionGenerators

View file

@ -0,0 +1,35 @@
package org.bitcoins.testkit.fixtures
import org.bitcoins.testkit.wallet.BitcoinSWalletTest
import org.scalatest._
import org.bitcoins.wallet.models._
import org.bitcoins.wallet.config.WalletAppConfig
import org.bitcoins.wallet.db.WalletDbManagement
import slick.jdbc.SQLiteProfile
case class WalletDAOs(
accountDAO: AccountDAO,
addressDAO: AddressDAO,
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 inTxo = IncomingTxoDAO(SQLiteProfile)
val outTxo = OutgoingTxoDAO(SQLiteProfile)
val utxo = SpendingInfoDAO()
WalletDAOs(account, address, inTxo, outTxo, utxo)
}
final override type FixtureParam = WalletDAOs
implicit private val walletConfig: WalletAppConfig = config
def withFixture(test: OneArgAsyncTest): FutureOutcome =
makeFixture(build = () => WalletDbManagement.createAll().map(_ => daos),
destroy = () => WalletDbManagement.dropAll())(test)
}

View file

@ -52,7 +52,8 @@ trait BitcoinSWalletTest
def destroyWallet(wallet: UnlockedWalletApi): Future[Unit] = {
WalletDbManagement
.dropAll()(config = config.walletConf, ec = implicitly[ExecutionContext])
.dropAll()(config = wallet.walletConfig,
ec = implicitly[ExecutionContext])
.map(_ => ())
}
@ -112,8 +113,8 @@ trait BitcoinSWalletTest
}
def withNewWallet(test: OneArgAsyncTest): FutureOutcome =
makeDependentFixture(build = createDefaultWallet, destroy = destroyWallet)(
test)
makeDependentFixture(build = createDefaultWallet _,
destroy = destroyWallet)(test)
case class WalletWithBitcoind(
wallet: UnlockedWalletApi,
@ -138,7 +139,7 @@ trait BitcoinSWalletTest
def withNewWalletAndBitcoind(test: OneArgAsyncTest): FutureOutcome = {
val builder: () => Future[WalletWithBitcoind] = composeBuildersAndWrap(
createDefaultWallet,
createDefaultWallet _,
createWalletWithBitcoind,
(_: UnlockedWalletApi, walletWithBitcoind: WalletWithBitcoind) =>
walletWithBitcoind

View file

@ -1,7 +1,9 @@
package org.bitcoins.testkit.wallet
import org.bitcoins.testkit.Implicits._
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,
@ -14,6 +16,18 @@ import scodec.bits.HexStringSyntax
import org.bitcoins.core.hd._
import org.bitcoins.core.protocol.script.ScriptWitness
import org.bitcoins.core.protocol.script.P2WPKHWitnessV0
import org.bitcoins.testkit.core.gen.TransactionGenerators
import scala.concurrent.Future
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 {
@ -53,16 +67,94 @@ object WalletTestUtil {
HDChainType.Change,
addressIndex = 0)
def freshXpub: ExtPublicKey =
CryptoGenerators.extPublicKey.sample.getOrElse(freshXpub)
def freshXpub(): ExtPublicKey =
CryptoGenerators.extPublicKey.sampleSome
val firstAccount = HDAccount(HDCoin(HDPurposes.SegWit, hdCoinType), 0)
def firstAccountDb = AccountDb(freshXpub, firstAccount)
def firstAccountDb = AccountDb(freshXpub(), firstAccount)
lazy val sampleTxid: DoubleSha256Digest = DoubleSha256Digest(
hex"a910523c0b6752fbcb9c24303b4e068c505825d074a45d1c787122efb4649215")
lazy val sampleVout: UInt32 = UInt32.zero
lazy val sampleSPK: ScriptPubKey =
ScriptPubKey.fromAsmBytes(hex"001401b2ac67587e4b603bb3ad709a8102c30113892d")
lazy val sampleScriptWitness: ScriptWitness = P2WPKHWitnessV0(freshXpub.key)
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 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 insertIncomingTxo(daos: WalletDAOs, utxo: SpendingInfoDb)(
implicit ec: ExecutionContext): Future[(IncomingWalletTXO, AddressDb)] = {
require(utxo.id.isDefined)
val WalletDAOs(accountDAO, addressDAO, txoDAO, _, utxoDAO) = daos
val account = WalletTestUtil.firstAccountDb
val address = {
val pub = ECPublicKey()
val path =
account.hdAccount
.toChain(HDChainType.External)
.toHDAddress(0)
.toPath
AddressDbHelper.getAddress(pub, path, RegTest)
}
val tx = TransactionGenerators.nonEmptyOutputTransaction.sampleSome
val txoDb = IncomingWalletTXO(confirmations = 3,
txid = tx.txIdBE,
spent = false,
scriptPubKey = address.scriptPubKey,
spendingInfoID = utxo.id.get)
for {
_ <- accountDAO.create(account)
_ <- addressDAO.create(address)
_ <- utxoDAO.create(utxo)
createdTxo <- txoDAO.create(txoDb)
txAndAddrs <- txoDAO.withAddress(createdTxo.txid)
} yield
txAndAddrs match {
case Vector() =>
throw new org.scalatest.exceptions.TestFailedException(
s"Couldn't read back TX with address from DB!",
0)
case ((foundTxo, foundAddr)) +: _ =>
assert(foundTxo.confirmations == txoDb.confirmations)
assert(foundTxo.scriptPubKey == txoDb.scriptPubKey)
assert(foundTxo.txid == txoDb.txid)
assert(foundAddr == address)
(foundTxo, foundAddr)
}
}
}

View file

@ -1,6 +1,7 @@
package org.bitcoins.testkit.db
import org.bitcoins.testkit.util.BitcoinSUnitTest
import org.bitcoins.testkit.Implicits._
import org.bitcoins.testkit.BitcoinSAppConfig
import org.bitcoins.testkit.BitcoinSAppConfig._
import com.typesafe.config.ConfigFactory
@ -99,7 +100,7 @@ class AppConfigTest extends BitcoinSUnitTest {
_ <- {
val hdAccount =
HDAccount(HDCoin(HDPurposes.Legacy, HDCoinType.Bitcoin), 0)
val xpub = CryptoGenerators.extPublicKey.sample.get
val xpub = CryptoGenerators.extPublicKey.sampleSome
val account = AccountDb(xpub, hdAccount)
accountDAO.create(account)
}

View file

@ -3,6 +3,7 @@ package org.bitcoins.wallet
import org.bitcoins.core.crypto.{AesPassword, MnemonicCode}
import org.bitcoins.testkit.core.gen.CryptoGenerators
import org.bitcoins.testkit.util.BitcoinSUnitTest
import org.bitcoins.testkit.Implicits._
import scala.util.{Failure, Success}
@ -13,10 +14,7 @@ class EncryptedMnemonicTest extends BitcoinSUnitTest {
val password = AesPassword.fromNonEmptyString("good")
val badPassword = AesPassword.fromNonEmptyString("bad")
def getMnemonic(): MnemonicCode =
CryptoGenerators.mnemonicCode.sample.getOrElse(getMnemonic())
val mnemonic = getMnemonic()
val mnemonic = CryptoGenerators.mnemonicCode.sampleSome
val encrypted = EncryptedMnemonicHelper.encrypt(mnemonic, password)
val decrypted = encrypted.toMnemonic(badPassword)

View file

@ -0,0 +1,97 @@
package org.bitcoins.wallet
import org.bitcoins.testkit.wallet.BitcoinSWalletTest
import org.bitcoins.testkit.Implicits._
import org.scalatest.FutureOutcome
import org.bitcoins.core.currency._
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 = UnlockedWalletApi
def withFixture(test: OneArgAsyncTest): FutureOutcome = {
withNewWallet(test)
}
behavior of "Wallet.processTransaction"
/** Verifies that executing the given action doesn't change wallet state */
private def checkUtxosAndBalance(wallet: UnlockedWalletApi)(
action: => Future[_]): Future[Assertion] =
for {
oldUtxos <- wallet.listUtxos()
oldUnconfirmed <- wallet.getUnconfirmedBalance()
oldConfirmed <- wallet.getBalance()
_ <- action // by name
newUtxos <- wallet.listUtxos()
newUnconfirmed <- wallet.getUnconfirmedBalance()
newConfirmed <- wallet.getBalance()
} yield {
assert(oldConfirmed == newConfirmed)
assert(oldUnconfirmed == newUnconfirmed)
assert(oldUtxos == newUtxos)
}
it must "not change state when processing the same transaction twice" in {
wallet =>
for {
address <- wallet.getNewAddress()
tx = TransactionGenerators
.transactionTo(address.scriptPubKey)
.sampleSome
_ = 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) {
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) {
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 {
wallet =>
val unrelated = TransactionGenerators.transaction.sampleSome
for {
_ <- checkUtxosAndBalance(wallet) {
wallet.processTransaction(unrelated, confirmations = 4)
}
balance <- wallet.getBalance()
unconfirmed <- wallet.getUnconfirmedBalance()
} yield {
assert(balance == 0.sats)
assert(unconfirmed == 0.sats)
}
}
}

View file

@ -27,40 +27,45 @@ class WalletIntegrationTest extends BitcoinSWalletTest {
val WalletWithBitcoind(wallet, bitcoind) = walletWithBitcoind
val valueFromBitcoind = Bitcoins.one
val addUtxoF: Future[Unit] = for {
for {
addr <- wallet.getNewAddress()
txid <- bitcoind.sendToAddress(addr, valueFromBitcoind)
_ <- bitcoind.generate(6)
tx <- bitcoind.getRawTransaction(txid)
tx <- bitcoind
.sendToAddress(addr, valueFromBitcoind)
.flatMap(bitcoind.getRawTransactionRaw(_))
addUtxoRes <- {
val voutOpt = tx.vout.find { rpcOut =>
val addressesOpt = rpcOut.scriptPubKey.addresses
addressesOpt.exists(_.contains(addr))
_ <- wallet.listUtxos().map(utxos => assert(utxos.isEmpty))
_ <- wallet.getBalance().map(confirmed => assert(confirmed == 0.bitcoin))
_ <- wallet
.getUnconfirmedBalance()
.map(unconfirmed => assert(unconfirmed == 0.bitcoin))
// after this, tx is unconfirmed in wallet
_ <- wallet.processTransaction(tx, confirmations = 0)
utxosPostAdd <- wallet.listUtxos()
_ = assert(utxosPostAdd.nonEmpty)
_ <- wallet.getBalance().map(confirmed => assert(confirmed == 0.bitcoin))
_ <- wallet
.getUnconfirmedBalance()
.map(unconfirmed => assert(unconfirmed == valueFromBitcoind))
// after this, tx should be confirmed
_ <- wallet.processTransaction(tx, confirmations = 6)
_ <- wallet
.listUtxos()
.map { utxos =>
// we want to make sure no new utxos were added,
// i.e. that we only modified an existing one
assert(utxos.length == utxosPostAdd.length)
}
val vout = voutOpt.getOrElse(
throw new IllegalArgumentException(
"Could not find ouput that spent to our address!"))
wallet.addUtxo(tx.hex, UInt32(vout.n))
}
} yield {
addUtxoRes match {
case err: AddUtxoError => fail(err)
case AddUtxoSuccess(w: WalletApi) => () // continue test
}
}
for {
_ <- addUtxoF
utxos <- wallet.listUtxos()
_ = assert(utxos.nonEmpty)
balance <- wallet.getBalance()
_ = assert(balance > Bitcoins.zero)
_ <- wallet
.getBalance()
.map(confirmed => assert(confirmed == valueFromBitcoind))
_ <- wallet
.getUnconfirmedBalance()
.map(unconfirmed => assert(unconfirmed == 0.bitcoin))
addressFromBitcoind <- bitcoind.getNewAddress
signedTx <- wallet.sendToAddress(addressFromBitcoind,

View file

@ -6,21 +6,19 @@ import org.bitcoins.core.protocol.script.ScriptPubKey
import org.bitcoins.core.protocol.transaction.TransactionOutput
import org.bitcoins.core.wallet.fee.{FeeUnit, SatoshisPerByte}
import org.bitcoins.testkit.core.gen.{TransactionGenerators, WitnessGenerators}
import org.bitcoins.wallet.models.{
NativeV0UTXOSpendingInfoDb,
UTXOSpendingInfoDb
}
import org.bitcoins.testkit.wallet.{BitcoinSWalletTest, WalletTestUtil}
import org.scalatest.FutureOutcome
import org.bitcoins.wallet.models.SpendingInfoDb
import org.bitcoins.wallet.models.SegwitV0SpendingInfo
class CoinSelectorTest extends BitcoinSWalletTest {
case class CoinSelectionFixture(
output: TransactionOutput,
feeRate: FeeUnit,
utxo1: UTXOSpendingInfoDb,
utxo2: UTXOSpendingInfoDb,
utxo3: UTXOSpendingInfoDb) {
val utxoSet: Vector[UTXOSpendingInfoDb] = Vector(utxo1, utxo2, utxo3)
utxo1: SpendingInfoDb,
utxo2: SpendingInfoDb,
utxo3: SpendingInfoDb) {
val utxoSet: Vector[SpendingInfoDb] = Vector(utxo1, utxo2, utxo3)
}
override type FixtureParam = CoinSelectionFixture
@ -29,21 +27,21 @@ class CoinSelectorTest extends BitcoinSWalletTest {
val output = TransactionOutput(99.sats, ScriptPubKey.empty)
val feeRate = SatoshisPerByte(CurrencyUnits.zero)
val utxo1 = NativeV0UTXOSpendingInfoDb(
val utxo1 = SegwitV0SpendingInfo(
id = Some(1),
outPoint = TransactionGenerators.outPoint.sample.get,
output = TransactionOutput(10.sats, ScriptPubKey.empty),
privKeyPath = WalletTestUtil.sampleSegwitPath,
scriptWitness = WitnessGenerators.scriptWitness.sample.get
)
val utxo2 = NativeV0UTXOSpendingInfoDb(
val utxo2 = SegwitV0SpendingInfo(
id = Some(2),
outPoint = TransactionGenerators.outPoint.sample.get,
output = TransactionOutput(90.sats, ScriptPubKey.empty),
privKeyPath = WalletTestUtil.sampleSegwitPath,
scriptWitness = WitnessGenerators.scriptWitness.sample.get
)
val utxo3 = NativeV0UTXOSpendingInfoDb(
val utxo3 = SegwitV0SpendingInfo(
id = Some(3),
outPoint = TransactionGenerators.outPoint.sample.get,
output = TransactionOutput(20.sats, ScriptPubKey.empty),

View file

@ -1,17 +1,19 @@
package org.bitcoins.wallet.models
import org.bitcoins.testkit.core.gen.CryptoGenerators
import org.bitcoins.wallet.fixtures.AccountDAOFixture
import org.bitcoins.testkit.fixtures.WalletDAOFixture
import org.bitcoins.testkit.wallet.{BitcoinSWalletTest, WalletTestUtil}
import org.bitcoins.testkit.Implicits._
class AccountDAOTest extends BitcoinSWalletTest with AccountDAOFixture {
class AccountDAOTest extends BitcoinSWalletTest with WalletDAOFixture {
it should "insert and read an account into the database" in { accountDAO =>
it should "insert and read an account into the database" in { daos =>
val accountDAO = daos.accountDAO
for {
created <- {
val account = WalletTestUtil.firstAccount
val xpub = CryptoGenerators.extPublicKey.sample.get
val xpub = CryptoGenerators.extPublicKey.sampleSome
val accountDb = AccountDb(xpub, account)
accountDAO.create(accountDb)

View file

@ -7,7 +7,7 @@ import org.bitcoins.core.crypto.ECPublicKey
import org.bitcoins.core.protocol.P2SHAddress
import org.bitcoins.core.script.ScriptType
import org.bitcoins.core.util.CryptoUtil
import org.bitcoins.wallet.fixtures.AddressDAOFixture
import org.bitcoins.testkit.fixtures.WalletDAOFixture
import org.bitcoins.testkit.wallet.{BitcoinSWalletTest, WalletTestUtil}
import org.bitcoins.core.hd.HDChainType
import org.bitcoins.core.hd.SegWitHDPath
@ -18,7 +18,7 @@ import org.bitcoins.core.protocol.script.ScriptPubKey
import org.bitcoins.core.protocol.Bech32Address
import org.bitcoins.core.protocol.script.P2WPKHWitnessV0
class AddressDAOTest extends BitcoinSWalletTest with AddressDAOFixture {
class AddressDAOTest extends BitcoinSWalletTest with WalletDAOFixture {
// todo: do this with an actual working address
// todo: with script witness + redeem script
@ -37,14 +37,15 @@ class AddressDAOTest extends BitcoinSWalletTest with AddressDAOFixture {
ecPublicKey = pubkey,
hashedPubkey,
address,
scriptWitness)
scriptWitness,
scriptPubKey = wspk)
}
behavior of "AddressDAO"
it should "fail to insert and read an address into the database without a corresponding account" in {
daos =>
val (_, addressDAO) = daos
val addressDAO = daos.addressDAO
val readF = {
val addressDb = getAddressDb(WalletTestUtil.firstAccountDb)
addressDAO.create(addressDb)
@ -55,7 +56,8 @@ class AddressDAOTest extends BitcoinSWalletTest with AddressDAOFixture {
it should "insert and read an address into the database with a corresponding account" in {
daos =>
val (accountDAO, addressDAO) = daos
val accountDAO = daos.accountDAO
val addressDAO = daos.addressDAO
for {
createdAccount <- {
val account = WalletTestUtil.firstAccountDb

View file

@ -0,0 +1,29 @@
package org.bitcoins.wallet.models
import org.bitcoins.testkit.wallet.BitcoinSWalletTest
import org.bitcoins.testkit.core.gen.TransactionGenerators
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.testkit.fixtures.WalletDAOFixture
import org.bitcoins.testkit.wallet.WalletTestUtil
import org.bitcoins.core.config.RegTest
import org.bitcoins.core.crypto.ECPublicKey
import org.bitcoins.wallet.config.WalletAppConfig
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 IncomingTxoDAOTest extends BitcoinSWalletTest with WalletDAOFixture {
it must "insert a incoming transaction and read it back with its address" in {
daos =>
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 SpendingInfoDAOTest 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

@ -19,6 +19,26 @@ This is meant to be a stand alone project that can be used as a cold storage wal
[BIP44/BIP49/BIP84 paths](../core/src/main/scala/org/bitcoins/core/hd/HDPath.scala)
and script types, so that everything we need for spending the money sent to an address
is derivable.
- **The wallet is a "dumb" wallet that acts mostly as a database of UTXOs, transactions and
addresses, with associated operations on these.**
The wallet module does very little verification of incoming data about transactions,
UTXOs and reorgs. We're aiming to write small, self contained modules, that can be
composed together into more fully fledged systems. That means the `chain` and `node`
modules does the actual verification of data we receive, and `wallet` just blindly
acts on this. This results in a design where you can swap out `node` for a Bitcoin Core
full node, use it with hardware wallets, or something else entirely. However, that also
means that users of `wallet` that doesn't want to use the other modules we provide have
to make sure that the data they are feeding the wallet is correct.
#### Database structure
We store information in the following tables:
- UTXOs - must reference the incoming transaction it was received in
- Addresses - must reference the account it belongs to
- Accounts
- Incoming transactions - must reference the SPK (in our address table) that a TX spends to
- Outgoing transactions - must reference the UTXO(s) it spends
#### Mnemonic encryption

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

@ -3,7 +3,7 @@ package org.bitcoins.wallet.api
import org.bitcoins.core.currency.{CurrencyUnit, CurrencyUnits}
import org.bitcoins.core.protocol.transaction.TransactionOutput
import org.bitcoins.core.wallet.fee.FeeUnit
import org.bitcoins.wallet.models.UTXOSpendingInfoDb
import org.bitcoins.wallet.models.SpendingInfoDb
import scala.annotation.tailrec
@ -15,9 +15,9 @@ trait CoinSelector {
* below their fees. Better for high fee environments than accumulateSmallestViable.
*/
def accumulateLargest(
walletUtxos: Vector[UTXOSpendingInfoDb],
walletUtxos: Vector[SpendingInfoDb],
outputs: Vector[TransactionOutput],
feeRate: FeeUnit): Vector[UTXOSpendingInfoDb] = {
feeRate: FeeUnit): Vector[SpendingInfoDb] = {
val sortedUtxos =
walletUtxos.sortBy(_.value.satoshis.toLong).reverse
@ -31,9 +31,9 @@ trait CoinSelector {
* Has the potential privacy breach of connecting a ton of UTXOs to one address.
*/
def accumulateSmallestViable(
walletUtxos: Vector[UTXOSpendingInfoDb],
walletUtxos: Vector[SpendingInfoDb],
outputs: Vector[TransactionOutput],
feeRate: FeeUnit): Vector[UTXOSpendingInfoDb] = {
feeRate: FeeUnit): Vector[SpendingInfoDb] = {
val sortedUtxos = walletUtxos.sortBy(_.value.satoshis.toLong)
accumulate(sortedUtxos, outputs, feeRate)
@ -41,19 +41,19 @@ trait CoinSelector {
/** Greedily selects from walletUtxos in order, skipping outputs with values below their fees */
def accumulate(
walletUtxos: Vector[UTXOSpendingInfoDb],
walletUtxos: Vector[SpendingInfoDb],
outputs: Vector[TransactionOutput],
feeRate: FeeUnit): Vector[UTXOSpendingInfoDb] = {
feeRate: FeeUnit): Vector[SpendingInfoDb] = {
val totalValue = outputs.foldLeft(CurrencyUnits.zero) {
case (totVal, output) => totVal + output.value
}
@tailrec
def addUtxos(
alreadyAdded: Vector[UTXOSpendingInfoDb],
alreadyAdded: Vector[SpendingInfoDb],
valueSoFar: CurrencyUnit,
bytesSoFar: Long,
utxosLeft: Vector[UTXOSpendingInfoDb]): Vector[UTXOSpendingInfoDb] = {
utxosLeft: Vector[SpendingInfoDb]): Vector[SpendingInfoDb] = {
val fee = feeRate.currencyUnit * bytesSoFar
if (valueSoFar > totalValue + fee) {
alreadyAdded
@ -85,7 +85,7 @@ trait CoinSelector {
object CoinSelector extends CoinSelector {
/** Cribbed from [[https://github.com/bitcoinjs/coinselect/blob/master/utils.js]] */
def approximateUtxoSize(utxo: UTXOSpendingInfoDb): Long = {
def approximateUtxoSize(utxo: SpendingInfoDb): Long = {
val inputBase = 32 + 4 + 1 + 4
val scriptSize = utxo.redeemScriptOpt match {
case Some(script) => script.bytes.length

View file

@ -4,13 +4,12 @@ import org.bitcoins.core.config.NetworkParameters
import org.bitcoins.core.crypto._
import org.bitcoins.core.currency.CurrencyUnit
import org.bitcoins.core.hd.HDPurpose
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.BitcoinAddress
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
@ -46,16 +45,20 @@ trait LockedWalletApi extends WalletApi {
def getBloomFilter(): Future[BloomFilter]
/**
* Adds the provided UTXO to the wallet, making it
* available for spending.
* Processes the given transaction, updating our DB state if it's relevant to us.
* @param transaction The transacton we're processing
* @param confirmation How many confirmations the TX has
*/
def addUtxo(transaction: Transaction, vout: UInt32): Future[AddUtxoResult]
def processTransaction(
transaction: Transaction,
confirmations: Int): Future[LockedWalletApi]
/** Sums up the value of all UTXOs in the wallet */
// noinspection AccessorLikeMethodIsEmptyParen
// async calls have side effects :-)
/** Gets the sum of all confirmed UTXOs in this wallet */
def getBalance(): Future[CurrencyUnit]
/** Gets the sum of all unconfirmed UTXOs in this wallet */
def getUnconfirmedBalance(): Future[CurrencyUnit]
/**
* If a UTXO is spent outside of the wallet, we
* need to remove it from the database so it won't be
@ -63,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

@ -1,20 +1,22 @@
package org.bitcoins.wallet.db
import org.bitcoins.db.DbManagement
import org.bitcoins.wallet.models.{
AccountTable,
AddressTable,
UTXOSpendingInfoTable
}
import slick.jdbc.SQLiteProfile.api._
import org.bitcoins.wallet.models._
sealed abstract class WalletDbManagement extends DbManagement {
private val accountTable = TableQuery[AccountTable]
private val addressTable = TableQuery[AddressTable]
private val utxoDAO = TableQuery[UTXOSpendingInfoTable]
private val utxoTable = TableQuery[SpendingInfoTable]
private val incomingTxoTable = TableQuery[IncomingTXOTable]
private val outgoingTxoTable = TableQuery[OutgoingTXOTable]
override val allTables: List[TableQuery[_ <: Table[_]]] =
List(accountTable, addressTable, utxoDAO)
List(accountTable,
addressTable,
utxoTable,
incomingTxoTable,
outgoingTxoTable)
}

View file

@ -0,0 +1,19 @@
package org.bitcoins.wallet.models
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[OutgoingWalletTXO] {
import profile.api._
override val table = TableQuery[OutgoingTXOTable]
}

View file

@ -4,17 +4,13 @@ import org.bitcoins.db.CRUDAutoInc
import org.bitcoins.wallet.config._
import slick.jdbc.SQLiteProfile.api._
import scala.concurrent.Future
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]
def findAllUTXOs(): Future[Vector[UTXOSpendingInfoDb]] =
database.run(table.result).map(_.toVector)
override val table = TableQuery[SpendingInfoTable]
}

View file

@ -23,34 +23,37 @@ import org.bitcoins.core.hd.LegacyHDPath
* DB representation of a native V0
* SegWit UTXO
*/
case class NativeV0UTXOSpendingInfoDb(
id: Option[Long],
case class SegwitV0SpendingInfo(
outPoint: TransactionOutPoint,
output: TransactionOutput,
privKeyPath: SegWitHDPath,
scriptWitness: ScriptWitness
) extends UTXOSpendingInfoDb {
scriptWitness: ScriptWitness,
id: Option[Long] = None
) 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(
id: Option[Long],
/**
* DB representation of a legacy UTXO
*/
case class LegacySpendingInfo(
outPoint: TransactionOutPoint,
output: TransactionOutput,
privKeyPath: LegacyHDPath
) extends UTXOSpendingInfoDb {
privKeyPath: LegacyHDPath,
id: Option[Long] = None
) 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))
}
@ -63,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
@ -111,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] =
@ -135,16 +149,17 @@ case class UTXOSpendingInfoTable(tag: Tag)
TransactionOutput,
HDPath,
Option[ScriptPubKey],
Option[ScriptWitness])
Option[ScriptWitness]
)
private val fromTuple: UTXOTuple => UTXOSpendingInfoDb = {
private val fromTuple: UTXOTuple => SpendingInfoDb = {
case (id,
outpoint,
output,
path: SegWitHDPath,
None, // ReedemScript
Some(scriptWitness)) =>
NativeV0UTXOSpendingInfoDb(id, outpoint, output, path, scriptWitness)
SegwitV0SpendingInfo(outpoint, output, path, scriptWitness, id)
case (id,
outpoint,
@ -153,15 +168,15 @@ case class UTXOSpendingInfoTable(tag: Tag)
None, // RedeemScript
None // ScriptWitness
) =>
LegacyUTXOSpendingInfoDb(id, outpoint, output, path)
LegacySpendingInfo(outpoint, output, path, id)
case (id, outpoint, output, path, spkOpt, swOpt) =>
throw new IllegalArgumentException(
"Could not construct UtxoSpendingInfoDb from bad tuple:"
+ s" ($id, $outpoint, $output, $path, $spkOpt, $swOpt) . 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,
@ -171,6 +186,6 @@ case class UTXOSpendingInfoTable(tag: Tag)
utxo.redeemScriptOpt,
utxo.scriptWitnessOpt))
def * : ProvenShape[UTXOSpendingInfoDb] =
def * : ProvenShape[SpendingInfoDb] =
(id.?, outPoint, output, privKeyPath, redeemScriptOpt, scriptWitnessOpt) <> (fromTuple, toTuple)
}