mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-03-13 19:37:30 +01:00
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:
parent
8d44862148
commit
e960422ff8
3 changed files with 206 additions and 37 deletions
|
@ -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
|
||||
|
|
|
@ -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]]
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue