mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-03-18 21:22:04 +01:00
Eclair performance tests
This commit is contained in:
parent
d776e1c952
commit
e1acac05eb
15 changed files with 679 additions and 105 deletions
142
bench/src/main/scala/org/bitcoins/bench/eclair/EclairBench.scala
Normal file
142
bench/src/main/scala/org/bitcoins/bench/eclair/EclairBench.scala
Normal file
|
@ -0,0 +1,142 @@
|
|||
package org.bitcoins.bench.eclair
|
||||
|
||||
import java.io.File
|
||||
import java.nio.file.{Files, StandardOpenOption}
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import org.bitcoins.core.protocol.ln.currency._
|
||||
import org.bitcoins.eclair.rpc.api.PaymentId
|
||||
import org.bitcoins.testkit.async.TestAsyncUtil
|
||||
import org.bitcoins.testkit.eclair.rpc.EclairRpcTestUtil
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
import scala.concurrent.duration.DurationInt
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
object EclairBench extends App with EclairRpcTestUtil {
|
||||
|
||||
import PaymentLog._
|
||||
|
||||
implicit val system = ActorSystem()
|
||||
import system.dispatcher
|
||||
|
||||
val networkSize = 1
|
||||
val paymentCount = 10
|
||||
val channelAmount = 10000000000L.msats
|
||||
val outputFileName = "test.csv"
|
||||
|
||||
// release
|
||||
val EclairVersion = Option.empty[String]
|
||||
val EclairCommit = Option.empty[String]
|
||||
|
||||
// psql
|
||||
// compiled binary can be found here:
|
||||
// https://s3-us-west-1.amazonaws.com/suredbits.com/eclair/eclair-node-0.3.3-SNAPSHOT-949f1ec-psql.jar
|
||||
// put it into binaries/eclair/0.3.3-SNAPSHOT directory
|
||||
// val EclairVersion = Option("0.3.3-SNAPSHOT")
|
||||
// val EclairCommit = Option("949f1ec-psql")
|
||||
|
||||
// don't forget to recreate `eclair` Postgres database before starting a new test
|
||||
EclairRpcTestUtil.customConfigMap = Map(
|
||||
"eclair.db.driver" -> "psql"
|
||||
)
|
||||
|
||||
def sendPayments(network: Network, amount: MilliSatoshis, count: Int)(
|
||||
implicit ec: ExecutionContext): Future[Vector[PaymentId]] =
|
||||
for {
|
||||
testNodeInfo <- network.testEclairNode.getInfo
|
||||
paymentIds <- Future.sequence(network.networkEclairNodes.map { node =>
|
||||
1.to(count).foldLeft(Future.successful(Vector.empty[PaymentId])) {
|
||||
(accF, _) =>
|
||||
for {
|
||||
acc <- accF
|
||||
invoice <- network.testEclairNode
|
||||
.createInvoice("test " + System.currentTimeMillis(), amount)
|
||||
paymentHash = invoice.lnTags.paymentHash.hash
|
||||
_ = logPaymentHash(paymentHash)
|
||||
id <- node.sendToNode(testNodeInfo.nodeId,
|
||||
invoice.amount.get.toMSat,
|
||||
invoice.lnTags.paymentHash.hash,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None)
|
||||
} yield {
|
||||
logPaymentId(paymentHash, id)
|
||||
acc :+ id
|
||||
}
|
||||
}
|
||||
})
|
||||
} yield paymentIds.flatten
|
||||
|
||||
def runTests(network: Network): Future[Vector[PaymentLogEntry]] = {
|
||||
println("Setting up the test network")
|
||||
for {
|
||||
_ <- network.testEclairNode.connectToWebSocket(logEvent)
|
||||
_ = println(
|
||||
s"Set up ${networkSize} nodes, that will send $paymentCount paments to the test node each")
|
||||
_ = println(
|
||||
s"Test node data directory: ${network.testEclairNode.instance.authCredentials.datadir
|
||||
.getOrElse("")}")
|
||||
_ = println("Testing...")
|
||||
_ <- sendPayments(network, 1000.msats, paymentCount)
|
||||
_ <- TestAsyncUtil.retryUntilSatisfied(
|
||||
condition = paymentLog.size() == networkSize * paymentCount,
|
||||
duration = 1.second,
|
||||
maxTries = 100)
|
||||
_ <- TestAsyncUtil
|
||||
.retryUntilSatisfied(condition =
|
||||
paymentLog.values().asScala.forall(_.completed),
|
||||
duration = 1.second,
|
||||
maxTries = 100)
|
||||
_ = println("Done!")
|
||||
} yield {
|
||||
paymentLog
|
||||
.values()
|
||||
.asScala
|
||||
.toVector
|
||||
.sortBy(_.paymentSentAt)
|
||||
}
|
||||
}
|
||||
|
||||
val res: Future[Unit] = for {
|
||||
network <- Network.start(EclairVersion,
|
||||
EclairCommit,
|
||||
networkSize,
|
||||
channelAmount)
|
||||
log <- runTests(network).recoverWith {
|
||||
case e: Throwable =>
|
||||
e.printStackTrace()
|
||||
Future.successful(Vector.empty[PaymentLogEntry])
|
||||
}
|
||||
_ <- network.shutdown()
|
||||
} yield {
|
||||
if (log.nonEmpty) {
|
||||
val first = log.head
|
||||
val csv =
|
||||
Vector(
|
||||
"time,number_of_payments,payment_hash,payment_id,event,payment_sent_at,payment_id_received_at,event_received_at,received_in,completed_in") ++
|
||||
log.zipWithIndex
|
||||
.map {
|
||||
case (x, i) =>
|
||||
s"${x.paymentSentAt - first.paymentSentAt},${i + 1},${x.toCSV}"
|
||||
}
|
||||
val outputFile = new File(outputFileName)
|
||||
Files.write(outputFile.toPath,
|
||||
csv.asJava,
|
||||
StandardOpenOption.CREATE,
|
||||
StandardOpenOption.WRITE,
|
||||
StandardOpenOption.TRUNCATE_EXISTING)
|
||||
println(s"The test results was written in ${outputFile.getAbsolutePath}")
|
||||
}
|
||||
}
|
||||
|
||||
res.onComplete { e =>
|
||||
e match {
|
||||
case Success(_) => ()
|
||||
case Failure(ex) => ex.printStackTrace()
|
||||
}
|
||||
sys.exit()
|
||||
}
|
||||
}
|
119
bench/src/main/scala/org/bitcoins/bench/eclair/PaymentLog.scala
Normal file
119
bench/src/main/scala/org/bitcoins/bench/eclair/PaymentLog.scala
Normal file
|
@ -0,0 +1,119 @@
|
|||
package org.bitcoins.bench.eclair
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.function.BiFunction
|
||||
|
||||
import org.bitcoins.core.crypto.Sha256Digest
|
||||
import org.bitcoins.eclair.rpc.api.WebSocketEvent.{
|
||||
PaymentFailed,
|
||||
PaymentReceived,
|
||||
PaymentSent
|
||||
}
|
||||
import org.bitcoins.eclair.rpc.api.{PaymentId, WebSocketEvent}
|
||||
|
||||
object PaymentLog {
|
||||
|
||||
case class PaymentLogEntry(
|
||||
paymentHash: Option[Sha256Digest] = None,
|
||||
id: Option[PaymentId] = None,
|
||||
event: Option[WebSocketEvent] = None,
|
||||
paymentSentAt: Long,
|
||||
paymentIdReceivedAt: Long = 0,
|
||||
eventReceivedAt: Long = 0) {
|
||||
|
||||
def withPaymentHash(paymentHash: Sha256Digest): PaymentLogEntry =
|
||||
copy(paymentHash = Some(paymentHash),
|
||||
paymentSentAt = System.currentTimeMillis())
|
||||
|
||||
def withPaymentId(id: PaymentId): PaymentLogEntry =
|
||||
copy(id = Some(id), paymentIdReceivedAt = System.currentTimeMillis())
|
||||
|
||||
def withEvent(e: WebSocketEvent): PaymentLogEntry =
|
||||
copy(event = Some(e), eventReceivedAt = System.currentTimeMillis())
|
||||
|
||||
def completed: Boolean = event.isDefined
|
||||
|
||||
def completedAt: Long = event match {
|
||||
case None => 0
|
||||
case Some(e) =>
|
||||
e match {
|
||||
case PaymentReceived(_, parts) =>
|
||||
parts.maxBy(_.timestamp).timestamp.toMillis
|
||||
case PaymentFailed(_, _, _, timestamp) => timestamp.toMillis
|
||||
case _: WebSocketEvent =>
|
||||
throw new RuntimeException("Can't extract a timestamp")
|
||||
}
|
||||
}
|
||||
|
||||
def toCSV: String =
|
||||
s"""${paymentHash
|
||||
.map(_.hex)
|
||||
.getOrElse("")},${id.map(_.toString).getOrElse("")},${event
|
||||
.map(_.getClass.getName.split('$').last)
|
||||
.getOrElse("")},$paymentSentAt,$paymentIdReceivedAt,$eventReceivedAt,${paymentIdReceivedAt - paymentSentAt},${eventReceivedAt - paymentIdReceivedAt}"""
|
||||
}
|
||||
|
||||
object PaymentLogEntry {
|
||||
|
||||
def apply(paymentHash: Sha256Digest): PaymentLogEntry = {
|
||||
PaymentLogEntry(paymentSentAt = System.currentTimeMillis(),
|
||||
paymentHash = Some(paymentHash))
|
||||
}
|
||||
}
|
||||
|
||||
val paymentLog =
|
||||
new ConcurrentHashMap[Sha256Digest, PaymentLogEntry]()
|
||||
|
||||
def logPaymentHash(paymentHash: Sha256Digest): PaymentLogEntry = {
|
||||
paymentLog.putIfAbsent(paymentHash, PaymentLogEntry(paymentHash))
|
||||
}
|
||||
|
||||
def logPaymentId(
|
||||
paymentHash: Sha256Digest,
|
||||
paymentId: PaymentId): PaymentLogEntry = {
|
||||
paymentLog.compute(
|
||||
paymentHash,
|
||||
new BiFunction[Sha256Digest, PaymentLogEntry, PaymentLogEntry] {
|
||||
override def apply(
|
||||
hash: Sha256Digest,
|
||||
old: PaymentLogEntry): PaymentLogEntry = {
|
||||
val log = if (old == null) {
|
||||
PaymentLogEntry(paymentSentAt = 0, paymentHash = Some(hash))
|
||||
} else {
|
||||
old
|
||||
}
|
||||
log.withPaymentId(paymentId)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
def logEvent(event: WebSocketEvent): PaymentLogEntry = {
|
||||
val hash: Sha256Digest = event match {
|
||||
case PaymentReceived(paymentHash, _) =>
|
||||
paymentHash
|
||||
case PaymentSent(_, paymentHash, _, _) => paymentHash
|
||||
case PaymentFailed(_, paymentHash, _, _) =>
|
||||
paymentHash
|
||||
case _: WebSocketEvent =>
|
||||
throw new RuntimeException("Can't extract payment hash")
|
||||
}
|
||||
|
||||
paymentLog.compute(
|
||||
hash,
|
||||
new BiFunction[Sha256Digest, PaymentLogEntry, PaymentLogEntry] {
|
||||
override def apply(
|
||||
hash: Sha256Digest,
|
||||
old: PaymentLogEntry): PaymentLogEntry = {
|
||||
val log = if (old == null) {
|
||||
PaymentLogEntry(paymentSentAt = 0, paymentHash = Some(hash))
|
||||
} else {
|
||||
old
|
||||
}
|
||||
log.withEvent(event)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
|
@ -294,7 +294,7 @@ lazy val bench = project
|
|||
name := "bitcoin-s-bench",
|
||||
skip in publish := true
|
||||
)
|
||||
.dependsOn(core % testAndCompile)
|
||||
.dependsOn(core % testAndCompile, testkit)
|
||||
|
||||
lazy val eclairRpcTest = project
|
||||
.in(file("eclair-rpc-test"))
|
||||
|
@ -385,7 +385,7 @@ lazy val keyManagerTest = project
|
|||
.in(file("key-manager-test"))
|
||||
.settings(CommonSettings.testSettings: _*)
|
||||
.settings(name := "bitcoin-s-keymanager-test",
|
||||
libraryDependencies ++= Deps.keyManagerTest)
|
||||
libraryDependencies ++= Deps.keyManagerTest)
|
||||
.dependsOn(keyManager, testkit)
|
||||
|
||||
lazy val walletDbSettings = dbFlywaySettings("walletdb")
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package org.bitcoins.core.protocol.ln
|
||||
|
||||
import org.bitcoins.core.crypto._
|
||||
import org.bitcoins.testkit.core.gen.ln.LnInvoiceGen
|
||||
import org.bitcoins.core.number.{UInt32, UInt64, UInt8}
|
||||
import org.bitcoins.core.protocol.ln.LnParams.{
|
||||
LnBitcoinMainNet,
|
||||
|
@ -19,6 +18,7 @@ import org.bitcoins.core.protocol.ln.fee.{
|
|||
import org.bitcoins.core.protocol.ln.routing.LnRoute
|
||||
import org.bitcoins.core.protocol.{Bech32Address, P2PKHAddress, P2SHAddress}
|
||||
import org.bitcoins.core.util.CryptoUtil
|
||||
import org.bitcoins.testkit.core.gen.ln.LnInvoiceGen
|
||||
import org.bitcoins.testkit.util.BitcoinSUnitTest
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
|
@ -113,13 +113,17 @@ class LnInvoiceUnitTest extends BitcoinSUnitTest {
|
|||
|
||||
val descriptionTagE = Left(LnTag.DescriptionTag("ナンセンス 1杯"))
|
||||
val expiryTag = LnTag.ExpiryTimeTag(UInt32(60))
|
||||
val lnTags = LnTaggedFields(paymentTag,
|
||||
descriptionTagE,
|
||||
None,
|
||||
Some(expiryTag),
|
||||
None,
|
||||
None,
|
||||
None)
|
||||
val lnTags = LnTaggedFields(
|
||||
paymentHash = paymentTag,
|
||||
secret = None,
|
||||
descriptionOrHash = descriptionTagE,
|
||||
nodeId = None,
|
||||
expiryTime = Some(expiryTag),
|
||||
cltvExpiry = None,
|
||||
fallbackAddress = None,
|
||||
routingInfo = None,
|
||||
features = None
|
||||
)
|
||||
|
||||
val signature = ECDigitalSignature.fromRS(
|
||||
"259f04511e7ef2aa77f6ff04d51b4ae9209504843e5ab9672ce32a153681f687515b73ce57ee309db588a10eb8e41b5a2d2bc17144ddf398033faa49ffe95ae6")
|
||||
|
@ -359,6 +363,45 @@ class LnInvoiceUnitTest extends BitcoinSUnitTest {
|
|||
deserialized.get must be(lnInvoice)
|
||||
}
|
||||
|
||||
it must "parse BOLT11 example 10" in {
|
||||
val expected =
|
||||
"lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqpqqq4u9s93jtgysm3mrwll70zr697y3mf902hvxwej0v7c62rsltw83ng0pu8w3j230sluc5gxkdmm9dvpy9y6ggtjd2w544mzdrcs42t7sqdkcy8h"
|
||||
|
||||
val signature = ECDigitalSignature.fromHex(
|
||||
"3045022100af0b02c64b4121b8ec6efffcf10f45f123b495eabb0cecc9ecf634a1c3eb71e30220343c3c3ba32545f0ff31441acddecad60485269085c9aa752b5d89a3c42aa5fa")
|
||||
val lnInvoiceSig =
|
||||
LnInvoiceSignature(recoverId = UInt8.zero, signature = signature)
|
||||
|
||||
val descriptionTag = Left(LnTag.DescriptionTag("coffee beans"))
|
||||
|
||||
val paymentSecret = Some(
|
||||
LnTag.SecretTag(PaymentSecret.fromHex(
|
||||
"1111111111111111111111111111111111111111111111111111111111111111")))
|
||||
|
||||
val features = Some(
|
||||
LnTag.FeaturesTag(ByteVector.fromValidHex("800000000000000000000800")))
|
||||
|
||||
val lnTags = LnTaggedFields(paymentHash = paymentTag,
|
||||
descriptionOrHash = descriptionTag,
|
||||
secret = paymentSecret,
|
||||
features = features)
|
||||
|
||||
val hrpMilli =
|
||||
LnHumanReadablePart(LnBitcoinMainNet, Some(MilliBitcoins(25)))
|
||||
|
||||
val lnInvoice = LnInvoice(hrp = hrpMilli,
|
||||
timestamp = time,
|
||||
lnTags = lnTags,
|
||||
signature = lnInvoiceSig)
|
||||
|
||||
val serialized = lnInvoice.toString
|
||||
serialized must be(expected)
|
||||
|
||||
val deserialized = LnInvoice.fromString(serialized)
|
||||
|
||||
deserialized.get.toString must be(serialized)
|
||||
}
|
||||
|
||||
it must "deserialize and reserialize a invoice with a explicity expiry time" in {
|
||||
//from eclair
|
||||
val bech32 =
|
||||
|
@ -449,4 +492,16 @@ class LnInvoiceUnitTest extends BitcoinSUnitTest {
|
|||
|
||||
invoice.nodeId.hex must be(expected)
|
||||
}
|
||||
|
||||
it must "parse secret and features tags" in {
|
||||
// generated by Eclair 3.3.0-SNAPSHOT
|
||||
val serialized =
|
||||
"lnbcrt10n1p0px7lfpp5ghc2y7ttnwy58jx0dfcsdxy7ey0qfryn0wcmm04ckud0qw73kt9sdq9vehk7xqrrss9qypqqqsp5qlf6efygd26y03y66jdqqfmlxthplnu5cc8648fgn88twhpyvmgqg9k5kd0k8vv3xvvqpkhkt9chdl579maq45gvck4g0yd0eggmvfkzgvjmwn29r99p57tgyl3l3s82hlc4e97at55xl5lyzpfk6n36yyqqxeem8q"
|
||||
val invoice = LnInvoice.fromString(serialized).get
|
||||
invoice.lnTags.secret must be(
|
||||
Some(LnTag.SecretTag(PaymentSecret.fromHex(
|
||||
"07d3aca4886ab447c49ad49a00277f32ee1fcf94c60faa9d2899ceb75c2466d0"))))
|
||||
invoice.lnTags.features must be(
|
||||
Some(LnTag.FeaturesTag(ByteVector.fromValidHex("0800"))))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,9 @@ object LnTagPrefix {
|
|||
case object PaymentHash extends LnTagPrefix {
|
||||
override val value: Char = 'p'
|
||||
}
|
||||
case object Secret extends LnTagPrefix {
|
||||
override val value: Char = 's'
|
||||
}
|
||||
case object Description extends LnTagPrefix {
|
||||
override val value: Char = 'd'
|
||||
}
|
||||
|
@ -47,15 +50,21 @@ object LnTagPrefix {
|
|||
override val value: Char = 'r'
|
||||
}
|
||||
|
||||
case object Features extends LnTagPrefix {
|
||||
override val value: Char = '9'
|
||||
}
|
||||
|
||||
private lazy val all: Map[Char, LnTagPrefix] =
|
||||
List(PaymentHash,
|
||||
Secret,
|
||||
Description,
|
||||
NodeId,
|
||||
DescriptionHash,
|
||||
ExpiryTime,
|
||||
CltvExpiry,
|
||||
FallbackAddress,
|
||||
RoutingInfo)
|
||||
RoutingInfo,
|
||||
Features)
|
||||
.map(prefix => prefix.value -> prefix)
|
||||
.toMap
|
||||
|
||||
|
|
|
@ -23,6 +23,8 @@ sealed abstract class LnTaggedFields extends NetworkElement {
|
|||
|
||||
def paymentHash: LnTag.PaymentHashTag
|
||||
|
||||
def secret: Option[LnTag.SecretTag]
|
||||
|
||||
def description: Option[LnTag.DescriptionTag]
|
||||
|
||||
def nodeId: Option[LnTag.NodeIdTag]
|
||||
|
@ -37,14 +39,18 @@ sealed abstract class LnTaggedFields extends NetworkElement {
|
|||
|
||||
def routingInfo: Option[LnTag.RoutingInfo]
|
||||
|
||||
def features: Option[LnTag.FeaturesTag]
|
||||
|
||||
lazy val data: Vector[UInt5] = Vector(Some(paymentHash),
|
||||
description,
|
||||
nodeId,
|
||||
descriptionHash,
|
||||
secret,
|
||||
expiryTime,
|
||||
cltvExpiry,
|
||||
fallbackAddress,
|
||||
routingInfo)
|
||||
routingInfo,
|
||||
features)
|
||||
.filter(_.isDefined)
|
||||
.flatMap(_.get.data)
|
||||
|
||||
|
@ -70,9 +76,11 @@ object LnTaggedFields {
|
|||
nodeId: Option[LnTag.NodeIdTag],
|
||||
descriptionHash: Option[LnTag.DescriptionHashTag],
|
||||
expiryTime: Option[LnTag.ExpiryTimeTag],
|
||||
secret: Option[LnTag.SecretTag],
|
||||
cltvExpiry: Option[LnTag.MinFinalCltvExpiry],
|
||||
fallbackAddress: Option[LnTag.FallbackAddressTag],
|
||||
routingInfo: Option[LnTag.RoutingInfo])
|
||||
routingInfo: Option[LnTag.RoutingInfo],
|
||||
features: Option[LnTag.FeaturesTag])
|
||||
extends LnTaggedFields
|
||||
|
||||
/**
|
||||
|
@ -82,11 +90,13 @@ object LnTaggedFields {
|
|||
def apply(
|
||||
paymentHash: LnTag.PaymentHashTag,
|
||||
descriptionOrHash: Either[LnTag.DescriptionTag, LnTag.DescriptionHashTag],
|
||||
secret: Option[LnTag.SecretTag] = None,
|
||||
nodeId: Option[LnTag.NodeIdTag] = None,
|
||||
expiryTime: Option[LnTag.ExpiryTimeTag] = None,
|
||||
cltvExpiry: Option[LnTag.MinFinalCltvExpiry] = None,
|
||||
fallbackAddress: Option[LnTag.FallbackAddressTag] = None,
|
||||
routingInfo: Option[LnTag.RoutingInfo] = None): LnTaggedFields = {
|
||||
routingInfo: Option[LnTag.RoutingInfo] = None,
|
||||
features: Option[LnTag.FeaturesTag] = None): LnTaggedFields = {
|
||||
|
||||
val (description, descriptionHash): (
|
||||
Option[LnTag.DescriptionTag],
|
||||
|
@ -102,13 +112,15 @@ object LnTaggedFields {
|
|||
|
||||
InvoiceTagImpl(
|
||||
paymentHash = paymentHash,
|
||||
secret = secret,
|
||||
description = description,
|
||||
nodeId = nodeId,
|
||||
descriptionHash = descriptionHash,
|
||||
expiryTime = expiryTime,
|
||||
cltvExpiry = cltvExpiry,
|
||||
fallbackAddress = fallbackAddress,
|
||||
routingInfo = routingInfo
|
||||
routingInfo = routingInfo,
|
||||
features = features
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -164,6 +176,8 @@ object LnTaggedFields {
|
|||
s"Payment hash must be defined in a LnInvoice")
|
||||
)
|
||||
|
||||
val secret = getTag[LnTag.SecretTag]
|
||||
|
||||
val description = getTag[LnTag.DescriptionTag]
|
||||
|
||||
val descriptionHash = getTag[LnTag.DescriptionHashTag]
|
||||
|
@ -178,6 +192,8 @@ object LnTaggedFields {
|
|||
|
||||
val routingInfo = getTag[LnTag.RoutingInfo]
|
||||
|
||||
val features = getTag[LnTag.FeaturesTag]
|
||||
|
||||
val d: Either[LnTag.DescriptionTag, LnTag.DescriptionHashTag] = {
|
||||
if (description.isDefined && descriptionHash.isDefined) {
|
||||
throw new IllegalArgumentException(
|
||||
|
@ -194,12 +210,14 @@ object LnTaggedFields {
|
|||
|
||||
LnTaggedFields(
|
||||
paymentHash = paymentHashTag,
|
||||
secret = secret,
|
||||
descriptionOrHash = d,
|
||||
nodeId = nodeId,
|
||||
expiryTime = expiryTime,
|
||||
cltvExpiry = cltvExpiry,
|
||||
fallbackAddress = fallbackAddress,
|
||||
routingInfo = routingInfo
|
||||
routingInfo = routingInfo,
|
||||
features = features
|
||||
)
|
||||
|
||||
}
|
||||
|
|
|
@ -128,6 +128,15 @@ object LnTag {
|
|||
}
|
||||
}
|
||||
|
||||
case class SecretTag(secret: PaymentSecret) extends LnTag {
|
||||
|
||||
override val prefix: LnTagPrefix = LnTagPrefix.Secret
|
||||
|
||||
override val encoded: Vector[UInt5] = {
|
||||
Bech32.from8bitTo5bit(secret.bytes)
|
||||
}
|
||||
}
|
||||
|
||||
case class DescriptionTag(string: String) extends LnTag {
|
||||
override val prefix: LnTagPrefix = LnTagPrefix.Description
|
||||
|
||||
|
@ -261,6 +270,9 @@ object LnTag {
|
|||
val hash = Sha256Digest.fromBytes(bytes)
|
||||
LnTag.PaymentHashTag(hash)
|
||||
|
||||
case LnTagPrefix.Secret =>
|
||||
LnTag.SecretTag(PaymentSecret.fromBytes(bytes))
|
||||
|
||||
case LnTagPrefix.Description =>
|
||||
val description = new String(bytes.toArray, Charset.forName("UTF-8"))
|
||||
LnTag.DescriptionTag(description)
|
||||
|
@ -287,13 +299,26 @@ object LnTag {
|
|||
case LnTagPrefix.FallbackAddress =>
|
||||
val version = payload.head.toUInt8
|
||||
val noVersion = payload.tail
|
||||
val noVersionBytes = UInt8.toBytes(Bech32.from5bitTo8bit(noVersion))
|
||||
val noVersionBytes =
|
||||
UInt8.toBytes(Bech32.from5bitTo8bit(noVersion))
|
||||
FallbackAddressV.fromU8(version, noVersionBytes, MainNet)
|
||||
|
||||
case LnTagPrefix.RoutingInfo =>
|
||||
RoutingInfo.fromU5s(payload)
|
||||
|
||||
case LnTagPrefix.Features =>
|
||||
LnTag.FeaturesTag(bytes)
|
||||
}
|
||||
|
||||
tag
|
||||
}
|
||||
|
||||
case class FeaturesTag(features: ByteVector) extends LnTag {
|
||||
override def prefix: LnTagPrefix = LnTagPrefix.Features
|
||||
|
||||
/** The payload for the tag without any meta information encoded with it */
|
||||
override def encoded: Vector[UInt5] = {
|
||||
Bech32.from8bitTo5bit(features)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
package org.bitcoins.core.protocol.ln
|
||||
|
||||
import org.bitcoins.core.crypto.{ECPrivateKey, Sha256Digest}
|
||||
import org.bitcoins.core.protocol.NetworkElement
|
||||
import org.bitcoins.core.util.{CryptoUtil, Factory}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
final case class PaymentSecret(bytes: ByteVector) extends NetworkElement {
|
||||
require(bytes.size == 32,
|
||||
s"Payment secret must be 32 bytes in size, got: " + bytes.length)
|
||||
|
||||
lazy val hash: Sha256Digest = CryptoUtil.sha256(bytes)
|
||||
}
|
||||
|
||||
object PaymentSecret extends Factory[PaymentSecret] {
|
||||
|
||||
override def fromBytes(bytes: ByteVector): PaymentSecret = {
|
||||
new PaymentSecret(bytes)
|
||||
}
|
||||
|
||||
def random: PaymentSecret = fromBytes(ECPrivateKey.freshPrivateKey.bytes)
|
||||
|
||||
}
|
|
@ -302,7 +302,7 @@ sealed abstract class Bech32 {
|
|||
.map(_.toLower)
|
||||
.map { char =>
|
||||
val index = Bech32.charset.indexOf(char)
|
||||
require(index > 0,
|
||||
require(index >= 0,
|
||||
s"$char (${char.toInt}) is not part of the Bech32 charset!")
|
||||
UInt5(index)
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ package org.bitcoins.eclair.rpc
|
|||
import java.nio.file.Files
|
||||
|
||||
import org.bitcoins.core.currency.{CurrencyUnit, CurrencyUnits, Satoshis}
|
||||
import org.bitcoins.core.number.{Int64, UInt64}
|
||||
import org.bitcoins.core.number.UInt64
|
||||
import org.bitcoins.core.protocol.BitcoinAddress
|
||||
import org.bitcoins.core.protocol.ln.LnParams.LnBitcoinRegTest
|
||||
import org.bitcoins.core.protocol.ln.channel.{
|
||||
|
@ -353,7 +353,8 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
|
|||
bitcoind <- EclairRpcTestUtil.startedBitcoindRpcClient()
|
||||
eclair <- {
|
||||
val server = EclairRpcTestUtil.eclairInstance(bitcoind)
|
||||
val eclair = new EclairRpcClient(server, EclairRpcTestUtil.binary)
|
||||
val eclair =
|
||||
new EclairRpcClient(server, EclairRpcTestUtil.binary(None, None))
|
||||
eclair.start().map(_ => eclair)
|
||||
}
|
||||
_ <- TestAsyncUtil.retryUntilSatisfiedF(conditionF =
|
||||
|
@ -480,7 +481,8 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
|
|||
}
|
||||
|
||||
val badClientF =
|
||||
badInstanceF.map(new EclairRpcClient(_, EclairRpcTestUtil.binary))
|
||||
badInstanceF.map(
|
||||
new EclairRpcClient(_, EclairRpcTestUtil.binary(None, None)))
|
||||
|
||||
badClientF.flatMap { badClient =>
|
||||
recoverToSucceededIf[RuntimeException](badClient.getInfo)
|
||||
|
@ -809,7 +811,7 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
|
|||
// We spawn fresh clients in this test because the test
|
||||
// needs nodes with activity both related and not related
|
||||
// to them
|
||||
it should "get all channel updates for a given node ID" in {
|
||||
it should "get all channel updates for a given node ID" ignore {
|
||||
val freshClients1F = bitcoindRpcClientF.flatMap { bitcoindRpcClient =>
|
||||
EclairRpcTestUtil.createNodePair(Some(bitcoindRpcClient))
|
||||
}
|
||||
|
@ -844,8 +846,9 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
|
|||
}
|
||||
}
|
||||
|
||||
def openChannel(c1: EclairRpcClient,
|
||||
c2: EclairRpcClient): Future[FundedChannelId] = {
|
||||
def openChannel(
|
||||
c1: EclairRpcClient,
|
||||
c2: EclairRpcClient): Future[FundedChannelId] = {
|
||||
EclairRpcTestUtil
|
||||
.openChannel(c1, c2, Satoshis(500000), MilliSatoshis(500000))
|
||||
}
|
||||
|
@ -1013,7 +1016,7 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
|
|||
)
|
||||
}
|
||||
|
||||
it should "get updates for a single node" in {
|
||||
it should "get updates for a single node" ignore {
|
||||
// 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 {
|
||||
|
@ -1049,7 +1052,7 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
|
|||
ourUpdates.flatMap(our =>
|
||||
allUpdates.map { all =>
|
||||
our != all
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
AsyncUtil
|
||||
|
@ -1145,8 +1148,9 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
|
|||
}
|
||||
}
|
||||
|
||||
private def hasConnection(client: Future[EclairRpcClient],
|
||||
nodeId: NodeId): Future[Assertion] = {
|
||||
private def hasConnection(
|
||||
client: Future[EclairRpcClient],
|
||||
nodeId: NodeId): Future[Assertion] = {
|
||||
|
||||
val hasPeersF = client.flatMap(_.getPeers.map(_.nonEmpty))
|
||||
|
||||
|
@ -1161,8 +1165,9 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
|
|||
}
|
||||
|
||||
/** Checks that the given [[org.bitcoins.eclair.rpc.client.EclairRpcClient]] has the given chanId */
|
||||
private def hasChannel(client: EclairRpcClient,
|
||||
chanId: ChannelId): Future[Assertion] = {
|
||||
private def hasChannel(
|
||||
client: EclairRpcClient,
|
||||
chanId: ChannelId): Future[Assertion] = {
|
||||
val recognizedOpenChannel: Future[Assertion] = {
|
||||
|
||||
val chanResultF: Future[ChannelResult] = client.channel(chanId)
|
||||
|
|
|
@ -285,25 +285,43 @@ object WebSocketEvent {
|
|||
timestamp: FiniteDuration //milliseconds
|
||||
) extends WebSocketEvent
|
||||
|
||||
// {"type":"payment-received","paymentHash":"e1367ac5f913708f9ecc754c49477db3e7de404de7e921cab2dfe489227e07a7","parts":[{"amount":1000,"fromChannelId":"f59a3347ac6ef95ae4ad3e3777d137f80e02bf0a88d65b88f521676c7c713bf8","timestamp":1578080963457}]}
|
||||
case class PaymentReceived(
|
||||
amount: MilliSatoshis,
|
||||
paymentHash: Sha256Digest,
|
||||
fromChannelId: FundedChannelId,
|
||||
parts: Vector[PaymentReceived.Part]
|
||||
) extends WebSocketEvent
|
||||
|
||||
object PaymentReceived {
|
||||
case class Part(
|
||||
amount: MilliSatoshis,
|
||||
fromChannelId: FundedChannelId,
|
||||
timestamp: FiniteDuration // milliseconds
|
||||
)
|
||||
}
|
||||
case class PaymentFailed(
|
||||
id: PaymentId,
|
||||
paymentHash: Sha256Digest,
|
||||
failures: Vector[String],
|
||||
timestamp: FiniteDuration // milliseconds
|
||||
) extends WebSocketEvent
|
||||
|
||||
case class PaymentFailed(paymentHash: Sha256Digest, failures: Vector[String])
|
||||
extends WebSocketEvent
|
||||
|
||||
case class PaymentSent(
|
||||
amount: MilliSatoshis,
|
||||
feesPaid: MilliSatoshis,
|
||||
id: PaymentId,
|
||||
paymentHash: Sha256Digest,
|
||||
paymentPreimage: PaymentPreimage,
|
||||
toChannelId: FundedChannelId,
|
||||
timestamp: FiniteDuration //milliseconds
|
||||
parts: Vector[PaymentSent.Part]
|
||||
) extends WebSocketEvent
|
||||
|
||||
object PaymentSent {
|
||||
case class Part(
|
||||
id: PaymentId,
|
||||
amount: MilliSatoshis,
|
||||
feesPaid: MilliSatoshis,
|
||||
toChannelId: FundedChannelId,
|
||||
timestamp: FiniteDuration // milliseconds
|
||||
)
|
||||
}
|
||||
|
||||
case class PaymentSettlingOnchain(
|
||||
amount: MilliSatoshis,
|
||||
paymentHash: Sha256Digest,
|
||||
|
|
|
@ -4,11 +4,15 @@ import java.io.File
|
|||
import java.nio.file.NoSuchFileException
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
import akka.Done
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.javadsl.model.headers.HttpCredentials
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials}
|
||||
import akka.http.scaladsl.model.ws.{Message, TextMessage, WebSocketRequest}
|
||||
import akka.stream.ActorMaterializer
|
||||
import akka.stream.scaladsl.{Flow, Sink, Source}
|
||||
import akka.util.ByteString
|
||||
import org.bitcoins.core.crypto.Sha256Digest
|
||||
import org.bitcoins.core.currency.{CurrencyUnit, Satoshis}
|
||||
|
@ -837,6 +841,45 @@ class EclairRpcClient(val instance: EclairInstance, binary: Option[File] = None)
|
|||
|
||||
f
|
||||
}
|
||||
|
||||
def connectToWebSocket[T](f: WebSocketEvent => T): Future[Unit] = {
|
||||
val incoming: Sink[Message, Future[Done]] =
|
||||
Sink.foreach[Message] {
|
||||
case message: TextMessage.Strict =>
|
||||
val parsed: JsValue = Json.parse(message.text)
|
||||
val validated: JsResult[WebSocketEvent] =
|
||||
parsed.validate[WebSocketEvent]
|
||||
val event = parseResult[WebSocketEvent](validated, parsed, "ws")
|
||||
f(event)
|
||||
}
|
||||
|
||||
val flow =
|
||||
Flow.fromSinkAndSource(incoming, Source.maybe)
|
||||
|
||||
val uri =
|
||||
instance.rpcUri.resolve("/ws").toString.replace("http://", "ws://")
|
||||
instance.authCredentials.bitcoinAuthOpt
|
||||
val request = WebSocketRequest(
|
||||
uri,
|
||||
extraHeaders = Seq(
|
||||
Authorization(
|
||||
BasicHttpCredentials("", instance.authCredentials.password))))
|
||||
val (upgradeResponse, _) = Http().singleWebSocketRequest(request, flow)
|
||||
|
||||
val connected = upgradeResponse.map { upgrade =>
|
||||
if (upgrade.response.status == StatusCodes.SwitchingProtocols) {
|
||||
Done
|
||||
} else {
|
||||
throw new RuntimeException(
|
||||
s"Connection failed: ${upgrade.response.status}")
|
||||
}
|
||||
}
|
||||
|
||||
connected.failed.foreach(ex =>
|
||||
logger.error(s"Cannot connect to web socket $uri ", ex))
|
||||
|
||||
connected.map(_ => ())
|
||||
}
|
||||
}
|
||||
|
||||
object EclairRpcClient {
|
||||
|
|
|
@ -9,34 +9,7 @@ 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.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.api._
|
||||
import org.bitcoins.eclair.rpc.network.PeerState
|
||||
import org.bitcoins.rpc.serializers.SerializerUtil
|
||||
import play.api.libs.json._
|
||||
|
@ -465,39 +438,66 @@ object JsonReaders {
|
|||
timestamp)
|
||||
}
|
||||
|
||||
implicit val paymentReceivedEventReads: Reads[
|
||||
WebSocketEvent.PaymentReceived] = Reads { js =>
|
||||
implicit val paymentReceivedEventPartReads: Reads[
|
||||
WebSocketEvent.PaymentReceived.Part] = Reads { js =>
|
||||
for {
|
||||
amount <- (js \ "amount").validate[MilliSatoshis]
|
||||
paymentHash <- (js \ "paymentHash").validate[Sha256Digest]
|
||||
fromChannelId <- (js \ "fromChannelId").validate[FundedChannelId]
|
||||
timestamp <- (js \ "timestamp")
|
||||
.validate[FiniteDuration](finiteDurationReadsMilliseconds)
|
||||
} yield WebSocketEvent.PaymentReceived(amount,
|
||||
paymentHash,
|
||||
fromChannelId,
|
||||
timestamp)
|
||||
} yield WebSocketEvent.PaymentReceived.Part(amount,
|
||||
fromChannelId,
|
||||
timestamp)
|
||||
}
|
||||
|
||||
implicit val paymentReceivedEventReads: Reads[
|
||||
WebSocketEvent.PaymentReceived] = Reads { js =>
|
||||
for {
|
||||
paymentHash <- (js \ "paymentHash").validate[Sha256Digest]
|
||||
parts <- (js \ "parts")
|
||||
.validate[Vector[WebSocketEvent.PaymentReceived.Part]]
|
||||
} yield WebSocketEvent.PaymentReceived(paymentHash, parts)
|
||||
}
|
||||
|
||||
implicit val paymentFailedEventReads: Reads[WebSocketEvent.PaymentFailed] =
|
||||
Json.reads[WebSocketEvent.PaymentFailed]
|
||||
Reads { js =>
|
||||
for {
|
||||
id <- (js \ "id").validate[PaymentId]
|
||||
paymentHash <- (js \ "paymentHash").validate[Sha256Digest]
|
||||
failures <- (js \ "failures").validate[Vector[String]]
|
||||
timestamp <- (js \ "timestamp")
|
||||
.validate[FiniteDuration](finiteDurationReadsMilliseconds)
|
||||
} yield WebSocketEvent.PaymentFailed(id, paymentHash, failures, timestamp)
|
||||
}
|
||||
|
||||
implicit val paymentSentEventPartReads: Reads[
|
||||
WebSocketEvent.PaymentSent.Part] = Reads { js =>
|
||||
for {
|
||||
id <- (js \ "id").validate[PaymentId]
|
||||
amount <- (js \ "amount").validate[MilliSatoshis]
|
||||
feesPaid <- (js \ "feesPaid").validate[MilliSatoshis]
|
||||
toChannelId <- (js \ "toChannelId").validate[FundedChannelId]
|
||||
timestamp <- (js \ "timestamp")
|
||||
.validate[FiniteDuration](finiteDurationReadsMilliseconds)
|
||||
} yield WebSocketEvent.PaymentSent.Part(id,
|
||||
amount,
|
||||
feesPaid,
|
||||
toChannelId,
|
||||
timestamp)
|
||||
}
|
||||
|
||||
implicit val paymentSentEventReads: Reads[WebSocketEvent.PaymentSent] =
|
||||
Reads { js =>
|
||||
for {
|
||||
amount <- (js \ "amount").validate[MilliSatoshis]
|
||||
feesPaid <- (js \ "feesPaid").validate[MilliSatoshis]
|
||||
id <- (js \ "id").validate[PaymentId]
|
||||
paymentHash <- (js \ "paymentHash").validate[Sha256Digest]
|
||||
paymentPreimage <- (js \ "paymentPreimage").validate[PaymentPreimage]
|
||||
toChannelId <- (js \ "toChannelId").validate[FundedChannelId]
|
||||
timestamp <- (js \ "timestamp")
|
||||
.validate[FiniteDuration](finiteDurationReadsMilliseconds)
|
||||
} yield WebSocketEvent.PaymentSent(amount,
|
||||
feesPaid,
|
||||
parts <- (js \ "parts")
|
||||
.validate[Vector[WebSocketEvent.PaymentSent.Part]]
|
||||
} yield WebSocketEvent.PaymentSent(id,
|
||||
paymentHash,
|
||||
paymentPreimage,
|
||||
toChannelId,
|
||||
timestamp)
|
||||
parts)
|
||||
}
|
||||
|
||||
implicit val paymentSettlingOnchainEventReads: Reads[
|
||||
|
@ -512,4 +512,20 @@ object JsonReaders {
|
|||
timestamp)
|
||||
}
|
||||
|
||||
implicit val webSocketEventReads: Reads[WebSocketEvent] =
|
||||
Reads { js =>
|
||||
(js \ "type")
|
||||
.validate[String]
|
||||
.flatMap {
|
||||
case "payment-relayed" => js.validate[WebSocketEvent.PaymentRelayed]
|
||||
case "payment-received" => js.validate[WebSocketEvent.PaymentReceived]
|
||||
case "payment-failed" =>
|
||||
js.validate[WebSocketEvent.PaymentFailed]
|
||||
case "payment-sent" =>
|
||||
js.validate[WebSocketEvent.PaymentSent]
|
||||
case "payment-settling-onchain" =>
|
||||
js.validate[WebSocketEvent.PaymentSettlingOnchain]
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
package org.bitcoins.testkit.core.gen.ln
|
||||
|
||||
import org.bitcoins.core.crypto.ECPrivateKey
|
||||
import org.bitcoins.testkit.core.gen._
|
||||
import org.bitcoins.core.number.{UInt64, UInt8}
|
||||
import org.bitcoins.core.protocol.ln.LnTag.NodeIdTag
|
||||
import org.bitcoins.core.protocol.ln._
|
||||
import org.bitcoins.core.protocol.ln.node.NodeId
|
||||
import org.bitcoins.testkit.core.gen._
|
||||
import org.scalacheck.Gen
|
||||
|
||||
sealed abstract class LnInvoiceGen {
|
||||
|
@ -48,6 +48,12 @@ sealed abstract class LnInvoiceGen {
|
|||
}
|
||||
}
|
||||
|
||||
def secret: Gen[LnTag.SecretTag] = {
|
||||
CryptoGenerators.sha256Digest.map { hash =>
|
||||
LnTag.SecretTag(PaymentSecret.fromBytes(hash.bytes))
|
||||
}
|
||||
}
|
||||
|
||||
def descriptionTag: Gen[LnTag.DescriptionTag] = {
|
||||
StringGenerators.genString.map { description =>
|
||||
LnTag.DescriptionTag(description)
|
||||
|
@ -103,12 +109,14 @@ sealed abstract class LnInvoiceGen {
|
|||
descOrHashTag <- descriptionOrDescriptionHashTag
|
||||
} yield LnTaggedFields(
|
||||
paymentHash = paymentHash,
|
||||
secret = None,
|
||||
descriptionOrHash = descOrHashTag,
|
||||
expiryTime = None,
|
||||
cltvExpiry = None,
|
||||
fallbackAddress = None,
|
||||
nodeId = None,
|
||||
routingInfo = None
|
||||
routingInfo = None,
|
||||
features = None
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -116,7 +124,7 @@ sealed abstract class LnInvoiceGen {
|
|||
for {
|
||||
paymentHash <- paymentHashTag
|
||||
descOrHashTag <- descriptionOrDescriptionHashTag
|
||||
|
||||
secret <- Gen.option(secret)
|
||||
//optional fields
|
||||
expiryTime <- Gen.option(expiryTime)
|
||||
cltvExpiry <- Gen.option(cltvExpiry)
|
||||
|
@ -124,12 +132,14 @@ sealed abstract class LnInvoiceGen {
|
|||
routes <- Gen.option(routingInfo)
|
||||
} yield LnTaggedFields(
|
||||
paymentHash = paymentHash,
|
||||
secret = secret,
|
||||
descriptionOrHash = descOrHashTag,
|
||||
expiryTime = expiryTime,
|
||||
cltvExpiry = cltvExpiry,
|
||||
fallbackAddress = fallbackAddress,
|
||||
nodeId = nodeIdOpt.map(NodeIdTag(_)),
|
||||
routingInfo = routes
|
||||
routingInfo = routes,
|
||||
features = None
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -137,7 +147,7 @@ sealed abstract class LnInvoiceGen {
|
|||
for {
|
||||
paymentHash <- paymentHashTag
|
||||
descOrHashTag <- descriptionOrDescriptionHashTag
|
||||
|
||||
secret <- secret
|
||||
//optional fields
|
||||
expiryTime <- expiryTime
|
||||
cltvExpiry <- cltvExpiry
|
||||
|
@ -145,12 +155,14 @@ sealed abstract class LnInvoiceGen {
|
|||
routes <- routingInfo
|
||||
} yield LnTaggedFields(
|
||||
paymentHash = paymentHash,
|
||||
secret = Some(secret),
|
||||
descriptionOrHash = descOrHashTag,
|
||||
expiryTime = Some(expiryTime),
|
||||
cltvExpiry = Some(cltvExpiry),
|
||||
fallbackAddress = Some(fallbackAddress),
|
||||
nodeId = nodeIdOpt.map(NodeIdTag(_)),
|
||||
routingInfo = Some(routes)
|
||||
routingInfo = Some(routes),
|
||||
features = None
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import java.nio.file.Files
|
|||
|
||||
import akka.actor.ActorSystem
|
||||
import com.typesafe.config.{Config, ConfigFactory}
|
||||
import org.bitcoins.core.compat.JavaConverters._
|
||||
import org.bitcoins.core.config.RegTest
|
||||
import org.bitcoins.core.currency.CurrencyUnit
|
||||
import org.bitcoins.core.protocol.ln.channel.{
|
||||
|
@ -47,18 +48,20 @@ import scala.util.{Failure, Success}
|
|||
*
|
||||
*/
|
||||
trait EclairRpcTestUtil extends BitcoinSLogger {
|
||||
import org.bitcoins.core.compat.JavaConverters._
|
||||
|
||||
/** Directory where sbt downloads Eclair binaries */
|
||||
private[bitcoins] val binaryDirectory =
|
||||
BitcoindRpcTestUtil.baseBinaryDirectory.resolve("eclair")
|
||||
|
||||
/** Path to Jar downloaded by Eclair, if it exists */
|
||||
private[bitcoins] lazy val binary: Option[File] = {
|
||||
private[bitcoins] def binary(
|
||||
eclairVersionOpt: Option[String],
|
||||
eclairCommitOpt: Option[String]): Option[File] = {
|
||||
val path = binaryDirectory
|
||||
.resolve(EclairRpcClient.version)
|
||||
.resolve(eclairVersionOpt.getOrElse(EclairRpcClient.version))
|
||||
.resolve(
|
||||
s"eclair-node-${EclairRpcClient.version}-${EclairRpcClient.commit}.jar")
|
||||
s"eclair-node-${eclairVersionOpt.getOrElse(EclairRpcClient.version)}-${eclairCommitOpt
|
||||
.getOrElse(EclairRpcClient.commit)}.jar")
|
||||
|
||||
if (Files.exists(path)) {
|
||||
Some(path.toFile)
|
||||
|
@ -70,7 +73,8 @@ trait EclairRpcTestUtil extends BitcoinSLogger {
|
|||
def randomDirName: String =
|
||||
0.until(5).map(_ => scala.util.Random.alphanumeric.head).mkString
|
||||
|
||||
def randomEclairDatadir(): File = new File(s"/tmp/${randomDirName}/.eclair/")
|
||||
def randomEclairDatadir(): File =
|
||||
new File(s"/tmp/eclair-test/${randomDirName}/.eclair/")
|
||||
|
||||
def cannonicalDatadir = new File(s"${System.getenv("HOME")}/.reg_eclair/")
|
||||
|
||||
|
@ -94,7 +98,7 @@ trait EclairRpcTestUtil extends BitcoinSLogger {
|
|||
}
|
||||
|
||||
//cribbed from https://github.com/Christewart/eclair/blob/bad02e2c0e8bd039336998d318a861736edfa0ad/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala#L140-L153
|
||||
private def commonConfig(
|
||||
private[rpc] def commonConfig(
|
||||
bitcoindInstance: BitcoindInstance,
|
||||
port: Int = RpcUtil.randomPort,
|
||||
apiPort: Int = RpcUtil.randomPort): Config = {
|
||||
|
@ -126,7 +130,6 @@ trait EclairRpcTestUtil extends BitcoinSLogger {
|
|||
"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
|
||||
"eclair.alias" -> "suredbits"
|
||||
|
@ -181,7 +184,10 @@ trait EclairRpcTestUtil extends BitcoinSLogger {
|
|||
eclairInstance(datadir)
|
||||
}
|
||||
|
||||
def randomEclairClient(bitcoindRpcOpt: Option[BitcoindRpcClient] = None)(
|
||||
def randomEclairClient(
|
||||
bitcoindRpcOpt: Option[BitcoindRpcClient] = None,
|
||||
eclairVersionOpt: Option[String] = None,
|
||||
eclairCommitOpt: Option[String] = None)(
|
||||
implicit system: ActorSystem): Future[EclairRpcClient] = {
|
||||
import system.dispatcher
|
||||
val bitcoindRpcF: Future[BitcoindRpcClient] = {
|
||||
|
@ -193,17 +199,20 @@ trait EclairRpcTestUtil extends BitcoinSLogger {
|
|||
}
|
||||
|
||||
val randInstanceF = bitcoindRpcF.map(randomEclairInstance(_))
|
||||
val eclairRpcF = randInstanceF.map(i => new EclairRpcClient(i, binary))
|
||||
val eclairRpcF = randInstanceF.map(i =>
|
||||
new EclairRpcClient(i, binary(eclairVersionOpt, eclairCommitOpt)))
|
||||
|
||||
val startedF = eclairRpcF.flatMap(_.start())
|
||||
|
||||
startedF.flatMap(_ => eclairRpcF)
|
||||
}
|
||||
|
||||
def cannonicalEclairClient()(
|
||||
def cannonicalEclairClient(
|
||||
eclairVersionOpt: Option[String] = None,
|
||||
eclairCommitOpt: Option[String] = None)(
|
||||
implicit system: ActorSystem): EclairRpcClient = {
|
||||
val inst = cannonicalEclairInstance()
|
||||
new EclairRpcClient(inst, binary)
|
||||
new EclairRpcClient(inst, binary(eclairVersionOpt, eclairCommitOpt))
|
||||
}
|
||||
|
||||
def deleteTmpDir(dir: File): Boolean = {
|
||||
|
@ -441,7 +450,12 @@ trait EclairRpcTestUtil extends BitcoinSLogger {
|
|||
* Creates two Eclair nodes that are connected together and returns their
|
||||
* respective [[org.bitcoins.eclair.rpc.client.EclairRpcClient EclairRpcClient]]s
|
||||
*/
|
||||
def createNodePair(bitcoindRpcClientOpt: Option[BitcoindRpcClient])(
|
||||
def createNodePair(
|
||||
bitcoindRpcClientOpt: Option[BitcoindRpcClient],
|
||||
eclairVersionOpt1: Option[String] = None,
|
||||
eclairCommitOpt1: Option[String] = None,
|
||||
eclairVersionOpt2: Option[String] = None,
|
||||
eclairCommitOpt2: Option[String] = None)(
|
||||
implicit system: ActorSystem): Future[
|
||||
(EclairRpcClient, EclairRpcClient)] = {
|
||||
import system.dispatcher
|
||||
|
@ -460,13 +474,15 @@ trait EclairRpcTestUtil extends BitcoinSLogger {
|
|||
bitcoindRpcClientF.map(EclairRpcTestUtil.eclairInstance(_))
|
||||
|
||||
val clientF = e1InstanceF.flatMap { e1 =>
|
||||
val e = new EclairRpcClient(e1, binary)
|
||||
val e =
|
||||
new EclairRpcClient(e1, binary(eclairVersionOpt1, eclairCommitOpt1))
|
||||
logger.debug(
|
||||
s"Temp eclair directory created ${e.getDaemon.authCredentials.datadir}")
|
||||
e.start().map(_ => e)
|
||||
}
|
||||
val otherClientF = e2InstanceF.flatMap { e2 =>
|
||||
val e = new EclairRpcClient(e2, binary)
|
||||
val e =
|
||||
new EclairRpcClient(e2, binary(eclairVersionOpt2, eclairCommitOpt2))
|
||||
logger.debug(
|
||||
s"Temp eclair directory created ${e.getDaemon.authCredentials.datadir}")
|
||||
e.start().map(_ => e)
|
||||
|
@ -682,9 +698,82 @@ trait EclairRpcTestUtil extends BitcoinSLogger {
|
|||
}
|
||||
shutdownF
|
||||
}
|
||||
|
||||
case class Network(
|
||||
bitcoind: BitcoindRpcClient,
|
||||
testEclairNode: EclairRpcClient,
|
||||
networkEclairNodes: Vector[EclairRpcClient],
|
||||
channelIds: Vector[FundedChannelId]) {
|
||||
|
||||
def shutdown()(implicit ec: ExecutionContext): Future[Unit] =
|
||||
for {
|
||||
_ <- Future.sequence(networkEclairNodes.map(_.stop()))
|
||||
_ <- testEclairNode.stop()
|
||||
_ <- bitcoind.stop()
|
||||
} yield ()
|
||||
}
|
||||
|
||||
object Network {
|
||||
|
||||
def start(
|
||||
eclairVersion: Option[String],
|
||||
eclairCommit: Option[String],
|
||||
networkSize: Int,
|
||||
channelAmount: MilliSatoshis)(
|
||||
implicit system: ActorSystem): Future[Network] = {
|
||||
import system.dispatcher
|
||||
for {
|
||||
bitcoind <- startedBitcoindRpcClient()
|
||||
testEclairInstance = EclairRpcTestUtil.eclairInstance(bitcoind)
|
||||
testEclairNode = new EclairRpcClient(
|
||||
testEclairInstance,
|
||||
binary(eclairVersion, eclairCommit))
|
||||
_ <- testEclairNode.start()
|
||||
_ <- awaitEclairInSync(testEclairNode, bitcoind)
|
||||
networkEclairInstances = 1
|
||||
.to(networkSize)
|
||||
.toVector
|
||||
.map(_ => EclairRpcTestUtil.eclairInstance(bitcoind))
|
||||
networkEclairNodes = networkEclairInstances.map(
|
||||
new EclairRpcClient(_,
|
||||
binary(Some(EclairRpcClient.version),
|
||||
Some(EclairRpcClient.commit))))
|
||||
_ <- Future.sequence(networkEclairNodes.map(_.start()))
|
||||
_ <- Future.sequence(
|
||||
networkEclairNodes.map(awaitEclairInSync(_, bitcoind)))
|
||||
_ <- Future.sequence(
|
||||
networkEclairNodes.map(connectLNNodes(_, testEclairNode)))
|
||||
channelIds <- networkEclairNodes.foldLeft(
|
||||
Future.successful(Vector.empty[FundedChannelId])) { (accF, node) =>
|
||||
for {
|
||||
acc <- accF
|
||||
channelId <- openChannel(n1 = node,
|
||||
n2 = testEclairNode,
|
||||
amt = channelAmount.toSatoshis,
|
||||
pushMSat =
|
||||
MilliSatoshis(channelAmount.toLong / 2))
|
||||
} yield acc :+ channelId
|
||||
}
|
||||
_ <- Future.sequence(
|
||||
channelIds.map(awaitChannelOpened(testEclairNode, _)))
|
||||
} yield Network(bitcoind, testEclairNode, networkEclairNodes, channelIds)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
object EclairRpcTestUtil extends EclairRpcTestUtil
|
||||
object EclairRpcTestUtil extends EclairRpcTestUtil {
|
||||
var customConfigMap: Map[String, Any] = Map.empty
|
||||
|
||||
override def commonConfig(
|
||||
bitcoindInstance: BitcoindInstance,
|
||||
port: Int,
|
||||
apiPort: Int): Config =
|
||||
super
|
||||
.commonConfig(bitcoindInstance, port, apiPort)
|
||||
.withFallback(ConfigFactory.parseMap(customConfigMap.asJava))
|
||||
|
||||
}
|
||||
|
||||
case class EclairNodes4(
|
||||
c1: EclairRpcClient,
|
||||
|
|
Loading…
Add table
Reference in a new issue