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:
parent
a00fd96ca6
commit
a1d69af597
21 changed files with 380 additions and 265 deletions
54
README.md
54
README.md
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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")),
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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)
|
||||||
|
}))
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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") {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 =>
|
||||||
|
|
|
@ -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")
|
||||||
|
|
Loading…
Add table
Reference in a new issue