mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-03-13 11:35:40 +01:00
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:
parent
3556788cc6
commit
9101aece9b
32 changed files with 1312 additions and 932 deletions
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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 { _ =>
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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]]
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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] = {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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]
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
Loading…
Add table
Reference in a new issue