1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-02-23 14:40:34 +01:00

Fixed eclair-cli (#354)

* reworked eclair-cli

* API is disabled by default, disabled CORS and require basic auth password

* better error handling

* Fixed latest version in README

* Increased connection timeout to 15s in electrum client test

* Rgb in NodeAnnouncement is now a Color object

Makes the color field more practical to handle and enable finer
serialization with a more readable code.

allnodes command in api now exports a list with node announcements.

* Added api call to list all the channels updates

This call can also filter the channels for a given nodeId

This fixes #344.
This commit is contained in:
Pierre-Marie Padiou 2018-01-11 19:23:17 +01:00 committed by GitHub
parent a00fd96ca6
commit a1d69af597
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 380 additions and 265 deletions

View file

@ -27,7 +27,7 @@ Please see the latest [release note](https://github.com/ACINQ/eclair/releases) f
## Installation
:warning: **Those are valid for the most up-to-date, unreleased, version of eclair. Here are the [instructions for Eclair 0.2-alpha5](https://github.com/ACINQ/eclair/blob/v0.2-alpha5/README.md#installation)**.
:warning: **Those are valid for the most up-to-date, unreleased, version of eclair. Here are the [instructions for Eclair 0.2-alpha8](https://github.com/ACINQ/eclair/blob/v0.2-alpha8/README.md#installation)**.
### Configuring Bitcoin Core
@ -93,7 +93,10 @@ Here are some of the most common options:
name | description | default value
-----------------------------|---------------------------|--------------
eclair.server.port | Lightning TCP port | 9735
eclair.api.enabled | Enable/disable the API | false. By default the API is disabled. If you want to enable it, you must set a user/password.
eclair.api.port | API HTTP port | 8080
eclair.api.user | API user (BASIC) | "" (must be set if the API is enabled)
eclair.api.password | API password (BASIC) | "" (must be set if the API is enabled)
eclair.bitcoind.rpcuser | Bitcoin Core RPC user | foo
eclair.bitcoind.rpcpassword | Bitcoin Core RPC password | bar
eclair.bitcoind.zmq | Bitcoin Core ZMQ address | tcp://127.0.0.1:29000
@ -121,27 +124,30 @@ java -Declair.datadir=/tmp/node1 -jar eclair-node-gui-<version>-<commit_id>.jar
## JSON-RPC API
method | params | description
-------------|-----------------------------------------------|-----------------------------------------------------------
getinfo | | return basic node information (id, chain hash, current block height)
connect | uri | open a secure connection to a lightning node
open | nodeId, fundingSatoshis, pushMsat | open a channel with another lightning node
peers | | list existing local peers
channels | | list existing local channels
channels | nodeId | list existing local channels opened with a particular nodeId
channel | channelId | retrieve detailed information about a given channel
allnodes | | list all known nodes
allchannels | | list all known channels
receive | description | generate a payment request without a required amount (can be useful for donations)
receive | amountMsat, description | generate a payment request for a given amount
send | amountMsat, paymentHash, nodeId | send a payment to a lightning node
send | paymentRequest | send a payment to a lightning node using a BOLT11 payment request
send | paymentRequest, amountMsat | send a payment to a lightning node using a BOLT11 payment request and a custom amount
checkpayment| paymentHash | returns true if the payment has been received, false otherwise
checkpayment| paymentRequest | returns true if the payment has been received, false otherwise
close | channelId | close a channel
close | channelId, scriptPubKey (optional) | close a channel and send the funds to the given scriptPubKey
help | | display available methods
method | params | description
------------- |------------------------------------------------------------|-----------------------------------------------------------
getinfo | | return basic node information (id, chain hash, current block height)
connect | nodeId, host, port | open a secure connection to a lightning node
connect | uri | open a secure connection to a lightning node
open | nodeId, fundingSatoshis, pushMsat = 0, channelFlags = 0x01 | open a channel with another lightning node, by default push = 0 and channel is announced
peers | | list existing local peers
channels | | list existing local channels
channels | nodeId | list existing local channels opened with a particular nodeId
channel | channelId | retrieve detailed information about a given channel
allnodes | | list all known nodes
allchannels | | list all known channels
allupdates | | list all channels updates
allupdates | nodeId | list all channels updates for this nodeId
receive | description | generate a payment request without a required amount (can be useful for donations)
receive | amountMsat, description | generate a payment request for a given amount
send | amountMsat, paymentHash, nodeId | send a payment to a lightning node
send | paymentRequest | send a payment to a lightning node using a BOLT11 payment request
send | paymentRequest, amountMsat | send a payment to a lightning node using a BOLT11 payment request and a custom amount
checkpayment | paymentHash | returns true if the payment has been received, false otherwise
checkpayment | paymentRequest | returns true if the payment has been received, false otherwise
close | channelId | close a channel
close | channelId, scriptPubKey | close a channel and send the funds to the given scriptPubKey
help | | display available methods
## Docker
@ -161,8 +167,8 @@ docker run -ti --rm -v "/path_on_host:/data" -e "JAVA_OPTS=-Declair.printToConso
## Resources
- [1] [The Bitcoin Lightning Network: Scalable Off-Chain Instant Payments](https://lightning.network/lightning-network-paper.pdf) by Joseph Poon and Thaddeus Dryja
- [2] [Reaching The Ground With Lightning](https://github.com/ElementsProject/lightning/raw/master/doc/deployable-lightning.pdf) by Rusty Russell
- [1] [The Bitcoin Lightning Network: Scalable Off-Chain Instant Payments](https://lightning.network/lightning-network-paper.pdf) by Joseph Poon and Thaddeus Dryja
- [2] [Reaching The Ground With Lightning](https://github.com/ElementsProject/lightning/raw/master/doc/deployable-lightning.pdf) by Rusty Russell
- [3] [Lightning Network Explorer](https://explorer.acinq.co) - Explore testnet LN nodes you can connect to
[Amiko-Pay]: https://github.com/cornwarecjp/amiko-pay

View file

@ -1,59 +1,95 @@
#!/bin/bash
[ -z "$1" ] && (
echo "usage: "
echo " eclair-cli help"
) && exit 1
# Check if jq is installed. If not, display instructions and abort program
command -v jq >/dev/null 2>&1 || { echo -e "This tool requires jq.\nFor installation instructions, visit https://stedolan.github.io/jq/download/.\n\nAborting..."; exit 1; }
URL="http://localhost:8080"
CURL_OPTS="-sS -X POST -H \"Content-Type: application/json\""
FULL_OUTPUT='false'
URL='http://localhost:8080'
PASSWORD=''
# -------------------- METHODS
displayhelp() {
echo -e "Usage: eclair-cli [OPTION]... [COMMAND]
Client for an eclair node.
With COMMAND is one of the command listed by \e[01;33meclair-cli help\e[0m.
-p <password> api's password
-a <address> Override the api URL with <address>
-v Outputs full json returned by the API
Examples:
eclair-cli -a localhost:1234 peers list the peers
eclair-cli close 006fb... closes the channel with id 006fb...
Note: Uses the json-rpc api exposed by the node on localhost:8080. Make sure the api is enabled.
Full documentation at: <https://github.com/ACINQ/eclair>"
}
# Executes a JSON RPC call to a node listening on ${URL}
call() {
jqexp='if .error == null then .result else .error.message end'
# override default jq parsing expression
if [ $# -ge 3 ] && [ ${FULL_OUTPUT} == "false" ]; then jqexp=${3}; fi
# set password
if [ -z ${PASSWORD} ]; then auth="eclair-cli";
else auth="eclair-cli:"${PASSWORD}; fi
eval curl "--user ${auth} --silent --show-error -X POST -H \"Content-Type: application/json\" -d '{ \"method\": \"'${1}'\", \"params\": '${2}' }' ${URL}" | jq -r "$jqexp"
}
# get script options
while getopts 'vu:p:a:' flag; do
case "${flag}" in
p) PASSWORD="${OPTARG}" ;;
a) URL="${OPTARG}" ;;
v) FULL_OUTPUT="true" ;;
*) echo -e "\nAborting..."; exit 1; ;;
esac
done
shift $(($OPTIND - 1))
# assigning JSON RPC method and params values from arguments
METHOD=${1}
shift 1
# Create a JSON Array containing the remaining program args as QUOTED STRINGS, separated with a `,` character
PARAMS=""
i=1
for arg in "${@}"; do
if [ $i -eq 1 ]; then PARAMS=$(printf '"%s"' "$arg");
else PARAMS=$(printf '%s,"%s"' "$PARAMS" "$arg");
fi
let "i++"
done;
PARAMS="[${PARAMS}]"
# Whatever the arguments provided to eclair-cli, a call to the API will be sent. Let it fail!
case ${METHOD}_${#} in
""_*) displayhelp ;;
"help"*) displayhelp
echo -e "\nAvailable commands:\n"
call "help" [] ;;
"connect_3") call ${METHOD} "'$(printf '["%s","%s",%s]' "${1}" "${2}" "${3}")'" ;; # ${3} is numeric
"open_4") call ${METHOD} "'$(printf '["%s",%s,%s,%s]' "${1}" "${2}" "${3}" "${4}")'" ;; # ${2} ${3} ${4} are numeric (funding, push, flags)
"open_3") call ${METHOD} "'$(printf '["%s",%s,%s]' "${1}" "${2}" "${3}")'" ;; # ${2} ${3} are numeric (funding, push)
"open_2") call ${METHOD} "'$(printf '["%s",%s]' "${1}" "${2}")'" ;; # ${2} is numeric (funding)
"receive_2") call ${METHOD} "'$(printf '[%s,"%s"]' "${1}" "${2}")'" ;; # ${1} is numeric (amount to receive)
"channel_"*) call ${METHOD} "'${PARAMS}'" "if .error != null then .error.message else .result | { nodeId, shortChannelId: .data.shortChannelId, channelId, state, balanceMsat: .data.commitments.localCommit.spec.toLocalMsat, capacitySat: .data.commitments.commitInput.txOut.amount.amount } end" ;;
"send_3") call ${METHOD} "'$(printf '[%s,"%s","%s"]' "${1}" "${2}" "${3}")'" ;; # ${1} is numeric (amount of the payment)
"send_2") call ${METHOD} "'$(printf '["%s",%s]' "${1}" "${2}")'" ;; # ${2} is numeric (amount overriding the payment request)
*) # Default case.
# Sends the method and, for parameters, use the JSON table containing the remaining args.
#
# NOTE: Arguments will be sent as QUOTED STRING so if this particular API call requires an INT param,
# this call will fail. In that case, a specific rule for that method MUST be set and the ${PARAMS} JSON array can not be used.
call ${METHOD} "'${PARAMS}'" ;;
case $1 in
"help")
eval curl "$CURL_OPTS -d '{ \"method\": \"help\", \"params\" : [] }' $URL" | jq -r ".result[]"
;;
"getinfo")
eval curl "$CURL_OPTS -d '{ \"method\": \"getinfo\", \"params\" : [] }' $URL" | jq ".result"
;;
"channels")
eval curl "$CURL_OPTS -d '{ \"method\": \"channels\", \"params\" : [] }' $URL" | jq ".result[]"
;;
"channels")
if [ $# -ge 2 ]
then
eval curl "$CURL_OPTS -d '{ \"method\": \"channels\", \"params\" : [\"${2?"missing node id"}\"] }' $URL" | jq ".result[]"
else
eval curl "$CURL_OPTS -d '{ \"method\": \"channels\", \"params\" : [] }' $URL" | jq ".result[]"
fi
;;
"channel")
eval curl "$CURL_OPTS -d '{ \"method\": \"channel\", \"params\" : [\"${2?"missing channel id"}\"] }' $URL" | jq ".result | { nodeid, channelId, state, balanceMsat: .data.commitments.localCommit.spec.toLocalMsat, capacitySat: .data.commitments.commitInput.txOut.amount.amount }"
;;
"connect")
eval curl "$CURL_OPTS -d '{ \"method\": \"open\", \"params\" : [\"${2?"missing uri"}\"] }' $URL" | jq -r "if .error == null then .result else .error.message end"
;;
"open")
eval curl "$CURL_OPTS -d '{ \"method\": \"open\", \"params\" : [\"${2?"missing node id"}\", ${3?"missing amount (sat)"}, ${4?"missing push amount (msat)"}] }' $URL" | jq -r "if .error == null then .result else .error.message end"
;;
"close")
eval curl "$CURL_OPTS -d '{ \"method\": \"close\", \"params\" : [\"${2?"missing channel id"}\"] }' $URL"
;;
"receive")
eval curl "$CURL_OPTS -d '{ \"method\": \"receive\", \"params\" : [${2?"missing amount"}, \"${3?"missing description"}\"] }' $URL" | jq -r "if .error == null then .result else .error.message end"
;;
"send")
eval curl "$CURL_OPTS -d '{ \"method\": \"send\", \"params\" : [\"${2?"missing request"}\"] }' $URL" | jq -r "if .error == null then .result else .error.message end"
;;
"allnodes")
eval curl "$CURL_OPTS -d '{ \"method\": \"allnodes\", \"params\" : [] }' $URL" | jq ".result"
;;
"allchannels")
eval curl "$CURL_OPTS -d '{ \"method\": \"allchannels\", \"params\" : [] }' $URL" | jq ".result"
;;
"peers")
eval curl "$CURL_OPTS -d '{ \"method\": \"peers\", \"params\" : [] }' $URL" | jq ".result"
;;
"checkpayment")
eval curl "$CURL_OPTS -d '{ \"method\": \"checkpayment\", \"params\" : [\"${2?"missing payment request or payment hash"}\"] }' $URL" | jq ".result"
;;
esac

View file

@ -9,8 +9,10 @@ eclair {
}
api {
enabled = false // disabled by default for security reasons
binding-ip = "127.0.0.1"
port = 8080
password = "" // password for basic auth, must be non empty if json-rpc api is enabled
}
watcher-type = "bitcoind" // other *experimental* values include "bitcoinj" or "electrum"

View file

@ -14,6 +14,7 @@ import fr.acinq.eclair.NodeParams.WatcherType
import fr.acinq.eclair.channel.Channel
import fr.acinq.eclair.db._
import fr.acinq.eclair.db.sqlite._
import fr.acinq.eclair.wire.Color
import scala.collection.JavaConversions._
import scala.concurrent.duration.FiniteDuration
@ -24,7 +25,7 @@ import scala.concurrent.duration.FiniteDuration
case class NodeParams(extendedPrivateKey: ExtendedPrivateKey,
privateKey: PrivateKey,
alias: String,
color: (Byte, Byte, Byte),
color: Color,
publicAddresses: List[InetSocketAddress],
globalFeatures: BinaryData,
localFeatures: BinaryData,
@ -131,7 +132,7 @@ object NodeParams {
extendedPrivateKey = extendedPrivateKey,
privateKey = extendedPrivateKey.privateKey,
alias = config.getString("node-alias").take(32),
color = (color.data(0), color.data(1), color.data(2)),
color = Color(color.data(0), color.data(1), color.data(2)),
publicAddresses = config.getStringList("server.public-ips").toList.map(ip => new InetSocketAddress(ip, config.getInt("server.port"))),
globalFeatures = BinaryData(config.getString("global-features")),
localFeatures = BinaryData(config.getString("local-features")),

View file

@ -37,9 +37,9 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act
logger.info(s"hello!")
logger.info(s"version=${getClass.getPackage.getImplementationVersion} commit=${getClass.getPackage.getSpecificationVersion}")
val config = NodeParams.loadConfiguration(datadir, overrideDefaults)
val nodeParams = NodeParams.makeNodeParams(datadir, config)
val chain = config.getString("chain")
val config: Config = NodeParams.loadConfiguration(datadir, overrideDefaults)
val nodeParams: NodeParams = NodeParams.makeNodeParams(datadir, config)
val chain: String = config.getString("chain")
// early checks
DBCompatChecker.checkDBCompatibility(nodeParams)
@ -172,31 +172,43 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act
server = server,
wallet = wallet)
val api = new Service {
override def getInfoResponse: Future[GetInfoResponse] = Future.successful(GetInfoResponse(nodeId = nodeParams.privateKey.publicKey, alias = nodeParams.alias, port = config.getInt("server.port"), chainHash = nodeParams.chainHash, blockHeight = Globals.blockCount.intValue()))
override def appKit = kit
}
val httpBound = Http().bindAndHandle(api.route, config.getString("api.binding-ip"), config.getInt("api.port")).recover {
case _: BindFailedException => throw TCPBindException(config.getInt("api.port"))
}
val zmqTimeout = after(5 seconds, using = system.scheduler)(Future.failed(BitcoinZMQConnectionTimeoutException))
val tcpTimeout = after(5 seconds, using = system.scheduler)(Future.failed(TCPBindException(config.getInt("server.port"))))
val httpTimeout = after(5 seconds, using = system.scheduler)(Future.failed(TCPBindException(config.getInt("api.port"))))
for {
_ <- Future.firstCompletedOf(zmqConnected.future :: zmqTimeout :: Nil)
_ <- Future.firstCompletedOf(tcpBound.future :: tcpTimeout :: Nil)
_ <- Future.firstCompletedOf(httpBound :: httpTimeout :: Nil)
_ <- if (config.getBoolean("api.enabled")) {
logger.info(s"json-rpc api enabled on port=${config.getInt("api.port")}")
val api = new Service {
override val password = {
val p = config.getString("api.password")
if (p.isEmpty) throw EmptyAPIPasswordException else p
}
override def getInfoResponse: Future[GetInfoResponse] = Future.successful(
GetInfoResponse(nodeId = nodeParams.privateKey.publicKey,
alias = nodeParams.alias,
port = config.getInt("server.port"),
chainHash = nodeParams.chainHash,
blockHeight = Globals.blockCount.intValue()))
override def appKit: Kit = kit
}
val httpBound = Http().bindAndHandle(api.route, config.getString("api.binding-ip"), config.getInt("api.port")).recover {
case _: BindFailedException => throw TCPBindException(config.getInt("api.port"))
}
val httpTimeout = after(5 seconds, using = system.scheduler)(Future.failed(TCPBindException(config.getInt("api.port"))))
Future.firstCompletedOf(httpBound :: httpTimeout :: Nil)
} else {
Future.successful(logger.info("json-rpc api is disabled"))
}
} yield kit
}
}
// @formatter:off
sealed trait Bitcoin
case class Bitcoind(extendedBitcoinClient: ExtendedBitcoinClient) extends Bitcoin
@ -219,3 +231,5 @@ case class Kit(nodeParams: NodeParams,
case object BitcoinZMQConnectionTimeoutException extends RuntimeException("could not connect to bitcoind using zeromq")
case object BitcoinRPCConnectionException extends RuntimeException("could not connect to bitcoind using json-rpc")
case object EmptyAPIPasswordException extends RuntimeException("must set a user/password for the json-rpc api")

View file

@ -3,17 +3,18 @@ package fr.acinq.eclair.api
import java.net.InetSocketAddress
import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, PublicKey, Scalar}
import fr.acinq.bitcoin.{BinaryData, OutPoint, Transaction}
import fr.acinq.bitcoin.{BinaryData, OutPoint}
import fr.acinq.eclair.channel.State
import fr.acinq.eclair.crypto.ShaChain
import fr.acinq.eclair.transactions.Transactions.TransactionWithInputInfo
import org.json4s.{CustomKeySerializer, CustomSerializer}
import fr.acinq.eclair.wire.Color
import org.json4s.JsonAST.{JNull, JString}
import org.json4s.{CustomKeySerializer, CustomSerializer}
/**
* Created by PM on 28/01/2016.
*/
class BinaryDataSerializer extends CustomSerializer[BinaryData](format => ( {
class BinaryDataSerializer extends CustomSerializer[BinaryData](format => ({
case JString(hex) if (false) => // NOT IMPLEMENTED
???
}, {
@ -21,7 +22,7 @@ class BinaryDataSerializer extends CustomSerializer[BinaryData](format => ( {
}
))
class StateSerializer extends CustomSerializer[State](format => ( {
class StateSerializer extends CustomSerializer[State](format => ({
case JString(x) if (false) => // NOT IMPLEMENTED
???
}, {
@ -29,7 +30,7 @@ class StateSerializer extends CustomSerializer[State](format => ( {
}
))
class ShaChainSerializer extends CustomSerializer[ShaChain](format => ( {
class ShaChainSerializer extends CustomSerializer[ShaChain](format => ({
case JString(x) if (false) => // NOT IMPLEMENTED
???
}, {
@ -37,7 +38,7 @@ class ShaChainSerializer extends CustomSerializer[ShaChain](format => ( {
}
))
class PublicKeySerializer extends CustomSerializer[PublicKey](format => ( {
class PublicKeySerializer extends CustomSerializer[PublicKey](format => ({
case JString(x) if (false) => // NOT IMPLEMENTED
???
}, {
@ -45,7 +46,7 @@ class PublicKeySerializer extends CustomSerializer[PublicKey](format => ( {
}
))
class PrivateKeySerializer extends CustomSerializer[PrivateKey](format => ( {
class PrivateKeySerializer extends CustomSerializer[PrivateKey](format => ({
case JString(x) if (false) => // NOT IMPLEMENTED
???
}, {
@ -53,7 +54,7 @@ class PrivateKeySerializer extends CustomSerializer[PrivateKey](format => ( {
}
))
class PointSerializer extends CustomSerializer[Point](format => ( {
class PointSerializer extends CustomSerializer[Point](format => ({
case JString(x) if (false) => // NOT IMPLEMENTED
???
}, {
@ -61,7 +62,7 @@ class PointSerializer extends CustomSerializer[Point](format => ( {
}
))
class ScalarSerializer extends CustomSerializer[Scalar](format => ( {
class ScalarSerializer extends CustomSerializer[Scalar](format => ({
case JString(x) if (false) => // NOT IMPLEMENTED
???
}, {
@ -69,7 +70,7 @@ class ScalarSerializer extends CustomSerializer[Scalar](format => ( {
}
))
class TransactionWithInputInfoSerializer extends CustomSerializer[TransactionWithInputInfo](format => ( {
class TransactionWithInputInfoSerializer extends CustomSerializer[TransactionWithInputInfo](format => ({
case JString(x) if (false) => // NOT IMPLEMENTED
???
}, {
@ -77,7 +78,7 @@ class TransactionWithInputInfoSerializer extends CustomSerializer[TransactionWit
}
))
class InetSocketAddressSerializer extends CustomSerializer[InetSocketAddress](format => ( {
class InetSocketAddressSerializer extends CustomSerializer[InetSocketAddress](format => ({
case JString(x) if (false) => // NOT IMPLEMENTED
???
}, {
@ -85,7 +86,7 @@ class InetSocketAddressSerializer extends CustomSerializer[InetSocketAddress](fo
}
))
class OutPointKeySerializer extends CustomKeySerializer[OutPoint](format => ( {
class OutPointKeySerializer extends CustomKeySerializer[OutPoint](format => ({
case x: String =>
val Array(k, v) = x.split(":")
OutPoint(BinaryData(k), v.toLong)
@ -93,3 +94,10 @@ class OutPointKeySerializer extends CustomKeySerializer[OutPoint](format => ( {
case x: OutPoint => s"${x.hash}:${x.index}"
}
))
class ColorSerializer extends CustomSerializer[Color](format => ({
case JString(x) if (false) => // NOT IMPLEMENTED
???
}, {
case c: Color => JString(c.toString)
}))

View file

@ -1,15 +1,13 @@
package fr.acinq.eclair.api
import akka.actor.ActorRef
import akka.http.scaladsl.model.HttpMethods._
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.model.StatusCodes.{register => _}
import akka.http.scaladsl.model.headers.CacheDirectives.{`max-age`, `no-store`, public}
import akka.http.scaladsl.model.headers.HttpOriginRange.*
import akka.http.scaladsl.model.headers._
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._
import akka.http.scaladsl.server.directives.Credentials
import akka.http.scaladsl.server.directives.RouteDirectives.reject
import akka.http.scaladsl.server.{ExceptionHandler, Rejection, RejectionHandler, Route}
import akka.pattern.ask
import akka.util.Timeout
import de.heikoseeberger.akkahttpjson4s.Json4sSupport
@ -20,9 +18,9 @@ import fr.acinq.eclair.Kit
import fr.acinq.eclair.channel._
import fr.acinq.eclair.io.Peer.{GetPeerInfo, PeerInfo}
import fr.acinq.eclair.io.{NodeURI, Peer}
import fr.acinq.eclair.payment.{PaymentRequest, PaymentResult, ReceivePayment, SendPayment}
import fr.acinq.eclair.payment._
import fr.acinq.eclair.wire.{ChannelAnnouncement, NodeAnnouncement}
import fr.acinq.eclair.payment.{PaymentRequest, PaymentResult, ReceivePayment, SendPayment, _}
import fr.acinq.eclair.router.ChannelDesc
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement}
import grizzled.slf4j.Logging
import org.json4s.JsonAST.{JBool, JInt, JString}
import org.json4s.{JValue, jackson}
@ -37,14 +35,15 @@ case class Error(code: Int, message: String)
case class JsonRPCRes(result: AnyRef, error: Option[Error], id: String)
case class Status(node_id: String)
case class GetInfoResponse(nodeId: PublicKey, alias: String, port: Int, chainHash: BinaryData, blockHeight: Int)
case class LocalChannelInfo(nodeId: BinaryData, channelId: BinaryData, state: String)
case class ChannelInfo(shortChannelId: String, nodeId1: PublicKey, nodeId2: PublicKey)
trait RPCRejection extends Rejection {
def requestId: String
}
final case class UnknownMethodRejection(requestId: String) extends RPCRejection
final case class UnknownParamsRejection(requestId: String, message: String) extends RPCRejection
final case class NotFoundRejection(requestId: String) extends RPCRejection
final case class ValidationRejection(requestId: String, message: String) extends RPCRejection
final case class RpcValidationRejection(requestId: String, message: String) extends RPCRejection
final case class ExceptionRejection(requestId: String, message: String) extends RPCRejection
// @formatter:on
trait Service extends Logging {
@ -52,31 +51,38 @@ trait Service extends Logging {
implicit def ec: ExecutionContext = ExecutionContext.Implicits.global
implicit val serialization = jackson.Serialization
implicit val formats = org.json4s.DefaultFormats + new BinaryDataSerializer + new StateSerializer + new ShaChainSerializer + new PublicKeySerializer + new PrivateKeySerializer + new ScalarSerializer + new PointSerializer + new TransactionWithInputInfoSerializer + new InetSocketAddressSerializer + new OutPointKeySerializer
implicit val formats = org.json4s.DefaultFormats + new BinaryDataSerializer + new StateSerializer + new ShaChainSerializer + new PublicKeySerializer + new PrivateKeySerializer + new ScalarSerializer + new PointSerializer + new TransactionWithInputInfoSerializer + new InetSocketAddressSerializer + new OutPointKeySerializer + new ColorSerializer
implicit val timeout = Timeout(30 seconds)
implicit val shouldWritePretty: ShouldWritePretty = ShouldWritePretty.True
import Json4sSupport.{marshaller, unmarshaller}
def password: String
def appKit: Kit
val customHeaders = `Access-Control-Allow-Origin`(*) ::
`Access-Control-Allow-Headers`("Content-Type, Authorization") ::
`Access-Control-Allow-Methods`(PUT, GET, POST, DELETE, OPTIONS) ::
`Cache-Control`(public, `no-store`, `max-age`(0)) ::
`Access-Control-Allow-Headers`("x-requested-with") :: Nil
def userPassAuthenticator(credentials: Credentials): Option[String] = credentials match {
case p@Credentials.Provided(id) if p.verify(password) => Some(id)
case _ =>
// TODO deter brute force with a forced delay
None
}
val customHeaders = `Access-Control-Allow-Headers`("Content-Type, Authorization") ::
`Access-Control-Allow-Methods`(POST) ::
`Cache-Control`(public, `no-store`, `max-age`(0)) :: Nil
val myExceptionHandler = ExceptionHandler {
case t: Throwable =>
extractRequest { request =>
extractRequest { _ =>
logger.info(s"API call failed with cause=${t.getMessage}")
complete(StatusCodes.InternalServerError, JsonRPCRes(null, Some(Error(StatusCodes.InternalServerError.intValue, t.getMessage)), request.asInstanceOf[JsonRPCBody].id))
complete(StatusCodes.InternalServerError, JsonRPCRes(null, Some(Error(StatusCodes.InternalServerError.intValue, t.getMessage)), "-1"))
}
}
def completeRpcFuture(requestId: String, future: Future[AnyRef]): Route = onComplete(future) {
case Success(s) => completeRpc(requestId, s)
case Failure(_) => reject
case Failure(t) => reject(ExceptionRejection(requestId, t.getLocalizedMessage))
}
def completeRpc(requestId: String, result: AnyRef): Route = complete(JsonRPCRes(result, None, requestId))
@ -85,11 +91,15 @@ trait Service extends Logging {
complete(StatusCodes.NotFound, JsonRPCRes(null, Some(Error(StatusCodes.NotFound.intValue, "not found")), "-1"))
}
.handle {
case v: ValidationRejection complete(StatusCodes.BadRequest, JsonRPCRes(null, Some(Error(StatusCodes.BadRequest.intValue, v.message)), v.requestId))
case nf: NotFoundRejection complete(StatusCodes.NotFound, JsonRPCRes(null, Some(Error(StatusCodes.NotFound.intValue, "not found")), nf.requestId))
case _: AuthenticationFailedRejection complete(StatusCodes.Unauthorized, JsonRPCRes(null, Some(Error(StatusCodes.Unauthorized.intValue, "Access restricted")), "-1"))
case v: RpcValidationRejection complete(StatusCodes.BadRequest, JsonRPCRes(null, Some(Error(StatusCodes.BadRequest.intValue, v.message)), v.requestId))
case ukm: UnknownMethodRejection complete(StatusCodes.BadRequest, JsonRPCRes(null, Some(Error(StatusCodes.BadRequest.intValue, "method not found")), ukm.requestId))
case p: UnknownParamsRejection complete(StatusCodes.BadRequest,
JsonRPCRes(null, Some(Error(StatusCodes.BadRequest.intValue, s"invalid parameters for this method, should be: ${p.message}")), p.requestId))
case m: MalformedRequestContentRejection complete(StatusCodes.BadRequest,
JsonRPCRes(null, Some(Error(StatusCodes.BadRequest.intValue, s"malformed parameters for this method: ${m.message}")), "-1"))
case e: ExceptionRejection complete(StatusCodes.BadRequest,
JsonRPCRes(null, Some(Error(StatusCodes.BadRequest.intValue, s"command failed: ${e.message}")), e.requestId))
case r logger.error(s"API call failed with cause=$r")
complete(StatusCodes.BadRequest, JsonRPCRes(null, Some(Error(StatusCodes.BadRequest.intValue, r.toString)), "-1"))
}
@ -99,117 +109,142 @@ trait Service extends Logging {
respondWithDefaultHeaders(customHeaders) {
handleExceptions(myExceptionHandler) {
handleRejections(myRejectionHandler) {
pathSingleSlash {
post {
entity(as[JsonRPCBody]) {
req =>
val kit = appKit
import kit._
authenticateBasic(realm = "Access restricted", userPassAuthenticator) { _ =>
pathSingleSlash {
post {
entity(as[JsonRPCBody]) {
req =>
val kit = appKit
import kit._
req.method match {
// utility methods
case "getinfo" => completeRpcFuture(req.id, getInfoResponse)
case "help" => completeRpc(req.id, help)
req.method match {
// utility methods
case "getinfo" => completeRpcFuture(req.id, getInfoResponse)
case "help" => completeRpc(req.id, help)
// channel lifecycle methods
case "connect" => req.params match {
case JString(uri) :: Nil =>
completeRpcFuture(req.id, (switchboard ? Peer.Connect(NodeURI.parse(uri))).mapTo[String])
case _ => reject(UnknownParamsRejection(req.id, "[nodeId, host, port]"))
}
case "open" => req.params match {
case JString(nodeId) :: JInt(fundingSatoshi) :: JInt(pushMsat) :: JInt(flags) :: Nil =>
completeRpcFuture(req.id, (switchboard ? Peer.OpenChannel(PublicKey(nodeId), Satoshi(fundingSatoshi.toLong), MilliSatoshi(pushMsat.toLong), channelFlags = Some(flags.toByte))).mapTo[String])
case JString(nodeId) :: JInt(fundingSatoshi) :: JInt(pushMsat) :: Nil =>
completeRpcFuture(req.id, (switchboard ? Peer.OpenChannel(PublicKey(nodeId), Satoshi(fundingSatoshi.toLong), MilliSatoshi(pushMsat.toLong), channelFlags = None)).mapTo[String])
case _ => reject(UnknownParamsRejection(req.id, "[nodeId, host, port, fundingSatoshi, pushMsat] or [nodeId, host, port, fundingSatoshi, pushMsat, newChannel]"))
}
case "close" => req.params match {
case JString(identifier) :: Nil => completeRpc(req.id, sendToChannel(identifier, CMD_CLOSE(scriptPubKey = None)).mapTo[String])
case JString(identifier) :: JString(scriptPubKey) :: Nil => completeRpc(req.id, sendToChannel(identifier, CMD_CLOSE(scriptPubKey = Some(scriptPubKey))).mapTo[String])
case _ => reject(UnknownParamsRejection(req.id, "[channelId] or [channelId, scriptPubKey]"))
}
// local network methods
case "peers" => completeRpcFuture(req.id, for {
peers <- (switchboard ? 'peers).mapTo[Map[PublicKey, ActorRef]]
peerinfos <- Future.sequence(peers.values.map(peer => (peer ? GetPeerInfo).mapTo[PeerInfo]))
} yield peerinfos)
case "channels" => req.params match {
case Nil => completeRpcFuture(req.id, (register ? 'channels).mapTo[Map[Long, ActorRef]].map(_.keys))
case JString(remoteNodeId) :: Nil => Try(PublicKey(remoteNodeId)) match {
case Success(pk) => completeRpcFuture(req.id, (register ? 'channelsTo).mapTo[Map[BinaryData, PublicKey]].map(_.filter(_._2 == pk).keys))
case Failure(f) => reject(ValidationRejection(req.id, s"invalid remote node id '$remoteNodeId'"))
}
case _ => reject(UnknownParamsRejection(req.id, "no arguments or [remoteNodeId]"))
}
case "channel" => req.params match {
case JString(identifier) :: Nil => completeRpcFuture(req.id, sendToChannel(identifier, CMD_GETINFO).mapTo[RES_GETINFO])
case _ => reject(UnknownParamsRejection(req.id, "[channelId]"))
}
// global network methods
case "allnodes" => completeRpcFuture(req.id, (router ? 'nodes).mapTo[Iterable[NodeAnnouncement]].map(_.map(_.nodeId)))
case "allchannels" => completeRpcFuture(req.id, (router ? 'channels).mapTo[Iterable[ChannelAnnouncement]].map(_.map(c => ChannelInfo(c.shortChannelId.toHexString, c.nodeId1, c.nodeId2))))
// payment methods
case "receive" => req.params match {
// only the payment description is given: user may want to generate a donation payment request
case JString(description) :: Nil =>
completeRpcFuture(req.id, (paymentHandler ? ReceivePayment(None, description)).mapTo[PaymentRequest].map(PaymentRequest.write))
// the amount is now given with the description
case JInt(amountMsat) :: JString(description) :: Nil =>
completeRpcFuture(req.id, (paymentHandler ? ReceivePayment(Some(MilliSatoshi(amountMsat.toLong)), description)).mapTo[PaymentRequest].map(PaymentRequest.write))
case _ => reject(UnknownParamsRejection(req.id, "[description] or [amount, description]"))
}
case "send" => req.params match {
// user manually sets the payment information
case JInt(amountMsat) :: JString(paymentHash) :: JString(nodeId) :: Nil =>
(Try(BinaryData(paymentHash)), Try(PublicKey(nodeId))) match {
case (Success(ph), Success(pk)) => completeRpcFuture(req.id, (paymentInitiator ? SendPayment(amountMsat.toLong, ph, pk)).mapTo[PaymentResult])
case (Failure(_), _) => reject(ValidationRejection(req.id, s"invalid payment hash '$paymentHash'"))
case _ => reject(ValidationRejection(req.id, s"invalid node id '$nodeId'"))
}
// user gives a Lightning payment request
case JString(paymentRequest) :: rest => Try(PaymentRequest.read(paymentRequest)) match {
case Success(pr) =>
// setting the payment amount
val amount_msat: Long = (pr.amount, rest) match {
// optional amount always overrides the amount in the payment request
case (_, JInt(amount_msat_override) :: Nil) => amount_msat_override.toLong
case (Some(amount_msat_pr), _) => amount_msat_pr.amount
case _ => throw new RuntimeException("you must manually specify an amount for this payment request")
}
logger.debug(s"api call for sending payment with amount_msat=$amount_msat")
// optional cltv expiry
val sendPayment = pr.minFinalCltvExpiry match {
case None => SendPayment(amount_msat, pr.paymentHash, pr.nodeId)
case Some(minFinalCltvExpiry) => SendPayment(amount_msat, pr.paymentHash, pr.nodeId, assistedRoutes = Nil, minFinalCltvExpiry)
}
completeRpcFuture(req.id, (paymentInitiator ? sendPayment).mapTo[PaymentResult])
case _ => reject(ValidationRejection(req.id, s"payment request is not valid"))
// channel lifecycle methods
case "connect" => req.params match {
case JString(pubkey) :: JString(host) :: JInt(port) :: Nil =>
completeRpcFuture(req.id, (switchboard ? Peer.Connect(NodeURI.parse(s"$pubkey@$host:$port"))).mapTo[String])
case JString(uri) :: Nil =>
completeRpcFuture(req.id, (switchboard ? Peer.Connect(NodeURI.parse(uri))).mapTo[String])
case _ => reject(UnknownParamsRejection(req.id, "[nodeId@host:port] or [nodeId, host, port]"))
}
case "open" => req.params match {
case JString(nodeId) :: JInt(fundingSatoshi) :: Nil =>
completeRpcFuture(req.id, (switchboard ? Peer.OpenChannel(PublicKey(nodeId), Satoshi(fundingSatoshi.toLong), MilliSatoshi(0), channelFlags = None)).mapTo[String])
case JString(nodeId) :: JInt(fundingSatoshi) :: JInt(pushMsat) :: Nil =>
completeRpcFuture(req.id, (switchboard ? Peer.OpenChannel(PublicKey(nodeId), Satoshi(fundingSatoshi.toLong), MilliSatoshi(pushMsat.toLong), channelFlags = None)).mapTo[String])
case JString(nodeId) :: JInt(fundingSatoshi) :: JInt(pushMsat) :: JInt(flags) :: Nil =>
completeRpcFuture(req.id, (switchboard ? Peer.OpenChannel(PublicKey(nodeId), Satoshi(fundingSatoshi.toLong), MilliSatoshi(pushMsat.toLong), channelFlags = Some(flags.toByte))).mapTo[String])
case _ => reject(UnknownParamsRejection(req.id, s"[nodeId, fundingSatoshi], [nodeId, fundingSatoshi, pushMsat] or [nodeId, fundingSatoshi, pushMsat, newChannel]"))
}
case "close" => req.params match {
case JString(identifier) :: Nil => completeRpc(req.id, sendToChannel(identifier, CMD_CLOSE(scriptPubKey = None)).mapTo[String])
case JString(identifier) :: JString(scriptPubKey) :: Nil => completeRpc(req.id, sendToChannel(identifier, CMD_CLOSE(scriptPubKey = Some(scriptPubKey))).mapTo[String])
case _ => reject(UnknownParamsRejection(req.id, "[channelId] or [channelId, scriptPubKey]"))
}
case _ => reject(UnknownParamsRejection(req.id, "[amountMsat, paymentHash, nodeId or [paymentRequest] or [paymentRequest, amountMsat]"))
}
// check received payments
case "checkpayment" => req.params match {
case JString(identifier) :: Nil => completeRpcFuture(req.id, for {
paymentHash <- Try(PaymentRequest.read(identifier)) match {
case Success(pr) => Future.successful(pr.paymentHash)
case _ => Try(BinaryData(identifier)) match {
case Success(s) => Future.successful(s)
case _ => Future.failed(new IllegalArgumentException("payment identifier must be a payment request or a payment hash"))
// local network methods
case "peers" => completeRpcFuture(req.id, for {
peers <- (switchboard ? 'peers).mapTo[Map[PublicKey, ActorRef]]
peerinfos <- Future.sequence(peers.values.map(peer => (peer ? GetPeerInfo).mapTo[PeerInfo]))
} yield peerinfos)
case "channels" => req.params match {
case Nil =>
val f = for {
channels_id <- (register ? 'channels).mapTo[Map[BinaryData, ActorRef]].map(_.keys)
channels <- Future.sequence(channels_id.map(channel_id => sendToChannel(channel_id.toString(), CMD_GETINFO).mapTo[RES_GETINFO]
.map(gi => LocalChannelInfo(gi.nodeId, gi.channelId, gi.state.toString))))
} yield channels
completeRpcFuture(req.id, f)
case JString(remoteNodeId) :: Nil => Try(PublicKey(remoteNodeId)) match {
case Success(pk) =>
val f = for {
channels_id <- (register ? 'channelsTo).mapTo[Map[BinaryData, PublicKey]].map(_.filter(_._2 == pk).keys)
channels <- Future.sequence(channels_id.map(channel_id => sendToChannel(channel_id.toString(), CMD_GETINFO).mapTo[RES_GETINFO]
.map(gi => LocalChannelInfo(gi.nodeId, gi.channelId, gi.state.toString))))
} yield channels
completeRpcFuture(req.id, f)
case Failure(_) => reject(RpcValidationRejection(req.id, s"invalid remote node id '$remoteNodeId'"))
}
}
found <- (paymentHandler ? CheckPayment(paymentHash)).map(found => new JBool(found.asInstanceOf[Boolean]))
} yield found)
case _ => reject(UnknownParamsRejection(req.id, "[paymentHash] or [paymentRequest]"))
}
case _ => reject(UnknownParamsRejection(req.id, "no arguments or [remoteNodeId]"))
}
case "channel" => req.params match {
case JString(identifier) :: Nil => completeRpcFuture(req.id, sendToChannel(identifier, CMD_GETINFO).mapTo[RES_GETINFO])
case _ => reject(UnknownParamsRejection(req.id, "[channelId]"))
}
// method name was not found
case _ => reject(UnknownMethodRejection(req.id))
}
// global network methods
case "allnodes" => completeRpcFuture(req.id, (router ? 'nodes).mapTo[Iterable[NodeAnnouncement]])
case "allchannels" => completeRpcFuture(req.id, (router ? 'channels).mapTo[Iterable[ChannelAnnouncement]].map(_.map(c => ChannelInfo(c.shortChannelId.toHexString, c.nodeId1, c.nodeId2))))
case "allupdates" => req.params match {
case JString(nodeId) :: Nil => Try(PublicKey(nodeId)) match {
case Success(pk) => completeRpcFuture(req.id, (router ? 'updatesMap).mapTo[Map[ChannelDesc, ChannelUpdate]].map(_.filter(e => e._1.a == pk || e._1.b == pk).values))
case Failure(_) => reject(RpcValidationRejection(req.id, s"invalid remote node id '$nodeId'"))
}
case _ => completeRpcFuture(req.id, (router ? 'updates).mapTo[Iterable[ChannelUpdate]])
}
// payment methods
case "receive" => req.params match {
// only the payment description is given: user may want to generate a donation payment request
case JString(description) :: Nil =>
completeRpcFuture(req.id, (paymentHandler ? ReceivePayment(None, description)).mapTo[PaymentRequest].map(PaymentRequest.write))
// the amount is now given with the description
case JInt(amountMsat) :: JString(description) :: Nil =>
completeRpcFuture(req.id, (paymentHandler ? ReceivePayment(Some(MilliSatoshi(amountMsat.toLong)), description)).mapTo[PaymentRequest].map(PaymentRequest.write))
case _ => reject(UnknownParamsRejection(req.id, "[description] or [amount, description]"))
}
case "send" => req.params match {
// user manually sets the payment information
case JInt(amountMsat) :: JString(paymentHash) :: JString(nodeId) :: Nil =>
(Try(BinaryData(paymentHash)), Try(PublicKey(nodeId))) match {
case (Success(ph), Success(pk)) => completeRpcFuture(req.id, (paymentInitiator ? SendPayment(amountMsat.toLong, ph, pk)).mapTo[PaymentResult])
case (Failure(_), _) => reject(RpcValidationRejection(req.id, s"invalid payment hash '$paymentHash'"))
case _ => reject(RpcValidationRejection(req.id, s"invalid node id '$nodeId'"))
}
// user gives a Lightning payment request
case JString(paymentRequest) :: rest => Try(PaymentRequest.read(paymentRequest)) match {
case Success(pr) =>
// setting the payment amount
val amount_msat: Long = (pr.amount, rest) match {
// optional amount always overrides the amount in the payment request
case (_, JInt(amount_msat_override) :: Nil) => amount_msat_override.toLong
case (Some(amount_msat_pr), _) => amount_msat_pr.amount
case _ => throw new RuntimeException("you must manually specify an amount for this payment request")
}
logger.debug(s"api call for sending payment with amount_msat=$amount_msat")
// optional cltv expiry
val sendPayment = pr.minFinalCltvExpiry match {
case None => SendPayment(amount_msat, pr.paymentHash, pr.nodeId)
case Some(minFinalCltvExpiry) => SendPayment(amount_msat, pr.paymentHash, pr.nodeId, assistedRoutes = Nil, minFinalCltvExpiry)
}
completeRpcFuture(req.id, (paymentInitiator ? sendPayment).mapTo[PaymentResult])
case _ => reject(RpcValidationRejection(req.id, s"payment request is not valid"))
}
case _ => reject(UnknownParamsRejection(req.id, "[amountMsat, paymentHash, nodeId or [paymentRequest] or [paymentRequest, amountMsat]"))
}
// check received payments
case "checkpayment" => req.params match {
case JString(identifier) :: Nil => completeRpcFuture(req.id, for {
paymentHash <- Try(PaymentRequest.read(identifier)) match {
case Success(pr) => Future.successful(pr.paymentHash)
case _ => Try(BinaryData(identifier)) match {
case Success(s) => Future.successful(s)
case _ => Future.failed(new IllegalArgumentException("payment identifier must be a payment request or a payment hash"))
}
}
found <- (paymentHandler ? CheckPayment(paymentHash)).map(found => new JBool(found.asInstanceOf[Boolean]))
} yield found)
case _ => reject(UnknownParamsRejection(req.id, "[paymentHash] or [paymentRequest]"))
}
// method name was not found
case _ => reject(UnknownMethodRejection(req.id))
}
}
}
}
}
@ -221,13 +256,16 @@ trait Service extends Logging {
def help = List(
"connect (uri): open a secure connection to a lightning node",
"open (nodeId, fundingSatoshi, pushMsat, channelFlags = 0x01): open a channel with another lightning node",
"connect (nodeId, host, port): open a secure connection to a lightning node",
"open (nodeId, fundingSatoshi, pushMsat = 0, channelFlags = 0x01): open a channel with another lightning node",
"peers: list existing local peers",
"channels: list existing local channels",
"channels (nodeId): list existing local channels to a particular nodeId",
"channel (channelId): retrieve detailed information about a given channel",
"allnodes: list all known nodes",
"allchannels: list all known channels",
"allupdates: list all channels updates",
"allupdates (nodeId): list all channels updates for this nodeId",
"receive (amountMsat, description): generate a payment request for a given amount",
"send (amountMsat, paymentHash, nodeId): send a payment to a lightning node",
"send (paymentRequest): send a payment to a lightning node using a BOLT11 payment request",

View file

@ -7,7 +7,7 @@ import fr.acinq.eclair.UInt64
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.transactions.CommitmentSpec
import fr.acinq.eclair.transactions.Transactions.CommitTx
import fr.acinq.eclair.wire.{AcceptChannel, AnnouncementSignatures, ChannelAnnouncement, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, OpenChannel, Shutdown, UpdateAddHtlc}
import fr.acinq.eclair.wire.{AcceptChannel, ChannelAnnouncement, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, OpenChannel, Shutdown, UpdateAddHtlc}
/**
@ -101,7 +101,7 @@ final case class CMD_CLOSE(scriptPubKey: Option[BinaryData]) extends Command
case object CMD_GETSTATE extends Command
case object CMD_GETSTATEDATA extends Command
case object CMD_GETINFO extends Command
final case class RES_GETINFO(nodeid: BinaryData, channelId: BinaryData, state: State, data: Data)
final case class RES_GETINFO(nodeId: BinaryData, channelId: BinaryData, state: State, data: Data)
/*
8888888b. d8888 88888888888 d8888

View file

@ -5,7 +5,7 @@ import java.net.InetSocketAddress
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, sha256, verifySignature}
import fr.acinq.bitcoin.{BinaryData, Crypto, LexicographicalOrdering}
import fr.acinq.eclair.serializationResult
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, LightningMessageCodecs, NodeAnnouncement}
import fr.acinq.eclair.wire._
import scodec.bits.BitVector
import shapeless.HNil
@ -20,8 +20,8 @@ object Announcements {
def channelAnnouncementWitnessEncode(chainHash: BinaryData, shortChannelId: Long, nodeId1: PublicKey, nodeId2: PublicKey, bitcoinKey1: PublicKey, bitcoinKey2: PublicKey, features: BinaryData): BinaryData =
sha256(sha256(serializationResult(LightningMessageCodecs.channelAnnouncementWitnessCodec.encode(features :: chainHash :: shortChannelId :: nodeId1 :: nodeId2 :: bitcoinKey1 :: bitcoinKey2 :: HNil))))
def nodeAnnouncementWitnessEncode(timestamp: Long, nodeId: PublicKey, rgbColor: (Byte, Byte, Byte), alias: String, features: BinaryData, addresses: List[InetSocketAddress]): BinaryData =
sha256(sha256(serializationResult(LightningMessageCodecs.nodeAnnouncementWitnessCodec.encode(features :: timestamp :: nodeId :: rgbColor :: alias :: addresses :: HNil))))
def nodeAnnouncementWitnessEncode(timestamp: Long, nodeId: PublicKey, rgbColor: Color, alias: String, features: BinaryData, addresses: List[InetSocketAddress]): BinaryData =
sha256(sha256(serializationResult(LightningMessageCodecs.nodeAnnouncementWitnessCodec.encode(features :: timestamp :: nodeId :: (rgbColor) :: alias :: addresses :: HNil))))
def channelUpdateWitnessEncode(chainHash: BinaryData, shortChannelId: Long, timestamp: Long, flags: BinaryData, cltvExpiryDelta: Int, htlcMinimumMsat: Long, feeBaseMsat: Long, feeProportionalMillionths: Long): BinaryData =
sha256(sha256(serializationResult(LightningMessageCodecs.channelUpdateWitnessCodec.encode(chainHash :: shortChannelId :: timestamp :: flags :: cltvExpiryDelta :: htlcMinimumMsat :: feeBaseMsat :: feeProportionalMillionths :: HNil))))
@ -59,7 +59,7 @@ object Announcements {
)
}
def makeNodeAnnouncement(nodeSecret: PrivateKey, alias: String, color: (Byte, Byte, Byte), addresses: List[InetSocketAddress], timestamp: Long = Platform.currentTime / 1000): NodeAnnouncement = {
def makeNodeAnnouncement(nodeSecret: PrivateKey, alias: String, color: Color, addresses: List[InetSocketAddress], timestamp: Long = Platform.currentTime / 1000): NodeAnnouncement = {
require(alias.size <= 32)
val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, "", addresses)
val sig = Crypto.encodeSignature(Crypto.sign(witness, nodeSecret)) :+ 1.toByte

View file

@ -406,6 +406,10 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSM[State, Data]
sender ! (d.updates ++ d.privateUpdates).values
stay
case Event('updatesMap, d) =>
sender ! (d.updates ++ d.privateUpdates)
stay
case Event('dot, d) =>
graph2dot(d.nodes, d.channels) pipeTo sender
stay
@ -549,7 +553,7 @@ object Router {
override def getComponentAttributes(nodeId: PublicKey): java.util.Map[String, String] =
nodes.get(nodeId) match {
case Some(ann) => Map("label" -> ann.alias, "color" -> f"#${ann.rgbColor._1}%02x${ann.rgbColor._2}%02x${ann.rgbColor._3}%02x")
case Some(ann) => Map("label" -> ann.alias, "color" -> ann.rgbColor.toString)
case None => Map.empty[String, String]
}
}

View file

@ -81,7 +81,7 @@ object LightningMessageCodecs {
}))
)
def rgb: Codec[(Byte, Byte, Byte)] = bytes(3).xmap(buf => (buf(0), buf(1), buf(2)), t => ByteVector(t._1, t._2, t._3))
def rgb: Codec[Color] = bytes(3).xmap(buf => Color(buf(0), buf(1), buf(2)), t => ByteVector(t.r, t.g, t.b))
def zeropaddedstring(size: Int): Codec[String] = fixedSizeBytes(32, utf8).xmap(s => s.takeWhile(_ != '\u0000'), s => s)

View file

@ -136,11 +136,15 @@ case class ChannelAnnouncement(nodeSignature1: BinaryData,
bitcoinKey1: PublicKey,
bitcoinKey2: PublicKey) extends RoutingMessage
case class Color(r: Byte, g: Byte, b: Byte) {
override def toString: String = f"#$r%02x$g%02x$b%02x" // to hexa s"# ${r}%02x ${r & 0xFF}${g & 0xFF}${b & 0xFF}"
}
case class NodeAnnouncement(signature: BinaryData,
features: BinaryData,
timestamp: Long,
nodeId: PublicKey,
rgbColor: (Byte, Byte, Byte),
rgbColor: Color,
alias: String,
// TODO: check address order + support padding data (type 0)
addresses: List[InetSocketAddress]) extends RoutingMessage
@ -157,4 +161,4 @@ case class ChannelUpdate(signature: BinaryData,
case class PerHopPayload(channel_id: Long,
amtToForward: Long,
outgoingCltvValue: Long)
outgoingCltvValue: Long)

View file

@ -8,6 +8,7 @@ import fr.acinq.bitcoin.{BinaryData, Block, DeterministicWallet, Script}
import fr.acinq.eclair.NodeParams.BITCOIND
import fr.acinq.eclair.db.sqlite._
import fr.acinq.eclair.io.Peer
import fr.acinq.eclair.wire.Color
import scala.concurrent.duration._
@ -31,7 +32,7 @@ object TestConstants {
extendedPrivateKey = extendedPrivateKey,
privateKey = extendedPrivateKey.privateKey,
alias = "alice",
color = (1: Byte, 2: Byte, 3: Byte),
color = Color(1, 2, 3),
publicAddresses = new InetSocketAddress("localhost", 9731) :: Nil,
globalFeatures = "",
localFeatures = "00",
@ -85,7 +86,7 @@ object TestConstants {
extendedPrivateKey = extendedPrivateKey,
privateKey = extendedPrivateKey.privateKey,
alias = "bob",
color = (4: Byte, 5: Byte, 6: Byte),
color = Color(4, 5, 6),
publicAddresses = new InetSocketAddress("localhost", 9732) :: Nil,
globalFeatures = "",
localFeatures = "00", // no announcement

View file

@ -33,7 +33,7 @@ class ElectrumClientSpec extends TestKit(ActorSystem("test")) with FunSuiteLike
test("connect to an electrumx testnet server") {
probe.send(client, AddStatusListener(probe.ref))
probe.expectMsg(5 seconds, ElectrumReady)
probe.expectMsg(15 seconds, ElectrumReady)
}
test("get transaction") {

View file

@ -7,6 +7,7 @@ import fr.acinq.bitcoin.{Block, Crypto}
import fr.acinq.eclair.db.sqlite.SqliteNetworkDb
import fr.acinq.eclair.randomKey
import fr.acinq.eclair.router.Announcements
import fr.acinq.eclair.wire.Color
import org.junit.runner.RunWith
import org.scalatest.FunSuite
import org.scalatest.junit.JUnitRunner
@ -27,9 +28,9 @@ class SqliteNetworkDbSpec extends FunSuite {
val sqlite = inmem
val db = new SqliteNetworkDb(sqlite)
val node_1 = Announcements.makeNodeAnnouncement(randomKey, "node-alice", (100.toByte, 200.toByte, 300.toByte), new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000) :: Nil)
val node_2 = Announcements.makeNodeAnnouncement(randomKey, "node-bob", (100.toByte, 200.toByte, 300.toByte), new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000) :: Nil)
val node_3 = Announcements.makeNodeAnnouncement(randomKey, "node-charlie", (100.toByte, 200.toByte, 300.toByte), new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000) :: Nil)
val node_1 = Announcements.makeNodeAnnouncement(randomKey, "node-alice", Color(100.toByte, 200.toByte, 300.toByte), new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000) :: Nil)
val node_2 = Announcements.makeNodeAnnouncement(randomKey, "node-bob", Color(100.toByte, 200.toByte, 300.toByte), new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000) :: Nil)
val node_3 = Announcements.makeNodeAnnouncement(randomKey, "node-charlie", Color(100.toByte, 200.toByte, 300.toByte), new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000) :: Nil)
assert(db.listNodes().toSet === Set.empty)
db.addNode(node_1)

View file

@ -34,12 +34,12 @@ abstract class BaseRouterSpec extends TestkitBaseClass {
//val DUMMY_SIG = BinaryData("3045022100e0a180fdd0fe38037cc878c03832861b40a29d32bd7b40b10c9e1efc8c1468a002205ae06d1624896d0d29f4b31e32772ea3cb1b4d7ed4e077e5da28dcc33c0e781201")
val ann_a = makeNodeAnnouncement(priv_a, "node-A", (15, 10, -70), Nil)
val ann_b = makeNodeAnnouncement(priv_b, "node-B", (50, 99, -80), Nil)
val ann_c = makeNodeAnnouncement(priv_c, "node-C", (123, 100, -40), Nil)
val ann_d = makeNodeAnnouncement(priv_d, "node-D", (-120, -20, 60), Nil)
val ann_e = makeNodeAnnouncement(priv_e, "node-E", (-50, 0, 10), Nil)
val ann_f = makeNodeAnnouncement(priv_f, "node-F", (30, 10, -50), Nil)
val ann_a = makeNodeAnnouncement(priv_a, "node-A", Color(15, 10, -70), Nil)
val ann_b = makeNodeAnnouncement(priv_b, "node-B", Color(50, 99, -80), Nil)
val ann_c = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), Nil)
val ann_d = makeNodeAnnouncement(priv_d, "node-D", Color(-120, -20, 60), Nil)
val ann_e = makeNodeAnnouncement(priv_e, "node-E", Color(-50, 0, 10), Nil)
val ann_f = makeNodeAnnouncement(priv_f, "node-F", Color(30, 10, -50), Nil)
val channelId_ab = toShortId(420000, 1, 0)
val channelId_bc = toShortId(420000, 2, 0)

View file

@ -54,7 +54,7 @@ class LightningMessageCodecsSpec extends FunSuite {
}
test("encode/decode with rgb codec") {
val color = (47.toByte, 255.toByte, 142.toByte)
val color = Color(47.toByte, 255.toByte, 142.toByte)
val bin = rgb.encode(color).require
assert(bin === hex"2f ff 8e".toBitVector)
val color2 = rgb.decode(bin).require.value
@ -178,7 +178,7 @@ class LightningMessageCodecsSpec extends FunSuite {
val commit_sig = CommitSig(randomBytes(32), randomSignature, randomSignature :: randomSignature :: randomSignature :: Nil)
val revoke_and_ack = RevokeAndAck(randomBytes(32), scalar(0), point(1))
val channel_announcement = ChannelAnnouncement(randomSignature, randomSignature, randomSignature, randomSignature, bin(7, 9), Block.RegtestGenesisBlock.hash, 1, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey)
val node_announcement = NodeAnnouncement(randomSignature, bin(0, 0), 1, randomKey.publicKey, (100.toByte, 200.toByte, 300.toByte), "node-alias", new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000) :: Nil)
val node_announcement = NodeAnnouncement(randomSignature, bin(0, 0), 1, randomKey.publicKey, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000) :: Nil)
val channel_update = ChannelUpdate(randomSignature, Block.RegtestGenesisBlock.hash, 1, 2, bin(2, 2), 3, 4, 5, 6)
val announcement_signatures = AnnouncementSignatures(randomBytes(32), 42, randomSignature, randomSignature)
val ping = Ping(100, BinaryData("01" * 10))

View file

@ -40,7 +40,7 @@
</children>
</VBox>
<Label onMouseClicked="#openGithubPage" VBox.vgrow="NEVER" styleClass="link"
text="Consult our readme to get started."/>
text="Check our readme to get started."/>
<Button fx:id="closeButton" VBox.vgrow="NEVER" mnemonicParsing="false" onAction="#closeAndKill"
text="Close" cancelButton="true"/>
</children>

View file

@ -22,6 +22,8 @@ import grizzled.slf4j.Logging
import scala.concurrent.Promise
import scala.util.{Failure, Success}
import scala.concurrent.ExecutionContext.Implicits.global
/**
* Created by PM on 16/08/2016.
@ -46,7 +48,7 @@ class FxApp extends Application with Logging {
notifyPreloader(new AppNotification(InfoAppNotification, "Eclair is still in alpha, and under heavy development. Last update was not backward compatible."))
notifyPreloader(new AppNotification(InfoAppNotification, "Please reset your datadir."))
case t: Throwable =>
notifyPreloader(new ErrorNotification("Setup", s"Internal error: ${t.toString}", t))
notifyPreloader(new ErrorNotification("Setup", s"Error: ${t.getLocalizedMessage}", t))
}
override def start(primaryStage: Stage): Unit = {
@ -71,7 +73,6 @@ class FxApp extends Application with Logging {
setup.system.eventStream.subscribe(guiUpdater, classOf[ZMQEvent])
setup.system.eventStream.subscribe(guiUpdater, classOf[ElectrumEvent])
pKit.completeWith(setup.bootstrap)
import scala.concurrent.ExecutionContext.Implicits.global
pKit.future.onComplete {
case Success(_) =>
Platform.runLater(new Runnable {

View file

@ -59,7 +59,7 @@ class FxPreloader extends Preloader with Logging {
info match {
case n: ErrorNotification =>
logger.debug(s"Preloader error notification => ${n.getDetails}")
logger.error("An error has occured", n.getCause)
logger.error("", n.getCause)
controller.map(_.addError(n.getDetails))
controller.map(_.showErrorBox)
case n: AppNotification =>

View file

@ -162,8 +162,7 @@ class MainController(val handlers: Handlers, val hostServices: HostServices) ext
def call(pn: CellDataFeatures[NodeAnnouncement, String]) = new SimpleStringProperty(pn.getValue.alias)
})
networkNodesRGBColumn.setCellValueFactory(new Callback[CellDataFeatures[NodeAnnouncement, String], ObservableValue[String]]() {
def call(pn: CellDataFeatures[NodeAnnouncement, String]) = new SimpleStringProperty(
s"rgb(${new Integer(pn.getValue.rgbColor._1 & 0xFF)}, ${new Integer(pn.getValue.rgbColor._2 & 0xFF)}, ${new Integer(pn.getValue.rgbColor._3 & 0xFF)})")
def call(pn: CellDataFeatures[NodeAnnouncement, String]) = new SimpleStringProperty(pn.getValue.rgbColor.toString)
})
networkNodesIPColumn.setCellValueFactory(new Callback[CellDataFeatures[NodeAnnouncement, String], ObservableValue[String]]() {
def call(pn: CellDataFeatures[NodeAnnouncement, String]) = {
@ -313,7 +312,7 @@ class MainController(val handlers: Handlers, val hostServices: HostServices) ext
// init status bar
labelNodeId.setText(s"${setup.nodeParams.privateKey.publicKey}")
labelAlias.setText(s"${setup.nodeParams.alias}")
rectRGB.setFill(Color.rgb(setup.nodeParams.color._1 & 0xFF, setup.nodeParams.color._2 & 0xFF, setup.nodeParams.color._3 & 0xFF))
rectRGB.setFill(Color.web(setup.nodeParams.color.toString))
labelApi.setText(s"${setup.config.getInt("api.port")}")
labelServer.setText(s"${setup.config.getInt("server.port")}")
bitcoinVersion.setText(s"v0.0.0")