mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-03-23 23:49:16 +01:00
Upgrade Eclair to v0.7.0 (#4308)
* Upgrade Eclair to v0.7.0 * Scala 2.12 compatibility * reformat
This commit is contained in:
parent
ce00d3ac36
commit
3dc709386a
9 changed files with 359 additions and 87 deletions
|
@ -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)
|
||||
|
||||
|
|
|
@ -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]))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue