mirror of
https://github.com/ACINQ/eclair.git
synced 2025-03-04 09:58:02 +01:00
Trampoline/MPP API changes (#1297)
Let a sender manually split a payment and specify a trampoline route. Fix two flaky tests where the order of payment parts could be different, resulting in a failed equality test. If we're relaying multiple HTLCs for the same payment_hash, we need to list all of those. The previous code only handled that when Trampoline was used.
This commit is contained in:
parent
78e6cdbec9
commit
48ad9b30e6
10 changed files with 282 additions and 170 deletions
|
@ -33,7 +33,7 @@ import fr.acinq.eclair.io.{NodeURI, Peer}
|
||||||
import fr.acinq.eclair.payment._
|
import fr.acinq.eclair.payment._
|
||||||
import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment
|
import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment
|
||||||
import fr.acinq.eclair.payment.relay.Relayer.{GetOutgoingChannels, OutgoingChannels, UsableBalance}
|
import fr.acinq.eclair.payment.relay.Relayer.{GetOutgoingChannels, OutgoingChannels, UsableBalance}
|
||||||
import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentRequest, SendTrampolinePaymentRequest}
|
import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentRequest, SendPaymentToRouteRequest, SendPaymentToRouteResponse}
|
||||||
import fr.acinq.eclair.router._
|
import fr.acinq.eclair.router._
|
||||||
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAddress, NodeAnnouncement}
|
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAddress, NodeAnnouncement}
|
||||||
import scodec.bits.ByteVector
|
import scodec.bits.ByteVector
|
||||||
|
@ -88,13 +88,11 @@ trait Eclair {
|
||||||
|
|
||||||
def send(externalId_opt: Option[String], recipientNodeId: PublicKey, amount: MilliSatoshi, paymentHash: ByteVector32, invoice_opt: Option[PaymentRequest] = None, maxAttempts_opt: Option[Int] = None, feeThresholdSat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None)(implicit timeout: Timeout): Future[UUID]
|
def send(externalId_opt: Option[String], recipientNodeId: PublicKey, amount: MilliSatoshi, paymentHash: ByteVector32, invoice_opt: Option[PaymentRequest] = None, maxAttempts_opt: Option[Int] = None, feeThresholdSat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None)(implicit timeout: Timeout): Future[UUID]
|
||||||
|
|
||||||
def sendToTrampoline(invoice: PaymentRequest, trampolineId: PublicKey, trampolineFees: MilliSatoshi, trampolineExpiryDelta: CltvExpiryDelta)(implicit timeout: Timeout): Future[UUID]
|
|
||||||
|
|
||||||
def sentInfo(id: Either[UUID, ByteVector32])(implicit timeout: Timeout): Future[Seq[OutgoingPayment]]
|
def sentInfo(id: Either[UUID, ByteVector32])(implicit timeout: Timeout): Future[Seq[OutgoingPayment]]
|
||||||
|
|
||||||
def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty)(implicit timeout: Timeout): Future[RouteResponse]
|
def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty)(implicit timeout: Timeout): Future[RouteResponse]
|
||||||
|
|
||||||
def sendToRoute(externalId_opt: Option[String], route: Seq[PublicKey], amount: MilliSatoshi, paymentHash: ByteVector32, finalCltvExpiryDelta: CltvExpiryDelta, invoice_opt: Option[PaymentRequest] = None)(implicit timeout: Timeout): Future[UUID]
|
def sendToRoute(amount: MilliSatoshi, recipientAmount_opt: Option[MilliSatoshi], externalId_opt: Option[String], parentId_opt: Option[UUID], invoice: PaymentRequest, finalCltvExpiryDelta: CltvExpiryDelta, route: Seq[PublicKey], trampolineSecret_opt: Option[ByteVector32] = None, trampolineFees_opt: Option[MilliSatoshi] = None, trampolineExpiryDelta_opt: Option[CltvExpiryDelta] = None, trampolineNodes_opt: Seq[PublicKey] = Nil)(implicit timeout: Timeout): Future[SendPaymentToRouteResponse]
|
||||||
|
|
||||||
def audit(from_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[AuditResponse]
|
def audit(from_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[AuditResponse]
|
||||||
|
|
||||||
|
@ -214,10 +212,21 @@ class EclairImpl(appKit: Kit) extends Eclair {
|
||||||
(appKit.router ? RouteRequest(appKit.nodeParams.nodeId, targetNodeId, amount, assistedRoutes)).mapTo[RouteResponse]
|
(appKit.router ? RouteRequest(appKit.nodeParams.nodeId, targetNodeId, amount, assistedRoutes)).mapTo[RouteResponse]
|
||||||
}
|
}
|
||||||
|
|
||||||
override def sendToRoute(externalId_opt: Option[String], route: Seq[PublicKey], amount: MilliSatoshi, paymentHash: ByteVector32, finalCltvExpiryDelta: CltvExpiryDelta, invoice_opt: Option[PaymentRequest] = None)(implicit timeout: Timeout): Future[UUID] = {
|
override def sendToRoute(amount: MilliSatoshi, recipientAmount_opt: Option[MilliSatoshi], externalId_opt: Option[String], parentId_opt: Option[UUID], invoice: PaymentRequest, finalCltvExpiryDelta: CltvExpiryDelta, route: Seq[PublicKey], trampolineSecret_opt: Option[ByteVector32], trampolineFees_opt: Option[MilliSatoshi], trampolineExpiryDelta_opt: Option[CltvExpiryDelta], trampolineNodes_opt: Seq[PublicKey])(implicit timeout: Timeout): Future[SendPaymentToRouteResponse] = {
|
||||||
externalId_opt match {
|
val recipientAmount = recipientAmount_opt.getOrElse(invoice.amount.getOrElse(amount))
|
||||||
case Some(externalId) if externalId.length > externalIdMaxLength => Future.failed(new IllegalArgumentException("externalId is too long: cannot exceed 66 characters"))
|
val sendPayment = SendPaymentToRouteRequest(amount, recipientAmount, externalId_opt, parentId_opt, invoice, finalCltvExpiryDelta, route, trampolineSecret_opt, trampolineFees_opt.getOrElse(0 msat), trampolineExpiryDelta_opt.getOrElse(CltvExpiryDelta(0)), trampolineNodes_opt)
|
||||||
case _ => (appKit.paymentInitiator ? SendPaymentRequest(amount, paymentHash, route.last, 1, finalCltvExpiryDelta, invoice_opt, externalId_opt, route)).mapTo[UUID]
|
if (invoice.isExpired) {
|
||||||
|
Future.failed(new IllegalArgumentException("invoice has expired"))
|
||||||
|
} else if (route.isEmpty) {
|
||||||
|
Future.failed(new IllegalArgumentException("missing payment route"))
|
||||||
|
} else if (externalId_opt.exists(_.length > externalIdMaxLength)) {
|
||||||
|
Future.failed(new IllegalArgumentException(s"externalId is too long: cannot exceed $externalIdMaxLength characters"))
|
||||||
|
} else if (trampolineNodes_opt.nonEmpty && (trampolineFees_opt.isEmpty || trampolineExpiryDelta_opt.isEmpty)) {
|
||||||
|
Future.failed(new IllegalArgumentException("trampoline payments must specify a trampoline fee and cltv delta"))
|
||||||
|
} else if (trampolineNodes_opt.nonEmpty && trampolineNodes_opt.length != 2) {
|
||||||
|
Future.failed(new IllegalArgumentException("trampoline payments currently only support paying a trampoline node via a single other trampoline node"))
|
||||||
|
} else {
|
||||||
|
(appKit.paymentInitiator ? sendPayment).mapTo[SendPaymentToRouteResponse]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -230,7 +239,7 @@ class EclairImpl(appKit: Kit) extends Eclair {
|
||||||
)
|
)
|
||||||
|
|
||||||
externalId_opt match {
|
externalId_opt match {
|
||||||
case Some(externalId) if externalId.length > externalIdMaxLength => Future.failed(new IllegalArgumentException("externalId is too long: cannot exceed 66 characters"))
|
case Some(externalId) if externalId.length > externalIdMaxLength => Future.failed(new IllegalArgumentException(s"externalId is too long: cannot exceed $externalIdMaxLength characters"))
|
||||||
case _ => invoice_opt match {
|
case _ => invoice_opt match {
|
||||||
case Some(invoice) if invoice.isExpired => Future.failed(new IllegalArgumentException("invoice has expired"))
|
case Some(invoice) if invoice.isExpired => Future.failed(new IllegalArgumentException("invoice has expired"))
|
||||||
case Some(invoice) =>
|
case Some(invoice) =>
|
||||||
|
@ -246,12 +255,6 @@ class EclairImpl(appKit: Kit) extends Eclair {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override def sendToTrampoline(invoice: PaymentRequest, trampolineId: PublicKey, trampolineFees: MilliSatoshi, trampolineExpiryDelta: CltvExpiryDelta)(implicit timeout: Timeout): Future[UUID] = {
|
|
||||||
val defaultRouteParams = Router.getDefaultRouteParams(appKit.nodeParams.routerConf)
|
|
||||||
val sendPayment = SendTrampolinePaymentRequest(invoice.amount.get, invoice, trampolineId, Seq((trampolineFees, trampolineExpiryDelta)), invoice.minFinalCltvExpiryDelta.getOrElse(Channel.MIN_CLTV_EXPIRY_DELTA), Some(defaultRouteParams))
|
|
||||||
(appKit.paymentInitiator ? sendPayment).mapTo[UUID]
|
|
||||||
}
|
|
||||||
|
|
||||||
override def sentInfo(id: Either[UUID, ByteVector32])(implicit timeout: Timeout): Future[Seq[OutgoingPayment]] = Future {
|
override def sentInfo(id: Either[UUID, ByteVector32])(implicit timeout: Timeout): Future[Seq[OutgoingPayment]] = Future {
|
||||||
id match {
|
id match {
|
||||||
case Left(uuid) => appKit.nodeParams.db.payments.listOutgoingPayments(uuid)
|
case Left(uuid) => appKit.nodeParams.db.payments.listOutgoingPayments(uuid)
|
||||||
|
|
|
@ -278,17 +278,19 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging {
|
||||||
rs.getLong("timestamp"))
|
rs.getLong("timestamp"))
|
||||||
relayedByHash = relayedByHash + (paymentHash -> (relayedByHash.getOrElse(paymentHash, Nil) :+ part))
|
relayedByHash = relayedByHash + (paymentHash -> (relayedByHash.getOrElse(paymentHash, Nil) :+ part))
|
||||||
}
|
}
|
||||||
relayedByHash.map {
|
relayedByHash.flatMap {
|
||||||
case (paymentHash, parts) => parts.head.relayType match {
|
case (paymentHash, parts) =>
|
||||||
case "channel" => parts.foldLeft(ChannelPaymentRelayed(0 msat, 0 msat, paymentHash, ByteVector32.Zeroes, ByteVector32.Zeroes, parts.head.timestamp)) {
|
// We may have been routing multiple payments for the same payment_hash (MPP) in both cases (trampoline and channel).
|
||||||
case (e, part) if part.direction == "IN" => e.copy(amountIn = part.amount, fromChannelId = part.channelId)
|
// NB: we may link the wrong in-out parts, but the overall sum will be correct: we sort by amounts to minimize the risk of mismatch.
|
||||||
case (e, part) if part.direction == "OUT" => e.copy(amountOut = part.amount, toChannelId = part.channelId)
|
val incoming = parts.filter(_.direction == "IN").map(p => PaymentRelayed.Part(p.amount, p.channelId)).sortBy(_.amount)
|
||||||
|
val outgoing = parts.filter(_.direction == "OUT").map(p => PaymentRelayed.Part(p.amount, p.channelId)).sortBy(_.amount)
|
||||||
|
parts.headOption match {
|
||||||
|
case Some(RelayedPart(_, _, _, "channel", timestamp)) => incoming.zip(outgoing).map {
|
||||||
|
case (in, out) => ChannelPaymentRelayed(in.amount, out.amount, paymentHash, in.channelId, out.channelId, timestamp)
|
||||||
|
}
|
||||||
|
case Some(RelayedPart(_, _, _, "trampoline", timestamp)) => TrampolinePaymentRelayed(paymentHash, incoming, outgoing, timestamp) :: Nil
|
||||||
|
case _ => Nil
|
||||||
}
|
}
|
||||||
case "trampoline" => parts.foldLeft(TrampolinePaymentRelayed(paymentHash, Nil, Nil, parts.head.timestamp)) {
|
|
||||||
case (e, part) if part.direction == "IN" => e.copy(incoming = e.incoming :+ PaymentRelayed.Part(part.amount, part.channelId))
|
|
||||||
case (e, part) if part.direction == "OUT" => e.copy(outgoing = e.outgoing :+ PaymentRelayed.Part(part.amount, part.channelId))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.toSeq.sortBy(_.timestamp)
|
}.toSeq.sortBy(_.timestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,8 +29,8 @@ import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.SendMultiPartPayme
|
||||||
import fr.acinq.eclair.payment.send.PaymentLifecycle.{SendPayment, SendPaymentToRoute}
|
import fr.acinq.eclair.payment.send.PaymentLifecycle.{SendPayment, SendPaymentToRoute}
|
||||||
import fr.acinq.eclair.router.{ChannelHop, Hop, NodeHop, RouteParams}
|
import fr.acinq.eclair.router.{ChannelHop, Hop, NodeHop, RouteParams}
|
||||||
import fr.acinq.eclair.wire.Onion.FinalLegacyPayload
|
import fr.acinq.eclair.wire.Onion.FinalLegacyPayload
|
||||||
import fr.acinq.eclair.wire.{Onion, OnionTlv, TrampolineExpiryTooSoon, TrampolineFeeInsufficient}
|
import fr.acinq.eclair.wire._
|
||||||
import fr.acinq.eclair.{CltvExpiryDelta, Features, LongToBtcAmount, MilliSatoshi, NodeParams, randomBytes32}
|
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, LongToBtcAmount, MilliSatoshi, NodeParams, randomBytes32}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by PM on 29/08/2016.
|
* Created by PM on 29/08/2016.
|
||||||
|
@ -52,19 +52,14 @@ class PaymentInitiator(nodeParams: NodeParams, router: ActorRef, relayer: ActorR
|
||||||
sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(InvalidInvoice(s"unknown invoice features (${invoice.features})")) :: Nil)
|
sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(InvalidInvoice(s"unknown invoice features (${invoice.features})")) :: Nil)
|
||||||
case Some(invoice) if invoice.features.allowMultiPart && Features.hasFeature(nodeParams.features, Features.BasicMultiPartPayment) =>
|
case Some(invoice) if invoice.features.allowMultiPart && Features.hasFeature(nodeParams.features, Features.BasicMultiPartPayment) =>
|
||||||
invoice.paymentSecret match {
|
invoice.paymentSecret match {
|
||||||
case Some(paymentSecret) => r.predefinedRoute match {
|
case Some(paymentSecret) =>
|
||||||
case Nil => spawnMultiPartPaymentFsm(paymentCfg) forward SendMultiPartPayment(paymentSecret, r.recipientNodeId, r.recipientAmount, finalExpiry, r.maxAttempts, r.assistedRoutes, r.routeParams)
|
spawnMultiPartPaymentFsm(paymentCfg) forward SendMultiPartPayment(paymentSecret, r.recipientNodeId, r.recipientAmount, finalExpiry, r.maxAttempts, r.assistedRoutes, r.routeParams)
|
||||||
case hops => spawnPaymentFsm(paymentCfg) forward SendPaymentToRoute(hops, Onion.createMultiPartPayload(r.recipientAmount, invoice.amount.getOrElse(r.recipientAmount), finalExpiry, paymentSecret))
|
case None =>
|
||||||
}
|
sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(InvalidInvoice("multi-part invoice is missing a payment secret")) :: Nil)
|
||||||
case None => sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(InvalidInvoice("multi-part invoice is missing a payment secret")) :: Nil)
|
|
||||||
}
|
}
|
||||||
case _ =>
|
case _ =>
|
||||||
val payFsm = spawnPaymentFsm(paymentCfg)
|
|
||||||
// NB: we only generate legacy payment onions for now for maximum compatibility.
|
// NB: we only generate legacy payment onions for now for maximum compatibility.
|
||||||
r.predefinedRoute match {
|
spawnPaymentFsm(paymentCfg) forward SendPayment(r.recipientNodeId, FinalLegacyPayload(r.recipientAmount, finalExpiry), r.maxAttempts, r.assistedRoutes, r.routeParams)
|
||||||
case Nil => payFsm forward SendPayment(r.recipientNodeId, FinalLegacyPayload(r.recipientAmount, finalExpiry), r.maxAttempts, r.assistedRoutes, r.routeParams)
|
|
||||||
case hops => payFsm forward SendPaymentToRoute(hops, FinalLegacyPayload(r.recipientAmount, finalExpiry))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case r: SendTrampolinePaymentRequest =>
|
case r: SendTrampolinePaymentRequest =>
|
||||||
|
@ -101,18 +96,42 @@ class PaymentInitiator(nodeParams: NodeParams, router: ActorRef, relayer: ActorR
|
||||||
context.system.eventStream.publish(ps)
|
context.system.eventStream.publish(ps)
|
||||||
context become main(pending - ps.id)
|
context become main(pending - ps.id)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
case r: SendPaymentToRouteRequest =>
|
||||||
|
val paymentId = UUID.randomUUID()
|
||||||
|
val parentPaymentId = r.parentId.getOrElse(UUID.randomUUID())
|
||||||
|
val finalExpiry = r.finalExpiry(nodeParams.currentBlockHeight)
|
||||||
|
val additionalHops = r.trampolineNodes.sliding(2).map(hop => NodeHop(hop.head, hop(1), CltvExpiryDelta(0), 0 msat)).toSeq
|
||||||
|
val paymentCfg = SendPaymentConfig(paymentId, parentPaymentId, r.externalId, r.paymentHash, r.recipientAmount, r.recipientNodeId, Upstream.Local(paymentId), Some(r.paymentRequest), storeInDb = true, publishEvent = true, additionalHops)
|
||||||
|
val payFsm = spawnPaymentFsm(paymentCfg)
|
||||||
|
r.trampolineNodes match {
|
||||||
|
case trampoline :: recipient :: Nil =>
|
||||||
|
log.info(s"sending trampoline payment to $recipient with trampoline=$trampoline, trampoline fees=${r.trampolineFees}, expiry delta=${r.trampolineExpiryDelta}")
|
||||||
|
// We generate a random secret for the payment to the first trampoline node.
|
||||||
|
val trampolineSecret = r.trampolineSecret.getOrElse(randomBytes32)
|
||||||
|
sender ! SendPaymentToRouteResponse(paymentId, parentPaymentId, Some(trampolineSecret))
|
||||||
|
val (trampolineAmount, trampolineExpiry, trampolineOnion) = buildTrampolinePayment(SendTrampolinePaymentRequest(r.recipientAmount, r.paymentRequest, trampoline, Seq((r.trampolineFees, r.trampolineExpiryDelta)), r.finalExpiryDelta), r.trampolineFees, r.trampolineExpiryDelta)
|
||||||
|
payFsm forward SendPaymentToRoute(r.route, Onion.createMultiPartPayload(r.amount, trampolineAmount, trampolineExpiry, trampolineSecret, Seq(OnionTlv.TrampolineOnion(trampolineOnion))))
|
||||||
|
case Nil =>
|
||||||
|
sender ! SendPaymentToRouteResponse(paymentId, parentPaymentId, None)
|
||||||
|
r.paymentRequest.paymentSecret match {
|
||||||
|
case Some(paymentSecret) => payFsm forward SendPaymentToRoute(r.route, Onion.createMultiPartPayload(r.amount, r.recipientAmount, finalExpiry, paymentSecret))
|
||||||
|
case None => payFsm forward SendPaymentToRoute(r.route, FinalLegacyPayload(r.recipientAmount, finalExpiry))
|
||||||
|
}
|
||||||
|
case _ =>
|
||||||
|
sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(new IllegalArgumentException(s"unsupported number of trampoline nodes: ${r.trampolineNodes}")) :: Nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def spawnPaymentFsm(paymentCfg: SendPaymentConfig): ActorRef = context.actorOf(PaymentLifecycle.props(nodeParams, paymentCfg, router, register))
|
def spawnPaymentFsm(paymentCfg: SendPaymentConfig): ActorRef = context.actorOf(PaymentLifecycle.props(nodeParams, paymentCfg, router, register))
|
||||||
|
|
||||||
def spawnMultiPartPaymentFsm(paymentCfg: SendPaymentConfig): ActorRef = context.actorOf(MultiPartPaymentLifecycle.props(nodeParams, paymentCfg, relayer, router, register))
|
def spawnMultiPartPaymentFsm(paymentCfg: SendPaymentConfig): ActorRef = context.actorOf(MultiPartPaymentLifecycle.props(nodeParams, paymentCfg, relayer, router, register))
|
||||||
|
|
||||||
private def sendTrampolinePayment(paymentId: UUID, r: SendTrampolinePaymentRequest, trampolineFees: MilliSatoshi, trampolineExpiryDelta: CltvExpiryDelta): Unit = {
|
private def buildTrampolinePayment(r: SendTrampolinePaymentRequest, trampolineFees: MilliSatoshi, trampolineExpiryDelta: CltvExpiryDelta): (MilliSatoshi, CltvExpiry, OnionRoutingPacket) = {
|
||||||
val trampolineRoute = Seq(
|
val trampolineRoute = Seq(
|
||||||
NodeHop(nodeParams.nodeId, r.trampolineNodeId, nodeParams.expiryDeltaBlocks, 0 msat),
|
NodeHop(nodeParams.nodeId, r.trampolineNodeId, nodeParams.expiryDeltaBlocks, 0 msat),
|
||||||
NodeHop(r.trampolineNodeId, r.recipientNodeId, trampolineExpiryDelta, trampolineFees) // for now we only use a single trampoline hop
|
NodeHop(r.trampolineNodeId, r.recipientNodeId, trampolineExpiryDelta, trampolineFees) // for now we only use a single trampoline hop
|
||||||
)
|
)
|
||||||
val paymentCfg = SendPaymentConfig(paymentId, paymentId, None, r.paymentHash, r.recipientAmount, r.recipientNodeId, Upstream.Local(paymentId), Some(r.paymentRequest), storeInDb = true, publishEvent = false, trampolineRoute.tail)
|
|
||||||
val finalPayload = if (r.paymentRequest.features.allowMultiPart) {
|
val finalPayload = if (r.paymentRequest.features.allowMultiPart) {
|
||||||
Onion.createMultiPartPayload(r.recipientAmount, r.recipientAmount, r.finalExpiry(nodeParams.currentBlockHeight), r.paymentRequest.paymentSecret.get)
|
Onion.createMultiPartPayload(r.recipientAmount, r.recipientAmount, r.finalExpiry(nodeParams.currentBlockHeight), r.paymentRequest.paymentSecret.get)
|
||||||
} else {
|
} else {
|
||||||
|
@ -124,9 +143,15 @@ class PaymentInitiator(nodeParams: NodeParams, router: ActorRef, relayer: ActorR
|
||||||
} else {
|
} else {
|
||||||
OutgoingPacket.buildTrampolineToLegacyPacket(r.paymentRequest, trampolineRoute, finalPayload)
|
OutgoingPacket.buildTrampolineToLegacyPacket(r.paymentRequest, trampolineRoute, finalPayload)
|
||||||
}
|
}
|
||||||
|
(trampolineAmount, trampolineExpiry, trampolineOnion.packet)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def sendTrampolinePayment(paymentId: UUID, r: SendTrampolinePaymentRequest, trampolineFees: MilliSatoshi, trampolineExpiryDelta: CltvExpiryDelta): Unit = {
|
||||||
|
val paymentCfg = SendPaymentConfig(paymentId, paymentId, None, r.paymentHash, r.recipientAmount, r.recipientNodeId, Upstream.Local(paymentId), Some(r.paymentRequest), storeInDb = true, publishEvent = false, Seq(NodeHop(r.trampolineNodeId, r.recipientNodeId, trampolineExpiryDelta, trampolineFees)))
|
||||||
// We generate a random secret for this payment to avoid leaking the invoice secret to the first trampoline node.
|
// We generate a random secret for this payment to avoid leaking the invoice secret to the first trampoline node.
|
||||||
val trampolineSecret = randomBytes32
|
val trampolineSecret = randomBytes32
|
||||||
spawnMultiPartPaymentFsm(paymentCfg) ! SendMultiPartPayment(trampolineSecret, r.trampolineNodeId, trampolineAmount, trampolineExpiry, 1, r.paymentRequest.routingInfo, r.routeParams, Seq(OnionTlv.TrampolineOnion(trampolineOnion.packet)))
|
val (trampolineAmount, trampolineExpiry, trampolineOnion) = buildTrampolinePayment(r, trampolineFees, trampolineExpiryDelta)
|
||||||
|
spawnMultiPartPaymentFsm(paymentCfg) ! SendMultiPartPayment(trampolineSecret, r.trampolineNodeId, trampolineAmount, trampolineExpiry, 1, r.paymentRequest.routingInfo, r.routeParams, Seq(OnionTlv.TrampolineOnion(trampolineOnion)))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -173,7 +198,6 @@ object PaymentInitiator {
|
||||||
* @param finalExpiryDelta expiry delta for the final recipient.
|
* @param finalExpiryDelta expiry delta for the final recipient.
|
||||||
* @param paymentRequest (optional) Bolt 11 invoice.
|
* @param paymentRequest (optional) Bolt 11 invoice.
|
||||||
* @param externalId (optional) externally-controlled identifier (to reconcile between application DB and eclair DB).
|
* @param externalId (optional) externally-controlled identifier (to reconcile between application DB and eclair DB).
|
||||||
* @param predefinedRoute (optional) route to use for the payment.
|
|
||||||
* @param assistedRoutes (optional) routing hints (usually from a Bolt 11 invoice).
|
* @param assistedRoutes (optional) routing hints (usually from a Bolt 11 invoice).
|
||||||
* @param routeParams (optional) parameters to fine-tune the routing algorithm.
|
* @param routeParams (optional) parameters to fine-tune the routing algorithm.
|
||||||
*/
|
*/
|
||||||
|
@ -184,13 +208,74 @@ object PaymentInitiator {
|
||||||
finalExpiryDelta: CltvExpiryDelta = Channel.MIN_CLTV_EXPIRY_DELTA,
|
finalExpiryDelta: CltvExpiryDelta = Channel.MIN_CLTV_EXPIRY_DELTA,
|
||||||
paymentRequest: Option[PaymentRequest] = None,
|
paymentRequest: Option[PaymentRequest] = None,
|
||||||
externalId: Option[String] = None,
|
externalId: Option[String] = None,
|
||||||
predefinedRoute: Seq[PublicKey] = Nil,
|
|
||||||
assistedRoutes: Seq[Seq[ExtraHop]] = Nil,
|
assistedRoutes: Seq[Seq[ExtraHop]] = Nil,
|
||||||
routeParams: Option[RouteParams] = None) {
|
routeParams: Option[RouteParams] = None) {
|
||||||
// We add one block in order to not have our htlcs fail when a new block has just been found.
|
// We add one block in order to not have our htlcs fail when a new block has just been found.
|
||||||
def finalExpiry(currentBlockHeight: Long) = finalExpiryDelta.toCltvExpiry(currentBlockHeight + 1)
|
def finalExpiry(currentBlockHeight: Long) = finalExpiryDelta.toCltvExpiry(currentBlockHeight + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The sender can skip the routing algorithm by specifying the route to use.
|
||||||
|
* When combining with MPP and Trampoline, extra-care must be taken to make sure payments are correctly grouped: only
|
||||||
|
* amount, route and trampolineNodes should be changing.
|
||||||
|
*
|
||||||
|
* Example 1: MPP containing two HTLCs for a 600 msat invoice:
|
||||||
|
* SendPaymentToRouteRequest(200 msat, 600 msat, None, parentId, invoice, CltvExpiryDelta(9), Seq(alice, bob, dave), None, 0 msat, CltvExpiryDelta(0), Nil)
|
||||||
|
* SendPaymentToRouteRequest(400 msat, 600 msat, None, parentId, invoice, CltvExpiryDelta(9), Seq(alice, carol, dave), None, 0 msat, CltvExpiryDelta(0), Nil)
|
||||||
|
*
|
||||||
|
* Example 2: Trampoline with MPP for a 600 msat invoice and 100 msat trampoline fees:
|
||||||
|
* SendPaymentToRouteRequest(250 msat, 600 msat, None, parentId, invoice, CltvExpiryDelta(9), Seq(alice, bob, dave), secret, 100 msat, CltvExpiryDelta(144), Seq(dave, peter))
|
||||||
|
* SendPaymentToRouteRequest(450 msat, 600 msat, None, parentId, invoice, CltvExpiryDelta(9), Seq(alice, carol, dave), secret, 100 msat, CltvExpiryDelta(144), Seq(dave, peter))
|
||||||
|
*
|
||||||
|
* @param amount amount that should be received by the last node in the route (should take trampoline
|
||||||
|
* fees into account).
|
||||||
|
* @param recipientAmount amount that should be received by the final recipient (usually from a Bolt 11 invoice).
|
||||||
|
* This amount may be split between multiple requests if using MPP.
|
||||||
|
* @param externalId (optional) externally-controlled identifier (to reconcile between application DB and eclair DB).
|
||||||
|
* @param parentId id of the whole payment. When manually sending a multi-part payment, you need to make
|
||||||
|
* sure all partial payments use the same parentId. If not provided, a random parentId will
|
||||||
|
* be generated that can be used for the remaining partial payments.
|
||||||
|
* @param paymentRequest Bolt 11 invoice.
|
||||||
|
* @param finalExpiryDelta expiry delta for the final recipient.
|
||||||
|
* @param route route to use to reach either the final recipient or the first trampoline node.
|
||||||
|
* @param trampolineSecret if trampoline is used, this is a secret to protect the payment to the first trampoline
|
||||||
|
* node against probing. When manually sending a multi-part payment, you need to make sure
|
||||||
|
* all partial payments use the same trampolineSecret.
|
||||||
|
* @param trampolineFees if trampoline is used, fees for the first trampoline node. This value must be the same
|
||||||
|
* for all partial payments in the set.
|
||||||
|
* @param trampolineExpiryDelta if trampoline is used, expiry delta for the first trampoline node. This value must be
|
||||||
|
* the same for all partial payments in the set.
|
||||||
|
* @param trampolineNodes if trampoline is used, list of trampoline nodes to use (we currently support only a
|
||||||
|
* single trampoline node).
|
||||||
|
*/
|
||||||
|
case class SendPaymentToRouteRequest(amount: MilliSatoshi,
|
||||||
|
recipientAmount: MilliSatoshi,
|
||||||
|
externalId: Option[String],
|
||||||
|
parentId: Option[UUID],
|
||||||
|
paymentRequest: PaymentRequest,
|
||||||
|
finalExpiryDelta: CltvExpiryDelta = Channel.MIN_CLTV_EXPIRY_DELTA,
|
||||||
|
route: Seq[PublicKey],
|
||||||
|
trampolineSecret: Option[ByteVector32],
|
||||||
|
trampolineFees: MilliSatoshi,
|
||||||
|
trampolineExpiryDelta: CltvExpiryDelta,
|
||||||
|
trampolineNodes: Seq[PublicKey]) {
|
||||||
|
val recipientNodeId = paymentRequest.nodeId
|
||||||
|
val paymentHash = paymentRequest.paymentHash
|
||||||
|
|
||||||
|
// We add one block in order to not have our htlcs fail when a new block has just been found.
|
||||||
|
def finalExpiry(currentBlockHeight: Long) = finalExpiryDelta.toCltvExpiry(currentBlockHeight + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param paymentId id of the outgoing payment (mapped to a single outgoing HTLC).
|
||||||
|
* @param parentId id of the whole payment. When manually sending a multi-part payment, you need to make sure
|
||||||
|
* all partial payments use the same parentId.
|
||||||
|
* @param trampolineSecret if trampoline is used, this is a secret to protect the payment to the first trampoline node
|
||||||
|
* against probing. When manually sending a multi-part payment, you need to make sure all
|
||||||
|
* partial payments use the same trampolineSecret.
|
||||||
|
*/
|
||||||
|
case class SendPaymentToRouteResponse(paymentId: UUID, parentId: UUID, trampolineSecret: Option[ByteVector32])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration for an instance of a payment state machine.
|
* Configuration for an instance of a payment state machine.
|
||||||
*
|
*
|
||||||
|
|
|
@ -16,6 +16,8 @@
|
||||||
|
|
||||||
package fr.acinq.eclair
|
package fr.acinq.eclair
|
||||||
|
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
import akka.actor.ActorSystem
|
import akka.actor.ActorSystem
|
||||||
import akka.testkit.{TestKit, TestProbe}
|
import akka.testkit.{TestKit, TestProbe}
|
||||||
import akka.util.Timeout
|
import akka.util.Timeout
|
||||||
|
@ -30,7 +32,7 @@ import fr.acinq.eclair.payment.PaymentRequest
|
||||||
import fr.acinq.eclair.payment.PaymentRequest.ExtraHop
|
import fr.acinq.eclair.payment.PaymentRequest.ExtraHop
|
||||||
import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment
|
import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment
|
||||||
import fr.acinq.eclair.payment.receive.PaymentHandler
|
import fr.acinq.eclair.payment.receive.PaymentHandler
|
||||||
import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentRequest
|
import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentRequest, SendPaymentToRouteRequest}
|
||||||
import fr.acinq.eclair.router.RouteCalculationSpec.makeUpdate
|
import fr.acinq.eclair.router.RouteCalculationSpec.makeUpdate
|
||||||
import fr.acinq.eclair.router.{Announcements, PublicChannel, Router, GetNetworkStats, NetworkStats, Stats}
|
import fr.acinq.eclair.router.{Announcements, PublicChannel, Router, GetNetworkStats, NetworkStats, Stats}
|
||||||
import org.mockito.Mockito
|
import org.mockito.Mockito
|
||||||
|
@ -313,18 +315,15 @@ class EclairImplSpec extends TestKit(ActorSystem("test")) with fixture.FunSuiteL
|
||||||
test("sendtoroute should pass the parameters correctly") { f =>
|
test("sendtoroute should pass the parameters correctly") { f =>
|
||||||
import f._
|
import f._
|
||||||
|
|
||||||
val route = Seq(PublicKey(hex"030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87"))
|
|
||||||
val eclair = new EclairImpl(kit)
|
val eclair = new EclairImpl(kit)
|
||||||
|
val route = Seq(randomKey.publicKey)
|
||||||
|
val trampolines = Seq(randomKey.publicKey, randomKey.publicKey)
|
||||||
|
val parentId = UUID.randomUUID()
|
||||||
|
val secret = randomBytes32
|
||||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(1234 msat), ByteVector32.One, randomKey, "Some invoice")
|
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(1234 msat), ByteVector32.One, randomKey, "Some invoice")
|
||||||
eclair.sendToRoute(Some("42"), route, 1234 msat, ByteVector32.One, CltvExpiryDelta(123), Some(pr))
|
eclair.sendToRoute(1000 msat, Some(1200 msat), Some("42"), Some(parentId), pr, CltvExpiryDelta(123), route, Some(secret), Some(100 msat), Some(CltvExpiryDelta(144)), trampolines)
|
||||||
|
|
||||||
val send = paymentInitiator.expectMsgType[SendPaymentRequest]
|
paymentInitiator.expectMsg(SendPaymentToRouteRequest(1000 msat, 1200 msat, Some("42"), Some(parentId), pr, CltvExpiryDelta(123), route, Some(secret), 100 msat, CltvExpiryDelta(144), trampolines))
|
||||||
assert(send.externalId === Some("42"))
|
|
||||||
assert(send.predefinedRoute === route)
|
|
||||||
assert(send.recipientAmount === 1234.msat)
|
|
||||||
assert(send.finalExpiryDelta === CltvExpiryDelta(123))
|
|
||||||
assert(send.paymentHash === ByteVector32.One)
|
|
||||||
assert(send.paymentRequest === Some(pr))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,7 +61,10 @@ class SqliteAuditDbSpec extends FunSuite {
|
||||||
val e8 = ChannelLifecycleEvent(randomBytes32, randomKey.publicKey, 456123000 sat, isFunder = true, isPrivate = false, "mutual")
|
val e8 = ChannelLifecycleEvent(randomBytes32, randomKey.publicKey, 456123000 sat, isFunder = true, isPrivate = false, "mutual")
|
||||||
val e9 = ChannelErrorOccurred(null, randomBytes32, randomKey.publicKey, null, LocalError(new RuntimeException("oops")), isFatal = true)
|
val e9 = ChannelErrorOccurred(null, randomBytes32, randomKey.publicKey, null, LocalError(new RuntimeException("oops")), isFatal = true)
|
||||||
val e10 = ChannelErrorOccurred(null, randomBytes32, randomKey.publicKey, null, RemoteError(wire.Error(randomBytes32, "remote oops")), isFatal = true)
|
val e10 = ChannelErrorOccurred(null, randomBytes32, randomKey.publicKey, null, RemoteError(wire.Error(randomBytes32, "remote oops")), isFatal = true)
|
||||||
val e11 = TrampolinePaymentRelayed(randomBytes32, Seq(PaymentRelayed.Part(20000 msat, randomBytes32), PaymentRelayed.Part(22000 msat, randomBytes32)), Seq(PaymentRelayed.Part(20000 msat, randomBytes32), PaymentRelayed.Part(10000 msat, randomBytes32), PaymentRelayed.Part(10000 msat, randomBytes32)))
|
val e11 = TrampolinePaymentRelayed(randomBytes32, Seq(PaymentRelayed.Part(20000 msat, randomBytes32), PaymentRelayed.Part(22000 msat, randomBytes32)), Seq(PaymentRelayed.Part(10000 msat, randomBytes32), PaymentRelayed.Part(12000 msat, randomBytes32), PaymentRelayed.Part(15000 msat, randomBytes32)))
|
||||||
|
val multiPartPaymentHash = randomBytes32
|
||||||
|
val e12 = ChannelPaymentRelayed(13000 msat, 11000 msat, multiPartPaymentHash, randomBytes32, randomBytes32)
|
||||||
|
val e13 = ChannelPaymentRelayed(15000 msat, 12500 msat, multiPartPaymentHash, randomBytes32, randomBytes32)
|
||||||
|
|
||||||
db.add(e1)
|
db.add(e1)
|
||||||
db.add(e2)
|
db.add(e2)
|
||||||
|
@ -74,11 +77,13 @@ class SqliteAuditDbSpec extends FunSuite {
|
||||||
db.add(e9)
|
db.add(e9)
|
||||||
db.add(e10)
|
db.add(e10)
|
||||||
db.add(e11)
|
db.add(e11)
|
||||||
|
db.add(e12)
|
||||||
|
db.add(e13)
|
||||||
|
|
||||||
assert(db.listSent(from = 0L, to = (Platform.currentTime.milliseconds + 15.minute).toMillis).toSet === Set(e1, e5, e6))
|
assert(db.listSent(from = 0L, to = (Platform.currentTime.milliseconds + 15.minute).toMillis).toSet === Set(e1, e5, e6))
|
||||||
assert(db.listSent(from = 100000L, to = (Platform.currentTime.milliseconds + 1.minute).toMillis).toList === List(e1))
|
assert(db.listSent(from = 100000L, to = (Platform.currentTime.milliseconds + 1.minute).toMillis).toList === List(e1))
|
||||||
assert(db.listReceived(from = 0L, to = (Platform.currentTime.milliseconds + 1.minute).toMillis).toList === List(e2))
|
assert(db.listReceived(from = 0L, to = (Platform.currentTime.milliseconds + 1.minute).toMillis).toList === List(e2))
|
||||||
assert(db.listRelayed(from = 0L, to = (Platform.currentTime.milliseconds + 1.minute).toMillis).toList === List(e3, e11))
|
assert(db.listRelayed(from = 0L, to = (Platform.currentTime.milliseconds + 1.minute).toMillis).toList === List(e3, e11, e12, e13))
|
||||||
assert(db.listNetworkFees(from = 0L, to = (Platform.currentTime.milliseconds + 1.minute).toMillis).size === 1)
|
assert(db.listNetworkFees(from = 0L, to = (Platform.currentTime.milliseconds + 1.minute).toMillis).size === 1)
|
||||||
assert(db.listNetworkFees(from = 0L, to = (Platform.currentTime.milliseconds + 1.minute).toMillis).head.txType === "mutual")
|
assert(db.listNetworkFees(from = 0L, to = (Platform.currentTime.milliseconds + 1.minute).toMillis).head.txType === "mutual")
|
||||||
}
|
}
|
||||||
|
@ -343,7 +348,7 @@ class SqliteAuditDbSpec extends FunSuite {
|
||||||
PaymentSent.PartialPayment(UUID.randomUUID(), 500 msat, 10 msat, randomBytes32, None, 160),
|
PaymentSent.PartialPayment(UUID.randomUUID(), 500 msat, 10 msat, randomBytes32, None, 160),
|
||||||
PaymentSent.PartialPayment(UUID.randomUUID(), 600 msat, 5 msat, randomBytes32, None, 165)
|
PaymentSent.PartialPayment(UUID.randomUUID(), 600 msat, 5 msat, randomBytes32, None, 165)
|
||||||
))
|
))
|
||||||
val relayed3 = TrampolinePaymentRelayed(randomBytes32, Seq(PaymentRelayed.Part(500 msat, randomBytes32), PaymentRelayed.Part(450 msat, randomBytes32)), Seq(PaymentRelayed.Part(800 msat, randomBytes32)), 150)
|
val relayed3 = TrampolinePaymentRelayed(randomBytes32, Seq(PaymentRelayed.Part(450 msat, randomBytes32), PaymentRelayed.Part(500 msat, randomBytes32)), Seq(PaymentRelayed.Part(800 msat, randomBytes32)), 150)
|
||||||
|
|
||||||
postMigrationDb.add(ps2)
|
postMigrationDb.add(ps2)
|
||||||
assert(postMigrationDb.listSent(155, 200) === Seq(ps2))
|
assert(postMigrationDb.listSent(155, 200) === Seq(ps2))
|
||||||
|
@ -351,7 +356,7 @@ class SqliteAuditDbSpec extends FunSuite {
|
||||||
assert(postMigrationDb.listRelayed(100, 160) === Seq(relayed1, relayed2, relayed3))
|
assert(postMigrationDb.listRelayed(100, 160) === Seq(relayed1, relayed2, relayed3))
|
||||||
}
|
}
|
||||||
|
|
||||||
test("fails if the DB contains invalid values") {
|
test("ignore invalid values in the DB") {
|
||||||
val sqlite = TestConstants.sqliteInMemory()
|
val sqlite = TestConstants.sqliteInMemory()
|
||||||
val db = new SqliteAuditDb(sqlite)
|
val db = new SqliteAuditDb(sqlite)
|
||||||
|
|
||||||
|
@ -365,8 +370,6 @@ class SqliteAuditDbSpec extends FunSuite {
|
||||||
statement.executeUpdate()
|
statement.executeUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
assertThrows[MatchError](db.listRelayed(5, 15))
|
|
||||||
|
|
||||||
using(sqlite.prepareStatement("INSERT INTO relayed (payment_hash, amount_msat, channel_id, direction, relay_type, timestamp) VALUES (?, ?, ?, ?, ?, ?)")) { statement =>
|
using(sqlite.prepareStatement("INSERT INTO relayed (payment_hash, amount_msat, channel_id, direction, relay_type, timestamp) VALUES (?, ?, ?, ?, ?, ?)")) { statement =>
|
||||||
statement.setBytes(1, randomBytes32.toArray)
|
statement.setBytes(1, randomBytes32.toArray)
|
||||||
statement.setLong(2, 51)
|
statement.setLong(2, 51)
|
||||||
|
@ -377,8 +380,6 @@ class SqliteAuditDbSpec extends FunSuite {
|
||||||
statement.executeUpdate()
|
statement.executeUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
assertThrows[MatchError](db.listRelayed(15, 25))
|
|
||||||
|
|
||||||
val paymentHash = randomBytes32
|
val paymentHash = randomBytes32
|
||||||
val channelId = randomBytes32
|
val channelId = randomBytes32
|
||||||
|
|
||||||
|
@ -386,13 +387,13 @@ class SqliteAuditDbSpec extends FunSuite {
|
||||||
statement.setBytes(1, paymentHash.toArray)
|
statement.setBytes(1, paymentHash.toArray)
|
||||||
statement.setLong(2, 65)
|
statement.setLong(2, 65)
|
||||||
statement.setBytes(3, channelId.toArray)
|
statement.setBytes(3, channelId.toArray)
|
||||||
statement.setString(4, "IN")
|
statement.setString(4, "IN") // missing a corresponding OUT
|
||||||
statement.setString(5, "channel")
|
statement.setString(5, "channel")
|
||||||
statement.setLong(6, 30)
|
statement.setLong(6, 30)
|
||||||
statement.executeUpdate()
|
statement.executeUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(db.listRelayed(25, 35) === Seq(ChannelPaymentRelayed(65 msat, 0 msat, paymentHash, channelId, ByteVector32.Zeroes, 30)))
|
assert(db.listRelayed(0, 40) === Nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -451,24 +451,26 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
|
||||||
sender.send(nodes("B").paymentInitiator, SendPaymentRequest(amount, pr.paymentHash, nodes("D").nodeParams.nodeId, 5, paymentRequest = Some(pr)))
|
sender.send(nodes("B").paymentInitiator, SendPaymentRequest(amount, pr.paymentHash, nodes("D").nodeParams.nodeId, 5, paymentRequest = Some(pr)))
|
||||||
val paymentId = sender.expectMsgType[UUID](30 seconds)
|
val paymentId = sender.expectMsgType[UUID](30 seconds)
|
||||||
val paymentSent = sender.expectMsgType[PaymentSent](30 seconds)
|
val paymentSent = sender.expectMsgType[PaymentSent](30 seconds)
|
||||||
assert(paymentSent.id === paymentId)
|
assert(paymentSent.id === paymentId, paymentSent)
|
||||||
assert(paymentSent.paymentHash === pr.paymentHash)
|
assert(paymentSent.paymentHash === pr.paymentHash, paymentSent)
|
||||||
assert(paymentSent.parts.length > 1)
|
assert(paymentSent.parts.length > 1, paymentSent)
|
||||||
assert(paymentSent.recipientNodeId === nodes("D").nodeParams.nodeId)
|
assert(paymentSent.recipientNodeId === nodes("D").nodeParams.nodeId, paymentSent)
|
||||||
assert(paymentSent.recipientAmount === amount)
|
assert(paymentSent.recipientAmount === amount, paymentSent)
|
||||||
assert(paymentSent.feesPaid > 0.msat)
|
assert(paymentSent.feesPaid > 0.msat, paymentSent)
|
||||||
assert(paymentSent.parts.forall(p => p.id != paymentSent.id))
|
assert(paymentSent.parts.forall(p => p.id != paymentSent.id), paymentSent)
|
||||||
assert(paymentSent.parts.forall(p => p.route.isDefined))
|
assert(paymentSent.parts.forall(p => p.route.isDefined), paymentSent)
|
||||||
|
|
||||||
val paymentParts = nodes("B").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(_.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])
|
val paymentParts = nodes("B").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(_.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])
|
||||||
assert(paymentParts.length == paymentSent.parts.length)
|
assert(paymentParts.length == paymentSent.parts.length, paymentParts)
|
||||||
assert(paymentParts.map(_.amount).sum === amount)
|
assert(paymentParts.map(_.amount).sum === amount, paymentParts)
|
||||||
assert(paymentParts.forall(p => p.parentId == paymentId))
|
assert(paymentParts.forall(p => p.parentId == paymentId), paymentParts)
|
||||||
assert(paymentParts.forall(p => p.parentId != p.id))
|
assert(paymentParts.forall(p => p.parentId != p.id), paymentParts)
|
||||||
assert(paymentParts.forall(p => p.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].feesPaid > 0.msat))
|
assert(paymentParts.forall(p => p.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].feesPaid > 0.msat), paymentParts)
|
||||||
|
|
||||||
awaitCond(nodes("B").nodeParams.db.audit.listSent(start, Platform.currentTime).nonEmpty)
|
awaitCond(nodes("B").nodeParams.db.audit.listSent(start, Platform.currentTime).nonEmpty)
|
||||||
assert(nodes("B").nodeParams.db.audit.listSent(start, Platform.currentTime) === Seq(paymentSent.copy(parts = paymentSent.parts.map(_.copy(route = None)))))
|
val sent = nodes("B").nodeParams.db.audit.listSent(start, Platform.currentTime)
|
||||||
|
assert(sent.length === 1, sent)
|
||||||
|
assert(sent.head.copy(parts = sent.head.parts.sortBy(_.timestamp)) === paymentSent.copy(parts = paymentSent.parts.map(_.copy(route = None)).sortBy(_.timestamp)), sent)
|
||||||
|
|
||||||
awaitCond(nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received]))
|
awaitCond(nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received]))
|
||||||
val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
|
val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
|
||||||
|
@ -492,9 +494,9 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
|
||||||
sender.send(nodes("B").paymentInitiator, SendPaymentRequest(amount, pr.paymentHash, nodes("D").nodeParams.nodeId, 1, paymentRequest = Some(pr)))
|
sender.send(nodes("B").paymentInitiator, SendPaymentRequest(amount, pr.paymentHash, nodes("D").nodeParams.nodeId, 1, paymentRequest = Some(pr)))
|
||||||
val paymentId = sender.expectMsgType[UUID](30 seconds)
|
val paymentId = sender.expectMsgType[UUID](30 seconds)
|
||||||
val paymentFailed = sender.expectMsgType[PaymentFailed](45 seconds)
|
val paymentFailed = sender.expectMsgType[PaymentFailed](45 seconds)
|
||||||
assert(paymentFailed.id === paymentId)
|
assert(paymentFailed.id === paymentId, paymentFailed)
|
||||||
assert(paymentFailed.paymentHash === pr.paymentHash)
|
assert(paymentFailed.paymentHash === pr.paymentHash, paymentFailed)
|
||||||
assert(paymentFailed.failures.length > 1)
|
assert(paymentFailed.failures.length > 1, paymentFailed)
|
||||||
|
|
||||||
assert(nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
|
assert(nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
|
||||||
|
|
||||||
|
@ -515,17 +517,17 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
|
||||||
sender.send(nodes("D").paymentInitiator, SendPaymentRequest(amount, pr.paymentHash, nodes("C").nodeParams.nodeId, 3, paymentRequest = Some(pr)))
|
sender.send(nodes("D").paymentInitiator, SendPaymentRequest(amount, pr.paymentHash, nodes("C").nodeParams.nodeId, 3, paymentRequest = Some(pr)))
|
||||||
val paymentId = sender.expectMsgType[UUID](30 seconds)
|
val paymentId = sender.expectMsgType[UUID](30 seconds)
|
||||||
val paymentSent = sender.expectMsgType[PaymentSent](30 seconds)
|
val paymentSent = sender.expectMsgType[PaymentSent](30 seconds)
|
||||||
assert(paymentSent.id === paymentId)
|
assert(paymentSent.id === paymentId, paymentSent)
|
||||||
assert(paymentSent.paymentHash === pr.paymentHash)
|
assert(paymentSent.paymentHash === pr.paymentHash, paymentSent)
|
||||||
assert(paymentSent.parts.length > 1)
|
assert(paymentSent.parts.length > 1, paymentSent)
|
||||||
assert(paymentSent.recipientAmount === amount)
|
assert(paymentSent.recipientAmount === amount, paymentSent)
|
||||||
assert(paymentSent.feesPaid === 0.msat) // no fees when using direct channels
|
assert(paymentSent.feesPaid === 0.msat, paymentSent) // no fees when using direct channels
|
||||||
|
|
||||||
val paymentParts = nodes("D").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(_.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])
|
val paymentParts = nodes("D").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(_.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])
|
||||||
assert(paymentParts.map(_.amount).sum === amount)
|
assert(paymentParts.map(_.amount).sum === amount, paymentParts)
|
||||||
assert(paymentParts.forall(p => p.parentId == paymentId))
|
assert(paymentParts.forall(p => p.parentId == paymentId), paymentParts)
|
||||||
assert(paymentParts.forall(p => p.parentId != p.id))
|
assert(paymentParts.forall(p => p.parentId != p.id), paymentParts)
|
||||||
assert(paymentParts.forall(p => p.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].feesPaid == 0.msat))
|
assert(paymentParts.forall(p => p.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].feesPaid == 0.msat), paymentParts)
|
||||||
|
|
||||||
awaitCond(nodes("C").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received]))
|
awaitCond(nodes("C").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received]))
|
||||||
val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("C").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
|
val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("C").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
|
||||||
|
@ -547,10 +549,11 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
|
||||||
sender.send(nodes("D").paymentInitiator, SendPaymentRequest(amount, pr.paymentHash, nodes("C").nodeParams.nodeId, 1, paymentRequest = Some(pr)))
|
sender.send(nodes("D").paymentInitiator, SendPaymentRequest(amount, pr.paymentHash, nodes("C").nodeParams.nodeId, 1, paymentRequest = Some(pr)))
|
||||||
val paymentId = sender.expectMsgType[UUID](30 seconds)
|
val paymentId = sender.expectMsgType[UUID](30 seconds)
|
||||||
val paymentFailed = sender.expectMsgType[PaymentFailed](30 seconds)
|
val paymentFailed = sender.expectMsgType[PaymentFailed](30 seconds)
|
||||||
assert(paymentFailed.id === paymentId)
|
assert(paymentFailed.id === paymentId, paymentFailed)
|
||||||
assert(paymentFailed.paymentHash === pr.paymentHash)
|
assert(paymentFailed.paymentHash === pr.paymentHash, paymentFailed)
|
||||||
|
|
||||||
assert(nodes("C").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
|
val incoming = nodes("C").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
|
||||||
|
assert(incoming.get.status === IncomingPaymentStatus.Pending, incoming)
|
||||||
|
|
||||||
sender.send(nodes("D").relayer, GetOutgoingChannels())
|
sender.send(nodes("D").relayer, GetOutgoingChannels())
|
||||||
val canSend2 = sender.expectMsgType[OutgoingChannels].channels.map(_.commitments.availableBalanceForSend).sum
|
val canSend2 = sender.expectMsgType[OutgoingChannels].channels.map(_.commitments.availableBalanceForSend).sum
|
||||||
|
@ -573,12 +576,12 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
|
||||||
sender.send(nodes("B").paymentInitiator, payment)
|
sender.send(nodes("B").paymentInitiator, payment)
|
||||||
val paymentId = sender.expectMsgType[UUID](30 seconds)
|
val paymentId = sender.expectMsgType[UUID](30 seconds)
|
||||||
val paymentSent = sender.expectMsgType[PaymentSent](30 seconds)
|
val paymentSent = sender.expectMsgType[PaymentSent](30 seconds)
|
||||||
assert(paymentSent.id === paymentId)
|
assert(paymentSent.id === paymentId, paymentSent)
|
||||||
assert(paymentSent.paymentHash === pr.paymentHash)
|
assert(paymentSent.paymentHash === pr.paymentHash, paymentSent)
|
||||||
assert(paymentSent.recipientNodeId === nodes("F3").nodeParams.nodeId)
|
assert(paymentSent.recipientNodeId === nodes("F3").nodeParams.nodeId, paymentSent)
|
||||||
assert(paymentSent.recipientAmount === amount)
|
assert(paymentSent.recipientAmount === amount, paymentSent)
|
||||||
assert(paymentSent.feesPaid === 1000000.msat)
|
assert(paymentSent.feesPaid === 1000000.msat, paymentSent)
|
||||||
assert(paymentSent.nonTrampolineFees === 0.msat)
|
assert(paymentSent.nonTrampolineFees === 0.msat, paymentSent)
|
||||||
|
|
||||||
awaitCond(nodes("F3").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received]))
|
awaitCond(nodes("F3").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received]))
|
||||||
val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("F3").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
|
val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("F3").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
|
||||||
|
@ -586,15 +589,15 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
|
||||||
|
|
||||||
awaitCond(nodes("G").nodeParams.db.audit.listRelayed(start, Platform.currentTime).exists(_.paymentHash == pr.paymentHash))
|
awaitCond(nodes("G").nodeParams.db.audit.listRelayed(start, Platform.currentTime).exists(_.paymentHash == pr.paymentHash))
|
||||||
val relayed = nodes("G").nodeParams.db.audit.listRelayed(start, Platform.currentTime).filter(_.paymentHash == pr.paymentHash).head
|
val relayed = nodes("G").nodeParams.db.audit.listRelayed(start, Platform.currentTime).filter(_.paymentHash == pr.paymentHash).head
|
||||||
assert(relayed.amountIn - relayed.amountOut > 0.msat)
|
assert(relayed.amountIn - relayed.amountOut > 0.msat, relayed)
|
||||||
assert(relayed.amountIn - relayed.amountOut < 1000000.msat)
|
assert(relayed.amountIn - relayed.amountOut < 1000000.msat, relayed)
|
||||||
|
|
||||||
val outgoingSuccess = nodes("B").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(p => p.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])
|
val outgoingSuccess = nodes("B").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(p => p.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])
|
||||||
outgoingSuccess.foreach { case OutgoingPayment(_, _, _, _, _, _, _, recipientNodeId, _, _, OutgoingPaymentStatus.Succeeded(_, _, route, _)) =>
|
outgoingSuccess.foreach { case p@OutgoingPayment(_, _, _, _, _, _, _, recipientNodeId, _, _, OutgoingPaymentStatus.Succeeded(_, _, route, _)) =>
|
||||||
assert(recipientNodeId === nodes("F3").nodeParams.nodeId)
|
assert(recipientNodeId === nodes("F3").nodeParams.nodeId, p)
|
||||||
assert(route.lastOption === Some(HopSummary(nodes("G").nodeParams.nodeId, nodes("F3").nodeParams.nodeId)))
|
assert(route.lastOption === Some(HopSummary(nodes("G").nodeParams.nodeId, nodes("F3").nodeParams.nodeId)), p)
|
||||||
}
|
}
|
||||||
assert(outgoingSuccess.map(_.amount).sum === amount + 1000000.msat)
|
assert(outgoingSuccess.map(_.amount).sum === amount + 1000000.msat, outgoingSuccess)
|
||||||
}
|
}
|
||||||
|
|
||||||
test("send a trampoline payment D->B (via trampoline C)") {
|
test("send a trampoline payment D->B (via trampoline C)") {
|
||||||
|
@ -610,11 +613,11 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
|
||||||
sender.send(nodes("D").paymentInitiator, payment)
|
sender.send(nodes("D").paymentInitiator, payment)
|
||||||
val paymentId = sender.expectMsgType[UUID](30 seconds)
|
val paymentId = sender.expectMsgType[UUID](30 seconds)
|
||||||
val paymentSent = sender.expectMsgType[PaymentSent](30 seconds)
|
val paymentSent = sender.expectMsgType[PaymentSent](30 seconds)
|
||||||
assert(paymentSent.id === paymentId)
|
assert(paymentSent.id === paymentId, paymentSent)
|
||||||
assert(paymentSent.paymentHash === pr.paymentHash)
|
assert(paymentSent.paymentHash === pr.paymentHash, paymentSent)
|
||||||
assert(paymentSent.recipientAmount === amount)
|
assert(paymentSent.recipientAmount === amount, paymentSent)
|
||||||
assert(paymentSent.feesPaid === 300000.msat)
|
assert(paymentSent.feesPaid === 300000.msat, paymentSent)
|
||||||
assert(paymentSent.nonTrampolineFees === 0.msat)
|
assert(paymentSent.nonTrampolineFees === 0.msat, paymentSent)
|
||||||
|
|
||||||
awaitCond(nodes("B").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received]))
|
awaitCond(nodes("B").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received]))
|
||||||
val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("B").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
|
val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("B").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
|
||||||
|
@ -622,18 +625,20 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
|
||||||
|
|
||||||
awaitCond(nodes("C").nodeParams.db.audit.listRelayed(start, Platform.currentTime).exists(_.paymentHash == pr.paymentHash))
|
awaitCond(nodes("C").nodeParams.db.audit.listRelayed(start, Platform.currentTime).exists(_.paymentHash == pr.paymentHash))
|
||||||
val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, Platform.currentTime).filter(_.paymentHash == pr.paymentHash).head
|
val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, Platform.currentTime).filter(_.paymentHash == pr.paymentHash).head
|
||||||
assert(relayed.amountIn - relayed.amountOut > 0.msat)
|
assert(relayed.amountIn - relayed.amountOut > 0.msat, relayed)
|
||||||
assert(relayed.amountIn - relayed.amountOut < 300000.msat)
|
assert(relayed.amountIn - relayed.amountOut < 300000.msat, relayed)
|
||||||
|
|
||||||
val outgoingSuccess = nodes("D").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(p => p.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])
|
val outgoingSuccess = nodes("D").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(p => p.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])
|
||||||
outgoingSuccess.foreach { case OutgoingPayment(_, _, _, _, _, _, _, recipientNodeId, _, _, OutgoingPaymentStatus.Succeeded(_, _, route, _)) =>
|
outgoingSuccess.foreach { case p@OutgoingPayment(_, _, _, _, _, _, _, recipientNodeId, _, _, OutgoingPaymentStatus.Succeeded(_, _, route, _)) =>
|
||||||
assert(recipientNodeId === nodes("B").nodeParams.nodeId)
|
assert(recipientNodeId === nodes("B").nodeParams.nodeId, p)
|
||||||
assert(route.lastOption === Some(HopSummary(nodes("C").nodeParams.nodeId, nodes("B").nodeParams.nodeId)))
|
assert(route.lastOption === Some(HopSummary(nodes("C").nodeParams.nodeId, nodes("B").nodeParams.nodeId)), p)
|
||||||
}
|
}
|
||||||
assert(outgoingSuccess.map(_.amount).sum === amount + 300000.msat)
|
assert(outgoingSuccess.map(_.amount).sum === amount + 300000.msat, outgoingSuccess)
|
||||||
|
|
||||||
awaitCond(nodes("D").nodeParams.db.audit.listSent(start, Platform.currentTime).nonEmpty)
|
awaitCond(nodes("D").nodeParams.db.audit.listSent(start, Platform.currentTime).nonEmpty)
|
||||||
assert(nodes("D").nodeParams.db.audit.listSent(start, Platform.currentTime) === Seq(paymentSent.copy(parts = paymentSent.parts.map(_.copy(route = None)))))
|
val sent = nodes("D").nodeParams.db.audit.listSent(start, Platform.currentTime)
|
||||||
|
assert(sent.length === 1, sent)
|
||||||
|
assert(sent.head.copy(parts = sent.head.parts.sortBy(_.timestamp)) === paymentSent.copy(parts = paymentSent.parts.map(_.copy(route = None)).sortBy(_.timestamp)), sent)
|
||||||
}
|
}
|
||||||
|
|
||||||
test("send a trampoline payment F3->A (via trampoline C, non-trampoline recipient)") {
|
test("send a trampoline payment F3->A (via trampoline C, non-trampoline recipient)") {
|
||||||
|
@ -654,10 +659,10 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
|
||||||
sender.send(nodes("F3").paymentInitiator, payment)
|
sender.send(nodes("F3").paymentInitiator, payment)
|
||||||
val paymentId = sender.expectMsgType[UUID](30 seconds)
|
val paymentId = sender.expectMsgType[UUID](30 seconds)
|
||||||
val paymentSent = sender.expectMsgType[PaymentSent](30 seconds)
|
val paymentSent = sender.expectMsgType[PaymentSent](30 seconds)
|
||||||
assert(paymentSent.id === paymentId)
|
assert(paymentSent.id === paymentId, paymentSent)
|
||||||
assert(paymentSent.paymentHash === pr.paymentHash)
|
assert(paymentSent.paymentHash === pr.paymentHash, paymentSent)
|
||||||
assert(paymentSent.recipientAmount === amount)
|
assert(paymentSent.recipientAmount === amount, paymentSent)
|
||||||
assert(paymentSent.trampolineFees === 1000000.msat)
|
assert(paymentSent.trampolineFees === 1000000.msat, paymentSent)
|
||||||
|
|
||||||
awaitCond(nodes("A").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received]))
|
awaitCond(nodes("A").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received]))
|
||||||
val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("A").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
|
val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("A").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
|
||||||
|
@ -665,15 +670,15 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
|
||||||
|
|
||||||
awaitCond(nodes("C").nodeParams.db.audit.listRelayed(start, Platform.currentTime).exists(_.paymentHash == pr.paymentHash))
|
awaitCond(nodes("C").nodeParams.db.audit.listRelayed(start, Platform.currentTime).exists(_.paymentHash == pr.paymentHash))
|
||||||
val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, Platform.currentTime).filter(_.paymentHash == pr.paymentHash).head
|
val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, Platform.currentTime).filter(_.paymentHash == pr.paymentHash).head
|
||||||
assert(relayed.amountIn - relayed.amountOut > 0.msat)
|
assert(relayed.amountIn - relayed.amountOut > 0.msat, relayed)
|
||||||
assert(relayed.amountIn - relayed.amountOut < 1000000.msat)
|
assert(relayed.amountIn - relayed.amountOut < 1000000.msat, relayed)
|
||||||
|
|
||||||
val outgoingSuccess = nodes("F3").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(p => p.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])
|
val outgoingSuccess = nodes("F3").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(p => p.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])
|
||||||
outgoingSuccess.foreach { case OutgoingPayment(_, _, _, _, _, _, _, recipientNodeId, _, _, OutgoingPaymentStatus.Succeeded(_, _, route, _)) =>
|
outgoingSuccess.foreach { case p@OutgoingPayment(_, _, _, _, _, _, _, recipientNodeId, _, _, OutgoingPaymentStatus.Succeeded(_, _, route, _)) =>
|
||||||
assert(recipientNodeId === nodes("A").nodeParams.nodeId)
|
assert(recipientNodeId === nodes("A").nodeParams.nodeId, p)
|
||||||
assert(route.lastOption === Some(HopSummary(nodes("C").nodeParams.nodeId, nodes("A").nodeParams.nodeId)))
|
assert(route.lastOption === Some(HopSummary(nodes("C").nodeParams.nodeId, nodes("A").nodeParams.nodeId)), p)
|
||||||
}
|
}
|
||||||
assert(outgoingSuccess.map(_.amount).sum === amount + 1000000.msat)
|
assert(outgoingSuccess.map(_.amount).sum === amount + 1000000.msat, outgoingSuccess)
|
||||||
}
|
}
|
||||||
|
|
||||||
test("send a trampoline payment B->D (temporary local failure at trampoline)") {
|
test("send a trampoline payment B->D (temporary local failure at trampoline)") {
|
||||||
|
@ -697,13 +702,13 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
|
||||||
sender.send(nodes("B").paymentInitiator, payment)
|
sender.send(nodes("B").paymentInitiator, payment)
|
||||||
val paymentId = sender.expectMsgType[UUID](30 seconds)
|
val paymentId = sender.expectMsgType[UUID](30 seconds)
|
||||||
val paymentFailed = sender.expectMsgType[PaymentFailed](30 seconds)
|
val paymentFailed = sender.expectMsgType[PaymentFailed](30 seconds)
|
||||||
assert(paymentFailed.id === paymentId)
|
assert(paymentFailed.id === paymentId, paymentFailed)
|
||||||
assert(paymentFailed.paymentHash === pr.paymentHash)
|
assert(paymentFailed.paymentHash === pr.paymentHash, paymentFailed)
|
||||||
|
|
||||||
assert(nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
|
assert(nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
|
||||||
val outgoingPayments = nodes("B").nodeParams.db.payments.listOutgoingPayments(paymentId)
|
val outgoingPayments = nodes("B").nodeParams.db.payments.listOutgoingPayments(paymentId)
|
||||||
assert(outgoingPayments.nonEmpty)
|
assert(outgoingPayments.nonEmpty, outgoingPayments)
|
||||||
assert(outgoingPayments.forall(p => p.status.isInstanceOf[OutgoingPaymentStatus.Failed]))
|
assert(outgoingPayments.forall(p => p.status.isInstanceOf[OutgoingPaymentStatus.Failed]), outgoingPayments)
|
||||||
}
|
}
|
||||||
|
|
||||||
test("send a trampoline payment A->D (temporary remote failure at trampoline)") {
|
test("send a trampoline payment A->D (temporary remote failure at trampoline)") {
|
||||||
|
@ -718,13 +723,13 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
|
||||||
sender.send(nodes("A").paymentInitiator, payment)
|
sender.send(nodes("A").paymentInitiator, payment)
|
||||||
val paymentId = sender.expectMsgType[UUID](30 seconds)
|
val paymentId = sender.expectMsgType[UUID](30 seconds)
|
||||||
val paymentFailed = sender.expectMsgType[PaymentFailed](30 seconds)
|
val paymentFailed = sender.expectMsgType[PaymentFailed](30 seconds)
|
||||||
assert(paymentFailed.id === paymentId)
|
assert(paymentFailed.id === paymentId, paymentFailed)
|
||||||
assert(paymentFailed.paymentHash === pr.paymentHash)
|
assert(paymentFailed.paymentHash === pr.paymentHash, paymentFailed)
|
||||||
|
|
||||||
assert(nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
|
assert(nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
|
||||||
val outgoingPayments = nodes("A").nodeParams.db.payments.listOutgoingPayments(paymentId)
|
val outgoingPayments = nodes("A").nodeParams.db.payments.listOutgoingPayments(paymentId)
|
||||||
assert(outgoingPayments.nonEmpty)
|
assert(outgoingPayments.nonEmpty, outgoingPayments)
|
||||||
assert(outgoingPayments.forall(p => p.status.isInstanceOf[OutgoingPaymentStatus.Failed]))
|
assert(outgoingPayments.forall(p => p.status.isInstanceOf[OutgoingPaymentStatus.Failed]), outgoingPayments)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -28,11 +28,11 @@ import fr.acinq.eclair.payment.PaymentPacketSpec._
|
||||||
import fr.acinq.eclair.payment.PaymentRequest.{ExtraHop, Features}
|
import fr.acinq.eclair.payment.PaymentRequest.{ExtraHop, Features}
|
||||||
import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.SendMultiPartPayment
|
import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.SendMultiPartPayment
|
||||||
import fr.acinq.eclair.payment.send.PaymentInitiator
|
import fr.acinq.eclair.payment.send.PaymentInitiator
|
||||||
import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentConfig, SendPaymentRequest, SendTrampolinePaymentRequest, TrampolineLegacyAmountLessInvoice}
|
import fr.acinq.eclair.payment.send.PaymentInitiator._
|
||||||
import fr.acinq.eclair.payment.send.PaymentLifecycle.{SendPayment, SendPaymentToRoute}
|
import fr.acinq.eclair.payment.send.PaymentLifecycle.{SendPayment, SendPaymentToRoute}
|
||||||
import fr.acinq.eclair.router.RouteParams
|
import fr.acinq.eclair.router.{NodeHop, RouteParams}
|
||||||
import fr.acinq.eclair.wire.Onion.FinalLegacyPayload
|
import fr.acinq.eclair.wire.Onion.FinalLegacyPayload
|
||||||
import fr.acinq.eclair.wire.{OnionCodecs, OnionTlv, TrampolineFeeInsufficient}
|
import fr.acinq.eclair.wire.{Onion, OnionCodecs, OnionTlv, TrampolineFeeInsufficient}
|
||||||
import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, NodeParams, TestConstants, randomBytes32, randomKey}
|
import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, NodeParams, TestConstants, randomBytes32, randomKey}
|
||||||
import org.scalatest.{Outcome, Tag, fixture}
|
import org.scalatest.{Outcome, Tag, fixture}
|
||||||
import scodec.bits.HexStringSyntax
|
import scodec.bits.HexStringSyntax
|
||||||
|
@ -83,9 +83,10 @@ class PaymentInitiatorSpec extends TestKit(ActorSystem("test")) with fixture.Fun
|
||||||
|
|
||||||
test("forward payment with pre-defined route") { f =>
|
test("forward payment with pre-defined route") { f =>
|
||||||
import f._
|
import f._
|
||||||
sender.send(initiator, SendPaymentRequest(finalAmount, paymentHash, c, 1, predefinedRoute = Seq(a, b, c)))
|
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some invoice", features = None)
|
||||||
val paymentId = sender.expectMsgType[UUID]
|
sender.send(initiator, SendPaymentToRouteRequest(finalAmount, finalAmount, None, None, pr, Channel.MIN_CLTV_EXPIRY_DELTA, Seq(a, b, c), None, 0 msat, CltvExpiryDelta(0), Nil))
|
||||||
payFsm.expectMsg(SendPaymentConfig(paymentId, paymentId, None, paymentHash, finalAmount, c, Upstream.Local(paymentId), None, storeInDb = true, publishEvent = true, Nil))
|
val payment = sender.expectMsgType[SendPaymentToRouteResponse]
|
||||||
|
payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(pr), storeInDb = true, publishEvent = true, Nil))
|
||||||
payFsm.expectMsg(SendPaymentToRoute(Seq(a, b, c), FinalLegacyPayload(finalAmount, Channel.MIN_CLTV_EXPIRY_DELTA.toCltvExpiry(nodeParams.currentBlockHeight + 1))))
|
payFsm.expectMsg(SendPaymentToRoute(Seq(a, b, c), FinalLegacyPayload(finalAmount, Channel.MIN_CLTV_EXPIRY_DELTA.toCltvExpiry(nodeParams.currentBlockHeight + 1))))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,11 +127,11 @@ class PaymentInitiatorSpec extends TestKit(ActorSystem("test")) with fixture.Fun
|
||||||
|
|
||||||
test("forward multi-part payment with pre-defined route") { f =>
|
test("forward multi-part payment with pre-defined route") { f =>
|
||||||
import f._
|
import f._
|
||||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, randomKey, "Some invoice", features = Some(Features(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional)))
|
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some invoice", features = Some(Features(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional)))
|
||||||
val req = SendPaymentRequest(finalAmount / 2, paymentHash, c, 1, paymentRequest = Some(pr), predefinedRoute = Seq(a, b, c))
|
val req = SendPaymentToRouteRequest(finalAmount / 2, finalAmount, None, None, pr, Channel.MIN_CLTV_EXPIRY_DELTA, Seq(a, b, c), None, 0 msat, CltvExpiryDelta(0), Nil)
|
||||||
sender.send(initiator, req)
|
sender.send(initiator, req)
|
||||||
val id = sender.expectMsgType[UUID]
|
val payment = sender.expectMsgType[SendPaymentToRouteResponse]
|
||||||
payFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, finalAmount / 2, c, Upstream.Local(id), Some(pr), storeInDb = true, publishEvent = true, Nil))
|
payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(pr), storeInDb = true, publishEvent = true, Nil))
|
||||||
val msg = payFsm.expectMsgType[SendPaymentToRoute]
|
val msg = payFsm.expectMsgType[SendPaymentToRoute]
|
||||||
assert(msg.hops === Seq(a, b, c))
|
assert(msg.hops === Seq(a, b, c))
|
||||||
assert(msg.finalPayload.amount === finalAmount / 2)
|
assert(msg.finalPayload.amount === finalAmount / 2)
|
||||||
|
@ -286,4 +287,32 @@ class PaymentInitiatorSpec extends TestKit(ActorSystem("test")) with fixture.Fun
|
||||||
eventListener.expectNoMsg(100 millis)
|
eventListener.expectNoMsg(100 millis)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test("forward trampoline payment with pre-defined route") { f =>
|
||||||
|
import f._
|
||||||
|
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some invoice")
|
||||||
|
val trampolineFees = 100 msat
|
||||||
|
val req = SendPaymentToRouteRequest(finalAmount + trampolineFees, finalAmount, None, None, pr, Channel.MIN_CLTV_EXPIRY_DELTA, Seq(a, b), None, trampolineFees, CltvExpiryDelta(144), Seq(b, c))
|
||||||
|
sender.send(initiator, req)
|
||||||
|
val payment = sender.expectMsgType[SendPaymentToRouteResponse]
|
||||||
|
assert(payment.trampolineSecret.nonEmpty)
|
||||||
|
payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(pr), storeInDb = true, publishEvent = true, Seq(NodeHop(b, c, CltvExpiryDelta(0), 0 msat))))
|
||||||
|
val msg = payFsm.expectMsgType[SendPaymentToRoute]
|
||||||
|
assert(msg.hops === Seq(a, b))
|
||||||
|
assert(msg.finalPayload.amount === finalAmount + trampolineFees)
|
||||||
|
assert(msg.finalPayload.paymentSecret === payment.trampolineSecret)
|
||||||
|
assert(msg.finalPayload.totalAmount === finalAmount + trampolineFees)
|
||||||
|
assert(msg.finalPayload.isInstanceOf[Onion.FinalTlvPayload])
|
||||||
|
val trampolineOnion = msg.finalPayload.asInstanceOf[Onion.FinalTlvPayload].records.get[OnionTlv.TrampolineOnion]
|
||||||
|
assert(trampolineOnion.nonEmpty)
|
||||||
|
|
||||||
|
// Verify that the trampoline node can correctly peel the trampoline onion.
|
||||||
|
val Right(decrypted) = Sphinx.TrampolinePacket.peel(priv_b.privateKey, pr.paymentHash, trampolineOnion.get.packet)
|
||||||
|
assert(!decrypted.isLastPacket)
|
||||||
|
val trampolinePayload = OnionCodecs.nodeRelayPerHopPayloadCodec.decode(decrypted.payload.bits).require.value
|
||||||
|
assert(trampolinePayload.amountToForward === finalAmount)
|
||||||
|
assert(trampolinePayload.totalAmount === finalAmount)
|
||||||
|
assert(trampolinePayload.outgoingNodeId === c)
|
||||||
|
assert(trampolinePayload.paymentSecret === pr.paymentSecret)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -261,7 +261,6 @@ object CustomTypeHints {
|
||||||
classOf[OutgoingPaymentStatus.Succeeded] -> "sent"
|
classOf[OutgoingPaymentStatus.Succeeded] -> "sent"
|
||||||
))
|
))
|
||||||
|
|
||||||
// TODO: @t-bast: don't forget to update slate-doc
|
|
||||||
val paymentEvent = CustomTypeHints(Map(
|
val paymentEvent = CustomTypeHints(Map(
|
||||||
classOf[PaymentSent] -> "payment-sent",
|
classOf[PaymentSent] -> "payment-sent",
|
||||||
classOf[ChannelPaymentRelayed] -> "payment-relayed",
|
classOf[ChannelPaymentRelayed] -> "payment-relayed",
|
||||||
|
|
|
@ -221,18 +221,6 @@ trait Service extends ExtraDirectives with Logging {
|
||||||
case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using the field 'amountMsat'"))
|
case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using the field 'amountMsat'"))
|
||||||
}
|
}
|
||||||
} ~
|
} ~
|
||||||
// TODO: @t-bast: remove this API once stabilized: should re-work the payment APIs to integrate Trampoline nicely
|
|
||||||
// - payinvoice: should stay the same, it's the easy flow where the node does its magic
|
|
||||||
// - sendtonode: exactly like payinvoice but without an invoice: let the node do its magic
|
|
||||||
// - sendtoroute: no path-finding, lets the user control exactly how to send (provide multiple routes, with trampoline or not, etc) -> maybe doesn't go through normal PayFSM (avoid retries)
|
|
||||||
// -> maybe somehow make one call per partial HTLC (allows easier failure reporting and out-of-node retry logic)?
|
|
||||||
// -> needs both trampolineRoute and routeToTrampoline arguments?
|
|
||||||
path("sendtotrampoline") {
|
|
||||||
formFields(invoiceFormParam, "trampolineId".as[PublicKey], "trampolineFeesMsat".as[MilliSatoshi], "trampolineExpiryDelta".as[Int]) {
|
|
||||||
(invoice, trampolineId, trampolineFees, trampolineExpiryDelta) =>
|
|
||||||
complete(eclairApi.sendToTrampoline(invoice, trampolineId, trampolineFees, CltvExpiryDelta(trampolineExpiryDelta)))
|
|
||||||
}
|
|
||||||
} ~
|
|
||||||
path("sendtonode") {
|
path("sendtonode") {
|
||||||
formFields(amountMsatFormParam, paymentHashFormParam, nodeIdFormParam, "maxAttempts".as[Int].?, "feeThresholdSat".as[Satoshi].?, "maxFeePct".as[Double].?, "externalId".?) {
|
formFields(amountMsatFormParam, paymentHashFormParam, nodeIdFormParam, "maxAttempts".as[Int].?, "feeThresholdSat".as[Satoshi].?, "maxFeePct".as[Double].?, "externalId".?) {
|
||||||
(amountMsat, paymentHash, nodeId, maxAttempts_opt, feeThresholdSat_opt, maxFeePct_opt, externalId_opt) =>
|
(amountMsat, paymentHash, nodeId, maxAttempts_opt, feeThresholdSat_opt, maxFeePct_opt, externalId_opt) =>
|
||||||
|
@ -240,9 +228,9 @@ trait Service extends ExtraDirectives with Logging {
|
||||||
}
|
}
|
||||||
} ~
|
} ~
|
||||||
path("sendtoroute") {
|
path("sendtoroute") {
|
||||||
formFields(amountMsatFormParam, paymentHashFormParam, "finalCltvExpiry".as[Int], "route".as[List[PublicKey]](pubkeyListUnmarshaller), "externalId".?, invoiceFormParam.?) {
|
formFields(amountMsatFormParam, "recipientAmountMsat".as[MilliSatoshi].?, invoiceFormParam, "finalCltvExpiry".as[Int], "route".as[List[PublicKey]](pubkeyListUnmarshaller), "externalId".?, "parentId".as[UUID].?, "trampolineSecret".as[ByteVector32].?, "trampolineFeesMsat".as[MilliSatoshi].?, "trampolineCltvExpiry".as[Int].?, "trampolineNodes".as[List[PublicKey]](pubkeyListUnmarshaller).?) {
|
||||||
(amountMsat, paymentHash, finalCltvExpiry, route, externalId_opt, invoice_opt) =>
|
(amountMsat, recipientAmountMsat_opt, invoice, finalCltvExpiry, route, externalId_opt, parentId_opt, trampolineSecret_opt, trampolineFeesMsat_opt, trampolineCltvExpiry_opt, trampolineNodes_opt) =>
|
||||||
complete(eclairApi.sendToRoute(externalId_opt, route, amountMsat, paymentHash, CltvExpiryDelta(finalCltvExpiry), invoice_opt))
|
complete(eclairApi.sendToRoute(amountMsat, recipientAmountMsat_opt, externalId_opt, parentId_opt, invoice, CltvExpiryDelta(finalCltvExpiry), route, trampolineSecret_opt, trampolineFeesMsat_opt, trampolineCltvExpiry_opt.map(CltvExpiryDelta), trampolineNodes_opt.getOrElse(Nil)))
|
||||||
}
|
}
|
||||||
} ~
|
} ~
|
||||||
path("getsentinfo") {
|
path("getsentinfo") {
|
||||||
|
|
|
@ -30,10 +30,11 @@ import de.heikoseeberger.akkahttpjson4s.Json4sSupport
|
||||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||||
import fr.acinq.bitcoin.{Block, ByteVector32}
|
import fr.acinq.bitcoin.{Block, ByteVector32}
|
||||||
import fr.acinq.eclair._
|
import fr.acinq.eclair._
|
||||||
import fr.acinq.eclair.db.{IncomingPayment, IncomingPaymentStatus, OutgoingPayment, OutgoingPaymentStatus, PaymentType}
|
import fr.acinq.eclair.db._
|
||||||
import fr.acinq.eclair.io.NodeURI
|
import fr.acinq.eclair.io.NodeURI
|
||||||
import fr.acinq.eclair.io.Peer.PeerInfo
|
import fr.acinq.eclair.io.Peer.PeerInfo
|
||||||
import fr.acinq.eclair.payment.relay.Relayer.UsableBalance
|
import fr.acinq.eclair.payment.relay.Relayer.UsableBalance
|
||||||
|
import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentToRouteResponse
|
||||||
import fr.acinq.eclair.payment.{PaymentFailed, _}
|
import fr.acinq.eclair.payment.{PaymentFailed, _}
|
||||||
import fr.acinq.eclair.router.{NetworkStats, Stats}
|
import fr.acinq.eclair.router.{NetworkStats, Stats}
|
||||||
import fr.acinq.eclair.wire.NodeAddress
|
import fr.acinq.eclair.wire.NodeAddress
|
||||||
|
@ -400,8 +401,8 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest with IdiomaticMock
|
||||||
}
|
}
|
||||||
|
|
||||||
test("'sendtoroute' method should accept a both a json-encoded AND comma separated list of pubkeys") {
|
test("'sendtoroute' method should accept a both a json-encoded AND comma separated list of pubkeys") {
|
||||||
val rawUUID = "487da196-a4dc-4b1e-92b4-3e5e905e9f3f"
|
val payment = SendPaymentToRouteResponse(UUID.fromString("487da196-a4dc-4b1e-92b4-3e5e905e9f3f"), UUID.fromString("2ad8c6d7-99cb-4238-8f67-89024b8eed0d"), None)
|
||||||
val paymentUUID = UUID.fromString(rawUUID)
|
val expected = """{"paymentId":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","parentId":"2ad8c6d7-99cb-4238-8f67-89024b8eed0d"}"""
|
||||||
val externalId = UUID.randomUUID().toString
|
val externalId = UUID.randomUUID().toString
|
||||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(1234 msat), ByteVector32.Zeroes, randomKey, "Some invoice")
|
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(1234 msat), ByteVector32.Zeroes, randomKey, "Some invoice")
|
||||||
val expectedRoute = List(PublicKey(hex"0217eb8243c95f5a3b7d4c5682d10de354b7007eb59b6807ae407823963c7547a9"), PublicKey(hex"0242a4ae0c5bef18048fbecf995094b74bfb0f7391418d71ed394784373f41e4f3"), PublicKey(hex"026ac9fcd64fb1aa1c491fc490634dc33da41d4a17b554e0adf1b32fee88ee9f28"))
|
val expectedRoute = List(PublicKey(hex"0217eb8243c95f5a3b7d4c5682d10de354b7007eb59b6807ae407823963c7547a9"), PublicKey(hex"0242a4ae0c5bef18048fbecf995094b74bfb0f7391418d71ed394784373f41e4f3"), PublicKey(hex"026ac9fcd64fb1aa1c491fc490634dc33da41d4a17b554e0adf1b32fee88ee9f28"))
|
||||||
|
@ -409,30 +410,30 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest with IdiomaticMock
|
||||||
val jsonNodes = serialization.write(expectedRoute)
|
val jsonNodes = serialization.write(expectedRoute)
|
||||||
|
|
||||||
val eclair = mock[Eclair]
|
val eclair = mock[Eclair]
|
||||||
eclair.sendToRoute(any[Option[String]], any[List[PublicKey]], any[MilliSatoshi], any[ByteVector32], any[CltvExpiryDelta], any[Option[PaymentRequest]])(any[Timeout]) returns Future.successful(paymentUUID)
|
eclair.sendToRoute(any[MilliSatoshi], any[Option[MilliSatoshi]], any[Option[String]], any[Option[UUID]], any[PaymentRequest], any[CltvExpiryDelta], any[List[PublicKey]], any[Option[ByteVector32]], any[Option[MilliSatoshi]], any[Option[CltvExpiryDelta]], any[List[PublicKey]])(any[Timeout]) returns Future.successful(payment)
|
||||||
val mockService = new MockService(eclair)
|
val mockService = new MockService(eclair)
|
||||||
|
|
||||||
Post("/sendtoroute", FormData("route" -> jsonNodes, "amountMsat" -> "1234", "paymentHash" -> ByteVector32.Zeroes.toHex, "finalCltvExpiry" -> "190", "externalId" -> externalId.toString, "invoice" -> PaymentRequest.write(pr)).toEntity) ~>
|
Post("/sendtoroute", FormData("route" -> jsonNodes, "amountMsat" -> "1234", "finalCltvExpiry" -> "190", "externalId" -> externalId.toString, "invoice" -> PaymentRequest.write(pr)).toEntity) ~>
|
||||||
addCredentials(BasicHttpCredentials("", mockService.password)) ~>
|
addCredentials(BasicHttpCredentials("", mockService.password)) ~>
|
||||||
addHeader("Content-Type", "application/json") ~>
|
addHeader("Content-Type", "application/json") ~>
|
||||||
Route.seal(mockService.route) ~>
|
Route.seal(mockService.route) ~>
|
||||||
check {
|
check {
|
||||||
assert(handled)
|
assert(handled)
|
||||||
assert(status == OK)
|
assert(status == OK)
|
||||||
assert(entityAs[String] == "\"" + rawUUID + "\"")
|
assert(entityAs[String] == expected)
|
||||||
eclair.sendToRoute(Some(externalId), expectedRoute, 1234 msat, ByteVector32.Zeroes, CltvExpiryDelta(190), Some(pr))(any[Timeout]).wasCalled(once)
|
eclair.sendToRoute(1234 msat, None, Some(externalId), None, pr, CltvExpiryDelta(190), expectedRoute, None, None, None, Nil)(any[Timeout]).wasCalled(once)
|
||||||
}
|
}
|
||||||
|
|
||||||
// this test uses CSV encoded route
|
// this test uses CSV encoded route
|
||||||
Post("/sendtoroute", FormData("route" -> csvNodes, "amountMsat" -> "1234", "paymentHash" -> ByteVector32.One.toHex, "finalCltvExpiry" -> "190").toEntity) ~>
|
Post("/sendtoroute", FormData("route" -> csvNodes, "amountMsat" -> "1234", "finalCltvExpiry" -> "190", "invoice" -> PaymentRequest.write(pr)).toEntity) ~>
|
||||||
addCredentials(BasicHttpCredentials("", mockService.password)) ~>
|
addCredentials(BasicHttpCredentials("", mockService.password)) ~>
|
||||||
addHeader("Content-Type", "application/json") ~>
|
addHeader("Content-Type", "application/json") ~>
|
||||||
Route.seal(mockService.route) ~>
|
Route.seal(mockService.route) ~>
|
||||||
check {
|
check {
|
||||||
assert(handled)
|
assert(handled)
|
||||||
assert(status == OK)
|
assert(status == OK)
|
||||||
assert(entityAs[String] == "\"" + rawUUID + "\"")
|
assert(entityAs[String] == expected)
|
||||||
eclair.sendToRoute(None, expectedRoute, 1234 msat, ByteVector32.One, CltvExpiryDelta(190), None)(any[Timeout]).wasCalled(once)
|
eclair.sendToRoute(1234 msat, None, None, None, pr, CltvExpiryDelta(190), expectedRoute, None, None, None, Nil)(any[Timeout]).wasCalled(once)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -440,7 +441,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest with IdiomaticMock
|
||||||
val capStat = Stats(30 sat, 12 sat, 14 sat, 20 sat, 40 sat, 46 sat, 48 sat)
|
val capStat = Stats(30 sat, 12 sat, 14 sat, 20 sat, 40 sat, 46 sat, 48 sat)
|
||||||
val cltvStat = Stats(CltvExpiryDelta(32), CltvExpiryDelta(11), CltvExpiryDelta(13), CltvExpiryDelta(22), CltvExpiryDelta(42), CltvExpiryDelta(51), CltvExpiryDelta(53))
|
val cltvStat = Stats(CltvExpiryDelta(32), CltvExpiryDelta(11), CltvExpiryDelta(13), CltvExpiryDelta(22), CltvExpiryDelta(42), CltvExpiryDelta(51), CltvExpiryDelta(53))
|
||||||
val feeBaseStat = Stats(32 msat, 11 msat, 13 msat, 22 msat, 42 msat, 51 msat, 53 msat)
|
val feeBaseStat = Stats(32 msat, 11 msat, 13 msat, 22 msat, 42 msat, 51 msat, 53 msat)
|
||||||
val feePropStat = Stats(32l, 11l, 13l, 22l, 42l, 51l, 53l)
|
val feePropStat = Stats(32L, 11L, 13L, 22L, 42L, 51L, 53L)
|
||||||
val networkStats = new NetworkStats(1, 2, capStat, cltvStat, feeBaseStat, feePropStat)
|
val networkStats = new NetworkStats(1, 2, capStat, cltvStat, feeBaseStat, feePropStat)
|
||||||
|
|
||||||
val eclair = mock[Eclair]
|
val eclair = mock[Eclair]
|
||||||
|
|
Loading…
Add table
Reference in a new issue