Process outgoing transactions (#555)

* Split wallet functionality into multiple traits

In this commit we refactor LockedWallet into multiple traits
that provide functionality related to a subset of total wallet
functionality. This has the benefit of making it clear which
methods are helper methods that are only intended to be used
in a very specific setting, and which methods are part of the
internal wallet API that other parts of the wallet can use.

* Rework TransactionOutput and TransactionOutPoint to case classes

* Add extension methods for flattening lists of assertions

* Segregate confirmed and unconfirmed balance methods

* Add test for FutureUtil.sequentially

* Add trace logging of balance fetching

* Process outgoing TXOs

Move TX processing into separate trait, add internal API method

Unify DB representation of TXOs

    Prior to this commit we stored TXO information
    across diferent tables, with joins and tuples
    needed a bunch of places to keep track of
    everything we needed. In this commit we unify
    the tables, leaving us with only one table for
    TXOs.
This commit is contained in:
Torkel Rogstad 2019-07-09 13:25:24 +02:00 committed by Chris Stewart
parent 3556788cc6
commit 9101aece9b
32 changed files with 1312 additions and 932 deletions

View file

@ -0,0 +1,53 @@
package org.bitcoins.core.util
import org.bitcoins.testkit.util.BitcoinSUnitTest
import scala.concurrent.duration._
import scala.concurrent._
import org.scalatest.compatible.Assertion
import org.scalatest.AsyncFlatSpec
import akka.actor.ActorSystem
class FutureUtilTest extends AsyncFlatSpec with BitcoinSLogger {
it must "execute futures sequentially in the correct order" in {
val actorSystem = ActorSystem()
implicit val ec = actorSystem.dispatcher
val scheduler = actorSystem.scheduler
val assertionP = Promise[Assertion]()
val assertionF = assertionP.future
val promise1 = Promise[Unit]()
val promise2 = Promise[Unit]()
val future1 = promise1.future
val future2 = promise2.future
future1.onComplete { _ =>
if (future2.isCompleted) {
assertionP.failure(new Error(s"future2 completed before future1"))
}
}
future2.onComplete { _ =>
if (!future1.isCompleted) {
assertionP.failure(
new Error(s"future1 was not complete by future2 completing"))
} else {
assertionP.success(succeed)
}
}
val futs = FutureUtil.sequentially(List(1, 2)) {
case 1 =>
promise1.success(())
Future.successful(1)
case 2 =>
promise2.success(())
Future.successful(2)
}
futs.map(xs => assert(List(1, 2) == xs)).flatMap(_ => assertionF)
}
}

View file

@ -150,10 +150,10 @@ class BitcoinTxBuilderTest extends AsyncFlatSpec with MustMatchers {
it must "be able to create a BitcoinTxBuilder from UTXOTuple and UTXOMap" in {
val creditingOutput =
TransactionOutput(currencyUnit = CurrencyUnits.zero, scriptPubKey = spk)
TransactionOutput(value = CurrencyUnits.zero, scriptPubKey = spk)
val destinations = {
Seq(
TransactionOutput(currencyUnit = Satoshis.one,
TransactionOutput(value = Satoshis.one,
scriptPubKey = EmptyScriptPubKey))
}
val creditingTx = BaseTransaction(version = tc.validLockVersion,
@ -297,7 +297,7 @@ class BitcoinTxBuilderTest extends AsyncFlatSpec with MustMatchers {
outputs = Seq(creditingOutput),
lockTime = tc.lockTime)
val outPoint =
TransactionOutPoint(txId = creditingTx.txId, index = UInt32.zero)
TransactionOutPoint(txId = creditingTx.txId, vout = UInt32.zero)
val utxo = BitcoinUTXOSpendingInfo(
outPoint = outPoint,
output = creditingOutput,
@ -322,18 +322,18 @@ class BitcoinTxBuilderTest extends AsyncFlatSpec with MustMatchers {
it must "fail to sign a p2wpkh if we don't pass in the public key" in {
val p2wpkh = P2WPKHWitnessSPKV0(pubKey = privKey.publicKey)
val creditingOutput = TransactionOutput(currencyUnit = CurrencyUnits.zero,
scriptPubKey = p2wpkh)
val creditingOutput =
TransactionOutput(value = CurrencyUnits.zero, scriptPubKey = p2wpkh)
val destinations =
Seq(
TransactionOutput(currencyUnit = Satoshis.one,
TransactionOutput(value = Satoshis.one,
scriptPubKey = EmptyScriptPubKey))
val creditingTx = BaseTransaction(version = tc.validLockVersion,
inputs = Nil,
outputs = Seq(creditingOutput),
lockTime = tc.lockTime)
val outPoint =
TransactionOutPoint(txId = creditingTx.txId, index = UInt32.zero)
TransactionOutPoint(txId = creditingTx.txId, vout = UInt32.zero)
val utxo = BitcoinUTXOSpendingInfo(
outPoint = outPoint,
output = creditingOutput,

View file

@ -4,58 +4,47 @@ import org.bitcoins.core.crypto.{DoubleSha256Digest, DoubleSha256DigestBE}
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.NetworkElement
import org.bitcoins.core.serializers.transaction.RawTransactionOutPointParser
import org.bitcoins.core.util.{BitcoinSUtil, Factory}
import scodec.bits.ByteVector
import org.bitcoins.core.util.Factory
import scodec.bits._
/**
* Created by chris on 12/26/15.
*
* @param The transaction id for the crediting transaction for this input
* @param vout The output index in the parent transaction for the output we are spending
*/
sealed abstract class TransactionOutPoint extends NetworkElement {
/** The transaction id for the crediting transaction for this input */
def txId: DoubleSha256Digest
case class TransactionOutPoint(txId: DoubleSha256Digest, vout: UInt32)
extends NetworkElement {
def txIdBE: DoubleSha256DigestBE = txId.flip
/** The output index in the parent transaction for the output we are spending */
def vout: UInt32
override def bytes = RawTransactionOutPointParser.write(this)
override def toString: String =
s"TransactionOutPoint(${txIdBE.hex}:${vout.toBigInt})"
}
/**
* UInt32s cannot hold negative numbers, but sometimes the Bitcoin Protocol requires the vout to be -1, which is serialized
* as "0xFFFFFFFF".
* https://github.com/bitcoin/bitcoin/blob/d612837814020ae832499d18e6ee5eb919a87907/src/primitives/transaction.h
* http://stackoverflow.com/questions/2711522/what-happens-if-i-assign-a-negative-value-to-an-unsigned-variable
* UInt32s cannot hold negative numbers, but sometimes the Bitcoin Protocol
* requires the vout to be -1, which is serialized as `0xFFFFFFFF`.
*
* @see [[https://github.com/bitcoin/bitcoin/blob/d612837814020ae832499d18e6ee5eb919a87907/src/primitives/transaction.h transaction.h]]
* @see http://stackoverflow.com/questions/2711522/what-happens-if-i-assign-a-negative-value-to-an-unsigned-variable
*/
final case object EmptyTransactionOutPoint extends TransactionOutPoint {
override val txId: DoubleSha256Digest =
DoubleSha256Digest(
BitcoinSUtil.decodeHex(
"0000000000000000000000000000000000000000000000000000000000000000"))
override val vout: UInt32 = UInt32("ffffffff")
final object EmptyTransactionOutPoint
extends TransactionOutPoint(txId = DoubleSha256Digest.empty,
vout = UInt32.max) {
override def toString(): String = "EmptyTransactionOutPoint"
}
object TransactionOutPoint extends Factory[TransactionOutPoint] {
private case class TransactionOutPointImpl(
txId: DoubleSha256Digest,
vout: UInt32)
extends TransactionOutPoint
def fromBytes(bytes: ByteVector): TransactionOutPoint =
RawTransactionOutPointParser.read(bytes)
def apply(txId: DoubleSha256Digest, index: UInt32): TransactionOutPoint = {
if (txId == EmptyTransactionOutPoint.txId && index == EmptyTransactionOutPoint.vout) {
EmptyTransactionOutPoint
} else TransactionOutPointImpl(txId, index)
}
def apply(txId: DoubleSha256DigestBE, index: UInt32): TransactionOutPoint = {
TransactionOutPoint(txId.flip, index)
/**
* @param The transaction id for the crediting transaction for this input
* @param vout The output index in the parent transaction for the output we are spending
*/
def apply(txId: DoubleSha256DigestBE, vout: UInt32): TransactionOutPoint = {
TransactionOutPoint(txId.flip, vout)
}
}

View file

@ -7,13 +7,8 @@ import org.bitcoins.core.serializers.transaction.RawTransactionOutputParser
import org.bitcoins.core.util.Factory
import scodec.bits.ByteVector
/**
* Created by chris on 12/26/15.
*/
sealed abstract class TransactionOutput extends NetworkElement {
def value: CurrencyUnit
def scriptPubKey: ScriptPubKey
case class TransactionOutput(value: CurrencyUnit, scriptPubKey: ScriptPubKey)
extends NetworkElement {
//https://bitcoin.org/en/developer-reference#txout
override def size = scriptPubKey.size + 8
@ -21,23 +16,14 @@ sealed abstract class TransactionOutput extends NetworkElement {
override def bytes = RawTransactionOutputParser.write(this)
}
case object EmptyTransactionOutput extends TransactionOutput {
override def value = CurrencyUnits.negativeSatoshi
override def scriptPubKey = ScriptPubKey.empty
final object EmptyTransactionOutput
extends TransactionOutput(CurrencyUnits.negativeSatoshi, ScriptPubKey.empty) {
override def toString(): String = "EmptyTransactionOutput"
}
object TransactionOutput extends Factory[TransactionOutput] {
private case class TransactionOutputImpl(
value: CurrencyUnit,
scriptPubKey: ScriptPubKey)
extends TransactionOutput
def fromBytes(bytes: ByteVector): TransactionOutput =
RawTransactionOutputParser.read(bytes)
def apply(
currencyUnit: CurrencyUnit,
scriptPubKey: ScriptPubKey): TransactionOutput = {
TransactionOutputImpl(currencyUnit, scriptPubKey)
}
}

View file

@ -21,6 +21,8 @@ import org.bitcoins.core.hd.SegWitHDPath
import slick.jdbc.GetResult
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.currency.CurrencyUnit
import org.bitcoins.core.number.Int64
abstract class DbCommonsColumnMappers {
@ -164,6 +166,10 @@ abstract class DbCommonsColumnMappers {
implicit val txMapper: BaseColumnType[Transaction] =
MappedColumnType.base[Transaction, String](_.hex, Transaction.fromHex)
implicit val currencyUnitMapper: BaseColumnType[CurrencyUnit] =
MappedColumnType
.base[CurrencyUnit, Long](_.satoshis.toLong, l => Satoshis(Int64(l)))
}
object DbCommonsColumnMappers extends DbCommonsColumnMappers

View file

@ -36,7 +36,7 @@ val amount = 10000.satoshis
// this is the UTXO we are going to be spending
val utxo =
TransactionOutput(currencyUnit = amount, scriptPubKey = creditingSpk)
TransactionOutput(value = amount, scriptPubKey = creditingSpk)
// the private key that locks the funds for the script we are spending too
val destinationPrivKey = ECPrivateKey.freshPrivateKey
@ -52,7 +52,7 @@ val destinationSPK =
// we could add more destinations here if we
// wanted to batch transactions
val destinations = {
val destination1 = TransactionOutput(currencyUnit = destinationAmount,
val destination1 = TransactionOutput(value = destinationAmount,
scriptPubKey = destinationSPK)
List(destination1)

View file

@ -2,6 +2,8 @@ package org.bitcoins.testkit
import org.scalacheck.Gen
import scala.annotation.tailrec
import org.scalatest.compatible.Assertion
import org.scalatest.exceptions.TestFailedException
/**
* Provides extension methods, syntax
@ -10,7 +12,7 @@ import scala.annotation.tailrec
*/
object Implicits {
/** Extension methods for Scalacheck generatos */
/** Extension methods for Scalacheck generators */
implicit class GeneratorOps[T](private val gen: Gen[T]) extends AnyVal {
/** Gets a sample from this generator that's not `None` */
@ -31,4 +33,22 @@ object Implicits {
loop(0)
}
}
/** Extension methods for sequences of assertions */
implicit class AssertionSeqOps(private val assertions: Seq[Assertion]) {
/** Flattens a sequence of assertions into only one */
def toAssertion: Assertion = assertions match {
case Seq() =>
throw new TestFailedException(
message = "Cannot turn an empty list into an assertion!",
failedCodeStackDepth = 0)
// this should force all collection kinds to
// evaluate all their members, throwing when
// evaluating a bad one
case nonEmpty =>
nonEmpty.foreach(_ => ())
nonEmpty.last
}
}
}

View file

@ -5,13 +5,10 @@ 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 {
@ -19,10 +16,8 @@ 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)
WalletDAOs(account, address, utxo)
}
final override type FixtureParam = WalletDAOs

View file

@ -4,30 +4,30 @@ 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,
RegTestNetChainParams
}
import org.bitcoins.core.protocol.script.ScriptPubKey
import org.bitcoins.testkit.core.gen.CryptoGenerators
import org.bitcoins.wallet.models.AccountDb
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
import org.bitcoins.testkit.core.gen.NumberGenerator
import org.scalacheck.Gen
import org.bitcoins.core.protocol.script.ScriptPubKey
import org.bitcoins.testkit.fixtures.WalletDAOs
import scala.concurrent.Future
import scala.concurrent.ExecutionContext
import org.bitcoins.wallet.models.SegWitAddressDb
import org.bitcoins.core.protocol.Bech32Address
import org.bitcoins.core.protocol.script.P2WPKHWitnessSPKV0
import org.bitcoins.core.util.CryptoUtil
import org.bitcoins.wallet.models.AddressDb
object WalletTestUtil {
@ -67,94 +67,89 @@ object WalletTestUtil {
HDChainType.Change,
addressIndex = 0)
def freshXpub(): ExtPublicKey =
private def freshXpub(): ExtPublicKey =
CryptoGenerators.extPublicKey.sampleSome
val firstAccount = HDAccount(HDCoin(HDPurposes.SegWit, hdCoinType), 0)
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")
private def randomScriptWitness: ScriptWitness =
P2WPKHWitnessV0(freshXpub().key)
lazy val sampleScriptWitness: ScriptWitness = P2WPKHWitnessV0(freshXpub().key)
private def randomTXID = CryptoGenerators.doubleSha256Digest.sampleSome.flip
private def randomVout = NumberGenerator.uInt32s.sampleSome
lazy val sampleSegwitUTXO: SegwitV0SpendingInfo = {
val outpoint =
TransactionOutPoint(WalletTestUtil.sampleTxid, WalletTestUtil.sampleVout)
val output = TransactionOutput(1.bitcoin, WalletTestUtil.sampleSPK)
val scriptWitness = WalletTestUtil.sampleScriptWitness
/** Between 0 and 10 confirmations */
private def randomConfs: Int = Gen.choose(0, 10).sampleSome
private def randomSpent: Boolean = math.random > 0.5
def sampleSegwitUTXO(spk: ScriptPubKey): SegwitV0SpendingInfo = {
val outpoint = TransactionOutPoint(randomTXID, randomVout)
val output =
TransactionOutput(1.bitcoin, spk)
val scriptWitness = randomScriptWitness
val privkeyPath = WalletTestUtil.sampleSegwitPath
SegwitV0SpendingInfo(outPoint = outpoint,
SegwitV0SpendingInfo(confirmations = randomConfs,
spent = randomSpent,
txid = randomTXID,
outPoint = outpoint,
output = output,
privKeyPath = privkeyPath,
scriptWitness = scriptWitness)
}
lazy val sampleLegacyUTXO: LegacySpendingInfo = {
def sampleLegacyUTXO(spk: ScriptPubKey): LegacySpendingInfo = {
val outpoint =
TransactionOutPoint(WalletTestUtil.sampleTxid, WalletTestUtil.sampleVout)
val output = TransactionOutput(1.bitcoin, WalletTestUtil.sampleSPK)
TransactionOutPoint(randomTXID, randomVout)
val output =
TransactionOutput(1.bitcoin, spk)
val privKeyPath = WalletTestUtil.sampleLegacyPath
LegacySpendingInfo(outPoint = outpoint,
LegacySpendingInfo(confirmations = randomConfs,
spent = randomSpent,
txid = randomTXID,
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)] = {
/** Given an account returns a sample address */
def getAddressDb(account: AccountDb): AddressDb = {
val path = SegWitHDPath(WalletTestUtil.hdCoinType,
chainType = HDChainType.External,
accountIndex = account.hdAccount.index,
addressIndex = 0)
val pubkey: ECPublicKey = ECPublicKey.freshPublicKey
val hashedPubkey = CryptoUtil.sha256Hash160(pubkey.bytes)
val wspk = P2WPKHWitnessSPKV0(pubkey)
val scriptWitness = P2WPKHWitnessV0(pubkey)
val address = Bech32Address.apply(wspk, WalletTestUtil.networkParam)
require(utxo.id.isDefined)
SegWitAddressDb(path = path,
ecPublicKey = pubkey,
hashedPubkey,
address,
scriptWitness,
scriptPubKey = wspk)
}
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)
/** Inserts an account, address and finally a UTXO */
def insertLegacyUTXO(daos: WalletDAOs)(
implicit ec: ExecutionContext): Future[LegacySpendingInfo] = {
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)
account <- daos.accountDAO.create(WalletTestUtil.firstAccountDb)
addr <- daos.addressDAO.create(getAddressDb(account))
utxo <- daos.utxoDAO.create(sampleLegacyUTXO(addr.scriptPubKey))
} yield utxo.asInstanceOf[LegacySpendingInfo]
}
assert(foundAddr == address)
(foundTxo, foundAddr)
}
/** Inserts an account, address and finally a UTXO */
def insertSegWitUTXO(daos: WalletDAOs)(
implicit ec: ExecutionContext): Future[SegwitV0SpendingInfo] = {
for {
account <- daos.accountDAO.create(WalletTestUtil.firstAccountDb)
addr <- daos.addressDAO.create(getAddressDb(account))
utxo <- daos.utxoDAO.create(sampleSegwitUTXO(addr.scriptPubKey))
} yield utxo.asInstanceOf[SegwitV0SpendingInfo]
}
}

View file

@ -0,0 +1,51 @@
package org.bitcoins.testkit
import util.BitcoinSUnitTest
import Implicits._
import org.scalatest.exceptions.TestFailedException
import ammonite.terminal.LazyList
class ImplicitsTest extends BitcoinSUnitTest {
behavior of "AssertionSeqOps"
it should "flatten succeeded assertions" in {
val assertions = List(succeed, assert(true), assert(4 + 4 == 8))
assertions.toAssertion
}
it should "fail to flatten a strict sequence of assertions where one has failed" in {
try {
val assertions: List[org.scalatest.Assertion] =
List(succeed, assert(4 + 4 == 7), assert(true))
assertions.toAssertion
} catch {
case e: TestFailedException =>
succeed
case e: Throwable => fail
}
}
it should "fail to flatten a lazy sequence of assertions where one has failed" in {
try {
val assertions: Stream[org.scalatest.Assertion] =
(0 until 10).toStream.map { i =>
if (i == 7) assert(false) else assert(true)
}
assertions.toAssertion
} catch {
case e: TestFailedException =>
succeed
case e: Throwable => fail
}
}
it should "fail to flatten an empty list" in {
intercept[IllegalArgumentException] {
val xs = List.empty[org.scalatest.Assertion]
xs.toAssertion
}
}
}

View file

@ -49,7 +49,7 @@ class ProcessTransactionTest extends BitcoinSWalletTest {
.sampleSome
_ <- wallet.processTransaction(tx, confirmations = 0)
oldBalance <- wallet.getBalance()
oldConfirmed <- wallet.getConfirmedBalance()
oldUnconfirmed <- wallet.getUnconfirmedBalance()
// repeating the action should not make a difference
@ -58,7 +58,7 @@ class ProcessTransactionTest extends BitcoinSWalletTest {
}
_ <- wallet.processTransaction(tx, confirmations = 3)
newBalance <- wallet.getBalance()
newConfirmed <- wallet.getConfirmedBalance()
newUnconfirmed <- wallet.getUnconfirmedBalance()
utxosPostAdd <- wallet.listUtxos()
@ -71,7 +71,7 @@ class ProcessTransactionTest extends BitcoinSWalletTest {
tx.outputs.filter(_.scriptPubKey == address.scriptPubKey)
assert(utxosPostAdd.length == ourOutputs.length)
assert(newBalance != oldBalance)
assert(newConfirmed != oldConfirmed)
assert(newUnconfirmed != oldUnconfirmed)
}
}

View file

@ -9,6 +9,7 @@ import org.bitcoins.testkit.wallet.BitcoinSWalletTest
import org.scalatest.FutureOutcome
import scala.concurrent.Future
import org.bitcoins.core.hd.HDChainType
class WalletIntegrationTest extends BitcoinSWalletTest {
@ -21,12 +22,31 @@ class WalletIntegrationTest extends BitcoinSWalletTest {
val feeRate = SatoshisPerByte(Satoshis.one)
/** Checks that the given vaues are the same-ish, save for fee-level deviations */
private def isCloseEnough(
first: CurrencyUnit,
second: CurrencyUnit,
delta: CurrencyUnit = 1000.sats): Boolean = {
val diff =
if (first > second) {
first - second
} else if (first < second) {
second - first
} else 0.sats
diff < delta
}
it should ("create an address, receive funds to it from bitcoind, import the"
+ " UTXO and construct a valid, signed transaction that's"
+ " broadcast and confirmed by bitcoind") in { walletWithBitcoind =>
val WalletWithBitcoind(wallet, bitcoind) = walletWithBitcoind
// the amount we're receiving from bitcoind
val valueFromBitcoind = Bitcoins.one
// the amount we're sending to bitcoind
val valueToBitcoind = Bitcoins(0.5)
for {
addr <- wallet.getNewAddress()
@ -34,6 +54,7 @@ class WalletIntegrationTest extends BitcoinSWalletTest {
.sendToAddress(addr, valueFromBitcoind)
.flatMap(bitcoind.getRawTransactionRaw(_))
// before processing TX, wallet should be completely empty
_ <- wallet.listUtxos().map(utxos => assert(utxos.isEmpty))
_ <- wallet.getBalance().map(confirmed => assert(confirmed == 0.bitcoin))
_ <- wallet
@ -43,9 +64,13 @@ class WalletIntegrationTest extends BitcoinSWalletTest {
// after this, tx is unconfirmed in wallet
_ <- wallet.processTransaction(tx, confirmations = 0)
// we should now have one UTXO in the wallet
// it should not be confirmed
utxosPostAdd <- wallet.listUtxos()
_ = assert(utxosPostAdd.nonEmpty)
_ <- wallet.getBalance().map(confirmed => assert(confirmed == 0.bitcoin))
_ = assert(utxosPostAdd.length == 1)
_ <- wallet
.getConfirmedBalance()
.map(confirmed => assert(confirmed == 0.bitcoin))
_ <- wallet
.getUnconfirmedBalance()
.map(unconfirmed => assert(unconfirmed == valueFromBitcoind))
@ -67,14 +92,30 @@ class WalletIntegrationTest extends BitcoinSWalletTest {
.getUnconfirmedBalance()
.map(unconfirmed => assert(unconfirmed == 0.bitcoin))
addressFromBitcoind <- bitcoind.getNewAddress
signedTx <- wallet.sendToAddress(addressFromBitcoind,
Bitcoins(0.5),
feeRate)
signedTx <- bitcoind.getNewAddress.flatMap {
wallet.sendToAddress(_, valueToBitcoind, feeRate)
}
txid <- bitcoind.sendRawTransaction(signedTx)
_ <- bitcoind.generate(1)
tx <- bitcoind.getRawTransaction(txid)
_ <- wallet.listUtxos().map {
case utxo +: Vector() =>
assert(utxo.privKeyPath.chain.chainType == HDChainType.Change)
case other => fail(s"Found ${other.length} utxos!")
}
balancePostSend <- wallet.getBalance()
_ = {
// change UTXO should be smaller than what we had, but still have money in it
assert(balancePostSend > 0.sats)
assert(balancePostSend < valueFromBitcoind)
assert(
isCloseEnough(balancePostSend, valueFromBitcoind - valueToBitcoind))
}
} yield {
assert(tx.confirmations.exists(_ > 0))
}

View file

@ -10,6 +10,8 @@ import org.bitcoins.testkit.wallet.{BitcoinSWalletTest, WalletTestUtil}
import org.scalatest.FutureOutcome
import org.bitcoins.wallet.models.SpendingInfoDb
import org.bitcoins.wallet.models.SegwitV0SpendingInfo
import org.bitcoins.testkit.Implicits._
import org.bitcoins.testkit.core.gen.CryptoGenerators
class CoinSelectorTest extends BitcoinSWalletTest {
case class CoinSelectionFixture(
@ -28,25 +30,34 @@ class CoinSelectorTest extends BitcoinSWalletTest {
val feeRate = SatoshisPerByte(CurrencyUnits.zero)
val utxo1 = SegwitV0SpendingInfo(
confirmations = 0,
txid = CryptoGenerators.doubleSha256Digest.sampleSome.flip,
spent = false,
id = Some(1),
outPoint = TransactionGenerators.outPoint.sample.get,
outPoint = TransactionGenerators.outPoint.sampleSome,
output = TransactionOutput(10.sats, ScriptPubKey.empty),
privKeyPath = WalletTestUtil.sampleSegwitPath,
scriptWitness = WitnessGenerators.scriptWitness.sample.get
scriptWitness = WitnessGenerators.scriptWitness.sampleSome
)
val utxo2 = SegwitV0SpendingInfo(
confirmations = 0,
txid = CryptoGenerators.doubleSha256Digest.sampleSome.flip,
spent = false,
id = Some(2),
outPoint = TransactionGenerators.outPoint.sample.get,
outPoint = TransactionGenerators.outPoint.sampleSome,
output = TransactionOutput(90.sats, ScriptPubKey.empty),
privKeyPath = WalletTestUtil.sampleSegwitPath,
scriptWitness = WitnessGenerators.scriptWitness.sample.get
scriptWitness = WitnessGenerators.scriptWitness.sampleSome
)
val utxo3 = SegwitV0SpendingInfo(
confirmations = 0,
txid = CryptoGenerators.doubleSha256Digest.sampleSome.flip,
spent = false,
id = Some(3),
outPoint = TransactionGenerators.outPoint.sample.get,
outPoint = TransactionGenerators.outPoint.sampleSome,
output = TransactionOutput(20.sats, ScriptPubKey.empty),
privKeyPath = WalletTestUtil.sampleSegwitPath,
scriptWitness = WitnessGenerators.scriptWitness.sample.get
scriptWitness = WitnessGenerators.scriptWitness.sampleSome
)
test(CoinSelectionFixture(output, feeRate, utxo1, utxo2, utxo3))

View file

@ -20,34 +20,14 @@ import org.bitcoins.core.protocol.script.P2WPKHWitnessV0
class AddressDAOTest extends BitcoinSWalletTest with WalletDAOFixture {
// todo: do this with an actual working address
// todo: with script witness + redeem script
private def getAddressDb(account: AccountDb): AddressDb = {
val path = SegWitHDPath(WalletTestUtil.hdCoinType,
chainType = HDChainType.External,
accountIndex = account.hdAccount.index,
addressIndex = 0)
val pubkey: ECPublicKey = ECPublicKey.freshPublicKey
val hashedPubkey = CryptoUtil.sha256Hash160(pubkey.bytes)
val wspk = P2WPKHWitnessSPKV0(pubkey)
val scriptWitness = P2WPKHWitnessV0(pubkey)
val address = Bech32Address.apply(wspk, WalletTestUtil.networkParam)
SegWitAddressDb(path = path,
ecPublicKey = pubkey,
hashedPubkey,
address,
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.addressDAO
val readF = {
val addressDb = getAddressDb(WalletTestUtil.firstAccountDb)
val addressDb =
WalletTestUtil.getAddressDb(WalletTestUtil.firstAccountDb)
addressDAO.create(addressDb)
}
@ -64,7 +44,7 @@ class AddressDAOTest extends BitcoinSWalletTest with WalletDAOFixture {
accountDAO.create(account)
}
createdAddress <- {
val addressDb = getAddressDb(createdAccount)
val addressDb = WalletTestUtil.getAddressDb(createdAccount)
addressDAO.create(addressDb)
}
readAddress <- addressDAO.read(createdAddress.address)

View file

@ -1,29 +0,0 @@
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

@ -9,6 +9,12 @@ import org.bitcoins.testkit.fixtures.WalletDAOFixture
import org.bitcoins.wallet.Wallet
import org.bitcoins.testkit.wallet.WalletTestUtil
import org.bitcoins.testkit.wallet.BitcoinSWalletTest
import org.bitcoins.testkit.fixtures.WalletDAOs
import org.bitcoins.testkit.core.gen.TransactionGenerators
import org.bitcoins.core.protocol.transaction.TransactionInput
import org.bitcoins.core.protocol.script.ScriptSignature
import org.bitcoins.core.protocol.transaction.BaseTransaction
import org.bitcoins.testkit.Implicits._
class SpendingInfoDAOTest extends BitcoinSWalletTest with WalletDAOFixture {
behavior of "SpendingInfoDAO"
@ -17,17 +23,110 @@ class SpendingInfoDAOTest extends BitcoinSWalletTest with WalletDAOFixture {
val utxoDAO = daos.utxoDAO
for {
created <- utxoDAO.create(WalletTestUtil.sampleSegwitUTXO)
created <- WalletTestUtil.insertSegWitUTXO(daos)
read <- utxoDAO.read(created.id.get)
} yield assert(read.contains(created))
} yield
read match {
case None => fail(s"Did not read back a UTXO")
case Some(_: SegwitV0SpendingInfo) => succeed
case Some(other) => fail(s"did not get segwit UTXO: $other")
}
}
it should "insert a legacy UTXO and read it" in { daos =>
val utxoDAO = daos.utxoDAO
for {
created <- utxoDAO.create(WalletTestUtil.sampleLegacyUTXO)
created <- WalletTestUtil.insertLegacyUTXO(daos)
read <- utxoDAO.read(created.id.get)
} yield assert(read.contains(created))
} yield
read match {
case None => fail(s"Did not read back a UTXO")
case Some(_: LegacySpendingInfo) => succeed
case Some(other) => fail(s"did not get a legacy UTXO: $other")
}
}
it should "find incoming outputs being spent, given a TX" in { daos =>
val WalletDAOs(_, _, utxoDAO) = daos
for {
utxo <- WalletTestUtil.insertLegacyUTXO(daos)
transaction = {
val randomTX = TransactionGenerators.transaction
.suchThat(_.inputs.nonEmpty)
.sampleSome
val inputs = {
val head = randomTX.inputs.head
val ourInput =
TransactionInput(utxo.outPoint,
ScriptSignature.empty,
head.sequence)
ourInput +: randomTX.inputs.tail
}
BaseTransaction(randomTX.version,
inputs = inputs,
outputs = randomTX.outputs,
lockTime = randomTX.lockTime)
}
txos <- utxoDAO.findOutputsBeingSpent(transaction)
} yield {
txos.map {
case txo =>
assert(transaction.inputs.exists(_.previousOutput == txo.outPoint))
}.toAssertion
}
}
it must "insert an unspent TXO and then mark it as spent" in { daos =>
val WalletDAOs(_, _, spendingInfoDAO) = daos
for {
utxo <- WalletTestUtil.insertSegWitUTXO(daos)
updated <- spendingInfoDAO.update(utxo.copy(spent = false))
unspent <- spendingInfoDAO.findAllUnspent()
updated <- spendingInfoDAO.markAsSpent(unspent.map(_.output))
unspentPostUpdate <- spendingInfoDAO.findAllUnspent()
} yield {
assert(unspent.nonEmpty)
assert(updated.length == unspent.length)
assert(unspentPostUpdate.isEmpty)
}
}
it must "insert an unspent TXO and find it as unspent" in { daos =>
val WalletDAOs(_, _, spendingInfoDAO) = daos
for {
utxo <- WalletTestUtil.insertLegacyUTXO(daos)
updated <- spendingInfoDAO.update(utxo.copy(spent = false))
unspent <- spendingInfoDAO.findAllUnspent()
} yield {
assert(unspent.length == 1)
val ourUnspent = unspent.head
assert(ourUnspent == updated)
}
}
it must "insert a spent TXO and NOT find it as unspent" in { daos =>
val WalletDAOs(_, _, spendingInfoDAO) = daos
for {
utxo <- WalletTestUtil.insertLegacyUTXO(daos)
updated <- spendingInfoDAO.update(utxo.copy(spent = true))
unspent <- spendingInfoDAO.findAllUnspent()
} yield assert(unspent.isEmpty)
}
it must "insert a TXO and read it back with through a TXID " in { daos =>
val WalletDAOs(_, _, spendingInfoDAO) = daos
for {
utxo <- WalletTestUtil.insertLegacyUTXO(daos)
foundTxos <- spendingInfoDAO.findTx(utxo.txid)
} yield assert(foundTxos.contains(utxo))
}
it should "insert a nested segwit UTXO and read it" ignore { _ =>

View file

@ -34,11 +34,10 @@ This is meant to be a stand alone project that can be used as a cold storage wal
We store information in the following tables:
- UTXOs - must reference the incoming transaction it was received in
- TXOs - Contains both the information needed to spent it as well as information related
to wallet state (confirmations, spent/unspent etc)
- Addresses - must reference the account it belongs to
- Accounts
- Incoming transactions - must reference the SPK (in our address table) that a TX spends to
- Outgoing transactions - must reference the UTXO(s) it spends
#### Mnemonic encryption

View file

@ -1,71 +1,61 @@
package org.bitcoins.wallet
import org.bitcoins.core.crypto._
import org.bitcoins.core.hd._
import org.bitcoins.core.currency._
import org.bitcoins.core.protocol.blockchain._
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.script.ScriptPubKey
import org.bitcoins.core.protocol.transaction.{
Transaction,
TransactionOutPoint,
TransactionOutput
}
import org.bitcoins.core.util.{BitcoinSLogger, EitherUtil}
import org.bitcoins.wallet.api.AddUtxoError.{AddressNotFound, BadSPK}
import org.bitcoins.wallet.api._
import org.bitcoins.wallet.models._
import scala.concurrent.Future
import scala.util.Success
import scala.util.Failure
import scala.concurrent.ExecutionContext
import org.bitcoins.wallet.ReadMnemonicError.DecryptionError
import org.bitcoins.wallet.ReadMnemonicError.JsonParsingError
import org.bitcoins.wallet.config.WalletAppConfig
import org.bitcoins.core.bloom.BloomFilter
import org.bitcoins.core.bloom.BloomUpdateAll
import slick.jdbc.SQLiteProfile
import org.bitcoins.core.util.FutureUtil
import org.bitcoins.core.util.BitcoinSLogger
import org.bitcoins.wallet.internal._
abstract class LockedWallet extends LockedWalletApi with BitcoinSLogger {
abstract class LockedWallet
extends LockedWalletApi
with UtxoHandling
with AddressHandling
with AccountHandling
with TransactionProcessing
with BitcoinSLogger {
private[wallet] val addressDAO: AddressDAO = AddressDAO()
private[wallet] val accountDAO: AccountDAO = AccountDAO()
private[wallet] val utxoDAO: SpendingInfoDAO = SpendingInfoDAO()
private[wallet] val incomingTxoDAO = IncomingTxoDAO(SQLiteProfile)
private[wallet] val outgoingTxoDAO = OutgoingTxoDAO(SQLiteProfile)
private[wallet] val spendingInfoDAO: SpendingInfoDAO = SpendingInfoDAO()
/** Sums up the value of all incoming
* TXs in the wallet, filtered by the given predicate */
// TODO account for outgoing TXs
/** Sums up the value of all unspent
* TXOs in the wallet, filtered by the given predicate */
private def filterThenSum(
predicate: IncomingWalletTXO => Boolean): Future[CurrencyUnit] = {
for (utxos <- incomingTxoDAO.findAllWithSpendingInfo())
yield
utxos
predicate: SpendingInfoDb => Boolean): Future[CurrencyUnit] = {
for (utxos <- spendingInfoDAO.findAll())
yield {
val filtered = utxos
.collect {
case (txo, spendInfo) if predicate(txo) => spendInfo.value
case (txo) if !txo.spent && predicate(txo) =>
txo.output.value
}
.fold(0.sats)(_ + _)
filtered.fold(0.sats)(_ + _)
}
}
// TODO account for outgoing TXs
override def getBalance(): Future[CurrencyUnit] =
filterThenSum(_.confirmations > 0)
override def getConfirmedBalance(): Future[CurrencyUnit] = {
val confirmed = filterThenSum(_.confirmations > 0)
confirmed.foreach(balance =>
logger.trace(s"Confirmed balance=${balance.satoshis}"))
confirmed
}
// TODO account for outgoing TXs
override def getUnconfirmedBalance(): Future[CurrencyUnit] =
filterThenSum(_.confirmations == 0)
override def getUnconfirmedBalance(): Future[CurrencyUnit] = {
val unconfirmed = filterThenSum(_.confirmations == 0)
unconfirmed.foreach(balance =>
logger.trace(s"Unconfirmed balance=${balance.satoshis}"))
unconfirmed
/** The default HD coin */
private[wallet] lazy val DEFAULT_HD_COIN: HDCoin = {
val coinType = chainParams match {
case MainNetChainParams => HDCoinType.Bitcoin
case RegTestNetChainParams | TestNetChainParams => HDCoinType.Testnet
}
HDCoin(walletConfig.defaultAccountKind, coinType)
}
/**
@ -91,16 +81,6 @@ abstract class LockedWallet extends LockedWalletApi with BitcoinSLogger {
}
}
override def listAccounts(): Future[Vector[AccountDb]] =
accountDAO.findAll()
override def listAddresses(): Future[Vector[AddressDb]] =
addressDAO.findAll()
/** Enumerates the public keys in this wallet */
private[wallet] def listPubkeys(): Future[Vector[ECPublicKey]] =
addressDAO.findAllPubkeys().map(_.toVector)
/** Gets the size of the bloom filter for this wallet */
private def getBloomFilterSize(): Future[Int] = {
for {
@ -133,420 +113,6 @@ abstract class LockedWallet extends LockedWalletApi with BitcoinSLogger {
pubkeys.foldLeft(baseBloom) { _.insert(_) }
}
}
/**
* Tries to convert the provided spk to an address, and then checks if we have
* it in our address table
*/
private def findAddress(
spk: ScriptPubKey): Future[Either[AddUtxoError, AddressDb]] =
BitcoinAddress.fromScriptPubKey(spk, networkParameters) match {
case Success(address) =>
addressDAO.findAddress(address).map {
case Some(addrDb) => Right(addrDb)
case None => Left(AddressNotFound)
}
case Failure(_) => Future.successful(Left(BadSPK))
}
/** Constructs a DB level representation of the given UTXO, and persist it to disk */
private def writeUtxo(
output: TransactionOutput,
outPoint: TransactionOutPoint,
addressDb: AddressDb): Future[SpendingInfoDb] = {
val utxo: SpendingInfoDb = addressDb match {
case segwitAddr: SegWitAddressDb =>
SegwitV0SpendingInfo(
id = None,
outPoint = outPoint,
output = output,
privKeyPath = segwitAddr.path,
scriptWitness = segwitAddr.witnessScript
)
case LegacyAddressDb(path, _, _, _, _) =>
LegacySpendingInfo(outPoint = outPoint,
output = output,
privKeyPath = path)
case nested: NestedSegWitAddressDb =>
throw new IllegalArgumentException(
s"Bad utxo $nested. Note: nested segwit is not implemented")
}
utxoDAO.create(utxo).map { written =>
val writtenOut = written.outPoint
logger.info(
s"Successfully inserted UTXO ${writtenOut.txId.hex}:${writtenOut.vout.toInt} into DB")
logger.info(s"UTXO details: ${written.output}")
written
}
}
private case class OutputWithIndex(output: TransactionOutput, index: Int)
/**
* Processes an incoming transaction that's new to us
* @return A list of inserted transaction outputs
*/
private def processNewIncomingTx(
transaction: Transaction,
confirmations: Int): Future[Vector[IncomingWalletTXO]] = {
addressDAO.findAll().flatMap { addrs =>
val relevantOutsWithIdx: Seq[OutputWithIndex] = {
val withIndex =
transaction.outputs.zipWithIndex
withIndex.collect {
case (out, idx)
if addrs.map(_.scriptPubKey).contains(out.scriptPubKey) =>
OutputWithIndex(out, idx)
}
}
relevantOutsWithIdx match {
case Nil =>
logger.debug(
s"Found no outputs relevant to us in transaction${transaction.txIdBE}")
Future.successful(Vector.empty)
case xs =>
val count = xs.length
val outputStr = {
xs.map { elem =>
s"${transaction.txIdBE.hex}:${elem.index}"
}
.mkString(", ")
}
logger.trace(
s"Found $count relevant output(s) in transaction=${transaction.txIdBE}: $outputStr")
if (xs.length > 1) {
logger.warn(
s"${xs.length} SPKs were relevant to transaction=${transaction.txIdBE}, but we aren't able to handle more than 1 for the time being")
}
val addUTXOsFut: Future[Seq[(SpendingInfoDb, OutputWithIndex)]] =
Future
.sequence {
xs.map(out => processUtxo(transaction, out.index).map(_ -> out))
}
val incomingTXOsFut =
addUTXOsFut.map(createIncomingTxos(transaction, confirmations, _))
val writeIncomingTXOsFut =
incomingTXOsFut.flatMap(incomingTxoDAO.createAll)
writeIncomingTXOsFut
}
}
}
/**
* Inserts the UTXO at the given index into our DB, swallowing the
* error if any (this is because we're operating on data we've
* already verified). This method is only meant as a helper method in
* processing transactions.
*/
// TODO move this into a TX processing trait
def processUtxo(
transaction: Transaction,
index: Int): Future[SpendingInfoDb] =
addUtxo(transaction, UInt32(index))
.flatMap {
case AddUtxoSuccess(utxo) => Future.successful(utxo)
case err: AddUtxoError =>
logger.error(s"Could not add UTXO", err)
Future.failed(err)
}
/**
* @param xs UTXO sequence we want to create
* incoming wallet TXOs for
*/
// TODO move this into a TX processing trait
private def createIncomingTxos(
transaction: Transaction,
confirmations: Int,
xs: Seq[(SpendingInfoDb, OutputWithIndex)]): Vector[IncomingWalletTXO] = {
xs.map {
case (utxo, OutputWithIndex(out, _)) =>
IncomingWalletTXO(
confirmations = confirmations,
// is this always the case?
spent = false,
scriptPubKey = out.scriptPubKey,
txid = transaction.txIdBE,
// always defined, as its freshly
// written to the DB
spendingInfoID = utxo.id.get
)
}.toVector
}
/**
* Processes an incoming transaction that already exists in our wallet.
* If the incoming transaction has more confirmations than what we
* have in the DB, we update the TX
*/
private def processExistingIncomingTxo(
transaction: Transaction,
confirmations: Int,
foundTxo: IncomingWalletTXO): Future[Option[IncomingWalletTXO]] = {
if (foundTxo.confirmations < confirmations) {
// TODO The assumption here is that double-spends never occur. That's not
// the case. This must be fixed when double-spend logic is implemented.
logger.debug(
s"Increasing confirmation count of txo=${transaction.txIdBE}, old=${foundTxo.confirmations} new=${confirmations}")
val updateF =
incomingTxoDAO.update(foundTxo.copy(confirmations = confirmations))
updateF.foreach(tx =>
logger.debug(
s"Updated confirmation count=${tx.confirmations} of output=${foundTxo}"))
updateF.failed.foreach(err =>
logger.error(
s"Failed to update confirmation count of transaction=${transaction.txIdBE}",
err))
updateF.map(Some(_))
} else if (foundTxo.confirmations > confirmations) {
val msg =
List(
s"Incoming transaction=${transaction.txIdBE} has fewer confirmations=$confirmations",
s"than what we already have registered=${foundTxo.confirmations}! I don't know how",
s"to handle this."
).mkString(" ")
logger.warn(msg)
Future.failed(new RuntimeException(msg))
} else {
// TODO: This is bad in the case of double-spends and re-orgs. Come back
// and look at this when implementing logic for those scenarios.
logger.debug(
s"Skipping further processing of transaction=${transaction.txIdBE}, already processed.")
Future.successful(None)
}
}
override def processTransaction(
transaction: Transaction,
confirmations: Int): Future[LockedWallet] = {
logger.info(
s"Processing transaction=${transaction.txIdBE} with confirmations=$confirmations")
val incomingTxoFut: Future[Vector[IncomingWalletTXO]] =
incomingTxoDAO
.findTx(transaction)
.flatMap {
// no existing elements found
case Vector() =>
processNewIncomingTx(transaction, confirmations)
case txos: Vector[IncomingWalletTXO] =>
val txoProcessingFutures: Vector[
Future[Option[IncomingWalletTXO]]] = txos
.map(processExistingIncomingTxo(transaction, confirmations, _))
Future
.sequence(txoProcessingFutures)
.map(_.flatten)
}
val outgoingTxFut: Future[Unit] = {
logger.warn(s"Skipping processing of outgoing TX state!")
FutureUtil.unit
}
val aggregateFut =
for {
incoming <- incomingTxoFut
_ <- outgoingTxFut
} yield {
logger.info(
s"Finished processing of transaction=${transaction.txIdBE}. Relevant incomingTXOs=${incoming.length}")
this
}
aggregateFut.failed.foreach { err =>
val msg = s"Error when processing transaction=${transaction.txIdBE}"
logger.error(msg, err)
}
aggregateFut
}
/**
* Adds the provided UTXO to the wallet, making it
* available for spending.
*/
private def addUtxo(
transaction: Transaction,
vout: UInt32): Future[AddUtxoResult] = {
import AddUtxoError._
import org.bitcoins.core.util.EitherUtil.EitherOps._
logger.info(s"Adding UTXO to wallet: ${transaction.txId.hex}:${vout.toInt}")
// first check: does the provided vout exist in the tx?
val voutIndexOutOfBounds: Boolean = {
val voutLength = transaction.outputs.length
val outOfBunds = voutLength <= vout.toInt
if (outOfBunds)
logger.error(
s"TX with TXID ${transaction.txId.hex} only has $voutLength, got request to add vout ${vout.toInt}!")
outOfBunds
}
if (voutIndexOutOfBounds) {
Future.successful(VoutIndexOutOfBounds)
} else {
val output = transaction.outputs(vout.toInt)
val outPoint = TransactionOutPoint(transaction.txId, vout)
// second check: do we have an address associated with the provided
// output in our DB?
val addressDbEitherF: Future[Either[AddUtxoError, AddressDb]] =
findAddress(output.scriptPubKey)
// insert the UTXO into the DB
addressDbEitherF.flatMap { addressDbE =>
val biasedE: Either[AddUtxoError, Future[SpendingInfoDb]] = for {
addressDb <- addressDbE
} yield writeUtxo(output, outPoint, addressDb)
EitherUtil.liftRightBiasedFutureE(biasedE)
} map {
case Right(utxo) => AddUtxoSuccess(utxo)
case Left(e) => e
}
}
}
/**
* @inheritdoc
*/
// override def updateUtxo: Future[WalletApi] = ???
override def listUtxos(): Future[Vector[SpendingInfoDb]] =
utxoDAO.findAll()
/**
* @param account Account to generate address from
* @param chainType What chain do we generate from? Internal change vs. external
*/
private def getNewAddressHelper(
account: AccountDb,
chainType: HDChainType
): Future[BitcoinAddress] = {
logger.debug(s"Getting new $chainType adddress for ${account.hdAccount}")
val accountIndex = account.hdAccount.index
val lastAddrOptF = chainType match {
case HDChainType.External =>
addressDAO.findMostRecentExternal(accountIndex)
case HDChainType.Change =>
addressDAO.findMostRecentChange(accountIndex)
}
lastAddrOptF.flatMap { lastAddrOpt =>
val addrPath: HDPath = lastAddrOpt match {
case Some(addr) =>
val next = addr.path.next
logger.debug(
s"Found previous address at path=${addr.path}, next=$next")
next
case None =>
val account = HDAccount(DEFAULT_HD_COIN, accountIndex)
val chain = account.toChain(chainType)
val address = HDAddress(chain, 0)
val path = address.toPath
logger.debug(s"Did not find previous address, next=$path")
path
}
val addressDb = {
val pathDiff =
account.hdAccount.diff(addrPath) match {
case Some(value) => value
case None =>
throw new RuntimeException(
s"Could not diff ${account.hdAccount} and $addrPath")
}
val pubkey = account.xpub.deriveChildPubKey(pathDiff) match {
case Failure(exception) => throw exception
case Success(value) => value.key
}
addrPath match {
case segwitPath: SegWitHDPath =>
AddressDbHelper
.getSegwitAddress(pubkey, segwitPath, networkParameters)
case legacyPath: LegacyHDPath =>
AddressDbHelper.getLegacyAddress(pubkey,
legacyPath,
networkParameters)
case nestedPath: NestedSegWitHDPath =>
AddressDbHelper.getNestedSegwitAddress(pubkey,
nestedPath,
networkParameters)
}
}
logger.debug(s"Writing $addressDb to DB")
val writeF = addressDAO.create(addressDb)
writeF.foreach { written =>
logger.debug(
s"Got ${chainType} address ${written.address} at key path ${written.path} with pubkey ${written.ecPublicKey}")
}
writeF.map(_.address)
}
}
/**
* right now only generates P2WPKH addresses
*
* @inheritdoc
*/
override def getNewAddress(account: AccountDb): Future[BitcoinAddress] = {
val addrF = getNewAddressHelper(account, HDChainType.External)
addrF
}
override def getAddressInfo(
address: BitcoinAddress): Future[Option[AddressInfo]] = {
val addressOptF = addressDAO.findAddress(address)
addressOptF.map { addressOpt =>
addressOpt.map { address =>
AddressInfo(pubkey = address.ecPublicKey,
network = address.address.networkParameters,
path = address.path)
}
}
}
/** Generates a new change address */
override protected[wallet] def getNewChangeAddress(
account: AccountDb): Future[BitcoinAddress] = {
getNewAddressHelper(account, HDChainType.Change)
}
/** @inheritdoc */
override protected[wallet] def getDefaultAccount(): Future[AccountDb] = {
for {
account <- accountDAO.read((DEFAULT_HD_COIN, 0))
} yield
account.getOrElse(
throw new RuntimeException(
s"Could not find account with ${DEFAULT_HD_COIN.purpose.constant} " +
s"purpose field and ${DEFAULT_HD_COIN.coinType.toInt} coin field"))
}
}
object LockedWallet {

View file

@ -80,12 +80,17 @@ sealed abstract class Wallet
}
signed <- txBuilder.sign
/* todo: add change output to UTXO DB
_ <- {
val changeVout = ???
addUtxo(signed, changeVout)
} */
ourOuts <- findOurOuts(signed)
// TODO internal
_ <- processOurTransaction(signed, confirmations = 0)
} yield {
logger.debug(
s"Signed transaction=${signed.txIdBE.hex} with outputs=${signed.outputs.length}, inputs=${signed.inputs.length}")
logger.trace(s"Change output(s) for transaction=${signed.txIdBE.hex}")
ourOuts.foreach { out =>
logger.trace(s" $out")
}
signed
}
}

View file

@ -19,7 +19,7 @@ trait CoinSelector {
outputs: Vector[TransactionOutput],
feeRate: FeeUnit): Vector[SpendingInfoDb] = {
val sortedUtxos =
walletUtxos.sortBy(_.value.satoshis.toLong).reverse
walletUtxos.sortBy(_.output.value).reverse
accumulate(sortedUtxos, outputs, feeRate)
}
@ -34,7 +34,7 @@ trait CoinSelector {
walletUtxos: Vector[SpendingInfoDb],
outputs: Vector[TransactionOutput],
feeRate: FeeUnit): Vector[SpendingInfoDb] = {
val sortedUtxos = walletUtxos.sortBy(_.value.satoshis.toLong)
val sortedUtxos = walletUtxos.sortBy(_.output.value)
accumulate(sortedUtxos, outputs, feeRate)
}
@ -64,11 +64,11 @@ trait CoinSelector {
val nextUtxo = utxosLeft.head
val approxUtxoSize = CoinSelector.approximateUtxoSize(nextUtxo)
val nextUtxoFee = feeRate.currencyUnit * approxUtxoSize
if (nextUtxo.value < nextUtxoFee) {
if (nextUtxo.output.value < nextUtxoFee) {
addUtxos(alreadyAdded, valueSoFar, bytesSoFar, utxosLeft.tail)
} else {
val newAdded = alreadyAdded.:+(nextUtxo)
val newValue = valueSoFar + nextUtxo.value
val newValue = valueSoFar + nextUtxo.output.value
addUtxos(newAdded,
newValue,

View file

@ -53,8 +53,19 @@ trait LockedWalletApi extends WalletApi {
transaction: Transaction,
confirmations: Int): Future[LockedWalletApi]
/** Gets the sum of all UTXOs in this wallet */
def getBalance(): Future[CurrencyUnit] = {
val confirmedF = getConfirmedBalance()
val unconfirmedF = getUnconfirmedBalance()
for {
confirmed <- confirmedF
unconfirmed <- unconfirmedF
} yield confirmed + unconfirmed
}
/** Gets the sum of all confirmed UTXOs in this wallet */
def getBalance(): Future[CurrencyUnit]
def getConfirmedBalance(): Future[CurrencyUnit]
/** Gets the sum of all unconfirmed UTXOs in this wallet */
def getUnconfirmedBalance(): Future[CurrencyUnit]
@ -66,6 +77,7 @@ trait LockedWalletApi extends WalletApi {
*/
// def updateUtxo: Future[WalletApi]
/** Lists unspent transaction outputs in the wallet */
def listUtxos(): Future[Vector[SpendingInfoDb]]
def listAddresses(): Future[Vector[AddressDb]]

View file

@ -8,15 +8,9 @@ sealed abstract class WalletDbManagement extends DbManagement {
private val accountTable = TableQuery[AccountTable]
private val addressTable = TableQuery[AddressTable]
private val utxoTable = TableQuery[SpendingInfoTable]
private val incomingTxoTable = TableQuery[IncomingTXOTable]
private val outgoingTxoTable = TableQuery[OutgoingTXOTable]
override val allTables: List[TableQuery[_ <: Table[_]]] =
List(accountTable,
addressTable,
utxoTable,
incomingTxoTable,
outgoingTxoTable)
List(accountTable, addressTable, utxoTable)
}

View file

@ -0,0 +1,41 @@
package org.bitcoins.wallet.internal
import org.bitcoins.wallet.LockedWallet
import scala.concurrent.Future
import org.bitcoins.wallet.models.AccountDb
import org.bitcoins.core.hd.HDCoinType
import org.bitcoins.core.hd.HDCoin
import org.bitcoins.core.protocol.blockchain.TestNetChainParams
import org.bitcoins.core.protocol.blockchain.RegTestNetChainParams
import org.bitcoins.core.protocol.blockchain.MainNetChainParams
/**
* Provides functionality related enumerating accounts. Account
* creation does not happen here, as that requires an unlocked wallet.
*/
private[wallet] trait AccountHandling { self: LockedWallet =>
/** @inheritdoc */
override def listAccounts(): Future[Vector[AccountDb]] =
accountDAO.findAll()
/** @inheritdoc */
override protected[wallet] def getDefaultAccount(): Future[AccountDb] = {
for {
account <- accountDAO.read((DEFAULT_HD_COIN, 0))
} yield
account.getOrElse(
throw new RuntimeException(
s"Could not find account with ${DEFAULT_HD_COIN.purpose.constant} " +
s"purpose field and ${DEFAULT_HD_COIN.coinType.toInt} coin field"))
}
/** The default HD coin for this wallet, read from config */
protected[wallet] lazy val DEFAULT_HD_COIN: HDCoin = {
val coinType = chainParams match {
case MainNetChainParams => HDCoinType.Bitcoin
case RegTestNetChainParams | TestNetChainParams => HDCoinType.Testnet
}
HDCoin(walletConfig.defaultAccountKind, coinType)
}
}

View file

@ -0,0 +1,164 @@
package org.bitcoins.wallet.internal
import org.bitcoins.wallet.LockedWallet
import scala.concurrent.Future
import org.bitcoins.wallet.models.AddressDb
import org.bitcoins.core.crypto.ECPublicKey
import org.bitcoins.wallet.models.AccountDb
import org.bitcoins.core.hd.HDChainType
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.hd.HDPath
import org.bitcoins.core.hd.HDAccount
import org.bitcoins.core.hd.HDAddress
import scala.util.Failure
import scala.util.Success
import org.bitcoins.wallet.models.AddressDbHelper
import org.bitcoins.core.hd.SegWitHDPath
import org.bitcoins.core.hd.LegacyHDPath
import org.bitcoins.core.hd.NestedSegWitHDPath
import org.bitcoins.wallet.api.AddressInfo
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.core.protocol.transaction.TransactionOutput
import org.bitcoins.core.protocol.script.ScriptPubKey
import org.bitcoins.core.protocol.transaction.TransactionOutPoint
import org.bitcoins.core.number.UInt32
/**
* Provides functionality related to addresses. This includes
* enumeratng and creating them, primarily.
*/
private[wallet] trait AddressHandling { self: LockedWallet =>
override def listAddresses(): Future[Vector[AddressDb]] =
addressDAO.findAll()
/** Enumerates the public keys in this wallet */
protected[wallet] def listPubkeys(): Future[Vector[ECPublicKey]] =
addressDAO.findAllPubkeys()
/** Enumerates the scriptPubKeys in this wallet */
protected[wallet] def listSPKs(): Future[Vector[ScriptPubKey]] =
addressDAO.findAllSPKs()
/** Given a transaction, returns the outputs (with their corresponding outpoints)
* that pay to this wallet */
def findOurOuts(transaction: Transaction): Future[
Vector[(TransactionOutput, TransactionOutPoint)]] =
for {
spks <- listSPKs()
} yield
transaction.outputs.zipWithIndex.collect {
case (out, index) if spks.contains(out.scriptPubKey) =>
(out, TransactionOutPoint(transaction.txId, UInt32(index)))
}.toVector
/**
* Derives a new address in the wallet for the
* given account and chain type (change/external).
* After deriving the address it inserts it into our
* table of addresses.
*
* This method is called with the approriate params
* from the public facing methods `getNewChangeAddress`
* and `getNewAddress`.
*
* @param account Account to generate address from
* @param chainType What chain do we generate from? Internal change vs. external
*/
private def getNewAddressHelper(
account: AccountDb,
chainType: HDChainType
): Future[BitcoinAddress] = {
logger.debug(s"Getting new $chainType adddress for ${account.hdAccount}")
val accountIndex = account.hdAccount.index
val lastAddrOptF = chainType match {
case HDChainType.External =>
addressDAO.findMostRecentExternal(accountIndex)
case HDChainType.Change =>
addressDAO.findMostRecentChange(accountIndex)
}
lastAddrOptF.flatMap { lastAddrOpt =>
val addrPath: HDPath = lastAddrOpt match {
case Some(addr) =>
val next = addr.path.next
logger.debug(
s"Found previous address at path=${addr.path}, next=$next")
next
case None =>
val account = HDAccount(DEFAULT_HD_COIN, accountIndex)
val chain = account.toChain(chainType)
val address = HDAddress(chain, 0)
val path = address.toPath
logger.debug(s"Did not find previous address, next=$path")
path
}
val addressDb = {
val pathDiff =
account.hdAccount.diff(addrPath) match {
case Some(value) => value
case None =>
throw new RuntimeException(
s"Could not diff ${account.hdAccount} and $addrPath")
}
val pubkey = account.xpub.deriveChildPubKey(pathDiff) match {
case Failure(exception) => throw exception
case Success(value) => value.key
}
addrPath match {
case segwitPath: SegWitHDPath =>
AddressDbHelper
.getSegwitAddress(pubkey, segwitPath, networkParameters)
case legacyPath: LegacyHDPath =>
AddressDbHelper.getLegacyAddress(pubkey,
legacyPath,
networkParameters)
case nestedPath: NestedSegWitHDPath =>
AddressDbHelper.getNestedSegwitAddress(pubkey,
nestedPath,
networkParameters)
}
}
logger.debug(s"Writing $addressDb to DB")
val writeF = addressDAO.create(addressDb)
writeF.foreach { written =>
logger.debug(
s"Got ${chainType} address ${written.address} at key path ${written.path} with pubkey ${written.ecPublicKey}")
}
writeF.map(_.address)
}
}
/** @inheritdoc */
override def getNewAddress(account: AccountDb): Future[BitcoinAddress] = {
val addrF = getNewAddressHelper(account, HDChainType.External)
addrF
}
/** Generates a new change address */
override protected[wallet] def getNewChangeAddress(
account: AccountDb): Future[BitcoinAddress] = {
getNewAddressHelper(account, HDChainType.Change)
}
/** @inheritdoc */
override def getAddressInfo(
address: BitcoinAddress): Future[Option[AddressInfo]] = {
val addressOptF = addressDAO.findAddress(address)
addressOptF.map { addressOpt =>
addressOpt.map { address =>
AddressInfo(pubkey = address.ecPublicKey,
network = address.address.networkParameters,
path = address.path)
}
}
}
}

View file

@ -0,0 +1,268 @@
package org.bitcoins.wallet.internal
import org.bitcoins.wallet.LockedWallet
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.wallet.models._
import scala.concurrent.Future
import org.bitcoins.core.protocol.transaction.TransactionOutput
import org.bitcoins.wallet.api.AddUtxoSuccess
import org.bitcoins.wallet.api.AddUtxoError
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.util.FutureUtil
/** Provides functionality for processing transactions. This
* includes importing UTXOs spent to our wallet, updating
* confirmation counts and marking UTXOs as spent when
* spending from our wallet
*/
private[wallet] trait TransactionProcessing { self: LockedWallet =>
/////////////////////
// Public facing API
/** @inheritdoc */
override def processTransaction(
transaction: Transaction,
confirmations: Int): Future[LockedWallet] = {
logger.info(
s"Processing transaction=${transaction.txIdBE} with confirmations=$confirmations")
processTransactionImpl(transaction, confirmations).map {
case ProcessTxResult(outgoing, incoming) =>
logger.info(
s"Finished processing of transaction=${transaction.txIdBE}. Relevant incomingTXOs=${incoming.length}, outgoingTXOs=${outgoing.length}")
this
}
}
private[wallet] case class ProcessTxResult(
updatedIncoming: List[SpendingInfoDb],
updatedOutgoing: List[SpendingInfoDb])
/////////////////////
// Internal wallet API
/**
* Processes TXs originating from our wallet.
* This is called right after we've signed a TX,
* updating our UTXO state.
*/
private[wallet] def processOurTransaction(
transaction: Transaction,
confirmations: Int): Future[ProcessTxResult] = {
logger.info(
s"Processing TX from our wallet, transaction=${transaction.txIdBE} with confirmations=$confirmations")
processTransactionImpl(transaction, confirmations).map { result =>
val txid = transaction.txIdBE
val changeOutputs = result.updatedIncoming.length
val spentOutputs = result.updatedOutgoing.length
logger.info(
s"Processing of internal transaction=$txid resulted in changeOutputs=$changeOutputs and spentUTXOs=$spentOutputs")
result
}
}
/////////////////////
// Private methods
/** Does the grunt work of processing a TX.
* This is called by either the internal or public TX
* processing method, which logs and transforms the
* output fittingly.
*/
private def processTransactionImpl(
transaction: Transaction,
confirmations: Int): Future[ProcessTxResult] = {
val incomingTxoFut: Future[Vector[SpendingInfoDb]] =
spendingInfoDAO
.findTx(transaction)
.flatMap {
// no existing elements found
case Vector() =>
processNewIncomingTx(transaction, confirmations).map(_.toVector)
case txos: Vector[SpendingInfoDb] =>
val txoProcessingFutures =
txos
.map(processExistingIncomingTxo(transaction, confirmations, _))
Future
.sequence(txoProcessingFutures)
}
val outgoingTxFut: Future[Vector[SpendingInfoDb]] = {
for {
outputsBeingSpent <- spendingInfoDAO.findOutputsBeingSpent(transaction)
processed <- FutureUtil.sequentially(outputsBeingSpent)(
markAsSpentIfUnspent)
} yield processed.flatten.toVector
}
val aggregateFut =
for {
incoming <- incomingTxoFut
outgoing <- outgoingTxFut
} yield {
ProcessTxResult(incoming.toList, outgoing.toList)
}
aggregateFut.failed.foreach { err =>
val msg = s"Error when processing transaction=${transaction.txIdBE}"
logger.error(msg, err)
}
aggregateFut
}
/** If the given UTXO is marked as unspent, updates
* its spending status. Otherwise returns `None`.
*/
private val markAsSpentIfUnspent: SpendingInfoDb => Future[
Option[SpendingInfoDb]] = { out =>
if (out.spent) {
Future.successful(None)
} else {
val updatedF =
spendingInfoDAO.update(out.copyWithSpent(spent = true))
updatedF.foreach(
updated =>
logger.debug(
s"Marked utxo=${updated.toHumanReadableString} as spent=${updated.spent}")
)
updatedF.map(Some(_))
}
}
/**
* Inserts the UTXO at the given index into our DB, swallowing the
* error if any (this is because we're operating on data we've
* already verified).
*/
private def processUtxo(
transaction: Transaction,
index: Int,
spent: Boolean,
confirmations: Int): Future[SpendingInfoDb] =
addUtxo(transaction,
UInt32(index),
spent = spent,
confirmations = confirmations)
.flatMap {
case AddUtxoSuccess(utxo) => Future.successful(utxo)
case err: AddUtxoError =>
logger.error(s"Could not add UTXO", err)
Future.failed(err)
}
private case class OutputWithIndex(output: TransactionOutput, index: Int)
/**
* Processes an incoming transaction that already exists in our wallet.
* If the incoming transaction has more confirmations than what we
* have in the DB, we update the TX
*/
private def processExistingIncomingTxo(
transaction: Transaction,
confirmations: Int,
foundTxo: SpendingInfoDb): Future[SpendingInfoDb] = {
if (foundTxo.confirmations < confirmations) {
// TODO The assumption here is that double-spends never occur. That's not
// the case. This must be fixed when double-spend logic is implemented.
logger.debug(
s"Increasing confirmation count of txo=${transaction.txIdBE}, old=${foundTxo.confirmations} new=${confirmations}")
val updateF =
spendingInfoDAO.update(
foundTxo.copyWithConfirmations(confirmations = confirmations))
updateF.foreach(tx =>
logger.debug(
s"Updated confirmation count=${tx.confirmations} of output=${foundTxo}"))
updateF.failed.foreach(err =>
logger.error(
s"Failed to update confirmation count of transaction=${transaction.txIdBE}",
err))
updateF
} else if (foundTxo.confirmations > confirmations) {
val msg =
List(
s"Incoming transaction=${transaction.txIdBE} has fewer confirmations=$confirmations",
s"than what we already have registered=${foundTxo.confirmations}! I don't know how",
s"to handle this."
).mkString(" ")
logger.warn(msg)
Future.failed(new RuntimeException(msg))
} else {
if (foundTxo.txid == transaction.txIdBE) {
logger.debug(
s"Skipping further processing of transaction=${transaction.txIdBE}, already processed.")
Future.successful(foundTxo)
} else {
val errMsg =
Seq(
s"Found TXO has txid=${foundTxo.txid}, tx we were given has txid=${transaction.txIdBE}.",
"This is either a reorg or a double spent, which is not implemented yet"
).mkString(" ")
logger.error(errMsg)
Future.failed(new RuntimeException(errMsg))
}
}
}
/**
* Processes an incoming transaction that's new to us
*
* @return A list of inserted transaction outputs
*/
private def processNewIncomingTx(
transaction: Transaction,
confirmations: Int): Future[Seq[SpendingInfoDb]] = {
addressDAO.findAll().flatMap { addrs =>
val relevantOutsWithIdx: Seq[OutputWithIndex] = {
val withIndex =
transaction.outputs.zipWithIndex
withIndex.collect {
case (out, idx)
if addrs.map(_.scriptPubKey).contains(out.scriptPubKey) =>
OutputWithIndex(out, idx)
}
}
relevantOutsWithIdx match {
case Nil =>
logger.debug(
s"Found no outputs relevant to us in transaction${transaction.txIdBE}")
Future.successful(Vector.empty)
case xs =>
val count = xs.length
val outputStr = {
xs.map { elem =>
s"${transaction.txIdBE.hex}:${elem.index}"
}
.mkString(", ")
}
logger.trace(
s"Found $count relevant output(s) in transaction=${transaction.txIdBE}: $outputStr")
val addUTXOsFut: Future[Seq[SpendingInfoDb]] =
Future
.sequence {
xs.map(
out =>
processUtxo(transaction,
out.index,
confirmations = confirmations,
// TODO is this correct?
spent = false))
}
addUTXOsFut
}
}
}
}

View file

@ -0,0 +1,150 @@
package org.bitcoins.wallet.internal
import org.bitcoins.wallet.LockedWallet
import org.bitcoins.core.protocol.transaction.TransactionOutput
import org.bitcoins.core.protocol.transaction.TransactionOutPoint
import scala.concurrent.Future
import org.bitcoins.wallet.models.AddressDb
import org.bitcoins.wallet.models.SpendingInfoDb
import org.bitcoins.wallet.models.SegWitAddressDb
import org.bitcoins.wallet.models.SegwitV0SpendingInfo
import org.bitcoins.wallet.models.LegacyAddressDb
import org.bitcoins.wallet.models.LegacySpendingInfo
import org.bitcoins.wallet.models.NestedSegWitAddressDb
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.wallet.api.AddUtxoResult
import org.bitcoins.core.number.UInt32
import org.bitcoins.wallet.api.AddUtxoError
import org.bitcoins.core.util.EitherUtil
import org.bitcoins.wallet.api.AddUtxoSuccess
import org.bitcoins.core.protocol.script.ScriptPubKey
import org.bitcoins.core.protocol.BitcoinAddress
import scala.util.Success
import scala.util.Failure
import org.bitcoins.core.crypto.DoubleSha256DigestBE
/**
* Provides functionality related to handling UTXOs in our wallet.
* The most notable examples of functioanlity here are enumerating
* UTXOs in the wallet and importing a UTXO into the wallet for later
* spending.
*/
private[wallet] trait UtxoHandling { self: LockedWallet =>
/** $inheritdoc */
override def listUtxos(): Future[Vector[SpendingInfoDb]] =
spendingInfoDAO.findAllUnspent()
/**
* Tries to convert the provided spk to an address, and then checks if we have
* it in our address table
*/
private def findAddress(
spk: ScriptPubKey): Future[Either[AddUtxoError, AddressDb]] =
BitcoinAddress.fromScriptPubKey(spk, networkParameters) match {
case Success(address) =>
addressDAO.findAddress(address).map {
case Some(addrDb) => Right(addrDb)
case None => Left(AddUtxoError.AddressNotFound)
}
case Failure(_) => Future.successful(Left(AddUtxoError.BadSPK))
}
/** Constructs a DB level representation of the given UTXO, and persist it to disk */
private def writeUtxo(
txid: DoubleSha256DigestBE,
confirmations: Int,
spent: Boolean,
output: TransactionOutput,
outPoint: TransactionOutPoint,
addressDb: AddressDb): Future[SpendingInfoDb] = {
val utxo: SpendingInfoDb = addressDb match {
case segwitAddr: SegWitAddressDb =>
SegwitV0SpendingInfo(
confirmations = confirmations,
spent = spent,
txid = txid,
outPoint = outPoint,
output = output,
privKeyPath = segwitAddr.path,
scriptWitness = segwitAddr.witnessScript
)
case LegacyAddressDb(path, _, _, _, _) =>
LegacySpendingInfo(confirmations = confirmations,
spent = spent,
txid = txid,
outPoint = outPoint,
output = output,
privKeyPath = path)
case nested: NestedSegWitAddressDb =>
throw new IllegalArgumentException(
s"Bad utxo $nested. Note: nested segwit is not implemented")
}
spendingInfoDAO.create(utxo).map { written =>
val writtenOut = written.outPoint
logger.info(
s"Successfully inserted UTXO ${writtenOut.txId.hex}:${writtenOut.vout.toInt} into DB")
logger.debug(s"UTXO details: ${written.output}")
written
}
}
/**
* Adds the provided UTXO to the wallet, making it
* available for spending.
*/
protected def addUtxo(
transaction: Transaction,
vout: UInt32,
confirmations: Int,
spent: Boolean): Future[AddUtxoResult] = {
import AddUtxoError._
import org.bitcoins.core.util.EitherUtil.EitherOps._
logger.info(s"Adding UTXO to wallet: ${transaction.txId.hex}:${vout.toInt}")
// first check: does the provided vout exist in the tx?
val voutIndexOutOfBounds: Boolean = {
val voutLength = transaction.outputs.length
val outOfBunds = voutLength <= vout.toInt
if (outOfBunds)
logger.error(
s"TX with TXID ${transaction.txId.hex} only has $voutLength, got request to add vout ${vout.toInt}!")
outOfBunds
}
if (voutIndexOutOfBounds) {
Future.successful(VoutIndexOutOfBounds)
} else {
val output = transaction.outputs(vout.toInt)
val outPoint = TransactionOutPoint(transaction.txId, vout)
// second check: do we have an address associated with the provided
// output in our DB?
val addressDbEitherF: Future[Either[AddUtxoError, AddressDb]] =
findAddress(output.scriptPubKey)
// insert the UTXO into the DB
addressDbEitherF.flatMap { addressDbE =>
val biasedE: Either[AddUtxoError, Future[SpendingInfoDb]] = for {
addressDb <- addressDbE
} yield
writeUtxo(txid = transaction.txIdBE,
confirmations = confirmations,
spent = spent,
output,
outPoint,
addressDb)
EitherUtil.liftRightBiasedFutureE(biasedE)
} map {
case Right(utxo) => AddUtxoSuccess(utxo)
case Left(e) => e
}
}
}
}

View file

@ -11,6 +11,7 @@ import scala.concurrent.{ExecutionContext, Future}
import org.bitcoins.core.hd.HDChainType
import org.bitcoins.wallet.config.WalletAppConfig
import org.bitcoins.core.crypto.ECPublicKey
import org.bitcoins.core.protocol.script.ScriptPubKey
case class AddressDAO()(
implicit val ec: ExecutionContext,
@ -55,6 +56,12 @@ case class AddressDAO()(
database.run(query.result).map(_.toVector)
}
/** Finds all SPKs in the wallet */
def findAllSPKs(): Future[Vector[ScriptPubKey]] = {
val query = table.map(_.scriptPubKey).distinct
database.run(query.result).map(_.toVector)
}
private def findMostRecentForChain(
accountIndex: Int,
chain: HDChainType): SqlAction[Option[AddressDb], NoStream, Effect.Read] = {

View file

@ -1,58 +0,0 @@
package org.bitcoins.wallet.models
import scala.concurrent.ExecutionContext
import org.bitcoins.wallet.config.WalletAppConfig
import org.bitcoins.db.CRUDAutoInc
import slick.jdbc.JdbcProfile
import scala.concurrent.Future
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.core.crypto.DoubleSha256DigestBE
/**
* DAO for incoming transaction outputs
*/
final case class IncomingTxoDAO(profile: JdbcProfile)(
implicit val ec: ExecutionContext,
val appConfig: WalletAppConfig)
extends CRUDAutoInc[IncomingWalletTXO] {
import profile.api._
import org.bitcoins.db.DbCommonsColumnMappers._
override val table = TableQuery[IncomingTXOTable]
private val addrTable = TableQuery[AddressTable]
private val spendingInfoDb = TableQuery[SpendingInfoTable]
/**
* Given a TXID, fetches all incoming TXOs and the address the TXO pays to
*/
def withAddress(txid: DoubleSha256DigestBE): Future[
Vector[(IncomingWalletTXO, AddressDb)]] = {
val query = {
val filtered = table.filter(_.txid === txid)
filtered join addrTable on (_.scriptPubKey === _.scriptPubKey)
}
database.runVec(query.result)
}
/**
* Fetches all the incoming TXOs in our DB that are in
* given TX
*/
def findTx(tx: Transaction): Future[Vector[IncomingWalletTXO]] =
findTx(tx.txIdBE)
def findTx(txid: DoubleSha256DigestBE): Future[Vector[IncomingWalletTXO]] = {
val filtered = table filter (_.txid === txid)
database.runVec(filtered.result)
}
def findAllWithSpendingInfo(): Future[
Vector[(IncomingWalletTXO, SpendingInfoDb)]] = {
val joined = (table join spendingInfoDb).result
database.runVec(joined)
}
}

View file

@ -1,19 +0,0 @@
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

@ -5,12 +5,95 @@ import org.bitcoins.wallet.config._
import slick.jdbc.SQLiteProfile.api._
import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.core.crypto.DoubleSha256DigestBE
import org.bitcoins.core.protocol.transaction.TransactionOutput
case class SpendingInfoDAO()(
implicit val ec: ExecutionContext,
val appConfig: WalletAppConfig)
extends CRUDAutoInc[SpendingInfoDb] {
import org.bitcoins.db.DbCommonsColumnMappers._
/** The table inside our database we are inserting into */
override val table = TableQuery[SpendingInfoTable]
private val addrTable = TableQuery[AddressTable]
/**
* Fetches all the incoming TXOs in our DB that are in
* the given TX
*/
def findTx(tx: Transaction): Future[Vector[SpendingInfoDb]] =
findTx(tx.txIdBE)
/**
* Finds all the outputs being spent in the given
* transaction
*/
def findOutputsBeingSpent(tx: Transaction): Future[Seq[SpendingInfoDb]] = {
val filtered = table
.filter {
case txo =>
txo.outPoint.inSet(tx.inputs.map(_.previousOutput))
}
database.run(filtered.result)
}
/**
* Given a TXID, fetches all incoming TXOs and the address the TXO pays to
*/
def withAddress(txid: DoubleSha256DigestBE): Future[
Vector[(SpendingInfoDb, AddressDb)]] = {
val query = {
val filtered = table.filter(_.txid === txid)
filtered.join(addrTable).on(_.scriptPubKey === _.scriptPubKey)
}
database.runVec(query.result)
}
/** Marks the given outputs as spent. Assumes that all the
* given outputs are ours, throwing if numbers aren't
* confirming that.
*/
def markAsSpent(
outputs: Seq[TransactionOutput]): Future[Vector[SpendingInfoDb]] = {
val spks = outputs.map(_.scriptPubKey)
val filtered = table.filter(_.scriptPubKey.inSet(spks))
for {
utxos <- database.run(filtered.result)
_ = assert(
utxos.length == outputs.length,
s"Was given ${outputs.length} outputs, found ${utxos.length} in DB")
updated <- updateAll(utxos.map(_.copyWithSpent(spent = true)).toVector)
} yield {
assert(utxos.length == updated.length,
"Updated a different number of UTXOs than what we found!")
logger.debug(s"Marked ${updated.length} UTXO(s) as spent")
updated
}
}
/**
* Fetches all the incoming TXOs in our DB that are in
* the transaction with the given TXID
*/
def findTx(txid: DoubleSha256DigestBE): Future[Vector[SpendingInfoDb]] = {
val filtered = table.filter(_.txid === txid)
database.runVec(filtered.result)
}
/** Enumerates all unspent TX outputs in the wallet */
def findAllUnspent(): Future[Vector[SpendingInfoDb]] = {
val query = table.filter(!_.spent)
database.run(query.result).map(_.toVector)
}
}

View file

@ -18,6 +18,7 @@ import org.bitcoins.core.hd.SegWitHDPath
import org.bitcoins.core.crypto.BIP39Seed
import org.bitcoins.core.util.BitcoinSLogger
import org.bitcoins.core.hd.LegacyHDPath
import org.bitcoins.core.crypto.DoubleSha256DigestBE
/**
* DB representation of a native V0
@ -28,12 +29,21 @@ case class SegwitV0SpendingInfo(
output: TransactionOutput,
privKeyPath: SegWitHDPath,
scriptWitness: ScriptWitness,
txid: DoubleSha256DigestBE,
spent: Boolean,
confirmations: Int,
id: Option[Long] = None
) extends SpendingInfoDb {
override val redeemScriptOpt: Option[ScriptPubKey] = None
override val scriptWitnessOpt: Option[ScriptWitness] = Some(scriptWitness)
override type PathType = SegWitHDPath
override type SpendingInfoType = SegwitV0SpendingInfo
def copyWithSpent(spent: Boolean): SegwitV0SpendingInfo = copy(spent = spent)
def copyWithConfirmations(confirmations: Int): SegwitV0SpendingInfo =
copy(confirmations = confirmations)
override def copyWithId(id: Long): SegwitV0SpendingInfo =
copy(id = Some(id))
@ -46,15 +56,24 @@ case class LegacySpendingInfo(
outPoint: TransactionOutPoint,
output: TransactionOutput,
privKeyPath: LegacyHDPath,
confirmations: Int,
spent: Boolean,
txid: DoubleSha256DigestBE,
id: Option[Long] = None
) extends SpendingInfoDb {
override val redeemScriptOpt: Option[ScriptPubKey] = None
override def scriptWitnessOpt: Option[ScriptWitness] = None
override type PathType = LegacyHDPath
type SpendingInfoType = LegacySpendingInfo
override def copyWithId(id: Long): LegacySpendingInfo =
copy(id = Some(id))
def copyWithSpent(spent: Boolean): LegacySpendingInfo = copy(spent = spent)
def copyWithConfirmations(confirmations: Int): LegacySpendingInfo =
copy(confirmations = confirmations)
}
// TODO add case for nested segwit
@ -72,6 +91,11 @@ sealed trait SpendingInfoDb
protected type PathType <: HDPath
/** This type is here to ensure copyWithSpent returns the same
* type as the one it was called on.
*/
protected type SpendingInfoType <: SpendingInfoDb
def id: Option[Long]
def outPoint: TransactionOutPoint
def output: TransactionOutput
@ -81,7 +105,27 @@ sealed trait SpendingInfoDb
val hashType: HashType = HashType.sigHashAll
def value: CurrencyUnit = output.value
/** How many confirmations this output has */
// MOVE ME
require(confirmations >= 0,
s"Confirmations cannot be negative! Got: $confirmations")
def confirmations: Int
/** Whether or not this TXO is spent from our wallet */
def spent: Boolean
/** The TXID of the transaction this output was received in */
def txid: DoubleSha256DigestBE
/** Converts the UTXO to the canonical `txid:vout` format */
def toHumanReadableString: String =
s"${outPoint.txId.flip.hex}:${outPoint.vout.toInt}"
/** Updates the `spent` field */
def copyWithSpent(spent: Boolean): SpendingInfoType
/** Updates the `confirmations` field */
def copyWithConfirmations(confirmations: Int): SpendingInfoType
/** Converts a non-sensitive DB representation of a UTXO into
* a signable (and sensitive) real-world UTXO
@ -130,49 +174,90 @@ case class SpendingInfoTable(tag: Tag)
import org.bitcoins.db.DbCommonsColumnMappers._
def outPoint: Rep[TransactionOutPoint] =
column[TransactionOutPoint]("tx_outpoint")
column("tx_outpoint")
def output: Rep[TransactionOutput] =
column[TransactionOutput]("tx_output")
def txid: Rep[DoubleSha256DigestBE] = column("txid")
def privKeyPath: Rep[HDPath] = column[HDPath]("hd_privkey_path")
def confirmations: Rep[Int] = column("confirmations")
def spent: Rep[Boolean] = column("spent")
def scriptPubKey: Rep[ScriptPubKey] = column("script_pub_key")
def value: Rep[CurrencyUnit] = column("value")
def privKeyPath: Rep[HDPath] = column("hd_privkey_path")
def redeemScriptOpt: Rep[Option[ScriptPubKey]] =
column[Option[ScriptPubKey]]("nullable_redeem_script")
column("redeem_script")
def scriptWitnessOpt: Rep[Option[ScriptWitness]] =
column[Option[ScriptWitness]]("script_witness")
def scriptWitnessOpt: Rep[Option[ScriptWitness]] = column("script_witness")
/** All UTXOs must have a SPK in the wallet that gets spent to */
def fk_scriptPubKey = {
val addressTable = TableQuery[AddressTable]
foreignKey("fk_scriptPubKey",
sourceColumns = scriptPubKey,
targetTableQuery = addressTable)(_.scriptPubKey)
}
private type UTXOTuple = (
Option[Long],
Option[Long], // ID
TransactionOutPoint,
TransactionOutput,
ScriptPubKey, // output SPK
CurrencyUnit, // output value
HDPath,
Option[ScriptPubKey],
Option[ScriptWitness]
Option[ScriptPubKey], // ReedemScript
Option[ScriptWitness],
Int, // confirmations
Boolean, // spent
DoubleSha256DigestBE // TXID
)
private val fromTuple: UTXOTuple => SpendingInfoDb = {
case (id,
outpoint,
output,
spk,
value,
path: SegWitHDPath,
None, // ReedemScript
Some(scriptWitness)) =>
SegwitV0SpendingInfo(outpoint, output, path, scriptWitness, id)
Some(scriptWitness),
confirmations,
spent,
txid) =>
SegwitV0SpendingInfo(
outPoint = outpoint,
output = TransactionOutput(value, spk),
privKeyPath = path,
scriptWitness = scriptWitness,
id = id,
confirmations = confirmations,
spent = spent,
txid = txid
)
case (id,
outpoint,
output,
spk,
value,
path: LegacyHDPath,
None, // RedeemScript
None // ScriptWitness
) =>
LegacySpendingInfo(outpoint, output, path, id)
case (id, outpoint, output, path, spkOpt, swOpt) =>
None, // ScriptWitness
confirmations,
spent,
txid) =>
LegacySpendingInfo(outPoint = outpoint,
output = TransactionOutput(value, spk),
privKeyPath = path,
id = id,
confirmations = confirmations,
spent = spent,
txid = txid)
case (id, outpoint, spk, value, path, spkOpt, swOpt, confs, spent, txid) =>
throw new IllegalArgumentException(
"Could not construct UtxoSpendingInfoDb from bad tuple:"
+ s" ($outpoint, $output, $path, $spkOpt, $swOpt, $id) . Note: Nested Segwit is not implemented")
+ s" ($id, $outpoint, $spk, $value, $path, $spkOpt, $swOpt, $confs, $spent, $txid)."
+ " Note: Nested Segwit is not implemented")
}
@ -181,11 +266,24 @@ case class SpendingInfoTable(tag: Tag)
Some(
(utxo.id,
utxo.outPoint,
utxo.output,
utxo.output.scriptPubKey,
utxo.output.value,
utxo.privKeyPath,
utxo.redeemScriptOpt,
utxo.scriptWitnessOpt))
utxo.scriptWitnessOpt,
utxo.confirmations,
utxo.spent,
utxo.txid))
def * : ProvenShape[SpendingInfoDb] =
(id.?, outPoint, output, privKeyPath, redeemScriptOpt, scriptWitnessOpt) <> (fromTuple, toTuple)
(id.?,
outPoint,
scriptPubKey,
value,
privKeyPath,
redeemScriptOpt,
scriptWitnessOpt,
confirmations,
spent,
txid) <> (fromTuple, toTuple)
}

View file

@ -1,127 +0,0 @@
package org.bitcoins.wallet.models
import slick.jdbc.SQLiteProfile.api._
import slick.lifted.ProvenShape
import org.bitcoins.db.TableAutoInc
import org.bitcoins.core.crypto.DoubleSha256DigestBE
import org.bitcoins.db.DbRowAutoInc
import org.bitcoins.core.protocol.script.ScriptPubKey
/**
* A transaction output that's relevant to our wallet
*/
sealed trait WalletTXO[T <: WalletTXO[T]] extends DbRowAutoInc[T] {
require(confirmations >= 0,
s"Confirmations cannot be negative! Got: $confirmations")
/** The transaction that this output was
* received/spent in
*/
val txid: DoubleSha256DigestBE
/** Whether or not this TXO is spent */
val spent: Boolean
/** How many confirmations this TXO has */
val confirmations: Int
}
/**
* A transaction output that has been spent to our wallet
* @param spendingInfoID Foreign key into the table with
* TXO spending info
* @param id
*/
final case class IncomingWalletTXO(
confirmations: Int,
txid: DoubleSha256DigestBE,
spent: Boolean,
scriptPubKey: ScriptPubKey,
spendingInfoID: Long,
id: Option[Long] = None)
extends WalletTXO[IncomingWalletTXO] {
override def copyWithId(id: Long): IncomingWalletTXO =
this.copy(id = Some(id))
}
final case class OutgoingWalletTXO(
confirmations: Int,
txid: DoubleSha256DigestBE,
incomingTxoID: Long,
id: Option[Long] = None)
extends WalletTXO[OutgoingWalletTXO] {
override def copyWithId(id: Long): OutgoingWalletTXO = copy(id = Some(id))
/** Outgoing TXOs are per definition spent */
val spent: Boolean = true
}
/**
* Table of outputs relevant to our wallet, somehow.
*
* This table does not contain information related to spending
* the TXO, that's handled in SpendingInfoTable
*/
sealed abstract class WalletTXOTable[TXOType <: WalletTXO[TXOType]](
tag: Tag,
tableName: String)
extends TableAutoInc[TXOType](tag, tableName) {
import org.bitcoins.db.DbCommonsColumnMappers._
def txid: Rep[DoubleSha256DigestBE] = column("txid")
def confirmations: Rep[Int] = column("confirmations")
def spent: Rep[Boolean] = column("spent")
}
final case class IncomingTXOTable(tag: Tag)
extends WalletTXOTable[IncomingWalletTXO](tag, "incoming_tx_outputs") {
import org.bitcoins.db.DbCommonsColumnMappers._
def scriptPubKey: Rep[ScriptPubKey] = column("script_pub_key")
def spendingInfoID: Rep[Long] = column("spending_info_id")
/** All incoming TXOs must have a SPK that gets spent to */
def fk_scriptPubKey = {
val addressTable = TableQuery[AddressTable]
foreignKey("fk_scriptPubKey",
sourceColumns = scriptPubKey,
targetTableQuery = addressTable)(_.scriptPubKey)
}
/**
* Every incoming TXO must be spendable, this foreign key ensures
* this
*/
def fk_spendingInfo = {
val spendingInfoTable = TableQuery[SpendingInfoTable]
foreignKey("fk_spending_info",
sourceColumns = spendingInfoID,
targetTableQuery = spendingInfoTable)(_.id)
}
override def * : ProvenShape[IncomingWalletTXO] =
(confirmations, txid, spent, scriptPubKey, spendingInfoID, id.?) <> (IncomingWalletTXO.tupled, IncomingWalletTXO.unapply)
}
final case class OutgoingTXOTable(tag: Tag)
extends WalletTXOTable[OutgoingWalletTXO](tag, "outgoing_tx_outputs") {
import org.bitcoins.db.DbCommonsColumnMappers._
/** Every outgoing TXO must reference how it entered our wallet */
def incomingTxoID: Rep[Long] = column("incoming_txo_id")
def fk_incoming = {
val incomingTable = TableQuery[IncomingTXOTable]
foreignKey("fk_incoming_txo",
sourceColumns = incomingTxoID,
targetTableQuery = incomingTable) { _.id }
}
override def * : ProvenShape[OutgoingWalletTXO] =
(confirmations, txid, incomingTxoID, id.?) <> (OutgoingWalletTXO.tupled, OutgoingWalletTXO.unapply)
}