Eclair RPC 0.4.1 (#1627)

* Eclair RPC 0.4.1

* channelstats

* remove the launch script editing code

* getinfo

* sendonchain, onchainbalance, onchaintransactions

* cleanup

* repond to the comments
This commit is contained in:
rorp 2020-07-07 13:22:44 -07:00 committed by GitHub
parent 194370622d
commit 43b6349758
7 changed files with 325 additions and 110 deletions

View File

@ -5,11 +5,12 @@ import java.time.Instant
import java.util.UUID
import org.bitcoins.commons.serializers.JsonReaders._
import org.bitcoins.core.config.BitcoinNetwork
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.protocol.ln.channel.{ChannelState, FundedChannelId}
import org.bitcoins.core.protocol.ln.currency.MilliSatoshis
import org.bitcoins.core.protocol.ln.fee.FeeProportionalMillionths
import org.bitcoins.core.protocol.ln.node.NodeId
import org.bitcoins.core.protocol.ln.node.{Feature, FeatureSupport, NodeId}
import org.bitcoins.core.protocol.ln.{
LnHumanReadablePart,
PaymentPreimage,
@ -28,11 +29,15 @@ import scala.concurrent.duration.FiniteDuration
sealed abstract class EclairModels
case class GetInfoResult(
version: String,
nodeId: NodeId,
alias: String,
features: Features,
chainHash: DoubleSha256Digest,
network: BitcoinNetwork,
blockHeight: Long,
publicAddresses: Seq[InetSocketAddress])
publicAddresses: Seq[InetSocketAddress],
instanceId: UUID)
case class PeerInfo(
nodeId: NodeId,
@ -40,6 +45,31 @@ case class PeerInfo(
address: Option[String],
channels: Int)
case class ChannelCommandResult(
results: scala.collection.Map[
Either[ShortChannelId, FundedChannelId],
ChannelCommandResult.State]
)
object ChannelCommandResult {
sealed trait State
case object OK extends State
case object ChannelOpened extends State
case object ChannelClosed extends State
case class Error(message: String) extends State
def fromString(s: String): State =
if (s == "ok") {
ChannelCommandResult.OK
} else if (s.startsWith("created channel ")) {
ChannelCommandResult.ChannelOpened
} else if (s.startsWith("closed channel ")) {
ChannelCommandResult.ChannelClosed
} else {
ChannelCommandResult.Error(s)
}
}
/**
* This is the data model returned by the RPC call
* `channels nodeId`. The content of the objects
@ -82,14 +112,23 @@ case class OpenChannelInfo(
state: ChannelState.NORMAL.type
) extends ChannelInfo
case class ActivatedFeature(feature: Feature, support: FeatureSupport)
case class UnknownFeature(bitIndex: Int)
case class Features(
activated: Set[ActivatedFeature],
unknown: Set[UnknownFeature])
case class NodeInfo(
signature: ECDigitalSignature,
features: String,
features: Features,
timestamp: Instant,
nodeId: NodeId,
rgbColor: String,
alias: String,
addresses: Vector[InetSocketAddress])
addresses: Vector[InetSocketAddress],
unknownFields: String)
case class ChannelDesc(shortChannelId: ShortChannelId, a: NodeId, b: NodeId)
@ -110,12 +149,31 @@ case class NetworkFeesResult(
case class ChannelStats(
channelId: FundedChannelId,
direction: ChannelStats.Direction,
avgPaymentAmount: Satoshis,
paymentCount: Long,
relayFee: Satoshis,
networkFee: Satoshis
)
object ChannelStats {
sealed trait Direction
case object In extends Direction
case object Out extends Direction
object Direction {
def fromString(s: String): Direction =
if (s.toUpperCase == "IN") {
ChannelStats.In
} else if (s.toUpperCase == "OUT") {
ChannelStats.Out
} else {
throw new RuntimeException(s"Unknown payment direction: `$s`")
}
}
}
case class UsableBalancesResult(
remoteNodeId: NodeId,
shortChannelId: ShortChannelId,
@ -362,3 +420,14 @@ object WebSocketEvent {
) extends WebSocketEvent
}
case class OnChainBalance(confirmed: Satoshis, unconfirmed: Satoshis)
case class WalletTransaction(
address: String,
amount: Satoshis,
fees: Satoshis,
blockHash: DoubleSha256DigestBE,
confirmations: Long,
txid: DoubleSha256DigestBE,
timestamp: Long)

View File

@ -19,7 +19,7 @@ import org.bitcoins.core.protocol.ln._
import org.bitcoins.core.protocol.ln.channel._
import org.bitcoins.core.protocol.ln.currency._
import org.bitcoins.core.protocol.ln.fee.FeeProportionalMillionths
import org.bitcoins.core.protocol.ln.node.NodeId
import org.bitcoins.core.protocol.ln.node.{Feature, FeatureSupport, NodeId}
import org.bitcoins.core.protocol.script.{
ScriptPubKey,
ScriptSignature,
@ -36,16 +36,7 @@ import org.bitcoins.core.protocol.{
import org.bitcoins.core.script.ScriptType
import org.bitcoins.core.script.crypto.HashType
import org.bitcoins.core.wallet.fee.{BitcoinFeeUnit, SatoshisPerByte}
import org.bitcoins.crypto.{
DoubleSha256Digest,
DoubleSha256DigestBE,
ECDigitalSignature,
ECPublicKey,
RipeMd160Digest,
RipeMd160DigestBE,
Sha256Digest,
Sha256Hash160Digest
}
import org.bitcoins.crypto._
import play.api.libs.json._
import scala.concurrent.duration._
@ -257,6 +248,13 @@ object JsonReaders {
}
}
implicit object BitcoinNetworkReads extends Reads[BitcoinNetwork] {
override def reads(json: JsValue): JsResult[BitcoinNetwork] =
SerializerUtil.processJsString(BitcoinNetworks.fromString)(json)
}
// Errors for Unit return types are caught in RpcClient::checkUnit
implicit object UnitReads extends Reads[Unit] {
override def reads(json: JsValue): JsResult[Unit] = JsSuccess(())
@ -718,6 +716,41 @@ object JsonReaders {
}
}
implicit val featureSupportReads: Reads[FeatureSupport] =
Reads { jsValue =>
SerializerUtil.processJsString {
case "mandatory" => FeatureSupport.Mandatory
case "optional" => FeatureSupport.Optional
case err: String =>
throw new RuntimeException(s"Invalid feature support value: `$err`")
}(jsValue)
}
lazy val featuresByName: Map[String, Feature] =
Feature.knownFeatures.map(f => (f.rfcName, f)).toMap
implicit val featureReads: Reads[Feature] =
Reads { jsValue =>
SerializerUtil.processJsString(featuresByName)(jsValue)
}
implicit val unknownFeatureReads: Reads[UnknownFeature] =
Reads { jsValue =>
SerializerUtil.processJsString(s => UnknownFeature(s.toInt))(jsValue)
}
implicit val activatedFeatureReads: Reads[ActivatedFeature] =
Reads { jsValue =>
for {
feature <- (jsValue \ "name").validate[Feature]
support <- (jsValue \ "support").validate[FeatureSupport]
} yield ActivatedFeature(feature, support)
}
implicit val featuresReads: Reads[Features] = {
Json.reads[Features]
}
implicit val getInfoResultReads: Reads[GetInfoResult] = {
Json.reads[GetInfoResult]
}
@ -737,20 +770,22 @@ object JsonReaders {
Reads { jsValue =>
for {
signature <- (jsValue \ "signature").validate[ECDigitalSignature]
features <- (jsValue \ "features").validate[String]
features <- (jsValue \ "features").validate[Features]
timestamp <- (jsValue \ "timestamp")
.validate[Instant](instantReadsSeconds)
nodeId <- (jsValue \ "nodeId").validate[NodeId]
rgbColor <- (jsValue \ "rgbColor").validate[String]
alias <- (jsValue \ "alias").validate[String]
addresses <- (jsValue \ "addresses").validate[Vector[InetSocketAddress]]
unknownFields <- (jsValue \ "unknownFields").validate[String]
} yield NodeInfo(signature,
features,
timestamp,
nodeId,
rgbColor,
alias,
addresses)
addresses,
unknownFields)
}
}
@ -843,6 +878,25 @@ object JsonReaders {
}
}
implicit val channelCommandResultStateReads: Reads[
ChannelCommandResult.State] = Reads { jsValue =>
SerializerUtil.processJsString(ChannelCommandResult.fromString)(jsValue)
}
implicit val channelCommandResultReads: Reads[ChannelCommandResult] = Reads {
case obj: JsObject =>
JsSuccess(ChannelCommandResult(obj.value.map { x =>
val channelId = Try(FundedChannelId.fromHex(x._1)) match {
case Success(id) => Right(id)
case Failure(_) => Left(ShortChannelId.fromHumanReadableString(x._1))
}
(channelId, x._2.validate[ChannelCommandResult.State].get)
}))
case err @ (JsNull | _: JsBoolean | _: JsString | _: JsArray |
_: JsNumber) =>
SerializerUtil.buildJsErrorMsg("jsobject", err)
}
implicit val channelUpdateReads: Reads[ChannelUpdate] = {
Reads { jsValue =>
for {
@ -1129,6 +1183,11 @@ object JsonReaders {
timestamp)
}
implicit val channelStatsDirectionReads: Reads[ChannelStats.Direction] =
Reads { json =>
SerializerUtil.processJsString(ChannelStats.Direction.fromString)(json)
}
implicit val channelStatsReads: Reads[ChannelStats] =
Json.reads[ChannelStats]
@ -1243,4 +1302,10 @@ object JsonReaders {
}
}
implicit val onChainBalanceReads: Reads[OnChainBalance] =
Json.reads[OnChainBalance]
implicit val walletTransactionReads: Reads[WalletTransaction] =
Json.reads[WalletTransaction]
}

View File

@ -0,0 +1,95 @@
package org.bitcoins.core.protocol.ln.node
sealed trait FeatureSupport
object FeatureSupport {
case object Mandatory extends FeatureSupport {
override def toString: String = "mandatory"
}
case object Optional extends FeatureSupport {
override def toString: String = "optional"
}
}
sealed trait Feature {
def rfcName: String
def mandatory: Int
def optional: Int = mandatory + 1
def supportBit(support: FeatureSupport): Int =
support match {
case FeatureSupport.Mandatory => mandatory
case FeatureSupport.Optional => optional
}
override def toString = rfcName
}
object Feature {
case object OptionDataLossProtect extends Feature {
val rfcName = "option_data_loss_protect"
val mandatory = 0
}
case object InitialRoutingSync extends Feature {
val rfcName = "initial_routing_sync"
// reserved but not used as per lightningnetwork/lightning-rfc/pull/178
val mandatory = 2
}
case object ChannelRangeQueries extends Feature {
val rfcName = "gossip_queries"
val mandatory = 6
}
case object VariableLengthOnion extends Feature {
val rfcName = "var_onion_optin"
val mandatory = 8
}
case object ChannelRangeQueriesExtended extends Feature {
val rfcName = "gossip_queries_ex"
val mandatory = 10
}
case object StaticRemoteKey extends Feature {
val rfcName = "option_static_remotekey"
val mandatory = 12
}
case object PaymentSecret extends Feature {
val rfcName = "payment_secret"
val mandatory = 14
}
case object BasicMultiPartPayment extends Feature {
val rfcName = "basic_mpp"
val mandatory = 16
}
case object Wumbo extends Feature {
val rfcName = "option_support_large_channel"
val mandatory = 18
}
case object TrampolinePayment extends Feature {
val rfcName = "trampoline_payment"
val mandatory = 50
}
val knownFeatures: Set[Feature] = Set(
OptionDataLossProtect,
InitialRoutingSync,
ChannelRangeQueries,
VariableLengthOnion,
ChannelRangeQueriesExtended,
PaymentSecret,
BasicMultiPartPayment,
Wumbo,
TrampolinePayment,
StaticRemoteKey
)
}

View File

@ -3,16 +3,9 @@ package org.bitcoins.eclair.rpc
import java.nio.file.Files
import java.time.Instant
import org.bitcoins.commons.jsonmodels.eclair.{
ChannelResult,
ChannelUpdate,
IncomingPaymentStatus,
InvoiceResult,
OpenChannelInfo,
OutgoingPaymentStatus,
WebSocketEvent
}
import org.bitcoins.core.currency.{CurrencyUnit, CurrencyUnits, Satoshis}
import org.bitcoins.commons.jsonmodels.eclair._
import org.bitcoins.core.config.RegTest
import org.bitcoins.core.currency.{CurrencyUnits, Satoshis}
import org.bitcoins.core.number.UInt64
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.ln.LnParams.LnBitcoinRegTest
@ -149,6 +142,23 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
executeWithClientOtherClient(f)
}
it should "perform on-chain operations" in {
for {
c <- clientF
address <- c.getNewAddress()
balance <- c.onChainBalance()
txid <- c.sendOnChain(address, Satoshis(5000), 1)
balance1 <- c.onChainBalance()
transactions <- c.onChainTransactions()
} yield {
assert(balance.confirmed > Satoshis(0))
assert(balance.unconfirmed == Satoshis(0))
// we sent 5000 sats to ourselves and paid some sats in fee
assert(balance1.confirmed < balance.confirmed)
assert(transactions.exists(_.txid == txid))
}
}
/**
* Please keep this test the very first. All other tests rely on the propagated gossip messages.
*/
@ -1189,15 +1199,6 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
}
}
it should "get new address" in {
for {
c <- clientF
res <- c.getNewAddress()
} yield {
assert(res.toString.nonEmpty)
}
}
it should "disconnect node" in {
for {
c1 <- clientF
@ -1205,6 +1206,8 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
nodeInfo2 <- c2.getInfo
_ <- c1.disconnect(nodeInfo2.nodeId)
} yield {
assert(nodeInfo2.features.activated.nonEmpty)
assert(nodeInfo2.network == RegTest)
succeed
}
}

View File

@ -21,8 +21,8 @@ TaskKeys.downloadEclair := {
Files.createDirectories(binaryDir)
}
val version = "0.4"
val commit = "69c538e"
val version = "0.4.1"
val commit = "e5fb281"
logger.debug(s"(Maybe) downloading Eclair binaries for version: $version")
@ -49,36 +49,6 @@ TaskKeys.downloadEclair := {
logger.info(s"Deleting archive")
Files.delete(archiveLocation)
fixShebang(
versionDir resolve s"eclair-node-$version-$commit" resolve "bin" resolve "eclair-node.sh")
logger.info(s"Download complete")
}
// remove me when https://github.com/ACINQ/eclair/issues/1421
// and https://github.com/ACINQ/eclair/issues/1422 are fixed
def fixShebang(scriptPath: Path): Unit = {
import java.nio.file.attribute.PosixFilePermissions
import scala.io.Source
import scala.collection.JavaConverters._
val tempPath = scriptPath.getParent resolve scriptPath.getFileName.toString + ".tmp"
Files.createFile(tempPath,
PosixFilePermissions.asFileAttribute(
PosixFilePermissions.fromString("rwxr-xr-x")))
val source = Source
.fromFile(scriptPath.toUri)
val lines = (Vector("#!/usr/bin/env bash") ++ source.getLines()).map(
line =>
if (line == "declare -r lib_dir=\"$(realpath \"${app_home::-4}/lib\")\" # {app_home::-4} transforms ../bin in ../")
"declare -r lib_dir=\"$(realpath \"${app_home:0:${#app_home}-4}/lib\")\" # {app_home:0:${#app_home}-4} transforms ../bin in ../"
else line)
source.close()
Files.write(tempPath, lines.asJava, StandardOpenOption.WRITE)
tempPath.toFile.renameTo(scriptPath.toFile)
}
}

View File

@ -3,25 +3,7 @@ package org.bitcoins.eclair.rpc.api
import java.net.InetSocketAddress
import java.time.Instant
import org.bitcoins.commons.jsonmodels.eclair.{
AuditResult,
ChannelDesc,
ChannelInfo,
ChannelResult,
ChannelStats,
ChannelUpdate,
GetInfoResult,
IncomingPayment,
InvoiceResult,
NetworkFeesResult,
NodeInfo,
OutgoingPayment,
PaymentId,
PeerInfo,
SendToRouteResult,
UsableBalancesResult,
WebSocketEvent
}
import org.bitcoins.commons.jsonmodels.eclair._
import org.bitcoins.core.currency.{CurrencyUnit, Satoshis}
import org.bitcoins.core.protocol.ln.channel.{ChannelId, FundedChannelId}
import org.bitcoins.core.protocol.ln.currency.MilliSatoshis
@ -35,7 +17,7 @@ import org.bitcoins.core.protocol.ln.{
import org.bitcoins.core.protocol.script.ScriptPubKey
import org.bitcoins.core.protocol.{Address, BitcoinAddress}
import org.bitcoins.core.wallet.fee.SatoshisPerByte
import org.bitcoins.crypto.Sha256Digest
import org.bitcoins.crypto.{DoubleSha256DigestBE, Sha256Digest}
import org.bitcoins.eclair.rpc.network.NodeUri
import scala.concurrent.duration._
@ -93,7 +75,7 @@ trait EclairApi {
def disconnect(nodeId: NodeId): Future[Unit]
def close(id: ChannelId, spk: ScriptPubKey): Future[Unit]
def close(id: ChannelId, spk: ScriptPubKey): Future[ChannelCommandResult]
def findRoute(
nodeId: NodeId,
@ -105,9 +87,9 @@ trait EclairApi {
invoice: LnInvoice,
amountMsat: MilliSatoshis): Future[Vector[NodeId]]
def forceClose(channelId: ChannelId): Future[Unit]
def forceClose(channelId: ChannelId): Future[ChannelCommandResult]
def forceClose(shortChannelId: ShortChannelId): Future[Unit]
def forceClose(shortChannelId: ShortChannelId): Future[ChannelCommandResult]
def getInfo: Future[GetInfoResult]
@ -120,13 +102,13 @@ trait EclairApi {
def updateRelayFee(
channelId: ChannelId,
feeBaseMsat: MilliSatoshis,
feePropertionalMillionths: Long): Future[Unit]
feePropertionalMillionths: Long): Future[ChannelCommandResult]
def updateRelayFee(
shortChannelId: ShortChannelId,
feeBaseMsat: MilliSatoshis,
feePropertionalMillionths: Long
): Future[Unit]
): Future[ChannelCommandResult]
def open(
nodeId: NodeId,
@ -296,4 +278,13 @@ trait EclairApi {
def connectToWebSocket(eventHandler: WebSocketEvent => Unit): Future[Unit]
def getNewAddress(): Future[BitcoinAddress]
def onChainBalance(): Future[OnChainBalance]
def onChainTransactions(): Future[Vector[WalletTransaction]]
def sendOnChain(
address: BitcoinAddress,
amount: Satoshis,
confirmationTarget: Int): Future[DoubleSha256DigestBE]
}

View File

@ -31,7 +31,7 @@ import org.bitcoins.core.protocol.script.ScriptPubKey
import org.bitcoins.core.protocol.{Address, BitcoinAddress}
import org.bitcoins.core.util.{BytesUtil, FutureUtil, StartStop}
import org.bitcoins.core.wallet.fee.SatoshisPerByte
import org.bitcoins.crypto.Sha256Digest
import org.bitcoins.crypto.{DoubleSha256DigestBE, Sha256Digest}
import org.bitcoins.eclair.rpc.api._
import org.bitcoins.eclair.rpc.config.EclairInstance
import org.bitcoins.eclair.rpc.network.NodeUri
@ -65,7 +65,7 @@ class EclairRpcClient(
}
override def allNodes(): Future[Vector[NodeInfo]] = {
eclairCall[Vector[NodeInfo]]("allnodes")
eclairCall[Vector[NodeInfo]]("nodes")
}
override def allUpdates(): Future[Vector[ChannelUpdate]] =
@ -108,22 +108,22 @@ class EclairRpcClient(
private def close(
channelId: ChannelId,
shortChannelId: Option[ShortChannelId],
scriptPubKey: Option[ScriptPubKey]): Future[Unit] = {
scriptPubKey: Option[ScriptPubKey]): Future[ChannelCommandResult] = {
val params =
Seq("channelId" -> channelId.hex) ++ Seq(
shortChannelId.map(x => "shortChannelId" -> x.toString),
scriptPubKey.map(x => "scriptPubKey" -> BytesUtil.encodeHex(x.asmBytes))
).flatten
eclairCall[String]("close", params: _*).map(_ => ())
eclairCall[ChannelCommandResult]("close", params: _*)
}
def close(channelId: ChannelId): Future[Unit] =
def close(channelId: ChannelId): Future[ChannelCommandResult] =
close(channelId, scriptPubKey = None, shortChannelId = None)
override def close(
channelId: ChannelId,
scriptPubKey: ScriptPubKey): Future[Unit] = {
scriptPubKey: ScriptPubKey): Future[ChannelCommandResult] = {
close(channelId, scriptPubKey = Some(scriptPubKey), shortChannelId = None)
}
@ -173,13 +173,16 @@ class EclairRpcClient(
eclairCall[Vector[NodeId]]("findroute", params: _*)
}
override def forceClose(channelId: ChannelId): Future[Unit] = {
eclairCall[String]("forceclose", "channelId" -> channelId.hex).map(_ => ())
override def forceClose(
channelId: ChannelId): Future[ChannelCommandResult] = {
eclairCall[ChannelCommandResult]("forceclose", "channelId" -> channelId.hex)
}
override def forceClose(shortChannelId: ShortChannelId): Future[Unit] = {
eclairCall[String]("forceclose",
"shortChannelId" -> shortChannelId.toString).map(_ => ())
override def forceClose(
shortChannelId: ShortChannelId): Future[ChannelCommandResult] = {
eclairCall[ChannelCommandResult](
"forceclose",
"shortChannelId" -> shortChannelId.toString)
}
override def getInfo: Future[GetInfoResult] = {
@ -530,8 +533,8 @@ class EclairRpcClient(
override def updateRelayFee(
channelId: ChannelId,
feeBaseMsat: MilliSatoshis,
feeProportionalMillionths: Long): Future[Unit] = {
eclairCall[Unit](
feeProportionalMillionths: Long): Future[ChannelCommandResult] = {
eclairCall[ChannelCommandResult](
"updaterelayfee",
"channelId" -> channelId.hex,
"feeBaseMsat" -> feeBaseMsat.toLong.toString,
@ -542,8 +545,8 @@ class EclairRpcClient(
override def updateRelayFee(
shortChannelId: ShortChannelId,
feeBaseMsat: MilliSatoshis,
feeProportionalMillionths: Long): Future[Unit] = {
eclairCall[Unit](
feeProportionalMillionths: Long): Future[ChannelCommandResult] = {
eclairCall[ChannelCommandResult](
"updaterelayfee",
"shortChannelId" -> shortChannelId.toHumanReadableString,
"feeBaseMsat" -> feeBaseMsat.toLong.toString,
@ -609,6 +612,25 @@ class EclairRpcClient(
eclairCall[BitcoinAddress]("getnewaddress")
}
override def onChainBalance(): Future[OnChainBalance] = {
eclairCall[OnChainBalance]("onchainbalance")
}
override def onChainTransactions(): Future[Vector[WalletTransaction]] = {
eclairCall[Vector[WalletTransaction]]("onchaintransactions")
}
override def sendOnChain(
address: BitcoinAddress,
amount: Satoshis,
confirmationTarget: Int): Future[DoubleSha256DigestBE] = {
eclairCall[DoubleSha256DigestBE](
"sendonchain",
"address" -> address.toString,
"amountSatoshis" -> amount.toLong.toString,
"confirmationTarget" -> confirmationTarget.toString)
}
private def eclairCall[T](command: String, parameters: (String, String)*)(
implicit reader: Reads[T]): Future[T] = {
val request = buildRequest(getDaemon, command, parameters: _*)
@ -946,8 +968,8 @@ object EclairRpcClient {
implicit system: ActorSystem) = new EclairRpcClient(instance, binary)
/** The current commit we support of Eclair */
private[bitcoins] val commit = "69c538e"
private[bitcoins] val commit = "e5fb281"
/** The current version we support of Eclair */
private[bitcoins] val version = "0.4"
private[bitcoins] val version = "0.4.1"
}