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" behavior of "HDAddress"
it must "fail to make addresses with neagtives indices" in { 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.testkit.util.BitcoinSUnitTest
import org.bitcoins.core.crypto.DoubleSha256Digest import org.bitcoins.core.crypto.DoubleSha256Digest
import org.bitcoins.testkit.core.gen.CryptoGenerators import org.bitcoins.testkit.core.gen.CryptoGenerators
import org.bitcoins.testkit.Implicits._
class GetHeadersMessageTest extends BitcoinSUnitTest { class GetHeadersMessageTest extends BitcoinSUnitTest {
@ -21,12 +22,13 @@ class GetHeadersMessageTest extends BitcoinSUnitTest {
} }
it must "be constructable without a stop" in { it must "be constructable without a stop" in {
def getHash: DoubleSha256Digest = def getHash(): DoubleSha256Digest =
CryptoGenerators.doubleSha256Digest.sample.getOrElse(getHash) CryptoGenerators.doubleSha256Digest.sampleSome
val msg = GetHeadersMessage(List.fill(10)(getHash))
val msg = GetHeadersMessage(List.fill(10)(getHash()))
assert(msg.hashStop == DoubleSha256Digest.empty) assert(msg.hashStop == DoubleSha256Digest.empty)
val hash = getHash val hash = getHash()
val otherMsg = GetHeadersMessage(hash) val otherMsg = GetHeadersMessage(hash)
assert(otherMsg == GetHeadersMessage(Vector(hash))) assert(otherMsg == GetHeadersMessage(Vector(hash)))
} }

View file

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

View file

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

View file

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

View file

@ -38,6 +38,11 @@
<logger name="org.bitcoins.node.config" level="INFO"/> <logger name="org.bitcoins.node.config" level="INFO"/>
<logger name="org.bitcoins.wallet.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 ║ --> <!-- ║ 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.HDPurposes
import org.bitcoins.core.hd.SegWitHDPath import org.bitcoins.core.hd.SegWitHDPath
import slick.jdbc.GetResult import slick.jdbc.GetResult
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.core.currency.Satoshis
abstract class DbCommonsColumnMappers { abstract class DbCommonsColumnMappers {
@ -159,6 +161,9 @@ abstract class DbCommonsColumnMappers {
MappedColumnType MappedColumnType
.base[ScriptType, String](_.toString, ScriptType.fromStringExn) .base[ScriptType, String](_.toString, ScriptType.fromStringExn)
implicit val txMapper: BaseColumnType[Transaction] =
MappedColumnType.base[Transaction, String](_.hex, Transaction.fromHex)
} }
object DbCommonsColumnMappers extends DbCommonsColumnMappers object DbCommonsColumnMappers extends DbCommonsColumnMappers

View file

@ -20,10 +20,22 @@ abstract class DbManagement extends BitcoinSLogger {
def listTables(db: SafeDatabase): Future[Vector[SQLiteTableInfo]] = def listTables(db: SafeDatabase): Future[Vector[SQLiteTableInfo]] =
listTables(db.config.database) listTables(db.config.database)
/** Creates all tables in our table list, in one SQL transaction */
def createAll()( def createAll()(
implicit config: AppConfig, implicit config: AppConfig,
ec: ExecutionContext): Future[Unit] = { ec: ExecutionContext
Future.sequence(allTables.map(createTable(_))).map(_ => ()) ): 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()( def dropAll()(
@ -32,6 +44,18 @@ abstract class DbManagement extends BitcoinSLogger {
Future.sequence(allTables.reverse.map(dropTable(_))).map(_ => ()) 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( def createTable(
table: TableQuery[_ <: Table[_]], table: TableQuery[_ <: Table[_]],
createIfNotExists: Boolean = true)( createIfNotExists: Boolean = true)(
@ -42,11 +66,7 @@ abstract class DbManagement extends BitcoinSLogger {
s"Creating table $tableName with DB config: ${config.dbConfig.config} ") s"Creating table $tableName with DB config: ${config.dbConfig.config} ")
import config.database import config.database
val query = if (createIfNotExists) { val query = createTableQuery(table, createIfNotExists)
table.schema.createIfNotExists
} else {
table.schema.create
}
database.run(query).map(_ => logger.debug(s"Created table $tableName")) 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 akka.actor.Cancellable
import org.bitcoins.core.protocol.transaction.Transaction import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.core.crypto.DoubleSha256DigestBE import org.bitcoins.core.crypto.DoubleSha256DigestBE
import scala.util.Try
import scala.util.Failure
import scala.util.Success
class NodeWithWalletTest extends BitcoinSWalletTest { class NodeWithWalletTest extends BitcoinSWalletTest {
@ -48,6 +51,8 @@ class NodeWithWalletTest extends BitcoinSWalletTest {
val completionP = Promise[Assertion] val completionP = Promise[Assertion]
val amountFromBitcoind = 1.bitcoin
val callbacks = { val callbacks = {
val onBlock: DataMessageHandler.OnBlockReceived = { block => val onBlock: DataMessageHandler.OnBlockReceived = { block =>
completionP.failure( completionP.failure(
@ -58,8 +63,21 @@ class NodeWithWalletTest extends BitcoinSWalletTest {
val onTx: DataMessageHandler.OnTxReceived = { tx => val onTx: DataMessageHandler.OnTxReceived = { tx =>
if (expectedTxId.contains(tx.txId)) { if (expectedTxId.contains(tx.txId)) {
logger.debug(s"Cancelling timeout we set earlier")
cancellable.map(_.cancel()) 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)) { } else if (unexpectedTxId.contains(tx.txId)) {
completionP.failure( completionP.failure(
new TestFailedException( 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)) cancellable = Some(actorSystem.scheduler.scheduleOnce(delay, runnable))
tx tx
} }
@ -116,7 +135,9 @@ class NodeWithWalletTest extends BitcoinSWalletTest {
_ <- spv.sync() _ <- spv.sync()
_ <- NodeTestUtil.awaitSync(spv, rpc) _ <- NodeTestUtil.awaitSync(spv, rpc)
ourTxid <- rpc.sendToAddress(address, 1.bitcoin).map(processWalletTx) ourTxid <- rpc
.sendToAddress(address, amountFromBitcoind)
.map(processWalletTx)
notOurTxid <- rpc.getNewAddress notOurTxid <- rpc.getNewAddress
.flatMap(rpc.sendToAddress(_, 1.bitcoin)) .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 package org.bitcoins.testkit.core.gen
import org.bitcoins.testkit.Implicits._
import org.bitcoins.core.consensus.Merkle import org.bitcoins.core.consensus.Merkle
import org.bitcoins.core.crypto.DoubleSha256Digest import org.bitcoins.core.crypto.DoubleSha256Digest
import org.bitcoins.core.number.{Int32, UInt32} import org.bitcoins.core.number.{Int32, UInt32}
@ -119,7 +120,7 @@ sealed abstract class BlockchainElementsGenerator {
prevBlockHash: DoubleSha256Digest, prevBlockHash: DoubleSha256Digest,
nBits: UInt32): BlockHeader = { nBits: UInt32): BlockHeader = {
//nonce for the unique hash //nonce for the unique hash
val nonce = NumberGenerator.uInt32s.sample.get val nonce = NumberGenerator.uInt32s.sampleSome
BlockHeader(Int32.one, BlockHeader(Int32.one,
prevBlockHash, prevBlockHash,
EmptyTransaction.txId, EmptyTransaction.txId,

View file

@ -1,7 +1,7 @@
package org.bitcoins.testkit.core.gen package org.bitcoins.testkit.core.gen
import org.bitcoins.core.crypto._ 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.number.{Int32, Int64, UInt32}
import org.bitcoins.core.policy.Policy import org.bitcoins.core.policy.Policy
import org.bitcoins.core.protocol.script._ 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.constant.ScriptNumber
import org.bitcoins.core.script.locktime.LockTimeInterpreter import org.bitcoins.core.script.locktime.LockTimeInterpreter
import org.bitcoins.core.util.BitcoinSLogger import org.bitcoins.core.util.BitcoinSLogger
import org.bitcoins.testkit.Implicits._
import org.scalacheck.Gen import org.scalacheck.Gen
import scala.annotation.tailrec import scala.annotation.tailrec
/** object TransactionGenerators extends BitcoinSLogger {
* Created by chris on 6/21/16.
*/
trait TransactionGenerators extends BitcoinSLogger {
/** Responsible for generating [[org.bitcoins.core.protocol.transaction.TransactionOutPoint TransactionOutPoint]] */ /** Responsible for generating [[org.bitcoins.core.protocol.transaction.TransactionOutPoint TransactionOutPoint]] */
def outPoint: Gen[TransactionOutPoint] = def outPoint: Gen[TransactionOutPoint] =
@ -30,6 +28,12 @@ trait TransactionGenerators extends BitcoinSLogger {
vout <- NumberGenerator.uInt32s vout <- NumberGenerator.uInt32s
} yield TransactionOutPoint(txId, vout) } 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]] */ /** Generates a random [[org.bitcoins.core.protocol.transaction.TransactionOutput TransactionOutput]] */
def output: Gen[TransactionOutput] = def output: Gen[TransactionOutput] =
for { for {
@ -51,13 +55,17 @@ trait TransactionGenerators extends BitcoinSLogger {
def realisticOutputs: Gen[Seq[TransactionOutput]] = def realisticOutputs: Gen[Seq[TransactionOutput]] =
Gen.choose(0, 5).flatMap(n => Gen.listOfN(n, realisticOutput)) 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]] */ /** Generates a small list of [[org.bitcoins.core.protocol.transaction.TransactionOutput TransactionOutput]] */
def smallOutputs: Gen[Seq[TransactionOutput]] = def smallOutputs: Gen[Seq[TransactionOutput]] =
Gen.choose(0, 5).flatMap(i => Gen.listOfN(i, output)) Gen.choose(0, 5).flatMap(i => Gen.listOfN(i, output))
/** Creates a small sequence of outputs whose total sum is <= totalAmount */ /** Creates a small sequence of outputs whose total sum is <= totalAmount */
def smallOutputs(totalAmount: CurrencyUnit): Gen[Seq[TransactionOutput]] = { def smallOutputs(totalAmount: CurrencyUnit): Gen[Seq[TransactionOutput]] = {
val numOutputs = Gen.choose(0, 5).sample.get val numOutputs = Gen.choose(0, 5).sampleSome
@tailrec @tailrec
def loop( def loop(
remaining: Int, remaining: Int,
@ -69,8 +77,7 @@ trait TransactionGenerators extends BitcoinSLogger {
val amt = Gen val amt = Gen
.choose(100, remainingAmount.toBigDecimal.toLongExact) .choose(100, remainingAmount.toBigDecimal.toLongExact)
.map(n => Satoshis(Int64(n))) .map(n => Satoshis(Int64(n)))
.sample .sampleSome
.get
loop(remaining - 1, remainingAmount - amt, amt +: accum) loop(remaining - 1, remainingAmount - amt, amt +: accum)
} }
} }
@ -124,6 +131,14 @@ trait TransactionGenerators extends BitcoinSLogger {
def transaction: Gen[Transaction] = def transaction: Gen[Transaction] =
Gen.oneOf(baseTransaction, witnessTransaction) 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] = def baseTransaction: Gen[BaseTransaction] =
for { for {
version <- NumberGenerator.int32s version <- NumberGenerator.int32s
@ -132,14 +147,23 @@ trait TransactionGenerators extends BitcoinSLogger {
lockTime <- NumberGenerator.uInt32s lockTime <- NumberGenerator.uInt32s
} yield BaseTransaction(version, is, os, lockTime) } yield BaseTransaction(version, is, os, lockTime)
/** Generates a random [[org.bitcoins.core.protocol.transaction.WitnessTransaction WitnessTransaction]] */ /** Generates a legacy transaction with at least one output paying to the given SPK */
def witnessTransaction: Gen[WitnessTransaction] = 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 { for {
version <- NumberGenerator.int32s version <- NumberGenerator.int32s
//we cannot have zero witnesses on a WitnessTx //we cannot have zero witnesses on a WitnessTx
//https://github.com/bitcoin/bitcoin/blob/e8cfe1ee2d01c493b758a67ad14707dca15792ea/src/primitives/transaction.h#L276-L281 //https://github.com/bitcoin/bitcoin/blob/e8cfe1ee2d01c493b758a67ad14707dca15792ea/src/primitives/transaction.h#L276-L281
is <- smallInputsNonEmpty is <- smallInputsNonEmpty
os <- smallOutputs
lockTime <- NumberGenerator.uInt32s lockTime <- NumberGenerator.uInt32s
//we have to have atleast one NON `EmptyScriptWitness` for a tx to be a valid WitnessTransaction, otherwise we //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 //revert to using the `BaseTransaction` serialization format
@ -148,7 +172,23 @@ trait TransactionGenerators extends BitcoinSLogger {
witness <- WitnessGenerators witness <- WitnessGenerators
.transactionWitness(is.size) .transactionWitness(is.size)
.suchThat(_.witnesses.exists(_ != EmptyScriptWitness)) .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 * 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] = { def destroyWallet(wallet: UnlockedWalletApi): Future[Unit] = {
WalletDbManagement WalletDbManagement
.dropAll()(config = config.walletConf, ec = implicitly[ExecutionContext]) .dropAll()(config = wallet.walletConfig,
ec = implicitly[ExecutionContext])
.map(_ => ()) .map(_ => ())
} }
@ -112,8 +113,8 @@ trait BitcoinSWalletTest
} }
def withNewWallet(test: OneArgAsyncTest): FutureOutcome = def withNewWallet(test: OneArgAsyncTest): FutureOutcome =
makeDependentFixture(build = createDefaultWallet, destroy = destroyWallet)( makeDependentFixture(build = createDefaultWallet _,
test) destroy = destroyWallet)(test)
case class WalletWithBitcoind( case class WalletWithBitcoind(
wallet: UnlockedWalletApi, wallet: UnlockedWalletApi,
@ -138,7 +139,7 @@ trait BitcoinSWalletTest
def withNewWalletAndBitcoind(test: OneArgAsyncTest): FutureOutcome = { def withNewWalletAndBitcoind(test: OneArgAsyncTest): FutureOutcome = {
val builder: () => Future[WalletWithBitcoind] = composeBuildersAndWrap( val builder: () => Future[WalletWithBitcoind] = composeBuildersAndWrap(
createDefaultWallet, createDefaultWallet _,
createWalletWithBitcoind, createWalletWithBitcoind,
(_: UnlockedWalletApi, walletWithBitcoind: WalletWithBitcoind) => (_: UnlockedWalletApi, walletWithBitcoind: WalletWithBitcoind) =>
walletWithBitcoind walletWithBitcoind

View file

@ -1,7 +1,9 @@
package org.bitcoins.testkit.wallet package org.bitcoins.testkit.wallet
import org.bitcoins.testkit.Implicits._
import org.bitcoins.core.config.RegTest import org.bitcoins.core.config.RegTest
import org.bitcoins.core.crypto._ import org.bitcoins.core.crypto._
import org.bitcoins.core.currency._
import org.bitcoins.core.number.UInt32 import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.blockchain.{ import org.bitcoins.core.protocol.blockchain.{
ChainParams, ChainParams,
@ -14,6 +16,18 @@ import scodec.bits.HexStringSyntax
import org.bitcoins.core.hd._ import org.bitcoins.core.hd._
import org.bitcoins.core.protocol.script.ScriptWitness import org.bitcoins.core.protocol.script.ScriptWitness
import org.bitcoins.core.protocol.script.P2WPKHWitnessV0 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 { object WalletTestUtil {
@ -53,16 +67,94 @@ object WalletTestUtil {
HDChainType.Change, HDChainType.Change,
addressIndex = 0) addressIndex = 0)
def freshXpub: ExtPublicKey = def freshXpub(): ExtPublicKey =
CryptoGenerators.extPublicKey.sample.getOrElse(freshXpub) CryptoGenerators.extPublicKey.sampleSome
val firstAccount = HDAccount(HDCoin(HDPurposes.SegWit, hdCoinType), 0) val firstAccount = HDAccount(HDCoin(HDPurposes.SegWit, hdCoinType), 0)
def firstAccountDb = AccountDb(freshXpub, firstAccount) def firstAccountDb = AccountDb(freshXpub(), firstAccount)
lazy val sampleTxid: DoubleSha256Digest = DoubleSha256Digest( lazy val sampleTxid: DoubleSha256Digest = DoubleSha256Digest(
hex"a910523c0b6752fbcb9c24303b4e068c505825d074a45d1c787122efb4649215") hex"a910523c0b6752fbcb9c24303b4e068c505825d074a45d1c787122efb4649215")
lazy val sampleVout: UInt32 = UInt32.zero lazy val sampleVout: UInt32 = UInt32.zero
lazy val sampleSPK: ScriptPubKey = lazy val sampleSPK: ScriptPubKey =
ScriptPubKey.fromAsmBytes(hex"001401b2ac67587e4b603bb3ad709a8102c30113892d") 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 package org.bitcoins.testkit.db
import org.bitcoins.testkit.util.BitcoinSUnitTest import org.bitcoins.testkit.util.BitcoinSUnitTest
import org.bitcoins.testkit.Implicits._
import org.bitcoins.testkit.BitcoinSAppConfig import org.bitcoins.testkit.BitcoinSAppConfig
import org.bitcoins.testkit.BitcoinSAppConfig._ import org.bitcoins.testkit.BitcoinSAppConfig._
import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigFactory
@ -99,7 +100,7 @@ class AppConfigTest extends BitcoinSUnitTest {
_ <- { _ <- {
val hdAccount = val hdAccount =
HDAccount(HDCoin(HDPurposes.Legacy, HDCoinType.Bitcoin), 0) HDAccount(HDCoin(HDPurposes.Legacy, HDCoinType.Bitcoin), 0)
val xpub = CryptoGenerators.extPublicKey.sample.get val xpub = CryptoGenerators.extPublicKey.sampleSome
val account = AccountDb(xpub, hdAccount) val account = AccountDb(xpub, hdAccount)
accountDAO.create(account) accountDAO.create(account)
} }

View file

@ -3,6 +3,7 @@ package org.bitcoins.wallet
import org.bitcoins.core.crypto.{AesPassword, MnemonicCode} import org.bitcoins.core.crypto.{AesPassword, MnemonicCode}
import org.bitcoins.testkit.core.gen.CryptoGenerators import org.bitcoins.testkit.core.gen.CryptoGenerators
import org.bitcoins.testkit.util.BitcoinSUnitTest import org.bitcoins.testkit.util.BitcoinSUnitTest
import org.bitcoins.testkit.Implicits._
import scala.util.{Failure, Success} import scala.util.{Failure, Success}
@ -13,10 +14,7 @@ class EncryptedMnemonicTest extends BitcoinSUnitTest {
val password = AesPassword.fromNonEmptyString("good") val password = AesPassword.fromNonEmptyString("good")
val badPassword = AesPassword.fromNonEmptyString("bad") val badPassword = AesPassword.fromNonEmptyString("bad")
def getMnemonic(): MnemonicCode = val mnemonic = CryptoGenerators.mnemonicCode.sampleSome
CryptoGenerators.mnemonicCode.sample.getOrElse(getMnemonic())
val mnemonic = getMnemonic()
val encrypted = EncryptedMnemonicHelper.encrypt(mnemonic, password) val encrypted = EncryptedMnemonicHelper.encrypt(mnemonic, password)
val decrypted = encrypted.toMnemonic(badPassword) 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 WalletWithBitcoind(wallet, bitcoind) = walletWithBitcoind
val valueFromBitcoind = Bitcoins.one val valueFromBitcoind = Bitcoins.one
val addUtxoF: Future[Unit] = for { for {
addr <- wallet.getNewAddress() addr <- wallet.getNewAddress()
txid <- bitcoind.sendToAddress(addr, valueFromBitcoind) tx <- bitcoind
_ <- bitcoind.generate(6) .sendToAddress(addr, valueFromBitcoind)
tx <- bitcoind.getRawTransaction(txid) .flatMap(bitcoind.getRawTransactionRaw(_))
addUtxoRes <- { _ <- wallet.listUtxos().map(utxos => assert(utxos.isEmpty))
val voutOpt = tx.vout.find { rpcOut => _ <- wallet.getBalance().map(confirmed => assert(confirmed == 0.bitcoin))
val addressesOpt = rpcOut.scriptPubKey.addresses _ <- wallet
addressesOpt.exists(_.contains(addr)) .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( _ <- wallet
throw new IllegalArgumentException( .getBalance()
"Could not find ouput that spent to our address!")) .map(confirmed => assert(confirmed == valueFromBitcoind))
_ <- wallet
wallet.addUtxo(tx.hex, UInt32(vout.n)) .getUnconfirmedBalance()
} .map(unconfirmed => assert(unconfirmed == 0.bitcoin))
} 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)
addressFromBitcoind <- bitcoind.getNewAddress addressFromBitcoind <- bitcoind.getNewAddress
signedTx <- wallet.sendToAddress(addressFromBitcoind, 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.protocol.transaction.TransactionOutput
import org.bitcoins.core.wallet.fee.{FeeUnit, SatoshisPerByte} import org.bitcoins.core.wallet.fee.{FeeUnit, SatoshisPerByte}
import org.bitcoins.testkit.core.gen.{TransactionGenerators, WitnessGenerators} import org.bitcoins.testkit.core.gen.{TransactionGenerators, WitnessGenerators}
import org.bitcoins.wallet.models.{
NativeV0UTXOSpendingInfoDb,
UTXOSpendingInfoDb
}
import org.bitcoins.testkit.wallet.{BitcoinSWalletTest, WalletTestUtil} import org.bitcoins.testkit.wallet.{BitcoinSWalletTest, WalletTestUtil}
import org.scalatest.FutureOutcome import org.scalatest.FutureOutcome
import org.bitcoins.wallet.models.SpendingInfoDb
import org.bitcoins.wallet.models.SegwitV0SpendingInfo
class CoinSelectorTest extends BitcoinSWalletTest { class CoinSelectorTest extends BitcoinSWalletTest {
case class CoinSelectionFixture( case class CoinSelectionFixture(
output: TransactionOutput, output: TransactionOutput,
feeRate: FeeUnit, feeRate: FeeUnit,
utxo1: UTXOSpendingInfoDb, utxo1: SpendingInfoDb,
utxo2: UTXOSpendingInfoDb, utxo2: SpendingInfoDb,
utxo3: UTXOSpendingInfoDb) { utxo3: SpendingInfoDb) {
val utxoSet: Vector[UTXOSpendingInfoDb] = Vector(utxo1, utxo2, utxo3) val utxoSet: Vector[SpendingInfoDb] = Vector(utxo1, utxo2, utxo3)
} }
override type FixtureParam = CoinSelectionFixture override type FixtureParam = CoinSelectionFixture
@ -29,21 +27,21 @@ class CoinSelectorTest extends BitcoinSWalletTest {
val output = TransactionOutput(99.sats, ScriptPubKey.empty) val output = TransactionOutput(99.sats, ScriptPubKey.empty)
val feeRate = SatoshisPerByte(CurrencyUnits.zero) val feeRate = SatoshisPerByte(CurrencyUnits.zero)
val utxo1 = NativeV0UTXOSpendingInfoDb( val utxo1 = SegwitV0SpendingInfo(
id = Some(1), id = Some(1),
outPoint = TransactionGenerators.outPoint.sample.get, outPoint = TransactionGenerators.outPoint.sample.get,
output = TransactionOutput(10.sats, ScriptPubKey.empty), output = TransactionOutput(10.sats, ScriptPubKey.empty),
privKeyPath = WalletTestUtil.sampleSegwitPath, privKeyPath = WalletTestUtil.sampleSegwitPath,
scriptWitness = WitnessGenerators.scriptWitness.sample.get scriptWitness = WitnessGenerators.scriptWitness.sample.get
) )
val utxo2 = NativeV0UTXOSpendingInfoDb( val utxo2 = SegwitV0SpendingInfo(
id = Some(2), id = Some(2),
outPoint = TransactionGenerators.outPoint.sample.get, outPoint = TransactionGenerators.outPoint.sample.get,
output = TransactionOutput(90.sats, ScriptPubKey.empty), output = TransactionOutput(90.sats, ScriptPubKey.empty),
privKeyPath = WalletTestUtil.sampleSegwitPath, privKeyPath = WalletTestUtil.sampleSegwitPath,
scriptWitness = WitnessGenerators.scriptWitness.sample.get scriptWitness = WitnessGenerators.scriptWitness.sample.get
) )
val utxo3 = NativeV0UTXOSpendingInfoDb( val utxo3 = SegwitV0SpendingInfo(
id = Some(3), id = Some(3),
outPoint = TransactionGenerators.outPoint.sample.get, outPoint = TransactionGenerators.outPoint.sample.get,
output = TransactionOutput(20.sats, ScriptPubKey.empty), output = TransactionOutput(20.sats, ScriptPubKey.empty),

View file

@ -1,17 +1,19 @@
package org.bitcoins.wallet.models package org.bitcoins.wallet.models
import org.bitcoins.testkit.core.gen.CryptoGenerators 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.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 { for {
created <- { created <- {
val account = WalletTestUtil.firstAccount val account = WalletTestUtil.firstAccount
val xpub = CryptoGenerators.extPublicKey.sample.get val xpub = CryptoGenerators.extPublicKey.sampleSome
val accountDb = AccountDb(xpub, account) val accountDb = AccountDb(xpub, account)
accountDAO.create(accountDb) 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.protocol.P2SHAddress
import org.bitcoins.core.script.ScriptType import org.bitcoins.core.script.ScriptType
import org.bitcoins.core.util.CryptoUtil 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.testkit.wallet.{BitcoinSWalletTest, WalletTestUtil}
import org.bitcoins.core.hd.HDChainType import org.bitcoins.core.hd.HDChainType
import org.bitcoins.core.hd.SegWitHDPath 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.Bech32Address
import org.bitcoins.core.protocol.script.P2WPKHWitnessV0 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: do this with an actual working address
// todo: with script witness + redeem script // todo: with script witness + redeem script
@ -37,14 +37,15 @@ class AddressDAOTest extends BitcoinSWalletTest with AddressDAOFixture {
ecPublicKey = pubkey, ecPublicKey = pubkey,
hashedPubkey, hashedPubkey,
address, address,
scriptWitness) scriptWitness,
scriptPubKey = wspk)
} }
behavior of "AddressDAO" behavior of "AddressDAO"
it should "fail to insert and read an address into the database without a corresponding account" in { it should "fail to insert and read an address into the database without a corresponding account" in {
daos => daos =>
val (_, addressDAO) = daos val addressDAO = daos.addressDAO
val readF = { val readF = {
val addressDb = getAddressDb(WalletTestUtil.firstAccountDb) val addressDb = getAddressDb(WalletTestUtil.firstAccountDb)
addressDAO.create(addressDb) 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 { it should "insert and read an address into the database with a corresponding account" in {
daos => daos =>
val (accountDAO, addressDAO) = daos val accountDAO = daos.accountDAO
val addressDAO = daos.addressDAO
for { for {
createdAccount <- { createdAccount <- {
val account = WalletTestUtil.firstAccountDb 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) [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 and script types, so that everything we need for spending the money sent to an address
is derivable. 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 #### Mnemonic encryption

View file

@ -1,11 +1,11 @@
package org.bitcoins.wallet.api package org.bitcoins.wallet.api
sealed trait AddUtxoResult { import org.bitcoins.wallet.models._
def flatMap(f: AddUtxoResult => AddUtxoResult) = ???
def map(success: AddUtxoSuccess => AddUtxoResult) = ???
}
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 */ /** Represents an error that might occur when adding an UTXO to the wallet */
sealed trait AddUtxoError extends Error with AddUtxoResult 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.currency.{CurrencyUnit, CurrencyUnits}
import org.bitcoins.core.protocol.transaction.TransactionOutput import org.bitcoins.core.protocol.transaction.TransactionOutput
import org.bitcoins.core.wallet.fee.FeeUnit import org.bitcoins.core.wallet.fee.FeeUnit
import org.bitcoins.wallet.models.UTXOSpendingInfoDb import org.bitcoins.wallet.models.SpendingInfoDb
import scala.annotation.tailrec import scala.annotation.tailrec
@ -15,9 +15,9 @@ trait CoinSelector {
* below their fees. Better for high fee environments than accumulateSmallestViable. * below their fees. Better for high fee environments than accumulateSmallestViable.
*/ */
def accumulateLargest( def accumulateLargest(
walletUtxos: Vector[UTXOSpendingInfoDb], walletUtxos: Vector[SpendingInfoDb],
outputs: Vector[TransactionOutput], outputs: Vector[TransactionOutput],
feeRate: FeeUnit): Vector[UTXOSpendingInfoDb] = { feeRate: FeeUnit): Vector[SpendingInfoDb] = {
val sortedUtxos = val sortedUtxos =
walletUtxos.sortBy(_.value.satoshis.toLong).reverse 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. * Has the potential privacy breach of connecting a ton of UTXOs to one address.
*/ */
def accumulateSmallestViable( def accumulateSmallestViable(
walletUtxos: Vector[UTXOSpendingInfoDb], walletUtxos: Vector[SpendingInfoDb],
outputs: Vector[TransactionOutput], outputs: Vector[TransactionOutput],
feeRate: FeeUnit): Vector[UTXOSpendingInfoDb] = { feeRate: FeeUnit): Vector[SpendingInfoDb] = {
val sortedUtxos = walletUtxos.sortBy(_.value.satoshis.toLong) val sortedUtxos = walletUtxos.sortBy(_.value.satoshis.toLong)
accumulate(sortedUtxos, outputs, feeRate) accumulate(sortedUtxos, outputs, feeRate)
@ -41,19 +41,19 @@ trait CoinSelector {
/** Greedily selects from walletUtxos in order, skipping outputs with values below their fees */ /** Greedily selects from walletUtxos in order, skipping outputs with values below their fees */
def accumulate( def accumulate(
walletUtxos: Vector[UTXOSpendingInfoDb], walletUtxos: Vector[SpendingInfoDb],
outputs: Vector[TransactionOutput], outputs: Vector[TransactionOutput],
feeRate: FeeUnit): Vector[UTXOSpendingInfoDb] = { feeRate: FeeUnit): Vector[SpendingInfoDb] = {
val totalValue = outputs.foldLeft(CurrencyUnits.zero) { val totalValue = outputs.foldLeft(CurrencyUnits.zero) {
case (totVal, output) => totVal + output.value case (totVal, output) => totVal + output.value
} }
@tailrec @tailrec
def addUtxos( def addUtxos(
alreadyAdded: Vector[UTXOSpendingInfoDb], alreadyAdded: Vector[SpendingInfoDb],
valueSoFar: CurrencyUnit, valueSoFar: CurrencyUnit,
bytesSoFar: Long, bytesSoFar: Long,
utxosLeft: Vector[UTXOSpendingInfoDb]): Vector[UTXOSpendingInfoDb] = { utxosLeft: Vector[SpendingInfoDb]): Vector[SpendingInfoDb] = {
val fee = feeRate.currencyUnit * bytesSoFar val fee = feeRate.currencyUnit * bytesSoFar
if (valueSoFar > totalValue + fee) { if (valueSoFar > totalValue + fee) {
alreadyAdded alreadyAdded
@ -85,7 +85,7 @@ trait CoinSelector {
object CoinSelector extends CoinSelector { object CoinSelector extends CoinSelector {
/** Cribbed from [[https://github.com/bitcoinjs/coinselect/blob/master/utils.js]] */ /** 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 inputBase = 32 + 4 + 1 + 4
val scriptSize = utxo.redeemScriptOpt match { val scriptSize = utxo.redeemScriptOpt match {
case Some(script) => script.bytes.length 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.crypto._
import org.bitcoins.core.currency.CurrencyUnit import org.bitcoins.core.currency.CurrencyUnit
import org.bitcoins.core.hd.HDPurpose import org.bitcoins.core.hd.HDPurpose
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.BitcoinAddress import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.blockchain.ChainParams import org.bitcoins.core.protocol.blockchain.ChainParams
import org.bitcoins.core.protocol.transaction.Transaction import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.core.wallet.fee.FeeUnit import org.bitcoins.core.wallet.fee.FeeUnit
import org.bitcoins.wallet.HDUtil 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.Future
import scala.concurrent.ExecutionContext import scala.concurrent.ExecutionContext
@ -46,16 +45,20 @@ trait LockedWalletApi extends WalletApi {
def getBloomFilter(): Future[BloomFilter] def getBloomFilter(): Future[BloomFilter]
/** /**
* Adds the provided UTXO to the wallet, making it * Processes the given transaction, updating our DB state if it's relevant to us.
* available for spending. * @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 */ /** Gets the sum of all confirmed UTXOs in this wallet */
// noinspection AccessorLikeMethodIsEmptyParen
// async calls have side effects :-)
def getBalance(): Future[CurrencyUnit] 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 * If a UTXO is spent outside of the wallet, we
* need to remove it from the database so it won't be * 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 updateUtxo: Future[WalletApi]
def listUtxos(): Future[Vector[UTXOSpendingInfoDb]] def listUtxos(): Future[Vector[SpendingInfoDb]]
def listAddresses(): Future[Vector[AddressDb]] def listAddresses(): Future[Vector[AddressDb]]

View file

@ -1,20 +1,22 @@
package org.bitcoins.wallet.db package org.bitcoins.wallet.db
import org.bitcoins.db.DbManagement import org.bitcoins.db.DbManagement
import org.bitcoins.wallet.models.{
AccountTable,
AddressTable,
UTXOSpendingInfoTable
}
import slick.jdbc.SQLiteProfile.api._ import slick.jdbc.SQLiteProfile.api._
import org.bitcoins.wallet.models._
sealed abstract class WalletDbManagement extends DbManagement { sealed abstract class WalletDbManagement extends DbManagement {
private val accountTable = TableQuery[AccountTable] private val accountTable = TableQuery[AccountTable]
private val addressTable = TableQuery[AddressTable] 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[_]]] = 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 org.bitcoins.wallet.config._
import slick.jdbc.SQLiteProfile.api._ import slick.jdbc.SQLiteProfile.api._
import scala.concurrent.Future
import scala.concurrent.ExecutionContext import scala.concurrent.ExecutionContext
case class UTXOSpendingInfoDAO()( case class SpendingInfoDAO()(
implicit val ec: ExecutionContext, implicit val ec: ExecutionContext,
val appConfig: WalletAppConfig) val appConfig: WalletAppConfig)
extends CRUDAutoInc[UTXOSpendingInfoDb] { extends CRUDAutoInc[SpendingInfoDb] {
/** The table inside our database we are inserting into */ /** The table inside our database we are inserting into */
override val table = TableQuery[UTXOSpendingInfoTable] override val table = TableQuery[SpendingInfoTable]
def findAllUTXOs(): Future[Vector[UTXOSpendingInfoDb]] =
database.run(table.result).map(_.toVector)
} }

View file

@ -23,34 +23,37 @@ import org.bitcoins.core.hd.LegacyHDPath
* DB representation of a native V0 * DB representation of a native V0
* SegWit UTXO * SegWit UTXO
*/ */
case class NativeV0UTXOSpendingInfoDb( case class SegwitV0SpendingInfo(
id: Option[Long],
outPoint: TransactionOutPoint, outPoint: TransactionOutPoint,
output: TransactionOutput, output: TransactionOutput,
privKeyPath: SegWitHDPath, privKeyPath: SegWitHDPath,
scriptWitness: ScriptWitness scriptWitness: ScriptWitness,
) extends UTXOSpendingInfoDb { id: Option[Long] = None
) extends SpendingInfoDb {
override val redeemScriptOpt: Option[ScriptPubKey] = None override val redeemScriptOpt: Option[ScriptPubKey] = None
override val scriptWitnessOpt: Option[ScriptWitness] = Some(scriptWitness) override val scriptWitnessOpt: Option[ScriptWitness] = Some(scriptWitness)
override type PathType = SegWitHDPath override type PathType = SegWitHDPath
override def copyWithId(id: Long): NativeV0UTXOSpendingInfoDb = override def copyWithId(id: Long): SegwitV0SpendingInfo =
copy(id = Some(id)) copy(id = Some(id))
} }
case class LegacyUTXOSpendingInfoDb( /**
id: Option[Long], * DB representation of a legacy UTXO
*/
case class LegacySpendingInfo(
outPoint: TransactionOutPoint, outPoint: TransactionOutPoint,
output: TransactionOutput, output: TransactionOutput,
privKeyPath: LegacyHDPath privKeyPath: LegacyHDPath,
) extends UTXOSpendingInfoDb { id: Option[Long] = None
) extends SpendingInfoDb {
override val redeemScriptOpt: Option[ScriptPubKey] = None override val redeemScriptOpt: Option[ScriptPubKey] = None
override def scriptWitnessOpt: Option[ScriptWitness] = None override def scriptWitnessOpt: Option[ScriptWitness] = None
override type PathType = LegacyHDPath override type PathType = LegacyHDPath
override def copyWithId(id: Long): LegacyUTXOSpendingInfoDb = override def copyWithId(id: Long): LegacySpendingInfo =
copy(id = Some(id)) copy(id = Some(id))
} }
@ -63,8 +66,8 @@ case class LegacyUTXOSpendingInfoDb(
* we need to derive the private keys, given * we need to derive the private keys, given
* the root wallet seed. * the root wallet seed.
*/ */
sealed trait UTXOSpendingInfoDb sealed trait SpendingInfoDb
extends DbRowAutoInc[UTXOSpendingInfoDb] extends DbRowAutoInc[SpendingInfoDb]
with BitcoinSLogger { with BitcoinSLogger {
protected type PathType <: HDPath 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._ import org.bitcoins.db.DbCommonsColumnMappers._
def outPoint: Rep[TransactionOutPoint] = def outPoint: Rep[TransactionOutPoint] =
@ -135,16 +149,17 @@ case class UTXOSpendingInfoTable(tag: Tag)
TransactionOutput, TransactionOutput,
HDPath, HDPath,
Option[ScriptPubKey], Option[ScriptPubKey],
Option[ScriptWitness]) Option[ScriptWitness]
)
private val fromTuple: UTXOTuple => UTXOSpendingInfoDb = { private val fromTuple: UTXOTuple => SpendingInfoDb = {
case (id, case (id,
outpoint, outpoint,
output, output,
path: SegWitHDPath, path: SegWitHDPath,
None, // ReedemScript None, // ReedemScript
Some(scriptWitness)) => Some(scriptWitness)) =>
NativeV0UTXOSpendingInfoDb(id, outpoint, output, path, scriptWitness) SegwitV0SpendingInfo(outpoint, output, path, scriptWitness, id)
case (id, case (id,
outpoint, outpoint,
@ -153,15 +168,15 @@ case class UTXOSpendingInfoTable(tag: Tag)
None, // RedeemScript None, // RedeemScript
None // ScriptWitness None // ScriptWitness
) => ) =>
LegacyUTXOSpendingInfoDb(id, outpoint, output, path) LegacySpendingInfo(outpoint, output, path, id)
case (id, outpoint, output, path, spkOpt, swOpt) => case (id, outpoint, output, path, spkOpt, swOpt) =>
throw new IllegalArgumentException( throw new IllegalArgumentException(
"Could not construct UtxoSpendingInfoDb from bad tuple:" "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 => utxo =>
Some( Some(
(utxo.id, (utxo.id,
@ -171,6 +186,6 @@ case class UTXOSpendingInfoTable(tag: Tag)
utxo.redeemScriptOpt, utxo.redeemScriptOpt,
utxo.scriptWitnessOpt)) utxo.scriptWitnessOpt))
def * : ProvenShape[UTXOSpendingInfoDb] = def * : ProvenShape[SpendingInfoDb] =
(id.?, outPoint, output, privKeyPath, redeemScriptOpt, scriptWitnessOpt) <> (fromTuple, toTuple) (id.?, outPoint, output, privKeyPath, redeemScriptOpt, scriptWitnessOpt) <> (fromTuple, toTuple)
} }