Upgrade Eclair to v0.7.0 (#4308)

* Upgrade Eclair to v0.7.0

* Scala 2.12 compatibility

* reformat
This commit is contained in:
rorp 2022-05-02 07:04:56 -07:00 committed by GitHub
parent ce00d3ac36
commit 3dc709386a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 359 additions and 87 deletions

View file

@ -285,8 +285,11 @@ case class PaymentRequest(
serialized: String,
description: String,
paymentHash: Sha256Digest,
paymentMetadata: String,
expiry: FiniteDuration, //seconds
amount: Option[MilliSatoshis])
minFinalCltvExpiry: Int,
amount: Option[MilliSatoshis],
features: Features)
sealed trait PaymentType
@ -295,13 +298,15 @@ object PaymentType extends StringFactory[PaymentType] {
case object Standard extends PaymentType
case object SwapIn extends PaymentType
case object SwapOut extends PaymentType
case object Placeholder extends PaymentType
override def fromString(str: String): PaymentType =
str match {
case "Standard" => Standard
case "SwapIn" => SwapIn
case "SwapOut" => SwapOut
case _ => throw new RuntimeException(s"Unknown payment type `$str`")
case "Standard" => Standard
case "SwapIn" => SwapIn
case "SwapOut" => SwapOut
case "placeholder" => Placeholder
case _ => throw new RuntimeException(s"Unknown payment type `$str`")
}
}
@ -322,6 +327,7 @@ case class OutgoingPayment(
case class IncomingPayment(
paymentRequest: PaymentRequest,
paymentPreimage: PaymentPreimage,
paymentType: PaymentType,
createdAt: Instant, //milliseconds
status: IncomingPaymentStatus)

View file

@ -16,6 +16,7 @@ import org.bitcoins.core.protocol.ln.channel._
import org.bitcoins.core.protocol.ln.currency._
import org.bitcoins.core.protocol.ln.fee.FeeProportionalMillionths
import org.bitcoins.core.protocol.ln.node.{Feature, FeatureSupport, NodeId}
import org.bitcoins.core.protocol.ln.routing.{ChannelRoute, NodeRoute, Route}
import org.bitcoins.core.protocol.script.{
ScriptPubKey,
ScriptSignature,
@ -805,7 +806,9 @@ object JsonReaders {
}
lazy val featuresByName: Map[String, Feature] =
Feature.knownFeatures.map(f => (f.rfcName, f)).toMap
org.bitcoins.core.protocol.ln.node.Features.knownFeatures
.map(f => (f.rfcName, f))
.toMap
implicit val featureReads: Reads[Feature] =
Reads { jsValue =>
@ -854,7 +857,7 @@ object JsonReaders {
for {
signature <- (jsValue \ "signature").validate[ECDigitalSignature]
features <- (jsValue \ "features").validate[Features]
timestamp <- (jsValue \ "timestamp")
timestamp <- (jsValue \ "timestamp" \ "unix")
.validate[Instant](instantReadsSeconds)
nodeId <- (jsValue \ "nodeId").validate[NodeId]
rgbColor <- (jsValue \ "rgbColor").validate[String]
@ -1021,7 +1024,7 @@ object JsonReaders {
signature <- (jsValue \ "signature").validate[ECDigitalSignature]
chainHash <- (jsValue \ "chainHash").validate[DoubleSha256Digest]
shortChannelId <- (jsValue \ "shortChannelId").validate[ShortChannelId]
timestamp <- (jsValue \ "timestamp")
timestamp <- (jsValue \ "timestamp" \ "unix")
.validate[Instant](instantReadsSeconds)
channelFlags <- (jsValue \ "channelFlags").validate[ChannelFlags]
cltvExpiryDelta <- (jsValue \ "cltvExpiryDelta").validate[Int]
@ -1080,7 +1083,13 @@ object JsonReaders {
}
implicit val paymentReceivedReads: Reads[IncomingPaymentStatus.Received] =
Json.reads[IncomingPaymentStatus.Received]
Reads { js =>
for {
amount <- (js \ "amount").validate[MilliSatoshis]
receivedAt <- (js \ "receivedAt" \ "unix")
.validate[Instant](instantReadsMilliseconds)
} yield IncomingPaymentStatus.Received(amount, receivedAt)
}
implicit val hopReads: Reads[Hop] =
Json.reads[Hop]
@ -1091,7 +1100,7 @@ object JsonReaders {
preimage <- (js \ "paymentPreimage").validate[PaymentPreimage]
feesPaid <- (js \ "feesPaid").validate[MilliSatoshis]
route <- (js \ "route").validate[Seq[Hop]]
completed <- (js \ "completedAt")
completed <- (js \ "completedAt" \ "unix")
.validate[Instant](instantReadsMilliseconds)
} yield OutgoingPaymentStatus.Succeeded(paymentPreimage = preimage,
feesPaid = feesPaid,
@ -1152,17 +1161,23 @@ object JsonReaders {
serialized <- (js \ "serialized").validate[String]
description <- (js \ "serialized").validate[String]
paymentHash <- (js \ "paymentHash").validate[Sha256Digest]
paymentMetadata <- (js \ "paymentMetadata").validate[String]
expiry <- (js \ "expiry")
.validate[FiniteDuration](finiteDurationReadsSeconds)
amount <- (js \ "amount").validateOpt[MilliSatoshis]
minFinalCltvExpiry <- (js \ "minFinalCltvExpiry").validate[Int]
features <- (js \ "features").validate[Features]
} yield PaymentRequest(prefix,
timestamp,
nodeId,
serialized,
description,
paymentHash,
paymentMetadata,
expiry,
amount)
minFinalCltvExpiry,
amount,
features)
}
implicit val paymentSucceededReads: Reads[OutgoingPayment] = Reads { js =>
@ -1175,7 +1190,7 @@ object JsonReaders {
amount <- (js \ "amount").validate[MilliSatoshis]
recipientAmount <- (js \ "recipientAmount").validate[MilliSatoshis]
recipientNodeId <- (js \ "recipientNodeId").validate[NodeId]
createdAt <- (js \ "createdAt")
createdAt <- (js \ "createdAt" \ "unix")
.validate[Instant](instantReadsMilliseconds)
paymentRequest <- (js \ "paymentRequest").validateOpt[PaymentRequest]
status <- (js \ "status").validate[OutgoingPaymentStatus]
@ -1197,11 +1212,13 @@ object JsonReaders {
for {
paymentRequest <- (js \ "paymentRequest").validate[PaymentRequest]
paymentPreimage <- (js \ "paymentPreimage").validate[PaymentPreimage]
createdAt <- (js \ "createdAt")
paymentType <- (js \ "paymentType").validate[PaymentType]
createdAt <- (js \ "createdAt" \ "unix")
.validate[Instant](instantReadsMilliseconds)
status <- (js \ "status").validate[IncomingPaymentStatus]
} yield IncomingPayment(paymentRequest,
paymentPreimage,
paymentType,
createdAt,
status)
}
@ -1243,7 +1260,7 @@ object JsonReaders {
for {
amount <- (js \ "amount").validate[MilliSatoshis]
fromChannelId <- (js \ "fromChannelId").validate[FundedChannelId]
timestamp <- (js \ "timestamp")
timestamp <- (js \ "timestamp" \ "unix")
.validate[Instant](instantReadsMilliseconds)
} yield ReceivedPayment.Part(amount, fromChannelId, timestamp)
}
@ -1257,7 +1274,7 @@ object JsonReaders {
amount <- (js \ "amount").validate[MilliSatoshis]
feesPaid <- (js \ "feesPaid").validate[MilliSatoshis]
toChannelId <- (js \ "toChannelId").validate[FundedChannelId]
timestamp <- (js \ "timestamp")
timestamp <- (js \ "timestamp" \ "unix")
.validate[Instant](instantReadsMilliseconds)
} yield SentPayment.Part(id, amount, feesPaid, toChannelId, timestamp)
}
@ -1271,7 +1288,7 @@ object JsonReaders {
paymentHash <- (js \ "paymentHash").validate[Sha256Digest]
fromChannelId <- (js \ "fromChannelId").validate[FundedChannelId]
toChannelId <- (js \ "toChannelId").validate[FundedChannelId]
timestamp <- (js \ "timestamp")
timestamp <- (js \ "timestamp" \ "unix")
.validate[Instant](instantReadsMilliseconds)
} yield RelayedPayment(amountIn,
amountOut,
@ -1289,7 +1306,7 @@ object JsonReaders {
txId <- (js \ "txId").validate[DoubleSha256DigestBE]
fee <- (js \ "fee").validate[Satoshis]
txType <- (js \ "txType").validate[String]
timestamp <- (js \ "timestamp")
timestamp <- (js \ "timestamp" \ "unix")
.validate[Instant](instantReadsMilliseconds)
} yield NetworkFeesResult(remoteNodeId,
channelId,
@ -1318,7 +1335,7 @@ object JsonReaders {
paymentHash <- (js \ "paymentHash").validate[Sha256Digest]
fromChannelId <- (js \ "fromChannelId").validate[FundedChannelId]
toChannelId <- (js \ "toChannelId").validate[FundedChannelId]
timestamp <- (js \ "timestamp")
timestamp <- (js \ "timestamp" \ "unix")
.validate[Instant](instantReadsMilliseconds)
} yield WebSocketEvent.PaymentRelayed(amountIn,
amountOut,
@ -1333,7 +1350,7 @@ object JsonReaders {
for {
amount <- (js \ "amount").validate[MilliSatoshis]
fromChannelId <- (js \ "fromChannelId").validate[FundedChannelId]
timestamp <- (js \ "timestamp")
timestamp <- (js \ "timestamp" \ "unix")
.validate[Instant](instantReadsMilliseconds)
} yield WebSocketEvent.PaymentReceived.Part(amount,
fromChannelId,
@ -1355,7 +1372,7 @@ object JsonReaders {
id <- (js \ "id").validate[PaymentId]
paymentHash <- (js \ "paymentHash").validate[Sha256Digest]
failures <- (js \ "failures").validate[Vector[JsObject]]
timestamp <- (js \ "timestamp")
timestamp <- (js \ "timestamp" \ "unix")
.validate[Instant](instantReadsMilliseconds)
} yield WebSocketEvent.PaymentFailed(id,
paymentHash,
@ -1370,7 +1387,7 @@ object JsonReaders {
amount <- (js \ "amount").validate[MilliSatoshis]
feesPaid <- (js \ "feesPaid").validate[MilliSatoshis]
toChannelId <- (js \ "toChannelId").validate[FundedChannelId]
timestamp <- (js \ "timestamp")
timestamp <- (js \ "timestamp" \ "unix")
.validate[Instant](instantReadsMilliseconds)
} yield WebSocketEvent.PaymentSent.Part(id,
amount,
@ -1398,7 +1415,7 @@ object JsonReaders {
for {
amount <- (js \ "amount").validate[MilliSatoshis]
paymentHash <- (js \ "paymentHash").validate[Sha256Digest]
timestamp <- (js \ "timestamp")
timestamp <- (js \ "timestamp" \ "unix")
.validate[Instant](instantReadsMilliseconds)
} yield WebSocketEvent.PaymentSettlingOnchain(amount,
paymentHash,
@ -1475,4 +1492,28 @@ object JsonReaders {
}
}
implicit val routeReads: Reads[Route] = {
Reads { js =>
for {
amount <- (js \ "amount").validate[MilliSatoshis]
nodeIds <- (js \ "nodeIds").validateOpt[Vector[NodeId]]
shortChannelIds <- (js \ "shortChannelIds")
.validateOpt[Vector[ShortChannelId]]
} yield {
if (shortChannelIds.nonEmpty) {
ChannelRoute(amount, shortChannelIds.get)
} else {
NodeRoute(amount, nodeIds.getOrElse(Vector.empty))
}
}
}
}
implicit val findRouteResultReads: Reads[Vector[Route]] = {
Reads { js =>
(js \ "routes").validate[JsArray].map(_.value.toVector.map(_.as[Route]))
}
}
}

View file

@ -1,5 +1,8 @@
package org.bitcoins.core.protocol.ln.node
import org.bitcoins.core.protocol.ln.node.FeatureSupport.{Mandatory, Optional}
import scodec.bits.{BitVector, ByteVector}
sealed trait FeatureSupport
object FeatureSupport {
@ -18,111 +21,321 @@ sealed trait Feature {
def mandatory: Int
def optional: Int = mandatory + 1
def supportBit(support: FeatureSupport): Int =
support match {
case FeatureSupport.Mandatory => mandatory
case FeatureSupport.Optional => optional
}
def supportBit(support: FeatureSupport): Int = support match {
case Mandatory => mandatory
case Optional => optional
}
override def toString = rfcName
}
object Feature {
/** Feature scope as defined in Bolt 9. */
/** Feature that should be advertised in init messages. */
trait InitFeature extends Feature
case object OptionDataLossProtect extends Feature {
/** Feature that should be advertised in node announcements. */
trait NodeFeature extends Feature
/** Feature that should be advertised in invoices. */
trait InvoiceFeature extends Feature
// @formatter:on
case class UnknownFeature(bitIndex: Int)
case class Features[T <: Feature](
activated: Map[T, FeatureSupport],
unknown: Set[UnknownFeature] = Set.empty) {
def isEmpty: Boolean = activated.isEmpty && unknown.isEmpty
def hasFeature(feature: T, support: Option[FeatureSupport] = None): Boolean =
support match {
case Some(s) => activated.get(feature).contains(s)
case None => activated.contains(feature)
}
def hasPluginFeature(feature: UnknownFeature): Boolean =
unknown.contains(feature)
/** NB: this method is not reflexive, see [[Features.areCompatible]] if you want symmetric validation. */
def areSupported(remoteFeatures: Features[T]): Boolean = {
// we allow unknown odd features (it's ok to be odd)
val unknownFeaturesOk = remoteFeatures.unknown.forall(_.bitIndex % 2 == 1)
// we verify that we activated every mandatory feature they require
val knownFeaturesOk = remoteFeatures.activated.forall {
case (_, Optional) => true
case (feature, Mandatory) => hasFeature(feature)
}
unknownFeaturesOk && knownFeaturesOk
}
def initFeatures(): Features[InitFeature] =
Features(activated.collect { case (f: InitFeature, s) => (f, s) }, unknown)
def nodeAnnouncementFeatures(): Features[NodeFeature] =
Features(activated.collect { case (f: NodeFeature, s) => (f, s) }, unknown)
def invoiceFeatures(): Features[InvoiceFeature] = Features(
activated.collect { case (f: InvoiceFeature, s) => (f, s) },
unknown)
def unscoped(): Features[Feature] = Features[Feature](
activated.collect { case (f, s) => (f: Feature, s) },
unknown)
def toByteVector: ByteVector = {
val activatedFeatureBytes = toByteVectorFromIndex(activated.map {
case (feature, support) => feature.supportBit(support)
}.toSet)
val unknownFeatureBytes = toByteVectorFromIndex(unknown.map(_.bitIndex))
val maxSize = activatedFeatureBytes.size.max(unknownFeatureBytes.size)
activatedFeatureBytes.padLeft(maxSize) | unknownFeatureBytes.padLeft(
maxSize)
}
private def toByteVectorFromIndex(indexes: Set[Int]): ByteVector = {
if (indexes.isEmpty) return ByteVector.empty
// When converting from BitVector to ByteVector, scodec pads right instead of left, so we make sure we pad to bytes *before* setting feature bits.
var buf = BitVector.fill(indexes.max + 1)(high = false).bytes.bits
indexes.foreach { i => buf = buf.set(i) }
buf.reverse.bytes
}
override def toString: String = {
val a = activated
.map { case (feature, support) => feature.rfcName + ":" + support }
.mkString(",")
val u = unknown.map(_.bitIndex).mkString(",")
s"$a" + (if (unknown.nonEmpty) s" (unknown=$u)" else "")
}
}
object Features {
def empty[T <: Feature]: Features[T] =
Features[T](Map.empty[T, FeatureSupport])
def apply[T <: Feature](features: (T, FeatureSupport)*): Features[T] =
Features[T](features.toMap)
def apply(bytes: ByteVector): Features[Feature] = apply(bytes.bits)
def apply(bits: BitVector): Features[Feature] = {
val all = bits.toIndexedSeq.reverse.zipWithIndex.collect {
case (true, idx) if knownFeatures.exists(_.optional == idx) =>
Right((knownFeatures.find(_.optional == idx).get, Optional))
case (true, idx) if knownFeatures.exists(_.mandatory == idx) =>
Right((knownFeatures.find(_.mandatory == idx).get, Mandatory))
case (true, idx) => Left(UnknownFeature(idx))
}
Features[Feature](
activated = all.collect { case Right((feature, support)) =>
feature -> support
}.toMap,
unknown = all.collect { case Left(inf) => inf }.toSet
)
}
case object DataLossProtect
extends Feature
with InitFeature
with NodeFeature {
val rfcName = "option_data_loss_protect"
val mandatory = 0
}
case object InitialRoutingSync extends Feature {
case object InitialRoutingSync extends Feature with InitFeature {
val rfcName = "initial_routing_sync"
// reserved but not used as per lightningnetwork/lightning-rfc/pull/178
val mandatory = 2
}
case object OptionUpfrontShutdownScript extends Feature {
case object UpfrontShutdownScript
extends Feature
with InitFeature
with NodeFeature {
val rfcName = "option_upfront_shutdown_script"
val mandatory = 4
}
case object ChannelRangeQueries extends Feature {
case object ChannelRangeQueries
extends Feature
with InitFeature
with NodeFeature {
val rfcName = "gossip_queries"
val mandatory = 6
}
case object VariableLengthOnion extends Feature {
case object VariableLengthOnion
extends Feature
with InitFeature
with NodeFeature
with InvoiceFeature {
val rfcName = "var_onion_optin"
val mandatory = 8
}
case object ChannelRangeQueriesExtended extends Feature {
case object ChannelRangeQueriesExtended
extends Feature
with InitFeature
with NodeFeature {
val rfcName = "gossip_queries_ex"
val mandatory = 10
}
case object StaticRemoteKey extends Feature {
case object StaticRemoteKey
extends Feature
with InitFeature
with NodeFeature {
val rfcName = "option_static_remotekey"
val mandatory = 12
}
case object PaymentSecret extends Feature {
case object PaymentSecret
extends Feature
with InitFeature
with NodeFeature
with InvoiceFeature {
val rfcName = "payment_secret"
val mandatory = 14
}
case object BasicMultiPartPayment extends Feature {
case object BasicMultiPartPayment
extends Feature
with InitFeature
with NodeFeature
with InvoiceFeature {
val rfcName = "basic_mpp"
val mandatory = 16
}
case object Wumbo extends Feature {
case object Wumbo extends Feature with InitFeature with NodeFeature {
val rfcName = "option_support_large_channel"
val mandatory = 18
}
case object AnchorOutputs extends Feature {
case object AnchorOutputs extends Feature with InitFeature with NodeFeature {
val rfcName = "option_anchor_outputs"
val mandatory = 20
}
case object AnchorOutputsZeroFeeHtlcTx extends Feature {
case object AnchorOutputsZeroFeeHtlcTx
extends Feature
with InitFeature
with NodeFeature {
val rfcName = "option_anchors_zero_fee_htlc_tx"
val mandatory = 22
}
case object ShutdownAnySegwit extends Feature {
case object ShutdownAnySegwit
extends Feature
with InitFeature
with NodeFeature {
val rfcName = "option_shutdown_anysegwit"
val mandatory = 26
}
case object DualFunding extends Feature with InitFeature with NodeFeature {
val rfcName = "option_dual_fund"
val mandatory = 28
}
case object OnionMessages extends Feature with InitFeature with NodeFeature {
val rfcName = "option_onion_messages"
val mandatory = 38
}
case object ChannelType extends Feature with InitFeature with NodeFeature {
val rfcName = "option_channel_type"
val mandatory = 44
}
case object PaymentMetadata extends Feature with InvoiceFeature {
val rfcName = "option_payment_metadata"
val mandatory = 48
}
case object KeySend extends Feature with NodeFeature {
val rfcName = "keysend"
val mandatory = 54
}
// TODO: @t-bast: update feature bits once spec-ed (currently reserved here: https://github.com/lightningnetwork/lightning-rfc/issues/605)
// We're not advertising these bits yet in our announcements, clients have to assume support.
// This is why we haven't added them yet to `areSupported`.
case object TrampolinePayment extends Feature {
val rfcName = "trampoline_payment"
val mandatory = 50
}
case object KeySend extends Feature {
val rfcName = "keysend"
val mandatory = 54
// The version of trampoline enabled by this feature bit does not match the latest spec PR: once the spec is accepted,
// we will introduce a new version of trampoline that will work in parallel to this legacy one, until we can safely
// deprecate it.
case object TrampolinePaymentPrototype
extends Feature
with InitFeature
with NodeFeature
with InvoiceFeature {
val rfcName = "trampoline_payment_prototype"
val mandatory = 148
}
val knownFeatures: Set[Feature] = Set(
OptionDataLossProtect,
DataLossProtect,
InitialRoutingSync,
OptionUpfrontShutdownScript,
UpfrontShutdownScript,
ChannelRangeQueries,
VariableLengthOnion,
ChannelRangeQueriesExtended,
PaymentSecret,
BasicMultiPartPayment,
Wumbo,
TrampolinePayment,
StaticRemoteKey,
AnchorOutputs,
AnchorOutputsZeroFeeHtlcTx,
ShutdownAnySegwit,
DualFunding,
OnionMessages,
ChannelType,
PaymentMetadata,
TrampolinePaymentPrototype,
KeySend
)
// Features may depend on other features, as specified in Bolt 9.
private val featuresDependency = Map(
ChannelRangeQueriesExtended -> (ChannelRangeQueries :: Nil),
PaymentSecret -> (VariableLengthOnion :: Nil),
BasicMultiPartPayment -> (PaymentSecret :: Nil),
AnchorOutputs -> (StaticRemoteKey :: Nil),
AnchorOutputsZeroFeeHtlcTx -> (StaticRemoteKey :: Nil),
DualFunding -> (AnchorOutputsZeroFeeHtlcTx :: Nil),
TrampolinePaymentPrototype -> (PaymentSecret :: Nil),
KeySend -> (VariableLengthOnion :: Nil)
)
case class FeatureException(message: String)
extends IllegalArgumentException(message)
def validateFeatureGraph[T <: Feature](
features: Features[T]): Option[FeatureException] =
featuresDependency.collectFirst {
case (feature, dependencies)
if features.unscoped().hasFeature(feature) && dependencies.exists(d =>
!features.unscoped().hasFeature(d)) =>
FeatureException(
s"$feature is set but is missing a dependency (${dependencies
.filter(d => !features.unscoped().hasFeature(d))
.mkString(" and ")})")
}
/** Returns true if both feature sets are compatible. */
def areCompatible[T <: Feature](
ours: Features[T],
theirs: Features[T]): Boolean =
ours.areSupported(theirs) && theirs.areSupported(ours)
/** returns true if both have at least optional support */
def canUseFeature[T <: Feature](
localFeatures: Features[T],
remoteFeatures: Features[T],
feature: T): Boolean = {
localFeatures.hasFeature(feature) && remoteFeatures.hasFeature(feature)
}
}

View file

@ -1,12 +1,21 @@
package org.bitcoins.core.protocol.ln.routing
import org.bitcoins.core.protocol.ln.channel.ShortChannelId
import org.bitcoins.core.protocol.ln.currency.MilliSatoshis
import org.bitcoins.core.protocol.ln.node.NodeId
/** Represent differet types of LN routes. Supports node and channel routes.
*/
sealed trait Route
sealed trait Route {
val amount: MilliSatoshis
}
case class NodeRoute(ids: Vector[NodeId]) extends Route
case class NodeRoute(
override val amount: MilliSatoshis,
nodeIds: Vector[NodeId])
extends Route
case class ChannelRoute(ids: Vector[ShortChannelId]) extends Route
case class ChannelRoute(
override val amount: MilliSatoshis,
shortChannelIds: Vector[ShortChannelId])
extends Route

View file

@ -14,6 +14,7 @@ import org.bitcoins.core.protocol.ln.channel.{
}
import org.bitcoins.core.protocol.ln.currency._
import org.bitcoins.core.protocol.ln.node.NodeId
import org.bitcoins.core.protocol.ln.routing.NodeRoute
import org.bitcoins.core.protocol.ln.{
LnHumanReadablePart,
LnInvoice,
@ -168,9 +169,9 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
info.publicAddresses
.map(_.getHostString)
.exists(_.endsWith(".onion")))
route <- client1.findRoute(invoice, None)
routes <- client1.findRoute(invoice, None)
} yield {
route.ids.size == 4
routes.head.asInstanceOf[NodeRoute].nodeIds.size == 4
}).recover {
case err: RuntimeException
if err.getMessage.contains("route not found") =>
@ -189,7 +190,7 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
.flatMap(_.getInfo)
.flatMap(info =>
firstClientF.flatMap(_.findRoute(info.nodeId, MilliSatoshis(100))))
.map(route => route.ids.length == 4)
.map(route => route.head.asInstanceOf[NodeRoute].nodeIds.length == 4)
.recover {
case err: RuntimeException
if err.getMessage.contains("route not found") =>
@ -416,6 +417,7 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
maxTries = 60)
} yield succeed
}
it should "be able to open and close a channel" in {
val changeAddrF = bitcoindRpcClientF.flatMap(_.getNewAddress)
@ -567,7 +569,7 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
invoice <- otherClient.createInvoice("foo", amt, preimage)
route <- client.findRoute(otherClientNodeId, amt)
result <- client.sendToRoute(invoice,
route,
route.head,
amt,
invoice.lnTags.paymentHash.hash,
finalCltvExpiry = 144,
@ -582,7 +584,7 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
invoice.lnTags.paymentHash.hash)
_ <- EclairRpcTestUtil.awaitUntilPaymentSucceeded(client,
result.parentId)
succeeded <- client.getSentInfo(invoice.lnTags.paymentHash.hash)
succeeded <- client.getSentInfo(result.parentId)
} yield {
assert(otherClientNodeId == invoice.nodeId)
assert(succeeded.nonEmpty)

View file

@ -19,8 +19,8 @@ TaskKeys.downloadEclair := {
Files.createDirectories(binaryDir)
}
val version = "0.6.2"
val commit = "6817d6f"
val version = "0.7.0"
val commit = "a804905"
logger.debug(s"(Maybe) downloading Eclair binaries for version: $version")
@ -48,7 +48,7 @@ TaskKeys.downloadEclair := {
.mkString
val expectedHash =
"e2407173036d9e2176c129f2328018c543c732a96d1f05c0fb35864c15efc9ba"
"482a00cc597fd4cc471a1b4035c72a440a9ab336ef5b9006629d2fd717b223b4"
if (hash.equalsIgnoreCase(expectedHash)) {
logger.info(s"Download complete and verified, unzipping result")

View file

@ -9,7 +9,7 @@ import org.bitcoins.core.protocol.ln.channel.{
}
import org.bitcoins.core.protocol.ln.currency.MilliSatoshis
import org.bitcoins.core.protocol.ln.node.{NodeId, NodeUri}
import org.bitcoins.core.protocol.ln.routing.{NodeRoute, Route}
import org.bitcoins.core.protocol.ln.routing.Route
import org.bitcoins.core.protocol.ln.{LnInvoice, LnParams, PaymentPreimage}
import org.bitcoins.core.protocol.script.ScriptPubKey
import org.bitcoins.core.protocol.{Address, BitcoinAddress}
@ -72,13 +72,15 @@ trait EclairApi {
def close(id: ChannelId, spk: ScriptPubKey): Future[ChannelCommandResult]
def findRoute(nodeId: NodeId, amountMsat: MilliSatoshis): Future[NodeRoute]
def findRoute(
nodeId: NodeId,
amountMsat: MilliSatoshis): Future[Vector[Route]]
def findRoute(invoice: LnInvoice): Future[NodeRoute]
def findRoute(invoice: LnInvoice): Future[Vector[Route]]
def findRoute(
invoice: LnInvoice,
amountMsat: MilliSatoshis): Future[NodeRoute]
amountMsat: MilliSatoshis): Future[Vector[Route]]
def forceClose(channelId: ChannelId): Future[ChannelCommandResult]

View file

@ -143,30 +143,29 @@ class EclairRpcClient(
override def findRoute(
nodeId: NodeId,
amountMsat: MilliSatoshis): Future[NodeRoute] = {
eclairCall[Vector[NodeId]](
"findroutetonode",
"nodeId" -> nodeId.toString,
"amountMsat" -> amountMsat.toBigDecimal.toString).map(NodeRoute.apply)
amountMsat: MilliSatoshis): Future[Vector[Route]] = {
eclairCall[Vector[Route]]("findroutetonode",
"nodeId" -> nodeId.toString,
"amountMsat" -> amountMsat.toBigDecimal.toString)
}
override def findRoute(invoice: LnInvoice): Future[NodeRoute] = {
override def findRoute(invoice: LnInvoice): Future[Vector[Route]] = {
findRoute(invoice, None)
}
override def findRoute(
invoice: LnInvoice,
amount: MilliSatoshis): Future[NodeRoute] = {
amount: MilliSatoshis): Future[Vector[Route]] = {
findRoute(invoice, Some(amount))
}
def findRoute(
invoice: LnInvoice,
amountMsat: Option[MilliSatoshis]): Future[NodeRoute] = {
amountMsat: Option[MilliSatoshis]): Future[Vector[Route]] = {
val params = Seq(
Some("invoice" -> invoice.toString),
amountMsat.map(x => "amountMsat" -> x.toBigDecimal.toString)).flatten
eclairCall[Vector[NodeId]]("findroute", params: _*).map(NodeRoute.apply)
eclairCall[Vector[Route]]("findroute", params: _*)
}
override def forceClose(
@ -377,8 +376,8 @@ class EclairRpcClient(
//register callback that publishes a payment to our actor system's
//event stream,
receivedInfoF.foreach {
case None |
Some(IncomingPayment(_, _, _, IncomingPaymentStatus.Pending)) =>
case None | Some(
IncomingPayment(_, _, _, _, IncomingPaymentStatus.Pending)) =>
if (attempts.incrementAndGet() >= maxAttempts) {
// too many tries to get info about a payment
// either Eclair is down or the payment is still in PENDING state for some reason
@ -513,8 +512,8 @@ class EclairRpcClient(
parentId: Option[PaymentId],
externalId: Option[String]): Future[SendToRouteResult] = {
val ids = route match {
case NodeRoute(ids) => "nodeIds" -> ids.mkString(",")
case ChannelRoute(ids) => "shortChannelIds" -> ids.mkString(",")
case NodeRoute(_, ids) => "nodeIds" -> ids.mkString(",")
case ChannelRoute(_, ids) => "shortChannelIds" -> ids.mkString(",")
}
val params = Seq(
"invoice" -> invoice.toString,
@ -954,10 +953,10 @@ object EclairRpcClient {
implicit system: ActorSystem) = new EclairRpcClient(instance, binary)
/** The current commit we support of Eclair */
private[bitcoins] val commit = "6817d6f"
private[bitcoins] val commit = "a804905"
/** The current version we support of Eclair */
private[bitcoins] val version = "0.6.2"
private[bitcoins] val version = "0.7.0"
/** The bitcoind version that eclair is officially tested & supported with by ACINQ
* @see https://github.com/ACINQ/eclair/releases/tag/v0.6.2

View file

@ -110,16 +110,16 @@ trait EclairRpcTestUtil extends Logging {
"eclair.api.binding-ip" -> "127.0.0.1",
"eclair.api.password" -> "abc123",
"eclair.api.port" -> apiPort,
"eclair.mindepth-blocks" -> 2,
"eclair.max-htlc-value-in-flight-msat" -> 100000000000L,
"eclair.channel.mindepth-blocks" -> 2,
"eclair.channel.max-htlc-value-in-flight-msat" -> 100000000000L,
"eclair.router.broadcast-interval" -> "2 second",
"eclair.auto-reconnect" -> false,
"eclair.to-remote-delay-blocks" -> 144,
"eclair.channel.to-remote-delay-blocks" -> 144,
"eclair.db.regtest.url" -> "jdbc:sqlite:regtest/",
"eclair.max-payment-fee" -> 10, // avoid complaints about too high fees
"eclair.alias" -> "suredbits",
"eclair.fulfill-safety-before-timeout-blocks" -> 1,
"eclair.min-final-expiry-delta-blocks" -> 2,
"eclair.channel.fulfill-safety-before-timeout-blocks" -> 1,
"eclair.channel.min-final-expiry-delta-blocks" -> 2,
"eclair.features.keysend" -> "optional"
)
}