mirror of
https://github.com/ACINQ/eclair.git
synced 2025-02-23 06:35:11 +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
|
||||
|
||||
:warning: **Those are valid for the most up-to-date, unreleased, version of eclair. Here are the [instructions for Eclair 0.2-alpha5](https://github.com/ACINQ/eclair/blob/v0.2-alpha5/README.md#installation)**.
|
||||
:warning: **Those are valid for the most up-to-date, unreleased, version of eclair. Here are the [instructions for Eclair 0.2-alpha8](https://github.com/ACINQ/eclair/blob/v0.2-alpha8/README.md#installation)**.
|
||||
|
||||
### Configuring Bitcoin Core
|
||||
|
||||
|
@ -93,7 +93,10 @@ Here are some of the most common options:
|
|||
name | description | default value
|
||||
-----------------------------|---------------------------|--------------
|
||||
eclair.server.port | Lightning TCP port | 9735
|
||||
eclair.api.enabled | Enable/disable the API | false. By default the API is disabled. If you want to enable it, you must set a user/password.
|
||||
eclair.api.port | API HTTP port | 8080
|
||||
eclair.api.user | API user (BASIC) | "" (must be set if the API is enabled)
|
||||
eclair.api.password | API password (BASIC) | "" (must be set if the API is enabled)
|
||||
eclair.bitcoind.rpcuser | Bitcoin Core RPC user | foo
|
||||
eclair.bitcoind.rpcpassword | Bitcoin Core RPC password | bar
|
||||
eclair.bitcoind.zmq | Bitcoin Core ZMQ address | tcp://127.0.0.1:29000
|
||||
|
@ -121,27 +124,30 @@ java -Declair.datadir=/tmp/node1 -jar eclair-node-gui-<version>-<commit_id>.jar
|
|||
|
||||
## JSON-RPC API
|
||||
|
||||
method | params | description
|
||||
-------------|-----------------------------------------------|-----------------------------------------------------------
|
||||
getinfo | | return basic node information (id, chain hash, current block height)
|
||||
connect | uri | open a secure connection to a lightning node
|
||||
open | nodeId, fundingSatoshis, pushMsat | open a channel with another lightning node
|
||||
peers | | list existing local peers
|
||||
channels | | list existing local channels
|
||||
channels | nodeId | list existing local channels opened with a particular nodeId
|
||||
channel | channelId | retrieve detailed information about a given channel
|
||||
allnodes | | list all known nodes
|
||||
allchannels | | list all known channels
|
||||
receive | description | generate a payment request without a required amount (can be useful for donations)
|
||||
receive | amountMsat, description | generate a payment request for a given amount
|
||||
send | amountMsat, paymentHash, nodeId | send a payment to a lightning node
|
||||
send | paymentRequest | send a payment to a lightning node using a BOLT11 payment request
|
||||
send | paymentRequest, amountMsat | send a payment to a lightning node using a BOLT11 payment request and a custom amount
|
||||
checkpayment| paymentHash | returns true if the payment has been received, false otherwise
|
||||
checkpayment| paymentRequest | returns true if the payment has been received, false otherwise
|
||||
close | channelId | close a channel
|
||||
close | channelId, scriptPubKey (optional) | close a channel and send the funds to the given scriptPubKey
|
||||
help | | display available methods
|
||||
method | params | description
|
||||
------------- |------------------------------------------------------------|-----------------------------------------------------------
|
||||
getinfo | | return basic node information (id, chain hash, current block height)
|
||||
connect | nodeId, host, port | open a secure connection to a lightning node
|
||||
connect | uri | open a secure connection to a lightning node
|
||||
open | nodeId, fundingSatoshis, pushMsat = 0, channelFlags = 0x01 | open a channel with another lightning node, by default push = 0 and channel is announced
|
||||
peers | | list existing local peers
|
||||
channels | | list existing local channels
|
||||
channels | nodeId | list existing local channels opened with a particular nodeId
|
||||
channel | channelId | retrieve detailed information about a given channel
|
||||
allnodes | | list all known nodes
|
||||
allchannels | | list all known channels
|
||||
allupdates | | list all channels updates
|
||||
allupdates | nodeId | list all channels updates for this nodeId
|
||||
receive | description | generate a payment request without a required amount (can be useful for donations)
|
||||
receive | amountMsat, description | generate a payment request for a given amount
|
||||
send | amountMsat, paymentHash, nodeId | send a payment to a lightning node
|
||||
send | paymentRequest | send a payment to a lightning node using a BOLT11 payment request
|
||||
send | paymentRequest, amountMsat | send a payment to a lightning node using a BOLT11 payment request and a custom amount
|
||||
checkpayment | paymentHash | returns true if the payment has been received, false otherwise
|
||||
checkpayment | paymentRequest | returns true if the payment has been received, false otherwise
|
||||
close | channelId | close a channel
|
||||
close | channelId, scriptPubKey | close a channel and send the funds to the given scriptPubKey
|
||||
help | | display available methods
|
||||
|
||||
## Docker
|
||||
|
||||
|
@ -161,8 +167,8 @@ docker run -ti --rm -v "/path_on_host:/data" -e "JAVA_OPTS=-Declair.printToConso
|
|||
|
||||
|
||||
## Resources
|
||||
- [1] [The Bitcoin Lightning Network: Scalable Off-Chain Instant Payments](https://lightning.network/lightning-network-paper.pdf) by Joseph Poon and Thaddeus Dryja
|
||||
- [2] [Reaching The Ground With Lightning](https://github.com/ElementsProject/lightning/raw/master/doc/deployable-lightning.pdf) by Rusty Russell
|
||||
- [1] [The Bitcoin Lightning Network: Scalable Off-Chain Instant Payments](https://lightning.network/lightning-network-paper.pdf) by Joseph Poon and Thaddeus Dryja
|
||||
- [2] [Reaching The Ground With Lightning](https://github.com/ElementsProject/lightning/raw/master/doc/deployable-lightning.pdf) by Rusty Russell
|
||||
- [3] [Lightning Network Explorer](https://explorer.acinq.co) - Explore testnet LN nodes you can connect to
|
||||
|
||||
[Amiko-Pay]: https://github.com/cornwarecjp/amiko-pay
|
||||
|
|
|
@ -1,59 +1,95 @@
|
|||
#!/bin/bash
|
||||
|
||||
[ -z "$1" ] && (
|
||||
echo "usage: "
|
||||
echo " eclair-cli help"
|
||||
) && exit 1
|
||||
# Check if jq is installed. If not, display instructions and abort program
|
||||
command -v jq >/dev/null 2>&1 || { echo -e "This tool requires jq.\nFor installation instructions, visit https://stedolan.github.io/jq/download/.\n\nAborting..."; exit 1; }
|
||||
|
||||
URL="http://localhost:8080"
|
||||
CURL_OPTS="-sS -X POST -H \"Content-Type: application/json\""
|
||||
FULL_OUTPUT='false'
|
||||
URL='http://localhost:8080'
|
||||
PASSWORD=''
|
||||
|
||||
# -------------------- METHODS
|
||||
|
||||
displayhelp() {
|
||||
echo -e "Usage: eclair-cli [OPTION]... [COMMAND]
|
||||
Client for an eclair node.
|
||||
|
||||
With COMMAND is one of the command listed by \e[01;33meclair-cli help\e[0m.
|
||||
|
||||
-p <password> api's password
|
||||
-a <address> Override the api URL with <address>
|
||||
-v Outputs full json returned by the API
|
||||
|
||||
Examples:
|
||||
eclair-cli -a localhost:1234 peers list the peers
|
||||
eclair-cli close 006fb... closes the channel with id 006fb...
|
||||
|
||||
Note: Uses the json-rpc api exposed by the node on localhost:8080. Make sure the api is enabled.
|
||||
Full documentation at: <https://github.com/ACINQ/eclair>"
|
||||
}
|
||||
|
||||
# Executes a JSON RPC call to a node listening on ${URL}
|
||||
call() {
|
||||
jqexp='if .error == null then .result else .error.message end'
|
||||
# override default jq parsing expression
|
||||
if [ $# -ge 3 ] && [ ${FULL_OUTPUT} == "false" ]; then jqexp=${3}; fi
|
||||
# set password
|
||||
if [ -z ${PASSWORD} ]; then auth="eclair-cli";
|
||||
else auth="eclair-cli:"${PASSWORD}; fi
|
||||
eval curl "--user ${auth} --silent --show-error -X POST -H \"Content-Type: application/json\" -d '{ \"method\": \"'${1}'\", \"params\": '${2}' }' ${URL}" | jq -r "$jqexp"
|
||||
}
|
||||
|
||||
# get script options
|
||||
while getopts 'vu:p:a:' flag; do
|
||||
case "${flag}" in
|
||||
p) PASSWORD="${OPTARG}" ;;
|
||||
a) URL="${OPTARG}" ;;
|
||||
v) FULL_OUTPUT="true" ;;
|
||||
*) echo -e "\nAborting..."; exit 1; ;;
|
||||
esac
|
||||
done
|
||||
|
||||
shift $(($OPTIND - 1))
|
||||
|
||||
# assigning JSON RPC method and params values from arguments
|
||||
METHOD=${1}
|
||||
shift 1
|
||||
|
||||
# Create a JSON Array containing the remaining program args as QUOTED STRINGS, separated with a `,` character
|
||||
PARAMS=""
|
||||
i=1
|
||||
for arg in "${@}"; do
|
||||
if [ $i -eq 1 ]; then PARAMS=$(printf '"%s"' "$arg");
|
||||
else PARAMS=$(printf '%s,"%s"' "$PARAMS" "$arg");
|
||||
fi
|
||||
let "i++"
|
||||
done;
|
||||
PARAMS="[${PARAMS}]"
|
||||
|
||||
# Whatever the arguments provided to eclair-cli, a call to the API will be sent. Let it fail!
|
||||
case ${METHOD}_${#} in
|
||||
""_*) displayhelp ;;
|
||||
"help"*) displayhelp
|
||||
echo -e "\nAvailable commands:\n"
|
||||
call "help" [] ;;
|
||||
|
||||
"connect_3") call ${METHOD} "'$(printf '["%s","%s",%s]' "${1}" "${2}" "${3}")'" ;; # ${3} is numeric
|
||||
|
||||
"open_4") call ${METHOD} "'$(printf '["%s",%s,%s,%s]' "${1}" "${2}" "${3}" "${4}")'" ;; # ${2} ${3} ${4} are numeric (funding, push, flags)
|
||||
"open_3") call ${METHOD} "'$(printf '["%s",%s,%s]' "${1}" "${2}" "${3}")'" ;; # ${2} ${3} are numeric (funding, push)
|
||||
"open_2") call ${METHOD} "'$(printf '["%s",%s]' "${1}" "${2}")'" ;; # ${2} is numeric (funding)
|
||||
|
||||
"receive_2") call ${METHOD} "'$(printf '[%s,"%s"]' "${1}" "${2}")'" ;; # ${1} is numeric (amount to receive)
|
||||
|
||||
"channel_"*) call ${METHOD} "'${PARAMS}'" "if .error != null then .error.message else .result | { nodeId, shortChannelId: .data.shortChannelId, channelId, state, balanceMsat: .data.commitments.localCommit.spec.toLocalMsat, capacitySat: .data.commitments.commitInput.txOut.amount.amount } end" ;;
|
||||
|
||||
"send_3") call ${METHOD} "'$(printf '[%s,"%s","%s"]' "${1}" "${2}" "${3}")'" ;; # ${1} is numeric (amount of the payment)
|
||||
"send_2") call ${METHOD} "'$(printf '["%s",%s]' "${1}" "${2}")'" ;; # ${2} is numeric (amount overriding the payment request)
|
||||
|
||||
*) # Default case.
|
||||
# Sends the method and, for parameters, use the JSON table containing the remaining args.
|
||||
#
|
||||
# NOTE: Arguments will be sent as QUOTED STRING so if this particular API call requires an INT param,
|
||||
# this call will fail. In that case, a specific rule for that method MUST be set and the ${PARAMS} JSON array can not be used.
|
||||
call ${METHOD} "'${PARAMS}'" ;;
|
||||
|
||||
case $1 in
|
||||
"help")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"help\", \"params\" : [] }' $URL" | jq -r ".result[]"
|
||||
;;
|
||||
"getinfo")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"getinfo\", \"params\" : [] }' $URL" | jq ".result"
|
||||
;;
|
||||
"channels")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"channels\", \"params\" : [] }' $URL" | jq ".result[]"
|
||||
;;
|
||||
"channels")
|
||||
if [ $# -ge 2 ]
|
||||
then
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"channels\", \"params\" : [\"${2?"missing node id"}\"] }' $URL" | jq ".result[]"
|
||||
else
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"channels\", \"params\" : [] }' $URL" | jq ".result[]"
|
||||
fi
|
||||
;;
|
||||
"channel")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"channel\", \"params\" : [\"${2?"missing channel id"}\"] }' $URL" | jq ".result | { nodeid, channelId, state, balanceMsat: .data.commitments.localCommit.spec.toLocalMsat, capacitySat: .data.commitments.commitInput.txOut.amount.amount }"
|
||||
;;
|
||||
"connect")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"open\", \"params\" : [\"${2?"missing uri"}\"] }' $URL" | jq -r "if .error == null then .result else .error.message end"
|
||||
;;
|
||||
"open")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"open\", \"params\" : [\"${2?"missing node id"}\", ${3?"missing amount (sat)"}, ${4?"missing push amount (msat)"}] }' $URL" | jq -r "if .error == null then .result else .error.message end"
|
||||
;;
|
||||
"close")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"close\", \"params\" : [\"${2?"missing channel id"}\"] }' $URL"
|
||||
;;
|
||||
"receive")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"receive\", \"params\" : [${2?"missing amount"}, \"${3?"missing description"}\"] }' $URL" | jq -r "if .error == null then .result else .error.message end"
|
||||
;;
|
||||
"send")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"send\", \"params\" : [\"${2?"missing request"}\"] }' $URL" | jq -r "if .error == null then .result else .error.message end"
|
||||
;;
|
||||
"allnodes")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"allnodes\", \"params\" : [] }' $URL" | jq ".result"
|
||||
;;
|
||||
"allchannels")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"allchannels\", \"params\" : [] }' $URL" | jq ".result"
|
||||
;;
|
||||
"peers")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"peers\", \"params\" : [] }' $URL" | jq ".result"
|
||||
;;
|
||||
"checkpayment")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"checkpayment\", \"params\" : [\"${2?"missing payment request or payment hash"}\"] }' $URL" | jq ".result"
|
||||
;;
|
||||
esac
|
||||
|
|
|
@ -9,8 +9,10 @@ eclair {
|
|||
}
|
||||
|
||||
api {
|
||||
enabled = false // disabled by default for security reasons
|
||||
binding-ip = "127.0.0.1"
|
||||
port = 8080
|
||||
password = "" // password for basic auth, must be non empty if json-rpc api is enabled
|
||||
}
|
||||
|
||||
watcher-type = "bitcoind" // other *experimental* values include "bitcoinj" or "electrum"
|
||||
|
|
|
@ -14,6 +14,7 @@ import fr.acinq.eclair.NodeParams.WatcherType
|
|||
import fr.acinq.eclair.channel.Channel
|
||||
import fr.acinq.eclair.db._
|
||||
import fr.acinq.eclair.db.sqlite._
|
||||
import fr.acinq.eclair.wire.Color
|
||||
|
||||
import scala.collection.JavaConversions._
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
|
@ -24,7 +25,7 @@ import scala.concurrent.duration.FiniteDuration
|
|||
case class NodeParams(extendedPrivateKey: ExtendedPrivateKey,
|
||||
privateKey: PrivateKey,
|
||||
alias: String,
|
||||
color: (Byte, Byte, Byte),
|
||||
color: Color,
|
||||
publicAddresses: List[InetSocketAddress],
|
||||
globalFeatures: BinaryData,
|
||||
localFeatures: BinaryData,
|
||||
|
@ -131,7 +132,7 @@ object NodeParams {
|
|||
extendedPrivateKey = extendedPrivateKey,
|
||||
privateKey = extendedPrivateKey.privateKey,
|
||||
alias = config.getString("node-alias").take(32),
|
||||
color = (color.data(0), color.data(1), color.data(2)),
|
||||
color = Color(color.data(0), color.data(1), color.data(2)),
|
||||
publicAddresses = config.getStringList("server.public-ips").toList.map(ip => new InetSocketAddress(ip, config.getInt("server.port"))),
|
||||
globalFeatures = BinaryData(config.getString("global-features")),
|
||||
localFeatures = BinaryData(config.getString("local-features")),
|
||||
|
|
|
@ -37,9 +37,9 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act
|
|||
logger.info(s"hello!")
|
||||
logger.info(s"version=${getClass.getPackage.getImplementationVersion} commit=${getClass.getPackage.getSpecificationVersion}")
|
||||
|
||||
val config = NodeParams.loadConfiguration(datadir, overrideDefaults)
|
||||
val nodeParams = NodeParams.makeNodeParams(datadir, config)
|
||||
val chain = config.getString("chain")
|
||||
val config: Config = NodeParams.loadConfiguration(datadir, overrideDefaults)
|
||||
val nodeParams: NodeParams = NodeParams.makeNodeParams(datadir, config)
|
||||
val chain: String = config.getString("chain")
|
||||
|
||||
// early checks
|
||||
DBCompatChecker.checkDBCompatibility(nodeParams)
|
||||
|
@ -172,31 +172,43 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act
|
|||
server = server,
|
||||
wallet = wallet)
|
||||
|
||||
val api = new Service {
|
||||
|
||||
override def getInfoResponse: Future[GetInfoResponse] = Future.successful(GetInfoResponse(nodeId = nodeParams.privateKey.publicKey, alias = nodeParams.alias, port = config.getInt("server.port"), chainHash = nodeParams.chainHash, blockHeight = Globals.blockCount.intValue()))
|
||||
|
||||
override def appKit = kit
|
||||
}
|
||||
val httpBound = Http().bindAndHandle(api.route, config.getString("api.binding-ip"), config.getInt("api.port")).recover {
|
||||
case _: BindFailedException => throw TCPBindException(config.getInt("api.port"))
|
||||
}
|
||||
|
||||
val zmqTimeout = after(5 seconds, using = system.scheduler)(Future.failed(BitcoinZMQConnectionTimeoutException))
|
||||
val tcpTimeout = after(5 seconds, using = system.scheduler)(Future.failed(TCPBindException(config.getInt("server.port"))))
|
||||
val httpTimeout = after(5 seconds, using = system.scheduler)(Future.failed(TCPBindException(config.getInt("api.port"))))
|
||||
|
||||
for {
|
||||
_ <- Future.firstCompletedOf(zmqConnected.future :: zmqTimeout :: Nil)
|
||||
_ <- Future.firstCompletedOf(tcpBound.future :: tcpTimeout :: Nil)
|
||||
_ <- Future.firstCompletedOf(httpBound :: httpTimeout :: Nil)
|
||||
_ <- if (config.getBoolean("api.enabled")) {
|
||||
logger.info(s"json-rpc api enabled on port=${config.getInt("api.port")}")
|
||||
val api = new Service {
|
||||
override val password = {
|
||||
val p = config.getString("api.password")
|
||||
if (p.isEmpty) throw EmptyAPIPasswordException else p
|
||||
}
|
||||
|
||||
override def getInfoResponse: Future[GetInfoResponse] = Future.successful(
|
||||
GetInfoResponse(nodeId = nodeParams.privateKey.publicKey,
|
||||
alias = nodeParams.alias,
|
||||
port = config.getInt("server.port"),
|
||||
chainHash = nodeParams.chainHash,
|
||||
blockHeight = Globals.blockCount.intValue()))
|
||||
|
||||
override def appKit: Kit = kit
|
||||
}
|
||||
val httpBound = Http().bindAndHandle(api.route, config.getString("api.binding-ip"), config.getInt("api.port")).recover {
|
||||
case _: BindFailedException => throw TCPBindException(config.getInt("api.port"))
|
||||
}
|
||||
val httpTimeout = after(5 seconds, using = system.scheduler)(Future.failed(TCPBindException(config.getInt("api.port"))))
|
||||
Future.firstCompletedOf(httpBound :: httpTimeout :: Nil)
|
||||
} else {
|
||||
Future.successful(logger.info("json-rpc api is disabled"))
|
||||
}
|
||||
} yield kit
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// @formatter:off
|
||||
sealed trait Bitcoin
|
||||
case class Bitcoind(extendedBitcoinClient: ExtendedBitcoinClient) extends Bitcoin
|
||||
|
@ -219,3 +231,5 @@ case class Kit(nodeParams: NodeParams,
|
|||
case object BitcoinZMQConnectionTimeoutException extends RuntimeException("could not connect to bitcoind using zeromq")
|
||||
|
||||
case object BitcoinRPCConnectionException extends RuntimeException("could not connect to bitcoind using json-rpc")
|
||||
|
||||
case object EmptyAPIPasswordException extends RuntimeException("must set a user/password for the json-rpc api")
|
||||
|
|
|
@ -3,17 +3,18 @@ package fr.acinq.eclair.api
|
|||
import java.net.InetSocketAddress
|
||||
|
||||
import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, PublicKey, Scalar}
|
||||
import fr.acinq.bitcoin.{BinaryData, OutPoint, Transaction}
|
||||
import fr.acinq.bitcoin.{BinaryData, OutPoint}
|
||||
import fr.acinq.eclair.channel.State
|
||||
import fr.acinq.eclair.crypto.ShaChain
|
||||
import fr.acinq.eclair.transactions.Transactions.TransactionWithInputInfo
|
||||
import org.json4s.{CustomKeySerializer, CustomSerializer}
|
||||
import fr.acinq.eclair.wire.Color
|
||||
import org.json4s.JsonAST.{JNull, JString}
|
||||
import org.json4s.{CustomKeySerializer, CustomSerializer}
|
||||
|
||||
/**
|
||||
* Created by PM on 28/01/2016.
|
||||
*/
|
||||
class BinaryDataSerializer extends CustomSerializer[BinaryData](format => ( {
|
||||
class BinaryDataSerializer extends CustomSerializer[BinaryData](format => ({
|
||||
case JString(hex) if (false) => // NOT IMPLEMENTED
|
||||
???
|
||||
}, {
|
||||
|
@ -21,7 +22,7 @@ class BinaryDataSerializer extends CustomSerializer[BinaryData](format => ( {
|
|||
}
|
||||
))
|
||||
|
||||
class StateSerializer extends CustomSerializer[State](format => ( {
|
||||
class StateSerializer extends CustomSerializer[State](format => ({
|
||||
case JString(x) if (false) => // NOT IMPLEMENTED
|
||||
???
|
||||
}, {
|
||||
|
@ -29,7 +30,7 @@ class StateSerializer extends CustomSerializer[State](format => ( {
|
|||
}
|
||||
))
|
||||
|
||||
class ShaChainSerializer extends CustomSerializer[ShaChain](format => ( {
|
||||
class ShaChainSerializer extends CustomSerializer[ShaChain](format => ({
|
||||
case JString(x) if (false) => // NOT IMPLEMENTED
|
||||
???
|
||||
}, {
|
||||
|
@ -37,7 +38,7 @@ class ShaChainSerializer extends CustomSerializer[ShaChain](format => ( {
|
|||
}
|
||||
))
|
||||
|
||||
class PublicKeySerializer extends CustomSerializer[PublicKey](format => ( {
|
||||
class PublicKeySerializer extends CustomSerializer[PublicKey](format => ({
|
||||
case JString(x) if (false) => // NOT IMPLEMENTED
|
||||
???
|
||||
}, {
|
||||
|
@ -45,7 +46,7 @@ class PublicKeySerializer extends CustomSerializer[PublicKey](format => ( {
|
|||
}
|
||||
))
|
||||
|
||||
class PrivateKeySerializer extends CustomSerializer[PrivateKey](format => ( {
|
||||
class PrivateKeySerializer extends CustomSerializer[PrivateKey](format => ({
|
||||
case JString(x) if (false) => // NOT IMPLEMENTED
|
||||
???
|
||||
}, {
|
||||
|
@ -53,7 +54,7 @@ class PrivateKeySerializer extends CustomSerializer[PrivateKey](format => ( {
|
|||
}
|
||||
))
|
||||
|
||||
class PointSerializer extends CustomSerializer[Point](format => ( {
|
||||
class PointSerializer extends CustomSerializer[Point](format => ({
|
||||
case JString(x) if (false) => // NOT IMPLEMENTED
|
||||
???
|
||||
}, {
|
||||
|
@ -61,7 +62,7 @@ class PointSerializer extends CustomSerializer[Point](format => ( {
|
|||
}
|
||||
))
|
||||
|
||||
class ScalarSerializer extends CustomSerializer[Scalar](format => ( {
|
||||
class ScalarSerializer extends CustomSerializer[Scalar](format => ({
|
||||
case JString(x) if (false) => // NOT IMPLEMENTED
|
||||
???
|
||||
}, {
|
||||
|
@ -69,7 +70,7 @@ class ScalarSerializer extends CustomSerializer[Scalar](format => ( {
|
|||
}
|
||||
))
|
||||
|
||||
class TransactionWithInputInfoSerializer extends CustomSerializer[TransactionWithInputInfo](format => ( {
|
||||
class TransactionWithInputInfoSerializer extends CustomSerializer[TransactionWithInputInfo](format => ({
|
||||
case JString(x) if (false) => // NOT IMPLEMENTED
|
||||
???
|
||||
}, {
|
||||
|
@ -77,7 +78,7 @@ class TransactionWithInputInfoSerializer extends CustomSerializer[TransactionWit
|
|||
}
|
||||
))
|
||||
|
||||
class InetSocketAddressSerializer extends CustomSerializer[InetSocketAddress](format => ( {
|
||||
class InetSocketAddressSerializer extends CustomSerializer[InetSocketAddress](format => ({
|
||||
case JString(x) if (false) => // NOT IMPLEMENTED
|
||||
???
|
||||
}, {
|
||||
|
@ -85,7 +86,7 @@ class InetSocketAddressSerializer extends CustomSerializer[InetSocketAddress](fo
|
|||
}
|
||||
))
|
||||
|
||||
class OutPointKeySerializer extends CustomKeySerializer[OutPoint](format => ( {
|
||||
class OutPointKeySerializer extends CustomKeySerializer[OutPoint](format => ({
|
||||
case x: String =>
|
||||
val Array(k, v) = x.split(":")
|
||||
OutPoint(BinaryData(k), v.toLong)
|
||||
|
@ -93,3 +94,10 @@ class OutPointKeySerializer extends CustomKeySerializer[OutPoint](format => ( {
|
|||
case x: OutPoint => s"${x.hash}:${x.index}"
|
||||
}
|
||||
))
|
||||
|
||||
class ColorSerializer extends CustomSerializer[Color](format => ({
|
||||
case JString(x) if (false) => // NOT IMPLEMENTED
|
||||
???
|
||||
}, {
|
||||
case c: Color => JString(c.toString)
|
||||
}))
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
package fr.acinq.eclair.api
|
||||
|
||||
import akka.actor.ActorRef
|
||||
import akka.http.scaladsl.model.HttpMethods._
|
||||
import akka.http.scaladsl.model.StatusCodes
|
||||
import akka.http.scaladsl.model.StatusCodes.{register => _}
|
||||
import akka.http.scaladsl.model.headers.CacheDirectives.{`max-age`, `no-store`, public}
|
||||
import akka.http.scaladsl.model.headers.HttpOriginRange.*
|
||||
import akka.http.scaladsl.model.headers._
|
||||
import akka.http.scaladsl.server.Directives._
|
||||
import akka.http.scaladsl.server._
|
||||
import akka.http.scaladsl.server.directives.Credentials
|
||||
import akka.http.scaladsl.server.directives.RouteDirectives.reject
|
||||
import akka.http.scaladsl.server.{ExceptionHandler, Rejection, RejectionHandler, Route}
|
||||
import akka.pattern.ask
|
||||
import akka.util.Timeout
|
||||
import de.heikoseeberger.akkahttpjson4s.Json4sSupport
|
||||
|
@ -20,9 +18,9 @@ import fr.acinq.eclair.Kit
|
|||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.io.Peer.{GetPeerInfo, PeerInfo}
|
||||
import fr.acinq.eclair.io.{NodeURI, Peer}
|
||||
import fr.acinq.eclair.payment.{PaymentRequest, PaymentResult, ReceivePayment, SendPayment}
|
||||
import fr.acinq.eclair.payment._
|
||||
import fr.acinq.eclair.wire.{ChannelAnnouncement, NodeAnnouncement}
|
||||
import fr.acinq.eclair.payment.{PaymentRequest, PaymentResult, ReceivePayment, SendPayment, _}
|
||||
import fr.acinq.eclair.router.ChannelDesc
|
||||
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement}
|
||||
import grizzled.slf4j.Logging
|
||||
import org.json4s.JsonAST.{JBool, JInt, JString}
|
||||
import org.json4s.{JValue, jackson}
|
||||
|
@ -37,14 +35,15 @@ case class Error(code: Int, message: String)
|
|||
case class JsonRPCRes(result: AnyRef, error: Option[Error], id: String)
|
||||
case class Status(node_id: String)
|
||||
case class GetInfoResponse(nodeId: PublicKey, alias: String, port: Int, chainHash: BinaryData, blockHeight: Int)
|
||||
case class LocalChannelInfo(nodeId: BinaryData, channelId: BinaryData, state: String)
|
||||
case class ChannelInfo(shortChannelId: String, nodeId1: PublicKey, nodeId2: PublicKey)
|
||||
trait RPCRejection extends Rejection {
|
||||
def requestId: String
|
||||
}
|
||||
final case class UnknownMethodRejection(requestId: String) extends RPCRejection
|
||||
final case class UnknownParamsRejection(requestId: String, message: String) extends RPCRejection
|
||||
final case class NotFoundRejection(requestId: String) extends RPCRejection
|
||||
final case class ValidationRejection(requestId: String, message: String) extends RPCRejection
|
||||
final case class RpcValidationRejection(requestId: String, message: String) extends RPCRejection
|
||||
final case class ExceptionRejection(requestId: String, message: String) extends RPCRejection
|
||||
// @formatter:on
|
||||
|
||||
trait Service extends Logging {
|
||||
|
@ -52,31 +51,38 @@ trait Service extends Logging {
|
|||
implicit def ec: ExecutionContext = ExecutionContext.Implicits.global
|
||||
|
||||
implicit val serialization = jackson.Serialization
|
||||
implicit val formats = org.json4s.DefaultFormats + new BinaryDataSerializer + new StateSerializer + new ShaChainSerializer + new PublicKeySerializer + new PrivateKeySerializer + new ScalarSerializer + new PointSerializer + new TransactionWithInputInfoSerializer + new InetSocketAddressSerializer + new OutPointKeySerializer
|
||||
implicit val formats = org.json4s.DefaultFormats + new BinaryDataSerializer + new StateSerializer + new ShaChainSerializer + new PublicKeySerializer + new PrivateKeySerializer + new ScalarSerializer + new PointSerializer + new TransactionWithInputInfoSerializer + new InetSocketAddressSerializer + new OutPointKeySerializer + new ColorSerializer
|
||||
implicit val timeout = Timeout(30 seconds)
|
||||
implicit val shouldWritePretty: ShouldWritePretty = ShouldWritePretty.True
|
||||
|
||||
import Json4sSupport.{marshaller, unmarshaller}
|
||||
|
||||
def password: String
|
||||
|
||||
def appKit: Kit
|
||||
|
||||
val customHeaders = `Access-Control-Allow-Origin`(*) ::
|
||||
`Access-Control-Allow-Headers`("Content-Type, Authorization") ::
|
||||
`Access-Control-Allow-Methods`(PUT, GET, POST, DELETE, OPTIONS) ::
|
||||
`Cache-Control`(public, `no-store`, `max-age`(0)) ::
|
||||
`Access-Control-Allow-Headers`("x-requested-with") :: Nil
|
||||
def userPassAuthenticator(credentials: Credentials): Option[String] = credentials match {
|
||||
case p@Credentials.Provided(id) if p.verify(password) => Some(id)
|
||||
case _ =>
|
||||
// TODO deter brute force with a forced delay
|
||||
None
|
||||
}
|
||||
|
||||
val customHeaders = `Access-Control-Allow-Headers`("Content-Type, Authorization") ::
|
||||
`Access-Control-Allow-Methods`(POST) ::
|
||||
`Cache-Control`(public, `no-store`, `max-age`(0)) :: Nil
|
||||
|
||||
val myExceptionHandler = ExceptionHandler {
|
||||
case t: Throwable =>
|
||||
extractRequest { request =>
|
||||
extractRequest { _ =>
|
||||
logger.info(s"API call failed with cause=${t.getMessage}")
|
||||
complete(StatusCodes.InternalServerError, JsonRPCRes(null, Some(Error(StatusCodes.InternalServerError.intValue, t.getMessage)), request.asInstanceOf[JsonRPCBody].id))
|
||||
complete(StatusCodes.InternalServerError, JsonRPCRes(null, Some(Error(StatusCodes.InternalServerError.intValue, t.getMessage)), "-1"))
|
||||
}
|
||||
}
|
||||
|
||||
def completeRpcFuture(requestId: String, future: Future[AnyRef]): Route = onComplete(future) {
|
||||
case Success(s) => completeRpc(requestId, s)
|
||||
case Failure(_) => reject
|
||||
case Failure(t) => reject(ExceptionRejection(requestId, t.getLocalizedMessage))
|
||||
}
|
||||
def completeRpc(requestId: String, result: AnyRef): Route = complete(JsonRPCRes(result, None, requestId))
|
||||
|
||||
|
@ -85,11 +91,15 @@ trait Service extends Logging {
|
|||
complete(StatusCodes.NotFound, JsonRPCRes(null, Some(Error(StatusCodes.NotFound.intValue, "not found")), "-1"))
|
||||
}
|
||||
.handle {
|
||||
case v: ValidationRejection ⇒ complete(StatusCodes.BadRequest, JsonRPCRes(null, Some(Error(StatusCodes.BadRequest.intValue, v.message)), v.requestId))
|
||||
case nf: NotFoundRejection ⇒ complete(StatusCodes.NotFound, JsonRPCRes(null, Some(Error(StatusCodes.NotFound.intValue, "not found")), nf.requestId))
|
||||
case _: AuthenticationFailedRejection ⇒ complete(StatusCodes.Unauthorized, JsonRPCRes(null, Some(Error(StatusCodes.Unauthorized.intValue, "Access restricted")), "-1"))
|
||||
case v: RpcValidationRejection ⇒ complete(StatusCodes.BadRequest, JsonRPCRes(null, Some(Error(StatusCodes.BadRequest.intValue, v.message)), v.requestId))
|
||||
case ukm: UnknownMethodRejection ⇒ complete(StatusCodes.BadRequest, JsonRPCRes(null, Some(Error(StatusCodes.BadRequest.intValue, "method not found")), ukm.requestId))
|
||||
case p: UnknownParamsRejection ⇒ complete(StatusCodes.BadRequest,
|
||||
JsonRPCRes(null, Some(Error(StatusCodes.BadRequest.intValue, s"invalid parameters for this method, should be: ${p.message}")), p.requestId))
|
||||
case m: MalformedRequestContentRejection ⇒ complete(StatusCodes.BadRequest,
|
||||
JsonRPCRes(null, Some(Error(StatusCodes.BadRequest.intValue, s"malformed parameters for this method: ${m.message}")), "-1"))
|
||||
case e: ExceptionRejection ⇒ complete(StatusCodes.BadRequest,
|
||||
JsonRPCRes(null, Some(Error(StatusCodes.BadRequest.intValue, s"command failed: ${e.message}")), e.requestId))
|
||||
case r ⇒ logger.error(s"API call failed with cause=$r")
|
||||
complete(StatusCodes.BadRequest, JsonRPCRes(null, Some(Error(StatusCodes.BadRequest.intValue, r.toString)), "-1"))
|
||||
}
|
||||
|
@ -99,117 +109,142 @@ trait Service extends Logging {
|
|||
respondWithDefaultHeaders(customHeaders) {
|
||||
handleExceptions(myExceptionHandler) {
|
||||
handleRejections(myRejectionHandler) {
|
||||
pathSingleSlash {
|
||||
post {
|
||||
entity(as[JsonRPCBody]) {
|
||||
req =>
|
||||
val kit = appKit
|
||||
import kit._
|
||||
authenticateBasic(realm = "Access restricted", userPassAuthenticator) { _ =>
|
||||
pathSingleSlash {
|
||||
post {
|
||||
entity(as[JsonRPCBody]) {
|
||||
req =>
|
||||
val kit = appKit
|
||||
import kit._
|
||||
|
||||
req.method match {
|
||||
// utility methods
|
||||
case "getinfo" => completeRpcFuture(req.id, getInfoResponse)
|
||||
case "help" => completeRpc(req.id, help)
|
||||
req.method match {
|
||||
// utility methods
|
||||
case "getinfo" => completeRpcFuture(req.id, getInfoResponse)
|
||||
case "help" => completeRpc(req.id, help)
|
||||
|
||||
// channel lifecycle methods
|
||||
case "connect" => req.params match {
|
||||
case JString(uri) :: Nil =>
|
||||
completeRpcFuture(req.id, (switchboard ? Peer.Connect(NodeURI.parse(uri))).mapTo[String])
|
||||
case _ => reject(UnknownParamsRejection(req.id, "[nodeId, host, port]"))
|
||||
}
|
||||
case "open" => req.params match {
|
||||
case JString(nodeId) :: JInt(fundingSatoshi) :: JInt(pushMsat) :: JInt(flags) :: Nil =>
|
||||
completeRpcFuture(req.id, (switchboard ? Peer.OpenChannel(PublicKey(nodeId), Satoshi(fundingSatoshi.toLong), MilliSatoshi(pushMsat.toLong), channelFlags = Some(flags.toByte))).mapTo[String])
|
||||
case JString(nodeId) :: JInt(fundingSatoshi) :: JInt(pushMsat) :: Nil =>
|
||||
completeRpcFuture(req.id, (switchboard ? Peer.OpenChannel(PublicKey(nodeId), Satoshi(fundingSatoshi.toLong), MilliSatoshi(pushMsat.toLong), channelFlags = None)).mapTo[String])
|
||||
case _ => reject(UnknownParamsRejection(req.id, "[nodeId, host, port, fundingSatoshi, pushMsat] or [nodeId, host, port, fundingSatoshi, pushMsat, newChannel]"))
|
||||
}
|
||||
case "close" => req.params match {
|
||||
case JString(identifier) :: Nil => completeRpc(req.id, sendToChannel(identifier, CMD_CLOSE(scriptPubKey = None)).mapTo[String])
|
||||
case JString(identifier) :: JString(scriptPubKey) :: Nil => completeRpc(req.id, sendToChannel(identifier, CMD_CLOSE(scriptPubKey = Some(scriptPubKey))).mapTo[String])
|
||||
case _ => reject(UnknownParamsRejection(req.id, "[channelId] or [channelId, scriptPubKey]"))
|
||||
}
|
||||
|
||||
// local network methods
|
||||
case "peers" => completeRpcFuture(req.id, for {
|
||||
peers <- (switchboard ? 'peers).mapTo[Map[PublicKey, ActorRef]]
|
||||
peerinfos <- Future.sequence(peers.values.map(peer => (peer ? GetPeerInfo).mapTo[PeerInfo]))
|
||||
} yield peerinfos)
|
||||
case "channels" => req.params match {
|
||||
case Nil => completeRpcFuture(req.id, (register ? 'channels).mapTo[Map[Long, ActorRef]].map(_.keys))
|
||||
case JString(remoteNodeId) :: Nil => Try(PublicKey(remoteNodeId)) match {
|
||||
case Success(pk) => completeRpcFuture(req.id, (register ? 'channelsTo).mapTo[Map[BinaryData, PublicKey]].map(_.filter(_._2 == pk).keys))
|
||||
case Failure(f) => reject(ValidationRejection(req.id, s"invalid remote node id '$remoteNodeId'"))
|
||||
}
|
||||
case _ => reject(UnknownParamsRejection(req.id, "no arguments or [remoteNodeId]"))
|
||||
}
|
||||
case "channel" => req.params match {
|
||||
case JString(identifier) :: Nil => completeRpcFuture(req.id, sendToChannel(identifier, CMD_GETINFO).mapTo[RES_GETINFO])
|
||||
case _ => reject(UnknownParamsRejection(req.id, "[channelId]"))
|
||||
}
|
||||
|
||||
// global network methods
|
||||
case "allnodes" => completeRpcFuture(req.id, (router ? 'nodes).mapTo[Iterable[NodeAnnouncement]].map(_.map(_.nodeId)))
|
||||
case "allchannels" => completeRpcFuture(req.id, (router ? 'channels).mapTo[Iterable[ChannelAnnouncement]].map(_.map(c => ChannelInfo(c.shortChannelId.toHexString, c.nodeId1, c.nodeId2))))
|
||||
|
||||
// payment methods
|
||||
case "receive" => req.params match {
|
||||
// only the payment description is given: user may want to generate a donation payment request
|
||||
case JString(description) :: Nil =>
|
||||
completeRpcFuture(req.id, (paymentHandler ? ReceivePayment(None, description)).mapTo[PaymentRequest].map(PaymentRequest.write))
|
||||
// the amount is now given with the description
|
||||
case JInt(amountMsat) :: JString(description) :: Nil =>
|
||||
completeRpcFuture(req.id, (paymentHandler ? ReceivePayment(Some(MilliSatoshi(amountMsat.toLong)), description)).mapTo[PaymentRequest].map(PaymentRequest.write))
|
||||
case _ => reject(UnknownParamsRejection(req.id, "[description] or [amount, description]"))
|
||||
}
|
||||
case "send" => req.params match {
|
||||
// user manually sets the payment information
|
||||
case JInt(amountMsat) :: JString(paymentHash) :: JString(nodeId) :: Nil =>
|
||||
(Try(BinaryData(paymentHash)), Try(PublicKey(nodeId))) match {
|
||||
case (Success(ph), Success(pk)) => completeRpcFuture(req.id, (paymentInitiator ? SendPayment(amountMsat.toLong, ph, pk)).mapTo[PaymentResult])
|
||||
case (Failure(_), _) => reject(ValidationRejection(req.id, s"invalid payment hash '$paymentHash'"))
|
||||
case _ => reject(ValidationRejection(req.id, s"invalid node id '$nodeId'"))
|
||||
}
|
||||
// user gives a Lightning payment request
|
||||
case JString(paymentRequest) :: rest => Try(PaymentRequest.read(paymentRequest)) match {
|
||||
case Success(pr) =>
|
||||
// setting the payment amount
|
||||
val amount_msat: Long = (pr.amount, rest) match {
|
||||
// optional amount always overrides the amount in the payment request
|
||||
case (_, JInt(amount_msat_override) :: Nil) => amount_msat_override.toLong
|
||||
case (Some(amount_msat_pr), _) => amount_msat_pr.amount
|
||||
case _ => throw new RuntimeException("you must manually specify an amount for this payment request")
|
||||
}
|
||||
logger.debug(s"api call for sending payment with amount_msat=$amount_msat")
|
||||
// optional cltv expiry
|
||||
val sendPayment = pr.minFinalCltvExpiry match {
|
||||
case None => SendPayment(amount_msat, pr.paymentHash, pr.nodeId)
|
||||
case Some(minFinalCltvExpiry) => SendPayment(amount_msat, pr.paymentHash, pr.nodeId, assistedRoutes = Nil, minFinalCltvExpiry)
|
||||
}
|
||||
completeRpcFuture(req.id, (paymentInitiator ? sendPayment).mapTo[PaymentResult])
|
||||
case _ => reject(ValidationRejection(req.id, s"payment request is not valid"))
|
||||
// channel lifecycle methods
|
||||
case "connect" => req.params match {
|
||||
case JString(pubkey) :: JString(host) :: JInt(port) :: Nil =>
|
||||
completeRpcFuture(req.id, (switchboard ? Peer.Connect(NodeURI.parse(s"$pubkey@$host:$port"))).mapTo[String])
|
||||
case JString(uri) :: Nil =>
|
||||
completeRpcFuture(req.id, (switchboard ? Peer.Connect(NodeURI.parse(uri))).mapTo[String])
|
||||
case _ => reject(UnknownParamsRejection(req.id, "[nodeId@host:port] or [nodeId, host, port]"))
|
||||
}
|
||||
case "open" => req.params match {
|
||||
case JString(nodeId) :: JInt(fundingSatoshi) :: Nil =>
|
||||
completeRpcFuture(req.id, (switchboard ? Peer.OpenChannel(PublicKey(nodeId), Satoshi(fundingSatoshi.toLong), MilliSatoshi(0), channelFlags = None)).mapTo[String])
|
||||
case JString(nodeId) :: JInt(fundingSatoshi) :: JInt(pushMsat) :: Nil =>
|
||||
completeRpcFuture(req.id, (switchboard ? Peer.OpenChannel(PublicKey(nodeId), Satoshi(fundingSatoshi.toLong), MilliSatoshi(pushMsat.toLong), channelFlags = None)).mapTo[String])
|
||||
case JString(nodeId) :: JInt(fundingSatoshi) :: JInt(pushMsat) :: JInt(flags) :: Nil =>
|
||||
completeRpcFuture(req.id, (switchboard ? Peer.OpenChannel(PublicKey(nodeId), Satoshi(fundingSatoshi.toLong), MilliSatoshi(pushMsat.toLong), channelFlags = Some(flags.toByte))).mapTo[String])
|
||||
case _ => reject(UnknownParamsRejection(req.id, s"[nodeId, fundingSatoshi], [nodeId, fundingSatoshi, pushMsat] or [nodeId, fundingSatoshi, pushMsat, newChannel]"))
|
||||
}
|
||||
case "close" => req.params match {
|
||||
case JString(identifier) :: Nil => completeRpc(req.id, sendToChannel(identifier, CMD_CLOSE(scriptPubKey = None)).mapTo[String])
|
||||
case JString(identifier) :: JString(scriptPubKey) :: Nil => completeRpc(req.id, sendToChannel(identifier, CMD_CLOSE(scriptPubKey = Some(scriptPubKey))).mapTo[String])
|
||||
case _ => reject(UnknownParamsRejection(req.id, "[channelId] or [channelId, scriptPubKey]"))
|
||||
}
|
||||
case _ => reject(UnknownParamsRejection(req.id, "[amountMsat, paymentHash, nodeId or [paymentRequest] or [paymentRequest, amountMsat]"))
|
||||
}
|
||||
|
||||
// check received payments
|
||||
case "checkpayment" => req.params match {
|
||||
case JString(identifier) :: Nil => completeRpcFuture(req.id, for {
|
||||
paymentHash <- Try(PaymentRequest.read(identifier)) match {
|
||||
case Success(pr) => Future.successful(pr.paymentHash)
|
||||
case _ => Try(BinaryData(identifier)) match {
|
||||
case Success(s) => Future.successful(s)
|
||||
case _ => Future.failed(new IllegalArgumentException("payment identifier must be a payment request or a payment hash"))
|
||||
// local network methods
|
||||
case "peers" => completeRpcFuture(req.id, for {
|
||||
peers <- (switchboard ? 'peers).mapTo[Map[PublicKey, ActorRef]]
|
||||
peerinfos <- Future.sequence(peers.values.map(peer => (peer ? GetPeerInfo).mapTo[PeerInfo]))
|
||||
} yield peerinfos)
|
||||
case "channels" => req.params match {
|
||||
case Nil =>
|
||||
val f = for {
|
||||
channels_id <- (register ? 'channels).mapTo[Map[BinaryData, ActorRef]].map(_.keys)
|
||||
channels <- Future.sequence(channels_id.map(channel_id => sendToChannel(channel_id.toString(), CMD_GETINFO).mapTo[RES_GETINFO]
|
||||
.map(gi => LocalChannelInfo(gi.nodeId, gi.channelId, gi.state.toString))))
|
||||
} yield channels
|
||||
completeRpcFuture(req.id, f)
|
||||
case JString(remoteNodeId) :: Nil => Try(PublicKey(remoteNodeId)) match {
|
||||
case Success(pk) =>
|
||||
val f = for {
|
||||
channels_id <- (register ? 'channelsTo).mapTo[Map[BinaryData, PublicKey]].map(_.filter(_._2 == pk).keys)
|
||||
channels <- Future.sequence(channels_id.map(channel_id => sendToChannel(channel_id.toString(), CMD_GETINFO).mapTo[RES_GETINFO]
|
||||
.map(gi => LocalChannelInfo(gi.nodeId, gi.channelId, gi.state.toString))))
|
||||
} yield channels
|
||||
completeRpcFuture(req.id, f)
|
||||
case Failure(_) => reject(RpcValidationRejection(req.id, s"invalid remote node id '$remoteNodeId'"))
|
||||
}
|
||||
}
|
||||
found <- (paymentHandler ? CheckPayment(paymentHash)).map(found => new JBool(found.asInstanceOf[Boolean]))
|
||||
} yield found)
|
||||
case _ => reject(UnknownParamsRejection(req.id, "[paymentHash] or [paymentRequest]"))
|
||||
}
|
||||
case _ => reject(UnknownParamsRejection(req.id, "no arguments or [remoteNodeId]"))
|
||||
}
|
||||
case "channel" => req.params match {
|
||||
case JString(identifier) :: Nil => completeRpcFuture(req.id, sendToChannel(identifier, CMD_GETINFO).mapTo[RES_GETINFO])
|
||||
case _ => reject(UnknownParamsRejection(req.id, "[channelId]"))
|
||||
}
|
||||
|
||||
// method name was not found
|
||||
case _ => reject(UnknownMethodRejection(req.id))
|
||||
}
|
||||
// global network methods
|
||||
case "allnodes" => completeRpcFuture(req.id, (router ? 'nodes).mapTo[Iterable[NodeAnnouncement]])
|
||||
case "allchannels" => completeRpcFuture(req.id, (router ? 'channels).mapTo[Iterable[ChannelAnnouncement]].map(_.map(c => ChannelInfo(c.shortChannelId.toHexString, c.nodeId1, c.nodeId2))))
|
||||
case "allupdates" => req.params match {
|
||||
case JString(nodeId) :: Nil => Try(PublicKey(nodeId)) match {
|
||||
case Success(pk) => completeRpcFuture(req.id, (router ? 'updatesMap).mapTo[Map[ChannelDesc, ChannelUpdate]].map(_.filter(e => e._1.a == pk || e._1.b == pk).values))
|
||||
case Failure(_) => reject(RpcValidationRejection(req.id, s"invalid remote node id '$nodeId'"))
|
||||
}
|
||||
case _ => completeRpcFuture(req.id, (router ? 'updates).mapTo[Iterable[ChannelUpdate]])
|
||||
}
|
||||
|
||||
// payment methods
|
||||
case "receive" => req.params match {
|
||||
// only the payment description is given: user may want to generate a donation payment request
|
||||
case JString(description) :: Nil =>
|
||||
completeRpcFuture(req.id, (paymentHandler ? ReceivePayment(None, description)).mapTo[PaymentRequest].map(PaymentRequest.write))
|
||||
// the amount is now given with the description
|
||||
case JInt(amountMsat) :: JString(description) :: Nil =>
|
||||
completeRpcFuture(req.id, (paymentHandler ? ReceivePayment(Some(MilliSatoshi(amountMsat.toLong)), description)).mapTo[PaymentRequest].map(PaymentRequest.write))
|
||||
case _ => reject(UnknownParamsRejection(req.id, "[description] or [amount, description]"))
|
||||
}
|
||||
case "send" => req.params match {
|
||||
// user manually sets the payment information
|
||||
case JInt(amountMsat) :: JString(paymentHash) :: JString(nodeId) :: Nil =>
|
||||
(Try(BinaryData(paymentHash)), Try(PublicKey(nodeId))) match {
|
||||
case (Success(ph), Success(pk)) => completeRpcFuture(req.id, (paymentInitiator ? SendPayment(amountMsat.toLong, ph, pk)).mapTo[PaymentResult])
|
||||
case (Failure(_), _) => reject(RpcValidationRejection(req.id, s"invalid payment hash '$paymentHash'"))
|
||||
case _ => reject(RpcValidationRejection(req.id, s"invalid node id '$nodeId'"))
|
||||
}
|
||||
// user gives a Lightning payment request
|
||||
case JString(paymentRequest) :: rest => Try(PaymentRequest.read(paymentRequest)) match {
|
||||
case Success(pr) =>
|
||||
// setting the payment amount
|
||||
val amount_msat: Long = (pr.amount, rest) match {
|
||||
// optional amount always overrides the amount in the payment request
|
||||
case (_, JInt(amount_msat_override) :: Nil) => amount_msat_override.toLong
|
||||
case (Some(amount_msat_pr), _) => amount_msat_pr.amount
|
||||
case _ => throw new RuntimeException("you must manually specify an amount for this payment request")
|
||||
}
|
||||
logger.debug(s"api call for sending payment with amount_msat=$amount_msat")
|
||||
// optional cltv expiry
|
||||
val sendPayment = pr.minFinalCltvExpiry match {
|
||||
case None => SendPayment(amount_msat, pr.paymentHash, pr.nodeId)
|
||||
case Some(minFinalCltvExpiry) => SendPayment(amount_msat, pr.paymentHash, pr.nodeId, assistedRoutes = Nil, minFinalCltvExpiry)
|
||||
}
|
||||
completeRpcFuture(req.id, (paymentInitiator ? sendPayment).mapTo[PaymentResult])
|
||||
case _ => reject(RpcValidationRejection(req.id, s"payment request is not valid"))
|
||||
}
|
||||
case _ => reject(UnknownParamsRejection(req.id, "[amountMsat, paymentHash, nodeId or [paymentRequest] or [paymentRequest, amountMsat]"))
|
||||
}
|
||||
|
||||
// check received payments
|
||||
case "checkpayment" => req.params match {
|
||||
case JString(identifier) :: Nil => completeRpcFuture(req.id, for {
|
||||
paymentHash <- Try(PaymentRequest.read(identifier)) match {
|
||||
case Success(pr) => Future.successful(pr.paymentHash)
|
||||
case _ => Try(BinaryData(identifier)) match {
|
||||
case Success(s) => Future.successful(s)
|
||||
case _ => Future.failed(new IllegalArgumentException("payment identifier must be a payment request or a payment hash"))
|
||||
}
|
||||
}
|
||||
found <- (paymentHandler ? CheckPayment(paymentHash)).map(found => new JBool(found.asInstanceOf[Boolean]))
|
||||
} yield found)
|
||||
case _ => reject(UnknownParamsRejection(req.id, "[paymentHash] or [paymentRequest]"))
|
||||
}
|
||||
|
||||
// method name was not found
|
||||
case _ => reject(UnknownMethodRejection(req.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -221,13 +256,16 @@ trait Service extends Logging {
|
|||
|
||||
def help = List(
|
||||
"connect (uri): open a secure connection to a lightning node",
|
||||
"open (nodeId, fundingSatoshi, pushMsat, channelFlags = 0x01): open a channel with another lightning node",
|
||||
"connect (nodeId, host, port): open a secure connection to a lightning node",
|
||||
"open (nodeId, fundingSatoshi, pushMsat = 0, channelFlags = 0x01): open a channel with another lightning node",
|
||||
"peers: list existing local peers",
|
||||
"channels: list existing local channels",
|
||||
"channels (nodeId): list existing local channels to a particular nodeId",
|
||||
"channel (channelId): retrieve detailed information about a given channel",
|
||||
"allnodes: list all known nodes",
|
||||
"allchannels: list all known channels",
|
||||
"allupdates: list all channels updates",
|
||||
"allupdates (nodeId): list all channels updates for this nodeId",
|
||||
"receive (amountMsat, description): generate a payment request for a given amount",
|
||||
"send (amountMsat, paymentHash, nodeId): send a payment to a lightning node",
|
||||
"send (paymentRequest): send a payment to a lightning node using a BOLT11 payment request",
|
||||
|
|
|
@ -7,7 +7,7 @@ import fr.acinq.eclair.UInt64
|
|||
import fr.acinq.eclair.crypto.Sphinx
|
||||
import fr.acinq.eclair.transactions.CommitmentSpec
|
||||
import fr.acinq.eclair.transactions.Transactions.CommitTx
|
||||
import fr.acinq.eclair.wire.{AcceptChannel, AnnouncementSignatures, ChannelAnnouncement, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, OpenChannel, Shutdown, UpdateAddHtlc}
|
||||
import fr.acinq.eclair.wire.{AcceptChannel, ChannelAnnouncement, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, OpenChannel, Shutdown, UpdateAddHtlc}
|
||||
|
||||
|
||||
/**
|
||||
|
@ -101,7 +101,7 @@ final case class CMD_CLOSE(scriptPubKey: Option[BinaryData]) extends Command
|
|||
case object CMD_GETSTATE extends Command
|
||||
case object CMD_GETSTATEDATA extends Command
|
||||
case object CMD_GETINFO extends Command
|
||||
final case class RES_GETINFO(nodeid: BinaryData, channelId: BinaryData, state: State, data: Data)
|
||||
final case class RES_GETINFO(nodeId: BinaryData, channelId: BinaryData, state: State, data: Data)
|
||||
|
||||
/*
|
||||
8888888b. d8888 88888888888 d8888
|
||||
|
|
|
@ -5,7 +5,7 @@ import java.net.InetSocketAddress
|
|||
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, sha256, verifySignature}
|
||||
import fr.acinq.bitcoin.{BinaryData, Crypto, LexicographicalOrdering}
|
||||
import fr.acinq.eclair.serializationResult
|
||||
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, LightningMessageCodecs, NodeAnnouncement}
|
||||
import fr.acinq.eclair.wire._
|
||||
import scodec.bits.BitVector
|
||||
import shapeless.HNil
|
||||
|
||||
|
@ -20,8 +20,8 @@ object Announcements {
|
|||
def channelAnnouncementWitnessEncode(chainHash: BinaryData, shortChannelId: Long, nodeId1: PublicKey, nodeId2: PublicKey, bitcoinKey1: PublicKey, bitcoinKey2: PublicKey, features: BinaryData): BinaryData =
|
||||
sha256(sha256(serializationResult(LightningMessageCodecs.channelAnnouncementWitnessCodec.encode(features :: chainHash :: shortChannelId :: nodeId1 :: nodeId2 :: bitcoinKey1 :: bitcoinKey2 :: HNil))))
|
||||
|
||||
def nodeAnnouncementWitnessEncode(timestamp: Long, nodeId: PublicKey, rgbColor: (Byte, Byte, Byte), alias: String, features: BinaryData, addresses: List[InetSocketAddress]): BinaryData =
|
||||
sha256(sha256(serializationResult(LightningMessageCodecs.nodeAnnouncementWitnessCodec.encode(features :: timestamp :: nodeId :: rgbColor :: alias :: addresses :: HNil))))
|
||||
def nodeAnnouncementWitnessEncode(timestamp: Long, nodeId: PublicKey, rgbColor: Color, alias: String, features: BinaryData, addresses: List[InetSocketAddress]): BinaryData =
|
||||
sha256(sha256(serializationResult(LightningMessageCodecs.nodeAnnouncementWitnessCodec.encode(features :: timestamp :: nodeId :: (rgbColor) :: alias :: addresses :: HNil))))
|
||||
|
||||
def channelUpdateWitnessEncode(chainHash: BinaryData, shortChannelId: Long, timestamp: Long, flags: BinaryData, cltvExpiryDelta: Int, htlcMinimumMsat: Long, feeBaseMsat: Long, feeProportionalMillionths: Long): BinaryData =
|
||||
sha256(sha256(serializationResult(LightningMessageCodecs.channelUpdateWitnessCodec.encode(chainHash :: shortChannelId :: timestamp :: flags :: cltvExpiryDelta :: htlcMinimumMsat :: feeBaseMsat :: feeProportionalMillionths :: HNil))))
|
||||
|
@ -59,7 +59,7 @@ object Announcements {
|
|||
)
|
||||
}
|
||||
|
||||
def makeNodeAnnouncement(nodeSecret: PrivateKey, alias: String, color: (Byte, Byte, Byte), addresses: List[InetSocketAddress], timestamp: Long = Platform.currentTime / 1000): NodeAnnouncement = {
|
||||
def makeNodeAnnouncement(nodeSecret: PrivateKey, alias: String, color: Color, addresses: List[InetSocketAddress], timestamp: Long = Platform.currentTime / 1000): NodeAnnouncement = {
|
||||
require(alias.size <= 32)
|
||||
val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, "", addresses)
|
||||
val sig = Crypto.encodeSignature(Crypto.sign(witness, nodeSecret)) :+ 1.toByte
|
||||
|
|
|
@ -406,6 +406,10 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSM[State, Data]
|
|||
sender ! (d.updates ++ d.privateUpdates).values
|
||||
stay
|
||||
|
||||
case Event('updatesMap, d) =>
|
||||
sender ! (d.updates ++ d.privateUpdates)
|
||||
stay
|
||||
|
||||
case Event('dot, d) =>
|
||||
graph2dot(d.nodes, d.channels) pipeTo sender
|
||||
stay
|
||||
|
@ -549,7 +553,7 @@ object Router {
|
|||
override def getComponentAttributes(nodeId: PublicKey): java.util.Map[String, String] =
|
||||
|
||||
nodes.get(nodeId) match {
|
||||
case Some(ann) => Map("label" -> ann.alias, "color" -> f"#${ann.rgbColor._1}%02x${ann.rgbColor._2}%02x${ann.rgbColor._3}%02x")
|
||||
case Some(ann) => Map("label" -> ann.alias, "color" -> ann.rgbColor.toString)
|
||||
case None => Map.empty[String, String]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -136,11 +136,15 @@ case class ChannelAnnouncement(nodeSignature1: BinaryData,
|
|||
bitcoinKey1: PublicKey,
|
||||
bitcoinKey2: PublicKey) extends RoutingMessage
|
||||
|
||||
case class Color(r: Byte, g: Byte, b: Byte) {
|
||||
override def toString: String = f"#$r%02x$g%02x$b%02x" // to hexa s"# ${r}%02x ${r & 0xFF}${g & 0xFF}${b & 0xFF}"
|
||||
}
|
||||
|
||||
case class NodeAnnouncement(signature: BinaryData,
|
||||
features: BinaryData,
|
||||
timestamp: Long,
|
||||
nodeId: PublicKey,
|
||||
rgbColor: (Byte, Byte, Byte),
|
||||
rgbColor: Color,
|
||||
alias: String,
|
||||
// TODO: check address order + support padding data (type 0)
|
||||
addresses: List[InetSocketAddress]) extends RoutingMessage
|
||||
|
@ -157,4 +161,4 @@ case class ChannelUpdate(signature: BinaryData,
|
|||
|
||||
case class PerHopPayload(channel_id: Long,
|
||||
amtToForward: Long,
|
||||
outgoingCltvValue: Long)
|
||||
outgoingCltvValue: Long)
|
||||
|
|
|
@ -8,6 +8,7 @@ import fr.acinq.bitcoin.{BinaryData, Block, DeterministicWallet, Script}
|
|||
import fr.acinq.eclair.NodeParams.BITCOIND
|
||||
import fr.acinq.eclair.db.sqlite._
|
||||
import fr.acinq.eclair.io.Peer
|
||||
import fr.acinq.eclair.wire.Color
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
|
@ -31,7 +32,7 @@ object TestConstants {
|
|||
extendedPrivateKey = extendedPrivateKey,
|
||||
privateKey = extendedPrivateKey.privateKey,
|
||||
alias = "alice",
|
||||
color = (1: Byte, 2: Byte, 3: Byte),
|
||||
color = Color(1, 2, 3),
|
||||
publicAddresses = new InetSocketAddress("localhost", 9731) :: Nil,
|
||||
globalFeatures = "",
|
||||
localFeatures = "00",
|
||||
|
@ -85,7 +86,7 @@ object TestConstants {
|
|||
extendedPrivateKey = extendedPrivateKey,
|
||||
privateKey = extendedPrivateKey.privateKey,
|
||||
alias = "bob",
|
||||
color = (4: Byte, 5: Byte, 6: Byte),
|
||||
color = Color(4, 5, 6),
|
||||
publicAddresses = new InetSocketAddress("localhost", 9732) :: Nil,
|
||||
globalFeatures = "",
|
||||
localFeatures = "00", // no announcement
|
||||
|
|
|
@ -33,7 +33,7 @@ class ElectrumClientSpec extends TestKit(ActorSystem("test")) with FunSuiteLike
|
|||
|
||||
test("connect to an electrumx testnet server") {
|
||||
probe.send(client, AddStatusListener(probe.ref))
|
||||
probe.expectMsg(5 seconds, ElectrumReady)
|
||||
probe.expectMsg(15 seconds, ElectrumReady)
|
||||
}
|
||||
|
||||
test("get transaction") {
|
||||
|
|
|
@ -7,6 +7,7 @@ import fr.acinq.bitcoin.{Block, Crypto}
|
|||
import fr.acinq.eclair.db.sqlite.SqliteNetworkDb
|
||||
import fr.acinq.eclair.randomKey
|
||||
import fr.acinq.eclair.router.Announcements
|
||||
import fr.acinq.eclair.wire.Color
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.FunSuite
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
|
@ -27,9 +28,9 @@ class SqliteNetworkDbSpec extends FunSuite {
|
|||
val sqlite = inmem
|
||||
val db = new SqliteNetworkDb(sqlite)
|
||||
|
||||
val node_1 = Announcements.makeNodeAnnouncement(randomKey, "node-alice", (100.toByte, 200.toByte, 300.toByte), new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000) :: Nil)
|
||||
val node_2 = Announcements.makeNodeAnnouncement(randomKey, "node-bob", (100.toByte, 200.toByte, 300.toByte), new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000) :: Nil)
|
||||
val node_3 = Announcements.makeNodeAnnouncement(randomKey, "node-charlie", (100.toByte, 200.toByte, 300.toByte), new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000) :: Nil)
|
||||
val node_1 = Announcements.makeNodeAnnouncement(randomKey, "node-alice", Color(100.toByte, 200.toByte, 300.toByte), new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000) :: Nil)
|
||||
val node_2 = Announcements.makeNodeAnnouncement(randomKey, "node-bob", Color(100.toByte, 200.toByte, 300.toByte), new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000) :: Nil)
|
||||
val node_3 = Announcements.makeNodeAnnouncement(randomKey, "node-charlie", Color(100.toByte, 200.toByte, 300.toByte), new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000) :: Nil)
|
||||
|
||||
assert(db.listNodes().toSet === Set.empty)
|
||||
db.addNode(node_1)
|
||||
|
|
|
@ -34,12 +34,12 @@ abstract class BaseRouterSpec extends TestkitBaseClass {
|
|||
|
||||
//val DUMMY_SIG = BinaryData("3045022100e0a180fdd0fe38037cc878c03832861b40a29d32bd7b40b10c9e1efc8c1468a002205ae06d1624896d0d29f4b31e32772ea3cb1b4d7ed4e077e5da28dcc33c0e781201")
|
||||
|
||||
val ann_a = makeNodeAnnouncement(priv_a, "node-A", (15, 10, -70), Nil)
|
||||
val ann_b = makeNodeAnnouncement(priv_b, "node-B", (50, 99, -80), Nil)
|
||||
val ann_c = makeNodeAnnouncement(priv_c, "node-C", (123, 100, -40), Nil)
|
||||
val ann_d = makeNodeAnnouncement(priv_d, "node-D", (-120, -20, 60), Nil)
|
||||
val ann_e = makeNodeAnnouncement(priv_e, "node-E", (-50, 0, 10), Nil)
|
||||
val ann_f = makeNodeAnnouncement(priv_f, "node-F", (30, 10, -50), Nil)
|
||||
val ann_a = makeNodeAnnouncement(priv_a, "node-A", Color(15, 10, -70), Nil)
|
||||
val ann_b = makeNodeAnnouncement(priv_b, "node-B", Color(50, 99, -80), Nil)
|
||||
val ann_c = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), Nil)
|
||||
val ann_d = makeNodeAnnouncement(priv_d, "node-D", Color(-120, -20, 60), Nil)
|
||||
val ann_e = makeNodeAnnouncement(priv_e, "node-E", Color(-50, 0, 10), Nil)
|
||||
val ann_f = makeNodeAnnouncement(priv_f, "node-F", Color(30, 10, -50), Nil)
|
||||
|
||||
val channelId_ab = toShortId(420000, 1, 0)
|
||||
val channelId_bc = toShortId(420000, 2, 0)
|
||||
|
|
|
@ -54,7 +54,7 @@ class LightningMessageCodecsSpec extends FunSuite {
|
|||
}
|
||||
|
||||
test("encode/decode with rgb codec") {
|
||||
val color = (47.toByte, 255.toByte, 142.toByte)
|
||||
val color = Color(47.toByte, 255.toByte, 142.toByte)
|
||||
val bin = rgb.encode(color).require
|
||||
assert(bin === hex"2f ff 8e".toBitVector)
|
||||
val color2 = rgb.decode(bin).require.value
|
||||
|
@ -178,7 +178,7 @@ class LightningMessageCodecsSpec extends FunSuite {
|
|||
val commit_sig = CommitSig(randomBytes(32), randomSignature, randomSignature :: randomSignature :: randomSignature :: Nil)
|
||||
val revoke_and_ack = RevokeAndAck(randomBytes(32), scalar(0), point(1))
|
||||
val channel_announcement = ChannelAnnouncement(randomSignature, randomSignature, randomSignature, randomSignature, bin(7, 9), Block.RegtestGenesisBlock.hash, 1, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey)
|
||||
val node_announcement = NodeAnnouncement(randomSignature, bin(0, 0), 1, randomKey.publicKey, (100.toByte, 200.toByte, 300.toByte), "node-alias", new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000) :: Nil)
|
||||
val node_announcement = NodeAnnouncement(randomSignature, bin(0, 0), 1, randomKey.publicKey, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000) :: Nil)
|
||||
val channel_update = ChannelUpdate(randomSignature, Block.RegtestGenesisBlock.hash, 1, 2, bin(2, 2), 3, 4, 5, 6)
|
||||
val announcement_signatures = AnnouncementSignatures(randomBytes(32), 42, randomSignature, randomSignature)
|
||||
val ping = Ping(100, BinaryData("01" * 10))
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
</children>
|
||||
</VBox>
|
||||
<Label onMouseClicked="#openGithubPage" VBox.vgrow="NEVER" styleClass="link"
|
||||
text="Consult our readme to get started."/>
|
||||
text="Check our readme to get started."/>
|
||||
<Button fx:id="closeButton" VBox.vgrow="NEVER" mnemonicParsing="false" onAction="#closeAndKill"
|
||||
text="Close" cancelButton="true"/>
|
||||
</children>
|
||||
|
|
|
@ -22,6 +22,8 @@ import grizzled.slf4j.Logging
|
|||
import scala.concurrent.Promise
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
||||
|
||||
/**
|
||||
* Created by PM on 16/08/2016.
|
||||
|
@ -46,7 +48,7 @@ class FxApp extends Application with Logging {
|
|||
notifyPreloader(new AppNotification(InfoAppNotification, "Eclair is still in alpha, and under heavy development. Last update was not backward compatible."))
|
||||
notifyPreloader(new AppNotification(InfoAppNotification, "Please reset your datadir."))
|
||||
case t: Throwable =>
|
||||
notifyPreloader(new ErrorNotification("Setup", s"Internal error: ${t.toString}", t))
|
||||
notifyPreloader(new ErrorNotification("Setup", s"Error: ${t.getLocalizedMessage}", t))
|
||||
}
|
||||
|
||||
override def start(primaryStage: Stage): Unit = {
|
||||
|
@ -71,7 +73,6 @@ class FxApp extends Application with Logging {
|
|||
setup.system.eventStream.subscribe(guiUpdater, classOf[ZMQEvent])
|
||||
setup.system.eventStream.subscribe(guiUpdater, classOf[ElectrumEvent])
|
||||
pKit.completeWith(setup.bootstrap)
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
pKit.future.onComplete {
|
||||
case Success(_) =>
|
||||
Platform.runLater(new Runnable {
|
||||
|
|
|
@ -59,7 +59,7 @@ class FxPreloader extends Preloader with Logging {
|
|||
info match {
|
||||
case n: ErrorNotification =>
|
||||
logger.debug(s"Preloader error notification => ${n.getDetails}")
|
||||
logger.error("An error has occured", n.getCause)
|
||||
logger.error("", n.getCause)
|
||||
controller.map(_.addError(n.getDetails))
|
||||
controller.map(_.showErrorBox)
|
||||
case n: AppNotification =>
|
||||
|
|
|
@ -162,8 +162,7 @@ class MainController(val handlers: Handlers, val hostServices: HostServices) ext
|
|||
def call(pn: CellDataFeatures[NodeAnnouncement, String]) = new SimpleStringProperty(pn.getValue.alias)
|
||||
})
|
||||
networkNodesRGBColumn.setCellValueFactory(new Callback[CellDataFeatures[NodeAnnouncement, String], ObservableValue[String]]() {
|
||||
def call(pn: CellDataFeatures[NodeAnnouncement, String]) = new SimpleStringProperty(
|
||||
s"rgb(${new Integer(pn.getValue.rgbColor._1 & 0xFF)}, ${new Integer(pn.getValue.rgbColor._2 & 0xFF)}, ${new Integer(pn.getValue.rgbColor._3 & 0xFF)})")
|
||||
def call(pn: CellDataFeatures[NodeAnnouncement, String]) = new SimpleStringProperty(pn.getValue.rgbColor.toString)
|
||||
})
|
||||
networkNodesIPColumn.setCellValueFactory(new Callback[CellDataFeatures[NodeAnnouncement, String], ObservableValue[String]]() {
|
||||
def call(pn: CellDataFeatures[NodeAnnouncement, String]) = {
|
||||
|
@ -313,7 +312,7 @@ class MainController(val handlers: Handlers, val hostServices: HostServices) ext
|
|||
// init status bar
|
||||
labelNodeId.setText(s"${setup.nodeParams.privateKey.publicKey}")
|
||||
labelAlias.setText(s"${setup.nodeParams.alias}")
|
||||
rectRGB.setFill(Color.rgb(setup.nodeParams.color._1 & 0xFF, setup.nodeParams.color._2 & 0xFF, setup.nodeParams.color._3 & 0xFF))
|
||||
rectRGB.setFill(Color.web(setup.nodeParams.color.toString))
|
||||
labelApi.setText(s"${setup.config.getInt("api.port")}")
|
||||
labelServer.setText(s"${setup.config.getInt("server.port")}")
|
||||
bitcoinVersion.setText(s"v0.0.0")
|
||||
|
|
Loading…
Add table
Reference in a new issue