mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-03-25 09:02:11 +01:00
[WIP] New Eclair RPC client (#535)
* [WIP] New Eclair RPC client * channel, close, connect, getinfo, open, peers * allchannels, allnodes, allupdates * audit, channels, findroutetonode, forceclose, updaterelayfee * Initial version of createinvoice * ShortChannelId: improved error handling and scaladoc * addressed the PR comments * parseinvoice, payinvoice, getsentinfo, getreceivedinfo, sendtonode, sendtoroute * unit tests * addressed the PR comments * ws, usablebalances, channelstats, networkfees, getinvoice, listinvoices * addressed PR comments * change eclair URL * cleanup * addressed comments * fidex compiler warnings * Eclair 0.3.1 * scaladoc * cleanup
This commit is contained in:
parent
57d4ae51fc
commit
854242b462
14 changed files with 1094 additions and 694 deletions
|
@ -40,7 +40,7 @@ install:
|
|||
- if [ $(($RANDOM%2)) == 1 ]; then BITCOIND_PATH=$BITCOIND_V16_PATH; else BITCOIND_PATH=$BITCOIND_V17_PATH; fi;
|
||||
- export PATH=$BITCOIND_PATH:$PATH
|
||||
# # # Eclair
|
||||
- wget https://github.com/ACINQ/eclair/releases/download/v0.2-beta8/eclair-node-0.2-beta8-52821b8.jar
|
||||
- wget https://github.com/ACINQ/eclair/releases/download/v0.3.1/eclair-node-0.3.1-6906ecb.jar
|
||||
- export ECLAIR_PATH=$(pwd)
|
||||
|
||||
before_script:
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
package org.bitcoins.core.protocol.ln
|
||||
|
||||
import org.scalatest.{FlatSpec, MustMatchers}
|
||||
|
||||
class ShortChannelIdTest extends FlatSpec with MustMatchers {
|
||||
|
||||
it must "convert short channel id to and from human readable form" in {
|
||||
// BOLT example
|
||||
ShortChannelId.fromHumanReadableString("539268x845x1") must be (ShortChannelId.fromHex("83a8400034d0001"))
|
||||
ShortChannelId.fromHex("83a8400034d0001").toHumanReadableString must be ("539268x845x1")
|
||||
|
||||
// min value
|
||||
ShortChannelId.fromHumanReadableString("0x0x0") must be (ShortChannelId.fromHex("0"))
|
||||
ShortChannelId.fromHex("0").toHumanReadableString must be ("0x0x0")
|
||||
|
||||
// max value
|
||||
ShortChannelId.fromHumanReadableString("16777215x16777215x65535") must be (ShortChannelId.fromHex("ffffffffffffffff"))
|
||||
ShortChannelId.fromHex("ffffffffffffffff").toHumanReadableString must be ("16777215x16777215x65535")
|
||||
}
|
||||
|
||||
it must "validate short channel id components" in {
|
||||
an [IllegalArgumentException] must be thrownBy ShortChannelId.fromHumanReadableString("16777216x0x0")
|
||||
an [IllegalArgumentException] must be thrownBy ShortChannelId.fromHumanReadableString("-1x0x0")
|
||||
an [IllegalArgumentException] must be thrownBy ShortChannelId.fromHumanReadableString("0x16777216x0")
|
||||
an [IllegalArgumentException] must be thrownBy ShortChannelId.fromHumanReadableString("0x-1x0")
|
||||
an [IllegalArgumentException] must be thrownBy ShortChannelId.fromHumanReadableString("0x0x65536")
|
||||
an [IllegalArgumentException] must be thrownBy ShortChannelId.fromHumanReadableString("0x0x-1")
|
||||
an [NoSuchElementException] must be thrownBy ShortChannelId.fromHumanReadableString("1x1x1x1")
|
||||
ShortChannelId.fromHumanReadableString("cafebabe") must be (ShortChannelId.fromHex("cafebabe"))
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package org.bitcoins.core.protocol.ln
|
||||
|
||||
import org.bitcoins.core.crypto.{ECPrivateKey, Sha256Digest}
|
||||
import org.bitcoins.core.protocol.NetworkElement
|
||||
import org.bitcoins.core.util.{CryptoUtil, Factory}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
/**
|
||||
* Payment preimage for generating LN invoices.
|
||||
*/
|
||||
final case class PaymentPreimage(bytes: ByteVector) extends NetworkElement {
|
||||
require(bytes.size == 32, s"Payment preimage size must be 32 bytes")
|
||||
|
||||
lazy val hash: Sha256Digest = CryptoUtil.sha256(bytes)
|
||||
}
|
||||
|
||||
object PaymentPreimage extends Factory[PaymentPreimage] {
|
||||
|
||||
override def fromBytes(bytes: ByteVector): PaymentPreimage = {
|
||||
new PaymentPreimage(bytes)
|
||||
}
|
||||
|
||||
def random: PaymentPreimage = fromBytes(ECPrivateKey.freshPrivateKey.bytes)
|
||||
|
||||
}
|
|
@ -12,10 +12,28 @@ case class ShortChannelId(u64: UInt64) extends NetworkElement {
|
|||
* Output example:
|
||||
* {{{
|
||||
* > ShortChannelId.fromHex("db0000010000")
|
||||
* ShortChannelId(db0000010000)
|
||||
* 219x1x0
|
||||
* }}}
|
||||
*/
|
||||
override def toString: String = s"ShortChannelId(${hex.drop(4)})"
|
||||
override def toString: String = toHumanReadableString
|
||||
|
||||
/**
|
||||
* Converts the short channel id into the human readable form defined in BOLT.
|
||||
* @see [[https://github.com/lightningnetwork/lightning-rfc/blob/master/07-routing-gossip.md BOLT7]]
|
||||
*
|
||||
* Output example:
|
||||
* {{{
|
||||
* > ShortChannelId.fromHex("db0000010000")
|
||||
* 219x1x0
|
||||
* }}}
|
||||
*/
|
||||
def toHumanReadableString: String = {
|
||||
val blockHeight = (u64 >> 40) & UInt64(0xFFFFFF)
|
||||
val txIndex = (u64 >> 16) & UInt64(0xFFFFFF)
|
||||
val outputIndex = u64 & UInt64(0xFFFF)
|
||||
s"${blockHeight.toInt}x${txIndex.toInt}x${outputIndex.toInt}"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object ShortChannelId extends Factory[ShortChannelId] {
|
||||
|
@ -23,4 +41,20 @@ object ShortChannelId extends Factory[ShortChannelId] {
|
|||
override def fromBytes(byteVector: ByteVector): ShortChannelId = {
|
||||
new ShortChannelId(UInt64.fromBytes(byteVector))
|
||||
}
|
||||
|
||||
def fromHumanReadableString(str: String): ShortChannelId = str.split("x") match {
|
||||
case Array(_blockHeight, _txIndex, _outputIndex) =>
|
||||
val blockHeight = BigInt(_blockHeight)
|
||||
require(blockHeight >= 0 && blockHeight <= 0xffffff, "ShortChannelId: invalid block height")
|
||||
|
||||
val txIndex = _txIndex.toInt
|
||||
require(txIndex >= 0 && txIndex <= 0xffffff, "ShortChannelId:invalid tx index")
|
||||
|
||||
val outputIndex = _outputIndex.toInt
|
||||
require(outputIndex >= 0 && outputIndex <= 0xffff, "ShortChannelId: invalid output index")
|
||||
|
||||
val u64 = UInt64(((blockHeight & 0xffffffL) << 40) | ((txIndex & 0xffffffL) << 16) | (outputIndex & 0xffffL))
|
||||
ShortChannelId(u64)
|
||||
case _: Array[String] => fromHex(str)
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -5,13 +5,10 @@ import org.bitcoins.eclair.rpc.client.EclairRpcClient
|
|||
import org.bitcoins.testkit.eclair.rpc.EclairRpcTestUtil
|
||||
import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil
|
||||
import org.scalatest.{AsyncFlatSpec, BeforeAndAfterAll}
|
||||
import org.slf4j.LoggerFactory
|
||||
import akka.stream.StreamTcpException
|
||||
|
||||
class EclairRpcTestUtilTest extends AsyncFlatSpec with BeforeAndAfterAll {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(getClass)
|
||||
|
||||
implicit private val actorSystem: ActorSystem =
|
||||
ActorSystem("EclairRpcTestUtilTest", BitcoindRpcTestUtil.AKKA_CONFIG)
|
||||
|
||||
|
|
|
@ -1,18 +1,25 @@
|
|||
package org.bitcoins.eclair.rpc.api
|
||||
|
||||
import org.bitcoins.core.crypto.Sha256Digest
|
||||
import org.bitcoins.core.currency.CurrencyUnit
|
||||
import org.bitcoins.core.protocol.ln.{LnInvoice, LnParams, ShortChannelId}
|
||||
import org.bitcoins.core.currency.{CurrencyUnit, Satoshis}
|
||||
import org.bitcoins.core.protocol.Address
|
||||
import org.bitcoins.core.protocol.ln.{LnInvoice, LnParams, PaymentPreimage, ShortChannelId}
|
||||
import org.bitcoins.core.protocol.ln.channel.{ChannelId, FundedChannelId}
|
||||
import org.bitcoins.core.protocol.ln.currency.{LnCurrencyUnit, MilliSatoshis}
|
||||
import org.bitcoins.core.protocol.ln.currency.MilliSatoshis
|
||||
import org.bitcoins.core.protocol.ln.node.NodeId
|
||||
import org.bitcoins.core.protocol.script.ScriptPubKey
|
||||
import org.bitcoins.core.wallet.fee.SatoshisPerByte
|
||||
import org.bitcoins.eclair.rpc.json._
|
||||
import org.bitcoins.eclair.rpc.network.NodeUri
|
||||
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
/**
|
||||
* This trait defines methods to interact with the Eclair lightning node via its API.
|
||||
*
|
||||
* @see [[https://acinq.github.io/eclair/]]
|
||||
*/
|
||||
trait EclairApi {
|
||||
|
||||
def allChannels(): Future[Vector[ChannelDesc]]
|
||||
|
@ -29,7 +36,7 @@ trait EclairApi {
|
|||
* @param from start timestamp
|
||||
* @param to end timestamp
|
||||
*/
|
||||
def audit(from: Long, to: Long): Future[AuditResult]
|
||||
def audit(from: Option[FiniteDuration], to: Option[FiniteDuration]): Future[AuditResult]
|
||||
|
||||
def allUpdates(): Future[Vector[ChannelUpdate]]
|
||||
|
||||
|
@ -46,20 +53,25 @@ trait EclairApi {
|
|||
|
||||
def channel(id: ChannelId): Future[ChannelResult]
|
||||
|
||||
def checkInvoice(invoice: LnInvoice): Future[PaymentRequest]
|
||||
def channelStats(): Future[Vector[ChannelStats]]
|
||||
|
||||
def checkPayment(
|
||||
invoiceOrHash: Either[LnInvoice, Sha256Digest]): Future[Boolean]
|
||||
def connect(nodeURI: NodeUri): Future[Unit]
|
||||
|
||||
def connect(nodeURI: NodeUri): Future[String]
|
||||
def connect(nodeId: NodeId, host: String, port: Int): Future[Unit]
|
||||
|
||||
def close(id: ChannelId, spk: ScriptPubKey): Future[String]
|
||||
def disconnect(nodeId: NodeId): Future[Unit]
|
||||
|
||||
def findRoute(nodeId: NodeId): Future[Vector[NodeId]]
|
||||
def close(id: ChannelId, spk: ScriptPubKey): Future[Unit]
|
||||
|
||||
def findRoute(nodeId: NodeId, amountMsat: MilliSatoshis): Future[Vector[NodeId]]
|
||||
|
||||
def findRoute(invoice: LnInvoice): Future[Vector[NodeId]]
|
||||
|
||||
def forceClose(id: ChannelId): Future[String]
|
||||
def findRoute(invoice: LnInvoice, amountMsat: MilliSatoshis): Future[Vector[NodeId]]
|
||||
|
||||
def forceClose(channelId: ChannelId): Future[Unit]
|
||||
|
||||
def forceClose(shortChannelId: ShortChannelId): Future[Unit]
|
||||
|
||||
def getInfo: Future[GetInfoResult]
|
||||
|
||||
|
@ -94,21 +106,50 @@ trait EclairApi {
|
|||
*/
|
||||
def network: LnParams
|
||||
|
||||
def networkFees(from: Option[FiniteDuration], to: Option[FiniteDuration]): Future[Vector[NetworkFeesResult]]
|
||||
|
||||
def nodeId()(implicit ec: ExecutionContext): Future[NodeId] = {
|
||||
getNodeURI.map(_.nodeId)
|
||||
}
|
||||
|
||||
def receive(
|
||||
amountMsat: LnCurrencyUnit,
|
||||
description: String): Future[LnInvoice]
|
||||
def createInvoice(description: String): Future[LnInvoice]
|
||||
|
||||
def receive(
|
||||
amountMsat: Option[LnCurrencyUnit],
|
||||
description: Option[String],
|
||||
expirySeconds: Option[Long]): Future[LnInvoice]
|
||||
def createInvoice(description: String, amountMsat: MilliSatoshis): Future[LnInvoice]
|
||||
|
||||
def send(paymentRequest: LnInvoice): Future[PaymentResult]
|
||||
def createInvoice(description: String, amountMsat: MilliSatoshis, expireIn: FiniteDuration): Future[LnInvoice]
|
||||
|
||||
def send(invoice: LnInvoice, amount: LnCurrencyUnit): Future[PaymentResult]
|
||||
def createInvoice(description: String, amountMsat: MilliSatoshis, paymentPreimage: PaymentPreimage): Future[LnInvoice]
|
||||
|
||||
def createInvoice(description: String, amountMsat: MilliSatoshis, expireIn: FiniteDuration, paymentPreimage: PaymentPreimage): Future[LnInvoice]
|
||||
|
||||
def createInvoice(description: String, amountMsat: Option[MilliSatoshis], expireIn: Option[FiniteDuration], fallbackAddress: Option[Address], paymentPreimage: Option[PaymentPreimage]): Future[LnInvoice]
|
||||
|
||||
def getInvoice(paymentHash: Sha256Digest): Future[LnInvoice]
|
||||
|
||||
def listInvoices(from: Option[FiniteDuration], to: Option[FiniteDuration]): Future[Vector[LnInvoice]]
|
||||
|
||||
def parseInvoice(invoice: LnInvoice): Future[InvoiceResult]
|
||||
|
||||
def payInvoice(invoice: LnInvoice): Future[PaymentId]
|
||||
|
||||
def payInvoice(invoice: LnInvoice, amount: MilliSatoshis): Future[PaymentId]
|
||||
|
||||
def payInvoice(invoice: LnInvoice, amountMsat: Option[MilliSatoshis], maxAttempts: Option[Int], feeThresholdSat: Option[Satoshis], maxFeePct: Option[Int]): Future[PaymentId]
|
||||
|
||||
def getSentInfo(paymentHash: Sha256Digest): Future[Vector[PaymentResult]]
|
||||
|
||||
def getSentInfo(id: PaymentId): Future[Vector[PaymentResult]]
|
||||
|
||||
def getReceivedInfo(paymentHash: Sha256Digest): Future[ReceivedPaymentResult]
|
||||
|
||||
def getReceivedInfo(invoice: LnInvoice): Future[ReceivedPaymentResult]
|
||||
|
||||
def sendToNode(nodeId: NodeId, amountMsat: MilliSatoshis, paymentHash: Sha256Digest, maxAttempts: Option[Int], feeThresholdSat: Option[Satoshis], maxFeePct: Option[Int]): Future[PaymentId]
|
||||
|
||||
/**
|
||||
* Documented by not implemented in Eclair
|
||||
*/
|
||||
def sendToRoute(route: TraversableOnce[NodeId], amountMsat: MilliSatoshis, paymentHash: Sha256Digest, finalCltvExpiry: Long): Future[PaymentId]
|
||||
|
||||
def usableBalances(): Future[Vector[UsableBalancesResult]]
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package org.bitcoins.eclair.rpc.client
|
||||
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.javadsl.model.headers.HttpCredentials
|
||||
|
@ -10,11 +9,12 @@ import akka.http.scaladsl.model._
|
|||
import akka.stream.ActorMaterializer
|
||||
import akka.util.ByteString
|
||||
import org.bitcoins.core.crypto.Sha256Digest
|
||||
import org.bitcoins.core.currency.CurrencyUnit
|
||||
import org.bitcoins.core.currency.{CurrencyUnit, Satoshis}
|
||||
import org.bitcoins.core.protocol.Address
|
||||
import org.bitcoins.core.protocol.ln.channel.{ChannelId, FundedChannelId}
|
||||
import org.bitcoins.core.protocol.ln.currency.{LnCurrencyUnit, MilliSatoshis}
|
||||
import org.bitcoins.core.protocol.ln.currency.MilliSatoshis
|
||||
import org.bitcoins.core.protocol.ln.node.NodeId
|
||||
import org.bitcoins.core.protocol.ln.{LnInvoice, LnParams, ShortChannelId}
|
||||
import org.bitcoins.core.protocol.ln.{LnInvoice, LnParams, PaymentPreimage, ShortChannelId}
|
||||
import org.bitcoins.core.protocol.script.ScriptPubKey
|
||||
import org.bitcoins.core.util.BitcoinSUtil
|
||||
import org.bitcoins.core.wallet.fee.SatoshisPerByte
|
||||
|
@ -27,7 +27,7 @@ import org.bitcoins.rpc.util.AsyncUtil
|
|||
import org.slf4j.LoggerFactory
|
||||
import play.api.libs.json._
|
||||
|
||||
import scala.concurrent.duration.DurationInt
|
||||
import scala.concurrent.duration.{DurationInt, FiniteDuration}
|
||||
import scala.concurrent.{ExecutionContext, Future, Promise}
|
||||
import scala.sys.process._
|
||||
import scala.util.{Failure, Properties, Success}
|
||||
|
@ -37,8 +37,6 @@ class EclairRpcClient(val instance: EclairInstance)(
|
|||
extends EclairApi {
|
||||
import JsonReaders._
|
||||
|
||||
private val resultKey = "result"
|
||||
private val errorKey = "error"
|
||||
implicit val m = ActorMaterializer.create(system)
|
||||
implicit val ec: ExecutionContext = m.executionContext
|
||||
private val logger = LoggerFactory.getLogger(this.getClass)
|
||||
|
@ -54,102 +52,96 @@ class EclairRpcClient(val instance: EclairInstance)(
|
|||
}
|
||||
|
||||
override def allUpdates(): Future[Vector[ChannelUpdate]] =
|
||||
eclairCall[Vector[ChannelUpdate]]("allupdates", List.empty)
|
||||
eclairCall[Vector[ChannelUpdate]]("allupdates")
|
||||
|
||||
override def allUpdates(nodeId: NodeId): Future[Vector[ChannelUpdate]] =
|
||||
eclairCall[Vector[ChannelUpdate]]("allupdates",
|
||||
List(JsString(nodeId.toString)))
|
||||
eclairCall[Vector[ChannelUpdate]]("allupdates", "nodeId" -> nodeId.toString)
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def audit(): Future[AuditResult] =
|
||||
eclairCall[AuditResult]("audit", List.empty)
|
||||
eclairCall[AuditResult]("audit")
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def audit(from: Long, to: Long): Future[AuditResult] =
|
||||
eclairCall[AuditResult]("audit", List(JsNumber(from), JsNumber(to)))
|
||||
override def audit(from: Option[FiniteDuration], to: Option[FiniteDuration]): Future[AuditResult] =
|
||||
eclairCall[AuditResult]("audit", Seq(
|
||||
from.map(x => "from" -> x.toSeconds.toString),
|
||||
to.map(x => "to" -> x.toSeconds.toString)).flatten: _*)
|
||||
|
||||
override def channel(channelId: ChannelId): Future[ChannelResult] = {
|
||||
eclairCall[ChannelResult]("channel", List(JsString(channelId.hex)))
|
||||
eclairCall[ChannelResult]("channel", "channelId" -> channelId.hex)
|
||||
}
|
||||
|
||||
private def channels(nodeId: Option[NodeId]): Future[Vector[ChannelInfo]] = {
|
||||
val params =
|
||||
if (nodeId.isEmpty) List.empty else List(JsString(nodeId.get.toString))
|
||||
|
||||
eclairCall[Vector[ChannelInfo]]("channels", params)
|
||||
val params = Seq(nodeId.map(id => "nodeId" -> id.toString)).flatten
|
||||
eclairCall[Vector[ChannelInfo]]("channels", params: _*)
|
||||
}
|
||||
|
||||
def channels(): Future[Vector[ChannelInfo]] = channels(nodeId = None)
|
||||
|
||||
override def channels(nodeId: NodeId): Future[Vector[ChannelInfo]] =
|
||||
channels(Some(nodeId))
|
||||
|
||||
override def checkInvoice(invoice: LnInvoice): Future[PaymentRequest] = {
|
||||
eclairCall[PaymentRequest]("checkinvoice", List(JsString(invoice.toString)))
|
||||
}
|
||||
|
||||
override def checkPayment(
|
||||
invoiceOrHash: Either[LnInvoice, Sha256Digest]): Future[Boolean] = {
|
||||
|
||||
val string = {
|
||||
if (invoiceOrHash.isLeft) {
|
||||
invoiceOrHash.left.get.toString
|
||||
} else {
|
||||
invoiceOrHash.right.get.hex
|
||||
}
|
||||
}
|
||||
|
||||
eclairCall[Boolean]("checkpayment", List(JsString(string)))
|
||||
}
|
||||
channels(Option(nodeId))
|
||||
|
||||
private def close(
|
||||
channelId: ChannelId,
|
||||
scriptPubKey: Option[ScriptPubKey]): Future[String] = {
|
||||
scriptPubKey: Option[ScriptPubKey]): Future[Unit] = {
|
||||
val params =
|
||||
if (scriptPubKey.isEmpty) {
|
||||
List(JsString(channelId.hex))
|
||||
Seq("channelId" -> channelId.hex)
|
||||
} else {
|
||||
|
||||
val asmHex = BitcoinSUtil.encodeHex(scriptPubKey.get.asmBytes)
|
||||
|
||||
List(JsString(channelId.hex), JsString(asmHex))
|
||||
Seq("channelId" -> channelId.hex, "scriptPubKey" -> asmHex)
|
||||
}
|
||||
|
||||
eclairCall[String]("close", params)
|
||||
eclairCall[String]("close", params: _*).map(_ => ())
|
||||
}
|
||||
|
||||
def close(channelId: ChannelId): Future[String] =
|
||||
def close(channelId: ChannelId): Future[Unit] =
|
||||
close(channelId, scriptPubKey = None)
|
||||
|
||||
override def close(
|
||||
channelId: ChannelId,
|
||||
scriptPubKey: ScriptPubKey): Future[String] = {
|
||||
scriptPubKey: ScriptPubKey): Future[Unit] = {
|
||||
close(channelId, Some(scriptPubKey))
|
||||
}
|
||||
|
||||
def connect(nodeId: NodeId, host: String, port: Int): Future[String] = {
|
||||
override def connect(nodeId: NodeId, host: String, port: Int): Future[Unit] = {
|
||||
val uri = NodeUri(nodeId, host, port)
|
||||
connect(uri)
|
||||
}
|
||||
|
||||
override def connect(uri: NodeUri): Future[String] = {
|
||||
eclairCall[String]("connect", List(JsString(uri.toString)))
|
||||
override def connect(uri: NodeUri): Future[Unit] = {
|
||||
eclairCall[String]("connect", "uri" -> uri.toString).map(_ => ())
|
||||
}
|
||||
|
||||
override def findRoute(nodeId: NodeId): Future[Vector[NodeId]] = {
|
||||
eclairCall[Vector[NodeId]]("findroute", List(JsString(nodeId.hex)))
|
||||
override def findRoute(nodeId: NodeId, amountMsat: MilliSatoshis): Future[Vector[NodeId]] = {
|
||||
eclairCall[Vector[NodeId]]("findroutetonode", "nodeId" -> nodeId.toString, "amountMsat" -> amountMsat.toBigDecimal.toString)
|
||||
}
|
||||
|
||||
override def findRoute(invoice: LnInvoice): Future[Vector[NodeId]] = {
|
||||
eclairCall[Vector[NodeId]]("findroute", List(JsString(invoice.toString)))
|
||||
findRoute(invoice, None)
|
||||
}
|
||||
|
||||
override def forceClose(channelId: ChannelId): Future[String] = {
|
||||
eclairCall[String]("forceclose", List(JsString(channelId.hex)))
|
||||
override def findRoute(invoice: LnInvoice, amount: MilliSatoshis): Future[Vector[NodeId]] = {
|
||||
findRoute(invoice, Some(amount))
|
||||
}
|
||||
|
||||
def findRoute(invoice: LnInvoice, amountMsat: Option[MilliSatoshis]): Future[Vector[NodeId]] = {
|
||||
val params = Seq(Some("invoice" -> invoice.toString), amountMsat.map(x => "amountMsat" -> x.toBigDecimal.toString)).flatten
|
||||
eclairCall[Vector[NodeId]]("findroute", params: _*)
|
||||
}
|
||||
|
||||
override def forceClose(channelId: ChannelId): Future[Unit] = {
|
||||
eclairCall[String]("forceclose", "channelId" -> channelId.hex).map(_ => ())
|
||||
}
|
||||
|
||||
override def forceClose(shortChannelId: ShortChannelId): Future[Unit] = {
|
||||
eclairCall[String]("forceclose", "shortChannelId" -> shortChannelId.toString).map(_ => ())
|
||||
}
|
||||
|
||||
override def getInfo: Future[GetInfoResult] = {
|
||||
|
@ -185,30 +177,31 @@ class EclairRpcClient(val instance: EclairInstance)(
|
|||
pushMsat: Option[MilliSatoshis],
|
||||
feerateSatPerByte: Option[SatoshisPerByte],
|
||||
channelFlags: Option[Byte]): Future[FundedChannelId] = {
|
||||
val num = pushMsat.getOrElse(MilliSatoshis.zero).toBigDecimal
|
||||
val pushMsatJson = JsNumber(num)
|
||||
val sat = fundingSatoshis.satoshis.toBigDecimal
|
||||
val _pushMsat = pushMsat.getOrElse(MilliSatoshis.zero).toBigDecimal.toString
|
||||
val _fundingSatoshis = fundingSatoshis.satoshis.toBigDecimal.toString
|
||||
|
||||
val params = {
|
||||
val params: Seq[(String, String)] = {
|
||||
if (feerateSatPerByte.isEmpty) {
|
||||
List(JsString(nodeId.toString), JsNumber(sat), pushMsatJson)
|
||||
Seq("nodeId" -> nodeId.toString,
|
||||
"fundingSatoshis" -> _fundingSatoshis,
|
||||
"pushMsat" -> _pushMsat)
|
||||
} else if (channelFlags.isEmpty) {
|
||||
List(JsString(nodeId.toString),
|
||||
JsNumber(sat),
|
||||
pushMsatJson,
|
||||
JsNumber(feerateSatPerByte.get.toLong))
|
||||
Seq("nodeId" -> nodeId.toString,
|
||||
"fundingSatoshis" -> _fundingSatoshis,
|
||||
"pushMsat" -> _pushMsat,
|
||||
"fundingFeerateSatByte" -> feerateSatPerByte.get.toLong.toString)
|
||||
} else {
|
||||
List(JsString(nodeId.toString),
|
||||
JsNumber(sat),
|
||||
pushMsatJson,
|
||||
JsNumber(feerateSatPerByte.get.toLong),
|
||||
JsString(channelFlags.toString))
|
||||
Seq("nodeId" -> nodeId.toString,
|
||||
"fundingSatoshis" -> _fundingSatoshis,
|
||||
"pushMsat" -> _pushMsat,
|
||||
"fundingFeerateSatByte" -> feerateSatPerByte.get.toLong.toString,
|
||||
"channelFlags" -> channelFlags.get.toString)
|
||||
}
|
||||
}
|
||||
|
||||
//this is unfortunately returned in this format
|
||||
//created channel 30bdf849eb9f72c9b41a09e38a6d83138c2edf332cb116dd7cf0f0dfb66be395
|
||||
val call = eclairCall[String]("open", params)
|
||||
val call = eclairCall[String]("open", params: _*)
|
||||
|
||||
//let's just return the chanId
|
||||
val chanIdF = call.map(_.split(" ").last)
|
||||
|
@ -278,208 +271,174 @@ class EclairRpcClient(val instance: EclairInstance)(
|
|||
eclairCall[Vector[PeerInfo]]("peers")
|
||||
}
|
||||
|
||||
/** A way to generate a [[org.bitcoins.core.protocol.ln.LnInvoice LnInvoice]]
|
||||
* with eclair.
|
||||
* @param amountMsat the amount to be encoded in the invoice
|
||||
* @param description meta information about the invoice
|
||||
* @param expirySeconds when the invoice expires
|
||||
* @return
|
||||
*/
|
||||
override def receive(
|
||||
amountMsat: Option[LnCurrencyUnit],
|
||||
description: Option[String],
|
||||
expirySeconds: Option[Long]): Future[LnInvoice] = {
|
||||
val msat = amountMsat.map(_.toMSat)
|
||||
override def createInvoice(description: String): Future[LnInvoice] = {
|
||||
createInvoice(description, None, None, None, None)
|
||||
}
|
||||
|
||||
val params = {
|
||||
if (amountMsat.isEmpty) {
|
||||
List(JsString(description.getOrElse("")))
|
||||
} else {
|
||||
val amt = JsNumber(msat.get.toBigDecimal)
|
||||
if (expirySeconds.isEmpty) {
|
||||
List(amt, JsString(description.getOrElse("")))
|
||||
} else {
|
||||
List(amt,
|
||||
JsString(description.getOrElse("")),
|
||||
JsNumber(expirySeconds.get))
|
||||
}
|
||||
}
|
||||
}
|
||||
override def createInvoice(description: String, amountMsat: MilliSatoshis): Future[LnInvoice] = {
|
||||
createInvoice(description, Some(amountMsat), None, None, None)
|
||||
}
|
||||
|
||||
val serializedF = eclairCall[String]("receive", params)
|
||||
override def createInvoice(description: String, amountMsat: MilliSatoshis, expireIn: FiniteDuration): Future[LnInvoice] = {
|
||||
createInvoice(description, Some(amountMsat), Some(expireIn), None, None)
|
||||
}
|
||||
|
||||
serializedF.flatMap { str =>
|
||||
val invoiceTry = LnInvoice.fromString(str)
|
||||
invoiceTry match {
|
||||
case Success(i) =>
|
||||
//register a monitor for when the payment is received
|
||||
registerPaymentMonitor(i)
|
||||
override def createInvoice(description: String, amountMsat: MilliSatoshis, paymentPreimage: PaymentPreimage): Future[LnInvoice] = {
|
||||
createInvoice(description, Some(amountMsat), None, None, Some(paymentPreimage))
|
||||
}
|
||||
|
||||
Future.successful(i)
|
||||
case Failure(err) =>
|
||||
Future.failed(err)
|
||||
}
|
||||
override def createInvoice(description: String, amountMsat: MilliSatoshis, expireIn: FiniteDuration, paymentPreimage: PaymentPreimage): Future[LnInvoice] = {
|
||||
createInvoice(description, Some(amountMsat), Some(expireIn), None, Some(paymentPreimage))
|
||||
}
|
||||
|
||||
override def createInvoice(description: String, amountMsat: Option[MilliSatoshis], expireIn: Option[FiniteDuration], fallbackAddress: Option[Address], paymentPreimage: Option[PaymentPreimage]): Future[LnInvoice] = {
|
||||
val params = Seq(
|
||||
Some("description" -> description),
|
||||
amountMsat.map(x => "amountMsat" -> x.toBigDecimal.toString),
|
||||
expireIn.map(x => "expireIn" -> x.toSeconds.toString),
|
||||
fallbackAddress.map(x => "fallbackAddress" -> x.toString),
|
||||
paymentPreimage.map(x => "paymentPreimage" -> x.hex)
|
||||
).flatten
|
||||
|
||||
val responseF = eclairCall[InvoiceResult]("createinvoice", params: _*)
|
||||
|
||||
responseF.flatMap {
|
||||
res =>
|
||||
Future.fromTry(LnInvoice.fromString(res.serialized))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pings eclair every second to see if a invoice has been paid
|
||||
* If the invoice has bene paid, we publish a
|
||||
* [[org.bitcoins.eclair.rpc.json.PaymentSucceeded PaymentSucceeded]]
|
||||
* event to the [[akka.actor.ActorSystem ActorSystem]]'s
|
||||
* [[akka.event.EventStream ActorSystem.eventStream]]
|
||||
*
|
||||
* If your application is interested in listening for payments,
|
||||
* you need to subscribe to the even stream and listen for a
|
||||
* [[org.bitcoins.eclair.rpc.json.PaymentSucceeded PaymentSucceeded]]
|
||||
* case class. You also need to check the
|
||||
* payment hash is the hash you expected
|
||||
*/
|
||||
private def registerPaymentMonitor(invoice: LnInvoice)(
|
||||
implicit system: ActorSystem): Unit = {
|
||||
|
||||
val p: Promise[Unit] = Promise[Unit]()
|
||||
|
||||
val runnable = new Runnable() {
|
||||
|
||||
override def run(): Unit = {
|
||||
val isPaidF = checkPayment(Left(invoice))
|
||||
|
||||
//register callback that publishes a payment to our actor system's
|
||||
//event stream,
|
||||
isPaidF.map { isPaid: Boolean =>
|
||||
if (!isPaid) {
|
||||
//do nothing since the invoice has not been paid yet
|
||||
()
|
||||
} else {
|
||||
//invoice has been paid, let's publish to event stream
|
||||
//so subscribers so the even stream can see that a payment
|
||||
//was received
|
||||
//we need to create a `PaymentSucceeded`
|
||||
val ps = PaymentSucceeded(amountMsat = invoice.amount.get.toMSat,
|
||||
paymentHash =
|
||||
invoice.lnTags.paymentHash.hash,
|
||||
paymentPreimage = "",
|
||||
route = JsArray.empty)
|
||||
system.eventStream.publish(ps)
|
||||
|
||||
//complete the promise so the runnable will be canceled
|
||||
p.success(())
|
||||
|
||||
()
|
||||
}
|
||||
}
|
||||
|
||||
()
|
||||
}
|
||||
}
|
||||
|
||||
val cancellable = system.scheduler.schedule(1.seconds, 1.seconds, runnable)
|
||||
|
||||
p.future.map(_ => cancellable.cancel())
|
||||
|
||||
()
|
||||
override def parseInvoice(invoice: LnInvoice): Future[InvoiceResult] = {
|
||||
eclairCall[InvoiceResult]("parseinvoice", "invoice" -> invoice.toString)
|
||||
}
|
||||
|
||||
def receive(): Future[LnInvoice] =
|
||||
receive(amountMsat = None, description = None, expirySeconds = None)
|
||||
|
||||
def receive(description: String): Future[LnInvoice] =
|
||||
receive(amountMsat = None, Some(description), expirySeconds = None)
|
||||
|
||||
override def receive(
|
||||
amountMsat: LnCurrencyUnit,
|
||||
description: String): Future[LnInvoice] =
|
||||
receive(Some(amountMsat), Some(description), expirySeconds = None)
|
||||
|
||||
def receive(
|
||||
amountMsat: LnCurrencyUnit,
|
||||
description: String,
|
||||
expirySeconds: Long): Future[LnInvoice] =
|
||||
receive(Some(amountMsat), Some(description), Some(expirySeconds))
|
||||
|
||||
def receive(amountMsat: LnCurrencyUnit): Future[LnInvoice] =
|
||||
receive(Some(amountMsat), description = None, expirySeconds = None)
|
||||
|
||||
def receive(
|
||||
amountMsat: LnCurrencyUnit,
|
||||
expirySeconds: Long): Future[LnInvoice] =
|
||||
receive(Some(amountMsat), description = None, Some(expirySeconds))
|
||||
|
||||
def send(
|
||||
amountMsat: LnCurrencyUnit,
|
||||
paymentHash: Sha256Digest,
|
||||
nodeId: NodeId): Future[PaymentResult] = {
|
||||
eclairCall[PaymentResult]("send",
|
||||
List(JsNumber(amountMsat.toMSat.toLong),
|
||||
JsString(paymentHash.hex),
|
||||
JsString(nodeId.toString)))
|
||||
override def payInvoice(invoice: LnInvoice): Future[PaymentId] = {
|
||||
payInvoice(invoice, None, None, None, None)
|
||||
}
|
||||
|
||||
private def send(
|
||||
invoice: LnInvoice,
|
||||
amountMsat: Option[LnCurrencyUnit]): Future[PaymentResult] = {
|
||||
|
||||
val params = {
|
||||
if (amountMsat.isEmpty) {
|
||||
List(JsString(invoice.toString))
|
||||
} else {
|
||||
List(JsString(invoice.toString), JsNumber(amountMsat.get.toMSat.toLong))
|
||||
}
|
||||
}
|
||||
|
||||
eclairCall[PaymentResult]("send", params)
|
||||
override def payInvoice(invoice: LnInvoice, amount: MilliSatoshis): Future[PaymentId] = {
|
||||
payInvoice(invoice, Some(amount), None, None, None)
|
||||
}
|
||||
|
||||
def send(invoice: LnInvoice): Future[PaymentResult] = send(invoice, None)
|
||||
override def payInvoice(invoice: LnInvoice, amountMsat: Option[MilliSatoshis], maxAttempts: Option[Int], feeThresholdSat: Option[Satoshis], maxFeePct: Option[Int]): Future[PaymentId] = {
|
||||
val params = Seq(
|
||||
Some("invoice" -> invoice.toString),
|
||||
amountMsat.map(x => "amountMsat" -> x.toBigDecimal.toString),
|
||||
maxAttempts.map(x => "maxAttempts" -> x.toString),
|
||||
feeThresholdSat.map(x => "feeThresholdSat" -> x.toBigDecimal.toString),
|
||||
maxFeePct.map(x => "maxFeePct" -> x.toString)
|
||||
).flatten
|
||||
|
||||
def send(
|
||||
invoice: LnInvoice,
|
||||
amountMsat: LnCurrencyUnit): Future[PaymentResult] =
|
||||
send(invoice, Some(amountMsat))
|
||||
eclairCall[PaymentId]("payinvoice", params: _*)
|
||||
}
|
||||
|
||||
override def getReceivedInfo(paymentHash: Sha256Digest): Future[ReceivedPaymentResult] = {
|
||||
eclairCall[ReceivedPaymentResult]("getreceivedinfo", "paymentHash" -> paymentHash.hex)
|
||||
}
|
||||
|
||||
override def getReceivedInfo(invoice: LnInvoice): Future[ReceivedPaymentResult] = {
|
||||
eclairCall[ReceivedPaymentResult]("getreceivedinfo", "invoice" -> invoice.toString)
|
||||
}
|
||||
|
||||
override def getSentInfo(paymentHash: Sha256Digest): Future[Vector[PaymentResult]] = {
|
||||
eclairCall[Vector[PaymentResult]]("getsentinfo", "paymentHash" -> paymentHash.hex)
|
||||
}
|
||||
|
||||
override def getSentInfo(id: PaymentId): Future[Vector[PaymentResult]] = {
|
||||
eclairCall[Vector[PaymentResult]]("getsentinfo", "id" -> id.toString)
|
||||
}
|
||||
|
||||
override def sendToNode(nodeId: NodeId, amountMsat: MilliSatoshis, paymentHash: Sha256Digest, maxAttempts: Option[Int], feeThresholdSat: Option[Satoshis], maxFeePct: Option[Int]): Future[PaymentId] = {
|
||||
val params = Seq(
|
||||
"nodeId" -> nodeId.toString,
|
||||
"amountMsat" -> amountMsat.toBigDecimal.toString,
|
||||
"paymentHash" -> paymentHash.hex) ++ Seq(
|
||||
maxAttempts.map(x => "maxAttempts" -> x.toString),
|
||||
feeThresholdSat.map(x => "feeThresholdSat" -> x.toBigDecimal.toString),
|
||||
maxFeePct.map(x => "maxFeePct" -> x.toString)
|
||||
).flatten
|
||||
|
||||
eclairCall[PaymentId]("sendtonode", params: _*)
|
||||
}
|
||||
|
||||
def sendToRoute(route: TraversableOnce[NodeId], amountMsat: MilliSatoshis, paymentHash: Sha256Digest, finalCltvExpiry: Long): Future[PaymentId] = {
|
||||
eclairCall[PaymentId]("sendtoroute",
|
||||
"route" -> route.mkString(","),
|
||||
"amountMsat" -> amountMsat.toBigDecimal.toString,
|
||||
"paymentHash" -> paymentHash.hex,
|
||||
"finalCltvExpiry" -> finalCltvExpiry.toString)
|
||||
}
|
||||
|
||||
override def updateRelayFee(
|
||||
channelId: ChannelId,
|
||||
feeBaseMsat: MilliSatoshis,
|
||||
feeProportionalMillionths: Long): Future[Unit] = {
|
||||
eclairCall[Unit]("updaterelayfee",
|
||||
List(JsString(channelId.hex),
|
||||
JsNumber(feeBaseMsat.toLong),
|
||||
JsNumber(feeProportionalMillionths)))
|
||||
"channelId" -> channelId.hex,
|
||||
"feeBaseMsat" -> feeBaseMsat.toLong.toString,
|
||||
"feeProportionalMillionths" -> feeProportionalMillionths.toString)
|
||||
}
|
||||
|
||||
override def updateRelayFee(
|
||||
shortChannelId: ShortChannelId,
|
||||
feeBaseMsat: MilliSatoshis,
|
||||
feePropertionalMillionths: Long): Future[Unit] = {
|
||||
feeProportionalMillionths: Long): Future[Unit] = {
|
||||
eclairCall[Unit]("updaterelayfee",
|
||||
List(JsString(shortChannelId.hex),
|
||||
JsNumber(feeBaseMsat.toLong),
|
||||
JsNumber(feePropertionalMillionths)))
|
||||
"shortChannelId" -> shortChannelId.toHumanReadableString,
|
||||
"feeBaseMsat" -> feeBaseMsat.toLong.toString,
|
||||
"feeProportionalMillionths" -> feeProportionalMillionths.toString)
|
||||
}
|
||||
|
||||
// TODO: channelstats, audit, networkfees?
|
||||
// TODO: Add types
|
||||
override def channelStats(): Future[Vector[ChannelStats]] = {
|
||||
eclairCall[Vector[ChannelStats]]("channelstats")
|
||||
}
|
||||
|
||||
private def eclairCall[T](
|
||||
command: String,
|
||||
parameters: List[JsValue] = List.empty)(
|
||||
override def networkFees(from: Option[FiniteDuration], to: Option[FiniteDuration]): Future[Vector[NetworkFeesResult]] = {
|
||||
eclairCall[Vector[NetworkFeesResult]]("networkfees", Seq(
|
||||
from.map(x => "from" -> x.toSeconds.toString),
|
||||
to.map(x => "to" -> x.toSeconds.toString)).flatten: _*)
|
||||
}
|
||||
|
||||
override def getInvoice(paymentHash: Sha256Digest): Future[LnInvoice] = {
|
||||
val resF = eclairCall[InvoiceResult]("getinvoice", "paymentHash" -> paymentHash.hex)
|
||||
resF.flatMap {
|
||||
res =>
|
||||
Future.fromTry(LnInvoice.fromString(res.serialized))
|
||||
}
|
||||
}
|
||||
|
||||
override def listInvoices(from: Option[FiniteDuration], to: Option[FiniteDuration]): Future[Vector[LnInvoice]] = {
|
||||
val resF = eclairCall[Vector[InvoiceResult]]("listinvoices", Seq(
|
||||
from.map(x => "from" -> x.toSeconds.toString),
|
||||
to.map(x => "to" -> x.toSeconds.toString)).flatten: _*)
|
||||
resF.flatMap(xs => Future.sequence(xs.map(x => Future.fromTry(LnInvoice.fromString(x.serialized)))))
|
||||
}
|
||||
|
||||
override def usableBalances(): Future[Vector[UsableBalancesResult]] = {
|
||||
eclairCall[Vector[UsableBalancesResult]]("usablebalances")
|
||||
}
|
||||
|
||||
override def disconnect(nodeId: NodeId): Future[Unit] = {
|
||||
eclairCall[String]("disconnect", "nodeId" -> nodeId.hex).map(_ => ())
|
||||
}
|
||||
|
||||
private def eclairCall[T](command: String, parameters: (String, String)*)(
|
||||
implicit
|
||||
reader: Reads[T]): Future[T] = {
|
||||
val request = buildRequest(getDaemon, command, JsArray(parameters))
|
||||
val request = buildRequest(getDaemon, command, parameters: _*)
|
||||
|
||||
logger.trace(s"eclair rpc call ${request}")
|
||||
val responseF = sendRequest(request)
|
||||
|
||||
val payloadF: Future[JsValue] = responseF.flatMap(getPayload)
|
||||
payloadF.map { payload =>
|
||||
val validated: JsResult[T] = (payload \ resultKey).validate[T]
|
||||
val parsed: T = parseResult(validated, payload, command)
|
||||
parsed
|
||||
val validated: JsResult[T] = payload.validate[T]
|
||||
val parsed: T = parseResult(validated, payload, command)
|
||||
parsed
|
||||
}
|
||||
}
|
||||
|
||||
case class RpcError(code: Int, message: String)
|
||||
case class RpcError(error: String)
|
||||
implicit val rpcErrorReads: Reads[RpcError] = Json.reads[RpcError]
|
||||
|
||||
private def parseResult[T](
|
||||
|
@ -490,19 +449,19 @@ class EclairRpcClient(val instance: EclairInstance)(
|
|||
case res: JsSuccess[T] =>
|
||||
res.value
|
||||
case res: JsError =>
|
||||
(json \ errorKey).validate[RpcError] match {
|
||||
json.validate[RpcError] match {
|
||||
case err: JsSuccess[RpcError] =>
|
||||
val datadirMsg = instance.authCredentials.datadir
|
||||
.map(d => s"datadir=${d}")
|
||||
.getOrElse("")
|
||||
val errMsg =
|
||||
s"Error for command=${commandName} ${datadirMsg}, ${err.value.code}=${err.value.message}"
|
||||
s"Error for command=${commandName} ${datadirMsg}, ${err.value.error}"
|
||||
logger.error(errMsg)
|
||||
throw new RuntimeException(errMsg)
|
||||
case _: JsError =>
|
||||
logger.error(JsError.toJson(res).toString())
|
||||
throw new IllegalArgumentException(
|
||||
s"Could not parse JsResult! JSON: ${(json \ resultKey).get}")
|
||||
s"Could not parse JsResult! JSON: ${json}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -524,22 +483,15 @@ class EclairRpcClient(val instance: EclairInstance)(
|
|||
private def buildRequest(
|
||||
instance: EclairInstance,
|
||||
methodName: String,
|
||||
params: JsArray): HttpRequest = {
|
||||
val uuid = UUID.randomUUID().toString
|
||||
params: (String, String)*): HttpRequest = {
|
||||
|
||||
val obj: JsObject = JsObject(
|
||||
Map("method" -> JsString(methodName),
|
||||
"params" -> params,
|
||||
"id" -> JsString(uuid)))
|
||||
|
||||
val uri = instance.rpcUri.toString
|
||||
val uri = instance.rpcUri.resolve("/" + methodName).toString
|
||||
// Eclair doesn't use a username
|
||||
val username = ""
|
||||
val password = instance.authCredentials.password
|
||||
HttpRequest(method = HttpMethods.POST,
|
||||
uri,
|
||||
entity =
|
||||
HttpEntity(ContentTypes.`application/json`, obj.toString))
|
||||
entity = FormData(params: _*).toEntity)
|
||||
.addCredentials(
|
||||
HttpCredentials.createBasicHttpCredentials(username, password))
|
||||
}
|
||||
|
@ -552,7 +504,7 @@ class EclairRpcClient(val instance: EclairInstance)(
|
|||
"This needs to be set to the directory containing the Eclair Jar")
|
||||
.mkString(" ")))
|
||||
|
||||
val eclairV = "/eclair-node-0.2-beta8-52821b8.jar"
|
||||
val eclairV = "/eclair-node-0.3.1-6906ecb.jar"
|
||||
val fullPath = path + eclairV
|
||||
|
||||
val jar = new File(fullPath)
|
||||
|
|
|
@ -1,30 +1,25 @@
|
|||
package org.bitcoins.eclair.rpc.json
|
||||
|
||||
import org.bitcoins.core.crypto.{
|
||||
DoubleSha256Digest,
|
||||
ECDigitalSignature,
|
||||
Sha256Digest
|
||||
}
|
||||
import org.bitcoins.core.protocol.ln.{
|
||||
LnHumanReadablePart,
|
||||
LnInvoiceSignature,
|
||||
ShortChannelId
|
||||
}
|
||||
import org.bitcoins.core.crypto.{DoubleSha256Digest, DoubleSha256DigestBE, ECDigitalSignature, Sha256Digest}
|
||||
import org.bitcoins.core.currency.Satoshis
|
||||
import org.bitcoins.core.protocol.ln.channel.{ChannelState, FundedChannelId}
|
||||
import org.bitcoins.core.protocol.ln.currency.MilliSatoshis
|
||||
import org.bitcoins.core.protocol.ln.fee.FeeProportionalMillionths
|
||||
import org.bitcoins.core.protocol.ln.node.NodeId
|
||||
import org.bitcoins.core.protocol.ln.{LnHumanReadablePart, LnInvoiceSignature, PaymentPreimage, ShortChannelId}
|
||||
import org.bitcoins.eclair.rpc.network.PeerState
|
||||
import play.api.libs.json.{JsArray, JsObject}
|
||||
import play.api.libs.json.JsObject
|
||||
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
|
||||
sealed abstract class EclairModels
|
||||
|
||||
case class GetInfoResult(
|
||||
nodeId: NodeId,
|
||||
alias: String,
|
||||
port: Int,
|
||||
chainHash: DoubleSha256Digest,
|
||||
blockHeight: Long)
|
||||
blockHeight: Long,
|
||||
publicAddresses: Seq[String])
|
||||
|
||||
case class PeerInfo(
|
||||
nodeId: NodeId,
|
||||
|
@ -81,7 +76,6 @@ case class NodeInfo(
|
|||
nodeId: NodeId,
|
||||
rgbColor: String,
|
||||
alias: String,
|
||||
shortChannelId: ShortChannelId,
|
||||
addresses: Vector[String])
|
||||
|
||||
case class ChannelDesc(shortChannelId: ShortChannelId, a: NodeId, b: NodeId)
|
||||
|
@ -92,11 +86,34 @@ case class AuditResult(
|
|||
received: Vector[ReceivedPayment]
|
||||
)
|
||||
|
||||
case class NetworkFeesResult(
|
||||
remoteNodeId: NodeId,
|
||||
channelId: FundedChannelId,
|
||||
txId: DoubleSha256DigestBE,
|
||||
feeSat: Satoshis,
|
||||
txType: String,
|
||||
timestamp: FiniteDuration
|
||||
)
|
||||
|
||||
case class ChannelStats(
|
||||
channelId: FundedChannelId,
|
||||
avgPaymentAmountSatoshi: Satoshis,
|
||||
paymentCount: Long,
|
||||
relayFeeSatoshi: Satoshis,
|
||||
networkFeeSatoshi: Satoshis
|
||||
)
|
||||
|
||||
case class UsableBalancesResult(
|
||||
canSendMsat: MilliSatoshis,
|
||||
canReceiveMsat: MilliSatoshis,
|
||||
isPublic: Boolean
|
||||
)
|
||||
|
||||
case class ReceivedPayment(
|
||||
amount: MilliSatoshis,
|
||||
paymentHash: Sha256Digest,
|
||||
fromChannelId: FundedChannelId,
|
||||
timestamp: Long
|
||||
timestamp: FiniteDuration
|
||||
)
|
||||
|
||||
case class RelayedPayment(
|
||||
|
@ -105,7 +122,7 @@ case class RelayedPayment(
|
|||
paymentHash: Sha256Digest,
|
||||
fromChannelId: FundedChannelId,
|
||||
toChannelId: FundedChannelId,
|
||||
timestamp: Long
|
||||
timestamp: FiniteDuration
|
||||
)
|
||||
|
||||
case class SentPayment(
|
||||
|
@ -114,7 +131,7 @@ case class SentPayment(
|
|||
paymentHash: Sha256Digest,
|
||||
paymentPreimage: String,
|
||||
toChannelId: FundedChannelId,
|
||||
timestamp: Long
|
||||
timestamp: FiniteDuration
|
||||
)
|
||||
|
||||
case class ChannelUpdate(
|
||||
|
@ -126,9 +143,9 @@ case class ChannelUpdate(
|
|||
channelFlags: Int,
|
||||
cltvExpiryDelta: Int,
|
||||
htlcMinimumMsat: MilliSatoshis,
|
||||
feeProportionalMillionths: FeeProportionalMillionths,
|
||||
htlcMaximumMsat: Option[MilliSatoshis],
|
||||
feeBaseMsat: MilliSatoshis,
|
||||
feeProportionalMillionths: Long)
|
||||
feeBaseMsat: MilliSatoshis)
|
||||
|
||||
/* ChannelResult starts here, some of this may be useful but it seems that data is different at different times
|
||||
|
||||
|
@ -246,10 +263,22 @@ case class ChannelResult(
|
|||
state: ChannelState,
|
||||
feeBaseMsat: Option[MilliSatoshis],
|
||||
feeProportionalMillionths: Option[FeeProportionalMillionths],
|
||||
data: JsObject)
|
||||
data: JsObject) {
|
||||
import JsonReaders._
|
||||
lazy val shortChannelId: Option[ShortChannelId] = (data \ "shortChannelId").validate[ShortChannelId].asOpt
|
||||
}
|
||||
|
||||
// ChannelResult ends here
|
||||
|
||||
case class InvoiceResult(
|
||||
prefix: LnHumanReadablePart,
|
||||
timestamp: FiniteDuration,
|
||||
nodeId: NodeId,
|
||||
serialized: String,
|
||||
description: String,
|
||||
paymentHash: Sha256Digest,
|
||||
expiry: FiniteDuration)
|
||||
|
||||
case class PaymentRequest(
|
||||
prefix: LnHumanReadablePart,
|
||||
amount: Option[MilliSatoshis],
|
||||
|
@ -258,25 +287,73 @@ case class PaymentRequest(
|
|||
tags: Vector[JsObject],
|
||||
signature: LnInvoiceSignature)
|
||||
|
||||
sealed abstract class PaymentResult
|
||||
case class PaymentSucceeded(
|
||||
amountMsat: MilliSatoshis,
|
||||
paymentHash: Sha256Digest,
|
||||
paymentPreimage: String,
|
||||
route: JsArray)
|
||||
extends PaymentResult
|
||||
case class PaymentResult(
|
||||
id: String,
|
||||
paymentHash: Sha256Digest,
|
||||
preimage: Option[PaymentPreimage],
|
||||
amountMsat: MilliSatoshis,
|
||||
createdAt: FiniteDuration,
|
||||
completedAt: Option[FiniteDuration],
|
||||
status: PaymentStatus)
|
||||
|
||||
case class PaymentFailed(paymentHash: Sha256Digest, failures: Vector[JsObject])
|
||||
extends PaymentResult
|
||||
/*
|
||||
case class PaymentFailure(???) extends SendResult
|
||||
implicit val paymentFailureReads: Reads[PaymentFailure] = Json.reads[PaymentFailure]
|
||||
implicit val sendResultReads: Reads[SendResult] = Reads[SendResult] { json =>
|
||||
json.validate[PaymentSucceeded] match {
|
||||
case success: JsSuccess[PaymentSucceeded] => success
|
||||
case err1: JsError => json.validate[PaymentFailure] match {
|
||||
case failure: JsSuccess[PaymentFailure] => failure
|
||||
case err2: JsError => JsError.merge(err1, err2)
|
||||
}
|
||||
case class ReceivedPaymentResult(
|
||||
paymentHash: Sha256Digest,
|
||||
amountMsat: MilliSatoshis,
|
||||
receivedAt: FiniteDuration)
|
||||
|
||||
sealed trait PaymentStatus
|
||||
object PaymentStatus {
|
||||
case object PENDING extends PaymentStatus
|
||||
case object SUCCEEDED extends PaymentStatus
|
||||
case object FAILED extends PaymentStatus
|
||||
|
||||
def apply(s: String): PaymentStatus = s match {
|
||||
case "PENDING" => PENDING
|
||||
case "SUCCEEDED" => SUCCEEDED
|
||||
case "FAILED" => FAILED
|
||||
case err => throw new IllegalArgumentException(s"Unknown payment status code `${err}`")
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
case class PaymentId(value: String) {
|
||||
override def toString: String = value
|
||||
}
|
||||
|
||||
|
||||
sealed trait WebSocketEvent
|
||||
|
||||
object WebSocketEvent {
|
||||
|
||||
|
||||
case class PaymentRelayed(
|
||||
amountIn: MilliSatoshis,
|
||||
amountOut: MilliSatoshis,
|
||||
paymentHash: Sha256Digest,
|
||||
fromChannelId: FundedChannelId,
|
||||
toChannelId: FundedChannelId,
|
||||
timestamp: FiniteDuration) extends WebSocketEvent
|
||||
|
||||
case class PaymentReceived(
|
||||
amount: MilliSatoshis,
|
||||
paymentHash: Sha256Digest,
|
||||
fromChannelId: FundedChannelId,
|
||||
timestamp: FiniteDuration) extends WebSocketEvent
|
||||
|
||||
case class PaymentFailed(
|
||||
paymentHash: Sha256Digest,
|
||||
failures: Vector[String]) extends WebSocketEvent
|
||||
|
||||
case class PaymentSent(
|
||||
amount: MilliSatoshis,
|
||||
feesPaid: MilliSatoshis,
|
||||
paymentHash: Sha256Digest,
|
||||
paymentPreimage: PaymentPreimage,
|
||||
toChannelId: FundedChannelId,
|
||||
timestamp: FiniteDuration) extends WebSocketEvent
|
||||
|
||||
case class PaymentSettlingOnchain(
|
||||
amount: MilliSatoshis,
|
||||
paymentHash: Sha256Digest,
|
||||
timestamp: FiniteDuration) extends WebSocketEvent
|
||||
|
||||
}
|
|
@ -1,24 +1,25 @@
|
|||
package org.bitcoins.eclair.rpc.json
|
||||
|
||||
import org.bitcoins.core.protocol.ln.{
|
||||
LnHumanReadablePart,
|
||||
LnInvoice,
|
||||
LnInvoiceSignature,
|
||||
ShortChannelId
|
||||
}
|
||||
import org.bitcoins.core.crypto.Sha256Digest
|
||||
import org.bitcoins.core.protocol.ln.channel.{ChannelState, FundedChannelId}
|
||||
import org.bitcoins.core.protocol.ln.currency.{MilliSatoshis, PicoBitcoins}
|
||||
import org.bitcoins.core.protocol.ln.fee.FeeProportionalMillionths
|
||||
import org.bitcoins.core.protocol.ln.node.NodeId
|
||||
import org.bitcoins.core.protocol.ln._
|
||||
import org.bitcoins.eclair.rpc.network.PeerState
|
||||
import org.bitcoins.rpc.serializers.SerializerUtil
|
||||
import play.api.libs.json._
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
object JsonReaders {
|
||||
import org.bitcoins.rpc.serializers.JsonReaders._
|
||||
|
||||
implicit val feeProportionalMillionthsReads: Reads[FeeProportionalMillionths] = Reads { js =>
|
||||
SerializerUtil.processJsNumberBigInt(FeeProportionalMillionths.fromBigInt)(js)
|
||||
}
|
||||
|
||||
implicit val channelStateReads: Reads[ChannelState] = {
|
||||
Reads { jsValue: JsValue =>
|
||||
SerializerUtil.processJsStringOpt(ChannelState.fromString)(jsValue)
|
||||
|
@ -84,7 +85,7 @@ object JsonReaders {
|
|||
|
||||
implicit val shortChannelIdReads: Reads[ShortChannelId] = {
|
||||
Reads { jsValue =>
|
||||
SerializerUtil.processJsString(ShortChannelId.fromHex)(jsValue)
|
||||
SerializerUtil.processJsString(ShortChannelId.fromHumanReadableString)(jsValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -92,6 +93,12 @@ object JsonReaders {
|
|||
Json.reads[NodeInfo]
|
||||
}
|
||||
|
||||
implicit val paymentPreimageReads: Reads[PaymentPreimage] = {
|
||||
Reads { jsValue: JsValue =>
|
||||
SerializerUtil.processJsString(PaymentPreimage.fromHex)(jsValue)
|
||||
}
|
||||
}
|
||||
|
||||
implicit val fundedChannelIdReads: Reads[FundedChannelId] = {
|
||||
Reads { jsValue: JsValue =>
|
||||
SerializerUtil.processJsString(FundedChannelId.fromHex)(jsValue)
|
||||
|
@ -102,6 +109,28 @@ object JsonReaders {
|
|||
Json.reads[ChannelDesc]
|
||||
}
|
||||
|
||||
implicit val createInvoiceResultReads: Reads[InvoiceResult] = {
|
||||
Reads { jsValue =>
|
||||
for {
|
||||
prefix <- (jsValue \ "prefix").validate[LnHumanReadablePart]
|
||||
timestamp <- (jsValue \ "timestamp").validate[Long]
|
||||
nodeId <- (jsValue \ "nodeId").validate[NodeId]
|
||||
serialized <- (jsValue \ "serialized").validate[String]
|
||||
description <- (jsValue \ "description").validate[String]
|
||||
paymentHash <- (jsValue \ "paymentHash").validate[Sha256Digest]
|
||||
expiry <- (jsValue \ "expiry").validate[Long]
|
||||
} yield
|
||||
InvoiceResult(
|
||||
prefix,
|
||||
timestamp.seconds,
|
||||
nodeId,
|
||||
serialized,
|
||||
description,
|
||||
paymentHash,
|
||||
expiry.seconds)
|
||||
}
|
||||
}
|
||||
|
||||
implicit val openChannelInfoReads: Reads[OpenChannelInfo] = Reads { jsValue =>
|
||||
for {
|
||||
nodeId <- (jsValue \ "nodeId").validate[NodeId]
|
||||
|
@ -160,36 +189,24 @@ object JsonReaders {
|
|||
Json.reads[PaymentRequest]
|
||||
}
|
||||
|
||||
implicit val paymentSucceededReads: Reads[PaymentSucceeded] = {
|
||||
Json.reads[PaymentSucceeded]
|
||||
implicit val paymentIdReads: Reads[PaymentId] = Reads { jsValue =>
|
||||
SerializerUtil.processJsString(PaymentId.apply)(jsValue)
|
||||
}
|
||||
|
||||
implicit val paymentFailedReads: Reads[PaymentFailed] = {
|
||||
Json.reads[PaymentFailed]
|
||||
implicit val paymentStatusReads: Reads[PaymentStatus] = Reads { jsValue =>
|
||||
SerializerUtil.processJsString(PaymentStatus.apply)(jsValue)
|
||||
}
|
||||
|
||||
implicit val paymentResultReads: Reads[PaymentResult] = {
|
||||
Reads[PaymentResult] { jsValue =>
|
||||
val sendResult = jsValue.validate[PaymentSucceeded]
|
||||
sendResult match {
|
||||
case p: JsSuccess[PaymentSucceeded] => p
|
||||
case err1: JsError =>
|
||||
val pFailedResult = jsValue.validate[PaymentFailed]
|
||||
pFailedResult match {
|
||||
case s: JsSuccess[PaymentFailed] => s
|
||||
case err2: JsError =>
|
||||
JsError.merge(err1, err2)
|
||||
}
|
||||
}
|
||||
implicit val finiteDurationReads: Reads[FiniteDuration] =
|
||||
Reads { js =>
|
||||
SerializerUtil.processJsNumberBigInt(_.longValue.millis)(js)
|
||||
}
|
||||
}
|
||||
|
||||
implicit val feeProportionalMillionthsReads: Reads[
|
||||
FeeProportionalMillionths] = Reads { js =>
|
||||
SerializerUtil.processJsNumberBigInt(
|
||||
FeeProportionalMillionths.fromBigInt
|
||||
)(js)
|
||||
}
|
||||
implicit val paymentSucceededReads: Reads[PaymentResult] =
|
||||
Json.reads[PaymentResult]
|
||||
|
||||
implicit val receivedPaymentResultReads: Reads[ReceivedPaymentResult] =
|
||||
Json.reads[ReceivedPaymentResult]
|
||||
|
||||
implicit val channelResultReads: Reads[ChannelResult] = Reads { js =>
|
||||
for {
|
||||
|
@ -229,4 +246,31 @@ object JsonReaders {
|
|||
implicit val relayedPaymentReads: Reads[RelayedPayment] =
|
||||
Json.reads[RelayedPayment]
|
||||
implicit val auditResultReads: Reads[AuditResult] = Json.reads[AuditResult]
|
||||
|
||||
implicit val networkFeesResultReads: Reads[NetworkFeesResult] =
|
||||
Json.reads[NetworkFeesResult]
|
||||
|
||||
implicit val channelStatsReads: Reads[ChannelStats] =
|
||||
Json.reads[ChannelStats]
|
||||
|
||||
implicit val usableBalancesResultReads: Reads[UsableBalancesResult] =
|
||||
Json.reads[UsableBalancesResult]
|
||||
|
||||
import WebSocketEvent._
|
||||
|
||||
implicit val paymentRelayedEventReads: Reads[PaymentRelayed] =
|
||||
Json.reads[PaymentRelayed]
|
||||
|
||||
implicit val paymentReceivedEventReads: Reads[PaymentReceived] =
|
||||
Json.reads[PaymentReceived]
|
||||
|
||||
implicit val paymentFailedEventReads: Reads[PaymentFailed] =
|
||||
Json.reads[PaymentFailed]
|
||||
|
||||
implicit val paymentSentEventReads: Reads[PaymentSent] =
|
||||
Json.reads[PaymentSent]
|
||||
|
||||
implicit val paymentSettlingOnchainEventReads: Reads[PaymentSettlingOnchain] =
|
||||
Json.reads[PaymentSettlingOnchain]
|
||||
|
||||
}
|
||||
|
|
|
@ -113,7 +113,7 @@ trait ChainUnitTest
|
|||
fixture: FixtureParam =>
|
||||
partialTestFun.applyOrElse[FixtureParam, Future[Assertion]](fixture, {
|
||||
_: FixtureParam =>
|
||||
Future.successful(fail("Incorrect tag/fixture for this test"))
|
||||
Future(fail("Incorrect tag/fixture for this test"))
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -142,7 +142,7 @@ trait ChainUnitTest
|
|||
fixture: FixtureParam =>
|
||||
partialTestFun.applyOrElse[FixtureParam, Future[Assertion]](fixture, {
|
||||
_: FixtureParam =>
|
||||
Future.successful(fail("Incorrect tag/fixture for this test"))
|
||||
Future(fail("Incorrect tag/fixture for this test"))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -7,25 +7,20 @@ import akka.actor.ActorSystem
|
|||
import com.typesafe.config.{Config, ConfigFactory}
|
||||
import org.bitcoins.core.config.RegTest
|
||||
import org.bitcoins.core.currency.CurrencyUnit
|
||||
import org.bitcoins.core.protocol.ln.channel.{
|
||||
ChannelId,
|
||||
ChannelState,
|
||||
FundedChannelId
|
||||
}
|
||||
import org.bitcoins.core.protocol.ln.channel.{ChannelId, ChannelState, FundedChannelId}
|
||||
import org.bitcoins.core.protocol.ln.currency.MilliSatoshis
|
||||
import org.bitcoins.core.protocol.ln.node.NodeId
|
||||
import org.bitcoins.core.util.BitcoinSLogger
|
||||
import org.bitcoins.eclair.rpc.client.EclairRpcClient
|
||||
import org.bitcoins.eclair.rpc.config.EclairInstance
|
||||
import org.bitcoins.eclair.rpc.json.PaymentResult
|
||||
import org.bitcoins.rpc.client.common.{BitcoindRpcClient, BitcoindVersion}
|
||||
import org.bitcoins.rpc.client.v16.BitcoindV16RpcClient
|
||||
import org.bitcoins.rpc.config.{BitcoindInstance}
|
||||
import org.bitcoins.eclair.rpc.json.{PaymentId, PaymentStatus}
|
||||
import org.bitcoins.rpc.client.common.{BitcoindRpcClient}
|
||||
import org.bitcoins.rpc.config.BitcoindInstance
|
||||
import org.bitcoins.rpc.util.RpcUtil
|
||||
import org.bitcoins.testkit.async.TestAsyncUtil
|
||||
import org.bitcoins.testkit.rpc.{BitcoindRpcTestUtil, TestRpcUtil}
|
||||
|
||||
import scala.concurrent.duration.DurationInt
|
||||
import scala.concurrent.duration.{DurationInt, FiniteDuration}
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.util.{Failure, Success}
|
||||
import org.bitcoins.rpc.config.BitcoindAuthCredentials
|
||||
|
@ -58,42 +53,16 @@ trait EclairRpcTestUtil extends BitcoinSLogger {
|
|||
*/
|
||||
def startedBitcoindRpcClient(instance: BitcoindInstance = bitcoindInstance())(
|
||||
implicit actorSystem: ActorSystem): Future[BitcoindRpcClient] = {
|
||||
import actorSystem.dispatcher
|
||||
for {
|
||||
cli <- BitcoindRpcTestUtil.startedBitcoindRpcClient(instance)
|
||||
// make sure we have enough money open channels
|
||||
//not async safe
|
||||
versionedCli: BitcoindRpcClient <- {
|
||||
if (cli.instance.getVersion == BitcoindVersion.V17) {
|
||||
val v16Cli = new BitcoindV16RpcClient(
|
||||
BitcoindRpcTestUtil.v16Instance())
|
||||
val startF =
|
||||
Future.sequence(List(cli.stop(), v16Cli.start())).map(_ => v16Cli)
|
||||
|
||||
startF.recover {
|
||||
case exception: Exception =>
|
||||
logger.error(
|
||||
List(
|
||||
"Eclair requires Bitcoin Core 0.16.",
|
||||
"You can set the environment variable BITCOIND_V16_PATH to override",
|
||||
"the default bitcoind executable on your PATH."
|
||||
).mkString(" "))
|
||||
throw exception
|
||||
}
|
||||
} else {
|
||||
Future.successful(cli)
|
||||
}
|
||||
}
|
||||
} yield versionedCli
|
||||
BitcoindRpcTestUtil.startedBitcoindRpcClient(instance)
|
||||
}
|
||||
|
||||
def bitcoindInstance(
|
||||
port: Int = RpcUtil.randomPort,
|
||||
rpcPort: Int = RpcUtil.randomPort,
|
||||
zmqPort: Int = RpcUtil.randomPort): BitcoindInstance = {
|
||||
BitcoindRpcTestUtil.instance(port = port,
|
||||
rpcPort = rpcPort,
|
||||
zmqPort = zmqPort)
|
||||
BitcoindRpcTestUtil.v17Instance(port = port,
|
||||
rpcPort = rpcPort,
|
||||
zmqPort = zmqPort)
|
||||
}
|
||||
|
||||
//cribbed from https://github.com/Christewart/eclair/blob/bad02e2c0e8bd039336998d318a861736edfa0ad/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala#L140-L153
|
||||
|
@ -254,6 +223,66 @@ trait EclairRpcTestUtil extends BitcoinSLogger {
|
|||
duration = 1.seconds)
|
||||
}
|
||||
|
||||
def awaitUntilPaymentSucceeded(
|
||||
client: EclairRpcClient,
|
||||
paymentId: PaymentId,
|
||||
duration: FiniteDuration = 1.second,
|
||||
maxTries: Int = 50,
|
||||
failFast: Boolean = true)
|
||||
(implicit system: ActorSystem): Future[Unit] = {
|
||||
awaitUntilPaymentStatus(client, paymentId, PaymentStatus.SUCCEEDED, duration, maxTries, failFast)
|
||||
}
|
||||
|
||||
def awaitUntilPaymentFailed(
|
||||
client: EclairRpcClient,
|
||||
paymentId: PaymentId,
|
||||
duration: FiniteDuration = 1.second,
|
||||
maxTries: Int = 50,
|
||||
failFast: Boolean = false)
|
||||
(implicit system: ActorSystem): Future[Unit] = {
|
||||
awaitUntilPaymentStatus(client, paymentId, PaymentStatus.FAILED, duration, maxTries, failFast)
|
||||
}
|
||||
|
||||
def awaitUntilPaymentPending(
|
||||
client: EclairRpcClient,
|
||||
paymentId: PaymentId,
|
||||
duration: FiniteDuration = 1.second,
|
||||
maxTries: Int = 50,
|
||||
failFast: Boolean = true)
|
||||
(implicit system: ActorSystem): Future[Unit] = {
|
||||
awaitUntilPaymentStatus(client, paymentId, PaymentStatus.PENDING, duration, maxTries, failFast)
|
||||
}
|
||||
|
||||
private def awaitUntilPaymentStatus(
|
||||
client: EclairRpcClient,
|
||||
paymentId: PaymentId,
|
||||
state: PaymentStatus,
|
||||
duration: FiniteDuration,
|
||||
maxTries: Int,
|
||||
failFast: Boolean)
|
||||
(implicit system: ActorSystem): Future[Unit] = {
|
||||
logger.debug(s"Awaiting payment ${paymentId} to enter ${state} state")
|
||||
|
||||
def isState(): Future[Boolean] = {
|
||||
|
||||
val sentInfoF = client.getSentInfo(paymentId)
|
||||
sentInfoF.map { payment =>
|
||||
if (failFast && payment.exists(_.status == PaymentStatus.FAILED)) {
|
||||
throw new RuntimeException(s"Payment ${paymentId} has failed")
|
||||
}
|
||||
if (!payment.exists(_.status == state)) {
|
||||
logger.trace(
|
||||
s"Payment ${paymentId} has not entered ${state} yet. Currently in ${payment.map(_.status).mkString(",")}")
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}(system.dispatcher)
|
||||
}
|
||||
|
||||
TestAsyncUtil.retryUntilSatisfiedF(conditionF = () => isState(), duration = duration, maxTries = maxTries)
|
||||
}
|
||||
|
||||
private def createNodeLink(
|
||||
bitcoindRpcClient: Option[BitcoindRpcClient],
|
||||
channelAmount: MilliSatoshis)(
|
||||
|
@ -425,8 +454,9 @@ trait EclairRpcTestUtil extends BitcoinSLogger {
|
|||
implicit val dispatcher = system.dispatcher
|
||||
val infoF = otherClient.getInfo
|
||||
val nodeIdF = infoF.map(_.nodeId)
|
||||
val connection: Future[String] = infoF.flatMap { info =>
|
||||
client.connect(info.nodeId, "localhost", info.port)
|
||||
val connection: Future[Unit] = infoF.flatMap { info =>
|
||||
val Array(host, port) = info.publicAddresses.head.split(":")
|
||||
client.connect(info.nodeId, host, port.toInt)
|
||||
}
|
||||
|
||||
def isConnected(): Future[Boolean] = {
|
||||
|
@ -460,13 +490,13 @@ trait EclairRpcTestUtil extends BitcoinSLogger {
|
|||
c1: EclairRpcClient,
|
||||
c2: EclairRpcClient,
|
||||
numPayments: Int = 5)(
|
||||
implicit ec: ExecutionContext): Future[Vector[PaymentResult]] = {
|
||||
implicit ec: ExecutionContext): Future[Vector[PaymentId]] = {
|
||||
val payments = (1 to numPayments)
|
||||
.map(MilliSatoshis(_))
|
||||
.map(
|
||||
sats =>
|
||||
c1.receive(s"this is a note for $sats")
|
||||
.flatMap(invoice => c2.send(invoice, sats.toLnCurrencyUnit))
|
||||
c1.createInvoice(s"this is a note for $sats")
|
||||
.flatMap(invoice => c2.payInvoice(invoice, sats))
|
||||
)
|
||||
|
||||
val resultF = Future.sequence(payments).map(_.toVector)
|
||||
|
|
|
@ -3,7 +3,7 @@ package org.bitcoins.testkit.node
|
|||
import java.net.InetSocketAddress
|
||||
|
||||
import akka.actor.ActorRefFactory
|
||||
import org.bitcoins.core.p2p.{NetworkIpAddress, NetworkMessage}
|
||||
import org.bitcoins.core.p2p.NetworkMessage
|
||||
import org.bitcoins.core.protocol.blockchain.BlockHeader
|
||||
import org.bitcoins.core.protocol.transaction.Transaction
|
||||
import org.bitcoins.node.config.NodeAppConfig
|
||||
|
|
|
@ -50,7 +50,6 @@ import scala.concurrent._
|
|||
import scala.concurrent.duration.{DurationInt, FiniteDuration}
|
||||
import scala.util._
|
||||
import org.bitcoins.rpc.config.BitcoindConfig
|
||||
import java.nio.file.Files
|
||||
import java.io.File
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigFactory
|
||||
|
|
Loading…
Add table
Reference in a new issue