diff --git a/eclair-rpc-test/src/test/scala/org/bitcoins/eclair/rpc/EclairRpcClientTest.scala b/eclair-rpc-test/src/test/scala/org/bitcoins/eclair/rpc/EclairRpcClientTest.scala index abf7e6a644..a05d67e0ec 100644 --- a/eclair-rpc-test/src/test/scala/org/bitcoins/eclair/rpc/EclairRpcClientTest.scala +++ b/eclair-rpc-test/src/test/scala/org/bitcoins/eclair/rpc/EclairRpcClientTest.scala @@ -560,8 +560,8 @@ class EclairRpcClientTest extends AsyncFlatSpec with BeforeAndAfterAll { } yield { assert(succeeded.nonEmpty) - assert(received.paymentHash == invoice.lnTags.paymentHash.hash) - assert(received.amountMsat == amt) + assert(received.get.paymentHash == invoice.lnTags.paymentHash.hash) + assert(received.get.amountMsat == amt) val succeededPayment = succeeded.head assert(succeededPayment.status == PaymentStatus.SUCCEEDED) @@ -695,6 +695,45 @@ class EclairRpcClientTest extends AsyncFlatSpec with BeforeAndAfterAll { } } + it should "fail to get received info about an invoice that hasn't been paid too, and then sucessfully get the info after the payment happened" in { + val amt = 1000.msat + for { + c1 <- clientF + c2 <- otherClientF + invoice <- c2.createInvoice(s"invoice-payment") + receiveOpt <- c2.getReceivedInfo(invoice) + _ = assert(receiveOpt.isEmpty) + _ <- c1.payInvoice(invoice, amt) + _ <- AsyncUtil.retryUntilSatisfiedF( + () => c2.getReceivedInfo(invoice).map(_.isDefined), + 1.seconds) + receivedAgainOpt <- c2.getReceivedInfo(invoice) + } yield { + assert(receivedAgainOpt.isDefined) + assert(receivedAgainOpt.get.amountMsat == amt) + assert( + receivedAgainOpt.get.paymentHash == invoice.lnTags.paymentHash.hash) + } + } + + it should "monitor an invoice" in { + val amt = 1000.msat + val test = (client: EclairRpcClient, otherClient: EclairRpcClient) => { + val invoiceF = otherClient.createInvoice("monitor an invoice", amt) + val paidF = invoiceF.flatMap(i => client.payInvoice(i)) + val monitorF = invoiceF.flatMap(i => otherClient.monitorInvoice(i)) + for { + paid <- paidF + invoice <- invoiceF + received <- monitorF + } yield { + assert(received.amountMsat == amt) + assert(received.paymentHash == invoice.lnTags.paymentHash.hash) + } + } + executeWithClientOtherClient(test) + } + // We spawn fresh clients in this test because the test // needs nodes with activity both related and not related // to them diff --git a/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/api/EclairApi.scala b/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/api/EclairApi.scala index 13713faf23..43622697e3 100644 --- a/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/api/EclairApi.scala +++ b/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/api/EclairApi.scala @@ -3,7 +3,12 @@ package org.bitcoins.eclair.rpc.api import org.bitcoins.core.crypto.Sha256Digest import org.bitcoins.core.currency.{CurrencyUnit, Satoshis} import org.bitcoins.core.protocol.Address -import org.bitcoins.core.protocol.ln.{LnInvoice, LnParams, PaymentPreimage, ShortChannelId} +import org.bitcoins.core.protocol.ln.{ + LnInvoice, + LnParams, + PaymentPreimage, + ShortChannelId +} import org.bitcoins.core.protocol.ln.channel.{ChannelId, FundedChannelId} import org.bitcoins.core.protocol.ln.currency.MilliSatoshis import org.bitcoins.core.protocol.ln.node.NodeId @@ -38,7 +43,9 @@ trait EclairApi { * @param from start timestamp * @param to end timestamp */ - def audit(from: Option[FiniteDuration], to: Option[FiniteDuration]): Future[AuditResult] + def audit( + from: Option[FiniteDuration], + to: Option[FiniteDuration]): Future[AuditResult] def allUpdates(): Future[Vector[ChannelUpdate]] @@ -65,11 +72,15 @@ trait EclairApi { def close(id: ChannelId, spk: ScriptPubKey): Future[Unit] - def findRoute(nodeId: NodeId, amountMsat: MilliSatoshis): Future[Vector[NodeId]] + def findRoute( + nodeId: NodeId, + amountMsat: MilliSatoshis): Future[Vector[NodeId]] def findRoute(invoice: LnInvoice): Future[Vector[NodeId]] - def findRoute(invoice: LnInvoice, amountMsat: MilliSatoshis): Future[Vector[NodeId]] + def findRoute( + invoice: LnInvoice, + amountMsat: MilliSatoshis): Future[Vector[NodeId]] def forceClose(channelId: ChannelId): Future[Unit] @@ -108,7 +119,9 @@ trait EclairApi { */ def network: LnParams - def networkFees(from: Option[FiniteDuration], to: Option[FiniteDuration]): Future[Vector[NetworkFeesResult]] + def networkFees( + from: Option[FiniteDuration], + to: Option[FiniteDuration]): Future[Vector[NetworkFeesResult]] def nodeId(): Future[NodeId] = { getNodeURI.map(_.nodeId) @@ -116,19 +129,50 @@ trait EclairApi { def createInvoice(description: String): Future[LnInvoice] - def createInvoice(description: String, amountMsat: MilliSatoshis): Future[LnInvoice] + def createInvoice( + description: String, + amountMsat: MilliSatoshis): Future[LnInvoice] - def createInvoice(description: String, amountMsat: MilliSatoshis, expireIn: FiniteDuration): Future[LnInvoice] + def createInvoice( + description: String, + amountMsat: MilliSatoshis, + expireIn: FiniteDuration): Future[LnInvoice] - def createInvoice(description: String, amountMsat: MilliSatoshis, paymentPreimage: PaymentPreimage): Future[LnInvoice] + def createInvoice( + description: String, + amountMsat: MilliSatoshis, + paymentPreimage: PaymentPreimage): Future[LnInvoice] - def createInvoice(description: String, amountMsat: MilliSatoshis, expireIn: FiniteDuration, paymentPreimage: PaymentPreimage): Future[LnInvoice] + def createInvoice( + description: String, + amountMsat: MilliSatoshis, + expireIn: FiniteDuration, + paymentPreimage: PaymentPreimage): Future[LnInvoice] - def createInvoice(description: String, amountMsat: Option[MilliSatoshis], expireIn: Option[FiniteDuration], fallbackAddress: Option[Address], paymentPreimage: Option[PaymentPreimage]): Future[LnInvoice] + def createInvoice( + description: String, + amountMsat: Option[MilliSatoshis], + expireIn: Option[FiniteDuration], + fallbackAddress: Option[Address], + paymentPreimage: Option[PaymentPreimage]): Future[LnInvoice] + + /** + * Returns a future that is completed when this invoice has been paid too. + * This also publishes the [[ReceivedPaymentResult received payment result]] to the event bush + * when the payment is received + * + * @param lnInvoice the invoice to monitor + * @param maxAttempts the number of attempts we ping eclair until we fail the returned future. Pinging occurrs every 1 second + * */ + def monitorInvoice( + lnInvoice: LnInvoice, + maxAttempts: Int): Future[ReceivedPaymentResult] def getInvoice(paymentHash: Sha256Digest): Future[LnInvoice] - def listInvoices(from: Option[FiniteDuration], to: Option[FiniteDuration]): Future[Vector[LnInvoice]] + def listInvoices( + from: Option[FiniteDuration], + to: Option[FiniteDuration]): Future[Vector[LnInvoice]] def parseInvoice(invoice: LnInvoice): Future[InvoiceResult] @@ -136,7 +180,12 @@ trait EclairApi { def payInvoice(invoice: LnInvoice, amount: MilliSatoshis): Future[PaymentId] - def payInvoice(invoice: LnInvoice, amountMsat: Option[MilliSatoshis], maxAttempts: Option[Int], feeThresholdSat: Option[Satoshis], maxFeePct: Option[Int]): Future[PaymentId] + def payInvoice( + invoice: LnInvoice, + amountMsat: Option[MilliSatoshis], + maxAttempts: Option[Int], + feeThresholdSat: Option[Satoshis], + maxFeePct: Option[Int]): Future[PaymentId] /** * Pings eclair to see if a invoice has been paid and returns [[org.bitcoins.eclair.rpc.json.PaymentResult PaymentResult]] @@ -146,16 +195,25 @@ trait EclairApi { * @param maxAttempts the maximum number of pings * */ - def monitorSentPayment(paymentId: PaymentId, interval: FiniteDuration, maxAttempts: Int): Future[PaymentResult] + def monitorSentPayment( + paymentId: PaymentId, + interval: FiniteDuration, + maxAttempts: Int): Future[PaymentResult] - def payAndMonitorInvoice(invoice: LnInvoice, interval: FiniteDuration, maxAttempts: Int): Future[PaymentResult] = + def payAndMonitorInvoice( + invoice: LnInvoice, + interval: FiniteDuration, + maxAttempts: Int): Future[PaymentResult] = for { paymentId <- payInvoice(invoice) paymentResult <- monitorSentPayment(paymentId, interval, maxAttempts) } yield paymentResult - - def payAndMonitorInvoice(invoice: LnInvoice, amount: MilliSatoshis, interval: FiniteDuration, maxAttempts: Int): Future[PaymentResult] = + def payAndMonitorInvoice( + invoice: LnInvoice, + amount: MilliSatoshis, + interval: FiniteDuration, + maxAttempts: Int): Future[PaymentResult] = for { paymentId <- payInvoice(invoice, amount) paymentResult <- monitorSentPayment(paymentId, interval, maxAttempts) @@ -165,16 +223,30 @@ trait EclairApi { def getSentInfo(id: PaymentId): Future[Vector[PaymentResult]] - def getReceivedInfo(paymentHash: Sha256Digest): Future[ReceivedPaymentResult] + def getReceivedInfo( + paymentHash: Sha256Digest): Future[Option[ReceivedPaymentResult]] - def getReceivedInfo(invoice: LnInvoice): Future[ReceivedPaymentResult] + def getReceivedInfo( + invoice: LnInvoice): Future[Option[ReceivedPaymentResult]] = { + getReceivedInfo(invoice.lnTags.paymentHash.hash) + } - def sendToNode(nodeId: NodeId, amountMsat: MilliSatoshis, paymentHash: Sha256Digest, maxAttempts: Option[Int], feeThresholdSat: Option[Satoshis], maxFeePct: Option[Int]): Future[PaymentId] + def sendToNode( + nodeId: NodeId, + amountMsat: MilliSatoshis, + paymentHash: Sha256Digest, + maxAttempts: Option[Int], + feeThresholdSat: Option[Satoshis], + maxFeePct: Option[Int]): Future[PaymentId] /** * Documented by not implemented in Eclair */ - def sendToRoute(route: TraversableOnce[NodeId], amountMsat: MilliSatoshis, paymentHash: Sha256Digest, finalCltvExpiry: Long): Future[PaymentId] + def sendToRoute( + route: TraversableOnce[NodeId], + amountMsat: MilliSatoshis, + paymentHash: Sha256Digest, + finalCltvExpiry: Long): Future[PaymentId] def usableBalances(): Future[Vector[UsableBalancesResult]] } diff --git a/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/client/EclairRpcClient.scala b/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/client/EclairRpcClient.scala index 045e80d44c..9717e0fb3d 100644 --- a/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/client/EclairRpcClient.scala +++ b/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/client/EclairRpcClient.scala @@ -15,9 +15,14 @@ import org.bitcoins.core.protocol.Address import org.bitcoins.core.protocol.ln.channel.{ChannelId, FundedChannelId} import org.bitcoins.core.protocol.ln.currency.MilliSatoshis import org.bitcoins.core.protocol.ln.node.NodeId -import org.bitcoins.core.protocol.ln.{LnInvoice, LnParams, PaymentPreimage, ShortChannelId} +import org.bitcoins.core.protocol.ln.{ + LnInvoice, + LnParams, + PaymentPreimage, + ShortChannelId +} import org.bitcoins.core.protocol.script.ScriptPubKey -import org.bitcoins.core.util.BitcoinSUtil +import org.bitcoins.core.util.{BitcoinSUtil, FutureUtil} import org.bitcoins.core.wallet.fee.SatoshisPerByte import org.bitcoins.eclair.rpc.api.EclairApi import org.bitcoins.eclair.rpc.config.EclairInstance @@ -43,7 +48,7 @@ class EclairRpcClient(val instance: EclairInstance)( def getDaemon: EclairInstance = instance - override implicit def executionContext: ExecutionContext = m.executionContext + implicit override def executionContext: ExecutionContext = m.executionContext override def allChannels(): Future[Vector[ChannelDesc]] = { eclairCall[Vector[ChannelDesc]]("allchannels") @@ -353,6 +358,50 @@ class EclairRpcClient(val instance: EclairInstance)( } } + /** @inheritdoc */ + override def monitorInvoice( + lnInvoice: LnInvoice, + maxAttempts: Int = 60): Future[ReceivedPaymentResult] = { + val p: Promise[ReceivedPaymentResult] = Promise[ReceivedPaymentResult]() + val attempts = new AtomicInteger(0) + val runnable = new Runnable() { + + override def run(): Unit = { + val receivedInfoF = getReceivedInfo(lnInvoice) + + //register callback that publishes a payment to our actor system's + //event stream, + receivedInfoF.map { + case None => + if (attempts.incrementAndGet() > maxAttempts) { + // too many tries to get info about a payment + // either Eclair is down or the payment is still in PENDING state for some reason + // complete the promise with an exception so the runnable will be canceled + p.failure( + new RuntimeException( + s"EclairApi.monitorInvoice() too many attempts: ${attempts + .get()} for invoice=${lnInvoice}")) + } + case Some(result) => + //invoice has been paid, let's publish to event stream + //so subscribers so the even stream can see that a payment + //was received + //we need to create a `PaymentSucceeded` + system.eventStream.publish(result) + + //complete the promise so the runnable will be canceled + p.success(result) + } + } + } + + val cancellable = system.scheduler.schedule(1.seconds, 1.seconds, runnable) + + p.future.map(_ => cancellable.cancel()) + + p.future + } + override def parseInvoice(invoice: LnInvoice): Future[InvoiceResult] = { eclairCall[InvoiceResult]("parseinvoice", "invoice" -> invoice.toString) } @@ -385,15 +434,21 @@ class EclairRpcClient(val instance: EclairInstance)( } override def getReceivedInfo( - paymentHash: Sha256Digest): Future[ReceivedPaymentResult] = { - eclairCall[ReceivedPaymentResult]("getreceivedinfo", - "paymentHash" -> paymentHash.hex) - } + paymentHash: Sha256Digest): Future[Option[ReceivedPaymentResult]] = { - override def getReceivedInfo( - invoice: LnInvoice): Future[ReceivedPaymentResult] = { - eclairCall[ReceivedPaymentResult]("getreceivedinfo", - "invoice" -> invoice.toString) + //eclair continues the tradition of not responding to things in json... + //the failure case here is the string 'Not found' + implicit val r: Reads[Option[ReceivedPaymentResult]] = Reads { js => + val result: JsResult[ReceivedPaymentResult] = + js.validate[ReceivedPaymentResult] + result match { + case JsSuccess(result, _) => JsSuccess(Some(result)) + case _: JsError => JsSuccess(None) + } + } + eclairCall[Option[ReceivedPaymentResult]]( + "getreceivedinfo", + "paymentHash" -> paymentHash.hex)(r) } override def getSentInfo( @@ -663,9 +718,9 @@ class EclairRpcClient(val instance: EclairInstance)( * 3. We have attempted to query the eclair more than maxAttempts, and the payment is still pending */ override def monitorSentPayment( - paymentId: PaymentId, - interval: FiniteDuration, - maxAttempts: Int): Future[PaymentResult] = { + paymentId: PaymentId, + interval: FiniteDuration, + maxAttempts: Int): Future[PaymentResult] = { val p: Promise[PaymentResult] = Promise[PaymentResult]() val runnable = new Runnable() { @@ -684,7 +739,10 @@ class EclairRpcClient(val instance: EclairInstance)( } else { val resultsF = getSentInfo(paymentId) resultsF.recover { - case e: Throwable => logger.error(s"Cannot check payment status for paymentId=${paymentId}", e) + case e: Throwable => + logger.error( + s"Cannot check payment status for paymentId=${paymentId}", + e) } val _ = for { results <- resultsF