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
8 changes: 4 additions & 4 deletions eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ trait Eclair {

def receivedInfo(paymentHash: ByteVector32)(implicit timeout: Timeout): Future[Option[IncomingPayment]]

def send(recipientNodeId: PublicKey, amount: MilliSatoshi, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, minFinalCltvExpiry_opt: Option[Long] = None, maxAttempts_opt: Option[Int] = None, feeThresholdSat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None)(implicit timeout: Timeout): Future[UUID]
def send(recipientNodeId: PublicKey, amount: MilliSatoshi, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, minFinalCltvExpiry_opt: Option[Long] = None, maxAttempts_opt: Option[Int] = None, feeThresholdSat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None, userProvidedPaymentId_opt: Option[UUID] = None)(implicit timeout: Timeout): Future[UUID]

def sentInfo(id: Either[UUID, ByteVector32])(implicit timeout: Timeout): Future[Seq[OutgoingPayment]]

Expand Down Expand Up @@ -190,7 +190,7 @@ class EclairImpl(appKit: Kit) extends Eclair {
(appKit.paymentInitiator ? SendPaymentToRoute(amount, paymentHash, route, finalCltvExpiry)).mapTo[UUID]
}

override def send(recipientNodeId: PublicKey, amount: MilliSatoshi, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, minFinalCltvExpiry_opt: Option[Long], maxAttempts_opt: Option[Int], feeThreshold_opt: Option[Satoshi], maxFeePct_opt: Option[Double])(implicit timeout: Timeout): Future[UUID] = {
override def send(recipientNodeId: PublicKey, amount: MilliSatoshi, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, minFinalCltvExpiry_opt: Option[Long], maxAttempts_opt: Option[Int], feeThreshold_opt: Option[Satoshi], maxFeePct_opt: Option[Double], userProvidedPaymentId_opt: Option[UUID])(implicit timeout: Timeout): Future[UUID] = {
val maxAttempts = maxAttempts_opt.getOrElse(appKit.nodeParams.maxPaymentAttempts)

val defaultRouteParams = Router.getDefaultRouteParams(appKit.nodeParams.routerConf)
Expand All @@ -200,8 +200,8 @@ class EclairImpl(appKit: Kit) extends Eclair {
)

val sendPayment = minFinalCltvExpiry_opt match {
case Some(minCltv) => SendPayment(amount, paymentHash, recipientNodeId, assistedRoutes, finalCltvExpiry = minCltv, maxAttempts = maxAttempts, routeParams = Some(routeParams))
case None => SendPayment(amount, paymentHash, recipientNodeId, assistedRoutes, maxAttempts = maxAttempts, routeParams = Some(routeParams))
case Some(minCltv) => SendPayment(amount, paymentHash, recipientNodeId, assistedRoutes, finalCltvExpiry = minCltv, maxAttempts = maxAttempts, routeParams = Some(routeParams), userProvidedUUID = userProvidedPaymentId_opt)
case None => SendPayment(amount, paymentHash, recipientNodeId, assistedRoutes, maxAttempts = maxAttempts, routeParams = Some(routeParams), userProvidedUUID = userProvidedPaymentId_opt)
}
(appKit.paymentInitiator ? sendPayment).mapTo[UUID]
}
Expand Down
15 changes: 8 additions & 7 deletions eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala
Original file line number Diff line number Diff line change
Expand Up @@ -222,17 +222,18 @@ trait Service extends ExtraDirectives with Logging {
}
} ~
path("payinvoice") {
formFields(invoiceFormParam, amountMsatFormParam.?, "maxAttempts".as[Int].?, "feeThresholdSat".as[Satoshi].?, "maxFeePct".as[Double].?) {
case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None, maxAttempts, feeThresholdSat_opt, maxFeePct_opt) =>
complete(eclairApi.send(nodeId, amount, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry, maxAttempts, feeThresholdSat_opt, maxFeePct_opt))
case (invoice, Some(overrideAmount), maxAttempts, feeThresholdSat_opt, maxFeePct_opt) =>
complete(eclairApi.send(invoice.nodeId, overrideAmount, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry, maxAttempts, feeThresholdSat_opt, maxFeePct_opt))
formFields(invoiceFormParam, amountMsatFormParam.?, "maxAttempts".as[Int].?, "feeThresholdSat".as[Satoshi].?, "maxFeePct".as[Double].?, "userProvidedPaymentId".as[UUID].?) {
case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None, maxAttempts_opt, feeThresholdSat_opt, maxFeePct_opt, userProvidedPaymentId_opt) =>
complete(eclairApi.send(nodeId, amount, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry, maxAttempts_opt, feeThresholdSat_opt, maxFeePct_opt, userProvidedPaymentId_opt))
case (invoice, Some(overrideAmount), maxAttempts_opt, feeThresholdSat_opt, maxFeePct_opt, userProvidedPaymentId_opt) =>
complete(eclairApi.send(invoice.nodeId, overrideAmount, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry, maxAttempts_opt, feeThresholdSat_opt, maxFeePct_opt, userProvidedPaymentId_opt))
case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using the field 'amountMsat'"))
}
} ~
path("sendtonode") {
formFields(amountMsatFormParam, paymentHashFormParam, nodeIdFormParam, "maxAttempts".as[Int].?, "feeThresholdSat".as[Satoshi].?, "maxFeePct".as[Double].?) { (amountMsat, paymentHash, nodeId, maxAttempts_opt, feeThresholdSat_opt, maxFeePct_opt) =>
complete(eclairApi.send(nodeId, amountMsat, paymentHash, maxAttempts_opt = maxAttempts_opt, feeThresholdSat_opt = feeThresholdSat_opt, maxFeePct_opt = maxFeePct_opt))
formFields(amountMsatFormParam, paymentHashFormParam, nodeIdFormParam, "maxAttempts".as[Int].?, "feeThresholdSat".as[Satoshi].?, "maxFeePct".as[Double].?, "userProvidedPaymentId".as[UUID].?) {
(amountMsat, paymentHash, nodeId, maxAttempts_opt, feeThresholdSat_opt, maxFeePct_opt, userProvidedPaymentId_opt) =>
complete(eclairApi.send(nodeId, amountMsat, paymentHash, maxAttempts_opt = maxAttempts_opt, feeThresholdSat_opt = feeThresholdSat_opt, maxFeePct_opt = maxFeePct_opt, userProvidedPaymentId_opt = userProvidedPaymentId_opt))
}
} ~
path("sendtoroute") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
package fr.acinq.eclair.payment

import java.util.UUID
import akka.actor.{Actor, ActorLogging, ActorRef, Props}

import akka.actor.{Actor, ActorLogging, ActorRef, Props, Status}
import fr.acinq.eclair.NodeParams
import fr.acinq.eclair.payment.PaymentLifecycle.GenericSendPayment

Expand All @@ -28,12 +29,21 @@ class PaymentInitiator(nodeParams: NodeParams, router: ActorRef, register: Actor

override def receive: Receive = {
case c: GenericSendPayment =>
val paymentId = UUID.randomUUID()
val payFsm = context.actorOf(PaymentLifecycle.props(nodeParams, paymentId, router, register))
payFsm forward c
sender ! paymentId
c.userProvidedUUID match {
case Some(uuid) if nodeParams.db.payments.getOutgoingPayment(uuid).isDefined =>
sender ! Status.Failure(new RuntimeException(s"User provided paymentId '$uuid' already exists in a database"))
case Some(uuid) =>
onSuccess(sender, c, uuid)
case None =>
onSuccess(sender, c, UUID.randomUUID())
}
}

def onSuccess(to: ActorRef, c: GenericSendPayment, paymentId: UUID): Unit = {
val payFsm = context.actorOf(PaymentLifecycle.props(nodeParams, paymentId, router, register))
payFsm forward c
to ! paymentId
}
}

object PaymentInitiator {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,15 +199,23 @@ object PaymentLifecycle {

// @formatter:off
case class ReceivePayment(amount_opt: Option[MilliSatoshi], description: String, expirySeconds_opt: Option[Long] = None, extraHops: List[List[ExtraHop]] = Nil, fallbackAddress: Option[String] = None, paymentPreimage: Option[ByteVector32] = None)
sealed trait GenericSendPayment
case class SendPaymentToRoute(amount: MilliSatoshi, paymentHash: ByteVector32, hops: Seq[PublicKey], finalCltvExpiry: Long = Channel.MIN_CLTV_EXPIRY) extends GenericSendPayment
sealed trait GenericSendPayment {
val userProvidedUUID: Option[UUID]
}
case class SendPaymentToRoute(amount: MilliSatoshi,
paymentHash: ByteVector32,
hops: Seq[PublicKey],
finalCltvExpiry: Long = Channel.MIN_CLTV_EXPIRY,
userProvidedUUID: Option[UUID] = None) extends GenericSendPayment

case class SendPayment(amount: MilliSatoshi,
paymentHash: ByteVector32,
targetNodeId: PublicKey,
assistedRoutes: Seq[Seq[ExtraHop]] = Nil,
finalCltvExpiry: Long = Channel.MIN_CLTV_EXPIRY,
maxAttempts: Int,
routeParams: Option[RouteParams] = None) extends GenericSendPayment {
routeParams: Option[RouteParams] = None,
userProvidedUUID: Option[UUID] = None) extends GenericSendPayment {
require(amount > MilliSatoshi(0), s"amountMsat must be > 0")
}

Expand Down
12 changes: 12 additions & 0 deletions eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package fr.acinq.eclair

import java.util.UUID

import akka.actor.ActorSystem
import akka.testkit.{TestKit, TestProbe}
import fr.acinq.bitcoin.{ByteVector32, Crypto, Satoshi}
Expand All @@ -35,6 +37,7 @@ import fr.acinq.eclair.channel._
import fr.acinq.eclair.db._
import fr.acinq.eclair.router.RouteCalculationSpec.makeUpdate
import org.mockito.scalatest.IdiomaticMockito

import scala.concurrent.Await
import scala.util.{Failure, Success}
import scala.concurrent.duration._
Expand Down Expand Up @@ -126,6 +129,15 @@ class EclairImplSpec extends TestKit(ActorSystem("mySystem")) with fixture.FunSu
assert(send3.paymentHash == ByteVector32.Zeroes)
assert(send3.routeParams.get.maxFeeBase == Satoshi(123).toMilliSatoshi) // conversion sat -> msat
assert(send3.routeParams.get.maxFeePct == 4.20)

// with user defined UUID
val uuid = UUID.randomUUID()
eclair.send(recipientNodeId = nodeId, amount = MilliSatoshi(123), paymentHash = ByteVector32.Zeroes, minFinalCltvExpiry_opt = None, userProvidedPaymentId_opt = Some(uuid))
val send4 = paymentInitiator.expectMsgType[SendPayment]
assert(send4.targetNodeId == nodeId)
assert(send4.amount == MilliSatoshi(123))
assert(send4.paymentHash == ByteVector32.Zeroes)
assert(send4.userProvidedUUID.contains(uuid))
}

test("allupdates can filter by nodeId") { f =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest with IdiomaticMock
val invoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqvk2plh"

val eclair = mock[Eclair]
eclair.send(any, any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(UUID.randomUUID())
eclair.send(any, any, any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(UUID.randomUUID())
val mockService = new MockService(eclair)

Post("/payinvoice", FormData("invoice" -> invoice).toEntity) ~>
Expand All @@ -270,19 +270,27 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest with IdiomaticMock
check {
assert(handled)
assert(status == OK)
eclair.send(any, MilliSatoshi(1258000), any, any, any, any, any, any)(any[Timeout]).wasCalled(once)
eclair.send(any, MilliSatoshi(1258000), any, any, any, any, any, any, any)(any[Timeout]).wasCalled(once)
}


Post("/payinvoice", FormData("invoice" -> invoice, "amountMsat" -> "123", "feeThresholdSat" -> "112233", "maxFeePct" -> "2.34").toEntity) ~>
addCredentials(BasicHttpCredentials("", mockService.password)) ~>
Route.seal(mockService.route) ~>
check {
assert(handled)
assert(status == OK)
eclair.send(any, MilliSatoshi(123), any, any, any, any, Some(Satoshi(112233)), Some(2.34))(any[Timeout]).wasCalled(once)
eclair.send(any, MilliSatoshi(123), any, any, any, any, Some(Satoshi(112233)), Some(2.34), any)(any[Timeout]).wasCalled(once)
}

val uuid = UUID.randomUUID()
Post("/payinvoice", FormData("invoice" -> invoice, "amountMsat" -> "123", "feeThresholdSat" -> "112233", "maxFeePct" -> "2.34", "userProvidedPaymentId" -> uuid.toString).toEntity) ~>
addCredentials(BasicHttpCredentials("", mockService.password)) ~>
Route.seal(mockService.route) ~>
check {
assert(handled)
assert(status == OK)
eclair.send(any, MilliSatoshi(123), any, any, any, any, Some(Satoshi(112233)), Some(2.34), Some(uuid))(any[Timeout]).wasCalled(once)
}
}

test("'getreceivedinfo' method should respond HTTP 404 with a JSON encoded response if the element is not found") {
Expand Down