mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2024-11-19 09:52:09 +01:00
Compute confirmations dynamically (#938)
* Compute confirmations dynamically
This commit is contained in:
parent
96aae0ca4f
commit
2c53a39fd1
@ -25,7 +25,7 @@ import org.scalatest.{Matchers, WordSpec}
|
||||
import ujson.Value.InvalidData
|
||||
import ujson.{Arr, Null, Num, Str}
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.concurrent.Future
|
||||
|
||||
class RoutesSpec
|
||||
extends WordSpec
|
||||
@ -52,9 +52,8 @@ class RoutesSpec
|
||||
"The server" should {
|
||||
|
||||
"return the block count" in {
|
||||
(mockChainApi
|
||||
.getBlockCount(_: ExecutionContext))
|
||||
.expects(*)
|
||||
(mockChainApi.getBlockCount: () => Future[Int])
|
||||
.expects()
|
||||
.returning(Future.successful(1234567890))
|
||||
|
||||
val route =
|
||||
@ -67,9 +66,8 @@ class RoutesSpec
|
||||
}
|
||||
|
||||
"return the best block hash" in {
|
||||
(mockChainApi
|
||||
.getBestBlockHash(_: ExecutionContext))
|
||||
.expects(*)
|
||||
(mockChainApi.getBestBlockHash: () => Future[DoubleSha256DigestBE])
|
||||
.expects()
|
||||
.returning(Future.successful(DoubleSha256DigestBE.empty))
|
||||
|
||||
val route =
|
||||
|
@ -5,7 +5,8 @@ import java.nio.file.Files
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import org.bitcoins.chain.config.ChainAppConfig
|
||||
import org.bitcoins.core.api.{ChainQueryApi, NodeApi}
|
||||
import org.bitcoins.core.api.ChainQueryApi
|
||||
import org.bitcoins.core.crypto.DoubleSha256Digest
|
||||
import org.bitcoins.node.config.NodeAppConfig
|
||||
import org.bitcoins.node.models.Peer
|
||||
import org.bitcoins.node.networking.peer.DataMessageHandler
|
||||
@ -115,7 +116,7 @@ object Main extends App {
|
||||
wallet: UnlockedWalletApi): Future[NodeCallbacks] = {
|
||||
import DataMessageHandler._
|
||||
lazy val onTx: OnTxReceived = { tx =>
|
||||
wallet.processTransaction(tx, confirmations = 0)
|
||||
wallet.processTransaction(tx, blockHash = None)
|
||||
()
|
||||
}
|
||||
lazy val onCompactFilter: OnCompactFilterReceived = {
|
||||
@ -123,7 +124,7 @@ object Main extends App {
|
||||
wallet.processCompactFilter(blockHash, blockFilter)
|
||||
}
|
||||
lazy val onBlock: OnBlockReceived = { block =>
|
||||
wallet.processBlock(block, 0)
|
||||
wallet.processBlock(block)
|
||||
()
|
||||
}
|
||||
if (nodeConf.isSPVEnabled) {
|
||||
|
@ -14,9 +14,10 @@ import org.bitcoins.core.crypto.{
|
||||
ECPrivateKey
|
||||
}
|
||||
import org.bitcoins.core.gcs.{BlockFilter, FilterHeader, FilterType}
|
||||
import org.bitcoins.core.number.{Int32, UInt32}
|
||||
import org.bitcoins.core.p2p.CompactFilterMessage
|
||||
import org.bitcoins.core.protocol.BitcoinAddress
|
||||
import org.bitcoins.core.protocol.blockchain.BlockHeader
|
||||
import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp}
|
||||
import org.bitcoins.core.util.CryptoUtil
|
||||
import org.bitcoins.testkit.BitcoinSTestAppConfig
|
||||
import org.bitcoins.testkit.chain.fixture.ChainFixtureTag
|
||||
@ -51,7 +52,9 @@ class ChainHandlerTest extends ChainUnitTest {
|
||||
source.close()
|
||||
|
||||
import org.bitcoins.rpc.serializers.JsonReaders.BlockHeaderReads
|
||||
val headersResult = Json.parse(arrStr).validate[Vector[BlockHeader]].get
|
||||
|
||||
val headersResult: Vector[BlockHeader] =
|
||||
Json.parse(arrStr).validate[Vector[BlockHeader]].get
|
||||
|
||||
override val defaultTag: ChainFixtureTag = ChainFixtureTag.GenisisChainHandler
|
||||
|
||||
@ -268,30 +271,14 @@ class ChainHandlerTest extends ChainUnitTest {
|
||||
|
||||
it must "get the highest filter header" in { chainHandler: ChainHandler =>
|
||||
{
|
||||
val firstFilterHeader = FilterHeader(
|
||||
filterHash =
|
||||
DoubleSha256Digest.fromBytes(ECPrivateKey.freshPrivateKey.bytes),
|
||||
prevHeaderHash = DoubleSha256Digest.empty)
|
||||
for {
|
||||
empty <- chainHandler.getFilterHeadersAtHeight(0)
|
||||
block <- chainHandler.getHeadersAtHeight(0)
|
||||
_ <- chainHandler.processFilterHeader(firstFilterHeader,
|
||||
block.head.hashBE)
|
||||
count <- chainHandler.getFilterHeaderCount
|
||||
first <- chainHandler.getFilterHeader(block.head.hashBE)
|
||||
vec <- chainHandler.getFilterHeadersAtHeight(count)
|
||||
genesisFilterHeader <- chainHandler.getFilterHeadersAtHeight(count)
|
||||
} yield {
|
||||
assert(empty.isEmpty)
|
||||
assert(first.nonEmpty)
|
||||
assert(vec.nonEmpty)
|
||||
assert(Vector(first.get) == vec)
|
||||
assert(first.get.hashBE == firstFilterHeader.hash.flip)
|
||||
assert(first.get.filterHashBE == firstFilterHeader.filterHash.flip)
|
||||
assert(genesisFilterHeader.size == 1)
|
||||
assert(
|
||||
first.get.previousFilterHeaderBE == firstFilterHeader.prevHeaderHash.flip)
|
||||
assert(first.get.blockHashBE == block.head.hashBE)
|
||||
assert(first.get.height == 0)
|
||||
assert(first.get.filterHeader == firstFilterHeader)
|
||||
genesisFilterHeader.contains(ChainUnitTest.genesisFilterHeaderDb))
|
||||
assert(count == 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -313,42 +300,36 @@ class ChainHandlerTest extends ChainUnitTest {
|
||||
it must "get the highest filter" in { chainHandler: ChainHandler =>
|
||||
{
|
||||
for {
|
||||
empty <- chainHandler.getFilterCount
|
||||
blockHashBE <- chainHandler.getHeadersAtHeight(0).map(_.head.hashBE)
|
||||
golombFilter = BlockFilter.fromHex("017fa880", blockHashBE.flip)
|
||||
firstFilter = CompactFilterMessage(blockHash = blockHashBE.flip,
|
||||
filter = golombFilter)
|
||||
firstFilterHeader = FilterHeader(filterHash = golombFilter.hash,
|
||||
prevHeaderHash =
|
||||
DoubleSha256Digest.empty)
|
||||
newChainHandler <- chainHandler.processFilterHeader(firstFilterHeader,
|
||||
blockHashBE)
|
||||
_ <- chainHandler.processFilter(firstFilter)
|
||||
count <- newChainHandler.getFilterCount
|
||||
first <- newChainHandler.getFiltersAtHeight(count).map(_.headOption)
|
||||
count <- chainHandler.getFilterCount
|
||||
genesisFilter <- chainHandler.getFiltersAtHeight(count)
|
||||
} yield {
|
||||
assert(empty == 0)
|
||||
assert(first.nonEmpty)
|
||||
assert(first.get.hashBE == golombFilter.hash.flip)
|
||||
assert(first.get.height == 0)
|
||||
assert(first.get.blockHashBE == blockHashBE)
|
||||
assert(first.get.filterType == FilterType.Basic)
|
||||
assert(first.get.golombFilter == golombFilter)
|
||||
assert(count == 0)
|
||||
assert(genesisFilter.contains(ChainUnitTest.genesisFilterDb))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it must "NOT create an unknown filter" in { chainHandler: ChainHandler =>
|
||||
{
|
||||
val blockHeader =
|
||||
BlockHeader(
|
||||
version = Int32(1),
|
||||
previousBlockHash = ChainUnitTest.genesisHeaderDb.hashBE.flip,
|
||||
merkleRootHash = DoubleSha256Digest.empty,
|
||||
time = UInt32(1231006505),
|
||||
nBits = UInt32(545259519),
|
||||
nonce = UInt32(2083236893)
|
||||
)
|
||||
val unknownHashF = for {
|
||||
blockHashBE <- chainHandler.getHeadersAtHeight(0).map(_.head.hashBE)
|
||||
_ <- chainHandler.processHeader(blockHeader)
|
||||
blockHashBE <- chainHandler.getHeadersAtHeight(1).map(_.head.hashBE)
|
||||
golombFilter = BlockFilter.fromHex("017fa880", blockHashBE.flip)
|
||||
firstFilter = CompactFilterMessage(blockHash = blockHashBE.flip,
|
||||
filter = golombFilter)
|
||||
firstFilterHeader = FilterHeader(
|
||||
filterHash =
|
||||
DoubleSha256Digest.fromBytes(ECPrivateKey.freshPrivateKey.bytes),
|
||||
prevHeaderHash = DoubleSha256Digest.empty)
|
||||
prevHeaderHash = ChainUnitTest.genesisFilterHeaderDb.hashBE.flip)
|
||||
newChainHandler <- chainHandler.processFilterHeader(firstFilterHeader,
|
||||
blockHashBE)
|
||||
process <- newChainHandler.processFilter(firstFilter)
|
||||
@ -400,7 +381,7 @@ class ChainHandlerTest extends ChainUnitTest {
|
||||
it must "generate a range for a block filter query" in {
|
||||
chainHandler: ChainHandler =>
|
||||
for {
|
||||
bestBlock <- chainHandler.getBestBlockHeader
|
||||
bestBlock <- chainHandler.getBestBlockHeader()
|
||||
bestBlockHashBE = bestBlock.hashBE
|
||||
rangeOpt <- chainHandler.nextHeaderBatchRange(
|
||||
DoubleSha256DigestBE.empty,
|
||||
@ -412,6 +393,47 @@ class ChainHandlerTest extends ChainUnitTest {
|
||||
}
|
||||
}
|
||||
|
||||
it must "generate a range for a block filter header query" in {
|
||||
chainHandler: ChainHandler =>
|
||||
for {
|
||||
bestBlock <- chainHandler.getBestBlockHeader()
|
||||
bestBlockHashBE = bestBlock.hashBE
|
||||
rangeOpt <- chainHandler.nextFilterHeaderBatchRange(
|
||||
DoubleSha256DigestBE.empty,
|
||||
1)
|
||||
} yield {
|
||||
assert(rangeOpt.nonEmpty)
|
||||
assert(rangeOpt.get._1 == 0)
|
||||
assert(rangeOpt.get._2 == bestBlockHashBE.flip)
|
||||
}
|
||||
}
|
||||
|
||||
it must "return the number of confirmations" in {
|
||||
chainHandler: ChainHandler =>
|
||||
for {
|
||||
bestBlockHashBE <- chainHandler.getBestBlockHash()
|
||||
confirmations <- chainHandler.getNumberOfConfirmations(bestBlockHashBE)
|
||||
} yield {
|
||||
assert(confirmations == Some(1))
|
||||
}
|
||||
}
|
||||
|
||||
it must "return the height by block stamp" in { chainHandler: ChainHandler =>
|
||||
for {
|
||||
bestBlock <- chainHandler.getBestBlockHeader()
|
||||
stamp1 = BlockStamp.BlockHash(bestBlock.hashBE)
|
||||
stamp2 = BlockStamp.BlockHeight(bestBlock.height)
|
||||
stamp3 = BlockStamp.BlockTime(bestBlock.time)
|
||||
height1 <- chainHandler.getHeightByBlockStamp(stamp1)
|
||||
height2 <- chainHandler.getHeightByBlockStamp(stamp2)
|
||||
// TODO implement BlockTime
|
||||
// height3 <- chainHandler.getHeightByBlockStamp(stamp3)
|
||||
} yield {
|
||||
assert(height1 == height2)
|
||||
// assert(height1 == height3)
|
||||
}
|
||||
}
|
||||
|
||||
it must "match block filters" in { chainHandler: ChainHandler =>
|
||||
import scodec.bits._
|
||||
|
||||
@ -462,7 +484,7 @@ class ChainHandlerTest extends ChainUnitTest {
|
||||
BitcoinAddress("n1RH2x3b3ah4TGQtgrmNAHfmad9wr8U2QY").get.scriptPubKey),
|
||||
startOpt = None,
|
||||
endOpt = None
|
||||
)
|
||||
)(system.dispatcher)
|
||||
} yield {
|
||||
assert(Vector(created.blockHashBE) == matched)
|
||||
}
|
||||
@ -524,7 +546,7 @@ class ChainHandlerTest extends ChainUnitTest {
|
||||
private def buildChainHandlerCompetingHeaders(
|
||||
chainHandler: ChainHandler): Future[ReorgFixture] = {
|
||||
for {
|
||||
oldBestTip <- chainHandler.getBestBlockHeader
|
||||
oldBestTip <- chainHandler.getBestBlockHeader()
|
||||
(newHeaderB, newHeaderC) = buildCompetingHeaders(oldBestTip)
|
||||
newChainApi <- chainHandler.processHeaders(Vector(newHeaderB, newHeaderC))
|
||||
newHeaderDbB <- newChainApi.getHeader(newHeaderB.hashBE)
|
||||
|
@ -25,8 +25,7 @@ trait ChainApi extends ChainQueryApi {
|
||||
* @param header
|
||||
* @return
|
||||
*/
|
||||
def processHeader(header: BlockHeader)(
|
||||
implicit ec: ExecutionContext): Future[ChainApi] = {
|
||||
def processHeader(header: BlockHeader): Future[ChainApi] = {
|
||||
processHeaders(Vector(header))
|
||||
}
|
||||
|
||||
@ -36,37 +35,22 @@ trait ChainApi extends ChainQueryApi {
|
||||
* @param headers
|
||||
* @return
|
||||
*/
|
||||
def processHeaders(headers: Vector[BlockHeader])(
|
||||
implicit ec: ExecutionContext): Future[ChainApi]
|
||||
def processHeaders(headers: Vector[BlockHeader]): Future[ChainApi]
|
||||
|
||||
/** Gets a [[org.bitcoins.chain.models.BlockHeaderDb]] from the chain's database */
|
||||
def getHeader(hash: DoubleSha256DigestBE)(
|
||||
implicit ec: ExecutionContext): Future[Option[BlockHeaderDb]]
|
||||
def getHeader(hash: DoubleSha256DigestBE): Future[Option[BlockHeaderDb]]
|
||||
|
||||
/** Gets all [[org.bitcoins.chain.models.BlockHeaderDb]]s at a given height */
|
||||
def getHeadersAtHeight(height: Int)(
|
||||
implicit ec: ExecutionContext): Future[Vector[BlockHeaderDb]]
|
||||
def getHeadersAtHeight(height: Int): Future[Vector[BlockHeaderDb]]
|
||||
|
||||
/** Gets the number of blocks in the database */
|
||||
def getBlockCount(implicit ec: ExecutionContext): Future[Int]
|
||||
def getBlockCount(): Future[Int]
|
||||
|
||||
/** Gets the hash of the block that is what we consider "best" */
|
||||
override def getBestBlockHash(
|
||||
implicit ec: ExecutionContext): Future[DoubleSha256DigestBE]
|
||||
// /** Gets the hash of the block that is what we consider "best" */
|
||||
// override def getBestBlockHash: Future[DoubleSha256DigestBE]
|
||||
|
||||
/** Gets the best block header we have */
|
||||
def getBestBlockHeader(
|
||||
implicit ec: ExecutionContext): Future[BlockHeaderDb] = {
|
||||
for {
|
||||
hash <- getBestBlockHash
|
||||
headerOpt <- getHeader(hash)
|
||||
} yield headerOpt match {
|
||||
case None =>
|
||||
throw new RuntimeException(
|
||||
s"We found best hash=${hash.hex} but could not retrieve the full header!!!")
|
||||
case Some(header) => header
|
||||
}
|
||||
}
|
||||
def getBestBlockHeader(): Future[BlockHeaderDb]
|
||||
|
||||
/**
|
||||
* Adds a compact filter header into the filter header chain and returns a new [[ChainApi chain api]]
|
||||
@ -74,8 +58,7 @@ trait ChainApi extends ChainQueryApi {
|
||||
*/
|
||||
def processFilterHeader(
|
||||
filterHeader: FilterHeader,
|
||||
blockHash: DoubleSha256DigestBE)(
|
||||
implicit ec: ExecutionContext): Future[ChainApi] = {
|
||||
blockHash: DoubleSha256DigestBE): Future[ChainApi] = {
|
||||
processFilterHeaders(Vector(filterHeader), blockHash)
|
||||
}
|
||||
|
||||
@ -85,44 +68,40 @@ trait ChainApi extends ChainQueryApi {
|
||||
*/
|
||||
def processFilterHeaders(
|
||||
filterHeaders: Vector[FilterHeader],
|
||||
stopHash: DoubleSha256DigestBE)(
|
||||
implicit ec: ExecutionContext): Future[ChainApi]
|
||||
stopHash: DoubleSha256DigestBE): Future[ChainApi]
|
||||
|
||||
/**
|
||||
* Generates a block range in form of (startHeight, stopHash) by the given stop hash.
|
||||
*/
|
||||
def nextHeaderBatchRange(stopHash: DoubleSha256DigestBE, batchSize: Int)(
|
||||
implicit ec: ExecutionContext): Future[Option[(Int, DoubleSha256Digest)]]
|
||||
def nextHeaderBatchRange(
|
||||
stopHash: DoubleSha256DigestBE,
|
||||
batchSize: Int): Future[Option[(Int, DoubleSha256Digest)]]
|
||||
|
||||
/**
|
||||
* Generates a filter header range in form of (startHeight, stopHash) by the given stop hash.
|
||||
*/
|
||||
def nextFilterHeaderBatchRange(
|
||||
stopHash: DoubleSha256DigestBE,
|
||||
batchSize: Int)(
|
||||
implicit ec: ExecutionContext): Future[Option[(Int, DoubleSha256Digest)]]
|
||||
batchSize: Int): Future[Option[(Int, DoubleSha256Digest)]]
|
||||
|
||||
/**
|
||||
* Adds a compact filter into the filter database.
|
||||
*/
|
||||
def processFilter(message: CompactFilterMessage)(
|
||||
implicit ec: ExecutionContext): Future[ChainApi] =
|
||||
def processFilter(message: CompactFilterMessage): Future[ChainApi] =
|
||||
processFilters(Vector(message))
|
||||
|
||||
/**
|
||||
* Process all of the given compact filters and returns a new [[ChainApi chain api]]
|
||||
* that contains these headers.
|
||||
*/
|
||||
def processFilters(message: Vector[CompactFilterMessage])(
|
||||
implicit ec: ExecutionContext): Future[ChainApi]
|
||||
def processFilters(message: Vector[CompactFilterMessage]): Future[ChainApi]
|
||||
|
||||
/**
|
||||
* Adds a compact filter header check point into the list of check points.
|
||||
*/
|
||||
def processCheckpoint(
|
||||
filterHeaderHash: DoubleSha256DigestBE,
|
||||
blockHash: DoubleSha256DigestBE)(
|
||||
implicit ec: ExecutionContext): Future[ChainApi] = {
|
||||
blockHash: DoubleSha256DigestBE): Future[ChainApi] = {
|
||||
processCheckpoints(Vector(filterHeaderHash), blockHash)
|
||||
}
|
||||
|
||||
@ -131,38 +110,35 @@ trait ChainApi extends ChainQueryApi {
|
||||
*/
|
||||
def processCheckpoints(
|
||||
checkpoints: Vector[DoubleSha256DigestBE],
|
||||
blockHash: DoubleSha256DigestBE)(
|
||||
implicit ec: ExecutionContext): Future[ChainApi]
|
||||
blockHash: DoubleSha256DigestBE): Future[ChainApi]
|
||||
|
||||
/** Gets the number of compact filter headers in the database */
|
||||
def getFilterHeaderCount(implicit ec: ExecutionContext): Future[Int]
|
||||
def getFilterHeaderCount: Future[Int]
|
||||
|
||||
/**
|
||||
* Looks up a compact filter header by its height.
|
||||
*/
|
||||
def getFilterHeadersAtHeight(height: Int)(
|
||||
implicit ec: ExecutionContext): Future[Vector[CompactFilterHeaderDb]]
|
||||
def getFilterHeadersAtHeight(
|
||||
height: Int): Future[Vector[CompactFilterHeaderDb]]
|
||||
|
||||
/**
|
||||
* Looks up a compact filter header by its hash.
|
||||
*/
|
||||
def getFilterHeader(blockHash: DoubleSha256DigestBE)(
|
||||
implicit ec: ExecutionContext): Future[Option[CompactFilterHeaderDb]]
|
||||
def getFilterHeader(
|
||||
blockHash: DoubleSha256DigestBE): Future[Option[CompactFilterHeaderDb]]
|
||||
|
||||
/**
|
||||
* Looks up a compact filter by its hash.
|
||||
*/
|
||||
def getFilter(hash: DoubleSha256DigestBE)(
|
||||
implicit ec: ExecutionContext): Future[Option[CompactFilterDb]]
|
||||
def getFilter(hash: DoubleSha256DigestBE): Future[Option[CompactFilterDb]]
|
||||
|
||||
/** Gets the number of compact filters in the database */
|
||||
def getFilterCount(implicit ec: ExecutionContext): Future[Int]
|
||||
def getFilterCount: Future[Int]
|
||||
|
||||
/**
|
||||
* Looks up a compact filter by its height.
|
||||
*/
|
||||
def getFiltersAtHeight(height: Int)(
|
||||
implicit ec: ExecutionContext): Future[Vector[CompactFilterDb]]
|
||||
def getFiltersAtHeight(height: Int): Future[Vector[CompactFilterDb]]
|
||||
|
||||
/**
|
||||
* Iterates over the block filters in order to find filters that match to the given addresses
|
||||
@ -182,15 +158,8 @@ trait ChainApi extends ChainQueryApi {
|
||||
endOpt: Option[BlockStamp],
|
||||
batchSize: Int,
|
||||
parallelismLevel: Int)(
|
||||
implicit ec: ExecutionContext): Future[Vector[DoubleSha256DigestBE]]
|
||||
ec: ExecutionContext): Future[Vector[DoubleSha256DigestBE]]
|
||||
|
||||
/** Returns the block height of the given block stamp */
|
||||
def getHeightByBlockStamp(blockStamp: BlockStamp)(
|
||||
implicit ec: ExecutionContext): Future[Int]
|
||||
|
||||
/** @inheritdoc */
|
||||
override def getBlockHeight(blockHash: DoubleSha256DigestBE)(
|
||||
implicit ec: ExecutionContext): Future[Option[Int]] =
|
||||
getHeader(blockHash).map(_.map(_.height))
|
||||
|
||||
def getHeightByBlockStamp(blockStamp: BlockStamp): Future[Int]
|
||||
}
|
||||
|
@ -33,12 +33,13 @@ case class ChainHandler(
|
||||
filterDAO: CompactFilterDAO,
|
||||
blockchains: Vector[Blockchain],
|
||||
blockFilterCheckpoints: Map[DoubleSha256DigestBE, DoubleSha256DigestBE])(
|
||||
implicit val chainConfig: ChainAppConfig)
|
||||
implicit val chainConfig: ChainAppConfig,
|
||||
executionContext: ExecutionContext)
|
||||
extends ChainApi
|
||||
with ChainVerificationLogger {
|
||||
|
||||
/** @inheritdoc */
|
||||
override def getBlockCount(implicit ec: ExecutionContext): Future[Int] = {
|
||||
override def getBlockCount(): Future[Int] = {
|
||||
logger.debug(s"Querying for block count")
|
||||
blockHeaderDAO.maxHeight.map { height =>
|
||||
logger.debug(s"getBlockCount result: count=$height")
|
||||
@ -46,9 +47,21 @@ case class ChainHandler(
|
||||
}
|
||||
}
|
||||
|
||||
override def getBestBlockHeader(): Future[BlockHeaderDb] = {
|
||||
for {
|
||||
hash <- getBestBlockHash()
|
||||
headerOpt <- getHeader(hash)
|
||||
} yield headerOpt match {
|
||||
case None =>
|
||||
throw new RuntimeException(
|
||||
s"We found best hash=${hash.hex} but could not retrieve the full header!!!")
|
||||
case Some(header) => header
|
||||
}
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
override def getHeader(hash: DoubleSha256DigestBE)(
|
||||
implicit ec: ExecutionContext): Future[Option[BlockHeaderDb]] = {
|
||||
override def getHeader(
|
||||
hash: DoubleSha256DigestBE): Future[Option[BlockHeaderDb]] = {
|
||||
blockHeaderDAO.findByHash(hash).map { header =>
|
||||
logger.debug(s"Looking for header by hash=$hash")
|
||||
val resultStr = header
|
||||
@ -60,8 +73,8 @@ case class ChainHandler(
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
override def processHeaders(headers: Vector[BlockHeader])(
|
||||
implicit ec: ExecutionContext): Future[ChainApi] = {
|
||||
override def processHeaders(
|
||||
headers: Vector[BlockHeader]): Future[ChainApi] = {
|
||||
if (headers.isEmpty) {
|
||||
Future.successful(this)
|
||||
} else {
|
||||
@ -92,8 +105,7 @@ case class ChainHandler(
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def getBestBlockHash(
|
||||
implicit ec: ExecutionContext): Future[DoubleSha256DigestBE] = {
|
||||
override def getBestBlockHash(): Future[DoubleSha256DigestBE] = {
|
||||
logger.debug(s"Querying for best block hash")
|
||||
//naive implementation, this is looking for the tip with the _most_ proof of work
|
||||
//this does _not_ mean that it is on the chain that has the most work
|
||||
@ -121,8 +133,7 @@ case class ChainHandler(
|
||||
/** @inheritdoc */
|
||||
override def nextHeaderBatchRange(
|
||||
prevStopHash: DoubleSha256DigestBE,
|
||||
batchSize: Int)(implicit ec: ExecutionContext): Future[
|
||||
Option[(Int, DoubleSha256Digest)]] = {
|
||||
batchSize: Int): Future[Option[(Int, DoubleSha256Digest)]] = {
|
||||
val startHeightF = if (prevStopHash == DoubleSha256DigestBE.empty) {
|
||||
Future.successful(0)
|
||||
} else {
|
||||
@ -151,8 +162,7 @@ case class ChainHandler(
|
||||
/** @inheritdoc */
|
||||
override def nextFilterHeaderBatchRange(
|
||||
prevStopHash: DoubleSha256DigestBE,
|
||||
batchSize: Int)(implicit ec: ExecutionContext): Future[
|
||||
Option[(Int, DoubleSha256Digest)]] = {
|
||||
batchSize: Int): Future[Option[(Int, DoubleSha256Digest)]] = {
|
||||
val startHeightF = if (prevStopHash == DoubleSha256DigestBE.empty) {
|
||||
Future.successful(0)
|
||||
} else {
|
||||
@ -182,8 +192,7 @@ case class ChainHandler(
|
||||
/** @inheritdoc */
|
||||
override def processFilterHeaders(
|
||||
filterHeaders: Vector[FilterHeader],
|
||||
stopHash: DoubleSha256DigestBE)(
|
||||
implicit ec: ExecutionContext): Future[ChainApi] = {
|
||||
stopHash: DoubleSha256DigestBE): Future[ChainApi] = {
|
||||
|
||||
val filterHeadersToCreateF = for {
|
||||
blockHeaders <- blockHeaderDAO
|
||||
@ -223,8 +232,8 @@ case class ChainHandler(
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
override def processFilters(messages: Vector[CompactFilterMessage])(
|
||||
implicit ec: ExecutionContext): Future[ChainApi] = {
|
||||
override def processFilters(
|
||||
messages: Vector[CompactFilterMessage]): Future[ChainApi] = {
|
||||
|
||||
logger.debug(s"processFilters: messages=${messages}")
|
||||
val filterHeadersF = filterHeaderDAO
|
||||
@ -291,8 +300,7 @@ case class ChainHandler(
|
||||
/** @inheritdoc */
|
||||
override def processCheckpoints(
|
||||
checkpoints: Vector[DoubleSha256DigestBE],
|
||||
blockHash: DoubleSha256DigestBE)(
|
||||
implicit ec: ExecutionContext): Future[ChainApi] = {
|
||||
blockHash: DoubleSha256DigestBE): Future[ChainApi] = {
|
||||
|
||||
val blockHeadersF: Future[Seq[BlockHeaderDb]] = Future
|
||||
.traverse(checkpoints.indices.toVector) { i =>
|
||||
@ -317,19 +325,17 @@ case class ChainHandler(
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
override def getFilter(blockHash: DoubleSha256DigestBE)(
|
||||
implicit ec: ExecutionContext): Future[Option[CompactFilterDb]] = {
|
||||
override def getFilter(
|
||||
blockHash: DoubleSha256DigestBE): Future[Option[CompactFilterDb]] = {
|
||||
filterDAO.findByBlockHash(blockHash)
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
override def getHeadersAtHeight(height: Int)(
|
||||
implicit ec: ExecutionContext): Future[Vector[BlockHeaderDb]] =
|
||||
override def getHeadersAtHeight(height: Int): Future[Vector[BlockHeaderDb]] =
|
||||
blockHeaderDAO.getAtHeight(height)
|
||||
|
||||
/** @inheritdoc */
|
||||
override def getFilterHeaderCount(
|
||||
implicit ec: ExecutionContext): Future[Int] = {
|
||||
override def getFilterHeaderCount: Future[Int] = {
|
||||
logger.debug(s"Querying for filter header count")
|
||||
filterHeaderDAO.maxHeight.map { height =>
|
||||
logger.debug(s"getFilterHeaderCount result: count=$height")
|
||||
@ -338,17 +344,17 @@ case class ChainHandler(
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
override def getFilterHeadersAtHeight(height: Int)(
|
||||
implicit ec: ExecutionContext): Future[Vector[CompactFilterHeaderDb]] =
|
||||
override def getFilterHeadersAtHeight(
|
||||
height: Int): Future[Vector[CompactFilterHeaderDb]] =
|
||||
filterHeaderDAO.getAtHeight(height)
|
||||
|
||||
/** @inheritdoc */
|
||||
override def getFilterHeader(blockHash: DoubleSha256DigestBE)(
|
||||
implicit ec: ExecutionContext): Future[Option[CompactFilterHeaderDb]] =
|
||||
override def getFilterHeader(
|
||||
blockHash: DoubleSha256DigestBE): Future[Option[CompactFilterHeaderDb]] =
|
||||
filterHeaderDAO.findByBlockHash(blockHash)
|
||||
|
||||
/** @inheritdoc */
|
||||
override def getFilterCount(implicit ec: ExecutionContext): Future[Int] = {
|
||||
override def getFilterCount: Future[Int] = {
|
||||
logger.debug(s"Querying for filter count")
|
||||
filterDAO.maxHeight.map { height =>
|
||||
logger.debug(s"getFilterCount result: count=$height")
|
||||
@ -357,8 +363,8 @@ case class ChainHandler(
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
override def getFiltersAtHeight(height: Int)(
|
||||
implicit ec: ExecutionContext): Future[Vector[CompactFilterDb]] =
|
||||
override def getFiltersAtHeight(
|
||||
height: Int): Future[Vector[CompactFilterDb]] =
|
||||
filterDAO.getAtHeight(height)
|
||||
|
||||
/** Implements [[ChainApi.getMatchingBlocks()]].
|
||||
@ -384,7 +390,7 @@ case class ChainHandler(
|
||||
endOpt: Option[BlockStamp] = None,
|
||||
batchSize: Int = chainConfig.filterBatchSize,
|
||||
parallelismLevel: Int = Runtime.getRuntime.availableProcessors())(
|
||||
implicit ec: ExecutionContext): Future[Vector[DoubleSha256DigestBE]] = {
|
||||
ec: ExecutionContext): Future[Vector[DoubleSha256DigestBE]] = {
|
||||
require(batchSize > 0, "batch size must be greater than zero")
|
||||
require(parallelismLevel > 0, "parallelism level must be greater than zero")
|
||||
|
||||
@ -485,8 +491,7 @@ case class ChainHandler(
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
override def getHeightByBlockStamp(blockStamp: BlockStamp)(
|
||||
implicit ec: ExecutionContext): Future[Int] =
|
||||
override def getHeightByBlockStamp(blockStamp: BlockStamp): Future[Int] =
|
||||
blockStamp match {
|
||||
case blockHeight: BlockStamp.BlockHeight =>
|
||||
Future.successful(blockHeight.height)
|
||||
@ -501,6 +506,25 @@ case class ChainHandler(
|
||||
Future.failed(new RuntimeException(s"Not implemented: $blockTime"))
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
override def getBlockHeight(
|
||||
blockHash: DoubleSha256DigestBE): Future[Option[Int]] =
|
||||
getHeader(blockHash).map(_.map(_.height))
|
||||
|
||||
/** @inheritdoc */
|
||||
override def getNumberOfConfirmations(
|
||||
blockHash: DoubleSha256DigestBE): Future[Option[Int]] = {
|
||||
getBlockHeight(blockHash).flatMap {
|
||||
case None => FutureUtil.none
|
||||
case Some(blockHeight) =>
|
||||
for {
|
||||
tipHash <- getBestBlockHash()
|
||||
tipHeightOpt <- getBlockHeight(tipHash)
|
||||
} yield {
|
||||
tipHeightOpt.map(tipHeight => tipHeight - blockHeight + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object ChainHandler {
|
||||
@ -530,7 +554,8 @@ object ChainHandler {
|
||||
filterHeaderDAO: CompactFilterHeaderDAO,
|
||||
filterDAO: CompactFilterDAO,
|
||||
blockchains: Blockchain)(
|
||||
implicit chainConfig: ChainAppConfig): ChainHandler = {
|
||||
implicit ec: ExecutionContext,
|
||||
chainConfig: ChainAppConfig): ChainHandler = {
|
||||
new ChainHandler(blockHeaderDAO = blockHeaderDAO,
|
||||
filterHeaderDAO = filterHeaderDAO,
|
||||
filterDAO = filterDAO,
|
||||
|
@ -1,8 +1,9 @@
|
||||
package org.bitcoins.core.api
|
||||
|
||||
import org.bitcoins.core.crypto.DoubleSha256DigestBE
|
||||
import org.bitcoins.core.util.FutureUtil
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.concurrent.Future
|
||||
|
||||
/**
|
||||
* This trait provides methods to query various types of blockchain data.
|
||||
@ -10,12 +11,14 @@ import scala.concurrent.{ExecutionContext, Future}
|
||||
trait ChainQueryApi {
|
||||
|
||||
/** Gets the height of the given block */
|
||||
def getBlockHeight(blockHash: DoubleSha256DigestBE)(
|
||||
implicit ec: ExecutionContext): Future[Option[Int]]
|
||||
def getBlockHeight(blockHash: DoubleSha256DigestBE): Future[Option[Int]]
|
||||
|
||||
/** Gets the hash of the block that is what we consider "best" */
|
||||
def getBestBlockHash(
|
||||
implicit ec: ExecutionContext): Future[DoubleSha256DigestBE]
|
||||
def getBestBlockHash(): Future[DoubleSha256DigestBE]
|
||||
|
||||
/** Gets number of confirmations for the given block hash*/
|
||||
def getNumberOfConfirmations(
|
||||
blockHashOpt: DoubleSha256DigestBE): Future[Option[Int]]
|
||||
|
||||
}
|
||||
|
||||
@ -24,13 +27,17 @@ object ChainQueryApi {
|
||||
object NoOp extends ChainQueryApi {
|
||||
|
||||
/** Gets the height of the given block */
|
||||
override def getBlockHeight(blockHash: DoubleSha256DigestBE)(
|
||||
implicit ec: ExecutionContext): Future[Option[Int]] =
|
||||
Future.successful(None)
|
||||
override def getBlockHeight(
|
||||
blockHash: DoubleSha256DigestBE): Future[Option[Int]] =
|
||||
FutureUtil.none
|
||||
|
||||
/** Gets the hash of the block that is what we consider "best" */
|
||||
override def getBestBlockHash(
|
||||
implicit ec: ExecutionContext): Future[DoubleSha256DigestBE] =
|
||||
override def getBestBlockHash(): Future[DoubleSha256DigestBE] =
|
||||
Future.successful(DoubleSha256DigestBE.empty)
|
||||
|
||||
/** Gets number of confirmations for the given block hash. It returns None of no block found */
|
||||
override def getNumberOfConfirmations(
|
||||
blockHashOpt: DoubleSha256DigestBE): Future[Option[Int]] =
|
||||
FutureUtil.none
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package org.bitcoins.core.util
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.concurrent.ExecutionContext
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
object FutureUtil {
|
||||
|
||||
@ -24,6 +23,8 @@ object FutureUtil {
|
||||
|
||||
val unit: Future[Unit] = Future.successful(())
|
||||
|
||||
def none[T]: Future[Option[T]] = Future.successful(Option.empty[T])
|
||||
|
||||
/**
|
||||
* Folds over the given elements sequentially in a non-blocking async way
|
||||
* @param init the initialized value for the accumulator
|
||||
|
@ -110,22 +110,23 @@ val walletF: Future[LockedWalletApi] = configF.flatMap { _ =>
|
||||
|
||||
// when this future completes, ww have sent a transaction
|
||||
// from bitcoind to the Bitcoin-S wallet
|
||||
import org.bitcoins.core.protocol.transaction.Transaction
|
||||
import org.bitcoins.core.crypto._
|
||||
import org.bitcoins.core.protocol.transaction._
|
||||
import org.bitcoins.core.currency._
|
||||
val transactionF: Future[Transaction] = for {
|
||||
val transactionF: Future[(Transaction, Option[DoubleSha256DigestBE])] = for {
|
||||
wallet <- walletF
|
||||
address <- wallet.getNewAddress()
|
||||
txid <- bitcoind.sendToAddress(address, 3.bitcoin)
|
||||
transaction <- bitcoind.getRawTransaction(txid)
|
||||
} yield transaction.hex
|
||||
} yield (transaction.hex, transaction.blockhash)
|
||||
|
||||
// when this future completes, we have processed
|
||||
// the transaction from bitcoind, and we have
|
||||
// queried our balance for the current balance
|
||||
val balanceF: Future[CurrencyUnit] = for {
|
||||
wallet <- walletF
|
||||
tx <- transactionF
|
||||
_ <- wallet.processTransaction(tx, confirmations = 0)
|
||||
(tx, blockhash) <- transactionF
|
||||
_ <- wallet.processTransaction(tx, blockhash)
|
||||
balance <- wallet.getBalance
|
||||
} yield balance
|
||||
|
||||
|
@ -53,7 +53,7 @@ class NeutrinoNodeWithWalletTest extends NodeUnitTest {
|
||||
val onBlock: DataMessageHandler.OnBlockReceived = { block =>
|
||||
for {
|
||||
wallet <- walletF
|
||||
_ <- wallet.processBlock(block, confirmations = 6)
|
||||
_ <- wallet.processBlock(block)
|
||||
} yield ()
|
||||
}
|
||||
val onCompactFilter: OnCompactFilterReceived = { (blockHash, blockFilter) =>
|
||||
@ -98,14 +98,16 @@ class NeutrinoNodeWithWalletTest extends NodeUnitTest {
|
||||
expectedConfirmedAmount = 0.sats,
|
||||
expectedUnconfirmedAmount = BitcoinSWalletTest.initialFunds - TestAmount - TestFees,
|
||||
expectedUtxos = 1,
|
||||
expectedAddresses = 2)
|
||||
expectedAddresses = 2
|
||||
)
|
||||
}
|
||||
val condition2 = { () =>
|
||||
condition(
|
||||
expectedConfirmedAmount = TestAmount,
|
||||
expectedUnconfirmedAmount = BitcoinSWalletTest.initialFunds - TestAmount - TestFees,
|
||||
expectedUtxos = 2,
|
||||
expectedAddresses = 3)
|
||||
expectedAddresses = 3
|
||||
)
|
||||
}
|
||||
|
||||
for {
|
||||
|
@ -46,7 +46,7 @@ class SpvNodeWithWalletTest extends NodeUnitTest {
|
||||
if (expectedTxId == tx.txId) {
|
||||
for {
|
||||
prevBalance <- wallet.getUnconfirmedBalance()
|
||||
_ <- wallet.processTransaction(tx, confirmations = 0)
|
||||
_ <- wallet.processTransaction(tx, None)
|
||||
balance <- wallet.getUnconfirmedBalance()
|
||||
} yield {
|
||||
val result = balance == prevBalance + amountFromBitcoind
|
||||
|
@ -33,7 +33,7 @@ case class NeutrinoNode(
|
||||
val res = for {
|
||||
node <- super.start()
|
||||
chainApi <- chainApiFromDb()
|
||||
bestHash <- chainApi.getBestBlockHash
|
||||
bestHash <- chainApi.getBestBlockHash()
|
||||
peerMsgSender <- peerMsgSenderF
|
||||
_ <- peerMsgSender.sendGetCompactFilterCheckPointMessage(
|
||||
stopHash = bestHash.flip)
|
||||
|
@ -8,8 +8,8 @@ import org.bitcoins.chain.models.{
|
||||
CompactFilterDAO,
|
||||
CompactFilterHeaderDAO
|
||||
}
|
||||
import org.bitcoins.core.api.NodeApi
|
||||
import org.bitcoins.core.crypto.DoubleSha256Digest
|
||||
import org.bitcoins.core.api.{ChainQueryApi, NodeApi}
|
||||
import org.bitcoins.core.crypto.{DoubleSha256Digest, DoubleSha256DigestBE}
|
||||
import org.bitcoins.core.p2p.{NetworkPayload, TypeIdentifier}
|
||||
import org.bitcoins.core.protocol.BlockStamp
|
||||
import org.bitcoins.core.protocol.script.ScriptPubKey
|
||||
@ -36,7 +36,7 @@ import scala.util.{Failure, Success}
|
||||
/**
|
||||
This a base trait for various kinds of nodes. It contains house keeping methods required for all nodes.
|
||||
*/
|
||||
trait Node extends NodeApi with P2PLogger {
|
||||
trait Node extends NodeApi with ChainQueryApi with P2PLogger {
|
||||
|
||||
implicit def system: ActorSystem
|
||||
|
||||
@ -180,7 +180,7 @@ trait Node extends NodeApi with P2PLogger {
|
||||
def sync(): Future[Unit] = {
|
||||
for {
|
||||
chainApi <- chainApiFromDb()
|
||||
hash <- chainApi.getBestBlockHash
|
||||
hash <- chainApi.getBestBlockHash()
|
||||
header <- chainApi
|
||||
.getHeader(hash)
|
||||
.map(_.get) // .get is safe since this is an internal call
|
||||
@ -223,4 +223,18 @@ trait Node extends NodeApi with P2PLogger {
|
||||
} yield ()
|
||||
}
|
||||
|
||||
/** Gets the height of the given block */
|
||||
override def getBlockHeight(
|
||||
blockHash: DoubleSha256DigestBE): Future[Option[Int]] =
|
||||
chainApiFromDb().flatMap(_.getBlockHeight(blockHash))
|
||||
|
||||
/** Gets the hash of the block that is what we consider "best" */
|
||||
override def getBestBlockHash(): Future[DoubleSha256DigestBE] =
|
||||
chainApiFromDb().flatMap(_.getBestBlockHash())
|
||||
|
||||
/** Gets number of confirmations for the given block hash*/
|
||||
def getNumberOfConfirmations(
|
||||
blockHashOpt: DoubleSha256DigestBE): Future[Option[Int]] =
|
||||
chainApiFromDb().flatMap(_.getNumberOfConfirmations(blockHashOpt))
|
||||
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
package org.bitcoins.testkit.chain
|
||||
|
||||
import org.bitcoins.chain.models.{BlockHeaderDb, BlockHeaderDbHelper}
|
||||
import org.bitcoins.chain.models._
|
||||
import org.bitcoins.core.crypto
|
||||
import org.bitcoins.core.crypto.DoubleSha256DigestBE
|
||||
import org.bitcoins.core.crypto.{DoubleSha256Digest, DoubleSha256DigestBE}
|
||||
import org.bitcoins.core.gcs.{BlockFilter, FilterHeader, GolombFilter}
|
||||
import org.bitcoins.core.protocol.blockchain.{
|
||||
BlockHeader,
|
||||
MainNetChainParams,
|
||||
@ -20,6 +21,20 @@ sealed abstract class ChainTestUtil {
|
||||
lazy val regTestGenesisHeaderDb: BlockHeaderDb = {
|
||||
BlockHeaderDbHelper.fromBlockHeader(height = 0, bh = regTestHeader)
|
||||
}
|
||||
lazy val regTestGenesisHeaderCompactFilter: GolombFilter =
|
||||
BlockFilter.apply(regTestChainParams.genesisBlock, Vector.empty)
|
||||
lazy val regTestGenesisHeaderCompactFilterDb: CompactFilterDb =
|
||||
CompactFilterDbHelper.fromGolombFilter(regTestGenesisHeaderCompactFilter,
|
||||
regTestHeader.hashBE,
|
||||
0)
|
||||
lazy val regTestGenesisHeaderCompactFilterHeader: FilterHeader = FilterHeader(
|
||||
regTestGenesisHeaderCompactFilter.hash,
|
||||
DoubleSha256Digest.empty)
|
||||
lazy val regTestGenesisHeaderCompactFilterHeaderDb: CompactFilterHeaderDb =
|
||||
CompactFilterHeaderDbHelper.fromFilterHeader(
|
||||
regTestGenesisHeaderCompactFilterHeader,
|
||||
regTestHeader.hashBE,
|
||||
0)
|
||||
|
||||
lazy val mainnetChainParam: MainNetChainParams.type = MainNetChainParams
|
||||
|
||||
|
@ -12,10 +12,10 @@ import org.bitcoins.chain.models._
|
||||
import org.bitcoins.core.protocol.blockchain.{Block, BlockHeader}
|
||||
import org.bitcoins.db.AppConfig
|
||||
import org.bitcoins.rpc.client.common.BitcoindRpcClient
|
||||
import org.bitcoins.testkit.{chain, BitcoinSTestAppConfig}
|
||||
import org.bitcoins.testkit.chain.fixture._
|
||||
import org.bitcoins.testkit.fixtures.BitcoinSFixture
|
||||
import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil
|
||||
import org.bitcoins.testkit.{chain, BitcoinSTestAppConfig}
|
||||
import org.bitcoins.zmq.ZMQSubscriber
|
||||
import org.scalatest._
|
||||
import play.api.libs.json.{JsError, JsSuccess, Json}
|
||||
@ -289,6 +289,12 @@ object ChainUnitTest extends ChainVerificationLogger {
|
||||
|
||||
val genesisHeaderDb: BlockHeaderDb = ChainTestUtil.regTestGenesisHeaderDb
|
||||
|
||||
val genesisFilterDb: CompactFilterDb =
|
||||
ChainTestUtil.regTestGenesisHeaderCompactFilterDb
|
||||
|
||||
val genesisFilterHeaderDb: CompactFilterHeaderDb =
|
||||
ChainTestUtil.regTestGenesisHeaderCompactFilterHeaderDb
|
||||
|
||||
def createChainHandler()(
|
||||
implicit ec: ExecutionContext,
|
||||
appConfig: ChainAppConfig): Future[ChainHandler] = {
|
||||
@ -449,6 +455,9 @@ object ChainUnitTest extends ChainVerificationLogger {
|
||||
for {
|
||||
chainHandler <- chainHandlerF
|
||||
genHeader <- chainHandler.blockHeaderDAO.create(genesisHeaderDb)
|
||||
genFilterHeader <- chainHandler.filterHeaderDAO.create(
|
||||
genesisFilterHeaderDb)
|
||||
genFilter <- chainHandler.filterDAO.create(genesisFilterDb)
|
||||
} yield genHeader
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,9 @@ import java.net.InetSocketAddress
|
||||
import akka.actor.ActorSystem
|
||||
import org.bitcoins.chain.api.ChainApi
|
||||
import org.bitcoins.chain.config.ChainAppConfig
|
||||
import org.bitcoins.core.api.ChainQueryApi
|
||||
import org.bitcoins.core.config.NetworkParameters
|
||||
import org.bitcoins.core.crypto.DoubleSha256DigestBE
|
||||
import org.bitcoins.db.AppConfig
|
||||
import org.bitcoins.node._
|
||||
import org.bitcoins.node.config.NodeAppConfig
|
||||
@ -252,10 +254,9 @@ object NodeUnitTest extends P2PLogger {
|
||||
for {
|
||||
bitcoind <- BitcoinSFixture.createBitcoindWithFunds(versionOpt)
|
||||
node <- createSpvNode(bitcoind, callbacks)
|
||||
chain <- node.chainApiFromDb()
|
||||
fundedWallet <- BitcoinSWalletTest.fundedWalletAndBitcoind(bitcoind,
|
||||
node,
|
||||
chain)
|
||||
node)
|
||||
} yield {
|
||||
SpvNodeFundedWalletBitcoind(node = node,
|
||||
wallet = fundedWallet.wallet,
|
||||
@ -274,10 +275,9 @@ object NodeUnitTest extends P2PLogger {
|
||||
for {
|
||||
bitcoind <- BitcoinSFixture.createBitcoindWithFunds(versionOpt)
|
||||
node <- createNeutrinoNode(bitcoind, callbacks)
|
||||
chain <- node.chainApiFromDb()
|
||||
fundedWallet <- BitcoinSWalletTest.fundedWalletAndBitcoind(bitcoind,
|
||||
node,
|
||||
chain)
|
||||
node)
|
||||
} yield {
|
||||
NeutrinoNodeFundedWalletBitcoind(node = node,
|
||||
wallet = fundedWallet.wallet,
|
||||
|
@ -3,7 +3,7 @@ package org.bitcoins.testkit.wallet
|
||||
import akka.actor.ActorSystem
|
||||
import com.typesafe.config.{Config, ConfigFactory}
|
||||
import org.bitcoins.core.api.{ChainQueryApi, NodeApi}
|
||||
import org.bitcoins.core.crypto.DoubleSha256Digest
|
||||
import org.bitcoins.core.crypto.DoubleSha256DigestBE
|
||||
import org.bitcoins.core.currency._
|
||||
import org.bitcoins.core.util.FutureUtil
|
||||
import org.bitcoins.db.AppConfig
|
||||
@ -227,12 +227,10 @@ object BitcoinSWalletTest extends WalletLogger {
|
||||
val WalletWithBitcoind(wallet, bitcoind) = pair
|
||||
for {
|
||||
addr <- wallet.getNewAddress()
|
||||
tx <- bitcoind
|
||||
.sendToAddress(addr, initialFunds)
|
||||
.flatMap(bitcoind.getRawTransaction(_))
|
||||
|
||||
txId <- bitcoind.sendToAddress(addr, initialFunds)
|
||||
_ <- bitcoind.getNewAddress.flatMap(bitcoind.generateToAddress(6, _))
|
||||
_ <- wallet.processTransaction(tx.hex, 6)
|
||||
tx <- bitcoind.getRawTransaction(txId)
|
||||
_ <- wallet.processTransaction(tx.hex, tx.blockhash)
|
||||
balance <- wallet.getBalance()
|
||||
|
||||
} yield {
|
||||
|
@ -1,33 +1,31 @@
|
||||
package org.bitcoins.testkit.wallet
|
||||
|
||||
import org.bitcoins.testkit.Implicits._
|
||||
import org.bitcoins.core.config.RegTest
|
||||
import org.bitcoins.core.crypto._
|
||||
import org.bitcoins.core.currency._
|
||||
import org.bitcoins.core.hd._
|
||||
import org.bitcoins.core.protocol.Bech32Address
|
||||
import org.bitcoins.core.protocol.blockchain.{
|
||||
ChainParams,
|
||||
RegTestNetChainParams
|
||||
}
|
||||
import org.bitcoins.testkit.core.gen.CryptoGenerators
|
||||
import org.bitcoins.wallet.models.AccountDb
|
||||
import org.bitcoins.core.hd._
|
||||
import org.bitcoins.core.protocol.script.ScriptWitness
|
||||
import org.bitcoins.core.protocol.script.P2WPKHWitnessV0
|
||||
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.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.protocol.script.{
|
||||
P2WPKHWitnessSPKV0,
|
||||
P2WPKHWitnessV0,
|
||||
ScriptPubKey,
|
||||
ScriptWitness
|
||||
}
|
||||
import org.bitcoins.core.protocol.transaction.{
|
||||
TransactionOutPoint,
|
||||
TransactionOutput
|
||||
}
|
||||
import org.bitcoins.core.util.CryptoUtil
|
||||
import org.bitcoins.wallet.models.AddressDb
|
||||
import org.bitcoins.testkit.Implicits._
|
||||
import org.bitcoins.testkit.core.gen.{CryptoGenerators, NumberGenerator}
|
||||
import org.bitcoins.testkit.fixtures.WalletDAOs
|
||||
import org.bitcoins.wallet.models._
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
object WalletTestUtil {
|
||||
|
||||
@ -78,9 +76,8 @@ object WalletTestUtil {
|
||||
|
||||
private def randomTXID = CryptoGenerators.doubleSha256Digest.sampleSome.flip
|
||||
private def randomVout = NumberGenerator.uInt32s.sampleSome
|
||||
|
||||
/** Between 0 and 10 confirmations */
|
||||
private def randomConfs: Int = Gen.choose(0, 10).sampleSome
|
||||
private def randomBlockHash =
|
||||
CryptoGenerators.doubleSha256Digest.sampleSome.flip
|
||||
|
||||
private def randomSpent: Boolean = math.random > 0.5
|
||||
|
||||
@ -90,13 +87,15 @@ object WalletTestUtil {
|
||||
TransactionOutput(1.bitcoin, spk)
|
||||
val scriptWitness = randomScriptWitness
|
||||
val privkeyPath = WalletTestUtil.sampleSegwitPath
|
||||
SegwitV0SpendingInfo(confirmations = randomConfs,
|
||||
spent = randomSpent,
|
||||
txid = randomTXID,
|
||||
outPoint = outpoint,
|
||||
output = output,
|
||||
privKeyPath = privkeyPath,
|
||||
scriptWitness = scriptWitness)
|
||||
SegwitV0SpendingInfo(
|
||||
spent = randomSpent,
|
||||
txid = randomTXID,
|
||||
outPoint = outpoint,
|
||||
output = output,
|
||||
privKeyPath = privkeyPath,
|
||||
scriptWitness = scriptWitness,
|
||||
blockHash = Some(randomBlockHash)
|
||||
)
|
||||
}
|
||||
|
||||
def sampleLegacyUTXO(spk: ScriptPubKey): LegacySpendingInfo = {
|
||||
@ -105,12 +104,12 @@ object WalletTestUtil {
|
||||
val output =
|
||||
TransactionOutput(1.bitcoin, spk)
|
||||
val privKeyPath = WalletTestUtil.sampleLegacyPath
|
||||
LegacySpendingInfo(confirmations = randomConfs,
|
||||
spent = randomSpent,
|
||||
LegacySpendingInfo(spent = randomSpent,
|
||||
txid = randomTXID,
|
||||
outPoint = outpoint,
|
||||
output = output,
|
||||
privKeyPath = privKeyPath)
|
||||
privKeyPath = privKeyPath,
|
||||
blockHash = Some(randomBlockHash))
|
||||
}
|
||||
|
||||
/** Given an account returns a sample address */
|
||||
|
@ -1,17 +1,15 @@
|
||||
package org.bitcoins.wallet
|
||||
|
||||
import org.bitcoins.testkit.wallet.BitcoinSWalletTest
|
||||
import org.bitcoins.testkit.Implicits._
|
||||
import org.scalatest.FutureOutcome
|
||||
import org.bitcoins.core.crypto.{DoubleSha256DigestBE, ECPrivateKey}
|
||||
import org.bitcoins.core.currency._
|
||||
import scala.concurrent.Future
|
||||
import org.scalatest.compatible.Assertion
|
||||
import org.bitcoins.wallet.api.UnlockedWalletApi
|
||||
import org.bitcoins.rpc.client.common.BitcoindRpcClient
|
||||
import org.bitcoins.core.protocol.transaction.Transaction
|
||||
import org.bitcoins.testkit.Implicits._
|
||||
import org.bitcoins.testkit.core.gen.TransactionGenerators
|
||||
import org.bitcoins.core.protocol.script.ScriptPubKey
|
||||
import scala.annotation.tailrec
|
||||
import org.bitcoins.testkit.wallet.BitcoinSWalletTest
|
||||
import org.bitcoins.wallet.api.UnlockedWalletApi
|
||||
import org.scalatest.FutureOutcome
|
||||
import org.scalatest.compatible.Assertion
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
class ProcessTransactionTest extends BitcoinSWalletTest {
|
||||
override type FixtureParam = UnlockedWalletApi
|
||||
@ -47,24 +45,26 @@ class ProcessTransactionTest extends BitcoinSWalletTest {
|
||||
tx = TransactionGenerators
|
||||
.transactionTo(address.scriptPubKey)
|
||||
.sampleSome
|
||||
blockHash = DoubleSha256DigestBE.fromBytes(
|
||||
ECPrivateKey.freshPrivateKey.bytes)
|
||||
|
||||
_ <- wallet.processTransaction(tx, confirmations = 0)
|
||||
_ <- wallet.processTransaction(tx, None)
|
||||
oldConfirmed <- wallet.getConfirmedBalance()
|
||||
oldUnconfirmed <- wallet.getUnconfirmedBalance()
|
||||
|
||||
// repeating the action should not make a difference
|
||||
_ <- checkUtxosAndBalance(wallet) {
|
||||
wallet.processTransaction(tx, confirmations = 0)
|
||||
wallet.processTransaction(tx, None)
|
||||
}
|
||||
|
||||
_ <- wallet.processTransaction(tx, confirmations = 3)
|
||||
_ <- wallet.processTransaction(tx, Some(blockHash))
|
||||
newConfirmed <- wallet.getConfirmedBalance()
|
||||
newUnconfirmed <- wallet.getUnconfirmedBalance()
|
||||
utxosPostAdd <- wallet.listUtxos()
|
||||
|
||||
// repeating the action should not make a difference
|
||||
_ <- checkUtxosAndBalance(wallet) {
|
||||
wallet.processTransaction(tx, confirmations = 3)
|
||||
wallet.processTransaction(tx, Some(blockHash))
|
||||
}
|
||||
} yield {
|
||||
val ourOutputs =
|
||||
@ -81,7 +81,7 @@ class ProcessTransactionTest extends BitcoinSWalletTest {
|
||||
val unrelated = TransactionGenerators.transaction.sampleSome
|
||||
for {
|
||||
_ <- checkUtxosAndBalance(wallet) {
|
||||
wallet.processTransaction(unrelated, confirmations = 4)
|
||||
wallet.processTransaction(unrelated, None)
|
||||
}
|
||||
|
||||
balance <- wallet.getBalance()
|
||||
|
@ -1,16 +1,11 @@
|
||||
package org.bitcoins.wallet
|
||||
|
||||
import org.bitcoins.core.currency._
|
||||
import org.bitcoins.core.number.UInt32
|
||||
import org.bitcoins.core.wallet.fee.SatoshisPerByte
|
||||
import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil
|
||||
import org.bitcoins.wallet.api.{AddUtxoError, AddUtxoSuccess, WalletApi}
|
||||
import org.bitcoins.testkit.wallet.BitcoinSWalletTest
|
||||
import org.scalatest.FutureOutcome
|
||||
|
||||
import scala.concurrent.Future
|
||||
import org.bitcoins.core.hd.HDChainType
|
||||
import org.bitcoins.core.wallet.fee.SatoshisPerByte
|
||||
import org.bitcoins.testkit.wallet.BitcoinSWalletTest
|
||||
import org.bitcoins.testkit.wallet.BitcoinSWalletTest.WalletWithBitcoind
|
||||
import org.scalatest.FutureOutcome
|
||||
|
||||
class WalletIntegrationTest extends BitcoinSWalletTest {
|
||||
|
||||
@ -50,10 +45,8 @@ class WalletIntegrationTest extends BitcoinSWalletTest {
|
||||
|
||||
for {
|
||||
addr <- wallet.getNewAddress()
|
||||
|
||||
tx <- bitcoind
|
||||
.sendToAddress(addr, valueFromBitcoind)
|
||||
.flatMap(bitcoind.getRawTransactionRaw(_))
|
||||
txId <- bitcoind.sendToAddress(addr, valueFromBitcoind)
|
||||
tx <- bitcoind.getRawTransactionRaw(txId)
|
||||
|
||||
// before processing TX, wallet should be completely empty
|
||||
_ <- wallet.listUtxos().map(utxos => assert(utxos.isEmpty))
|
||||
@ -63,7 +56,7 @@ class WalletIntegrationTest extends BitcoinSWalletTest {
|
||||
.map(unconfirmed => assert(unconfirmed == 0.bitcoin))
|
||||
|
||||
// after this, tx is unconfirmed in wallet
|
||||
_ <- wallet.processTransaction(tx, confirmations = 0)
|
||||
_ <- wallet.processTransaction(tx, None)
|
||||
|
||||
// we should now have one UTXO in the wallet
|
||||
// it should not be confirmed
|
||||
@ -79,8 +72,11 @@ class WalletIntegrationTest extends BitcoinSWalletTest {
|
||||
.getUnconfirmedBalance()
|
||||
.map(unconfirmed => assert(unconfirmed == valueFromBitcoind))
|
||||
|
||||
_ <- bitcoind.getNewAddress.flatMap(bitcoind.generateToAddress(6, _))
|
||||
rawTx <- bitcoind.getRawTransaction(txId)
|
||||
|
||||
// after this, tx should be confirmed
|
||||
_ <- wallet.processTransaction(tx, confirmations = 6)
|
||||
_ <- wallet.processTransaction(tx, rawTx.blockhash)
|
||||
_ <- wallet
|
||||
.listUtxos()
|
||||
.map { utxos =>
|
||||
|
@ -1,17 +1,18 @@
|
||||
package org.bitcoins.wallet.api
|
||||
|
||||
import org.bitcoins.core.currency._
|
||||
import org.bitcoins.core.number.Int64
|
||||
import org.bitcoins.core.protocol.script.ScriptPubKey
|
||||
import org.bitcoins.core.protocol.transaction.TransactionOutput
|
||||
import org.bitcoins.core.wallet.fee.{FeeUnit, SatoshisPerByte}
|
||||
import org.bitcoins.testkit.core.gen.{TransactionGenerators, WitnessGenerators}
|
||||
import org.bitcoins.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
|
||||
import org.bitcoins.testkit.core.gen.{
|
||||
CryptoGenerators,
|
||||
TransactionGenerators,
|
||||
WitnessGenerators
|
||||
}
|
||||
import org.bitcoins.testkit.wallet.{BitcoinSWalletTest, WalletTestUtil}
|
||||
import org.bitcoins.wallet.models.{SegwitV0SpendingInfo, SpendingInfoDb}
|
||||
import org.scalatest.FutureOutcome
|
||||
|
||||
class CoinSelectorTest extends BitcoinSWalletTest {
|
||||
case class CoinSelectionFixture(
|
||||
@ -30,34 +31,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.sampleSome,
|
||||
output = TransactionOutput(10.sats, ScriptPubKey.empty),
|
||||
privKeyPath = WalletTestUtil.sampleSegwitPath,
|
||||
scriptWitness = WitnessGenerators.scriptWitness.sampleSome
|
||||
scriptWitness = WitnessGenerators.scriptWitness.sampleSome,
|
||||
blockHash = None
|
||||
)
|
||||
val utxo2 = SegwitV0SpendingInfo(
|
||||
confirmations = 0,
|
||||
txid = CryptoGenerators.doubleSha256Digest.sampleSome.flip,
|
||||
spent = false,
|
||||
id = Some(2),
|
||||
outPoint = TransactionGenerators.outPoint.sampleSome,
|
||||
output = TransactionOutput(90.sats, ScriptPubKey.empty),
|
||||
privKeyPath = WalletTestUtil.sampleSegwitPath,
|
||||
scriptWitness = WitnessGenerators.scriptWitness.sampleSome
|
||||
scriptWitness = WitnessGenerators.scriptWitness.sampleSome,
|
||||
blockHash = None
|
||||
)
|
||||
val utxo3 = SegwitV0SpendingInfo(
|
||||
confirmations = 0,
|
||||
txid = CryptoGenerators.doubleSha256Digest.sampleSome.flip,
|
||||
spent = false,
|
||||
id = Some(3),
|
||||
outPoint = TransactionGenerators.outPoint.sampleSome,
|
||||
output = TransactionOutput(20.sats, ScriptPubKey.empty),
|
||||
privKeyPath = WalletTestUtil.sampleSegwitPath,
|
||||
scriptWitness = WitnessGenerators.scriptWitness.sampleSome
|
||||
scriptWitness = WitnessGenerators.scriptWitness.sampleSome,
|
||||
blockHash = None
|
||||
)
|
||||
|
||||
test(CoinSelectionFixture(output, feeRate, utxo1, utxo2, utxo3))
|
||||
|
@ -84,7 +84,7 @@ sealed abstract class Wallet extends LockedWallet with UnlockedWalletApi {
|
||||
signed <- txBuilder.sign
|
||||
ourOuts <- findOurOuts(signed)
|
||||
// TODO internal
|
||||
_ <- processOurTransaction(signed, confirmations = 0)
|
||||
_ <- processOurTransaction(signed, blockHashOpt = None)
|
||||
} yield {
|
||||
logger.debug(
|
||||
s"Signed transaction=${signed.txIdBE.hex} with outputs=${signed.outputs.length}, inputs=${signed.inputs.length}")
|
||||
|
@ -51,17 +51,17 @@ trait LockedWalletApi extends WalletApi {
|
||||
/**
|
||||
* Processes the given transaction, updating our DB state if it's relevant to us.
|
||||
* @param transaction The transaction we're processing
|
||||
* @param confirmations How many confirmations the TX has
|
||||
* @param blockHash Containing block hash
|
||||
*/
|
||||
def processTransaction(
|
||||
transaction: Transaction,
|
||||
confirmations: Int): Future[LockedWalletApi]
|
||||
blockHash: Option[DoubleSha256DigestBE]): Future[LockedWalletApi]
|
||||
|
||||
/**
|
||||
* Processes the give block, updating our DB state if it's relevant to us.
|
||||
* @param block The block we're processing
|
||||
*/
|
||||
def processBlock(block: Block, confirmations: Int): Future[LockedWalletApi]
|
||||
def processBlock(block: Block): Future[LockedWalletApi]
|
||||
|
||||
def processCompactFilter(
|
||||
blockHash: DoubleSha256Digest,
|
||||
|
@ -1,5 +1,6 @@
|
||||
package org.bitcoins.wallet.internal
|
||||
|
||||
import org.bitcoins.core.crypto.DoubleSha256DigestBE
|
||||
import org.bitcoins.core.number.UInt32
|
||||
import org.bitcoins.core.protocol.blockchain.Block
|
||||
import org.bitcoins.core.protocol.transaction.{Transaction, TransactionOutput}
|
||||
@ -23,29 +24,26 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
|
||||
/** @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(incoming, outgoing) =>
|
||||
logger.info(
|
||||
s"Finished processing of transaction=${transaction.txIdBE}. Relevant incomingTXOs=${incoming.length}, outgoingTXOs=${outgoing.length}")
|
||||
this
|
||||
blockHashOpt: Option[DoubleSha256DigestBE]
|
||||
): Future[LockedWallet] = {
|
||||
for {
|
||||
result <- processTransactionImpl(transaction, blockHashOpt)
|
||||
} yield {
|
||||
logger.info(
|
||||
s"Finished processing of transaction=${transaction.txIdBE}. Relevant incomingTXOs=${result.updatedIncoming.length}, outgoingTXOs=${result.updatedOutgoing.length}")
|
||||
this
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
override def processBlock(
|
||||
block: Block,
|
||||
confirmations: Int): Future[LockedWallet] = {
|
||||
logger.info(
|
||||
s"Processing block=${block.blockHeader.hash.flip} with confirmations=$confirmations")
|
||||
override def processBlock(block: Block): Future[LockedWallet] = {
|
||||
logger.info(s"Processing block=${block.blockHeader.hash.flip}")
|
||||
val res = block.transactions.foldLeft(Future.successful(this)) {
|
||||
(acc, transaction) =>
|
||||
for {
|
||||
_ <- acc
|
||||
newWallet <- processTransaction(transaction, confirmations)
|
||||
newWallet <- processTransaction(transaction,
|
||||
Some(block.blockHeader.hash.flip))
|
||||
} yield {
|
||||
newWallet
|
||||
}
|
||||
@ -74,10 +72,10 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
|
||||
*/
|
||||
private[wallet] def processOurTransaction(
|
||||
transaction: Transaction,
|
||||
confirmations: Int): Future[ProcessTxResult] = {
|
||||
blockHashOpt: Option[DoubleSha256DigestBE]): Future[ProcessTxResult] = {
|
||||
logger.info(
|
||||
s"Processing TX from our wallet, transaction=${transaction.txIdBE} with confirmations=$confirmations")
|
||||
processTransactionImpl(transaction, confirmations).map { result =>
|
||||
s"Processing TX from our wallet, transaction=${transaction.txIdBE} with blockHash=$blockHashOpt")
|
||||
processTransactionImpl(transaction, blockHashOpt).map { result =>
|
||||
val txid = transaction.txIdBE
|
||||
val changeOutputs = result.updatedIncoming.length
|
||||
val spentOutputs = result.updatedOutgoing.length
|
||||
@ -98,49 +96,61 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
|
||||
*/
|
||||
private def processTransactionImpl(
|
||||
transaction: Transaction,
|
||||
confirmations: Int): Future[ProcessTxResult] = {
|
||||
blockHashOpt: Option[DoubleSha256DigestBE]): Future[ProcessTxResult] = {
|
||||
|
||||
val incomingTxoFut: Future[Vector[SpendingInfoDb]] =
|
||||
spendingInfoDAO
|
||||
.findTx(transaction)
|
||||
.flatMap {
|
||||
// no existing elements found
|
||||
case Vector() =>
|
||||
processNewIncomingTx(transaction, confirmations).map(_.toVector)
|
||||
logger.info(
|
||||
s"Processing transaction=${transaction.txIdBE} with blockHash=$blockHashOpt")
|
||||
for {
|
||||
aggregate <- {
|
||||
|
||||
case txos: Vector[SpendingInfoDb] =>
|
||||
val txoProcessingFutures =
|
||||
txos
|
||||
.map(processExistingIncomingTxo(transaction, confirmations, _))
|
||||
val incomingTxoFut: Future[Vector[SpendingInfoDb]] =
|
||||
spendingInfoDAO
|
||||
.findTx(transaction)
|
||||
.flatMap {
|
||||
// no existing elements found
|
||||
case Vector() =>
|
||||
processNewIncomingTx(transaction, blockHashOpt)
|
||||
.map(_.toVector)
|
||||
|
||||
Future
|
||||
.sequence(txoProcessingFutures)
|
||||
case txos: Vector[SpendingInfoDb] =>
|
||||
val txoProcessingFutures =
|
||||
txos
|
||||
.map(
|
||||
processExistingIncomingTxo(transaction, blockHashOpt, _))
|
||||
|
||||
Future
|
||||
.sequence(txoProcessingFutures)
|
||||
|
||||
}
|
||||
|
||||
val outgoingTxFut: Future[Vector[SpendingInfoDb]] = {
|
||||
for {
|
||||
outputsBeingSpent <- spendingInfoDAO.findOutputsBeingSpent(
|
||||
transaction)
|
||||
processed <- FutureUtil.sequentially(outputsBeingSpent)(
|
||||
markAsSpentIfUnspent)
|
||||
} yield processed.flatten.toVector
|
||||
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
val aggregateFut =
|
||||
for {
|
||||
incoming <- incomingTxoFut
|
||||
outgoing <- outgoingTxFut
|
||||
} yield {
|
||||
ProcessTxResult(incoming.toList, outgoing.toList)
|
||||
aggregateFut
|
||||
}
|
||||
|
||||
aggregateFut.failed.foreach { err =>
|
||||
val msg = s"Error when processing transaction=${transaction.txIdBE}"
|
||||
logger.error(msg, err)
|
||||
} yield {
|
||||
aggregate
|
||||
}
|
||||
|
||||
aggregateFut
|
||||
}
|
||||
|
||||
/** If the given UTXO is marked as unspent, updates
|
||||
@ -149,7 +159,7 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
|
||||
private val markAsSpentIfUnspent: SpendingInfoDb => Future[
|
||||
Option[SpendingInfoDb]] = { out =>
|
||||
if (out.spent) {
|
||||
Future.successful(None)
|
||||
FutureUtil.none
|
||||
} else {
|
||||
val updatedF =
|
||||
spendingInfoDAO.update(out.copyWithSpent(spent = true))
|
||||
@ -171,11 +181,8 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
|
||||
transaction: Transaction,
|
||||
index: Int,
|
||||
spent: Boolean,
|
||||
confirmations: Int): Future[SpendingInfoDb] =
|
||||
addUtxo(transaction,
|
||||
UInt32(index),
|
||||
spent = spent,
|
||||
confirmations = confirmations)
|
||||
blockHash: Option[DoubleSha256DigestBE]): Future[SpendingInfoDb] =
|
||||
addUtxo(transaction, UInt32(index), spent = spent, blockHash = blockHash)
|
||||
.flatMap {
|
||||
case AddUtxoSuccess(utxo) => Future.successful(utxo)
|
||||
case err: AddUtxoError =>
|
||||
@ -192,48 +199,61 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
|
||||
*/
|
||||
private def processExistingIncomingTxo(
|
||||
transaction: Transaction,
|
||||
confirmations: Int,
|
||||
blockHashOpt: Option[DoubleSha256DigestBE],
|
||||
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."
|
||||
if (foundTxo.txid != transaction.txIdBE) {
|
||||
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.warn(msg)
|
||||
Future.failed(new RuntimeException(msg))
|
||||
logger.error(errMsg)
|
||||
Future.failed(new RuntimeException(errMsg))
|
||||
} 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))
|
||||
(foundTxo.blockHash, blockHashOpt) match {
|
||||
case (None, Some(blockHash)) =>
|
||||
logger.debug(
|
||||
s"Updating block_hash of txo=${transaction.txIdBE}, new block hash=${blockHash}")
|
||||
val updateF =
|
||||
spendingInfoDAO.update(
|
||||
foundTxo
|
||||
.copyWithBlockHash(blockHash = blockHash))
|
||||
|
||||
updateF.foreach(tx =>
|
||||
logger.debug(
|
||||
s"Updated block_hash of txo=${tx.txid} new block hash=${blockHash}"))
|
||||
updateF.failed.foreach(err =>
|
||||
logger.error(
|
||||
s"Failed to update confirmation count of transaction=${transaction.txIdBE}",
|
||||
err))
|
||||
|
||||
updateF
|
||||
case (Some(oldBlockHash), Some(newBlockHash)) =>
|
||||
if (oldBlockHash == newBlockHash) {
|
||||
logger.debug(
|
||||
s"Skipping further processing of transaction=${transaction.txIdBE}, already processed.")
|
||||
Future.successful(foundTxo)
|
||||
} else {
|
||||
val errMsg =
|
||||
Seq(
|
||||
s"Found TXO has block hash=${oldBlockHash}, tx we were given has block hash=${newBlockHash}.",
|
||||
"This is either a reorg or a double spent, which is not implemented yet"
|
||||
).mkString(" ")
|
||||
logger.error(errMsg)
|
||||
Future.failed(new RuntimeException(errMsg))
|
||||
}
|
||||
case (Some(blockHash), None) =>
|
||||
val msg =
|
||||
List(
|
||||
s"Incoming transaction=${transaction.txIdBE} already has block hash=$blockHash! assigned",
|
||||
s" I don't know how to handle this."
|
||||
).mkString(" ")
|
||||
logger.warn(msg)
|
||||
Future.failed(new RuntimeException(msg))
|
||||
case (None, None) =>
|
||||
logger.debug(
|
||||
s"Skipping further processing of transaction=${transaction.txIdBE}, already processed.")
|
||||
Future.successful(foundTxo)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -245,7 +265,7 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
|
||||
*/
|
||||
private def processNewIncomingTx(
|
||||
transaction: Transaction,
|
||||
confirmations: Int): Future[Seq[SpendingInfoDb]] = {
|
||||
blockHashOpt: Option[DoubleSha256DigestBE]): Future[Seq[SpendingInfoDb]] = {
|
||||
addressDAO.findAll().flatMap { addrs =>
|
||||
val relevantOutsWithIdx: Seq[OutputWithIndex] = {
|
||||
val withIndex =
|
||||
@ -281,9 +301,9 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
|
||||
out =>
|
||||
processUtxo(transaction,
|
||||
out.index,
|
||||
confirmations = confirmations,
|
||||
// TODO is this correct?
|
||||
spent = false))
|
||||
spent = false,
|
||||
blockHash = blockHashOpt))
|
||||
}
|
||||
|
||||
addUTXOsFut
|
||||
|
@ -48,30 +48,30 @@ private[wallet] trait UtxoHandling extends WalletLogger {
|
||||
/** 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] = {
|
||||
addressDb: AddressDb,
|
||||
blockHash: Option[DoubleSha256DigestBE]): 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
|
||||
scriptWitness = segwitAddr.witnessScript,
|
||||
blockHash = blockHash
|
||||
)
|
||||
case LegacyAddressDb(path, _, _, _, _) =>
|
||||
LegacySpendingInfo(confirmations = confirmations,
|
||||
spent = spent,
|
||||
LegacySpendingInfo(spent = spent,
|
||||
txid = txid,
|
||||
outPoint = outPoint,
|
||||
output = output,
|
||||
privKeyPath = path)
|
||||
privKeyPath = path,
|
||||
blockHash = blockHash)
|
||||
case nested: NestedSegWitAddressDb =>
|
||||
throw new IllegalArgumentException(
|
||||
s"Bad utxo $nested. Note: nested segwit is not implemented")
|
||||
@ -93,8 +93,8 @@ private[wallet] trait UtxoHandling extends WalletLogger {
|
||||
protected def addUtxo(
|
||||
transaction: Transaction,
|
||||
vout: UInt32,
|
||||
confirmations: Int,
|
||||
spent: Boolean): Future[AddUtxoResult] = {
|
||||
spent: Boolean,
|
||||
blockHash: Option[DoubleSha256DigestBE]): Future[AddUtxoResult] = {
|
||||
import AddUtxoError._
|
||||
|
||||
logger.info(s"Adding UTXO to wallet: ${transaction.txId.hex}:${vout.toInt}")
|
||||
@ -127,11 +127,11 @@ private[wallet] trait UtxoHandling extends WalletLogger {
|
||||
val biasedE: CompatEither[AddUtxoError, Future[SpendingInfoDb]] = for {
|
||||
addressDb <- addressDbE
|
||||
} yield writeUtxo(txid = transaction.txIdBE,
|
||||
confirmations = confirmations,
|
||||
spent = spent,
|
||||
output,
|
||||
outPoint,
|
||||
addressDb)
|
||||
addressDb,
|
||||
blockHash)
|
||||
|
||||
EitherUtil.liftRightBiasedFutureE(biasedE)
|
||||
} map {
|
||||
|
@ -1,7 +1,8 @@
|
||||
package org.bitcoins.wallet.models
|
||||
|
||||
import org.bitcoins.core.crypto.Sign
|
||||
import org.bitcoins.core.crypto.{BIP39Seed, DoubleSha256DigestBE, Sign}
|
||||
import org.bitcoins.core.currency.CurrencyUnit
|
||||
import org.bitcoins.core.hd.{HDPath, LegacyHDPath, SegWitHDPath}
|
||||
import org.bitcoins.core.protocol.script.{ScriptPubKey, ScriptWitness}
|
||||
import org.bitcoins.core.protocol.transaction.{
|
||||
TransactionOutPoint,
|
||||
@ -12,11 +13,6 @@ import org.bitcoins.core.wallet.utxo.{BitcoinUTXOSpendingInfo, ConditionalPath}
|
||||
import org.bitcoins.db.{DbRowAutoInc, TableAutoInc}
|
||||
import slick.jdbc.SQLiteProfile.api._
|
||||
import slick.lifted.ProvenShape
|
||||
import org.bitcoins.core.hd.HDPath
|
||||
import org.bitcoins.core.hd.SegWitHDPath
|
||||
import org.bitcoins.core.crypto.BIP39Seed
|
||||
import org.bitcoins.core.hd.LegacyHDPath
|
||||
import org.bitcoins.core.crypto.DoubleSha256DigestBE
|
||||
|
||||
/**
|
||||
* DB representation of a native V0
|
||||
@ -29,8 +25,8 @@ case class SegwitV0SpendingInfo(
|
||||
scriptWitness: ScriptWitness,
|
||||
txid: DoubleSha256DigestBE,
|
||||
spent: Boolean,
|
||||
confirmations: Int,
|
||||
id: Option[Long] = None
|
||||
id: Option[Long] = None,
|
||||
blockHash: Option[DoubleSha256DigestBE]
|
||||
) extends SpendingInfoDb {
|
||||
override val redeemScriptOpt: Option[ScriptPubKey] = None
|
||||
override val scriptWitnessOpt: Option[ScriptWitness] = Some(scriptWitness)
|
||||
@ -38,13 +34,16 @@ case class SegwitV0SpendingInfo(
|
||||
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 copyWithSpent(spent: Boolean): SegwitV0SpendingInfo =
|
||||
copy(spent = spent)
|
||||
|
||||
override def copyWithId(id: Long): SegwitV0SpendingInfo =
|
||||
copy(id = Some(id))
|
||||
|
||||
/** Updates the `blockHash` field */
|
||||
override def copyWithBlockHash(
|
||||
blockHash: DoubleSha256DigestBE): SegwitV0SpendingInfo =
|
||||
copy(blockHash = Some(blockHash))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -54,9 +53,9 @@ case class LegacySpendingInfo(
|
||||
outPoint: TransactionOutPoint,
|
||||
output: TransactionOutput,
|
||||
privKeyPath: LegacyHDPath,
|
||||
confirmations: Int,
|
||||
spent: Boolean,
|
||||
txid: DoubleSha256DigestBE,
|
||||
blockHash: Option[DoubleSha256DigestBE],
|
||||
id: Option[Long] = None
|
||||
) extends SpendingInfoDb {
|
||||
override val redeemScriptOpt: Option[ScriptPubKey] = None
|
||||
@ -68,10 +67,12 @@ case class LegacySpendingInfo(
|
||||
override def copyWithId(id: Long): LegacySpendingInfo =
|
||||
copy(id = Some(id))
|
||||
|
||||
def copyWithSpent(spent: Boolean): LegacySpendingInfo = copy(spent = spent)
|
||||
override def copyWithSpent(spent: Boolean): LegacySpendingInfo =
|
||||
copy(spent = spent)
|
||||
|
||||
def copyWithConfirmations(confirmations: Int): LegacySpendingInfo =
|
||||
copy(confirmations = confirmations)
|
||||
override def copyWithBlockHash(
|
||||
blockHash: DoubleSha256DigestBE): LegacySpendingInfo =
|
||||
copy(blockHash = Some(blockHash))
|
||||
}
|
||||
|
||||
// TODO add case for nested segwit
|
||||
@ -101,18 +102,15 @@ sealed trait SpendingInfoDb extends DbRowAutoInc[SpendingInfoDb] {
|
||||
|
||||
val hashType: HashType = HashType.sigHashAll
|
||||
|
||||
/** 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
|
||||
|
||||
/** The hash of the block in which the transaction was included */
|
||||
def blockHash: Option[DoubleSha256DigestBE]
|
||||
|
||||
/** Converts the UTXO to the canonical `txid:vout` format */
|
||||
def toHumanReadableString: String =
|
||||
s"${outPoint.txId.flip.hex}:${outPoint.vout.toInt}"
|
||||
@ -120,8 +118,8 @@ sealed trait SpendingInfoDb extends DbRowAutoInc[SpendingInfoDb] {
|
||||
/** Updates the `spent` field */
|
||||
def copyWithSpent(spent: Boolean): SpendingInfoType
|
||||
|
||||
/** Updates the `confirmations` field */
|
||||
def copyWithConfirmations(confirmations: Int): SpendingInfoType
|
||||
/** Updates the `blockHash` field */
|
||||
def copyWithBlockHash(blockHash: DoubleSha256DigestBE): SpendingInfoType
|
||||
|
||||
/** Converts a non-sensitive DB representation of a UTXO into
|
||||
* a signable (and sensitive) real-world UTXO
|
||||
@ -165,8 +163,6 @@ case class SpendingInfoTable(tag: Tag)
|
||||
|
||||
def txid: Rep[DoubleSha256DigestBE] = column("txid")
|
||||
|
||||
def confirmations: Rep[Int] = column("confirmations")
|
||||
|
||||
def spent: Rep[Boolean] = column("spent")
|
||||
|
||||
def scriptPubKey: Rep[ScriptPubKey] = column("script_pub_key")
|
||||
@ -180,6 +176,8 @@ case class SpendingInfoTable(tag: Tag)
|
||||
|
||||
def scriptWitnessOpt: Rep[Option[ScriptWitness]] = column("script_witness")
|
||||
|
||||
def blockHash: Rep[Option[DoubleSha256DigestBE]] = column("block_hash")
|
||||
|
||||
/** All UTXOs must have a SPK in the wallet that gets spent to */
|
||||
def fk_scriptPubKey = {
|
||||
val addressTable = TableQuery[AddressTable]
|
||||
@ -196,9 +194,9 @@ case class SpendingInfoTable(tag: Tag)
|
||||
HDPath,
|
||||
Option[ScriptPubKey], // ReedemScript
|
||||
Option[ScriptWitness],
|
||||
Int, // confirmations
|
||||
Boolean, // spent
|
||||
DoubleSha256DigestBE // TXID
|
||||
DoubleSha256DigestBE, // TXID
|
||||
Option[DoubleSha256DigestBE] // block hash
|
||||
)
|
||||
|
||||
private val fromTuple: UTXOTuple => SpendingInfoDb = {
|
||||
@ -209,18 +207,18 @@ case class SpendingInfoTable(tag: Tag)
|
||||
path: SegWitHDPath,
|
||||
None, // ReedemScript
|
||||
Some(scriptWitness),
|
||||
confirmations,
|
||||
spent,
|
||||
txid) =>
|
||||
txid,
|
||||
blockHash) =>
|
||||
SegwitV0SpendingInfo(
|
||||
outPoint = outpoint,
|
||||
output = TransactionOutput(value, spk),
|
||||
privKeyPath = path,
|
||||
scriptWitness = scriptWitness,
|
||||
id = id,
|
||||
confirmations = confirmations,
|
||||
spent = spent,
|
||||
txid = txid
|
||||
txid = txid,
|
||||
blockHash = blockHash
|
||||
)
|
||||
|
||||
case (id,
|
||||
@ -230,20 +228,29 @@ case class SpendingInfoTable(tag: Tag)
|
||||
path: LegacyHDPath,
|
||||
None, // RedeemScript
|
||||
None, // ScriptWitness
|
||||
confirmations,
|
||||
spent,
|
||||
txid) =>
|
||||
txid,
|
||||
blockHash) =>
|
||||
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) =>
|
||||
txid = txid,
|
||||
blockHash = blockHash)
|
||||
case (id,
|
||||
outpoint,
|
||||
spk,
|
||||
value,
|
||||
path,
|
||||
spkOpt,
|
||||
swOpt,
|
||||
spent,
|
||||
txid,
|
||||
blockHash) =>
|
||||
throw new IllegalArgumentException(
|
||||
"Could not construct UtxoSpendingInfoDb from bad tuple:"
|
||||
+ s" ($id, $outpoint, $spk, $value, $path, $spkOpt, $swOpt, $confs, $spent, $txid)."
|
||||
+ s" ($id, $outpoint, $spk, $value, $path, $spkOpt, $swOpt, $spent, $txid, $blockHash)."
|
||||
+ " Note: Nested Segwit is not implemented")
|
||||
|
||||
}
|
||||
@ -258,9 +265,9 @@ case class SpendingInfoTable(tag: Tag)
|
||||
utxo.privKeyPath,
|
||||
utxo.redeemScriptOpt,
|
||||
utxo.scriptWitnessOpt,
|
||||
utxo.confirmations,
|
||||
utxo.spent,
|
||||
utxo.txid))
|
||||
utxo.txid,
|
||||
utxo.blockHash))
|
||||
|
||||
def * : ProvenShape[SpendingInfoDb] =
|
||||
(id.?,
|
||||
@ -270,7 +277,7 @@ case class SpendingInfoTable(tag: Tag)
|
||||
privKeyPath,
|
||||
redeemScriptOpt,
|
||||
scriptWitnessOpt,
|
||||
confirmations,
|
||||
spent,
|
||||
txid) <> (fromTuple, toTuple)
|
||||
txid,
|
||||
blockHash) <> (fromTuple, toTuple)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user