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 ## 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 ### Configuring Bitcoin Core
@ -93,7 +93,10 @@ Here are some of the most common options:
name | description | default value name | description | default value
-----------------------------|---------------------------|-------------- -----------------------------|---------------------------|--------------
eclair.server.port | Lightning TCP port | 9735 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.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.rpcuser | Bitcoin Core RPC user | foo
eclair.bitcoind.rpcpassword | Bitcoin Core RPC password | bar eclair.bitcoind.rpcpassword | Bitcoin Core RPC password | bar
eclair.bitcoind.zmq | Bitcoin Core ZMQ address | tcp://127.0.0.1:29000 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 ## JSON-RPC API
method | params | description method | params | description
-------------|-----------------------------------------------|----------------------------------------------------------- ------------- |------------------------------------------------------------|-----------------------------------------------------------
getinfo | | return basic node information (id, chain hash, current block height) getinfo | | return basic node information (id, chain hash, current block height)
connect | uri | open a secure connection to a lightning node connect | nodeId, host, port | open a secure connection to a lightning node
open | nodeId, fundingSatoshis, pushMsat | open a channel with another lightning node connect | uri | open a secure connection to a lightning node
peers | | list existing local peers open | nodeId, fundingSatoshis, pushMsat = 0, channelFlags = 0x01 | open a channel with another lightning node, by default push = 0 and channel is announced
channels | | list existing local channels peers | | list existing local peers
channels | nodeId | list existing local channels opened with a particular nodeId channels | | list existing local channels
channel | channelId | retrieve detailed information about a given channel channels | nodeId | list existing local channels opened with a particular nodeId
allnodes | | list all known nodes channel | channelId | retrieve detailed information about a given channel
allchannels | | list all known channels allnodes | | list all known nodes
receive | description | generate a payment request without a required amount (can be useful for donations) allchannels | | list all known channels
receive | amountMsat, description | generate a payment request for a given amount allupdates | | list all channels updates
send | amountMsat, paymentHash, nodeId | send a payment to a lightning node allupdates | nodeId | list all channels updates for this nodeId
send | paymentRequest | send a payment to a lightning node using a BOLT11 payment request receive | description | generate a payment request without a required amount (can be useful for donations)
send | paymentRequest, amountMsat | send a payment to a lightning node using a BOLT11 payment request and a custom amount receive | amountMsat, description | generate a payment request for a given amount
checkpayment| paymentHash | returns true if the payment has been received, false otherwise send | amountMsat, paymentHash, nodeId | send a payment to a lightning node
checkpayment| paymentRequest | returns true if the payment has been received, false otherwise send | paymentRequest | send a payment to a lightning node using a BOLT11 payment request
close | channelId | close a channel send | paymentRequest, amountMsat | send a payment to a lightning node using a BOLT11 payment request and a custom amount
close | channelId, scriptPubKey (optional) | close a channel and send the funds to the given scriptPubKey checkpayment | paymentHash | returns true if the payment has been received, false otherwise
help | | display available methods 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 ## Docker
@ -161,8 +167,8 @@ docker run -ti --rm -v "/path_on_host:/data" -e "JAVA_OPTS=-Declair.printToConso
## Resources ## Resources
- [1] [The Bitcoin Lightning Network: Scalable Off-Chain Instant Payments](https://lightning.network/lightning-network-paper.pdf) by Joseph Poon and Thaddeus Dryja - [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 - [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 - [3] [Lightning Network Explorer](https://explorer.acinq.co) - Explore testnet LN nodes you can connect to
[Amiko-Pay]: https://github.com/cornwarecjp/amiko-pay [Amiko-Pay]: https://github.com/cornwarecjp/amiko-pay

View file

@ -1,59 +1,95 @@
#!/bin/bash #!/bin/bash
[ -z "$1" ] && ( # Check if jq is installed. If not, display instructions and abort program
echo "usage: " 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; }
echo " eclair-cli help"
) && exit 1
URL="http://localhost:8080" FULL_OUTPUT='false'
CURL_OPTS="-sS -X POST -H \"Content-Type: application/json\"" 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 esac

View file

@ -9,8 +9,10 @@ eclair {
} }
api { api {
enabled = false // disabled by default for security reasons
binding-ip = "127.0.0.1" binding-ip = "127.0.0.1"
port = 8080 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" 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.channel.Channel
import fr.acinq.eclair.db._ import fr.acinq.eclair.db._
import fr.acinq.eclair.db.sqlite._ import fr.acinq.eclair.db.sqlite._
import fr.acinq.eclair.wire.Color
import scala.collection.JavaConversions._ import scala.collection.JavaConversions._
import scala.concurrent.duration.FiniteDuration import scala.concurrent.duration.FiniteDuration
@ -24,7 +25,7 @@ import scala.concurrent.duration.FiniteDuration
case class NodeParams(extendedPrivateKey: ExtendedPrivateKey, case class NodeParams(extendedPrivateKey: ExtendedPrivateKey,
privateKey: PrivateKey, privateKey: PrivateKey,
alias: String, alias: String,
color: (Byte, Byte, Byte), color: Color,
publicAddresses: List[InetSocketAddress], publicAddresses: List[InetSocketAddress],
globalFeatures: BinaryData, globalFeatures: BinaryData,
localFeatures: BinaryData, localFeatures: BinaryData,
@ -131,7 +132,7 @@ object NodeParams {
extendedPrivateKey = extendedPrivateKey, extendedPrivateKey = extendedPrivateKey,
privateKey = extendedPrivateKey.privateKey, privateKey = extendedPrivateKey.privateKey,
alias = config.getString("node-alias").take(32), 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"))), publicAddresses = config.getStringList("server.public-ips").toList.map(ip => new InetSocketAddress(ip, config.getInt("server.port"))),
globalFeatures = BinaryData(config.getString("global-features")), globalFeatures = BinaryData(config.getString("global-features")),
localFeatures = BinaryData(config.getString("local-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"hello!")
logger.info(s"version=${getClass.getPackage.getImplementationVersion} commit=${getClass.getPackage.getSpecificationVersion}") logger.info(s"version=${getClass.getPackage.getImplementationVersion} commit=${getClass.getPackage.getSpecificationVersion}")
val config = NodeParams.loadConfiguration(datadir, overrideDefaults) val config: Config = NodeParams.loadConfiguration(datadir, overrideDefaults)
val nodeParams = NodeParams.makeNodeParams(datadir, config) val nodeParams: NodeParams = NodeParams.makeNodeParams(datadir, config)
val chain = config.getString("chain") val chain: String = config.getString("chain")
// early checks // early checks
DBCompatChecker.checkDBCompatibility(nodeParams) DBCompatChecker.checkDBCompatibility(nodeParams)
@ -172,31 +172,43 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act
server = server, server = server,
wallet = wallet) 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 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 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 { for {
_ <- Future.firstCompletedOf(zmqConnected.future :: zmqTimeout :: Nil) _ <- Future.firstCompletedOf(zmqConnected.future :: zmqTimeout :: Nil)
_ <- Future.firstCompletedOf(tcpBound.future :: tcpTimeout :: 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 } yield kit
} }
} }
// @formatter:off // @formatter:off
sealed trait Bitcoin sealed trait Bitcoin
case class Bitcoind(extendedBitcoinClient: ExtendedBitcoinClient) extends 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 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 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 java.net.InetSocketAddress
import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, PublicKey, Scalar} 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.channel.State
import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.crypto.ShaChain
import fr.acinq.eclair.transactions.Transactions.TransactionWithInputInfo 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.JsonAST.{JNull, JString}
import org.json4s.{CustomKeySerializer, CustomSerializer}
/** /**
* Created by PM on 28/01/2016. * 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 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 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 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 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 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 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 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 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 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 => case x: String =>
val Array(k, v) = x.split(":") val Array(k, v) = x.split(":")
OutPoint(BinaryData(k), v.toLong) OutPoint(BinaryData(k), v.toLong)
@ -93,3 +94,10 @@ class OutPointKeySerializer extends CustomKeySerializer[OutPoint](format => ( {
case x: OutPoint => s"${x.hash}:${x.index}" 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 package fr.acinq.eclair.api
import akka.actor.ActorRef import akka.actor.ActorRef
import akka.http.scaladsl.model.HttpMethods._ import akka.http.scaladsl.model.HttpMethods._
import akka.http.scaladsl.model.StatusCodes 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.CacheDirectives.{`max-age`, `no-store`, public}
import akka.http.scaladsl.model.headers.HttpOriginRange.*
import akka.http.scaladsl.model.headers._ import akka.http.scaladsl.model.headers._
import akka.http.scaladsl.server.Directives._ 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.directives.RouteDirectives.reject
import akka.http.scaladsl.server.{ExceptionHandler, Rejection, RejectionHandler, Route}
import akka.pattern.ask import akka.pattern.ask
import akka.util.Timeout import akka.util.Timeout
import de.heikoseeberger.akkahttpjson4s.Json4sSupport import de.heikoseeberger.akkahttpjson4s.Json4sSupport
@ -20,9 +18,9 @@ import fr.acinq.eclair.Kit
import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel._
import fr.acinq.eclair.io.Peer.{GetPeerInfo, PeerInfo} import fr.acinq.eclair.io.Peer.{GetPeerInfo, PeerInfo}
import fr.acinq.eclair.io.{NodeURI, Peer} import fr.acinq.eclair.io.{NodeURI, Peer}
import fr.acinq.eclair.payment.{PaymentRequest, PaymentResult, ReceivePayment, SendPayment} import fr.acinq.eclair.payment.{PaymentRequest, PaymentResult, ReceivePayment, SendPayment, _}
import fr.acinq.eclair.payment._ import fr.acinq.eclair.router.ChannelDesc
import fr.acinq.eclair.wire.{ChannelAnnouncement, NodeAnnouncement} import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement}
import grizzled.slf4j.Logging import grizzled.slf4j.Logging
import org.json4s.JsonAST.{JBool, JInt, JString} import org.json4s.JsonAST.{JBool, JInt, JString}
import org.json4s.{JValue, jackson} 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 JsonRPCRes(result: AnyRef, error: Option[Error], id: String)
case class Status(node_id: String) case class Status(node_id: String)
case class GetInfoResponse(nodeId: PublicKey, alias: String, port: Int, chainHash: BinaryData, blockHeight: Int) 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) case class ChannelInfo(shortChannelId: String, nodeId1: PublicKey, nodeId2: PublicKey)
trait RPCRejection extends Rejection { trait RPCRejection extends Rejection {
def requestId: String def requestId: String
} }
final case class UnknownMethodRejection(requestId: String) extends RPCRejection final case class UnknownMethodRejection(requestId: String) extends RPCRejection
final case class UnknownParamsRejection(requestId: String, message: String) extends RPCRejection final case class UnknownParamsRejection(requestId: String, message: String) extends RPCRejection
final case class NotFoundRejection(requestId: String) extends RPCRejection final case class RpcValidationRejection(requestId: String, message: String) extends RPCRejection
final case class ValidationRejection(requestId: String, message: String) extends RPCRejection final case class ExceptionRejection(requestId: String, message: String) extends RPCRejection
// @formatter:on // @formatter:on
trait Service extends Logging { trait Service extends Logging {
@ -52,31 +51,38 @@ trait Service extends Logging {
implicit def ec: ExecutionContext = ExecutionContext.Implicits.global implicit def ec: ExecutionContext = ExecutionContext.Implicits.global
implicit val serialization = jackson.Serialization 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 timeout = Timeout(30 seconds)
implicit val shouldWritePretty: ShouldWritePretty = ShouldWritePretty.True implicit val shouldWritePretty: ShouldWritePretty = ShouldWritePretty.True
import Json4sSupport.{marshaller, unmarshaller} import Json4sSupport.{marshaller, unmarshaller}
def password: String
def appKit: Kit def appKit: Kit
val customHeaders = `Access-Control-Allow-Origin`(*) :: def userPassAuthenticator(credentials: Credentials): Option[String] = credentials match {
`Access-Control-Allow-Headers`("Content-Type, Authorization") :: case p@Credentials.Provided(id) if p.verify(password) => Some(id)
`Access-Control-Allow-Methods`(PUT, GET, POST, DELETE, OPTIONS) :: case _ =>
`Cache-Control`(public, `no-store`, `max-age`(0)) :: // TODO deter brute force with a forced delay
`Access-Control-Allow-Headers`("x-requested-with") :: Nil 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 { val myExceptionHandler = ExceptionHandler {
case t: Throwable => case t: Throwable =>
extractRequest { request => extractRequest { _ =>
logger.info(s"API call failed with cause=${t.getMessage}") 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) { def completeRpcFuture(requestId: String, future: Future[AnyRef]): Route = onComplete(future) {
case Success(s) => completeRpc(requestId, s) 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)) 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")) complete(StatusCodes.NotFound, JsonRPCRes(null, Some(Error(StatusCodes.NotFound.intValue, "not found")), "-1"))
} }
.handle { .handle {
case v: ValidationRejection complete(StatusCodes.BadRequest, JsonRPCRes(null, Some(Error(StatusCodes.BadRequest.intValue, v.message)), v.requestId)) case _: AuthenticationFailedRejection complete(StatusCodes.Unauthorized, JsonRPCRes(null, Some(Error(StatusCodes.Unauthorized.intValue, "Access restricted")), "-1"))
case nf: NotFoundRejection complete(StatusCodes.NotFound, JsonRPCRes(null, Some(Error(StatusCodes.NotFound.intValue, "not found")), nf.requestId)) 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 ukm: UnknownMethodRejection complete(StatusCodes.BadRequest, JsonRPCRes(null, Some(Error(StatusCodes.BadRequest.intValue, "method not found")), ukm.requestId))
case p: UnknownParamsRejection complete(StatusCodes.BadRequest, 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)) 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") case r logger.error(s"API call failed with cause=$r")
complete(StatusCodes.BadRequest, JsonRPCRes(null, Some(Error(StatusCodes.BadRequest.intValue, r.toString)), "-1")) complete(StatusCodes.BadRequest, JsonRPCRes(null, Some(Error(StatusCodes.BadRequest.intValue, r.toString)), "-1"))
} }
@ -99,117 +109,142 @@ trait Service extends Logging {
respondWithDefaultHeaders(customHeaders) { respondWithDefaultHeaders(customHeaders) {
handleExceptions(myExceptionHandler) { handleExceptions(myExceptionHandler) {
handleRejections(myRejectionHandler) { handleRejections(myRejectionHandler) {
pathSingleSlash { authenticateBasic(realm = "Access restricted", userPassAuthenticator) { _ =>
post { pathSingleSlash {
entity(as[JsonRPCBody]) { post {
req => entity(as[JsonRPCBody]) {
val kit = appKit req =>
import kit._ val kit = appKit
import kit._
req.method match { req.method match {
// utility methods // utility methods
case "getinfo" => completeRpcFuture(req.id, getInfoResponse) case "getinfo" => completeRpcFuture(req.id, getInfoResponse)
case "help" => completeRpc(req.id, help) case "help" => completeRpc(req.id, help)
// channel lifecycle methods // channel lifecycle methods
case "connect" => req.params match { case "connect" => req.params match {
case JString(uri) :: Nil => case JString(pubkey) :: JString(host) :: JInt(port) :: Nil =>
completeRpcFuture(req.id, (switchboard ? Peer.Connect(NodeURI.parse(uri))).mapTo[String]) completeRpcFuture(req.id, (switchboard ? Peer.Connect(NodeURI.parse(s"$pubkey@$host:$port"))).mapTo[String])
case _ => reject(UnknownParamsRejection(req.id, "[nodeId, host, port]")) case JString(uri) :: Nil =>
} completeRpcFuture(req.id, (switchboard ? Peer.Connect(NodeURI.parse(uri))).mapTo[String])
case "open" => req.params match { case _ => reject(UnknownParamsRejection(req.id, "[nodeId@host:port] or [nodeId, host, port]"))
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 "open" => req.params match {
case JString(nodeId) :: JInt(fundingSatoshi) :: JInt(pushMsat) :: Nil => case JString(nodeId) :: JInt(fundingSatoshi) :: Nil =>
completeRpcFuture(req.id, (switchboard ? Peer.OpenChannel(PublicKey(nodeId), Satoshi(fundingSatoshi.toLong), MilliSatoshi(pushMsat.toLong), channelFlags = None)).mapTo[String]) completeRpcFuture(req.id, (switchboard ? Peer.OpenChannel(PublicKey(nodeId), Satoshi(fundingSatoshi.toLong), MilliSatoshi(0), channelFlags = None)).mapTo[String])
case _ => reject(UnknownParamsRejection(req.id, "[nodeId, host, port, fundingSatoshi, pushMsat] or [nodeId, host, port, fundingSatoshi, pushMsat, newChannel]")) 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 "close" => req.params match { case JString(nodeId) :: JInt(fundingSatoshi) :: JInt(pushMsat) :: JInt(flags) :: Nil =>
case JString(identifier) :: Nil => completeRpc(req.id, sendToChannel(identifier, CMD_CLOSE(scriptPubKey = None)).mapTo[String]) completeRpcFuture(req.id, (switchboard ? Peer.OpenChannel(PublicKey(nodeId), Satoshi(fundingSatoshi.toLong), MilliSatoshi(pushMsat.toLong), channelFlags = Some(flags.toByte))).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, s"[nodeId, fundingSatoshi], [nodeId, fundingSatoshi, pushMsat] or [nodeId, fundingSatoshi, pushMsat, newChannel]"))
case _ => reject(UnknownParamsRejection(req.id, "[channelId] or [channelId, scriptPubKey]")) }
} case "close" => req.params match {
case JString(identifier) :: Nil => completeRpc(req.id, sendToChannel(identifier, CMD_CLOSE(scriptPubKey = None)).mapTo[String])
// local network methods case JString(identifier) :: JString(scriptPubKey) :: Nil => completeRpc(req.id, sendToChannel(identifier, CMD_CLOSE(scriptPubKey = Some(scriptPubKey))).mapTo[String])
case "peers" => completeRpcFuture(req.id, for { case _ => reject(UnknownParamsRejection(req.id, "[channelId] or [channelId, scriptPubKey]"))
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"))
} }
case _ => reject(UnknownParamsRejection(req.id, "[amountMsat, paymentHash, nodeId or [paymentRequest] or [paymentRequest, amountMsat]"))
}
// check received payments // local network methods
case "checkpayment" => req.params match { case "peers" => completeRpcFuture(req.id, for {
case JString(identifier) :: Nil => completeRpcFuture(req.id, for { peers <- (switchboard ? 'peers).mapTo[Map[PublicKey, ActorRef]]
paymentHash <- Try(PaymentRequest.read(identifier)) match { peerinfos <- Future.sequence(peers.values.map(peer => (peer ? GetPeerInfo).mapTo[PeerInfo]))
case Success(pr) => Future.successful(pr.paymentHash) } yield peerinfos)
case _ => Try(BinaryData(identifier)) match { case "channels" => req.params match {
case Success(s) => Future.successful(s) case Nil =>
case _ => Future.failed(new IllegalArgumentException("payment identifier must be a payment request or a payment hash")) 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'"))
} }
} case _ => reject(UnknownParamsRejection(req.id, "no arguments or [remoteNodeId]"))
found <- (paymentHandler ? CheckPayment(paymentHash)).map(found => new JBool(found.asInstanceOf[Boolean])) }
} yield found) case "channel" => req.params match {
case _ => reject(UnknownParamsRejection(req.id, "[paymentHash] or [paymentRequest]")) 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 // global network methods
case _ => reject(UnknownMethodRejection(req.id)) 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( def help = List(
"connect (uri): open a secure connection to a lightning node", "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", "peers: list existing local peers",
"channels: list existing local channels", "channels: list existing local channels",
"channels (nodeId): list existing local channels to a particular nodeId", "channels (nodeId): list existing local channels to a particular nodeId",
"channel (channelId): retrieve detailed information about a given channel", "channel (channelId): retrieve detailed information about a given channel",
"allnodes: list all known nodes", "allnodes: list all known nodes",
"allchannels: list all known channels", "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", "receive (amountMsat, description): generate a payment request for a given amount",
"send (amountMsat, paymentHash, nodeId): send a payment to a lightning node", "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): 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.crypto.Sphinx
import fr.acinq.eclair.transactions.CommitmentSpec import fr.acinq.eclair.transactions.CommitmentSpec
import fr.acinq.eclair.transactions.Transactions.CommitTx 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_GETSTATE extends Command
case object CMD_GETSTATEDATA extends Command case object CMD_GETSTATEDATA extends Command
case object CMD_GETINFO 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 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.Crypto.{PrivateKey, PublicKey, sha256, verifySignature}
import fr.acinq.bitcoin.{BinaryData, Crypto, LexicographicalOrdering} import fr.acinq.bitcoin.{BinaryData, Crypto, LexicographicalOrdering}
import fr.acinq.eclair.serializationResult import fr.acinq.eclair.serializationResult
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, LightningMessageCodecs, NodeAnnouncement} import fr.acinq.eclair.wire._
import scodec.bits.BitVector import scodec.bits.BitVector
import shapeless.HNil 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 = 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)))) 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 = 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)))) 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 = 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)))) 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) require(alias.size <= 32)
val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, "", addresses) val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, "", addresses)
val sig = Crypto.encodeSignature(Crypto.sign(witness, nodeSecret)) :+ 1.toByte 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 sender ! (d.updates ++ d.privateUpdates).values
stay stay
case Event('updatesMap, d) =>
sender ! (d.updates ++ d.privateUpdates)
stay
case Event('dot, d) => case Event('dot, d) =>
graph2dot(d.nodes, d.channels) pipeTo sender graph2dot(d.nodes, d.channels) pipeTo sender
stay stay
@ -549,7 +553,7 @@ object Router {
override def getComponentAttributes(nodeId: PublicKey): java.util.Map[String, String] = override def getComponentAttributes(nodeId: PublicKey): java.util.Map[String, String] =
nodes.get(nodeId) match { 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] 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) 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, bitcoinKey1: PublicKey,
bitcoinKey2: PublicKey) extends RoutingMessage 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, case class NodeAnnouncement(signature: BinaryData,
features: BinaryData, features: BinaryData,
timestamp: Long, timestamp: Long,
nodeId: PublicKey, nodeId: PublicKey,
rgbColor: (Byte, Byte, Byte), rgbColor: Color,
alias: String, alias: String,
// TODO: check address order + support padding data (type 0) // TODO: check address order + support padding data (type 0)
addresses: List[InetSocketAddress]) extends RoutingMessage addresses: List[InetSocketAddress]) extends RoutingMessage
@ -157,4 +161,4 @@ case class ChannelUpdate(signature: BinaryData,
case class PerHopPayload(channel_id: Long, case class PerHopPayload(channel_id: Long,
amtToForward: 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.NodeParams.BITCOIND
import fr.acinq.eclair.db.sqlite._ import fr.acinq.eclair.db.sqlite._
import fr.acinq.eclair.io.Peer import fr.acinq.eclair.io.Peer
import fr.acinq.eclair.wire.Color
import scala.concurrent.duration._ import scala.concurrent.duration._
@ -31,7 +32,7 @@ object TestConstants {
extendedPrivateKey = extendedPrivateKey, extendedPrivateKey = extendedPrivateKey,
privateKey = extendedPrivateKey.privateKey, privateKey = extendedPrivateKey.privateKey,
alias = "alice", alias = "alice",
color = (1: Byte, 2: Byte, 3: Byte), color = Color(1, 2, 3),
publicAddresses = new InetSocketAddress("localhost", 9731) :: Nil, publicAddresses = new InetSocketAddress("localhost", 9731) :: Nil,
globalFeatures = "", globalFeatures = "",
localFeatures = "00", localFeatures = "00",
@ -85,7 +86,7 @@ object TestConstants {
extendedPrivateKey = extendedPrivateKey, extendedPrivateKey = extendedPrivateKey,
privateKey = extendedPrivateKey.privateKey, privateKey = extendedPrivateKey.privateKey,
alias = "bob", alias = "bob",
color = (4: Byte, 5: Byte, 6: Byte), color = Color(4, 5, 6),
publicAddresses = new InetSocketAddress("localhost", 9732) :: Nil, publicAddresses = new InetSocketAddress("localhost", 9732) :: Nil,
globalFeatures = "", globalFeatures = "",
localFeatures = "00", // no announcement 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") { test("connect to an electrumx testnet server") {
probe.send(client, AddStatusListener(probe.ref)) probe.send(client, AddStatusListener(probe.ref))
probe.expectMsg(5 seconds, ElectrumReady) probe.expectMsg(15 seconds, ElectrumReady)
} }
test("get transaction") { 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.db.sqlite.SqliteNetworkDb
import fr.acinq.eclair.randomKey import fr.acinq.eclair.randomKey
import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.router.Announcements
import fr.acinq.eclair.wire.Color
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.scalatest.FunSuite import org.scalatest.FunSuite
import org.scalatest.junit.JUnitRunner import org.scalatest.junit.JUnitRunner
@ -27,9 +28,9 @@ class SqliteNetworkDbSpec extends FunSuite {
val sqlite = inmem val sqlite = inmem
val db = new SqliteNetworkDb(sqlite) 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_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", (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", (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) assert(db.listNodes().toSet === Set.empty)
db.addNode(node_1) db.addNode(node_1)

View file

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

View file

@ -54,7 +54,7 @@ class LightningMessageCodecsSpec extends FunSuite {
} }
test("encode/decode with rgb codec") { 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 val bin = rgb.encode(color).require
assert(bin === hex"2f ff 8e".toBitVector) assert(bin === hex"2f ff 8e".toBitVector)
val color2 = rgb.decode(bin).require.value 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 commit_sig = CommitSig(randomBytes(32), randomSignature, randomSignature :: randomSignature :: randomSignature :: Nil)
val revoke_and_ack = RevokeAndAck(randomBytes(32), scalar(0), point(1)) 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 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 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 announcement_signatures = AnnouncementSignatures(randomBytes(32), 42, randomSignature, randomSignature)
val ping = Ping(100, BinaryData("01" * 10)) val ping = Ping(100, BinaryData("01" * 10))

View file

@ -40,7 +40,7 @@
</children> </children>
</VBox> </VBox>
<Label onMouseClicked="#openGithubPage" VBox.vgrow="NEVER" styleClass="link" <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" <Button fx:id="closeButton" VBox.vgrow="NEVER" mnemonicParsing="false" onAction="#closeAndKill"
text="Close" cancelButton="true"/> text="Close" cancelButton="true"/>
</children> </children>

View file

@ -22,6 +22,8 @@ import grizzled.slf4j.Logging
import scala.concurrent.Promise import scala.concurrent.Promise
import scala.util.{Failure, Success} import scala.util.{Failure, Success}
import scala.concurrent.ExecutionContext.Implicits.global
/** /**
* Created by PM on 16/08/2016. * 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, "Eclair is still in alpha, and under heavy development. Last update was not backward compatible."))
notifyPreloader(new AppNotification(InfoAppNotification, "Please reset your datadir.")) notifyPreloader(new AppNotification(InfoAppNotification, "Please reset your datadir."))
case t: Throwable => 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 = { 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[ZMQEvent])
setup.system.eventStream.subscribe(guiUpdater, classOf[ElectrumEvent]) setup.system.eventStream.subscribe(guiUpdater, classOf[ElectrumEvent])
pKit.completeWith(setup.bootstrap) pKit.completeWith(setup.bootstrap)
import scala.concurrent.ExecutionContext.Implicits.global
pKit.future.onComplete { pKit.future.onComplete {
case Success(_) => case Success(_) =>
Platform.runLater(new Runnable { Platform.runLater(new Runnable {

View file

@ -59,7 +59,7 @@ class FxPreloader extends Preloader with Logging {
info match { info match {
case n: ErrorNotification => case n: ErrorNotification =>
logger.debug(s"Preloader error notification => ${n.getDetails}") 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(_.addError(n.getDetails))
controller.map(_.showErrorBox) controller.map(_.showErrorBox)
case n: AppNotification => 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) def call(pn: CellDataFeatures[NodeAnnouncement, String]) = new SimpleStringProperty(pn.getValue.alias)
}) })
networkNodesRGBColumn.setCellValueFactory(new Callback[CellDataFeatures[NodeAnnouncement, String], ObservableValue[String]]() { networkNodesRGBColumn.setCellValueFactory(new Callback[CellDataFeatures[NodeAnnouncement, String], ObservableValue[String]]() {
def call(pn: CellDataFeatures[NodeAnnouncement, String]) = new SimpleStringProperty( def call(pn: CellDataFeatures[NodeAnnouncement, String]) = new SimpleStringProperty(pn.getValue.rgbColor.toString)
s"rgb(${new Integer(pn.getValue.rgbColor._1 & 0xFF)}, ${new Integer(pn.getValue.rgbColor._2 & 0xFF)}, ${new Integer(pn.getValue.rgbColor._3 & 0xFF)})")
}) })
networkNodesIPColumn.setCellValueFactory(new Callback[CellDataFeatures[NodeAnnouncement, String], ObservableValue[String]]() { networkNodesIPColumn.setCellValueFactory(new Callback[CellDataFeatures[NodeAnnouncement, String], ObservableValue[String]]() {
def call(pn: CellDataFeatures[NodeAnnouncement, String]) = { def call(pn: CellDataFeatures[NodeAnnouncement, String]) = {
@ -313,7 +312,7 @@ class MainController(val handlers: Handlers, val hostServices: HostServices) ext
// init status bar // init status bar
labelNodeId.setText(s"${setup.nodeParams.privateKey.publicKey}") labelNodeId.setText(s"${setup.nodeParams.privateKey.publicKey}")
labelAlias.setText(s"${setup.nodeParams.alias}") 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")}") labelApi.setText(s"${setup.config.getInt("api.port")}")
labelServer.setText(s"${setup.config.getInt("server.port")}") labelServer.setText(s"${setup.config.getInt("server.port")}")
bitcoinVersion.setText(s"v0.0.0") bitcoinVersion.setText(s"v0.0.0")