[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:
rorp 2019-07-03 11:38:56 -07:00 committed by Chris Stewart
parent 57d4ae51fc
commit 854242b462
14 changed files with 1094 additions and 694 deletions

View file

@ -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:

View file

@ -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"))
}
}

View file

@ -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)
}

View file

@ -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)
}
}

View file

@ -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)

View file

@ -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]]
}

View file

@ -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)

View file

@ -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
}

View file

@ -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]
}

View file

@ -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"))
})
}

View file

@ -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)

View file

@ -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

View file

@ -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