Upgrade to ecalir v0.3.2 (#818)

* Upgrade to ecalir v0.3.2

* addressed the PR comments

* some more changes

* and some more

* external payment id
This commit is contained in:
rorp 2019-10-24 11:19:59 -07:00 committed by Chris Stewart
parent a17a4d3673
commit 06d210ba80
7 changed files with 497 additions and 373 deletions

View file

@ -1,38 +1,48 @@
package org.bitcoins.eclair.rpc
import akka.actor.ActorSystem
import akka.stream.ActorMaterializer
import akka.testkit.TestKit
import org.bitcoins.core.config.RegTest
import org.bitcoins.core.currency.{CurrencyUnit, CurrencyUnits, Satoshis}
import org.bitcoins.core.number.{Int64, UInt64}
import org.bitcoins.core.protocol.ln.LnParams.LnBitcoinRegTest
import org.bitcoins.core.protocol.ln.channel.{ChannelId, ChannelState, FundedChannelId}
import org.bitcoins.core.protocol.ln.channel.{
ChannelId,
ChannelState,
FundedChannelId
}
import org.bitcoins.core.protocol.ln.currency._
import org.bitcoins.core.protocol.ln.node.NodeId
import org.bitcoins.core.util.BitcoinSLogger
import org.bitcoins.eclair.rpc.client.EclairRpcClient
import org.bitcoins.eclair.rpc.config.{EclairAuthCredentials, EclairInstance}
import org.bitcoins.eclair.rpc.json._
import org.bitcoins.rpc.client.common.BitcoindRpcClient
import org.bitcoins.rpc.util.AsyncUtil
import org.bitcoins.testkit.eclair.rpc.{EclairNodes4, EclairRpcTestUtil}
import org.scalatest.{Assertion, AsyncFlatSpec, BeforeAndAfterAll}
import org.slf4j.Logger
import org.scalatest.Assertion
import scala.concurrent._
import scala.concurrent.duration.DurationInt
import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil
import akka.stream.StreamTcpException
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.ln.{LnHumanReadablePart, LnInvoice, PaymentPreimage}
import org.bitcoins.core.protocol.ln.{
LnHumanReadablePart,
LnInvoice,
PaymentPreimage
}
import org.bitcoins.testkit.async.TestAsyncUtil
import scala.concurrent.duration._
import java.nio.file.Files
import org.bitcoins.eclair.rpc.api.{
ChannelResult,
ChannelUpdate,
IncomingPaymentStatus,
InvoiceResult,
OpenChannelInfo,
OutgoingPaymentStatus
}
import org.bitcoins.testkit.util.BitcoinSAsyncTest
import scala.reflect.ClassTag
class EclairRpcClientTest extends BitcoinSAsyncTest {
private val dirExists = Files.exists(EclairRpcTestUtil.binaryDirectory)
@ -53,9 +63,6 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
}
}
lazy val bitcoindRpcClientF: Future[BitcoindRpcClient] = {
for {
cli <- EclairRpcTestUtil.startedBitcoindRpcClient()
@ -137,22 +144,21 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
it should "wait for all gossip messages get propagated throughout the network and get a route to an invoice" in {
val invoiceF = fourthClientF.flatMap(_.createInvoice("foo", 1000.msats))
val hasRoute = () => {
invoiceF.flatMap { invoice =>
firstClientF
.flatMap(_.findRoute(invoice, None))
.map(route => route.length == 4)
.recover {
case err: RuntimeException
if err.getMessage.contains("route not found") =>
false
}
(for {
c1 <- firstClientF
invoice <- invoiceF
route <- c1.findRoute(invoice, None)
} yield {
route.size == 4
}).recover {
case err: RuntimeException
if err.getMessage.contains("route not found") =>
false
}
}
// Eclair is a bit slow in propagating channel changes
AsyncUtil
.awaitConditionF(hasRoute, duration = 10.seconds, maxTries = 20)
.awaitConditionF(hasRoute, duration = 1.second, maxTries = 60)
.map(_ => succeed)
}
@ -165,7 +171,7 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
paymentId <- client1.payInvoice(invoice)
_ <- EclairRpcTestUtil.awaitUntilPaymentSucceeded(client1,
paymentId,
duration = 5.seconds)
duration = 1.second)
received <- client4.audit()
relayed <- client2.audit()
sent <- client1.audit()
@ -190,9 +196,8 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
}
}
// Eclair is a bit slow in propagating channel changes
AsyncUtil
.awaitConditionF(hasRoute, duration = 10.seconds, maxTries = 20)
.awaitConditionF(hasRoute, duration = 1.second, maxTries = 60)
.map(_ => succeed)
}
@ -202,9 +207,13 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
for {
_ <- openAndConfirmChannel(clientF, otherClientF)
invoice <- otherClient.createInvoice("abc", 50.msats)
paymentResult <- client.payAndMonitorInvoice(invoice, 1.second, 10)
paymentResult <- client.payAndMonitorInvoice(invoice,
Some("ext_id"),
1.second,
60)
} yield {
assert(paymentResult.amountMsat == 50.msats)
assert(paymentResult.amount == 50.msats)
assert(paymentResult.externalId.contains("ext_id"))
}
}
@ -223,7 +232,7 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
_ <- EclairRpcTestUtil.awaitUntilPaymentSucceeded(client, paymentId)
sentInfo <- client.getSentInfo(invoice.lnTags.paymentHash.hash)
} yield {
assert(sentInfo.head.amountMsat == 50.msats)
assert(sentInfo.head.amount == 50.msats)
}
}
@ -489,21 +498,28 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
{
for {
otherClientNodeId <- otherClient.getInfo.map(_.nodeId)
invoice <- otherClient.createInvoice("foo", amt)
preimage = PaymentPreimage.random
invoice <- otherClient.createInvoice("foo", amt, preimage)
route <- client.findRoute(otherClientNodeId, amt)
paymentId <- client.sendToRoute(route,
amt,
invoice.lnTags.paymentHash.hash,
144)
144,
Some("ext_id"))
_ <- EclairRpcTestUtil.awaitUntilPaymentSucceeded(client, paymentId)
succeeded <- client.getSentInfo(invoice.lnTags.paymentHash.hash)
} yield {
assert(succeeded.nonEmpty)
val succeededPayment = succeeded.head
assert(succeededPayment.status == PaymentStatus.SUCCEEDED)
assert(succeededPayment.amountMsat == amt)
assert(succeededPayment.preimage.nonEmpty)
assert(succeededPayment.amount == amt)
assert(succeededPayment.externalId.contains("ext_id"))
succeededPayment.status match {
case sent: OutgoingPaymentStatus.Succeeded =>
assert(sent.paymentPreimage == preimage)
case s: OutgoingPaymentStatus =>
fail(s"Unexpected payment status ${s}")
}
}
}
}
@ -525,13 +541,15 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
// probably an async issue, this is more elegant than Thread.sleep
_ = assert(channels.exists(_.state == ChannelState.NORMAL),
"Nodes did not have open channel!")
invoice <- otherClient.createInvoice("foo", amt)
preimage = PaymentPreimage.random
invoice <- otherClient.createInvoice("foo", amt, preimage)
paymentId <- client.sendToNode(otherClientNodeId,
amt,
invoice.lnTags.paymentHash.hash,
None,
None,
None)
None,
Some("ext_id"))
_ <- EclairRpcTestUtil.awaitUntilPaymentSucceeded(client, paymentId)
succeeded <- client.getSentInfo(invoice.lnTags.paymentHash.hash)
_ <- client.close(channelId)
@ -542,9 +560,14 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
assert(succeeded.nonEmpty)
val succeededPayment = succeeded.head
assert(succeededPayment.status == PaymentStatus.SUCCEEDED)
assert(succeededPayment.amountMsat == amt)
assert(succeededPayment.preimage.nonEmpty)
assert(succeededPayment.amount == amt)
assert(succeededPayment.externalId.contains("ext_id"))
succeededPayment.status match {
case sent: OutgoingPaymentStatus.Succeeded =>
assert(sent.paymentPreimage == preimage)
case s: OutgoingPaymentStatus =>
fail(s"Unexpected payment status ${s}")
}
}
}
}
@ -561,7 +584,8 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
{
for {
channelId <- openAndConfirmChannel(clientF, otherClientF)
invoice <- otherClient.createInvoice("test", amt)
preimage = PaymentPreimage.random
invoice <- otherClient.createInvoice("test", amt, preimage)
paymentId <- client.payInvoice(invoice)
_ <- EclairRpcTestUtil.awaitUntilPaymentSucceeded(client, paymentId)
succeeded <- client.getSentInfo(invoice.lnTags.paymentHash.hash)
@ -576,13 +600,21 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
} yield {
assert(succeeded.nonEmpty)
assert(received.get.paymentHash == invoice.lnTags.paymentHash.hash)
assert(received.get.amountMsat == amt)
assert(
received.get.paymentRequest.paymentHash == invoice.lnTags.paymentHash.hash)
assert(
received.get.status
.asInstanceOf[IncomingPaymentStatus.Received]
.amount == amt)
val succeededPayment = succeeded.head
assert(succeededPayment.status == PaymentStatus.SUCCEEDED)
assert(succeededPayment.amountMsat == amt)
assert(succeededPayment.preimage.nonEmpty)
assert(succeededPayment.amount == amt)
succeededPayment.status match {
case sent: OutgoingPaymentStatus.Succeeded =>
assert(sent.paymentPreimage == preimage)
case s: OutgoingPaymentStatus =>
fail(s"Unexpected payment status ${s}")
}
assert(channel.state == ChannelState.CLOSING)
}
@ -602,7 +634,8 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
invoice <- otherClient.createInvoice("no amount")
paymentId <- client.payInvoice(invoice, amt)
_ <- EclairRpcTestUtil.awaitUntilPaymentSucceeded(client, paymentId)
succeeded <- client.getSentInfo(invoice.lnTags.paymentHash.hash)
_ <- client.getSentInfo(invoice.lnTags.paymentHash.hash)
succeeded <- client.getSentInfo(paymentId)
_ <- client.close(channelId)
bitcoind <- bitcoindRpcClientF
address <- bitcoind.getNewAddress
@ -611,9 +644,10 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
assert(succeeded.nonEmpty)
val succeededPayment = succeeded.head
assert(succeededPayment.status == PaymentStatus.SUCCEEDED)
assert(succeededPayment.amountMsat == amt)
assert(succeededPayment.preimage.nonEmpty)
assert(succeededPayment.amount == amt)
assert(
succeededPayment.status
.isInstanceOf[OutgoingPaymentStatus.Succeeded])
}
}
}
@ -720,17 +754,22 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
c2 <- otherClientF
invoice <- c2.createInvoice(s"invoice-payment")
receiveOpt <- c2.getReceivedInfo(invoice)
_ = assert(receiveOpt.isEmpty)
_ = assert(receiveOpt.get.status == IncomingPaymentStatus.Pending)
_ <- c1.payInvoice(invoice, amt)
_ <- AsyncUtil.retryUntilSatisfiedF(
() => c2.getReceivedInfo(invoice).map(_.isDefined),
() =>
c2.getReceivedInfo(invoice)
.map(_.get.status.isInstanceOf[IncomingPaymentStatus.Received]),
1.seconds)
receivedAgainOpt <- c2.getReceivedInfo(invoice)
} yield {
assert(receivedAgainOpt.isDefined)
assert(receivedAgainOpt.get.amountMsat == amt)
assert(
receivedAgainOpt.get.paymentHash == invoice.lnTags.paymentHash.hash)
receivedAgainOpt.get.status
.asInstanceOf[IncomingPaymentStatus.Received]
.amount == amt)
assert(
receivedAgainOpt.get.paymentRequest.paymentHash == invoice.lnTags.paymentHash.hash)
}
}
@ -740,13 +779,17 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
val invoiceF = otherClient.createInvoice("monitor an invoice", amt)
val paidF = invoiceF.flatMap(i => client.payInvoice(i))
for {
paid <- paidF
_ <- paidF
invoice <- invoiceF
//CI is super slow... wait 2 minutes
received <- otherClient.monitorInvoice(invoice, maxAttempts = 120)
received <- otherClient.monitorInvoice(invoice)
} yield {
assert(received.amountMsat == amt)
assert(received.paymentHash == invoice.lnTags.paymentHash.hash)
assert(
received.status
.asInstanceOf[IncomingPaymentStatus.Received]
.amount == amt)
assert(
received.paymentRequest.paymentHash == invoice.lnTags.paymentHash.hash)
}
}
executeWithClientOtherClient(test)
@ -846,11 +889,13 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
val client1F = connectedClientsF.map(_.c1)
val client2F = connectedClientsF.map(_.c2)
sendPaymentsF.flatMap { _ =>
// allupdates for a single node is broken in Eclair 0.3.2
// TODO remove recoverToPendingIf when https://github.com/ACINQ/eclair/issues/1179 is fixed
recoverToPendingIf[RuntimeException](sendPaymentsF.flatMap { _ =>
executeSpecificClients(clientF = client1F,
otherClientF = client2F,
test = getChannelUpdates)
}
})
}
@ -874,8 +919,8 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
paymentId2)
sentInfo2 <- otherClient.getSentInfo(invoice2.lnTags.paymentHash.hash)
} yield {
assert(sentInfo.head.amountMsat == paymentAmount)
assert(sentInfo2.head.amountMsat == paymentAmount2)
assert(sentInfo.head.amount == paymentAmount)
assert(sentInfo2.head.amount == paymentAmount2)
}
}
@ -924,6 +969,10 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
}
}
it should "get all nodes" in {
clientF.flatMap(_.allNodes().flatMap(nodes => assert(nodes.nonEmpty)))
}
it should "get channels" in {
clientF.flatMap(_.channels().flatMap(channels => assert(channels.nonEmpty)))
}
@ -939,18 +988,31 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
})
}
def recoverToPendingIf[T <: AnyRef](future: Future[Any])(
implicit classTag: ClassTag[T]): Future[Assertion] = {
val clazz = classTag.runtimeClass
future.failed.transform(
ex =>
if (!clazz.isAssignableFrom(ex.getClass))
fail(s"Unknown exception ${ex}")
else pending,
ex => {
fail(s"Unexpected exception ${ex}")
}
)
}
it should "get updates for a single node" in {
for {
// allupdates for a single node is broken in Eclair 0.3.2
// TODO remove recoverToPendingIf when https://github.com/ACINQ/eclair/issues/1179 is fixed
recoverToPendingIf[RuntimeException](for {
client <- clientF
nodeInfo <- client.getInfo
updates <- client.allUpdates(nodeInfo.nodeId)
} yield {
assert(updates.nonEmpty)
}
}
it should "get all nodes" in {
clientF.flatMap(_.allNodes().flatMap(nodes => assert(nodes.nonEmpty)))
})
}
it must "receive gossip messages about channel updates for nodes we do not have a direct channel with" in {
@ -980,27 +1042,24 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
})
}
val checkedUpatesF: Future[Unit] =
AsyncUtil.retryUntilSatisfiedF((() => checkUpdates()),
duration = 5.seconds,
maxTries = 15)
val hasUpdateP = Promise[Assertion]()
checkedUpatesF.onComplete { t =>
hasUpdateP.success(assert(t.isSuccess))
}
hasUpdateP.future
AsyncUtil
.retryUntilSatisfiedF((() => checkUpdates()),
duration = 1.second,
maxTries = 60)
.transform(_ => succeed, ex => ex)
}
}
//the second client and fourth client aren't directly connected
//which is why i am choosing to use them for this test
executeSpecificClients(
clientF = secondClientF,
otherClientF = fourthClientF,
test = gossipFromPeerWithNoChannel
)
// allupdates for a single node is broken in Eclair 0.3.2
// TODO remove pendingUntilFixed when https://github.com/ACINQ/eclair/issues/1179 is fixed
recoverToPendingIf[RuntimeException](
executeSpecificClients(
clientF = secondClientF,
otherClientF = fourthClientF,
test = gossipFromPeerWithNoChannel
))
}
it should "detect what network we are on" in {

View file

@ -19,8 +19,8 @@ TaskKeys.downloadEclair := {
Files.createDirectories(binaryDir)
}
val version = "0.3.1"
val commit = "6906ecb"
val version = "0.3.2"
val commit = "5ad3944"
logger.debug(s"(Maybe) downloading Eclair binaries for version: $version")

View file

@ -14,7 +14,6 @@ import org.bitcoins.core.protocol.ln.currency.MilliSatoshis
import org.bitcoins.core.protocol.ln.node.NodeId
import org.bitcoins.core.protocol.script.ScriptPubKey
import org.bitcoins.core.wallet.fee.SatoshisPerByte
import org.bitcoins.eclair.rpc.json._
import org.bitcoins.eclair.rpc.network.NodeUri
import scala.concurrent.duration._
@ -158,7 +157,7 @@ trait EclairApi {
/**
* 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
* This also publishes the [[IncomingPayment received payment result]] to the event bush
* when the payment is received
*
* @param lnInvoice the invoice to monitor
@ -166,7 +165,8 @@ trait EclairApi {
* */
def monitorInvoice(
lnInvoice: LnInvoice,
maxAttempts: Int): Future[ReceivedPaymentResult]
interval: FiniteDuration,
maxAttempts: Int): Future[IncomingPayment]
def getInvoice(paymentHash: Sha256Digest): Future[LnInvoice]
@ -180,15 +180,25 @@ trait EclairApi {
def payInvoice(invoice: LnInvoice, amount: MilliSatoshis): Future[PaymentId]
def payInvoice(
invoice: LnInvoice,
externalId: Option[String]): Future[PaymentId]
def payInvoice(
invoice: LnInvoice,
amount: MilliSatoshis,
externalId: Option[String]): Future[PaymentId]
def payInvoice(
invoice: LnInvoice,
amountMsat: Option[MilliSatoshis],
maxAttempts: Option[Int],
feeThresholdSat: Option[Satoshis],
maxFeePct: Option[Int]): Future[PaymentId]
maxFeePct: Option[Int],
externalId: Option[String]): Future[PaymentId]
/**
* Pings eclair to see if a invoice has been paid and returns [[org.bitcoins.eclair.rpc.json.PaymentResult PaymentResult]]
* Pings eclair to see if a invoice has been paid and returns [[OutgoingPayment PaymentResult]]
*
* @param paymentId the payment id returnned by [[org.bitcoins.eclair.rpc.api.EclairApi.payInvoice payInvoice]]
* @param interval the ping interval
@ -198,36 +208,37 @@ trait EclairApi {
def monitorSentPayment(
paymentId: PaymentId,
interval: FiniteDuration,
maxAttempts: Int): Future[PaymentResult]
maxAttempts: Int): Future[OutgoingPayment]
def payAndMonitorInvoice(
invoice: LnInvoice,
externalId: Option[String],
interval: FiniteDuration,
maxAttempts: Int): Future[PaymentResult] =
maxAttempts: Int): Future[OutgoingPayment] =
for {
paymentId <- payInvoice(invoice)
paymentId <- payInvoice(invoice, externalId)
paymentResult <- monitorSentPayment(paymentId, interval, maxAttempts)
} yield paymentResult
def payAndMonitorInvoice(
invoice: LnInvoice,
amount: MilliSatoshis,
externalId: Option[String],
interval: FiniteDuration,
maxAttempts: Int): Future[PaymentResult] =
maxAttempts: Int): Future[OutgoingPayment] =
for {
paymentId <- payInvoice(invoice, amount)
paymentId <- payInvoice(invoice, amount, externalId)
paymentResult <- monitorSentPayment(paymentId, interval, maxAttempts)
} yield paymentResult
def getSentInfo(paymentHash: Sha256Digest): Future[Vector[PaymentResult]]
def getSentInfo(paymentHash: Sha256Digest): Future[Vector[OutgoingPayment]]
def getSentInfo(id: PaymentId): Future[Vector[PaymentResult]]
def getSentInfo(id: PaymentId): Future[Vector[OutgoingPayment]]
def getReceivedInfo(
paymentHash: Sha256Digest): Future[Option[ReceivedPaymentResult]]
paymentHash: Sha256Digest): Future[Option[IncomingPayment]]
def getReceivedInfo(
invoice: LnInvoice): Future[Option[ReceivedPaymentResult]] = {
def getReceivedInfo(invoice: LnInvoice): Future[Option[IncomingPayment]] = {
getReceivedInfo(invoice.lnTags.paymentHash.hash)
}
@ -237,7 +248,8 @@ trait EclairApi {
paymentHash: Sha256Digest,
maxAttempts: Option[Int],
feeThresholdSat: Option[Satoshis],
maxFeePct: Option[Int]): Future[PaymentId]
maxFeePct: Option[Int],
externalId: Option[String]): Future[PaymentId]
/**
* Documented by not implemented in Eclair
@ -246,7 +258,8 @@ trait EclairApi {
route: scala.collection.immutable.Seq[NodeId],
amountMsat: MilliSatoshis,
paymentHash: Sha256Digest,
finalCltvExpiry: Long): Future[PaymentId]
finalCltvExpiry: Long,
externalId: Option[String]): Future[PaymentId]
def usableBalances(): Future[Vector[UsableBalancesResult]]
}

View file

@ -1,4 +1,6 @@
package org.bitcoins.eclair.rpc.json
package org.bitcoins.eclair.rpc.api
import java.util.UUID
import org.bitcoins.core.crypto.{
DoubleSha256Digest,
@ -13,7 +15,6 @@ import org.bitcoins.core.protocol.ln.fee.FeeProportionalMillionths
import org.bitcoins.core.protocol.ln.node.NodeId
import org.bitcoins.core.protocol.ln.{
LnHumanReadablePart,
LnInvoiceSignature,
PaymentPreimage,
ShortChannelId
}
@ -100,32 +101,40 @@ case class NetworkFeesResult(
remoteNodeId: NodeId,
channelId: FundedChannelId,
txId: DoubleSha256DigestBE,
feeSat: Satoshis,
fee: Satoshis,
txType: String,
timestamp: FiniteDuration
)
case class ChannelStats(
channelId: FundedChannelId,
avgPaymentAmountSatoshi: Satoshis,
avgPaymentAmount: Satoshis,
paymentCount: Long,
relayFeeSatoshi: Satoshis,
networkFeeSatoshi: Satoshis
relayFee: Satoshis,
networkFee: Satoshis
)
case class UsableBalancesResult(
canSendMsat: MilliSatoshis,
canReceiveMsat: MilliSatoshis,
remoteNodeId: NodeId,
shortChannelId: ShortChannelId,
canSend: MilliSatoshis,
canReceive: MilliSatoshis,
isPublic: Boolean
)
case class ReceivedPayment(
amount: MilliSatoshis,
paymentHash: Sha256Digest,
fromChannelId: FundedChannelId,
timestamp: FiniteDuration
parts: Vector[ReceivedPayment.Part]
)
object ReceivedPayment {
case class Part(
amount: MilliSatoshis,
fromChannelId: FundedChannelId,
timestamp: FiniteDuration
)
}
case class RelayedPayment(
amountIn: MilliSatoshis,
amountOut: MilliSatoshis,
@ -136,14 +145,22 @@ case class RelayedPayment(
)
case class SentPayment(
amount: MilliSatoshis,
feesPaid: MilliSatoshis,
id: PaymentId,
paymentHash: Sha256Digest,
paymentPreimage: String,
toChannelId: FundedChannelId,
timestamp: FiniteDuration
paymentPreimage: PaymentPreimage,
parts: Vector[SentPayment.Part]
)
object SentPayment {
case class Part(
id: PaymentId,
amount: MilliSatoshis,
feesPaid: MilliSatoshis,
toChannelId: FundedChannelId,
timestamp: FiniteDuration
)
}
case class ChannelUpdate(
signature: ECDigitalSignature,
chainHash: DoubleSha256Digest,
@ -157,116 +174,6 @@ case class ChannelUpdate(
htlcMaximumMsat: Option[MilliSatoshis],
feeBaseMsat: MilliSatoshis)
/* ChannelResult starts here, some of this may be useful but it seems that data is different at different times
case class CommitInput(
outPoint: String,
amountSatoshis: Long)
implicit val commitInputReads: Reads[CommitInput] =
Json.reads[CommitInput]
case class CommitChanges(
proposed: Vector[String], // IDK WHAT TYPE THIS SHOULD BE
signed: Vector[String], // IDK WHAT TYPE THIS SHOULD BE
acked: Vector[String] // IDK WHAT TYPE THIS SHOULD BE
)
implicit val commitChangesReads: Reads[CommitChanges] =
Json.reads[CommitChanges]
case class CommitSpec(
htlcs: Vector[String],
feeratePerKw: Long,
toLocalMsat: Long,
toRemoteMsat: Long)
implicit val commitSpecReads: Reads[CommitSpec] =
Json.reads[CommitSpec]
case class RemoteCommit(
index: Int,
spec: CommitSpec,
txid: String,
remotePerCommitmentPoint: String)
implicit val remoteCommitReads: Reads[RemoteCommit] =
Json.reads[RemoteCommit]
case class PublishableTxs(
commitTx: String,
htlcTxsAndSigs: Vector[String])
implicit val publishableTxsReads: Reads[PublishableTxs] =
Json.reads[PublishableTxs]
case class LocalCommit(
index: Int,
spec: CommitSpec,
publishableTxs: PublishableTxs)
implicit val localCommitReads: Reads[LocalCommit] =
Json.reads[LocalCommit]
case class RemoteParams(
nodeId: String,
dustLimitSatoshis: Long,
maxHtlcValueInFlightMsat: Long,
channelReserveSatoshis: Long,
htlcMinimumMsat: Long,
toSelfDelay: Long,
maxAcceptedHtlcs: Long,
fundingPubKey: String,
revocationBasepoint: String,
paymentBasepoint: String,
delayedPaymentBasepoint: String,
htlcBasepoint: String,
globalFeatures: String,
localFeatures: String)
implicit val remoteParamsReads: Reads[RemoteParams] =
Json.reads[RemoteParams]
case class ChannelKeyPath(
path: Vector[Long])
implicit val channelKeyPathReads: Reads[ChannelKeyPath] =
Json.reads[ChannelKeyPath]
case class LocalParams(
nodeId: String,
channelKeyPath: ChannelKeyPath,
dustLimitSatoshis: Long,
maxHtlcValueInFlightMsat: Long,
channelReserveSatoshis: Long,
htlcMinimumMsat: Long,
toSelfDelay: Long,
maxAcceptedHtlcs: Long,
isFunder: Boolean,
defaultFinalScriptPubKey: String,
globalFeatures: String,
localFeatures: String)
implicit val localParamsReads: Reads[LocalParams] =
Json.reads[LocalParams]
case class ChannelCommitments(
localParams: LocalParams,
remoteParams: RemoteParams,
channelFlags: Int,
localCommit: LocalCommit,
remoteCommit: RemoteCommit,
localChanges: CommitChanges,
remoteChanges: CommitChanges,
localNextHtlcId: Long,
remoteNextHtlcId: Long,
originChannels: String, // IDK WHAT TYPE THIS SHOULD BE
remoteNextCommitInfo: String,
commitInput: CommitInput,
remotePerCommitmentSecrets: Option[String], // IDK WHAT TYPE THIS SHOULD BE
channelId: String)
implicit val channelCommitmentsReads: Reads[ChannelCommitments] =
Json.reads[ChannelCommitments]
case class ChannelData(
commitments: ChannelCommitments,
shortChannelId: String,
buried: Boolean,
channelUpdate: ChannelUpdate)
implicit val channelDataReads: Reads[ChannelData] =
Json.reads[ChannelData]
*/
case class ChannelResult(
nodeId: NodeId,
channelId: FundedChannelId,
@ -274,7 +181,7 @@ case class ChannelResult(
feeBaseMsat: Option[MilliSatoshis],
feeProportionalMillionths: Option[FeeProportionalMillionths],
data: JsObject) {
import JsonReaders._
import org.bitcoins.eclair.rpc.client.JsonReaders._
lazy val shortChannelId: Option[ShortChannelId] =
(data \ "shortChannelId").validate[ShortChannelId].asOpt
}
@ -290,49 +197,81 @@ case class InvoiceResult(
paymentHash: Sha256Digest,
expiry: FiniteDuration)
case class PaymentId(value: UUID) {
override def toString: String = value.toString
}
case class PaymentRequest(
prefix: LnHumanReadablePart,
amount: Option[MilliSatoshis],
timestamp: Long,
nodeId: NodeId,
tags: Vector[JsObject],
signature: LnInvoiceSignature)
case class PaymentResult(
id: String,
serialized: String,
description: String,
paymentHash: Sha256Digest,
preimage: Option[PaymentPreimage],
amountMsat: MilliSatoshis,
expiry: FiniteDuration,
amount: Option[MilliSatoshis])
case class OutgoingPayment(
id: PaymentId,
parentId: PaymentId,
externalId: Option[String],
paymentHash: Sha256Digest,
amount: MilliSatoshis,
targetNodeId: NodeId,
createdAt: FiniteDuration,
completedAt: Option[FiniteDuration],
status: PaymentStatus)
paymentRequest: Option[PaymentRequest],
status: OutgoingPaymentStatus)
case class ReceivedPaymentResult(
paymentHash: Sha256Digest,
amountMsat: MilliSatoshis,
receivedAt: FiniteDuration)
case class IncomingPayment(
paymentRequest: PaymentRequest,
paymentPreimage: PaymentPreimage,
createdAt: FiniteDuration,
status: IncomingPaymentStatus)
sealed trait PaymentStatus
sealed trait IncomingPaymentStatus
object PaymentStatus {
case object PENDING extends PaymentStatus
case object SUCCEEDED extends PaymentStatus
case object FAILED extends PaymentStatus
object IncomingPaymentStatus {
case object Pending extends IncomingPaymentStatus
case object Expired extends IncomingPaymentStatus
case class Received(amount: MilliSatoshis, receivedAt: Long)
extends IncomingPaymentStatus
def apply(s: String): PaymentStatus = s match {
case "PENDING" => PENDING
case "SUCCEEDED" => SUCCEEDED
case "FAILED" => FAILED
case err =>
throw new IllegalArgumentException(
s"Unknown payment status code `${err}`")
}
}
case class PaymentId(value: String) {
override def toString: String = value
sealed trait OutgoingPaymentStatus
object OutgoingPaymentStatus {
case object Pending extends OutgoingPaymentStatus
case class Succeeded(
paymentPreimage: PaymentPreimage,
feesPaid: MilliSatoshis,
route: Seq[Hop],
completedAt: FiniteDuration)
extends OutgoingPaymentStatus
case class Failed(failures: Seq[PaymentFailure]) extends OutgoingPaymentStatus
}
case class PaymentFailure(
failureType: PaymentFailure.Type,
failureMessage: String,
failedRoute: Seq[Hop])
object PaymentFailure {
sealed trait Type
case object Local extends Type
case object Remote extends Type
case object UnreadableRemote extends Type
}
case class Hop(
nodeId: NodeId,
nextNodeId: NodeId,
shortChannelId: Option[ShortChannelId])
sealed trait WebSocketEvent
object WebSocketEvent {

View file

@ -24,9 +24,27 @@ import org.bitcoins.core.protocol.ln.{
import org.bitcoins.core.protocol.script.ScriptPubKey
import org.bitcoins.core.util.BitcoinSUtil
import org.bitcoins.core.wallet.fee.SatoshisPerByte
import org.bitcoins.eclair.rpc.api.EclairApi
import org.bitcoins.eclair.rpc.api.{
AuditResult,
ChannelDesc,
ChannelInfo,
ChannelResult,
ChannelStats,
ChannelUpdate,
EclairApi,
GetInfoResult,
IncomingPayment,
IncomingPaymentStatus,
InvoiceResult,
NetworkFeesResult,
NodeInfo,
OutgoingPayment,
OutgoingPaymentStatus,
PaymentId,
PeerInfo,
UsableBalancesResult
}
import org.bitcoins.eclair.rpc.config.EclairInstance
import org.bitcoins.eclair.rpc.json._
import org.bitcoins.eclair.rpc.network.{NodeUri, PeerState}
import org.bitcoins.rpc.serializers.JsonReaders._
import org.bitcoins.rpc.util.AsyncUtil
@ -38,6 +56,7 @@ import scala.concurrent.{ExecutionContext, Future, Promise}
import scala.sys.process._
import scala.util.{Failure, Properties, Success}
import java.nio.file.NoSuchFileException
import org.bitcoins.core.util.FutureUtil
/**
@ -367,8 +386,9 @@ class EclairRpcClient(val instance: EclairInstance, binary: Option[File] = None)
/** @inheritdoc */
override def monitorInvoice(
lnInvoice: LnInvoice,
maxAttempts: Int = 60): Future[ReceivedPaymentResult] = {
val p: Promise[ReceivedPaymentResult] = Promise[ReceivedPaymentResult]()
interval: FiniteDuration = 1.second,
maxAttempts: Int = 60): Future[IncomingPayment] = {
val p: Promise[IncomingPayment] = Promise[IncomingPayment]()
val attempts = new AtomicInteger(0)
val runnable = new Runnable() {
@ -378,7 +398,8 @@ class EclairRpcClient(val instance: EclairInstance, binary: Option[File] = None)
//register callback that publishes a payment to our actor system's
//event stream,
receivedInfoF.foreach {
case None =>
case None |
Some(IncomingPayment(_, _, _, IncomingPaymentStatus.Pending)) =>
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
@ -401,7 +422,7 @@ class EclairRpcClient(val instance: EclairInstance, binary: Option[File] = None)
}
}
val cancellable = system.scheduler.schedule(1.seconds, 1.seconds, runnable)
val cancellable = system.scheduler.schedule(interval, interval, runnable)
p.future.map(_ => cancellable.cancel())
@ -412,59 +433,69 @@ class EclairRpcClient(val instance: EclairInstance, binary: Option[File] = None)
eclairCall[InvoiceResult]("parseinvoice", "invoice" -> invoice.toString)
}
override def payInvoice(invoice: LnInvoice): Future[PaymentId] = {
payInvoice(invoice, None, None, None, None)
}
override def payInvoice(invoice: LnInvoice): Future[PaymentId] =
payInvoice(invoice, None, None, None, None, None)
override def payInvoice(
invoice: LnInvoice,
amount: MilliSatoshis): Future[PaymentId] = {
payInvoice(invoice, Some(amount), None, None, None)
}
amount: MilliSatoshis): Future[PaymentId] =
payInvoice(invoice, Some(amount), None, None, None, None)
override def payInvoice(
invoice: LnInvoice,
externalId: Option[String]): Future[PaymentId] =
payInvoice(invoice, None, None, None, None, externalId)
override def payInvoice(
invoice: LnInvoice,
amount: MilliSatoshis,
externalId: Option[String]): Future[PaymentId] =
payInvoice(invoice, Some(amount), None, None, None, externalId)
override def payInvoice(
invoice: LnInvoice,
amountMsat: Option[MilliSatoshis],
maxAttempts: Option[Int],
feeThresholdSat: Option[Satoshis],
maxFeePct: Option[Int]): Future[PaymentId] = {
maxFeePct: Option[Int],
externalId: Option[String]): Future[PaymentId] = {
val params = Seq(
Some("invoice" -> invoice.toString),
amountMsat.map(x => "amountMsat" -> x.toBigDecimal.toString),
maxAttempts.map(x => "maxAttempts" -> x.toString),
feeThresholdSat.map(x => "feeThresholdSat" -> x.toBigDecimal.toString),
maxFeePct.map(x => "maxFeePct" -> x.toString)
maxFeePct.map(x => "maxFeePct" -> x.toString),
externalId.map(x => "externalId" -> x)
).flatten
eclairCall[PaymentId]("payinvoice", params: _*)
}
override def getReceivedInfo(
paymentHash: Sha256Digest): Future[Option[ReceivedPaymentResult]] = {
paymentHash: Sha256Digest): Future[Option[IncomingPayment]] = {
//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]
implicit val r: Reads[Option[IncomingPayment]] = Reads { js =>
val result: JsResult[IncomingPayment] =
js.validate[IncomingPayment]
result match {
case JsSuccess(result, _) => JsSuccess(Some(result))
case _: JsError => JsSuccess(None)
}
}
eclairCall[Option[ReceivedPaymentResult]](
"getreceivedinfo",
"paymentHash" -> paymentHash.hex)(r)
eclairCall[Option[IncomingPayment]]("getreceivedinfo",
"paymentHash" -> paymentHash.hex)(r)
}
override def getSentInfo(
paymentHash: Sha256Digest): Future[Vector[PaymentResult]] = {
eclairCall[Vector[PaymentResult]]("getsentinfo",
"paymentHash" -> paymentHash.hex)
paymentHash: Sha256Digest): Future[Vector[OutgoingPayment]] = {
eclairCall[Vector[OutgoingPayment]]("getsentinfo",
"paymentHash" -> paymentHash.hex)
}
override def getSentInfo(id: PaymentId): Future[Vector[PaymentResult]] = {
eclairCall[Vector[PaymentResult]]("getsentinfo", "id" -> id.toString)
override def getSentInfo(id: PaymentId): Future[Vector[OutgoingPayment]] = {
eclairCall[Vector[OutgoingPayment]]("getsentinfo", "id" -> id.toString)
}
override def sendToNode(
@ -473,13 +504,15 @@ class EclairRpcClient(val instance: EclairInstance, binary: Option[File] = None)
paymentHash: Sha256Digest,
maxAttempts: Option[Int],
feeThresholdSat: Option[Satoshis],
maxFeePct: Option[Int]): Future[PaymentId] = {
maxFeePct: Option[Int],
externalId: Option[String]): Future[PaymentId] = {
val params = Seq("nodeId" -> nodeId.toString,
"amountMsat" -> amountMsat.toBigDecimal.toString,
"paymentHash" -> paymentHash.hex) ++ Seq(
maxAttempts.map(x => "maxAttempts" -> x.toString),
feeThresholdSat.map(x => "feeThresholdSat" -> x.toBigDecimal.toString),
maxFeePct.map(x => "maxFeePct" -> x.toString)
maxFeePct.map(x => "maxFeePct" -> x.toString),
externalId.map(x => "externalId" -> x)
).flatten
eclairCall[PaymentId]("sendtonode", params: _*)
@ -489,14 +522,15 @@ class EclairRpcClient(val instance: EclairInstance, binary: Option[File] = None)
route: scala.collection.immutable.Seq[NodeId],
amountMsat: MilliSatoshis,
paymentHash: Sha256Digest,
finalCltvExpiry: Long): Future[PaymentId] = {
eclairCall[PaymentId](
"sendtoroute",
finalCltvExpiry: Long,
externalId: Option[String]): Future[PaymentId] = {
val params = Seq(
"route" -> route.iterator.mkString(","),
"amountMsat" -> amountMsat.toBigDecimal.toString,
"paymentHash" -> paymentHash.hex,
"finalCltvExpiry" -> finalCltvExpiry.toString
)
) ++ Seq(externalId.map(x => "externalId" -> x)).flatten
eclairCall[PaymentId]("sendtoroute", params: _*)
}
override def updateRelayFee(
@ -599,7 +633,7 @@ class EclairRpcClient(val instance: EclairInstance, binary: Option[File] = None)
val errMsg =
s"Error for command=${commandName} ${datadirMsg}, ${err.value.error}"
logger.error(errMsg)
throw new RuntimeException(errMsg)
throw new RuntimeException(err.value.error)
case _: JsError =>
logger.error(JsError.toJson(res).toString())
throw new IllegalArgumentException(
@ -738,7 +772,7 @@ class EclairRpcClient(val instance: EclairInstance, binary: Option[File] = None)
/**
* Pings eclair to see if a invoice has been paid
* If the invoice has been paid or the payment has failed, we publish a
* [[org.bitcoins.eclair.rpc.json.PaymentResult PaymentResult]]
* [[OutgoingPayment]]
* event to the [[akka.actor.ActorSystem ActorSystem]]'s
* [[akka.event.EventStream ActorSystem.eventStream]]
*
@ -750,8 +784,8 @@ class EclairRpcClient(val instance: EclairInstance, binary: Option[File] = None)
override def monitorSentPayment(
paymentId: PaymentId,
interval: FiniteDuration,
maxAttempts: Int): Future[PaymentResult] = {
val p: Promise[PaymentResult] = Promise[PaymentResult]()
maxAttempts: Int): Future[OutgoingPayment] = {
val p: Promise[OutgoingPayment] = Promise[OutgoingPayment]()
val runnable = new Runnable() {
@ -779,9 +813,10 @@ class EclairRpcClient(val instance: EclairInstance, binary: Option[File] = None)
} yield {
results.foreach { result =>
result.status match {
case PaymentStatus.PENDING =>
case OutgoingPaymentStatus.Pending =>
//do nothing, while we wait for eclair to attempt to process
case PaymentStatus.SUCCEEDED | PaymentStatus.FAILED =>
case (_: OutgoingPaymentStatus.Succeeded |
_: OutgoingPaymentStatus.Failed) =>
// invoice has been succeeded or has failed, let's publish to event stream
// so subscribers to the event stream can see that a payment
// was received or failed
@ -832,8 +867,8 @@ object EclairRpcClient {
implicit system: ActorSystem) = new EclairRpcClient(instance, binary)
/** The current commit we support of Eclair */
private[bitcoins] val commit = "6906ecb"
private[bitcoins] val commit = "5ad3944"
/** The current version we support of Eclair */
private[bitcoins] val version = "0.3.1"
private[bitcoins] val version = "0.3.2"
}

View file

@ -1,11 +1,41 @@
package org.bitcoins.eclair.rpc.json
package org.bitcoins.eclair.rpc.client
import java.util.UUID
import org.bitcoins.core.crypto.Sha256Digest
import org.bitcoins.core.protocol.ln._
import org.bitcoins.core.protocol.ln.channel.{ChannelState, FundedChannelId}
import org.bitcoins.core.protocol.ln.currency.{MilliSatoshis, PicoBitcoins}
import org.bitcoins.core.protocol.ln.fee.FeeProportionalMillionths
import org.bitcoins.core.protocol.ln.node.NodeId
import org.bitcoins.core.protocol.ln._
import org.bitcoins.eclair.rpc.api.{
AuditResult,
BaseChannelInfo,
ChannelDesc,
ChannelInfo,
ChannelResult,
ChannelStats,
ChannelUpdate,
GetInfoResult,
Hop,
IncomingPayment,
IncomingPaymentStatus,
InvoiceResult,
NetworkFeesResult,
NodeInfo,
OpenChannelInfo,
OutgoingPayment,
OutgoingPaymentStatus,
PaymentFailure,
PaymentId,
PaymentRequest,
PeerInfo,
ReceivedPayment,
RelayedPayment,
SentPayment,
UsableBalancesResult,
WebSocketEvent
}
import org.bitcoins.eclair.rpc.network.PeerState
import org.bitcoins.rpc.serializers.SerializerUtil
import play.api.libs.json._
@ -139,9 +169,9 @@ object JsonReaders {
.validate[ShortChannelId]
channelId <- (jsValue \ "channelId").validate[FundedChannelId]
state <- (jsValue \ "state").validate[ChannelState.NORMAL.type]
remoteMsat <- (jsValue \ "data" \ "commitments" \ "localCommit" \ "spec" \ "toLocalMsat")
remoteMsat <- (jsValue \ "data" \ "commitments" \ "localCommit" \ "spec" \ "toRemote")
.validate[MilliSatoshis]
localMsat <- (jsValue \ "data" \ "commitments" \ "localCommit" \ "spec" \ "toLocalMsat")
localMsat <- (jsValue \ "data" \ "commitments" \ "localCommit" \ "spec" \ "toLocal")
.validate[MilliSatoshis]
} yield OpenChannelInfo(nodeId = nodeId,
@ -157,9 +187,9 @@ object JsonReaders {
nodeId <- (jsValue \ "nodeId").validate[NodeId]
channelId <- (jsValue \ "channelId").validate[FundedChannelId]
state <- (jsValue \ "state").validate[ChannelState]
remoteMsat <- (jsValue \ "data" \ "commitments" \ "localCommit" \ "spec" \ "toRemoteMsat")
remoteMsat <- (jsValue \ "data" \ "commitments" \ "localCommit" \ "spec" \ "toRemote")
.validate[MilliSatoshis]
localMsat <- (jsValue \ "data" \ "commitments" \ "localCommit" \ "spec" \ "toLocalMsat")
localMsat <- (jsValue \ "data" \ "commitments" \ "localCommit" \ "spec" \ "toLocal")
.validate[MilliSatoshis]
} yield BaseChannelInfo(nodeId = nodeId,
@ -184,16 +214,8 @@ object JsonReaders {
Json.reads[ChannelUpdate]
}
implicit val paymentRequestReads: Reads[PaymentRequest] = {
Json.reads[PaymentRequest]
}
implicit val paymentIdReads: Reads[PaymentId] = Reads { jsValue =>
SerializerUtil.processJsString(PaymentId.apply)(jsValue)
}
implicit val paymentStatusReads: Reads[PaymentStatus] = Reads { jsValue =>
SerializerUtil.processJsString(PaymentStatus.apply)(jsValue)
SerializerUtil.processJsString(s => PaymentId(UUID.fromString(s)))(jsValue)
}
implicit val finiteDurationReads: Reads[FiniteDuration] =
@ -201,11 +223,63 @@ object JsonReaders {
SerializerUtil.processJsNumberBigInt(_.longValue.millis)(js)
}
implicit val paymentSucceededReads: Reads[PaymentResult] =
Json.reads[PaymentResult]
implicit val paymentReceivedReads: Reads[IncomingPaymentStatus.Received] =
Json.reads[IncomingPaymentStatus.Received]
implicit val hopReads: Reads[Hop] =
Json.reads[Hop]
implicit val paymentSentReads: Reads[OutgoingPaymentStatus.Succeeded] =
Json.reads[OutgoingPaymentStatus.Succeeded]
implicit val paymentFailureTypeReads: Reads[PaymentFailure.Type] = Reads {
jsValue =>
(jsValue \ "type")
.validate[String]
.flatMap { s =>
s.toLowerCase match {
case "local" => JsSuccess(PaymentFailure.Local)
case "remote" => JsSuccess(PaymentFailure.Remote)
case "unreadableremote" =>
JsSuccess(PaymentFailure.UnreadableRemote)
case _ =>
throw new RuntimeException(s"Unknown payment failure type `$s`")
}
}
}
implicit val paymentFailureReads: Reads[PaymentFailure] =
Json.reads[PaymentFailure]
implicit val paymentFailedReads: Reads[OutgoingPaymentStatus.Failed] =
Json.reads[OutgoingPaymentStatus.Failed]
implicit val receivedPaymentResultReads: Reads[ReceivedPaymentResult] =
Json.reads[ReceivedPaymentResult]
implicit val outgoingPaymentStatusReads: Reads[OutgoingPaymentStatus] =
Reads { jsValue =>
(jsValue \ "type")
.validate[String]
.flatMap {
case "pending" => JsSuccess(OutgoingPaymentStatus.Pending)
case "sent" => jsValue.validate[OutgoingPaymentStatus.Succeeded]
case "failed" => jsValue.validate[OutgoingPaymentStatus.Failed]
}
}
implicit val incomingPaymentStatusReads: Reads[IncomingPaymentStatus] =
Reads { jsValue =>
(jsValue \ "type")
.validate[String]
.flatMap {
case "pending" => JsSuccess(IncomingPaymentStatus.Pending)
case "expired" => JsSuccess(IncomingPaymentStatus.Expired)
case "received" => jsValue.validate[IncomingPaymentStatus.Received]
}
}
implicit val paymentRequestReads: Reads[PaymentRequest] = {
Json.reads[PaymentRequest]
}
implicit val paymentSucceededReads: Reads[OutgoingPayment] =
Json.reads[OutgoingPayment]
implicit val receivedPaymentResultReads: Reads[IncomingPayment] =
Json.reads[IncomingPayment]
implicit val channelResultReads: Reads[ChannelResult] = Reads { js =>
for {
@ -238,8 +312,12 @@ object JsonReaders {
JsError(s"Invalid type on refund invoice: $bad, expected JsString")
}
implicit val receivedPaymentPartReads: Reads[ReceivedPayment.Part] =
Json.reads[ReceivedPayment.Part]
implicit val receivedPaymentReads: Reads[ReceivedPayment] =
Json.reads[ReceivedPayment]
implicit val sentPaymentPartReads: Reads[SentPayment.Part] =
Json.reads[SentPayment.Part]
implicit val sentPaymentReads: Reads[SentPayment] = Json.reads[SentPayment]
implicit val relayedPaymentReads: Reads[RelayedPayment] =
Json.reads[RelayedPayment]
@ -254,21 +332,21 @@ object JsonReaders {
implicit val usableBalancesResultReads: Reads[UsableBalancesResult] =
Json.reads[UsableBalancesResult]
import WebSocketEvent._
implicit val paymentRelayedEventReads: Reads[WebSocketEvent.PaymentRelayed] =
Json.reads[WebSocketEvent.PaymentRelayed]
implicit val paymentRelayedEventReads: Reads[PaymentRelayed] =
Json.reads[PaymentRelayed]
implicit val paymentReceivedEventReads: Reads[
WebSocketEvent.PaymentReceived] =
Json.reads[WebSocketEvent.PaymentReceived]
implicit val paymentReceivedEventReads: Reads[PaymentReceived] =
Json.reads[PaymentReceived]
implicit val paymentFailedEventReads: Reads[WebSocketEvent.PaymentFailed] =
Json.reads[WebSocketEvent.PaymentFailed]
implicit val paymentFailedEventReads: Reads[PaymentFailed] =
Json.reads[PaymentFailed]
implicit val paymentSentEventReads: Reads[WebSocketEvent.PaymentSent] =
Json.reads[WebSocketEvent.PaymentSent]
implicit val paymentSentEventReads: Reads[PaymentSent] =
Json.reads[PaymentSent]
implicit val paymentSettlingOnchainEventReads: Reads[PaymentSettlingOnchain] =
Json.reads[PaymentSettlingOnchain]
implicit val paymentSettlingOnchainEventReads: Reads[
WebSocketEvent.PaymentSettlingOnchain] =
Json.reads[WebSocketEvent.PaymentSettlingOnchain]
}

View file

@ -15,10 +15,14 @@ import org.bitcoins.core.protocol.ln.channel.{
import org.bitcoins.core.protocol.ln.currency.MilliSatoshis
import org.bitcoins.core.protocol.ln.node.NodeId
import org.bitcoins.core.util.BitcoinSLogger
import org.bitcoins.eclair.rpc.api.EclairApi
import org.bitcoins.eclair.rpc.api.{
EclairApi,
OutgoingPayment,
OutgoingPaymentStatus,
PaymentId
}
import org.bitcoins.eclair.rpc.client.EclairRpcClient
import org.bitcoins.eclair.rpc.config.EclairInstance
import org.bitcoins.eclair.rpc.json.{PaymentId, PaymentStatus}
import org.bitcoins.rpc.client.common.{BitcoindRpcClient, BitcoindVersion}
import org.bitcoins.rpc.config.BitcoindInstance
import org.bitcoins.rpc.util.RpcUtil
@ -31,9 +35,11 @@ import scala.util.{Failure, Success}
import org.bitcoins.rpc.config.BitcoindAuthCredentials
import java.nio.file.Files
import scala.reflect.ClassTag
/**
* @define nodeLinkDoc
* Creates two Eclair nodes that are connected in the following manner:
* Creates four Eclair nodes that are connected in the following manner:
* {{{
* node1 <-> node2 <-> node3 <-> node4
* }}}
@ -121,6 +127,7 @@ trait EclairRpcTestUtil extends BitcoinSLogger {
"eclair.max-htlc-value-in-flight-msat" -> 100000000000L,
"eclair.router.broadcast-interval" -> "2 second",
"eclair.auto-reconnect" -> false,
"eclair.to-remote-delay-blocks" -> 144,
"eclair.db.driver" -> "org.sqlite.JDBC",
"eclair.db.regtest.url" -> "jdbc:sqlite:regtest/",
"eclair.max-payment-fee" -> 10, // avoid complaints about too high fees
@ -251,63 +258,56 @@ trait EclairRpcTestUtil extends BitcoinSLogger {
client: EclairApi,
paymentId: PaymentId,
duration: FiniteDuration = 1.second,
maxTries: Int = 50,
maxTries: Int = 60,
failFast: Boolean = true)(implicit system: ActorSystem): Future[Unit] = {
awaitUntilPaymentStatus(client,
paymentId,
PaymentStatus.SUCCEEDED,
duration,
maxTries,
failFast)
awaitUntilPaymentStatus[OutgoingPaymentStatus.Succeeded](client,
paymentId,
duration,
maxTries,
failFast)
}
def awaitUntilPaymentFailed(
client: EclairApi,
paymentId: PaymentId,
duration: FiniteDuration = 1.second,
maxTries: Int = 50,
maxTries: Int = 60,
failFast: Boolean = false)(implicit system: ActorSystem): Future[Unit] = {
awaitUntilPaymentStatus(client,
paymentId,
PaymentStatus.FAILED,
duration,
maxTries,
failFast)
awaitUntilPaymentStatus[OutgoingPaymentStatus.Failed](client,
paymentId,
duration,
maxTries,
failFast)
}
def awaitUntilPaymentPending(
private def awaitUntilPaymentStatus[T <: OutgoingPaymentStatus](
client: EclairApi,
paymentId: PaymentId,
duration: FiniteDuration = 1.second,
maxTries: Int = 50,
failFast: Boolean = true)(implicit system: ActorSystem): Future[Unit] = {
awaitUntilPaymentStatus(client,
paymentId,
PaymentStatus.PENDING,
duration,
maxTries,
failFast)
}
private def awaitUntilPaymentStatus(
client: EclairApi,
paymentId: PaymentId,
state: PaymentStatus,
duration: FiniteDuration,
maxTries: Int,
failFast: Boolean)(implicit system: ActorSystem): Future[Unit] = {
logger.debug(s"Awaiting payment ${paymentId} to enter ${state} state")
failFast: Boolean)(
implicit system: ActorSystem,
tag: ClassTag[T]): Future[Unit] = {
logger.debug(
s"Awaiting payment ${paymentId} to enter ${tag.runtimeClass.getName} state")
def isState(): Future[Boolean] = {
def isFailed(status: OutgoingPaymentStatus): Boolean = status match {
case _: OutgoingPaymentStatus.Failed => true
case _: OutgoingPaymentStatus => false
}
val sentInfoF = client.getSentInfo(paymentId)
def isInState(): Future[Boolean] = {
val sentInfoF: Future[Vector[OutgoingPayment]] =
client.getSentInfo(paymentId)
sentInfoF.map { payment =>
if (failFast && payment.exists(_.status == PaymentStatus.FAILED)) {
if (failFast && payment.exists(result => isFailed(result.status))) {
throw new RuntimeException(s"Payment ${paymentId} has failed")
}
if (!payment.exists(_.status == state)) {
if (!payment.exists(
result => tag.runtimeClass == result.status.getClass)) {
logger.trace(
s"Payment ${paymentId} has not entered ${state} yet. Currently in ${payment.map(_.status).mkString(",")}")
s"Payment ${paymentId} has not entered ${tag.runtimeClass.getName} yet. Currently in ${payment.map(_.status).mkString(",")}")
false
} else {
true
@ -315,7 +315,7 @@ trait EclairRpcTestUtil extends BitcoinSLogger {
}(system.dispatcher)
}
TestAsyncUtil.retryUntilSatisfiedF(conditionF = () => isState(),
TestAsyncUtil.retryUntilSatisfiedF(conditionF = () => isInState(),
duration = duration,
maxTries = maxTries)
}