2021 12 14 websockets (#3906)

* WIP

* Get basic websocket working (sort of)

* Push websocket fixes

* WIP: Implementing SpendingInfoDb json serializer

* Implement onreserved callbacks, add json serializers for SpendingInfoDb

* Get first unit test working for websockets when a address is generated

* Rework WalletNotification to have case classes, get unit test working for receiving addresses over the websocket

* WIP

* Add websocket callback for when a wallet processes a block

* Cleanup

* basic updates to unit test

* Fix compile

* Fix submodule

* Fix compile

* Get both unit tests passing when run by themselves

* Fix so both test cases can be run

* Implement unit tests for blockpressed and reservedutxos websockets

* Fix RoutesSpec get a block header test

* Implement configuration for wsbind and wsport via bitcoin-s.conf

* Add some nonblocking sleeps on all WebsocketTests

* Add documentation

* Properly close the server with .terminate() rather than .unbind()

* Add BitcoinSServerMainBitcoindFixture.afterAll()

* Add println

* Add more println

* Try to downgrade bitcoind version

* Fix datadir bug

* Cleanup callbacks so they don't have nested futures

* Fix SpendingInfoDb.apply() pattern match

* Add spendingInfoDb json serializer test, does not pass

* Fix SpendingInfoDb json serializer

* Make small refactors

* Fix compile

* Add maxBufferSize, change overflow strategy to OverflowStrategy.dropHead

* Address Nadav's code review

* Address ben's code review
This commit is contained in:
Chris Stewart 2021-12-25 11:11:04 -06:00 committed by GitHub
parent 41b96c4c7e
commit 50bec2abc6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1448 additions and 218 deletions

View File

@ -0,0 +1,20 @@
package org.bitcoins.commons.json
import org.bitcoins.commons.serializers.Picklers
import org.bitcoins.core.api.wallet.db.SpendingInfoDb
import org.bitcoins.testkitcore.util.{BitcoinSUnitTest, TransactionTestUtil}
class SpendingInfoDbSerializerTest extends BitcoinSUnitTest {
behavior of "SpendingInfoDbSerializer"
it must "be symmetrical" in {
val original = TransactionTestUtil.spendingInfoDb
val json = upickle.default.writeJs[SpendingInfoDb](original)(
Picklers.spendingInfoDbPickler)
val parsed = upickle.default.read(json)(Picklers.spendingInfoDbPickler)
assert(parsed == original)
}
}

View File

@ -20,13 +20,19 @@ class ServerArgParserTest extends BitcoinSUnitTest {
it must "handle having all command line args we support" in {
val datadir = BitcoinSTestAppConfig.tmpDir()
val datadirString = datadir.toAbsolutePath.toString
val args = Vector("--rpcport",
"1234",
"--rpcbind",
"my.cool.site.com",
"--datadir",
s"${datadirString}",
"--force-recalc-chainwork")
val args = Vector(
"--rpcport",
"1234",
"--rpcbind",
"my.cool.site.com",
"--datadir",
s"${datadirString}",
"--force-recalc-chainwork",
"--wsbind",
"ws.my.cool.site.com",
"--wsport",
"5678"
)
val parser = ServerArgParser(args)
val config = parser.toConfig
@ -37,6 +43,9 @@ class ServerArgParserTest extends BitcoinSUnitTest {
assert(config.hasPath(s"bitcoin-s.server.rpcbind"))
assert(config.hasPath(s"bitcoin-s.server.rpcport"))
assert(config.hasPath(s"bitcoin-s.chain.force-recalc-chainwork"))
assert(config.hasPath("bitcoin-s.server.wsport"))
assert(config.hasPath("bitcoin-s.server.wsbind"))
val datadirFromConfig = config.getString(datadirPathConfigKey)
val path = Paths.get(datadirFromConfig)
assert(path == datadir)

View File

@ -133,6 +133,11 @@ object RpcOpts {
object LockUnspentOutputParameter {
def fromOutPoint(
outPoint: TransactionOutPoint): LockUnspentOutputParameter = {
LockUnspentOutputParameter(outPoint.txIdBE, outPoint.vout.toInt)
}
def fromJsonString(str: String): LockUnspentOutputParameter = {
val json = ujson.read(str)
fromJson(json)

View File

@ -0,0 +1,81 @@
package org.bitcoins.commons.jsonmodels.ws
import org.bitcoins.commons.jsonmodels.bitcoind.GetBlockHeaderResult
import org.bitcoins.core.api.wallet.db.SpendingInfoDb
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.crypto.StringFactory
/** The event type being sent over the websocket. An example is [[WalletWsType.BlockProcessed]] */
sealed trait WsType
object WsType extends StringFactory[WsType] {
override def fromString(string: String): WsType = {
WalletWsType.fromString(string)
}
}
sealed trait WalletWsType extends WsType
object WalletWsType extends StringFactory[WalletWsType] {
case object TxProcessed extends WalletWsType
case object TxBroadcast extends WalletWsType
case object ReservedUtxos extends WalletWsType
case object NewAddress extends WalletWsType
case object BlockProcessed extends WalletWsType
private val all =
Vector(TxProcessed, TxBroadcast, ReservedUtxos, NewAddress, BlockProcessed)
override def fromStringOpt(string: String): Option[WalletWsType] = {
all.find(_.toString.toLowerCase() == string.toLowerCase)
}
override def fromString(string: String): WalletWsType = {
fromStringOpt(string)
.getOrElse(sys.error(s"Cannot find wallet ws type for string=$string"))
}
}
/** A notification that we send over the websocket.
* The type of the notification is indicated by [[WsType]].
* An example is [[org.bitcoins.commons.jsonmodels.ws.WalletNotification.NewAddressNotification]]
* This sends a notification that the wallet generated a new address
*/
sealed trait WsNotification[T] {
def `type`: WsType
def payload: T
}
sealed trait WalletNotification[T] extends WsNotification[T] {
override def `type`: WalletWsType
}
object WalletNotification {
case class NewAddressNotification(payload: BitcoinAddress)
extends WalletNotification[BitcoinAddress] {
override val `type`: WalletWsType = WalletWsType.NewAddress
}
case class TxProcessedNotification(payload: Transaction)
extends WalletNotification[Transaction] {
override val `type`: WalletWsType = WalletWsType.TxProcessed
}
case class TxBroadcastNotification(payload: Transaction)
extends WalletNotification[Transaction] {
override val `type`: WalletWsType = WalletWsType.TxBroadcast
}
case class ReservedUtxosNotification(payload: Vector[SpendingInfoDb])
extends WalletNotification[Vector[SpendingInfoDb]] {
override val `type`: WalletWsType = WalletWsType.ReservedUtxos
}
case class BlockProcessedNotification(payload: GetBlockHeaderResult)
extends WalletNotification[GetBlockHeaderResult] {
override val `type`: WalletWsType = WalletWsType.BlockProcessed
}
}

View File

@ -1,13 +1,16 @@
package org.bitcoins.commons.serializers
import org.bitcoins.commons.jsonmodels.bitcoind.GetBlockHeaderResult
import org.bitcoins.commons.jsonmodels.bitcoind.RpcOpts.LockUnspentOutputParameter
import org.bitcoins.commons.serializers.JsonReaders.jsToSatoshis
import org.bitcoins.core.api.wallet.CoinSelectionAlgo
import org.bitcoins.core.api.wallet.db.SpendingInfoDb
import org.bitcoins.core.crypto._
import org.bitcoins.core.currency.{Bitcoins, Satoshis}
import org.bitcoins.core.dlc.accounting.DLCWalletAccounting
import org.bitcoins.core.hd.AddressType
import org.bitcoins.core.number.{UInt16, UInt32, UInt64}
import org.bitcoins.core.hd.{AddressType, HDPath}
import org.bitcoins.core.number.{Int32, UInt16, UInt32, UInt64}
import org.bitcoins.core.protocol.blockchain.Block
import org.bitcoins.core.protocol.dlc.models.DLCStatus._
import org.bitcoins.core.protocol.dlc.models._
import org.bitcoins.core.protocol.script.{
@ -17,7 +20,11 @@ import org.bitcoins.core.protocol.script.{
WitnessScriptPubKey
}
import org.bitcoins.core.protocol.tlv._
import org.bitcoins.core.protocol.transaction.{Transaction, TransactionOutPoint}
import org.bitcoins.core.protocol.transaction.{
Transaction,
TransactionOutPoint,
TransactionOutput
}
import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp}
import org.bitcoins.core.psbt.InputPSBTRecord.PartialSignature
import org.bitcoins.core.psbt.PSBT
@ -25,7 +32,7 @@ import org.bitcoins.core.serializers.PicklerKeys
import org.bitcoins.core.util.TimeUtil
import org.bitcoins.core.util.TimeUtil._
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.core.wallet.utxo.AddressLabelTag
import org.bitcoins.core.wallet.utxo.{AddressLabelTag, TxoState}
import org.bitcoins.crypto._
import scodec.bits.ByteVector
import ujson._
@ -129,6 +136,111 @@ object Picklers {
implicit val lnMessageDLCOfferTLVPickler: ReadWriter[LnMessage[DLCOfferTLV]] =
readwriter[String].bimap(_.hex, LnMessageFactory(DLCOfferTLV).fromHex)
implicit val txoStatePickler: ReadWriter[TxoState] = {
readwriter[String].bimap(_.toString.toLowerCase, TxoState.fromString)
}
implicit val privKeyPathPickler: ReadWriter[HDPath] = {
readwriter[String].bimap(_.toString, HDPath.fromString)
}
implicit val scriptPubKeyPickler: ReadWriter[ScriptPubKey] = {
readwriter[String].bimap(_.asmHex, ScriptPubKey.fromAsmHex(_))
}
private def parseWitnessElements(arr: ujson.Arr): ScriptWitness = {
val stackElements = arr.value.toVector.map {
case obj: ujson.Obj =>
val witnessStr = obj(PicklerKeys.witnessKey).str
ByteVector.fromValidHex(witnessStr)
case x: ujson.Value =>
sys.error(s"Expected witness json object, got=$x")
}
ScriptWitness.apply(stackElements.reverse)
}
private def writeWitnessElements(witness: ScriptWitness): ujson.Arr = {
val vec: Vector[ujson.Obj] = witness.stack.reverse.map { w =>
ujson.Obj(PicklerKeys.witnessKey -> Str(w.toHex))
}.toVector
ujson.Arr.from(vec)
}
implicit val scriptWitnessPickler: ReadWriter[ScriptWitness] = {
readwriter[Arr].bimap(writeWitnessElements, parseWitnessElements)
}
private def writeOutput(o: TransactionOutput): Obj = {
Obj(
PicklerKeys.satoshisKey -> writeJs(o.value.satoshis),
PicklerKeys.scriptPubKeyKey -> writeJs(o.scriptPubKey)
)
}
private def readOutput(obj: Obj): TransactionOutput = {
val sats = Satoshis(obj(PicklerKeys.satoshisKey).num.toLong)
val scriptPubKey =
ScriptPubKey.fromAsmHex(obj(PicklerKeys.scriptPubKeyKey).str)
TransactionOutput(sats, scriptPubKey)
}
implicit val txOutputPickler: ReadWriter[TransactionOutput] =
readwriter[Obj].bimap(writeOutput, readOutput)
private def writeSpendingInfoDb(si: SpendingInfoDb): Obj = {
Obj(
PicklerKeys.idKey -> {
si.id match {
case None => ujson.Null
case Some(id) => Num(id.toDouble)
}
},
PicklerKeys.outPointKey -> writeJs(si.outPoint),
PicklerKeys.outputKey -> writeJs(si.output),
PicklerKeys.hdPathKey -> writeJs(si.privKeyPath),
PicklerKeys.redeemScriptKey -> writeJs(si.redeemScriptOpt),
PicklerKeys.witnessKey -> writeJs(si.scriptWitnessOpt),
PicklerKeys.stateKey -> writeJs(si.state),
PicklerKeys.txIdKey -> writeJs(si.txid),
PicklerKeys.spendingTxIdKey -> writeJs(si.spendingTxIdOpt)
)
}
private def readSpendingInfoDb(obj: Obj): SpendingInfoDb = {
val id = obj(PicklerKeys.idKey).numOpt.map(_.toLong)
val outpoint =
upickle.default.read[TransactionOutPoint](obj(PicklerKeys.outPointKey))
val output =
upickle.default.read[TransactionOutput](obj(PicklerKeys.outputKey))
val hdPath = upickle.default.read[HDPath](obj(PicklerKeys.hdPathKey))
val redeemScript = upickle.default.read[Option[ScriptPubKey]](
obj(PicklerKeys.redeemScriptKey))
val scriptWitness =
upickle.default.read[Option[ScriptWitness]](obj(PicklerKeys.witnessKey))
val state = upickle.default.read[TxoState](obj(PicklerKeys.stateKey))
val txId =
upickle.default.read[DoubleSha256DigestBE](obj(PicklerKeys.txIdKey))
val spendingTxId = upickle.default.read[Option[DoubleSha256DigestBE]](
obj(PicklerKeys.spendingTxIdKey))
SpendingInfoDb(
id = id,
outpoint = outpoint,
output = output,
hdPath = hdPath,
redeemScriptOpt = redeemScript,
scriptWitnessOpt = scriptWitness,
state = state,
txId = txId,
spendingTxIdOpt = spendingTxId
)
}
implicit val spendingInfoDbPickler: ReadWriter[SpendingInfoDb] = {
readwriter[Obj].bimap(writeSpendingInfoDb, readSpendingInfoDb)
}
private def parseU64(str: ujson.Str): UInt64 = {
UInt64(BigInt(str.str))
}
@ -274,43 +386,27 @@ object Picklers {
arr.map {
case obj: ujson.Obj =>
val witnessElementsArr = obj(PicklerKeys.witnessElementsKey).arr
val witnesses: Vector[ByteVector] = {
val witness: ScriptWitness = {
parseWitnessElements(witnessElementsArr)
}
val scriptWitnessV0 = ScriptWitness
.apply(witnesses.reverse)
.asInstanceOf[ScriptWitnessV0]
val scriptWitnessV0 = witness.asInstanceOf[ScriptWitnessV0]
scriptWitnessV0
case x =>
sys.error(s"Expected array of objects for funding signatures, got=$x")
}
}
private def parseWitnessElements(arr: ujson.Arr): Vector[ByteVector] = {
arr.value.toVector.map {
case obj: ujson.Obj =>
val witnessStr = obj(PicklerKeys.witnessKey).str
ByteVector.fromValidHex(witnessStr)
case x: ujson.Value =>
sys.error(s"Expected witness json object, got=$x")
}
}
private def writeWitnessElements(witness: ScriptWitness): ujson.Obj = {
val vec: Vector[ujson.Obj] = witness.stack.reverse.map { w =>
ujson.Obj(PicklerKeys.witnessKey -> Str(w.toHex))
}.toVector
ujson.Obj(PicklerKeys.witnessElementsKey -> ujson.Arr.from(vec))
}
private def writeFundingSignatures(
fundingSigs: FundingSignaturesTLV): ujson.Obj = {
val sigs: Vector[ujson.Obj] = fundingSigs match {
case v0: FundingSignaturesV0TLV =>
val witnessJson: Vector[ujson.Obj] =
v0.witnesses.map(writeWitnessElements)
val witnessJson: Vector[Obj] = {
v0.witnesses.map { wit =>
val witJson = writeWitnessElements(wit)
ujson.Obj(PicklerKeys.witnessElementsKey -> witJson)
}
}
witnessJson
}
ujson.Obj(
@ -369,6 +465,10 @@ object Picklers {
implicit val transactionPickler: ReadWriter[Transaction] =
readwriter[String].bimap(_.hex, Transaction.fromHex)
implicit val blockPickler: ReadWriter[Block] = {
readwriter[String].bimap(_.hex, Block.fromHex)
}
implicit val extPubKeyPickler: ReadWriter[ExtPublicKey] =
readwriter[String].bimap(_.toString, ExtPublicKey.fromString)
@ -1141,4 +1241,81 @@ object Picklers {
}
ContractDescriptorV0TLV(outcomes = payouts)
}
private def readBlockHeaderResult(obj: Obj): GetBlockHeaderResult = {
val hash = DoubleSha256DigestBE.fromHex(obj(PicklerKeys.hashKey).str)
val confirmations = obj(PicklerKeys.confirmationsKey).num.toInt
val height = obj(PicklerKeys.heightKey).num.toInt
val version = obj(PicklerKeys.versionKey).num.toInt
val versionHex = Int32.fromHex(obj(PicklerKeys.versionHexKey).str)
val merkleroot =
DoubleSha256DigestBE.fromHex(obj(PicklerKeys.merklerootKey).str)
val time = UInt32(obj(PicklerKeys.timeKey).num.toLong)
val mediantime = UInt32(obj(PicklerKeys.mediantimeKey).num.toLong)
val nonce = UInt32(obj(PicklerKeys.nonceKey).num.toLong)
val bits = UInt32.fromHex(obj(PicklerKeys.bitsKey).str)
val difficulty = obj(PicklerKeys.difficultyKey).num
val chainWork = obj(PicklerKeys.chainworkKey).str
val previousBlockHash = obj(PicklerKeys.previousblockhashKey).strOpt.map {
str =>
DoubleSha256DigestBE.fromHex(str)
}
val nextblockhash = obj(PicklerKeys.nextblockhashKey).strOpt.map { str =>
DoubleSha256DigestBE.fromHex(str)
}
GetBlockHeaderResult(
hash = hash,
confirmations = confirmations,
height = height,
version = version,
versionHex = versionHex,
merkleroot = merkleroot,
time = time,
mediantime = mediantime,
nonce = nonce,
bits = bits,
difficulty = difficulty,
chainwork = chainWork,
previousblockhash = previousBlockHash,
nextblockhash = nextblockhash
)
}
private def writeBlockHeaderResult(header: GetBlockHeaderResult): Obj = {
val json = Obj(
PicklerKeys.rawKey -> Str(header.blockHeader.hex),
PicklerKeys.hashKey -> Str(header.hash.hex),
PicklerKeys.confirmationsKey -> Num(header.confirmations),
PicklerKeys.heightKey -> Num(header.height),
PicklerKeys.versionKey -> Num(header.version.toLong.toDouble),
PicklerKeys.versionHexKey -> Str(Int32(header.version).hex),
PicklerKeys.merklerootKey -> Str(header.merkleroot.hex),
PicklerKeys.timeKey -> Num(header.time.toBigInt.toDouble),
PicklerKeys.mediantimeKey -> Num(header.mediantime.toLong.toDouble),
PicklerKeys.nonceKey -> Num(header.nonce.toBigInt.toDouble),
PicklerKeys.bitsKey -> Str(header.bits.hex),
PicklerKeys.difficultyKey -> Num(header.difficulty.toDouble),
PicklerKeys.chainworkKey -> Str(header.chainwork),
PicklerKeys.previousblockhashKey -> {
header.previousblockhash.map(_.hex) match {
case Some(str) => Str(str)
case None => ujson.Null
}
},
PicklerKeys.nextblockhashKey -> {
header.nextblockhash.map(_.hex) match {
case Some(str) => Str(str)
case None => ujson.Null
}
}
)
json
}
implicit val getBlockHeaderResultPickler: ReadWriter[GetBlockHeaderResult] = {
readwriter[ujson.Obj]
.bimap(writeBlockHeaderResult(_), readBlockHeaderResult(_))
}
}

View File

@ -0,0 +1,105 @@
package org.bitcoins.commons.serializers
import org.bitcoins.commons.jsonmodels.ws.WalletNotification.{
BlockProcessedNotification,
NewAddressNotification,
ReservedUtxosNotification,
TxBroadcastNotification,
TxProcessedNotification
}
import org.bitcoins.commons.jsonmodels.ws.{WalletNotification, WalletWsType}
import org.bitcoins.core.serializers.PicklerKeys
import upickle.default._
object WsPicklers {
implicit val walletWsTypePickler: ReadWriter[WalletWsType] = {
readwriter[ujson.Str]
.bimap(_.toString.toLowerCase, str => WalletWsType.fromString(str.str))
}
private def writeWalletNotification(
notification: WalletNotification[_]): ujson.Obj = {
val payloadJson: ujson.Value = notification match {
case TxBroadcastNotification(tx) =>
upickle.default.writeJs(tx)(Picklers.transactionPickler)
case TxProcessedNotification(tx) =>
upickle.default.writeJs(tx)(Picklers.transactionPickler)
case NewAddressNotification(address) =>
upickle.default.writeJs(address)(Picklers.bitcoinAddressPickler)
case ReservedUtxosNotification(utxos) =>
val vec = utxos.map(u =>
upickle.default.writeJs(u)(Picklers.spendingInfoDbPickler))
ujson.Arr.from(vec)
case BlockProcessedNotification(block) =>
upickle.default.writeJs(block)(Picklers.getBlockHeaderResultPickler)
}
val notificationObj = ujson.Obj(
PicklerKeys.typeKey -> writeJs(notification.`type`),
PicklerKeys.payloadKey -> payloadJson
)
notificationObj
}
private def readWalletNotification(obj: ujson.Obj): WalletNotification[_] = {
val typeObj = read[WalletWsType](obj(PicklerKeys.typeKey))
val payloadObj = obj(PicklerKeys.payloadKey)
typeObj match {
case WalletWsType.TxBroadcast =>
val tx = upickle.default.read(payloadObj)(Picklers.transactionPickler)
TxBroadcastNotification(tx)
case WalletWsType.TxProcessed =>
val tx = upickle.default.read(payloadObj)(Picklers.transactionPickler)
TxProcessedNotification(tx)
case WalletWsType.NewAddress =>
val address =
upickle.default.read(payloadObj)(Picklers.bitcoinAddressPickler)
NewAddressNotification(address)
case WalletWsType.ReservedUtxos =>
val utxos = obj(PicklerKeys.payloadKey).arr.toVector.map { utxoJson =>
upickle.default.read(utxoJson)(Picklers.spendingInfoDbPickler)
}
ReservedUtxosNotification(utxos)
case WalletWsType.BlockProcessed =>
val block =
upickle.default.read(payloadObj)(Picklers.getBlockHeaderResultPickler)
BlockProcessedNotification(block)
}
}
implicit val newAddressPickler: ReadWriter[NewAddressNotification] = {
readwriter[ujson.Obj].bimap(
writeWalletNotification(_),
readWalletNotification(_).asInstanceOf[NewAddressNotification])
}
implicit val txProcessedPickler: ReadWriter[TxProcessedNotification] = {
readwriter[ujson.Obj].bimap(
writeWalletNotification(_),
readWalletNotification(_).asInstanceOf[TxProcessedNotification])
}
implicit val txBroadcastPickler: ReadWriter[TxBroadcastNotification] = {
readwriter[ujson.Obj].bimap(
writeWalletNotification(_),
readWalletNotification(_).asInstanceOf[TxBroadcastNotification])
}
implicit val reservedUtxosPickler: ReadWriter[ReservedUtxosNotification] = {
readwriter[ujson.Obj].bimap(
writeWalletNotification(_),
readWalletNotification(_).asInstanceOf[ReservedUtxosNotification])
}
implicit val walletNotificationPickler: ReadWriter[WalletNotification[_]] = {
readwriter[ujson.Obj].bimap(writeWalletNotification, readWalletNotification)
}
implicit val blockProcessedPickler: ReadWriter[BlockProcessedNotification] = {
readwriter[ujson.Obj].bimap(
writeWalletNotification(_),
readWalletNotification(_).asInstanceOf[BlockProcessedNotification]
)
}
}

View File

@ -30,6 +30,20 @@ case class ServerArgParser(commandLineArgs: Vector[String]) {
}
}
lazy val wsBindOpt: Option[String] = {
val wsBindOpt = argsWithIndex.find(_._1.toLowerCase == "--wsbind")
wsBindOpt.map { case (_, idx) =>
commandLineArgs(idx + 1)
}
}
lazy val wsPortOpt: Option[Int] = {
val portOpt = argsWithIndex.find(_._1.toLowerCase == "--wsport")
portOpt.map { case (_, idx) =>
commandLineArgs(idx + 1).toInt
}
}
lazy val networkOpt: Option[BitcoinNetwork] = {
val netOpt = argsWithIndex.find(_._1.toLowerCase == "--network")
netOpt.map { case (_, idx) =>
@ -109,10 +123,27 @@ case class ServerArgParser(commandLineArgs: Vector[String]) {
""
}
val wsBindString = wsBindOpt match {
case Some(wsBind) =>
s"bitcoin-s.server.wsbind=$wsBind\n"
case None => ""
}
val wsPortString = wsPortOpt match {
case Some(wsport) =>
s"bitcoin-s.server.wsport=$wsport\n"
case None => ""
}
//omitting configOpt as i don't know if we can do anything with that?
val all =
rpcPortString + rpcBindString + datadirString + forceChainWorkRecalcString
rpcPortString +
rpcBindString +
datadirString +
forceChainWorkRecalcString +
wsBindString +
wsPortString
ConfigFactory.parseString(all)
}

View File

@ -32,12 +32,14 @@ class OracleServerMain(override val serverArgParser: ServerArgParser)(implicit
Server(conf = conf,
handlers = routes,
rpcbindOpt = bindConfOpt,
rpcport = rpcport)
rpcport = rpcport,
None)
case None =>
Server(conf = conf,
handlers = routes,
rpcbindOpt = bindConfOpt,
rpcport = conf.rpcPort)
rpcport = conf.rpcPort,
None)
}
_ <- server.start()

View File

@ -1,14 +1,26 @@
package org.bitcoins.server.routes
import akka.NotUsed
import akka.actor.ActorSystem
import akka.event.Logging
import akka.http.scaladsl._
import akka.http.scaladsl.model._
import akka.http.scaladsl.model.ws.Message
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._
import akka.http.scaladsl.server.directives.DebuggingDirectives
import akka.stream.OverflowStrategy
import akka.stream.scaladsl.{
BroadcastHub,
Flow,
Keep,
Sink,
Source,
SourceQueueWithComplete
}
import de.heikoseeberger.akkahttpupickle.UpickleSupport._
import org.bitcoins.commons.config.AppConfig
import org.bitcoins.server.util.{ServerBindings, WsServerConfig}
import upickle.{default => up}
import scala.concurrent.Future
@ -17,7 +29,8 @@ case class Server(
conf: AppConfig,
handlers: Seq[ServerRoute],
rpcbindOpt: Option[String],
rpcport: Int)(implicit system: ActorSystem)
rpcport: Int,
wsConfigOpt: Option[WsServerConfig])(implicit system: ActorSystem)
extends HttpLogger {
import system.dispatcher
@ -78,7 +91,7 @@ case class Server(
}
}
def start(): Future[Http.ServerBinding] = {
def start(): Future[ServerBindings] = {
val httpFut =
Http()
.newServerAt(rpcbindOpt.getOrElse("localhost"), rpcport)
@ -86,8 +99,61 @@ case class Server(
httpFut.foreach { http =>
logger.info(s"Started Bitcoin-S HTTP server at ${http.localAddress}")
}
httpFut
val wsFut = startWsServer()
for {
http <- httpFut
ws <- wsFut
} yield ServerBindings(http, ws)
}
private def startWsServer(): Future[Option[Http.ServerBinding]] = {
wsConfigOpt match {
case Some(wsConfig) =>
val httpFut =
Http()
.newServerAt(wsConfig.wsBind, wsConfig.wsPort)
.bindFlow(wsRoutes)
httpFut.foreach { http =>
logger.info(s"Started Bitcoin-S websocket at ${http.localAddress}")
}
httpFut.map(Some(_))
case None =>
Future.successful(None)
}
}
private val maxBufferSize: Int = 25
/** This will queue [[maxBufferSize]] elements in the queue. Once the buffer size is reached,
* we will drop the first element in the buffer
*/
private val tuple = {
//from: https://github.com/akka/akka-http/issues/3039#issuecomment-610263181
//the BroadcastHub.sink is needed to avoid these errors
// 'Websocket handler failed with Processor actor'
Source
.queue[Message](maxBufferSize, OverflowStrategy.dropHead)
.toMat(BroadcastHub.sink)(Keep.both)
.run()
}
def walletQueue: SourceQueueWithComplete[Message] = tuple._1
def source: Source[Message, NotUsed] = tuple._2
private val eventsRoute = "events"
private def wsRoutes: Route = {
path(eventsRoute) {
Directives.handleWebSocketMessages(wsHandler)
}
}
private def wsHandler: Flow[Message, Message, Any] = {
//we don't allow input, so use Sink.ignore
Flow.fromSinkAndSource(Sink.ignore, source)
}
}
object Server {

View File

@ -0,0 +1,27 @@
package org.bitcoins.server.util
import akka.http.scaladsl.Http
import scala.concurrent.duration.DurationInt
import scala.concurrent.{ExecutionContext, Future}
case class ServerBindings(
httpServer: Http.ServerBinding,
webSocketServerOpt: Option[Http.ServerBinding]) {
private val terminateTimeout = 5.seconds
def stop()(implicit ec: ExecutionContext): Future[Unit] = {
val stopHttp = httpServer.terminate(terminateTimeout)
val stopWsFOpt = webSocketServerOpt.map(_.terminate(terminateTimeout))
for {
_ <- stopHttp
_ <- stopWsFOpt match {
case Some(doneF) =>
doneF
case None =>
Future.unit
}
} yield ()
}
}

View File

@ -0,0 +1,3 @@
package org.bitcoins.server.util
case class WsServerConfig(wsBind: String, wsPort: Int)

View File

@ -23,7 +23,7 @@ import org.bitcoins.core.protocol.BlockStamp.{
import org.bitcoins.core.protocol.blockchain.BlockHeader
import org.bitcoins.core.protocol.dlc.models.DLCMessage._
import org.bitcoins.core.protocol.dlc.models._
import org.bitcoins.core.protocol.script.{EmptyScriptWitness, P2WPKHWitnessV0}
import org.bitcoins.core.protocol.script.P2WPKHWitnessV0
import org.bitcoins.core.protocol.tlv._
import org.bitcoins.core.protocol.transaction._
import org.bitcoins.core.protocol.{
@ -42,6 +42,7 @@ import org.bitcoins.node.Node
import org.bitcoins.server.routes.{CommonRoutes, ServerCommand}
import org.bitcoins.testkit.BitcoinSTestAppConfig
import org.bitcoins.testkit.wallet.DLCWalletUtil
import org.bitcoins.testkitcore.util.TransactionTestUtil
import org.bitcoins.wallet.MockWalletApi
import org.scalamock.scalatest.MockFactory
import org.scalatest.wordspec.AnyWordSpec
@ -250,6 +251,7 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory {
(mockChainApi
.getHeader(_: DoubleSha256DigestBE))
.expects(blockHeader.hashBE)
.twice()
.returning(Future.successful(Some(blockHeaderDb)))
(mockChainApi
@ -264,7 +266,7 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory {
Get() ~> route ~> check {
assert(contentType == `application/json`)
assert(responseAs[
String] == s"""{"result":{"raw":"${blockHeader.hex}","hash":"${blockHeader.hashBE.hex}","confirmations":1,"height":1899697,"version":${blockHeader.version.toLong},"versionHex":"${blockHeader.version.hex}","merkleroot":"${blockHeader.merkleRootHashBE.hex}","time":${blockHeader.time.toLong},"nonce":${blockHeader.nonce.toLong},"bits":"${blockHeader.nBits.hex}","difficulty":${blockHeader.difficulty.toDouble},"chainwork":"$chainworkStr","previousblockhash":"${blockHeader.previousBlockHashBE.hex}"},"error":null}""")
String] == s"""{"result":{"raw":"${blockHeader.hex}","hash":"${blockHeader.hashBE.hex}","confirmations":1,"height":1899697,"version":${blockHeader.version.toLong},"versionHex":"${blockHeader.version.hex}","merkleroot":"${blockHeader.merkleRootHashBE.hex}","time":${blockHeader.time.toLong},"mediantime":${blockHeaderDb.time.toLong},"nonce":${blockHeader.nonce.toLong},"bits":"${blockHeader.nBits.hex}","difficulty":${blockHeader.difficulty.toDouble},"chainwork":"$chainworkStr","previousblockhash":"${blockHeader.previousBlockHashBE.hex}","nextblockhash":null},"error":null}""")
}
}
@ -358,16 +360,7 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory {
}
}
val spendingInfoDb = SegwitV0SpendingInfo(
outPoint = TransactionOutPoint(DoubleSha256DigestBE.empty, UInt32.zero),
output = EmptyTransactionOutput,
privKeyPath =
SegWitHDPath(HDCoinType.Testnet, 0, HDChainType.External, 0),
scriptWitness = EmptyScriptWitness,
txid = DoubleSha256DigestBE.empty,
state = TxoState.PendingConfirmationsSpent,
spendingTxIdOpt = Some(DoubleSha256DigestBE.empty)
)
val spendingInfoDb = TransactionTestUtil.spendingInfoDb
"return the wallet's balances in bitcoin" in {
(mockWalletApi.getConfirmedBalance: () => Future[CurrencyUnit])
@ -390,7 +383,7 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory {
Get() ~> route ~> check {
assert(contentType == `application/json`)
assert(
responseAs[String] == """{"result":{"confirmed":50,"unconfirmed":50,"reserved":-1.0E-8,"total":99.99999999},"error":null}""")
responseAs[String] == """{"result":{"confirmed":50,"unconfirmed":50,"reserved":1,"total":101},"error":null}""")
}
}
@ -415,7 +408,7 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory {
Get() ~> route ~> check {
assert(contentType == `application/json`)
assert(
responseAs[String] == """{"result":{"confirmed":5000000000,"unconfirmed":5000000000,"reserved":-1,"total":9999999999},"error":null}""")
responseAs[String] == """{"result":{"confirmed":5000000000,"unconfirmed":5000000000,"reserved":100000000,"total":10100000000},"error":null}""")
}
}
@ -460,7 +453,7 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory {
Get() ~> route ~> check {
assert(contentType == `application/json`)
assert(
responseAs[String] == """{"result":[{"outpoint":{"txid":"0000000000000000000000000000000000000000000000000000000000000000","vout":0},"value":-1}],"error":null}""")
responseAs[String] == """{"result":[{"outpoint":{"txid":"0000000000000000000000000000000000000000000000000000000000000000","vout":0},"value":100000000}],"error":null}""")
}
}
@ -477,7 +470,7 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory {
Get() ~> route ~> check {
assert(contentType == `application/json`)
assert(
responseAs[String] == """{"result":[{"outpoint":{"txid":"0000000000000000000000000000000000000000000000000000000000000000","vout":0},"value":-1}],"error":null}""")
responseAs[String] == """{"result":[{"outpoint":{"txid":"0000000000000000000000000000000000000000000000000000000000000000","vout":0},"value":100000000}],"error":null}""")
}
}

View File

@ -0,0 +1,245 @@
package org.bitcoins.server
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.ws.{
Message,
TextMessage,
WebSocketRequest,
WebSocketUpgradeResponse
}
import akka.stream.scaladsl.{Flow, Keep, Sink, Source}
import org.bitcoins.cli.{CliCommand, Config, ConsoleCli}
import org.bitcoins.commons.jsonmodels.ws.{WalletNotification, WalletWsType}
import org.bitcoins.commons.jsonmodels.ws.WalletNotification.{
BlockProcessedNotification,
NewAddressNotification,
TxBroadcastNotification,
TxProcessedNotification
}
import org.bitcoins.commons.serializers.{Picklers, WsPicklers}
import org.bitcoins.core.currency.Bitcoins
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.crypto.DoubleSha256DigestBE
import org.bitcoins.testkit.server.{
BitcoinSServerMainBitcoindFixture,
ServerWithBitcoind
}
import org.bitcoins.testkit.util.AkkaUtil
import scala.concurrent.duration.DurationInt
import scala.concurrent.{Future, Promise}
class WebsocketTests extends BitcoinSServerMainBitcoindFixture {
behavior of "Websocket Tests"
val endSink: Sink[WalletNotification[_], Future[Seq[WalletNotification[_]]]] =
Sink.seq[WalletNotification[_]]
val sink: Sink[Message, Future[Seq[WalletNotification[_]]]] = Flow[Message]
.map {
case message: TextMessage.Strict =>
//we should be able to parse the address message
val text = message.text
val notification: WalletNotification[_] =
upickle.default.read[WalletNotification[_]](text)(
WsPicklers.walletNotificationPickler)
notification
case msg =>
fail(s"Unexpected msg type received in the sink, msg=$msg")
}
.toMat(endSink)(Keep.right)
def buildReq(conf: BitcoinSAppConfig): WebSocketRequest = {
WebSocketRequest(s"ws://localhost:${conf.wsPort}/events")
}
val websocketFlow: Flow[
Message,
Message,
(Future[Seq[WalletNotification[_]]], Promise[Option[Message]])] = {
Flow
.fromSinkAndSourceCoupledMat(sink, Source.maybe[Message])(Keep.both)
}
it must "receive updates when an address is generated" in {
serverWithBitcoind =>
val ServerWithBitcoind(_, server) = serverWithBitcoind
val cliConfig = Config(rpcPortOpt = Some(server.conf.rpcPort))
val req = buildReq(server.conf)
val notificationsF: (
Future[WebSocketUpgradeResponse],
(Future[Seq[WalletNotification[_]]], Promise[Option[Message]])) = {
Http()
.singleWebSocketRequest(req, websocketFlow)
}
val walletNotificationsF: Future[Seq[WalletNotification[_]]] =
notificationsF._2._1
val promise: Promise[Option[Message]] = notificationsF._2._2
val expectedAddressStr = ConsoleCli
.exec(CliCommand.GetNewAddress(labelOpt = None), cliConfig)
.get
val expectedAddress = BitcoinAddress.fromString(expectedAddressStr)
for {
_ <- AkkaUtil.nonBlockingSleep(500.millis)
_ = promise.success(None)
notifications <- walletNotificationsF
} yield {
assert(
notifications.exists(_ == NewAddressNotification(expectedAddress)))
}
}
it must "receive updates when a transaction is broadcast" in {
serverWithBitcoind =>
val ServerWithBitcoind(bitcoind, server) = serverWithBitcoind
val cliConfig = Config(rpcPortOpt = Some(server.conf.rpcPort))
val req = buildReq(server.conf)
val tuple: (
Future[WebSocketUpgradeResponse],
(Future[Seq[WalletNotification[_]]], Promise[Option[Message]])) = {
Http()
.singleWebSocketRequest(req, websocketFlow)
}
val notificationsF = tuple._2._1
val promise = tuple._2._2
val addressF = bitcoind.getNewAddress
for {
address <- addressF
cmd = CliCommand.SendToAddress(destination = address,
amount = Bitcoins.one,
satoshisPerVirtualByte =
Some(SatoshisPerVirtualByte.one),
noBroadcast = false)
txIdStr = ConsoleCli.exec(cmd, cliConfig)
expectedTxId = DoubleSha256DigestBE.fromHex(txIdStr.get)
getTxCmd = CliCommand.GetTransaction(expectedTxId)
expectedTxStr = ConsoleCli.exec(getTxCmd, cliConfig)
expectedTx = Transaction.fromHex(expectedTxStr.get)
_ <- AkkaUtil.nonBlockingSleep(500.millis)
_ = promise.success(None)
notifications <- notificationsF
} yield {
assert(notifications.exists(_ == TxBroadcastNotification(expectedTx)))
}
}
it must "receive updates when a transaction is processed" in {
serverWithBitcoind =>
val ServerWithBitcoind(bitcoind, server) = serverWithBitcoind
val cliConfig = Config(rpcPortOpt = Some(server.conf.rpcPort))
val req = buildReq(server.conf)
val tuple: (
Future[WebSocketUpgradeResponse],
(Future[Seq[WalletNotification[_]]], Promise[Option[Message]])) = {
Http()
.singleWebSocketRequest(req, websocketFlow)
}
val notificationsF = tuple._2._1
val promise = tuple._2._2
val addressF = bitcoind.getNewAddress
for {
address <- addressF
cmd = CliCommand.SendToAddress(destination = address,
amount = Bitcoins.one,
satoshisPerVirtualByte =
Some(SatoshisPerVirtualByte.one),
noBroadcast = false)
txIdStr = ConsoleCli.exec(cmd, cliConfig)
expectedTxId = DoubleSha256DigestBE.fromHex(txIdStr.get)
getTxCmd = CliCommand.GetTransaction(expectedTxId)
expectedTxStr = ConsoleCli.exec(getTxCmd, cliConfig)
expectedTx = Transaction.fromHex(expectedTxStr.get)
_ <- AkkaUtil.nonBlockingSleep(500.millis)
_ = promise.success(None)
notifications <- notificationsF
} yield {
assert(notifications.exists(_ == TxProcessedNotification(expectedTx)))
}
}
it must "receive updates when a block is processed" in { serverWithBitcoind =>
val ServerWithBitcoind(bitcoind, server) = serverWithBitcoind
val cliConfig = Config(rpcPortOpt = Some(server.conf.rpcPort))
val req = buildReq(server.conf)
val tuple: (
Future[WebSocketUpgradeResponse],
(Future[Seq[WalletNotification[_]]], Promise[Option[Message]])) = {
Http()
.singleWebSocketRequest(req, websocketFlow)
}
val notificationsF = tuple._2._1
val promise = tuple._2._2
val addressF = bitcoind.getNewAddress
for {
address <- addressF
hashes <- bitcoind.generateToAddress(1, address)
cmd = CliCommand.GetBlockHeader(hash = hashes.head)
getBlockHeaderResultStr = ConsoleCli.exec(cmd, cliConfig)
getBlockHeaderResult = upickle.default.read(getBlockHeaderResultStr.get)(
Picklers.getBlockHeaderResultPickler)
block <- bitcoind.getBlockRaw(hashes.head)
wallet <- server.walletConf.createHDWallet(bitcoind, bitcoind, bitcoind)
_ <- wallet.processBlock(block)
_ <- AkkaUtil.nonBlockingSleep(500.millis)
_ = promise.success(None)
notifications <- notificationsF
} yield {
assert(
notifications.exists(
_ == BlockProcessedNotification(getBlockHeaderResult)))
}
}
it must "get notifications for reserving and unreserving utxos" in {
serverWithBitcoind =>
val ServerWithBitcoind(_, server) = serverWithBitcoind
val cliConfig = Config(rpcPortOpt = Some(server.conf.rpcPort))
val req = buildReq(server.conf)
val tuple: (
Future[WebSocketUpgradeResponse],
(Future[Seq[WalletNotification[_]]], Promise[Option[Message]])) = {
Http()
.singleWebSocketRequest(req, websocketFlow)
}
val notificationsF: Future[Seq[WalletNotification[_]]] = tuple._2._1
val promise = tuple._2._2
//lock all utxos
val lockCmd = CliCommand.LockUnspent(unlock = false, Vector.empty)
ConsoleCli.exec(lockCmd, cliConfig)
//unlock all utxos
val unlockCmd = CliCommand.LockUnspent(unlock = true, Vector.empty)
ConsoleCli.exec(unlockCmd, cliConfig)
for {
_ <- AkkaUtil.nonBlockingSleep(500.millis)
_ = promise.success(None)
notifications <- notificationsF
} yield {
//should have two notifications for locking and then unlocking the utxos
assert(notifications.count(_.`type` == WalletWsType.ReservedUtxos) == 2)
}
}
}

View File

@ -4,7 +4,7 @@ import akka.actor.ActorSystem
import com.typesafe.config.{Config, ConfigFactory}
import grizzled.slf4j.Logging
import org.bitcoins.chain.config.ChainAppConfig
import org.bitcoins.commons.config.AppConfig
import org.bitcoins.commons.config.{AppConfig, ConfigOps}
import org.bitcoins.commons.file.FileUtil
import org.bitcoins.commons.util.ServerArgParser
import org.bitcoins.core.config.NetworkParameters
@ -19,7 +19,9 @@ import org.bitcoins.tor.config.TorAppConfig
import org.bitcoins.wallet.config.WalletAppConfig
import java.nio.file.{Files, Path, Paths}
import java.util.concurrent.TimeUnit
import scala.concurrent.Future
import scala.concurrent.duration.{DurationInt, FiniteDuration}
/** A unified config class for all submodules of Bitcoin-S
* that accepts configuration. Thanks to implicit definitions
@ -108,6 +110,25 @@ case class BitcoinSAppConfig(
def rpcPort: Int = config.getInt("bitcoin-s.server.rpcport")
def wsPort: Int = config.getIntOrElse("bitcoin-s.server.wsport", 19999)
/** How long until we forcefully terminate connections to the server
* when shutting down the server
*/
def terminationDeadline: FiniteDuration = {
val opt = config.getDurationOpt("bitcoin-s.server.termination-deadline")
opt match {
case Some(duration) =>
if (duration.isFinite) {
new FiniteDuration(duration.toNanos, TimeUnit.NANOSECONDS)
} else {
sys.error(
s"Can only have a finite duration for termination deadline, got=$duration")
}
case None => 5.seconds //5 seconds by default
}
}
def rpcBindOpt: Option[String] = {
if (config.hasPath("bitcoin-s.server.rpcbind")) {
Some(config.getString("bitcoin-s.server.rpcbind"))
@ -116,6 +137,14 @@ case class BitcoinSAppConfig(
}
}
def wsBindOpt: Option[String] = {
if (config.hasPath("bitcoin-s.server.wsbind")) {
Some(config.getString("bitcoin-s.server.wsbind"))
} else {
None
}
}
def exists(): Boolean = {
directory.resolve("bitcoin-s.conf").toFile.isFile
}

View File

@ -2,7 +2,6 @@ package org.bitcoins.server
import akka.actor.ActorSystem
import akka.dispatch.Dispatchers
import akka.http.scaladsl.Http
import org.bitcoins.asyncutil.AsyncUtil
import org.bitcoins.chain.blockchain.ChainHandler
import org.bitcoins.chain.config.ChainAppConfig
@ -32,9 +31,14 @@ import org.bitcoins.rpc.BitcoindException.InWarmUp
import org.bitcoins.rpc.client.common.BitcoindRpcClient
import org.bitcoins.rpc.config.{BitcoindRpcAppConfig, ZmqConfig}
import org.bitcoins.server.routes.{BitcoinSServerRunner, CommonRoutes, Server}
import org.bitcoins.server.util.BitcoinSAppScalaDaemon
import org.bitcoins.server.util.{
BitcoinSAppScalaDaemon,
ServerBindings,
WebsocketUtil,
WsServerConfig
}
import org.bitcoins.tor.config.TorAppConfig
import org.bitcoins.wallet.Wallet
import org.bitcoins.wallet._
import org.bitcoins.wallet.config.WalletAppConfig
import scala.concurrent.duration.DurationInt
@ -42,7 +46,7 @@ import scala.concurrent.{ExecutionContext, Future, Promise}
class BitcoinSServerMain(override val serverArgParser: ServerArgParser)(implicit
override val system: ActorSystem,
conf: BitcoinSAppConfig)
val conf: BitcoinSAppConfig)
extends BitcoinSServerRunner {
implicit lazy val walletConf: WalletAppConfig = conf.walletConf
@ -74,7 +78,6 @@ class BitcoinSServerMain(override val serverArgParser: ServerArgParser)(implicit
case NodeType.BitcoindBackend =>
startBitcoindBackend()
}
}
} yield {
logger.info(
@ -86,14 +89,13 @@ class BitcoinSServerMain(override val serverArgParser: ServerArgParser)(implicit
override def stop(): Future[Unit] = {
logger.error(s"Exiting process")
for {
_ <- walletConf.stop()
_ <- nodeConf.stop()
_ <- chainConf.stop()
_ <- torConf.stop()
_ <- conf.stop()
_ <- serverBindingsOpt match {
case Some(bindings) => bindings.stop()
case None => Future.unit
}
_ = logger.info(s"Stopped ${nodeConf.nodeType.shortName} node")
_ <- system.terminate()
} yield {
logger.info(s"Actor system terminated")
()
}
}
@ -130,8 +132,8 @@ class BitcoinSServerMain(override val serverArgParser: ServerArgParser)(implicit
chainApi <- chainApiF
_ = logger.info("Initialized chain api")
wallet <- dlcConf.createDLCWallet(node, chainApi, feeProvider)
callbacks <- createCallbacks(wallet)
_ = nodeConf.addCallbacks(callbacks)
nodeCallbacks <- createCallbacks(wallet)
_ = nodeConf.addCallbacks(nodeCallbacks)
} yield {
logger.info(
s"Done configuring wallet, it took=${System.currentTimeMillis() - start}ms")
@ -172,11 +174,14 @@ class BitcoinSServerMain(override val serverArgParser: ServerArgParser)(implicit
dlcNode <- dlcNodeF
_ <- dlcNode.start()
binding <- startHttpServer(nodeApi = node,
chainApi = chainApi,
wallet = wallet,
dlcNode = dlcNode,
serverCmdLineArgs = serverArgParser)
server <- startHttpServer(nodeApi = node,
chainApi = chainApi,
wallet = wallet,
dlcNode = dlcNode,
serverCmdLineArgs = serverArgParser)
walletCallbacks = WebsocketUtil.buildWalletCallbacks(server.walletQueue,
chainApi)
_ = walletConf.addCallbacks(walletCallbacks)
_ = {
logger.info(
s"Starting ${nodeConf.nodeType.shortName} node sync, it took=${System
@ -253,12 +258,14 @@ class BitcoinSServerMain(override val serverArgParser: ServerArgParser)(implicit
_ = syncWalletWithBitcoindAndStartPolling(bitcoind, wallet)
dlcNode = dlcNodeConf.createDLCNode(wallet)
_ <- dlcNode.start()
_ <- startHttpServer(nodeApi = bitcoind,
chainApi = bitcoind,
wallet = wallet,
dlcNode = dlcNode,
serverCmdLineArgs = serverArgParser)
server <- startHttpServer(nodeApi = bitcoind,
chainApi = bitcoind,
wallet = wallet,
dlcNode = dlcNode,
serverCmdLineArgs = serverArgParser)
walletCallbacks = WebsocketUtil.buildWalletCallbacks(server.walletQueue,
bitcoind)
_ = walletConf.addCallbacks(walletCallbacks)
} yield {
logger.info(s"Done starting Main!")
()
@ -345,6 +352,8 @@ class BitcoinSServerMain(override val serverArgParser: ServerArgParser)(implicit
} yield chainApiWithWork
}
private var serverBindingsOpt: Option[ServerBindings] = None
private def startHttpServer(
nodeApi: NodeApi,
chainApi: ChainApi,
@ -352,7 +361,7 @@ class BitcoinSServerMain(override val serverArgParser: ServerArgParser)(implicit
dlcNode: DLCNode,
serverCmdLineArgs: ServerArgParser)(implicit
system: ActorSystem,
conf: BitcoinSAppConfig): Future[Http.ServerBinding] = {
conf: BitcoinSAppConfig): Future[Server] = {
implicit val nodeConf: NodeAppConfig = conf.nodeConf
implicit val walletConf: WalletAppConfig = conf.walletConf
@ -371,26 +380,46 @@ class BitcoinSServerMain(override val serverArgParser: ServerArgParser)(implicit
dlcRoutes,
commonRoutes)
val bindConfOpt = serverCmdLineArgs.rpcBindOpt match {
val rpcBindConfOpt = serverCmdLineArgs.rpcBindOpt match {
case Some(rpcbind) => Some(rpcbind)
case None => conf.rpcBindOpt
}
val wsBindConfOpt = serverCmdLineArgs.wsBindOpt match {
case Some(wsbind) => Some(wsbind)
case None => conf.wsBindOpt
}
val wsPort = serverCmdLineArgs.wsPortOpt match {
case Some(wsPort) => wsPort
case None => conf.wsPort
}
val wsServerConfig =
WsServerConfig(wsBindConfOpt.getOrElse("localhost"), wsPort = wsPort)
val server = {
serverCmdLineArgs.rpcPortOpt match {
case Some(rpcport) =>
Server(conf = nodeConf,
handlers = handlers,
rpcbindOpt = bindConfOpt,
rpcport = rpcport)
rpcbindOpt = rpcBindConfOpt,
rpcport = rpcport,
wsConfigOpt = Some(wsServerConfig))
case None =>
Server(conf = nodeConf,
handlers = handlers,
rpcbindOpt = bindConfOpt,
rpcport = conf.rpcPort)
rpcbindOpt = rpcBindConfOpt,
rpcport = conf.rpcPort,
wsConfigOpt = Some(wsServerConfig))
}
}
server.start()
val bindingF = server.start()
bindingF.map { bindings =>
serverBindingsOpt = Some(bindings)
server
}
}
/** Gets a Fee Provider from the given wallet app config
@ -487,6 +516,7 @@ class BitcoinSServerMain(override val serverArgParser: ServerArgParser)(implicit
logger.error(s"Error syncing bitcoin-s wallet with bitcoind", err))
f
}
}
object BitcoinSServerMain extends BitcoinSAppScalaDaemon {

View File

@ -4,12 +4,12 @@ import akka.actor.ActorSystem
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._
import org.bitcoins.commons.jsonmodels.BitcoinSServerInfo
import org.bitcoins.commons.serializers.Picklers
import org.bitcoins.commons.serializers.Picklers._
import org.bitcoins.core.api.chain.ChainApi
import org.bitcoins.core.config.BitcoinNetwork
import org.bitcoins.server.routes.{Server, ServerCommand, ServerRoute}
import scodec.bits.ByteVector
import ujson._
import org.bitcoins.server.util.ChainUtil
import scala.concurrent.Future
@ -50,38 +50,14 @@ case class ChainRoutes(chain: ChainApi, network: BitcoinNetwork)(implicit
complete {
chain.getHeader(hash).flatMap {
case None => Future.successful(Server.httpSuccess(ujson.Null))
case Some(header) =>
chain.getNumberOfConfirmations(hash).map {
case None =>
throw new RuntimeException(
s"Got unconfirmed header, ${header.hashBE.hex}")
case Some(confs) =>
val chainworkStr = {
val bytes = ByteVector(header.chainWork.toByteArray)
val padded = if (bytes.length <= 32) {
bytes.padLeft(32)
} else bytes
padded.toHex
}
val json = Obj(
"raw" -> Str(header.blockHeader.hex),
"hash" -> Str(header.hashBE.hex),
"confirmations" -> Num(confs),
"height" -> Num(header.height),
"version" -> Num(header.version.toLong.toDouble),
"versionHex" -> Str(header.version.hex),
"merkleroot" -> Str(header.merkleRootHashBE.hex),
"time" -> Num(header.time.toBigInt.toDouble),
"nonce" -> Num(header.nonce.toBigInt.toDouble),
"bits" -> Str(header.nBits.hex),
"difficulty" -> Num(header.difficulty.toDouble),
"chainwork" -> Str(chainworkStr),
"previousblockhash" -> Str(header.previousBlockHashBE.hex)
)
Server.httpSuccess(json)
case Some(_) =>
val resultF = ChainUtil.getBlockHeaderResult(hash, chain)
for {
result <- resultF
} yield {
val json = upickle.default.writeJs(result)(
Picklers.getBlockHeaderResultPickler)
Server.httpSuccess(json)
}
}
}

View File

@ -0,0 +1,56 @@
package org.bitcoins.server.util
import org.bitcoins.commons.jsonmodels.bitcoind.GetBlockHeaderResult
import org.bitcoins.core.api.chain.ChainApi
import org.bitcoins.core.api.chain.db.BlockHeaderDb
import org.bitcoins.crypto.DoubleSha256DigestBE
import scodec.bits.ByteVector
import scala.concurrent.{ExecutionContext, Future}
object ChainUtil {
def getBlockHeaderResult(hash: DoubleSha256DigestBE, chain: ChainApi)(implicit
ec: ExecutionContext): Future[GetBlockHeaderResult] = {
val headerOptF = chain.getHeader(hash)
val confsOptF = chain.getNumberOfConfirmations(hash)
for {
headerOpt <- headerOptF
confsOpt <- confsOptF
} yield {
val zipped: Option[(BlockHeaderDb, Int)] =
headerOpt.zip(confsOpt).headOption
zipped match {
case None =>
sys.error(
s"Could not find block header hash=$hash or confirmations for the header ")
case Some((header, confs)) =>
val chainworkStr = {
val bytes = ByteVector(header.chainWork.toByteArray)
val padded = if (bytes.length <= 32) {
bytes.padLeft(32)
} else bytes
padded.toHex
}
val result = GetBlockHeaderResult(
hash = header.hashBE,
confirmations = confs,
height = header.height,
version = header.version.toInt,
versionHex = header.version,
merkleroot = header.merkleRootHashBE,
time = header.time,
mediantime = header.time, // can't get this?
nonce = header.nonce,
bits = header.nBits,
difficulty = BigDecimal(header.difficulty),
chainwork = chainworkStr,
previousblockhash = Some(header.previousBlockHashBE),
nextblockhash = None
)
result
}
}
}
}

View File

@ -0,0 +1,106 @@
package org.bitcoins.server.util
import akka.http.scaladsl.model.ws.{Message, TextMessage}
import akka.stream.scaladsl.SourceQueueWithComplete
import org.bitcoins.commons.jsonmodels.ws.{WalletNotification, WalletWsType}
import org.bitcoins.commons.serializers.WsPicklers
import org.bitcoins.core.api.chain.ChainApi
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.wallet.{
OnBlockProcessed,
OnNewAddressGenerated,
OnReservedUtxos,
OnTransactionBroadcast,
OnTransactionProcessed,
WalletCallbacks
}
import scala.concurrent.{ExecutionContext, Future}
object WebsocketUtil {
/** Builds websocket callbacks for the wallet */
def buildWalletCallbacks(
walletQueue: SourceQueueWithComplete[Message],
chainApi: ChainApi)(implicit ec: ExecutionContext): WalletCallbacks = {
val onAddressCreated: OnNewAddressGenerated = { addr =>
val notification = WalletNotification.NewAddressNotification(addr)
val json =
upickle.default.writeJs(notification)(WsPicklers.newAddressPickler)
val msg = TextMessage.Strict(json.toString())
val offerF = walletQueue.offer(msg)
offerF.map(_ => ())
}
val onTxProcessed: OnTransactionProcessed = { tx =>
buildTxNotification(wsType = WalletWsType.TxProcessed,
tx = tx,
walletQueue = walletQueue)
}
val onTxBroadcast: OnTransactionBroadcast = { tx =>
buildTxNotification(wsType = WalletWsType.TxBroadcast,
tx = tx,
walletQueue = walletQueue)
}
val onReservedUtxo: OnReservedUtxos = { utxos =>
val notification =
WalletNotification.ReservedUtxosNotification(utxos)
val notificationJson =
upickle.default.writeJs(notification)(WsPicklers.reservedUtxosPickler)
val msg = TextMessage.Strict(notificationJson.toString())
val offerF = walletQueue.offer(msg)
offerF.map(_ => ())
}
val onBlockProcessed: OnBlockProcessed = { block =>
val resultF =
ChainUtil.getBlockHeaderResult(block.blockHeader.hashBE, chainApi)
val f = for {
result <- resultF
notification =
WalletNotification.BlockProcessedNotification(result)
notificationJson =
upickle.default.writeJs(notification)(
WsPicklers.blockProcessedPickler)
msg = TextMessage.Strict(notificationJson.toString())
_ <- walletQueue.offer(msg)
} yield {
()
}
f
}
WalletCallbacks(
onTransactionProcessed = Vector(onTxProcessed),
onNewAddressGenerated = Vector(onAddressCreated),
onReservedUtxos = Vector(onReservedUtxo),
onTransactionBroadcast = Vector(onTxBroadcast),
onBlockProcessed = Vector(onBlockProcessed)
)
}
private def buildTxNotification(
wsType: WalletWsType,
tx: Transaction,
walletQueue: SourceQueueWithComplete[Message])(implicit
ec: ExecutionContext): Future[Unit] = {
val json = wsType match {
case WalletWsType.TxProcessed =>
val notification = WalletNotification.TxProcessedNotification(tx)
upickle.default.writeJs(notification)(WsPicklers.txProcessedPickler)
case WalletWsType.TxBroadcast =>
val notification = WalletNotification.TxBroadcastNotification(tx)
upickle.default.writeJs(notification)(WsPicklers.txBroadcastPickler)
case x @ (WalletWsType.NewAddress | WalletWsType.ReservedUtxos |
WalletWsType.BlockProcessed) =>
sys.error(s"Cannot build tx notification for $x")
}
val msg = TextMessage.Strict(json.toString())
val offerF = walletQueue.offer(msg)
offerF.map(_ => ())
}
}

View File

@ -3,7 +3,13 @@ package org.bitcoins.core.api.wallet.db
import org.bitcoins.core.api.db.DbRowAutoInc
import org.bitcoins.core.api.keymanager.BIP39KeyManagerApi
import org.bitcoins.core.hd._
import org.bitcoins.core.protocol.script.{ScriptPubKey, ScriptWitness}
import org.bitcoins.core.protocol.script.{
P2SHScriptPubKey,
RawScriptPubKey,
ScriptPubKey,
ScriptWitness,
WitnessScriptPubKey
}
import org.bitcoins.core.protocol.transaction.{
Transaction,
TransactionOutPoint,
@ -34,7 +40,6 @@ case class SegwitV0SpendingInfo(
override val redeemScriptOpt: Option[ScriptPubKey] = None
override val scriptWitnessOpt: Option[ScriptWitness] = Some(scriptWitness)
override type PathType = SegWitHDPath
override type SpendingInfoType = SegwitV0SpendingInfo
override def copyWithState(state: TxoState): SegwitV0SpendingInfo =
@ -64,7 +69,6 @@ case class LegacySpendingInfo(
override def scriptWitnessOpt: Option[ScriptWitness] = None
override type PathType = LegacyHDPath
type SpendingInfoType = LegacySpendingInfo
override def copyWithId(id: Long): LegacySpendingInfo =
@ -96,7 +100,6 @@ case class NestedSegwitV0SpendingInfo(
override val redeemScriptOpt: Option[ScriptPubKey] = Some(redeemScript)
override val scriptWitnessOpt: Option[ScriptWitness] = Some(scriptWitness)
override type PathType = NestedSegWitHDPath
override type SpendingInfoType = NestedSegwitV0SpendingInfo
override def copyWithState(state: TxoState): NestedSegwitV0SpendingInfo =
@ -126,8 +129,6 @@ sealed trait SpendingInfoDb extends DbRowAutoInc[SpendingInfoDb] {
s"If we have spent a spendinginfodb, the spendingTxId must be defined. Outpoint=${outPoint.toString}")
}
protected type PathType <: HDPath
/** This type is here to ensure copyWithSpent returns the same
* type as the one it was called on.
*/
@ -136,7 +137,7 @@ sealed trait SpendingInfoDb extends DbRowAutoInc[SpendingInfoDb] {
def id: Option[Long]
def outPoint: TransactionOutPoint
def output: TransactionOutput
def privKeyPath: PathType
def privKeyPath: HDPath
def redeemScriptOpt: Option[ScriptPubKey]
def scriptWitnessOpt: Option[ScriptWitness]
@ -192,5 +193,78 @@ sealed trait SpendingInfoDb extends DbRowAutoInc[SpendingInfoDb] {
hashType
)
}
}
object SpendingInfoDb {
def apply(
id: Option[Long],
outpoint: TransactionOutPoint,
output: TransactionOutput,
hdPath: HDPath,
redeemScriptOpt: Option[ScriptPubKey],
scriptWitnessOpt: Option[ScriptWitness],
state: TxoState,
txId: DoubleSha256DigestBE,
spendingTxIdOpt: Option[DoubleSha256DigestBE]): SpendingInfoDb = {
require(
txId == outpoint.txIdBE,
s"Outpoint and crediting txid not the same, got=$txId expected=${outpoint.txIdBE}")
output.scriptPubKey match {
case _: P2SHScriptPubKey =>
if (scriptWitnessOpt.isDefined) {
require(
hdPath.isInstanceOf[NestedSegWitHDPath],
s"hdPath must be SegwitHdPath for SegwitV0SpendingInfo, got=$hdPath")
NestedSegwitV0SpendingInfo(outpoint,
output,
hdPath.asInstanceOf[NestedSegWitHDPath],
redeemScriptOpt.get,
scriptWitnessOpt.get,
txId,
state,
spendingTxIdOpt,
id)
} else {
require(
hdPath.isInstanceOf[LegacyHDPath],
s"hdPath must be LegacyHDPath for LegacySpendingInfo, got=$hdPath")
LegacySpendingInfo(outPoint = outpoint,
output = output,
privKeyPath = hdPath.asInstanceOf[LegacyHDPath],
state = state,
txid = txId,
spendingTxIdOpt = spendingTxIdOpt,
id = id)
}
case _: WitnessScriptPubKey =>
require(
hdPath.isInstanceOf[SegWitHDPath],
s"hdPath must be SegwitHdPath for SegwitV0SpendingInfo, got=$hdPath")
require(scriptWitnessOpt.isDefined,
s"ScriptWitness must be defined for SegwitV0SpendingInfo")
SegwitV0SpendingInfo(
outPoint = outpoint,
output = output,
privKeyPath = hdPath.asInstanceOf[SegWitHDPath],
scriptWitness = scriptWitnessOpt.get,
txid = txId,
state = state,
spendingTxIdOpt = spendingTxIdOpt,
id = id
)
case _: RawScriptPubKey =>
require(
hdPath.isInstanceOf[LegacyHDPath],
s"hdPath must be LegacyHDPath for LegacySpendingInfo, got=$hdPath")
LegacySpendingInfo(outPoint = outpoint,
output = output,
privKeyPath = hdPath.asInstanceOf[LegacyHDPath],
state = state,
txid = txId,
spendingTxIdOpt = spendingTxIdOpt,
id = id)
}
}
}

View File

@ -1,6 +1,33 @@
package org.bitcoins.core.serializers
object PicklerKeys {
final val idKey: String = "id"
final val outPointKey: String = "outpoint"
final val outputKey: String = "output"
final val satoshisKey: String = "satoshis"
final val scriptPubKeyKey: String = "scriptPubKey"
final val hdPathKey: String = "hdPath"
final val txIdKey: String = "txid"
final val stateKey: String = "state"
final val spendingTxIdKey: String = "spendingTxId"
//chain
final val rawKey: String = "raw"
final val hashKey: String = "hash"
final val confirmationsKey: String = "confirmations"
final val heightKey: String = "height"
final val versionKey: String = "version"
final val versionHexKey: String = "versionHex"
final val merklerootKey: String = "merkleroot"
final val timeKey: String = "time"
final val mediantimeKey: String = "mediantime"
final val nonceKey: String = "nonce"
final val bitsKey: String = "bits"
final val difficultyKey: String = "difficulty"
final val chainworkKey: String = "chainwork"
final val previousblockhashKey: String = "previousblockhash"
final val nextblockhashKey: String = "nextblockhash"
final val myCollateral: String = "myCollateral"
final val theirCollateral: String = "theirCollateral"
final val myPayout: String = "myPayout"
@ -113,4 +140,9 @@ object PicklerKeys {
val witnessElementsKey = "witnessElements"
val witnessKey = "witness"
val serializedKey = "serialized"
//ws types
final val typeKey: String = "type"
final val payloadKey: String = "payload"
}

View File

@ -152,6 +152,10 @@ bitcoin-s {
# The port we bind our rpc server on
rpcport = 9999
rpcbind = "127.0.0.1"
# The port we bind our websocket server on
wsport = 19999
wsbind = "127.0.0.1"
}
dlc = ${bitcoin-s.dbDefault}

View File

@ -336,3 +336,31 @@ CURL:
$ curl --data-binary '{"jsonrpc": "1.0", "id": "curltest", "method": "signpsbt", "params": ["cHNidP8BAP0FAQIAAAABWUWxYiPKgdGfXcIxJ6MRDxEpUecw59Gk4NpROI5oukoBAAAAAAAAAAAEPttkvdwAAAAXqRSOVAp6Qe/u2hq74e/ThB8foBKn7IfZYMgGCAAAAADbmaQ2nwAAAEdRIQLpfVqyaL9Jb/IkveatNyVeONE8Q/6TzXAWosxLo9e21SECc5G3XiK7xKLlkBG7prMx7p0fMeQwMH5e9H10mBon39JSrtgtgjjLAQAAUGMhAn2YaZnv25I6d6vbb1kw6Xp5IToDrEzl/0VBIW21gHrTZwXg5jGdALJ1IQKyNpDNiOiN6lWpYethib04+XC9bpFXrdpec+xO3U5IM2is9ckf5AABAD0CAAAAAALuiOL0rRcAABYAFPnpLByQq1Gg3vwiP6qR8FmOOjwxvVllM08DAAALBfXJH+QAsXUAAK4AAAAAAQcBAAAAAAAA"]}' -H "Content-Type: application/json" http://127.0.0.1:9999/
{"result":"cHNidP8BAP0FAQIAAAABWUWxYiPKgdGfXcIxJ6MRDxEpUecw59Gk4NpROI5oukoBAAAAAAAAAAAEPttkvdwAAAAXqRSOVAp6Qe/u2hq74e/ThB8foBKn7IfZYMgGCAAAAADbmaQ2nwAAAEdRIQLpfVqyaL9Jb/IkveatNyVeONE8Q/6TzXAWosxLo9e21SECc5G3XiK7xKLlkBG7prMx7p0fMeQwMH5e9H10mBon39JSrtgtgjjLAQAAUGMhAn2YaZnv25I6d6vbb1kw6Xp5IToDrEzl/0VBIW21gHrTZwXg5jGdALJ1IQKyNpDNiOiN6lWpYethib04+XC9bpFXrdpec+xO3U5IM2is9ckf5AABAD0CAAAAAALuiOL0rRcAABYAFPnpLByQq1Gg3vwiP6qR8FmOOjwxvVllM08DAAALBfXJH+QAsXUAAK4AAAAAAQcBAAAAAAAA","error":null}
```
## Websocket endpoints
Bitcoin-s offers websocket endpoints. By default, the endpoint is `ws://localhost:1999/events`
You can configure where how the endpoints are configured inside your `bitcoin-s.conf`
```
bitcoin-s.server.wsbind=localhost
bitcoin-s.server.wsport=19999
```
These events are implemented using our [internal callback mechanism](../wallet/wallet-callbacks.md#wallet-callbacks).
An example event that is defined is our `blockprocess` event.
Everytime our wallet processes a block, our wallet will notify you via websockets.
Here is an example payload
The current types of events defined are
1. `txprocessed` - when the wallet processes a transaction. Every transaction in a block will get relayed currently
2. `reservedutxos` - when the wallet reserves OR unreserves utxos
3. `newaddress` - when the wallet generates a new address
4. `txbroadcast` - when the wallet broadcasts a tx
5. `blockprocessed` - when the wallet processes a block
```
{"type":"blockprocessed","payload":{"raw":"04e0ff2ffce8c3e866e367d305886a3f9d353e557524f61f9cf0c26c46000000000000001206d2e396387bff1c13cbe572d4646abae1ae405f4066ab5e6f5edd6d8f028008a8bb61ffff001af23dd47e","hash":"00000000000000de21f23f6945f028d5ecb47863428f6e9e035ab2fb7a3ef356","confirmations":1,"height":2131641,"version":805298180,"versionHex":"2fffe004","merkleroot":"80028f6ddd5e6f5eab66405f40aee1ba6a64d472e5cb131cff7b3896e3d20612","time":1639688200,"mediantime":1639688200,"nonce":2127838706,"bits":"1a00ffff","difficulty":1.6069135243303364E60,"chainwork":"00000000000000000000000000000000000000000000062438437ddd009e698b","previousblockhash":"00000000000000466cc2f09c1ff62475553e359d3f6a8805d367e366e8c3e8fc","nextblockhash":null}}
```

View File

@ -319,6 +319,12 @@ bitcoin-s {
# The ip address we bind our server too
rpcbind = "127.0.0.1"
# The port we bind our websocket server on
wsport = 19999
# The ip address we bind the websocket server too
wsbind = "127.0.0.1"
}
oracle {

View File

@ -1,13 +1,21 @@
package org.bitcoins.testkitcore.util
import org.bitcoins.core.api.wallet.db.SegwitV0SpendingInfo
import org.bitcoins.core.crypto.ECPrivateKeyUtil
import org.bitcoins.core.currency.{CurrencyUnit, CurrencyUnits}
import org.bitcoins.core.currency.{Bitcoins, CurrencyUnit, CurrencyUnits}
import org.bitcoins.core.hd.{HDChainType, HDCoinType, SegWitHDPath}
import org.bitcoins.core.number.{Int32, UInt32}
import org.bitcoins.core.protocol.Bech32mAddress
import org.bitcoins.core.protocol.script._
import org.bitcoins.core.protocol.transaction._
import org.bitcoins.core.psbt.PSBT
import org.bitcoins.crypto.{DoubleSha256Digest, ECPublicKeyBytes}
import org.bitcoins.core.wallet.utxo.TxoState
import org.bitcoins.crypto.{
DoubleSha256Digest,
DoubleSha256DigestBE,
ECPublicKey,
ECPublicKeyBytes
}
/** Created by chris on 2/12/16.
*/
@ -264,6 +272,20 @@ trait TransactionTestUtil {
spk: ScriptPubKey = EmptyScriptPubKey): PSBT = {
PSBT.fromUnsignedTx(dummyTx(prevTxId, scriptSig, spk))
}
val segwitV0 = P2WPKHWitnessSPKV0(ECPublicKey.freshPublicKey)
val output = TransactionOutput(Bitcoins.one, segwitV0)
val spendingInfoDb = SegwitV0SpendingInfo(
outPoint = TransactionOutPoint(DoubleSha256DigestBE.empty, UInt32.zero),
output = output,
privKeyPath = SegWitHDPath(HDCoinType.Testnet, 0, HDChainType.External, 0),
scriptWitness = EmptyScriptWitness,
txid = DoubleSha256DigestBE.empty,
state = TxoState.PendingConfirmationsSpent,
spendingTxIdOpt = Some(DoubleSha256DigestBE.empty)
)
}
object TransactionTestUtil extends TransactionTestUtil

View File

@ -1,16 +1,9 @@
package org.bitcoins.testkit.fixtures
import com.typesafe.config.{Config, ConfigFactory}
import org.bitcoins.rpc.config.{
BitcoindInstance,
BitcoindInstanceLocal,
BitcoindInstanceRemote
}
import org.bitcoins.rpc.util.RpcUtil
import org.bitcoins.server.BitcoinSAppConfig
import org.bitcoins.testkit.EmbeddedPg
import org.bitcoins.testkit.rpc.CachedBitcoindNewest
import org.bitcoins.testkit.util.TorUtil
import org.bitcoins.testkit.{BitcoinSTestAppConfig, EmbeddedPg}
import org.bitcoins.testkit.server.BitcoinSServerMainUtil
import org.scalatest.FutureOutcome
import scala.concurrent.Future
@ -39,54 +32,18 @@ trait BitcoinSAppConfigBitcoinFixtureNotStarted
val builder: () => Future[BitcoinSAppConfig] = () => {
for {
bitcoind <- cachedBitcoindWithFundsF
datadir = bitcoind.instance match {
case local: BitcoindInstanceLocal => local.datadir
case _: BitcoindInstanceRemote =>
sys.error("Remote instance should not be used in tests")
}
conf = buildConfig(bitcoind.instance)
bitcoinSAppConfig = BitcoinSAppConfig(datadir.toPath, conf)
bitcoinSAppConfig = BitcoinSServerMainUtil
.buildBitcoindBitcoinSAppConfig(bitcoind)
} yield bitcoinSAppConfig
}
val destroyF: BitcoinSAppConfig => Future[Unit] = { appConfig =>
val stopF = appConfig
.stop()
.map(_ => BitcoinSTestAppConfig.deleteAppConfig(appConfig))
.map(_ => ())
stopF
BitcoinSServerMainUtil.destroyBitcoinSAppConfig(appConfig)
}
makeDependentFixture(builder, destroyF)(test)
}
/** Builds a configuration with the proper bitcoind credentials and bitcoin-s node mode set to bitcoind
* and sets tor config
*/
private def buildConfig(instance: BitcoindInstance): Config = {
val version = instance match {
case local: BitcoindInstanceLocal => local.getVersion
case _: BitcoindInstanceRemote =>
sys.error("Remote instance should not be used in tests")
}
val configStr =
s"""
|bitcoin-s.bitcoind-rpc.rpcuser="${instance.authCredentials.username}"
|bitcoin-s.bitcoind-rpc.rpcpassword="${instance.authCredentials.password}"
|bitcoin-s.bitcoind-rpc.rpcbind="${instance.rpcUri.getHost}"
|bitcoin-s.bitcoind-rpc.rpcport="${instance.rpcUri.getPort}"
|bitcoin-s.bitcoind-rpc.remote=true
|bitcoin-s.bitcoind-rpc.version="${version}"
|bitcoin-s.node.mode=bitcoind
|bitcoin-s.tor.enabled=${TorUtil.torEnabled}
|bitcoin-s.proxy.enabled=${TorUtil.torEnabled}
|bitcoin-s.dlcnode.listen = "0.0.0.0:${RpcUtil.randomPort}"
|bitcoin-s.server.rpcport = ${RpcUtil.randomPort}
|""".stripMargin
ConfigFactory.parseString(configStr)
}
override def afterAll(): Unit = {
super[CachedBitcoindNewest].afterAll()
super[BitcoinSAppConfigFixture].afterAll()

View File

@ -0,0 +1,69 @@
package org.bitcoins.testkit.server
import org.bitcoins.commons.util.ServerArgParser
import org.bitcoins.server.BitcoinSServerMain
import org.bitcoins.testkit.EmbeddedPg
import org.bitcoins.testkit.fixtures.BitcoinSFixture
import org.bitcoins.testkit.rpc.CachedBitcoindNewest
import org.bitcoins.testkit.wallet.{
FundWalletUtil,
WalletTestUtil,
WalletWithBitcoindRpc
}
import org.scalatest.FutureOutcome
import scala.concurrent.Future
/** Starts an instnace of [[BitcoinSserverMain]] that is
* using bitcoind as a backend
*/
trait BitcoinSServerMainBitcoindFixture
extends BitcoinSFixture
with EmbeddedPg
with CachedBitcoindNewest {
override type FixtureParam = ServerWithBitcoind
override def withFixture(test: OneArgAsyncTest): FutureOutcome = {
val builder: () => Future[ServerWithBitcoind] = () => {
for {
bitcoind <- cachedBitcoindWithFundsF
config = BitcoinSServerMainUtil.buildBitcoindBitcoinSAppConfig(bitcoind)
server = new BitcoinSServerMain(ServerArgParser.empty)(system, config)
_ <- server.start()
//need to create account 2 to use FundWalletUtil.fundWalletWithBitcoind
wallet <- server.walletConf.createHDWallet(bitcoind, bitcoind, bitcoind)
_ <- wallet.start()
account1 = WalletTestUtil.getHdAccount1(wallet.walletConfig)
//needed for fundWalletWithBitcoind
_ <- wallet.createNewAccount(hdAccount = account1,
kmParams = wallet.keyManager.kmParams)
_ <- FundWalletUtil.fundWalletWithBitcoind(
WalletWithBitcoindRpc(wallet, bitcoind))
} yield {
ServerWithBitcoind(bitcoind, server)
}
}
val destroy: ServerWithBitcoind => Future[Unit] = { serverWithBitcoind =>
val stopF = serverWithBitcoind.server.stop()
for {
_ <- stopF
_ <- BitcoinSServerMainUtil
.destroyBitcoinSAppConfig(serverWithBitcoind.server.conf)
} yield {
()
}
}
makeDependentFixture(builder, destroy)(test)
}
override def afterAll(): Unit = {
super[CachedBitcoindNewest].afterAll()
super[EmbeddedPg].afterAll()
super[BitcoinSFixture].afterAll()
}
}

View File

@ -0,0 +1,64 @@
package org.bitcoins.testkit.server
import akka.actor.ActorSystem
import com.typesafe.config.{Config, ConfigFactory}
import org.bitcoins.rpc.client.common.BitcoindRpcClient
import org.bitcoins.rpc.config.{
BitcoindInstance,
BitcoindInstanceLocal,
BitcoindInstanceRemote
}
import org.bitcoins.rpc.util.RpcUtil
import org.bitcoins.server.BitcoinSAppConfig
import org.bitcoins.testkit.BitcoinSTestAppConfig
import org.bitcoins.testkit.util.{FileUtil, TorUtil}
import scala.concurrent.{ExecutionContext, Future}
object BitcoinSServerMainUtil {
/** Builds a configuration with the proper bitcoind credentials and bitcoin-s node mode set to bitcoind
* and sets tor config
*/
def buildBitcoindConfig(instance: BitcoindInstance): Config = {
val version = instance match {
case local: BitcoindInstanceLocal => local.getVersion
case _: BitcoindInstanceRemote =>
sys.error("Remote instance should not be used in tests")
}
val configStr =
s"""
|bitcoin-s.bitcoind-rpc.rpcuser="${instance.authCredentials.username}"
|bitcoin-s.bitcoind-rpc.rpcpassword="${instance.authCredentials.password}"
|bitcoin-s.bitcoind-rpc.rpcbind="${instance.rpcUri.getHost}"
|bitcoin-s.bitcoind-rpc.rpcport="${instance.rpcUri.getPort}"
|bitcoin-s.bitcoind-rpc.remote=true
|bitcoin-s.bitcoind-rpc.version="${version}"
|bitcoin-s.node.mode=bitcoind
|bitcoin-s.tor.enabled=${TorUtil.torEnabled}
|bitcoin-s.proxy.enabled=${TorUtil.torEnabled}
|bitcoin-s.dlcnode.listen = "0.0.0.0:${RpcUtil.randomPort}"
|bitcoin-s.server.rpcport = ${RpcUtil.randomPort}
|bitcoin-s.server.wsport= ${RpcUtil.randomPort}
|""".stripMargin
ConfigFactory.parseString(configStr)
}
/** Builds a [[BitcoinSAppConfig]] that uses a bitcoind backend */
def buildBitcoindBitcoinSAppConfig(bitcoind: BitcoindRpcClient)(implicit
system: ActorSystem): BitcoinSAppConfig = {
val conf = BitcoinSServerMainUtil.buildBitcoindConfig(bitcoind.instance)
val datadir = FileUtil.tmpDir()
BitcoinSAppConfig(datadir.toPath, conf)
}
def destroyBitcoinSAppConfig(appConfig: BitcoinSAppConfig)(implicit
ec: ExecutionContext): Future[Unit] = {
val stopF = appConfig
.stop()
.map(_ => BitcoinSTestAppConfig.deleteAppConfig(appConfig))
.map(_ => ())
stopF
}
}

View File

@ -0,0 +1,8 @@
package org.bitcoins.testkit.server
import org.bitcoins.rpc.client.common.BitcoindRpcClient
import org.bitcoins.server.BitcoinSServerMain
case class ServerWithBitcoind(
bitcoind: BitcoindRpcClient,
server: BitcoinSServerMain)

View File

@ -214,7 +214,7 @@ trait BitcoinSWalletTest
.createBitcoindWithFunds(Some(BitcoindVersion.V19))
.map(_.asInstanceOf[BitcoindV19RpcClient])
wallet <- createWalletWithBitcoindCallbacks(bitcoind, bip39PasswordOpt)
fundedWallet <- fundWalletWithBitcoind(wallet)
fundedWallet <- FundWalletUtil.fundWalletWithBitcoind(wallet)
_ <- SyncUtil.syncWalletFullBlocks(wallet = fundedWallet.wallet,
bitcoind = bitcoind)
_ <- BitcoinSWalletTest.awaitWalletBalances(fundedWallet)
@ -620,7 +620,7 @@ object BitcoinSWalletTest extends WalletLogger {
chainQueryApi,
bip39PasswordOpt)(config.walletConf, system)
withBitcoind <- createWalletWithBitcoind(wallet, versionOpt)
funded <- fundWalletWithBitcoind(withBitcoind)
funded <- FundWalletUtil.fundWalletWithBitcoind(withBitcoind)
} yield funded
}
@ -651,38 +651,10 @@ object BitcoinSWalletTest extends WalletLogger {
BitcoinSWalletTest.createNeutrinoNodeCallbacksForWallet(wallet)
_ = config.nodeConf.addCallbacks(nodeCallbacks)
withBitcoind <- createWalletWithBitcoind(wallet, bitcoindRpcClient)
funded <- fundWalletWithBitcoind(withBitcoind)
funded <- FundWalletUtil.fundWalletWithBitcoind(withBitcoind)
} yield funded
}
/** Funds the given wallet with money from the given bitcoind */
def fundWalletWithBitcoind[T <: WalletWithBitcoind](pair: T)(implicit
ec: ExecutionContext): Future[T] = {
val (wallet, bitcoind) = (pair.wallet, pair.bitcoind)
val defaultAccount = wallet.walletConfig.defaultAccount
val fundedDefaultAccountWalletF =
FundWalletUtil.fundAccountForWalletWithBitcoind(
amts = defaultAcctAmts,
account = defaultAccount,
wallet = wallet,
bitcoind = bitcoind
)
val hdAccount1 = WalletTestUtil.getHdAccount1(wallet.walletConfig)
val fundedAccount1WalletF = for {
fundedDefaultAcct <- fundedDefaultAccountWalletF
fundedAcct1 <- FundWalletUtil.fundAccountForWalletWithBitcoind(
amts = account1Amt,
account = hdAccount1,
wallet = fundedDefaultAcct,
bitcoind = bitcoind
)
} yield fundedAcct1
fundedAccount1WalletF.map(_ => pair)
}
def destroyWalletWithBitcoind(walletWithBitcoind: WalletWithBitcoind)(implicit
ec: ExecutionContext): Future[Unit] = {
val (wallet, bitcoind) =

View File

@ -14,8 +14,7 @@ import org.bitcoins.testkit.rpc.{
import org.bitcoins.testkit.wallet.BitcoinSWalletTest.{
createWalletWithBitcoind,
createWalletWithBitcoindCallbacks,
destroyWallet,
fundWalletWithBitcoind
destroyWallet
}
import org.bitcoins.wallet.config.WalletAppConfig
import org.scalatest.{FutureOutcome, Outcome}
@ -50,7 +49,8 @@ trait BitcoinSWalletTestCachedBitcoind
walletWithBitcoind <- createWalletWithBitcoindCallbacks(
bitcoind = bitcoind,
bip39PasswordOpt = bip39PasswordOpt)
fundedWallet <- fundWalletWithBitcoind(walletWithBitcoind)
fundedWallet <- FundWalletUtil.fundWalletWithBitcoind(
walletWithBitcoind)
_ <- SyncUtil.syncWalletFullBlocks(wallet = fundedWallet.wallet,
bitcoind = bitcoind)
_ <- BitcoinSWalletTest.awaitWalletBalances(fundedWallet)
@ -170,8 +170,8 @@ trait BitcoinSWalletTestCachedBitcoinV19
bip39PasswordOpt = bip39PasswordOpt)
walletWithBitcoindV19 = WalletWithBitcoindV19(walletWithBitcoind.wallet,
bitcoind)
fundedWallet <- fundWalletWithBitcoind[WalletWithBitcoindV19](
walletWithBitcoindV19)
fundedWallet <- FundWalletUtil
.fundWalletWithBitcoind[WalletWithBitcoindV19](walletWithBitcoindV19)
_ <- SyncUtil.syncWalletFullBlocks(wallet = fundedWallet.wallet,
bitcoind = bitcoind)
_ <- BitcoinSWalletTest.awaitWalletBalances(fundedWallet)

View File

@ -12,6 +12,10 @@ import org.bitcoins.core.protocol.transaction.TransactionOutput
import org.bitcoins.dlc.wallet.DLCWallet
import org.bitcoins.rpc.client.common.BitcoindRpcClient
import org.bitcoins.server.{BitcoinSAppConfig, BitcoindRpcBackendUtil}
import org.bitcoins.testkit.wallet.BitcoinSWalletTest.{
account1Amt,
defaultAcctAmts
}
import org.bitcoins.testkit.wallet.FundWalletUtil.{
FundedTestWallet,
FundedWallet
@ -24,6 +28,35 @@ import scala.concurrent.{ExecutionContext, Future}
trait FundWalletUtil extends Logging {
/** Funds the given wallet with money from the given bitcoind */
def fundWalletWithBitcoind[T <: WalletWithBitcoind](pair: T)(implicit
ec: ExecutionContext): Future[T] = {
val (wallet, bitcoind) = (pair.wallet, pair.bitcoind)
val defaultAccount = wallet.walletConfig.defaultAccount
val fundedDefaultAccountWalletF =
FundWalletUtil.fundAccountForWalletWithBitcoind(
amts = defaultAcctAmts,
account = defaultAccount,
wallet = wallet,
bitcoind = bitcoind
)
val hdAccount1 = WalletTestUtil.getHdAccount1(wallet.walletConfig)
val fundedAccount1WalletF = for {
fundedDefaultAcct <- fundedDefaultAccountWalletF
fundedAcct1 <- FundWalletUtil.fundAccountForWalletWithBitcoind(
amts = account1Amt,
account = hdAccount1,
wallet = fundedDefaultAcct,
bitcoind = bitcoind
)
} yield fundedAcct1
fundedAccount1WalletF.map(_ => pair)
}
def fundAccountForWallet(
amts: Vector[CurrencyUnit],
account: HDAccount,

View File

@ -71,8 +71,8 @@ private[bitcoins] trait TransactionProcessing extends WalletLogger {
hash = block.blockHeader.hashBE
height <- chainQueryApi.getBlockHeight(hash)
_ <- stateDescriptorDAO.updateSyncHeight(hash, height.get)
_ <- walletConfig.walletCallbacks.executeOnBlockProcessed(logger, block)
} yield {
walletConfig.walletCallbacks.executeOnBlockProcessed(logger, block)
res
}