implement ability to monitor a invoice that we are generated (#649)

* implement ability to monitor a invoice that we are generated

* reorganize tests to get them to pass
This commit is contained in:
Chris Stewart 2019-07-19 13:03:43 -05:00 committed by GitHub
parent 8d44862148
commit e960422ff8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 206 additions and 37 deletions

View file

@ -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

View file

@ -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]]
}

View file

@ -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