mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-02-22 14:33:06 +01:00
Merge pull request #534 from torkelrogstad/2019-06-17-wallet-transactions
Process incoming transactions
This commit is contained in:
commit
d5a7b7aa0f
32 changed files with 642 additions and 154 deletions
|
@ -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 {
|
||||
|
|
|
@ -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)))
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 ║ -->
|
||||
<!-- ╚═════════════════╝ -->
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"))
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
34
testkit/src/main/scala/org/bitcoins/testkit/Implicits.scala
Normal file
34
testkit/src/main/scala/org/bitcoins/testkit/Implicits.scala
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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 { _ =>
|
||||
???
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]]
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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]
|
||||
}
|
|
@ -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]
|
||||
}
|
|
@ -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)
|
||||
}
|
Loading…
Add table
Reference in a new issue