Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
Original file line number Diff line number Diff line change
Expand Up @@ -315,15 +315,20 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
for {
ignoredChannels <- getChannelDescs(ignoreShortChannelIds.toSet)
ignore = Ignore(ignoreNodeIds.toSet, ignoredChannels)
response <- (appKit.router ? RouteRequest(sourceNodeId, targetNodeId, amount, maxFee, extraEdges, ignore = ignore, routeParams = routeParams.copy(includeLocalChannelCost = includeLocalChannelCost))).mapTo[RouteResponse]
response <- (appKit.router ? RouteRequest(sourceNodeId, Seq(ClearRecipient(targetNodeId, randomBytes32(), None)), amount, maxFee, extraEdges, ignore = ignore, routeParams = routeParams.copy(includeLocalChannelCost = includeLocalChannelCost))).mapTo[RouteResponse]
} yield response
case Left(t) => Future.failed(t)
}
}

override def sendToRoute(amount: MilliSatoshi, recipientAmount_opt: Option[MilliSatoshi], externalId_opt: Option[String], parentId_opt: Option[UUID], invoice: Bolt11Invoice, route: PredefinedRoute, trampolineSecret_opt: Option[ByteVector32], trampolineFees_opt: Option[MilliSatoshi], trampolineExpiryDelta_opt: Option[CltvExpiryDelta], trampolineNodes_opt: Seq[PublicKey])(implicit timeout: Timeout): Future[SendPaymentToRouteResponse] = {
val recipientAmount = recipientAmount_opt.getOrElse(invoice.amount_opt.getOrElse(amount))
val sendPayment = SendPaymentToRoute(amount, recipientAmount, invoice, route, externalId_opt, parentId_opt, trampolineSecret_opt, trampolineFees_opt.getOrElse(0 msat), trampolineExpiryDelta_opt.getOrElse(CltvExpiryDelta(0)), trampolineNodes_opt)
val sendPayment =
if (trampolineNodes_opt.nonEmpty) {
SendTrampolinePaymentToRoute(amount, recipientAmount, invoice, route, externalId_opt, parentId_opt, trampolineSecret_opt, trampolineFees_opt.getOrElse(0 msat), trampolineExpiryDelta_opt.getOrElse(CltvExpiryDelta(0)), trampolineNodes_opt)
} else {
SendPaymentToRoute(amount, recipientAmount, invoice, route, externalId_opt, parentId_opt)
}
if (invoice.isExpired()) {
Future.failed(new IllegalArgumentException("invoice has expired"))
} else if (route.isEmpty) {
Expand Down Expand Up @@ -405,6 +410,7 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
case PendingPaymentToNode(_, r) => OutgoingPayment(paymentId, paymentId, r.externalId, paymentHash, paymentType, r.recipientAmount, r.recipientAmount, r.recipientNodeId, TimestampMilli.now(), Some(r.invoice), OutgoingPaymentStatus.Pending)
case PendingPaymentToRoute(_, r) => OutgoingPayment(paymentId, paymentId, r.externalId, paymentHash, paymentType, r.recipientAmount, r.recipientAmount, r.recipientNodeId, TimestampMilli.now(), Some(r.invoice), OutgoingPaymentStatus.Pending)
case PendingTrampolinePayment(_, _, r) => OutgoingPayment(paymentId, paymentId, None, paymentHash, paymentType, r.recipientAmount, r.recipientAmount, r.recipientNodeId, TimestampMilli.now(), Some(r.invoice), OutgoingPaymentStatus.Pending)
case PendingTrampolinePaymentToRoute(_, r) => OutgoingPayment(paymentId, paymentId, r.externalId, paymentHash, paymentType, r.recipientAmount, r.recipientAmount, r.recipientNodeId, TimestampMilli.now(), Some(r.invoice), OutgoingPaymentStatus.Pending)
}
dummyOutgoingPayment +: outgoingDbPayments
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,9 +246,9 @@ object FailureType extends Enumeration {

object FailureSummary {
def apply(f: PaymentFailure): FailureSummary = f match {
case LocalFailure(_, route, t) => FailureSummary(FailureType.LOCAL, t.getMessage, route.map(h => HopSummary(h)).toList, route.headOption.map(_.nodeId))
case RemoteFailure(_, route, e) => FailureSummary(FailureType.REMOTE, e.failureMessage.message, route.map(h => HopSummary(h)).toList, Some(e.originNode))
case UnreadableRemoteFailure(_, route) => FailureSummary(FailureType.UNREADABLE_REMOTE, "could not decrypt failure onion", route.map(h => HopSummary(h)).toList, None)
case LocalFailure(_, route, t) => FailureSummary(FailureType.LOCAL, t.getMessage, route.hops.map(h => HopSummary(h)).toList, route.hops.headOption.map(_.nodeId))
case RemoteFailure(_, route, e) => FailureSummary(FailureType.REMOTE, e.failureMessage.message, route.hops.map(h => HopSummary(h)).toList, Some(e.originNode))
case UnreadableRemoteFailure(_, route) => FailureSummary(FailureType.UNREADABLE_REMOTE, "could not decrypt failure onion", route.hops.map(h => HopSummary(h)).toList, None)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ class PgPaymentsDb(implicit ds: DataSource, lock: PgLock) extends PaymentsDb wit
statement.setTimestamp(1, p.timestamp.toSqlTimestamp)
statement.setString(2, paymentResult.paymentPreimage.toHex)
statement.setLong(3, p.feesPaid.toLong)
statement.setBytes(4, encodeRoute(p.route.getOrElse(Nil).map(h => HopSummary(h)).toList))
statement.setBytes(4, encodeRoute(p.route.map(_.hops).getOrElse(Nil).map(h => HopSummary(h)).toList))
statement.setString(5, p.id.toString)
statement.addBatch()
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ class SqlitePaymentsDb(val sqlite: Connection) extends PaymentsDb with Logging {
statement.setLong(1, p.timestamp.toLong)
statement.setBytes(2, paymentResult.paymentPreimage.toArray)
statement.setLong(3, p.feesPaid.toLong)
statement.setBytes(4, encodeRoute(p.route.getOrElse(Nil).map(h => HopSummary(h)).toList))
statement.setBytes(4, encodeRoute(p.route.map(_.hops).getOrElse(Nil).map(h => HopSummary(h)).toList))
statement.setString(5, p.id.toString)
statement.addBatch()
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,32 +295,32 @@ object ColorSerializer extends MinimalSerializer({

// @formatter:off
private case class ChannelHopJson(nodeId: PublicKey, nextNodeId: PublicKey, source: ChannelRelayParams)
private case class RouteFullJson(amount: MilliSatoshi, hops: Seq[ChannelHopJson])
object RouteFullSerializer extends ConvertClassSerializer[Route](route => RouteFullJson(route.amount, route.hops.map(h => ChannelHopJson(h.nodeId, h.nextNodeId, h.params))))
private case class RouteFullJson(amount: MilliSatoshi, clearHops: Seq[ChannelHopJson], recipient: Recipient)
object RouteFullSerializer extends ConvertClassSerializer[Route](route => RouteFullJson(route.amount, route.clearHops.map(h => ChannelHopJson(h.nodeId, h.nextNodeId, h.params)), route.recipient))

private case class RouteNodeIdsJson(amount: MilliSatoshi, nodeIds: Seq[PublicKey])
object RouteNodeIdsSerializer extends ConvertClassSerializer[Route](route => {
val nodeIds = route.hops match {
val nodeIds = route.clearHops match {
case rest :+ last => rest.map(_.nodeId) :+ last.nodeId :+ last.nextNodeId
case Nil => Nil
}
RouteNodeIdsJson(route.amount, nodeIds)
})

private case class RouteShortChannelIdsJson(amount: MilliSatoshi, shortChannelIds: Seq[ShortChannelId])
object RouteShortChannelIdsSerializer extends ConvertClassSerializer[Route](route => RouteShortChannelIdsJson(route.amount, route.hops.map(_.shortChannelId)))
object RouteShortChannelIdsSerializer extends ConvertClassSerializer[Route](route => RouteShortChannelIdsJson(route.amount, route.clearHops.map(_.shortChannelId)))
// @formatter:on

// @formatter:off
private case class PaymentFailureSummaryJson(amount: MilliSatoshi, route: Seq[PublicKey], message: String)
private case class PaymentFailedSummaryJson(paymentHash: ByteVector32, destination: PublicKey, totalAmount: MilliSatoshi, pathFindingExperiment: String, failures: Seq[PaymentFailureSummaryJson])
object PaymentFailedSummarySerializer extends ConvertClassSerializer[PaymentFailedSummary](p => PaymentFailedSummaryJson(
p.cfg.paymentHash,
p.cfg.recipientNodeId,
p.cfg.recipientNodeIds.head,
p.cfg.recipientAmount,
p.pathFindingExperiment,
p.paymentFailed.failures.map(f => {
val route = f.route.map(_.nodeId) ++ f.route.lastOption.map(_.nextNodeId)
val route = f.route.hops.map(_.nodeId) ++ f.route.hops.lastOption.map(_.nextNodeId)
val message = f match {
case LocalFailure(_, _, t) => t.getMessage
case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(origin, failureMessage)) => s"$origin returned: ${failureMessage.message}"
Expand Down Expand Up @@ -404,10 +404,10 @@ object InvoiceSerializer extends MinimalSerializer({
FeatureSupportSerializer +
UnknownFeatureSerializer
)),
JField("blindedPaths", JArray(p.blindedPaths.map(path => {
JField("blindedPaths", JArray(p.recipients.map(paymentRoute => {
JObject(List(
JField("introductionNodeId", JString(path.introductionNodeId.toString())),
JField("blindedNodeIds", JArray(path.blindedNodes.map(n => JString(n.blindedPublicKey.toString())).toList))
JField("introductionNodeId", JString(paymentRoute.route.introductionNodeId.toString())),
JField("blindedNodeIds", JArray(paymentRoute.route.blindedNodes.map(n => JString(n.blindedPublicKey.toString())).toList))
))
}).toList)),
JField("createdAt", JLong(p.createdAt.toLong)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ case class Bolt11Invoice(prefix: String, amount_opt: Option[MilliSatoshi], creat

amount_opt.foreach(a => require(a > 0.msat, s"amount is not valid"))
require(tags.collect { case _: Bolt11Invoice.PaymentHash => }.size == 1, "there must be exactly one payment hash tag")
require(tags.collect { case _: Bolt11Invoice.PaymentSecret => }.size == 1, "there must be exactly one payment secret tag")
require(tags.collect { case Bolt11Invoice.Description(_) | Bolt11Invoice.DescriptionHash(_) => }.size == 1, "there must be exactly one description tag or one description hash tag")

{
Expand All @@ -63,7 +64,7 @@ case class Bolt11Invoice(prefix: String, amount_opt: Option[MilliSatoshi], creat
/**
* @return the payment secret
*/
lazy val paymentSecret = tags.collectFirst { case p: Bolt11Invoice.PaymentSecret => p.secret }
lazy val paymentSecret = tags.collectFirst { case p: Bolt11Invoice.PaymentSecret => p.secret }.get

/**
* @return the description of the payment, or its hash
Expand All @@ -78,6 +79,9 @@ case class Bolt11Invoice(prefix: String, amount_opt: Option[MilliSatoshi], creat
*/
lazy val paymentMetadata: Option[ByteVector] = tags.collectFirst { case m: Bolt11Invoice.PaymentMetadata => m.data }

val recipient: ClearRecipient = ClearRecipient(nodeId, paymentSecret, paymentMetadata, features)
override val recipients: Seq[ClearRecipient] = Seq(recipient)

/**
* @return the fallback address if any. It could be a script address, pubkey address, ..
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import fr.acinq.bitcoin.Bech32
import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey
import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto}
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.crypto.Sphinx.RouteBlinding
import fr.acinq.eclair.wire.protocol.OfferTypes._
import fr.acinq.eclair.wire.protocol.OnionRoutingCodecs.{InvalidTlvPayload, MissingRequiredTlv}
import fr.acinq.eclair.wire.protocol.{OfferCodecs, OfferTypes, TlvStream}
Expand All @@ -41,19 +40,27 @@ case class Bolt12Invoice(records: TlvStream[InvoiceTlv]) extends Invoice {

val amount: MilliSatoshi = records.get[Amount].map(_.amount).get
override val amount_opt: Option[MilliSatoshi] = Some(amount)
override val nodeId: Crypto.PublicKey = records.get[NodeId].get.publicKey
val nodeId: Crypto.PublicKey = records.get[NodeId].get.publicKey
override val paymentHash: ByteVector32 = records.get[PaymentHash].get.hash
override val paymentSecret: Option[ByteVector32] = None
override val paymentMetadata: Option[ByteVector] = None
override val description: Either[String, ByteVector32] = Left(records.get[Description].get.description)
override val extraEdges: Seq[Invoice.ExtraEdge] = Seq.empty // TODO: the blinded paths need to be converted to graph edges
override val extraEdges: Seq[Invoice.ExtraEdge] = Seq.empty
override val createdAt: TimestampSecond = records.get[CreatedAt].get.timestamp
override val relativeExpiry: FiniteDuration = FiniteDuration(records.get[RelativeExpiry].map(_.seconds).getOrElse(DEFAULT_EXPIRY_SECONDS), TimeUnit.SECONDS)
override val minFinalCltvExpiryDelta: CltvExpiryDelta = records.get[Cltv].map(_.minFinalCltvExpiry).getOrElse(DEFAULT_MIN_FINAL_EXPIRY_DELTA)
override val features: Features[InvoiceFeature] = records.get[FeaturesTlv].map(_.features.invoiceFeatures()).getOrElse(Features.empty)
val chain: ByteVector32 = records.get[Chain].map(_.hash).getOrElse(Block.LivenetGenesisBlock.hash)
val offerId: Option[ByteVector32] = records.get[OfferId].map(_.offerId)
val blindedPaths: Seq[RouteBlinding.BlindedRoute] = records.get[Paths].get.paths
val recipients: Seq[BlindRecipient] = {
val routesAndInfos = records.get[Paths].get.paths.zip(records.get[PaymentPathsInfo].get.paymentInfo)
records.get[PaymentPathsCapacities] match {
case Some(PaymentPathsCapacities(capacities)) => routesAndInfos.zip(capacities).map {
case ((route, payInfo), capacity) => BlindRecipient(route, payInfo, Some(capacity), Nil, Nil)
}
case None => routesAndInfos.map {
case (route, payInfo) => BlindRecipient(route, payInfo, None, Nil, Nil)
}
}
}
val issuer: Option[String] = records.get[Issuer].map(_.issuer)
val quantity: Option[Long] = records.get[Quantity].map(_.quantity)
val refundFor: Option[ByteVector32] = records.get[RefundFor].map(_.refundedPaymentHash)
Expand Down Expand Up @@ -153,6 +160,7 @@ object Bolt12Invoice {
if (records.get[Description].isEmpty) return Left(MissingRequiredTlv(UInt64(10)))
if (records.get[Paths].isEmpty) return Left(MissingRequiredTlv(UInt64(16)))
if (records.get[PaymentPathsInfo].map(_.paymentInfo.length) != records.get[Paths].map(_.paths.length)) return Left(MissingRequiredTlv(UInt64(18)))
if (records.get[PaymentPathsCapacities].exists(_.capacities.length != records.get[Paths].get.paths.length)) return Left(MissingRequiredTlv(UInt64(19)))
if (records.get[NodeId].isEmpty) return Left(MissingRequiredTlv(UInt64(30)))
if (records.get[CreatedAt].isEmpty) return Left(MissingRequiredTlv(UInt64(40)))
if (records.get[PaymentHash].isEmpty) return Left(MissingRequiredTlv(UInt64(42)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.eclair.payment.relay.Relayer
import fr.acinq.eclair.wire.protocol.ChannelUpdate
import fr.acinq.eclair.{CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TimestampSecond}
import scodec.bits.ByteVector

import scala.concurrent.duration.FiniteDuration
import scala.util.Try
Expand All @@ -31,14 +30,10 @@ trait Invoice {

val createdAt: TimestampSecond

val nodeId: PublicKey
val recipients: Seq[Recipient]

val paymentHash: ByteVector32

val paymentSecret: Option[ByteVector32]

val paymentMetadata: Option[ByteVector]

val description: Either[String, ByteVector32]

val extraEdges: Seq[Invoice.ExtraEdge]
Expand Down
Loading