diff --git a/app-commons/src/main/scala/org/bitcoins/commons/jsonmodels/eclair/EclairModels.scala b/app-commons/src/main/scala/org/bitcoins/commons/jsonmodels/eclair/EclairModels.scala index 2fdae08ca3..9cad0779c9 100644 --- a/app-commons/src/main/scala/org/bitcoins/commons/jsonmodels/eclair/EclairModels.scala +++ b/app-commons/src/main/scala/org/bitcoins/commons/jsonmodels/eclair/EclairModels.scala @@ -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) diff --git a/app-commons/src/main/scala/org/bitcoins/commons/serializers/JsonReaders.scala b/app-commons/src/main/scala/org/bitcoins/commons/serializers/JsonReaders.scala index 6c026e6ef8..be28ac1a8b 100644 --- a/app-commons/src/main/scala/org/bitcoins/commons/serializers/JsonReaders.scala +++ b/app-commons/src/main/scala/org/bitcoins/commons/serializers/JsonReaders.scala @@ -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] + } diff --git a/core/src/main/scala/org/bitcoins/core/protocol/ln/node/Feature.scala b/core/src/main/scala/org/bitcoins/core/protocol/ln/node/Feature.scala new file mode 100644 index 0000000000..5b588d6857 --- /dev/null +++ b/core/src/main/scala/org/bitcoins/core/protocol/ln/node/Feature.scala @@ -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 + ) +} diff --git a/eclair-rpc-test/src/test/scala/org/bitcoins/eclair/rpc/EclairRpcClientTest.scala b/eclair-rpc-test/src/test/scala/org/bitcoins/eclair/rpc/EclairRpcClientTest.scala index e11ff51613..62b76c0249 100644 --- a/eclair-rpc-test/src/test/scala/org/bitcoins/eclair/rpc/EclairRpcClientTest.scala +++ b/eclair-rpc-test/src/test/scala/org/bitcoins/eclair/rpc/EclairRpcClientTest.scala @@ -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 } } diff --git a/eclair-rpc/eclair-rpc.sbt b/eclair-rpc/eclair-rpc.sbt index 6129810f18..b63d2496f2 100644 --- a/eclair-rpc/eclair-rpc.sbt +++ b/eclair-rpc/eclair-rpc.sbt @@ -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) - } } diff --git a/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/api/EclairApi.scala b/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/api/EclairApi.scala index 755b284685..0c77d5d458 100644 --- a/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/api/EclairApi.scala +++ b/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/api/EclairApi.scala @@ -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] } diff --git a/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/client/EclairRpcClient.scala b/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/client/EclairRpcClient.scala index 54d030d9c6..5e78807cb1 100644 --- a/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/client/EclairRpcClient.scala +++ b/eclair-rpc/src/main/scala/org/bitcoins/eclair/rpc/client/EclairRpcClient.scala @@ -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" }