mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2024-11-19 09:52:09 +01:00
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:
parent
41b96c4c7e
commit
50bec2abc6
@ -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)
|
||||
}
|
||||
}
|
@ -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",
|
||||
val args = Vector(
|
||||
"--rpcport",
|
||||
"1234",
|
||||
"--rpcbind",
|
||||
"my.cool.site.com",
|
||||
"--datadir",
|
||||
s"${datadirString}",
|
||||
"--force-recalc-chainwork")
|
||||
"--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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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(_))
|
||||
}
|
||||
}
|
||||
|
@ -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]
|
||||
)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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 {
|
||||
|
@ -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 ()
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
package org.bitcoins.server.util
|
||||
|
||||
case class WsServerConfig(wsBind: String, wsPort: Int)
|
@ -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}""")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
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,
|
||||
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 {
|
||||
|
@ -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,37 +50,13 @@ 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)
|
||||
)
|
||||
|
||||
case Some(_) =>
|
||||
val resultF = ChainUtil.getBlockHeaderResult(hash, chain)
|
||||
for {
|
||||
result <- resultF
|
||||
} yield {
|
||||
val json = upickle.default.writeJs(result)(
|
||||
Picklers.getBlockHeaderResultPickler)
|
||||
Server.httpSuccess(json)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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(_ => ())
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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}}
|
||||
```
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
@ -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) =
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user