1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-03-13 19:37:35 +01:00

make eclair run on android

This commit is contained in:
pm47 2017-08-25 12:24:54 +02:00
parent 4d1ea8e402
commit 50729674e4
78 changed files with 513 additions and 3543 deletions

View file

@ -1,5 +1,5 @@
sudo: required
dist: trusty
dist: precise
language: scala
scala:
- 2.11.11

View file

@ -5,7 +5,7 @@
<parent>
<groupId>fr.acinq.eclair</groupId>
<artifactId>eclair_2.11</artifactId>
<version>0.2-spv-SNAPSHOT</version>
<version>0.2-android-SNAPSHOT</version>
</parent>
<artifactId>eclair-core_2.11</artifactId>
@ -107,10 +107,11 @@
<artifactId>akka-slf4j_${scala.version.short}</artifactId>
<version>${akka.version}</version>
</dependency>
<!-- HTTP -->
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-http-core_${scala.version.short}</artifactId>
<version>10.0.7</version>
<groupId>com.ning</groupId>
<artifactId>async-http-client</artifactId>
<version>1.9.40</version>
</dependency>
<!-- JSON -->
<dependency>
@ -118,11 +119,6 @@
<artifactId>json4s-jackson_${scala.version.short}</artifactId>
<version>3.5.2</version>
</dependency>
<dependency>
<groupId>de.heikoseeberger</groupId>
<artifactId>akka-http-json4s_${scala.version.short}</artifactId>
<version>1.16.1</version>
</dependency>
<!-- BITCOIN -->
<dependency>
<groupId>fr.acinq</groupId>
@ -141,7 +137,7 @@
<version>0.4.0</version>
</dependency>
<dependency>
<groupId>org.bitcoinj</groupId>
<groupId>fr.acinq</groupId>
<artifactId>bitcoinj-core</artifactId>
<version>${bitcoinj.version}</version>
</dependency>
@ -161,12 +157,12 @@
<dependency>
<groupId>org.jgrapht</groupId>
<artifactId>jgrapht-core</artifactId>
<version>1.0.1</version>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>org.jgrapht</groupId>
<artifactId>jgrapht-ext</artifactId>
<version>1.0.1</version>
<version>0.9.0</version>
<exclusions>
<exclusion>
<groupId>org.tinyjee.jgraphx</groupId>

View file

@ -24,7 +24,7 @@ eclair {
node-color = "49daaa"
global-features = ""
local-features = "08" // initial_routing_sync
channel-flags = 1 // announce channels
channel-flags = 0 // do not announce channels
dust-limit-satoshis = 542
default-feerate-per-kb = 20000 // default bitcoin core value

View file

@ -0,0 +1,37 @@
package fr.acinq.eclair
import com.ning.http.client.{AsyncCompletionHandler, AsyncHttpClient, AsyncHttpClientConfig, Response}
import grizzled.slf4j.Logging
import org.json4s.DefaultFormats
import org.json4s.JsonAST.{JNothing, JValue}
import org.json4s.jackson.JsonMethods.parse
import scala.concurrent.{ExecutionContext, Future, Promise}
import scala.util.{Failure, Success, Try}
object HttpHelper extends Logging {
val client = new AsyncHttpClient(new AsyncHttpClientConfig.Builder().setAcceptAnyCertificate(true).build())
implicit val formats = DefaultFormats
def get(url: String)(implicit ec: ExecutionContext): Future[JValue] = {
val promise = Promise[JValue]
client
.prepareGet(url)
.execute(new AsyncCompletionHandler[Unit] {
override def onCompleted(response: Response): Unit = {
Try(parse(response.getResponseBody)) match {
case Success(json) => promise.success(json)
case Failure(t) => promise.success(JNothing)
}
}
})
val f = promise.future
f onFailure {
case t: Throwable => logger.error(s"GET $url failed: ", t)
}
f
}
}

View file

@ -2,9 +2,9 @@ package fr.acinq.eclair
import java.io.File
import java.net.InetSocketAddress
import java.nio.file.Files
import java.util.concurrent.TimeUnit
import com.google.common.io.Files
import com.typesafe.config.{Config, ConfigFactory}
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.DeterministicWallet.ExtendedPrivateKey
@ -73,10 +73,10 @@ object NodeParams {
val seedPath = new File(datadir, "seed.dat")
val seed: BinaryData = seedPath.exists() match {
case true => Files.readAllBytes(seedPath.toPath)
case true => Files.toByteArray(seedPath)
case false =>
val seed = randomKey.toBin
Files.write(seedPath.toPath, seed)
Files.write(seed, seedPath)
seed
}
val master = DeterministicWallet.generate(seed)
@ -111,9 +111,9 @@ object NodeParams {
channelsDb = Dbs.makeChannelDb(db),
peersDb = Dbs.makePeerDb(db),
announcementsDb = Dbs.makeAnnouncementDb(db),
routerBroadcastInterval = FiniteDuration(config.getDuration("router-broadcast-interval").getSeconds, TimeUnit.SECONDS),
routerValidateInterval = FiniteDuration(config.getDuration("router-validate-interval").getSeconds, TimeUnit.SECONDS),
pingInterval = FiniteDuration(config.getDuration("ping-interval").getSeconds, TimeUnit.SECONDS),
routerBroadcastInterval = FiniteDuration(config.getDuration("router-broadcast-interval", TimeUnit.SECONDS), TimeUnit.SECONDS),
routerValidateInterval = FiniteDuration(config.getDuration("router-validate-interval", TimeUnit.SECONDS), TimeUnit.SECONDS),
pingInterval = FiniteDuration(config.getDuration("ping-interval", TimeUnit.SECONDS), TimeUnit.SECONDS),
maxFeerateMismatch = config.getDouble("max-feerate-mismatch"),
updateFeeMinDiffRatio = config.getDouble("update-fee_min-diff-ratio"),
autoReconnect = config.getBoolean("auto-reconnect"),

View file

@ -1,36 +1,29 @@
package fr.acinq.eclair
import java.io.File
import java.net.InetSocketAddress
import akka.actor.{ActorRef, ActorSystem, Props, SupervisorStrategy}
import akka.http.scaladsl.Http
import akka.pattern.after
import akka.stream.{ActorMaterializer, BindFailedException}
import akka.util.Timeout
import com.typesafe.config.{Config, ConfigFactory}
import fr.acinq.bitcoin.{BinaryData, Block}
import fr.acinq.eclair.api.{GetInfoResponse, Service}
import fr.acinq.eclair.blockchain._
import fr.acinq.bitcoin.Block
import fr.acinq.eclair.blockchain.SpvWatcher
import fr.acinq.eclair.blockchain.fee.{BitpayInsightFeeProvider, ConstantFeeProvider}
import fr.acinq.eclair.blockchain.rpc.{BitcoinJsonRPCClient, ExtendedBitcoinClient}
import fr.acinq.eclair.blockchain.spv.BitcoinjKit
import fr.acinq.eclair.blockchain.wallet.{BitcoinCoreWallet, BitcoinjWallet}
import fr.acinq.eclair.blockchain.zmq.ZMQActor
import fr.acinq.eclair.blockchain.wallet.{BitcoinjWallet, EclairWallet}
import fr.acinq.eclair.channel.Register
import fr.acinq.eclair.io.{Server, Switchboard}
import fr.acinq.eclair.io.Switchboard
import fr.acinq.eclair.payment._
import fr.acinq.eclair.router._
import grizzled.slf4j.Logging
import scala.concurrent.duration._
import scala.concurrent.{Await, ExecutionContext, Future, Promise}
import scala.util.Try
import scala.concurrent.{ExecutionContext, Future}
/**
* Created by PM on 25/01/2016.
*/
class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), actorSystem: ActorSystem = ActorSystem()) extends Logging {
class Setup(datadir: File, wallet_opt: Option[EclairWallet] = None, overrideDefaults: Config = ConfigFactory.empty(), actorSystem: ActorSystem = ActorSystem()) extends Logging {
logger.info(s"hello!")
logger.info(s"version=${getClass.getPackage.getImplementationVersion} commit=${getClass.getPackage.getSpecificationVersion}")
@ -38,15 +31,7 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act
val spv = config.getBoolean("spv")
// early check
PortChecker.checkAvailable(config.getString("server.binding-ip"), config.getInt("server.port"))
logger.info(s"initializing secure random generator")
// this will force the secure random instance to initialize itself right now, making sure it doesn't hang later (see comment in package.scala)
secureRandom.nextInt()
implicit val system = actorSystem
implicit val materializer = ActorMaterializer()
implicit val timeout = Timeout(30 seconds)
implicit val formats = org.json4s.DefaultFormats
implicit val ec = ExecutionContext.Implicits.global
@ -60,30 +45,13 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act
}
val bitcoinjKit = new BitcoinjKit(chain, datadir)
(chain, chainHash, Left(bitcoinjKit))
} else {
val bitcoinClient = new ExtendedBitcoinClient(new BitcoinJsonRPCClient(
user = config.getString("bitcoind.rpcuser"),
password = config.getString("bitcoind.rpcpassword"),
host = config.getString("bitcoind.host"),
port = config.getInt("bitcoind.rpcport")))
val future = for {
json <- bitcoinClient.rpcClient.invoke("getblockchaininfo")
chain = (json \ "chain").extract[String]
progress = (json \ "verificationprogress").extract[Double]
chainHash <- bitcoinClient.rpcClient.invoke("getblockhash", 0).map(_.extract[String]).map(BinaryData(_))
bitcoinVersion <- bitcoinClient.rpcClient.invoke("getnetworkinfo").map(json => (json \ "version")).map(_.extract[String])
} yield (chain, progress, chainHash, bitcoinVersion)
val (chain, progress, chainHash, bitcoinVersion) = Try(Await.result(future, 10 seconds)).recover { case _ => throw BitcoinRPCConnectionException }.get
assert(progress > 0.99, "bitcoind should be synchronized")
(chain, chainHash, Right(bitcoinClient))
}
} else ???
val nodeParams = NodeParams.makeNodeParams(datadir, config, chainHash)
logger.info(s"using chain=$chain chainHash=$chainHash")
logger.info(s"nodeid=${nodeParams.privateKey.publicKey.toBin} alias=${nodeParams.alias}")
def bootstrap: Future[Kit] = {
val zmqConnected = Promise[Boolean]()
val tcpBound = Promise[Unit]()
def bootstrap: Future[Kit] = Future {
val defaultFeeratePerKb = config.getLong("default-feerate-per-kb")
Globals.feeratePerKw.set(feerateKb2Kw(defaultFeeratePerKb))
@ -100,17 +68,15 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act
val watcher = bitcoin match {
case Left(bitcoinj) =>
zmqConnected.success(true)
bitcoinj.startAsync()
system.actorOf(SimpleSupervisor.props(SpvWatcher.props(nodeParams, bitcoinj), "watcher", SupervisorStrategy.Resume))
case Right(bitcoinClient) =>
system.actorOf(SimpleSupervisor.props(Props(new ZMQActor(config.getString("bitcoind.zmq"), Some(zmqConnected))), "zmq", SupervisorStrategy.Restart))
system.actorOf(SimpleSupervisor.props(ZmqWatcher.props(nodeParams, bitcoinClient), "watcher", SupervisorStrategy.Resume))
case _ => ???
}
val wallet = bitcoin match {
case _ if wallet_opt.isDefined => wallet_opt.get
case Left(bitcoinj) => new BitcoinjWallet(bitcoinj.initialized.map(_ => bitcoinj.wallet()))
case Right(bitcoinClient) => new BitcoinCoreWallet(bitcoinClient.rpcClient, watcher)
case _ => ???
}
wallet.getFinalAddress.map {
case address => logger.info(s"initial wallet address=$address")
@ -122,10 +88,13 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act
}, "payment-handler", SupervisorStrategy.Resume))
val register = system.actorOf(SimpleSupervisor.props(Props(new Register), "register", SupervisorStrategy.Resume))
val relayer = system.actorOf(SimpleSupervisor.props(Relayer.props(nodeParams.privateKey, paymentHandler), "relayer", SupervisorStrategy.Resume))
val router = system.actorOf(SimpleSupervisor.props(Router.props(nodeParams, watcher), "router", SupervisorStrategy.Resume))
val router = if (spv) {
system.actorOf(SimpleSupervisor.props(YesRouter.props(nodeParams, watcher), "yes-router", SupervisorStrategy.Resume))
} else {
system.actorOf(SimpleSupervisor.props(Router.props(nodeParams, watcher), "router", SupervisorStrategy.Resume))
}
val switchboard = system.actorOf(SimpleSupervisor.props(Switchboard.props(nodeParams, watcher, router, relayer, wallet), "switchboard", SupervisorStrategy.Resume))
val paymentInitiator = system.actorOf(SimpleSupervisor.props(PaymentInitiator.props(nodeParams.privateKey.publicKey, router, register), "payment-initiator", SupervisorStrategy.Restart))
val server = system.actorOf(SimpleSupervisor.props(Server.props(nodeParams, switchboard, new InetSocketAddress(config.getString("server.binding-ip"), config.getInt("server.port")), Some(tcpBound)), "server", SupervisorStrategy.Restart))
val kit = Kit(
nodeParams = nodeParams,
@ -136,29 +105,9 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act
relayer = relayer,
router = router,
switchboard = switchboard,
paymentInitiator = paymentInitiator,
server = server)
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 = 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)
} yield kit
paymentInitiator = paymentInitiator)
kit
}
}
@ -171,9 +120,4 @@ case class Kit(nodeParams: NodeParams,
relayer: ActorRef,
router: ActorRef,
switchboard: ActorRef,
paymentInitiator: ActorRef,
server: ActorRef)
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")
paymentInitiator: ActorRef)

View file

@ -1,76 +0,0 @@
package fr.acinq.eclair.api
import fr.acinq.bitcoin.{BinaryData, Script, ScriptElt, Transaction}
import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, PublicKey, Scalar}
import fr.acinq.eclair.channel.State
import fr.acinq.eclair.crypto.ShaChain
import fr.acinq.eclair.transactions.Transactions.TransactionWithInputInfo
import org.json4s.CustomSerializer
import org.json4s.JsonAST.{JNull, JString}
/**
* Created by PM on 28/01/2016.
*/
class BinaryDataSerializer extends CustomSerializer[BinaryData](format => ( {
case JString(hex) if (false) => // NOT IMPLEMENTED
???
}, {
case x: BinaryData => JString(x.toString())
}
))
class StateSerializer extends CustomSerializer[State](format => ( {
case JString(x) if (false) => // NOT IMPLEMENTED
???
}, {
case x: State => JString(x.toString())
}
))
class ShaChainSerializer extends CustomSerializer[ShaChain](format => ( {
case JString(x) if (false) => // NOT IMPLEMENTED
???
}, {
case x: ShaChain => JNull
}
))
class PublicKeySerializer extends CustomSerializer[PublicKey](format => ( {
case JString(x) if (false) => // NOT IMPLEMENTED
???
}, {
case x: PublicKey => JString(x.toString())
}
))
class PrivateKeySerializer extends CustomSerializer[PrivateKey](format => ( {
case JString(x) if (false) => // NOT IMPLEMENTED
???
}, {
case x: PrivateKey => JString("XXX")
}
))
class PointSerializer extends CustomSerializer[Point](format => ( {
case JString(x) if (false) => // NOT IMPLEMENTED
???
}, {
case x: Point => JString(x.toString())
}
))
class ScalarSerializer extends CustomSerializer[Scalar](format => ( {
case JString(x) if (false) => // NOT IMPLEMENTED
???
}, {
case x: Scalar => JString("XXX")
}
))
class TransactionWithInputInfoSerializer extends CustomSerializer[TransactionWithInputInfo](format => ( {
case JString(x) if (false) => // NOT IMPLEMENTED
???
}, {
case x: TransactionWithInputInfo => JString(Transaction.write(x.tx).toString())
}
))

View file

@ -1,137 +0,0 @@
package fr.acinq.eclair.api
import java.net.InetSocketAddress
import akka.actor.ActorRef
import akka.http.scaladsl.model.HttpMethods._
import akka.http.scaladsl.model.StatusCodes
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.pattern.ask
import akka.util.Timeout
import de.heikoseeberger.akkahttpjson4s.Json4sSupport
import de.heikoseeberger.akkahttpjson4s.Json4sSupport.ShouldWritePretty
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{BinaryData, MilliSatoshi, Satoshi}
import fr.acinq.eclair.Kit
import fr.acinq.eclair.channel._
import fr.acinq.eclair.io.Switchboard.{NewChannel, NewConnection}
import fr.acinq.eclair.payment.{PaymentRequest, PaymentResult, ReceivePayment, SendPayment}
import fr.acinq.eclair.wire.{ChannelAnnouncement, NodeAnnouncement}
import grizzled.slf4j.Logging
import org.json4s.JsonAST.{JInt, JString}
import org.json4s.{JValue, jackson}
import scala.concurrent.duration._
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success}
/**
* Created by PM on 25/01/2016.
*/
// @formatter:off
case class JsonRPCBody(jsonrpc: String = "1.0", id: String = "scala-client", method: String, params: Seq[JValue])
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 ChannelInfo(shortChannelId: String, nodeId1: PublicKey , nodeId2: PublicKey)
// @formatter:on
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
implicit val timeout = Timeout(30 seconds)
implicit val shouldWritePretty: ShouldWritePretty = ShouldWritePretty.True
import Json4sSupport.{marshaller, unmarshaller}
def appKit: Kit
def getInfoResponse: Future[GetInfoResponse]
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 getChannel(channelId: String): Future[ActorRef] =
for {
channels <- (appKit.register ? 'channels).mapTo[Map[BinaryData, ActorRef]]
} yield channels.get(BinaryData(channelId)).getOrElse(throw new RuntimeException("unknown channel"))
val route =
respondWithDefaultHeaders(customHeaders) {
pathSingleSlash {
post {
entity(as[JsonRPCBody]) {
req =>
val kit = appKit
import kit._
val f_res: Future[AnyRef] = req match {
case JsonRPCBody(_, _, "getinfo", _) => getInfoResponse
case JsonRPCBody(_, _, "connect", JString(host) :: JInt(port) :: JString(nodeId) :: Nil) =>
(switchboard ? NewConnection(PublicKey(nodeId), new InetSocketAddress(host, port.toInt), None)).mapTo[String]
case JsonRPCBody(_, _, "open", JString(nodeId) :: JString(host) :: JInt(port) :: JInt(fundingSatoshi) :: JInt(pushMsat) :: options) =>
val channelFlags = options match {
case JInt(value) :: Nil => Some(value.toByte)
case _ => None // TODO: too lax?
}
(switchboard ? NewConnection(PublicKey(nodeId), new InetSocketAddress(host, port.toInt), Some(NewChannel(Satoshi(fundingSatoshi.toLong), MilliSatoshi(pushMsat.toLong), channelFlags)))).mapTo[String]
case JsonRPCBody(_, _, "peers", _) =>
(switchboard ? 'peers).mapTo[Map[PublicKey, ActorRef]].map(_.map(_._1.toBin))
case JsonRPCBody(_, _, "channels", _) =>
(register ? 'channels).mapTo[Map[Long, ActorRef]].map(_.keys)
case JsonRPCBody(_, _, "channel", JString(channelId) :: Nil) =>
getChannel(channelId).flatMap(_ ? CMD_GETINFO).mapTo[RES_GETINFO]
case JsonRPCBody(_, _, "allnodes", _) =>
(router ? 'nodes).mapTo[Iterable[NodeAnnouncement]].map(_.map(_.nodeId))
case JsonRPCBody(_, _, "allchannels", _) =>
(router ? 'channels).mapTo[Iterable[ChannelAnnouncement]].map(_.map(c => ChannelInfo(c.shortChannelId.toHexString, c.nodeId1, c.nodeId2)))
case JsonRPCBody(_,_, "receive", JInt(amountMsat) :: JString(description) :: Nil) =>
(paymentHandler ? ReceivePayment(MilliSatoshi(amountMsat.toLong), description)).mapTo[PaymentRequest].map(PaymentRequest.write)
case JsonRPCBody(_, _, "send", JInt(amountMsat) :: JString(paymentHash) :: JString(nodeId) :: Nil) =>
(paymentInitiator ? SendPayment(amountMsat.toLong, paymentHash, PublicKey(nodeId))).mapTo[PaymentResult]
case JsonRPCBody(_, _, "send", JString(paymentRequest) :: Nil) =>
for {
req <- Future(PaymentRequest.read(paymentRequest))
res <- (paymentInitiator ? SendPayment(req.amount.getOrElse(throw new RuntimeException("request without amounts are not supported")).amount, req.paymentHash, req.nodeId)).mapTo[PaymentResult]
} yield res
case JsonRPCBody(_, _, "close", JString(channelId) :: JString(scriptPubKey) :: Nil) =>
getChannel(channelId).flatMap(_ ? CMD_CLOSE(scriptPubKey = Some(scriptPubKey))).mapTo[String]
case JsonRPCBody(_, _, "close", JString(channelId) :: Nil) =>
getChannel(channelId).flatMap(_ ? CMD_CLOSE(scriptPubKey = None)).mapTo[String]
case JsonRPCBody(_, _, "help", _) =>
Future.successful(List(
"connect (host, port, nodeId): connect to another lightning node through a secure connection",
"open (nodeId, host, port, fundingSatoshi, pushMsat, channelFlags = 0x01): open a channel with another lightning node",
"peers: list existing local peers",
"channels: list existing local channels",
"channel (channelId): retrieve detailed information about a given channel",
"allnodes: list all known nodes",
"allchannels: list all known channels",
"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",
"close (channelId): close a channel",
"close (channelId, scriptPubKey): close a channel and send the funds to the given scriptPubKey",
"help: display this message"))
case _ => Future.failed(new RuntimeException("method not found"))
}
onComplete(f_res) {
case Success(res) => complete(JsonRPCRes(res, None, req.id))
case Failure(t) => complete(StatusCodes.InternalServerError, JsonRPCRes(null, Some(Error(-1, t.getMessage)), req.id))
}
}
}
}
}
}

View file

@ -1,33 +1,21 @@
package fr.acinq.eclair.blockchain.fee
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.model._
import akka.http.scaladsl.unmarshalling.Unmarshal
import akka.stream.ActorMaterializer
import fr.acinq.bitcoin.{Btc, Satoshi}
import org.json4s.JsonAST.{JDouble, JValue}
import org.json4s.{DefaultFormats, jackson}
import akka.http.scaladsl.unmarshalling.Unmarshal
import akka.stream.ActorMaterializer
import de.heikoseeberger.akkahttpjson4s.Json4sSupport._
import fr.acinq.eclair.HttpHelper.get
import grizzled.slf4j.Logging
import org.json4s.JsonAST.JDouble
import scala.concurrent.{ExecutionContext, Future}
/**
* Created by PM on 09/07/2017.
*/
class BitpayInsightFeeProvider(implicit system: ActorSystem, ec: ExecutionContext) extends FeeProvider {
implicit val materializer = ActorMaterializer()
val httpClient = Http(system)
implicit val serialization = jackson.Serialization
implicit val formats = DefaultFormats
class BitpayInsightFeeProvider(implicit system: ActorSystem, ec: ExecutionContext) extends FeeProvider with Logging {
override def getFeeratePerKB: Future[Long] =
for {
httpRes <- httpClient.singleRequest(HttpRequest(uri = Uri("https://test-insight.bitpay.com/api/utils/estimatefee?nbBlocks=3"), method = HttpMethods.GET))
json <- Unmarshal(httpRes).to[JValue]
json <- get("https://test-insight.bitpay.com/api/utils/estimatefee?nbBlocks=3")
JDouble(fee_per_kb) = json \ "3"
} yield (Btc(fee_per_kb): Satoshi).amount
}

View file

@ -3,17 +3,13 @@ package fr.acinq.eclair.blockchain.rpc
import java.io.IOException
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.marshalling.Marshal
import akka.http.scaladsl.model._
import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials}
import akka.http.scaladsl.unmarshalling.Unmarshal
import akka.stream.ActorMaterializer
import de.heikoseeberger.akkahttpjson4s.Json4sSupport._
import org.json4s.JsonAST.JValue
import org.json4s.{DefaultFormats, jackson}
import com.ning.http.client._
import org.json4s.{DefaultFormats, DefaultReaders}
import org.json4s.JsonAST.{JInt, JNull, JString, JValue}
import org.json4s.jackson.JsonMethods.parse
import org.json4s.jackson.Serialization._
import scala.concurrent.{ExecutionContext, Future}
import scala.concurrent.{ExecutionContext, Future, Promise}
// @formatter:off
case class JsonRPCRequest(jsonrpc: String = "1.0", id: String = "scala-client", method: String, params: Seq[Any])
@ -22,38 +18,50 @@ case class JsonRPCResponse(result: JValue, error: Option[Error], id: String)
case class JsonRPCError(error: Error) extends IOException(s"${error.message} (code: ${error.code})")
// @formatter:on
class BitcoinJsonRPCClient(user: String, password: String, host: String = "127.0.0.1", port: Int = 8332, ssl: Boolean = false)(implicit system: ActorSystem) {
class BitcoinJsonRPCClient(config: AsyncHttpClientConfig, host: String, port: Int, ssl: Boolean)(implicit system: ActorSystem) {
val scheme = if (ssl) "https" else "http"
val uri = Uri(s"$scheme://$host:$port")
def this(user: String, password: String, host: String = "127.0.0.1", port: Int = 8332, ssl: Boolean = false)(implicit system: ActorSystem) = this(
new AsyncHttpClientConfig.Builder()
.setRealm(new Realm.RealmBuilder().setPrincipal(user).setPassword(password).setUsePreemptiveAuth(true).setScheme(Realm.AuthScheme.BASIC).build)
.build,
host,
port,
ssl
)
val client: AsyncHttpClient = new AsyncHttpClient(config)
implicit val materializer = ActorMaterializer()
val httpClient = Http(system)
implicit val serialization = jackson.Serialization
implicit val formats = DefaultFormats
def invoke(method: String, params: Any*)(implicit ec: ExecutionContext): Future[JValue] =
for {
entity <- Marshal(JsonRPCRequest(method = method, params = params)).to[RequestEntity]
httpRes <- httpClient.singleRequest(HttpRequest(uri = uri, method = HttpMethods.POST).addHeader(Authorization(BasicHttpCredentials(user, password))).withEntity(entity))
jsonRpcRes <- Unmarshal(httpRes).to[JsonRPCResponse].map {
case JsonRPCResponse(_, Some(error), _) => throw JsonRPCError(error)
case o => o
} recover {
case t: Throwable if httpRes.status == StatusCodes.Unauthorized => throw new RuntimeException("bitcoind replied with 401/Unauthorized (bad user/password?)", t)
}
} yield jsonRpcRes.result
def invoke(method: String, params: Any*)(implicit ec: ExecutionContext): Future[JValue] = {
val promise = Promise[JValue]()
client
.preparePost((if (ssl) "https" else "http") + s"://$host:$port/")
.addHeader("Content-Type", "application/json")
.setBody(write(JsonRPCRequest(method = method, params = params)))
.execute(new AsyncCompletionHandler[Unit] {
override def onCompleted(response: Response): Unit =
try {
val jvalue = parse(response.getResponseBody)
val jerror = jvalue \ "error"
val result = jvalue \ "result"
if (jerror != JNull) {
for {
JInt(code) <- jerror \ "code"
JString(message) <- jerror \ "message"
} yield promise.failure(new JsonRPCError(Error(code.toInt, message)))
} else {
promise.success(result)
}
} catch {
case t: Throwable => promise.failure(t)
}
def invoke(request: Seq[(String, Seq[Any])])(implicit ec: ExecutionContext): Future[Seq[JValue]] =
for {
entity <- Marshal(request.map(r => JsonRPCRequest(method = r._1, params = r._2))).to[RequestEntity]
httpRes <- httpClient.singleRequest(HttpRequest(uri = uri, method = HttpMethods.POST).addHeader(Authorization(BasicHttpCredentials(user, password))).withEntity(entity))
jsonRpcRes <- Unmarshal(httpRes).to[Seq[JsonRPCResponse]].map {
//case JsonRPCResponse(_, Some(error), _) => throw JsonRPCError(error)
case o => o
} recover {
case t: Throwable if httpRes.status == StatusCodes.Unauthorized => throw new RuntimeException("bitcoind replied with 401/Unauthorized (bad user/password?)", t)
}
} yield jsonRpcRes.map(_.result)
override def onThrowable(t: Throwable): Unit = promise.failure(t)
})
promise.future
}
def invoke(request: Seq[(String, Seq[Any])])(implicit ec: ExecutionContext): Future[Seq[JValue]] = ???
}

View file

@ -9,7 +9,7 @@ import fr.acinq.eclair.blockchain.spv.BitcoinjKit._
import fr.acinq.eclair.blockchain.{CurrentBlockCount, NewConfidenceLevel}
import grizzled.slf4j.Logging
import org.bitcoinj.core.TransactionConfidence.ConfidenceType
import org.bitcoinj.core.listeners._
import org.bitcoinj.core.listeners.{NewBestBlockListener, PeerConnectedEventListener, TransactionConfidenceEventListener}
import org.bitcoinj.core.{NetworkParameters, Peer, StoredBlock, Transaction => BitcoinjTransaction}
import org.bitcoinj.kits.WalletAppKit
import org.bitcoinj.params.{RegTestParams, TestNet3Params}

View file

@ -0,0 +1,35 @@
package fr.acinq.eclair.blockchain.spv
import java.io.File
import akka.actor.ActorSystem
import fr.acinq.bitcoin.Transaction
import fr.acinq.eclair.Globals
import fr.acinq.eclair.blockchain.spv.BitcoinjKit._
import fr.acinq.eclair.blockchain.{CurrentBlockCount, NewConfidenceLevel}
import grizzled.slf4j.Logging
import org.bitcoinj.core.listeners.{NewBestBlockListener, PeerConnectedEventListener, TransactionConfidenceEventListener}
import org.bitcoinj.core.{Peer, StoredBlock, Transaction => BitcoinjTransaction}
import org.bitcoinj.kits.WalletAppKit
import org.bitcoinj.wallet.Wallet
import scala.concurrent.Promise
import scala.util.{Failure, Success, Try}
/**
* Created by PM on 09/07/2017.
*/
class BitcoinjKit2(chain: String, datadir: File) extends WalletAppKit(chain2Params(chain), datadir, "bitcoinj-wallet", true) with Logging {
// so that we know when the peerGroup/chain/wallet are accessible
private val initializedPromise = Promise[Boolean]()
val initialized = initializedPromise.future
override def onSetupCompleted(): Unit = {
initializedPromise.success(true)
}
}

View file

@ -0,0 +1,47 @@
package fr.acinq.eclair.blockchain.wallet
import fr.acinq.bitcoin.Crypto.PrivateKey
import fr.acinq.bitcoin.{Base58Check, BinaryData, Satoshi, Transaction, TxIn, TxOut}
import fr.acinq.eclair.HttpHelper.get
import org.json4s.JsonAST.{JField, JInt, JObject, JString}
import scala.concurrent.{ExecutionContext, Future}
/**
* Created by PM on 06/07/2017.
*/
class APIWallet(implicit ec: ExecutionContext) extends EclairWallet {
val priv = PrivateKey(Base58Check.decode("cVa6PtdYqbfpM6oH1zhz8TnDaRTCdA4okv6x6SGxZhDcCztpPh6e")._2, compressed = true)
val addr = "2MviVGDzjXmxaZNYYm12F6HfUDs19HH3YxZ"
def getBalance: Future[Satoshi] = {
for {
JInt(balance) <- get(s"https://test-insight.bitpay.com/api/addr/$addr/balance")
} yield Satoshi(balance.toLong)
}
override def getFinalAddress: Future[String] = Future.successful(addr)
override def makeFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] =
for {
address <- get(s"https://testnet-api.smartbit.com.au/v1/blockchain/address/$addr/unspent")
utxos = for {
JObject(utxo) <- address \ "unspent"
JField("txid", JString(txid)) <- utxo
JField("value_int", JInt(value_int)) <- utxo
JField("n", JInt(n)) <- utxo
} yield Utxo(txid = txid, n = n.toInt, value = value_int.toInt)
// now we create the funding tx
partialFundingTx = Transaction(
version = 2,
txIn = Seq.empty[TxIn],
txOut = TxOut(amount, pubkeyScript) :: Nil,
lockTime = 0)
} yield {
val fundingTx = MiniWallet.fundTransaction(partialFundingTx, utxos, Satoshi(1000000), priv)
MakeFundingTxResponse(fundingTx, 0)
}
override def commit(tx: Transaction): Future[Boolean] = Future.successful(true) // not implemented
}

View file

@ -0,0 +1,70 @@
package fr.acinq.eclair.blockchain.wallet
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.{Base58, Base58Check, BinaryData, Crypto, OP_PUSHDATA, OutPoint, Satoshi, Script, ScriptWitness, SigVersion, Transaction, TxIn, TxOut, _}
import scala.annotation.tailrec
/**
* Created by PM on 30/05/2017.
*/
case class Utxo(txid: String, n: Int, value: Long)
object MiniWallet {
/**
* see https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#P2WPKH_nested_in_BIP16_P2SH
*
* @param publicKey public key
* @return the P2SH(P2WPKH(publicKey)) address
*/
def witnessAddress(publicKey: PublicKey): String = Base58Check.encode(Base58.Prefix.ScriptAddressTestnet, Crypto.hash160(Script.write(Script.pay2wpkh(publicKey))))
def witnessAddress(privateKey: PrivateKey): String = witnessAddress(privateKey.publicKey)
/**
*
* @param tx transction to fund. must have no inputs
* @param utxos UTXOS to spend from. They must all send to P2SH(P2WPKH(privateKey.publicKey))
* @param fee network fee
* @param privateKey private key that control all utxos
* @return a signed transaction that may include an additional change outputs (that sends to P2SH(P2WPKH(privateKey.publicKey)))
*/
def fundTransaction(tx: Transaction, utxos: Seq[Utxo], fee: Satoshi, privateKey: PrivateKey) = {
require(tx.txIn.isEmpty, s"cannot fund a tx that alray has inputs ")
val totalOut = tx.txOut.map(_.amount).sum
val sortedUtxos = utxos.sortBy(_.value)
@tailrec
def select(candidates: Seq[Utxo], remaining: Seq[Utxo]): Seq[Utxo] = {
if (Satoshi(candidates.map(_.value).sum) > totalOut) candidates
else if (remaining.isEmpty) throw new RuntimeException("not enough funds")
else select(candidates :+ remaining.head, remaining.tail)
}
// select candidates
val candidates = select(Nil, sortedUtxos)
val inputs = candidates.map(utxo => TxIn(OutPoint(BinaryData(utxo.txid).reverse, utxo.n), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL))
val tx1 = tx.copy(txIn = inputs)
val totalIn = Satoshi(candidates.map(_.value).sum)
// add a change output if necessary
var tx2 = if (totalIn - totalOut > fee) {
val changeOutput = TxOut(amount = totalIn - totalOut - fee, publicKeyScript = Script.pay2sh(Script.pay2wpkh(privateKey.publicKey)))
tx1.copy(txOut = tx1.txOut :+ changeOutput)
} else tx1
// all our utxos are P2SH(P2WPKH) which is the recommended way of using segwit right now
// so to sign an input we need to provide
// a witness with the right signature and pubkey
// and a signature script with a script that is the preimage of our p2sh output
val script = Script.write(Script.pay2wpkh(privateKey.publicKey))
for (i <- 0 until tx2.txIn.length) yield {
val sig = Transaction.signInput(tx2, i, Script.pay2pkh(privateKey.publicKey), SIGHASH_ALL, Satoshi(candidates(i).value), SigVersion.SIGVERSION_WITNESS_V0, privateKey)
val witness = ScriptWitness(Seq(sig, privateKey.publicKey))
tx2 = tx2.updateWitness(i, witness).updateSigScript(i, OP_PUSHDATA(script) :: Nil)
}
tx2
}
}

View file

@ -356,10 +356,15 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
deferred.map(self ! _)
goto(WAIT_FOR_FUNDING_LOCKED) using store(DATA_WAIT_FOR_FUNDING_LOCKED(commitments, fundingLocked)) sending fundingLocked
case Event(WatchEventDoubleSpent(_), d: DATA_WAIT_FOR_FUNDING_CONFIRMED) =>
log.error(s"fundingTx=${d.commitments.commitInput.outPoint.txid} was doublespent and won't ever confirm!")
val error = Error(d.channelId, "Funding tx doublespent".getBytes)
goto(ERR_FUNDING_DOUBLESPENT) sending error
// TODO: not implemented, maybe should be done with a state timer and not a blockchain watch?
case Event(BITCOIN_FUNDING_TIMEOUT, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) =>
val error = Error(d.channelId, "Funding tx timed out".getBytes)
goto(CLOSED) sending error
goto(ERR_FUNDING_TIMEOUT) sending error
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_WAIT_FOR_FUNDING_CONFIRMED) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d)
@ -1054,12 +1059,20 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: HasCommitments) => handleRemoteSpentOther(tx, d)
})
when(ERR_INFORMATION_LEAK, stateTimeout = 10 seconds) {
def errorStateHandler: StateFunction = {
case Event(StateTimeout, _) =>
log.info("shutting down")
log.error(s"shutting down (was in state=$stateName)")
stop(FSM.Normal)
}
when(ERR_INFORMATION_LEAK, stateTimeout = 10 seconds)(errorStateHandler)
when(ERR_FUNDING_DOUBLESPENT, stateTimeout = 10 seconds)(errorStateHandler)
when(ERR_FUNDING_TIMEOUT, stateTimeout = 10 seconds)(errorStateHandler)
when(ERR_FUNDING_LOST, stateTimeout = 10 seconds)(errorStateHandler)
whenUnhandled {
case Event(INPUT_PUBLISH_LOCALCOMMIT, d: HasCommitments) => handleLocalError(ForcedLocalCommit("manual unilateral close"), d)

View file

@ -43,6 +43,7 @@ case object CLOSED extends State
case object OFFLINE extends State
case object SYNCING extends State
case object ERR_FUNDING_LOST extends State
case object ERR_FUNDING_DOUBLESPENT extends State
case object ERR_FUNDING_TIMEOUT extends State
case object ERR_INFORMATION_LEAK extends State

View file

@ -1,8 +1,8 @@
package fr.acinq.eclair.db
import java.io.File
import java.nio.file.Files
import com.google.common.io.Files
import fr.acinq.bitcoin.BinaryData
import grizzled.slf4j.Logging
@ -17,10 +17,10 @@ case class SimpleFileDb(root: File) extends SimpleDb with Logging {
override def put(key: String, value: BinaryData): Unit = {
logger.debug(s"put $key -> $value")
Files.write(new File(root, key).toPath, value)
Files.write(value, new File(root, key))
}
override def get(key: String): Option[BinaryData] = Try(Files.readAllBytes(new File(root, key).toPath)).toOption.map(a => BinaryData(a))
override def get(key: String): Option[BinaryData] = Try(Files.toByteArray(new File(root, key))).toOption.map(a => BinaryData(a))
override def delete(key: String): Boolean = new File(root, key).delete()

View file

@ -57,6 +57,7 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, address_opt: Option[
channel ! INPUT_RESTORED(state)
HotChannel(FinalChannelId(state.channelId), channel)
}, attempts = 0))
setTimer(RECONNECT_TIMER, Reconnect, 1 second, repeat = false)
when(DISCONNECTED) {
case Event(c: NewChannel, d@DisconnectedData(offlineChannels, _)) =>
@ -95,9 +96,10 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, address_opt: Option[
case Event(remoteInit: Init, InitializingData(transport, offlineChannels)) =>
log.info(s"$remoteNodeId has features: initialRoutingSync=${Features.initialRoutingSync(remoteInit.localFeatures)}")
if (Features.areSupported(remoteInit.localFeatures)) {
/* disabled for performance reasons
if (Features.initialRoutingSync(remoteInit.localFeatures)) {
router ! SendRoutingState(transport)
}
}*/
// let's bring existing/requested channels online
val channels: Map[ChannelId, ActorRef] = offlineChannels.map {
case BrandNewChannel(c) =>

View file

@ -13,7 +13,7 @@ import fr.acinq.eclair.channel._
import fr.acinq.eclair.io.Peer
import fr.acinq.eclair.transactions.Scripts
import fr.acinq.eclair.wire._
import org.jgrapht.alg.shortestpath.DijkstraShortestPath
import org.jgrapht.alg.DijkstraShortestPath
import org.jgrapht.ext._
import org.jgrapht.graph.{DefaultDirectedGraph, DefaultEdge, SimpleGraph}
@ -334,7 +334,7 @@ object Router {
g.addEdge(d.a, d.b, new DescEdge(d))
})
Try(Option(DijkstraShortestPath.findPathBetween(g, localNodeId, targetNodeId))) match {
case Success(Some(path)) => path.getEdgeList.map(_.desc)
case Success(Some(path)) => path.map(_.desc)
case _ => throw RouteNotFound
}
}
@ -344,39 +344,7 @@ object Router {
.map(desc => Hop(desc.a, desc.b, updates(desc)))
}
def graph2dot(nodes: Map[PublicKey, NodeAnnouncement], channels: Map[Long, ChannelAnnouncement])(implicit ec: ExecutionContext): Future[String] = Future {
case class DescEdge(channelId: Long) extends DefaultEdge
val g = new SimpleGraph[PublicKey, DescEdge](classOf[DescEdge])
channels.foreach(d => {
g.addVertex(d._2.nodeId1)
g.addVertex(d._2.nodeId2)
g.addEdge(d._2.nodeId1, d._2.nodeId2, new DescEdge(d._1))
})
val vertexIDProvider = new ComponentNameProvider[PublicKey]() {
override def getName(nodeId: PublicKey): String = "\"" + nodeId.toString() + "\""
}
val edgeLabelProvider = new ComponentNameProvider[DescEdge]() {
override def getName(e: DescEdge): String = e.channelId.toString
}
val vertexAttributeProvider = new ComponentAttributeProvider[PublicKey]() {
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 None => Map.empty[String, String]
}
}
val exporter = new DOTExporter[PublicKey, DescEdge](vertexIDProvider, null, edgeLabelProvider, vertexAttributeProvider, null)
val writer = new StringWriter()
try {
exporter.exportGraph(g, writer)
writer.toString
} finally {
writer.close()
}
}
def graph2dot(nodes: Map[PublicKey, NodeAnnouncement], channels: Map[Long, ChannelAnnouncement])(implicit ec: ExecutionContext): Future[String] = ???
}

View file

@ -138,13 +138,8 @@ class YesRouter(nodeParams: NodeParams, watcher: ActorRef) extends FSM[State, Da
case Event('tick_validate, d) => stay // ignored
case Event('tick_broadcast, d) =>
d.rebroadcast match {
case Nil => stay using d.copy(origins = Map.empty)
case _ =>
log.info(s"broadcasting ${d.rebroadcast.size} routing messages")
context.actorSelection(context.system / "*" / "switchboard") ! Rebroadcast(d.rebroadcast, d.origins)
stay using d.copy(rebroadcast = Nil, origins = Map.empty)
}
// no-op to save bandwidth and also to not broadcast announcements that we have not verified
stay using d.copy(rebroadcast = Nil, origins = Map.empty)
case Event('nodes, d) =>
sender ! d.nodes.values

View file

@ -68,7 +68,7 @@ class WaitForFundingConfirmedStateSpec extends TestkitBaseClass with StateTestsH
within(30 seconds) {
alice ! BITCOIN_FUNDING_TIMEOUT
alice2bob.expectMsgType[Error]
awaitCond(alice.stateName == CLOSED)
awaitCond(alice.stateName == ERR_FUNDING_TIMEOUT)
}
}

View file

@ -123,17 +123,18 @@ class TransportHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLik
probe1.expectTerminated(pipe)
}
test("failed handshake") {
val pipe = system.actorOf(Props[MyPipe])
val probe1 = TestProbe()
val supervisor = TestActorRef(Props(new MySupervisor()))
val initiator = TestFSMRef(new TransportHandler(Initiator.s, Some(Initiator.s.pub), pipe, LightningMessageCodecs.varsizebinarydata), supervisor, "ini")
val responder = TestFSMRef(new TransportHandler(Responder.s, None, pipe, LightningMessageCodecs.varsizebinarydata), supervisor, "res")
probe1.watch(responder)
pipe ! (initiator, responder)
probe1.expectTerminated(responder, 3 seconds)
}
// NOTE: disabled because TestFSMRef interface changed between akka versions
// test("failed handshake") {
// val pipe = system.actorOf(Props[MyPipe])
// val probe1 = TestProbe()
// val supervisor = TestActorRef(Props(new MySupervisor()))
// val initiator = TestFSMRef(new TransportHandler(Initiator.s, Some(Initiator.s.pub), pipe, LightningMessageCodecs.varsizebinarydata), supervisor, "ini")
// val responder = TestFSMRef(new TransportHandler(Responder.s, None, pipe, LightningMessageCodecs.varsizebinarydata), supervisor, "res")
// probe1.watch(responder)
// pipe ! (initiator, responder)
//
// probe1.expectTerminated(responder, 3 seconds)
// }
test("key rotation") {

View file

@ -26,7 +26,7 @@ import org.json4s.JsonAST.JValue
import org.json4s.{DefaultFormats, JString}
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
import org.scalatest.{BeforeAndAfterAll, FunSuiteLike}
import org.scalatest.{BeforeAndAfterAll, FunSuiteLike, Ignore}
import scala.concurrent.Await
import scala.concurrent.ExecutionContext.Implicits.global
@ -37,6 +37,7 @@ import scala.sys.process._
* Created by PM on 15/03/2017.
*/
@RunWith(classOf[JUnitRunner])
@Ignore
class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with BeforeAndAfterAll with Logging {
val INTEGRATION_TMP_DIR = s"${System.getProperty("buildDirectory")}/integration-${UUID.randomUUID().toString}"
@ -79,7 +80,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
nodes.foreach {
case (name, setup) =>
logger.info(s"stopping node $name")
setup.system.terminate()
setup.system.shutdown()
}
// logger.warn(s"starting bitcoin-qt")
// val PATH_BITCOINQT = new File(System.getProperty("buildDirectory"), "bitcoin-0.14.0/bin/bitcoin-qt").toPath

View file

@ -144,7 +144,7 @@ class RouterSpec extends BaseRouterSpec {
sender.expectMsg(Failure(RouteNotFound))
}
test("export graph in dot format") { case (router, _) =>
ignore("export graph in dot format") { case (router, _) =>
val sender = TestProbe()
sender.send(router, 'dot)
val dot = sender.expectMsgType[String]

View file

@ -1,121 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>fr.acinq.eclair</groupId>
<artifactId>eclair_2.11</artifactId>
<version>0.2-spv-SNAPSHOT</version>
</parent>
<artifactId>eclair-node-gui_2.11</artifactId>
<packaging>jar</packaging>
<name>eclair-node-gui</name>
<build>
<plugins>
<plugin>
<groupId>pl.project13.maven</groupId>
<artifactId>git-commit-id-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>revision</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
<manifestEntries>
<!-- we hide the git commit in the Specification-Version standard field-->
<Specification-Version>${git.commit.id}</Specification-Version>
<Url>${project.parent.url}</Url>
</manifestEntries>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>com.github.chrisdchristo</groupId>
<artifactId>capsule-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>build</goal>
</goals>
<configuration>
<appClass>fr.acinq.eclair.JavafxBoot</appClass>
<type>fat</type>
<fileName>${project.name}-${project.version}</fileName>
<fileDesc>-${git.commit.id.abbrev}</fileDesc>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>installer</id>
<build>
<plugins>
<plugin>
<groupId>com.zenjava</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>8.8.3</version>
<executions>
<execution>
<!-- required before build-native -->
<id>create-jfxjar</id>
<phase>package</phase>
<goals>
<goal>build-jar</goal>
</goals>
</execution>
<execution>
<phase>package</phase>
<goals>
<goal>build-native</goal>
</goals>
</execution>
</executions>
<configuration>
<vendor>ACINQ</vendor>
<needShortcut>true</needShortcut>
<appName>Eclair</appName>
<nativeReleaseVersion>${project.version}</nativeReleaseVersion>
<skipNativeVersionNumberSanitizing>true</skipNativeVersionNumberSanitizing>
<nativeOutputDir>${project.build.directory}/jfx/installer</nativeOutputDir>
<mainClass>fr.acinq.eclair.JavafxBoot</mainClass>
<verbose>false</verbose>
<bundler>EXE</bundler>
<updateExistingJar>true</updateExistingJar>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<dependencies>
<dependency>
<groupId>fr.acinq.eclair</groupId>
<artifactId>eclair-node_${scala.version.short}</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.3.0</version>
</dependency>
</dependencies>
</project>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View file

@ -1,132 +0,0 @@
/* ---------- Root ---------- */
.root {
-fx-font-size: 14px;
-fx-text-fill: rgb(80, 82, 84);
}
/* ---------- Text Utilities (color, weight) ---------- */
.text-mono {
-fx-font-family: monospace;
}
.text-strong {
-fx-font-weight: bold;
}
.text-sm {
-fx-font-size: 12px;
}
.text-error {
-fx-text-fill: rgb(216,31,74);
-fx-font-size: 11px;
}
.text-error.text-error-downward {
-fx-translate-y: 24px;
}
.text-error.text-error-upward {
-fx-translate-y: -24px;
}
.label-description {
-fx-text-fill: rgb(146,149,151);
-fx-font-size: 11px;
}
.link {
-fx-text-fill: rgb(25,157,221);
-fx-fill: rgb(25,157,221);
-fx-underline: true;
-fx-cursor: hand;
}
.link:hover {
-fx-text-fill: rgb(93,199,254);
-fx-fill: rgb(93,199,254);
}
.text-muted,
.label.text-muted {
-fx-text-fill: rgb(146,149,151);
}
.align-right {
/* useful for table columns */
-fx-alignment: CENTER_RIGHT;
}
/* ---------- Context Menu ---------- */
.context-menu {
-fx-padding: 4px;
-fx-font-weight: normal;
-fx-font-size: 12px;
}
.context-menu .menu-item:focused {
-fx-background-color: rgb(63,179,234);
}
.context-menu .menu-item:focused .label {
-fx-text-fill: white;
}
.context-menu .separator {
-fx-padding: 2px 0;
}
.menu-bar .context-menu {
/* font size in menu context popup is standard */
-fx-font-size: 14px;
}
/* ---------- Grid Structure ---------- */
.grid {
-fx-vgap: 1em;
-fx-hgap: 1em;
-fx-padding: 1em;
}
/* ------------- Not Editable TextFields ------------- */
/* Java FX text can only be selected if in a TextField or TextArea */
/* Make it look like a standard label with the editable = false prop and a special styling */
.text-area.noteditable,
.text-field.noteditable,
.text-field.noteditable:hover,
.text-field.noteditable:focused,
.noteditable {
-fx-background-color: transparent;
-fx-border-width: 0;
-fx-border-color: transparent;
-fx-padding: 0;
}
.text-area.noteditable .scroll-pane {
-fx-background-color: transparent;
}
.text-area.noteditable .scroll-pane .viewport{
-fx-background-color: transparent;
}
.text-area.noteditable .scroll-pane .content{
-fx-background-color: transparent;
-fx-padding: 0;
}
/* ---------- Progress Bar ---------- */
.bar {
-fx-background-color: rgb(63,179,234);
-fx-background-insets: 0;
-fx-background-radius: 0;
}
.track {
-fx-background-color: rgb(206,230,255);
-fx-background-insets: 0;
-fx-background-radius: 0;
}
/* ---------- Forms ----------- */
.options-separator {
-fx-translate-y: -7px;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 318 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 513 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 524 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 526 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 513 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

View file

@ -1,68 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import java.net.URL?>
<VBox fx:id="root" styleClass="channel" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"
onContextMenuRequested="#openChannelContext">
<children>
<GridPane styleClass="grid" prefWidth="400.0">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" minWidth="140.0" prefWidth="150.0" maxWidth="180.0"/>
<ColumnConstraints hgrow="ALWAYS" minWidth="30.0" prefWidth="30.0" />
<ColumnConstraints hgrow="SOMETIMES" minWidth="40.0" prefWidth="60.0" maxWidth="60.0"/>
<ColumnConstraints hgrow="ALWAYS" minWidth="30.0" prefWidth="40.0" />
</columnConstraints>
<rowConstraints>
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
<RowConstraints minHeight="4.0" vgrow="SOMETIMES"/>
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
</rowConstraints>
<children>
<HBox GridPane.columnSpan="4" GridPane.columnIndex="0" alignment="CENTER" spacing="10.0">
<children>
<TextField fx:id="channelId" text="N/A" editable="false" styleClass="noteditable, text-strong"
HBox.hgrow="ALWAYS" focusTraversable="false" />
<HBox GridPane.columnIndex="4" GridPane.halignment="RIGHT" alignment="CENTER_RIGHT" HBox.hgrow="NEVER" spacing="5.0">
<children>
<Button fx:id="close" mnemonicParsing="false" styleClass="close-channel" text="Close"/>
</children>
</HBox>
</children>
</HBox>
<ProgressBar fx:id="balanceBar" minHeight="4.0" prefHeight="4.0" maxWidth="1.7976931348623157E308"
progress="0.0" snapToPixel="false" focusTraversable="false"
GridPane.columnSpan="4" GridPane.hgrow="ALWAYS" GridPane.rowIndex="1"/>
<Label styleClass="text-muted" text="Remote node id" GridPane.columnIndex="0" GridPane.rowIndex="2"/>
<TextField fx:id="nodeId" text="N/A" focusTraversable="false" editable="false" styleClass="noteditable"
GridPane.columnIndex="1" GridPane.columnSpan="3" GridPane.rowIndex="2"/>
<Label styleClass="text-muted" text="Your balance (milliBTC)" GridPane.rowIndex="3"/>
<TextField fx:id="amountUs" text="N/A" focusTraversable="false" editable="false" styleClass="noteditable"
GridPane.columnIndex="1" GridPane.rowIndex="3"/>
<Label styleClass="text-muted" text="Capacity (milliBTC)" GridPane.rowIndex="4"/>
<TextField fx:id="capacity" text="N/A" focusTraversable="false" editable="false" styleClass="noteditable"
GridPane.columnIndex="1" GridPane.rowIndex="4"/>
<Label styleClass="text-muted" text="Funder" GridPane.columnIndex="2" GridPane.rowIndex="3"/>
<TextField fx:id="funder" text="N/A" focusTraversable="false" editable="false" styleClass="noteditable"
GridPane.columnIndex="3" GridPane.rowIndex="3"/>
<Label styleClass="text-muted" text="State" GridPane.columnIndex="2" GridPane.rowIndex="4"/>
<TextField fx:id="state" text="N/A" focusTraversable="false" editable="false" styleClass="noteditable"
GridPane.columnIndex="3" GridPane.rowIndex="4"/>
</children>
</GridPane>
<HBox styleClass="channel-separator"/>
</children>
<stylesheets>
<URL value="@../commons/globals.css"/>
</stylesheets>
</VBox>

View file

@ -1,210 +0,0 @@
/* ---------- Status Bar ---------- */
.status-bar {
-fx-padding: .5em 1em;
-fx-background-color: rgb(221,221,221);
-fx-border-width: 1px 0 0 0;
-fx-border-color: rgb(181,181,181);
}
.status-bar .separator:vertical {
-fx-padding: -.5em 9px -.5em 0;
}
.status-bar .separator:vertical .line {
-fx-background-color: rgb(210,210,210);
-fx-border-width: 0 1px 0 0;
-fx-border-insets: 0;
}
.status-bar .label {
-fx-padding: 2px 0px 2px 5px;
-fx-font-size: 12px;
}
/* ---------- Bitcoin Chain Color ---------- */
.status-bar .label.chain {
-fx-text-fill: rgb(255,148,40);
}
.status-bar .label.chain.regtest {
-fx-text-fill: rgb(20,208,255);
}
.status-bar .label.chain.testnet, .status-bar .label.chain.test {
-fx-text-fill: rgb(54,207,26);
}
.status-bar .label.chain.segnet4 {
-fx-text-fill: rgb(87,67,246);
}
/* ---------- Protocol badges ---------- */
.label.badge {
-fx-font-size: 11px;
-fx-text-fill: rgb(160,160,160);
}
/* ---------- Channels ---------- */
.channel {
-fx-padding: 0;
}
.channel .grid {
-fx-padding: .5em;
-fx-vgap: .5em;
-fx-hgap: .25em;
-fx-font-size: 12px;
}
.channel-separator {
-fx-background-color: rgb(220,220,220);
-fx-pref-height: 1px;
-fx-padding: 0 -.25em;
}
.channels-info {
-fx-padding: 4em 0 0 0;
}
.channel-container {
-fx-padding: 0;
}
.context-menu.context-channel .menu-item .label {
-fx-font-size: 12px;
}
.tab:top:selected {
-fx-focus-color: transparent;
-fx-faint-focus-color: transparent;
}
/* ---------- Table ---------- */
.table-view {
-fx-focus-color: transparent;
-fx-faint-focus-color: transparent;
-fx-background-insets: 0, 0, 0, 0;
-fx-border-insets: 0, 0, 0, 0;
-fx-padding: 0;
-fx-border-color: rgb(200, 200, 200);
}
.table-column:last-visible {
-fx-background-insets: 0px, 0px 0px 1px 0, 1px 1px 2px 1px;
}
/* ---------- Notifications ---------- */
.notifications-box {
-fx-background-color: transparent;
-fx-padding: 0;
}
.notification-pane.grid {
-fx-background-color: #252525;
-fx-border-width: 0 0 0 3px;
-fx-padding: 1em;
-fx-vgap: 5px;
-fx-hgap: 1em;
}
.notification-pane .label {
-fx-text-fill: rgb(255, 255, 255);
}
.notification-pane .label.notification-title {
-fx-text-fill: rgb(220, 220, 220);
}
.notification-pane .label.notification-message {
-fx-font-size: 18px;
-fx-font-weight: bold;
}
.button.notification-close {
-fx-background-color: transparent;
-fx-background-image: url("../commons/images/close.png");
-fx-background-repeat: no-repeat;
-fx-background-size: 12px;
-fx-background-position: center center;
-fx-border-color: transparent;
-fx-padding: 0;
-fx-translate-y: -7px;
-fx-translate-x: -3px;
}
.button.notification-close:hover,
.button.notification-close:pressed {
-fx-background-color: #353535;
}
/* ------------- Activity tab -------------- */
.activities-tab.tab-pane > *.tab-header-area {
-fx-padding: 0;
}
.activities-tab.tab-pane > *.tab-header-area > *.tab-header-background {
-fx-background-color: rgb(244,244,244);
}
/* header buttons style */
.activities-tab.tab-pane .tab:top {
-fx-padding: 0.25em 1em;
-fx-background-color: transparent;
-fx-focus-color: transparent;
-fx-faint-focus-color: transparent;
-fx-background-insets: 0;
-fx-border-width: 0;
}
/* header buttons style */
.activities-tab.tab-pane .tab:top .text {
-fx-fill: rgb(100, 104, 108);
}
.activities-tab.tab-pane .tab:top:selected .text {
-fx-font-weight: bold;
-fx-fill: rgb(0, 0, 0);
}
/* table style */
.activities-tab .table-view {
-fx-border-width: 1px 0 0 0;
-fx-font-size: 12px;
}
.label.activity-disclaimer {
-fx-font-size: 10px;
-fx-text-fill: rgb(166,169,171);
-fx-padding: 2px 7px 0 0;
}
/* --------------- Blocker modal ----------------- */
.blocker-cover {
-fx-background-color: rgba(0,0,0,0.3);
}
.blocker-dialog {
-fx-padding: 15px;
-fx-border-width: 1px;
-fx-border-color: #888888;
-fx-background-color: #f4f4f4;
}
/* -------------- Receive Modal ------------------ */
.result-box {
-fx-background-color: #ffffff;
-fx-border-width: 1px 0 0 0;
-fx-border-color: rgb(210,210,210);
}
.button.copy-clipboard {
-fx-background-color: rgb(240,240,240);
-fx-background-image: url("../commons/images/copy_icon.png");
-fx-background-repeat: no-repeat;
-fx-background-size: 12px;
-fx-background-position: 4px center;
-fx-border-color: transparent;
-fx-padding: 2px 4px 2px 20px;
-fx-font-size: 12px;
}
.button.copy-clipboard:hover {
-fx-background-color: rgb(230,232,235);
}
.button.copy-clipboard:pressed {
-fx-background-color: rgb(220,222,225);
}

View file

@ -1,229 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.*?>
<?import java.net.URL?>
<?import javafx.scene.shape.Rectangle?>
<AnchorPane fx:id="root" minHeight="300.0" prefHeight="400.0" styleClass="root" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
<children>
<BorderPane AnchorPane.leftAnchor="0" AnchorPane.rightAnchor="0" AnchorPane.topAnchor="0" AnchorPane.bottomAnchor="0">
<center>
<TabPane tabClosingPolicy="UNAVAILABLE" BorderPane.alignment="CENTER">
<tabs>
<Tab fx:id="channelsTab" closable="false" text="Local Channels">
<content>
<StackPane>
<children>
<ScrollPane fitToWidth="true" styleClass="channel-container">
<content>
<VBox fx:id="channelBox"/>
</content>
</ScrollPane>
<VBox fx:id="channelInfo" alignment="TOP_CENTER" styleClass="channels-info">
<children>
<Label styleClass="text-strong" text="No channels opened yet..."/>
<Label styleClass="text-muted"
text="You can open a new channel by clicking on &quot;Channels&quot; &gt; &quot;Open Channel...&quot;"
wrapText="true"/>
</children>
</VBox>
</children>
</StackPane>
</content>
</Tab>
<Tab text="All Nodes" fx:id="networkNodesTab" closable="false">
<content>
<VBox spacing="10.0" styleClass="grid">
<children>
<TableView fx:id="networkNodesTable" minHeight="50.0" prefHeight="5000.0">
<columnResizePolicy><TableView fx:constant="CONSTRAINED_RESIZE_POLICY"/></columnResizePolicy>
<columns>
<TableColumn fx:id="networkNodesRGBColumn" minWidth="20.0" prefWidth="20.0" maxWidth="20.0" text="" sortable="false"/>
<TableColumn fx:id="networkNodesAliasColumn" minWidth="80.0" prefWidth="180.0" maxWidth="300.0" text="Alias"/>
<TableColumn fx:id="networkNodesIdColumn" text="Node Id"/>
<TableColumn fx:id="networkNodesIPColumn" minWidth="150.0" prefWidth="250.0" maxWidth="300.0" text="IP"/>
</columns>
</TableView>
</children>
</VBox>
</content>
</Tab>
<Tab text="All Channels" fx:id="networkChannelsTab" closable="false">
<content>
<VBox spacing="10.0" styleClass="grid">
<children>
<TableView fx:id="networkChannelsTable" minHeight="50.0" prefHeight="5000.0">
<columnResizePolicy><TableView fx:constant="CONSTRAINED_RESIZE_POLICY"/></columnResizePolicy>
<columns>
<TableColumn fx:id="networkChannelsIdColumn" minWidth="120.0" prefWidth="170.0" maxWidth="300.0" text="Short Channel Id"/>
<TableColumn fx:id="networkChannelsNode1Column" text="Node 1"/>
<TableColumn fx:id="networkChannelsDirectionsColumn" minWidth="30.0" prefWidth="30.0" maxWidth="30.0"/>
<TableColumn fx:id="networkChannelsNode2Column" text="Node 2"/>
</columns>
</TableView>
</children>
</VBox>
</content>
</Tab>
<Tab text="Activity" closable="false">
<content>
<AnchorPane>
<children>
<TabPane AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" AnchorPane.bottomAnchor="0.0"
styleClass="activities-tab" tabClosingPolicy="UNAVAILABLE" BorderPane.alignment="CENTER">
<tabs>
<Tab fx:id="paymentSentTab" closable="false" text="Sent">
<TableView fx:id="paymentSentTable" minHeight="50.0" prefHeight="5000.0">
<columnResizePolicy><TableView fx:constant="CONSTRAINED_RESIZE_POLICY"/></columnResizePolicy>
<columns>
<TableColumn fx:id="paymentSentDateColumn" resizable="false" minWidth="150.0" prefWidth="150.0" maxWidth="150.0" text="Date"/>
<TableColumn fx:id="paymentSentAmountColumn" text="Amount (msat)"
styleClass="align-right" resizable="false" minWidth="150.0" prefWidth="150.0" maxWidth="150.0"/>
<TableColumn fx:id="paymentSentFeesColumn" text="Fees Paid (msat)"
styleClass="align-right" resizable="false" minWidth="150.0" prefWidth="150.0" maxWidth="150.0"/>
<TableColumn fx:id="paymentSentHashColumn" text="Payment Hash"/>
</columns>
</TableView>
</Tab>
<Tab fx:id="paymentReceivedTab" closable="false" text="Received">
<TableView fx:id="paymentReceivedTable" minHeight="50.0" prefHeight="5000.0">
<columnResizePolicy><TableView fx:constant="CONSTRAINED_RESIZE_POLICY"/></columnResizePolicy>
<columns>
<TableColumn fx:id="paymentReceivedDateColumn" resizable="false" minWidth="150.0" prefWidth="150.0" maxWidth="150.0" text="Date"/>
<TableColumn fx:id="paymentReceivedAmountColumn" text="Amount (msat)"
styleClass="align-right" resizable="false" minWidth="150.0" prefWidth="150.0" maxWidth="150.0"/>
<TableColumn fx:id="paymentReceivedHashColumn" text="Payment Hash"/>
</columns>
</TableView>
</Tab>
<Tab fx:id="paymentRelayedTab" closable="false" text="Relayed">
<TableView fx:id="paymentRelayedTable" minHeight="50.0" prefHeight="5000.0">
<columnResizePolicy><TableView fx:constant="CONSTRAINED_RESIZE_POLICY"/></columnResizePolicy>
<columns>
<TableColumn fx:id="paymentRelayedDateColumn" resizable="false" minWidth="150.0" prefWidth="150.0" maxWidth="150.0" text="Date"/>
<TableColumn fx:id="paymentRelayedAmountColumn" text="Amount (msat)"
styleClass="align-right" resizable="false" minWidth="150.0" prefWidth="150.0" maxWidth="150.0"/>
<TableColumn fx:id="paymentRelayedFeesColumn" text="Fees Earned (msat)"
styleClass="align-right" resizable="false" minWidth="150.0" prefWidth="150.0" maxWidth="150.0"/>
<TableColumn fx:id="paymentRelayedHashColumn" text="Payment Hash"/>
</columns>
</TableView>
</Tab>
</tabs>
</TabPane>
<Label AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" textAlignment="RIGHT"
maxWidth="180.0" wrapText="true" styleClass="activity-disclaimer"
text="Payment history will be cleared when the node is shut down." />
</children>
</AnchorPane>
</content>
</Tab>
</tabs>
</TabPane>
</center>
<bottom>
<HBox fx:id="statusBarBox" styleClass="status-bar" spacing="10">
<children>
<HBox alignment="CENTER_LEFT" HBox.hgrow="ALWAYS" onContextMenuRequested="#openNodeIdContext">
<children>
<ImageView fitHeight="16.0" fitWidth="27.0" opacity="0.52" pickOnBounds="true" preserveRatio="true">
<image><Image url="@../commons/images/eclair-shape.png"/></image>
</ImageView>
<Label fx:id="labelNodeId" text="N/A"/>
</children>
</HBox>
<HBox alignment="CENTER_LEFT" HBox.hgrow="SOMETIMES" minWidth="80.0">
<children>
<Separator orientation="VERTICAL"/>
<Rectangle fx:id="rectRGB" width="7" height="7" fill="transparent" />
<Label fx:id="labelAlias" text="N/A"/>
</children>
</HBox>
<HBox alignment="CENTER_LEFT" HBox.hgrow="NEVER" minWidth="85.0">
<children>
<Separator orientation="VERTICAL"/>
<Label text="HTTP" styleClass="badge, badge-http"/>
<Label fx:id="labelApi" styleClass="value" text="N/A"/>
</children>
</HBox>
<HBox alignment="CENTER_LEFT" HBox.hgrow="NEVER" minWidth="85.0">
<children>
<Separator orientation="VERTICAL"/>
<Label text="TCP" styleClass="badge, badge-tcp"/>
<Label fx:id="labelServer" text="N/A"/>
</children>
</HBox>
<HBox alignment="CENTER_LEFT" HBox.hgrow="NEVER" minWidth="6.0">
<children>
<Separator orientation="VERTICAL"/>
</children>
</HBox>
<HBox alignment="CENTER_RIGHT" HBox.hgrow="SOMETIMES" minWidth="195.0">
<children>
<Label text="Bitcoin-core" textAlignment="RIGHT" textOverrun="CLIP"/>
<Label fx:id="bitcoinVersion" text="N/A" textOverrun="CLIP"/>
<Label fx:id="bitcoinChain" styleClass="chain" text="(N/A)" textOverrun="CLIP"/>
</children>
</HBox>
</children>
</HBox>
</bottom>
<top>
<MenuBar BorderPane.alignment="CENTER">
<menus>
<Menu mnemonicParsing="false" text="Channels">
<items>
<MenuItem fx:id="menuOpen" mnemonicParsing="false" onAction="#handleOpenChannel"
text="Open channel..."/>
<SeparatorMenuItem mnemonicParsing="false"/>
<MenuItem fx:id="menuSend" mnemonicParsing="false" onAction="#handleSendPayment"
text="Send Payment..."/>
<MenuItem fx:id="menuReceive" mnemonicParsing="false" onAction="#handleReceivePayment"
text="Receive Payment..."/>
<SeparatorMenuItem mnemonicParsing="false"/>
<MenuItem mnemonicParsing="false" onAction="#handleCloseRequest" text="Close"/>
</items>
</Menu>
<Menu mnemonicParsing="false" text="Tools">
<items>
<MenuItem mnemonicParsing="false" onAction="#handleExportDot" text="Export Graph to .dot"/>
</items>
</Menu>
<Menu mnemonicParsing="false" text="Help">
<items>
<MenuItem mnemonicParsing="false" onAction="#handleOpenAbout" text="About Eclair..."/>
</items>
</Menu>
</menus>
</MenuBar>
</top>
</BorderPane>
<StackPane fx:id="blocker" styleClass="blocker-cover" opacity="0" visible="false" alignment="CENTER"
AnchorPane.topAnchor="0" AnchorPane.leftAnchor="0" AnchorPane.bottomAnchor="0" AnchorPane.rightAnchor="0">
<children>
<HBox fx:id="blockerDialog" opacity="0" styleClass="blocker-dialog" fillHeight="false" alignment="CENTER_LEFT" spacing="20"
minWidth="430.0" minHeight="100.0" prefWidth="430.0" prefHeight="100.0" maxWidth="430.0" maxHeight="100.0">
<children>
<ImageView fitHeight="40.0" fitWidth="40.0" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@../commons/images/connection_icon.png" />
</image>
</ImageView>
<VBox spacing="10.0" GridPane.columnIndex="1">
<children>
<Label fx:id="message" styleClass="text-strong" text="Lost connection to Bitcoin Core..." wrapText="true" />
<Label fx:id="details" text="Make sure that Bitcoin core is up and running." wrapText="true" />
</children>
</VBox>
</children>
</HBox>
</children>
</StackPane>
</children>
<stylesheets>
<URL value="@main.css"/>
<URL value="@../commons/globals.css"/>
</stylesheets>
</AnchorPane>

View file

@ -1,44 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.String?>
<?import java.net.URL?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.RowConstraints?>
<GridPane fx:id="rootPane" minWidth="300.0" prefWidth="300.0" maxWidth="300.0"
xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"
opacity="0" onMouseEntered="#handleMouseEnter" onMouseExited="#handleMouseExit">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" maxWidth="25.0" prefWidth="25.0"/>
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="220.0"/>
<ColumnConstraints hgrow="SOMETIMES" maxWidth="30.0" minWidth="5.0" prefWidth="20.0"/>
</columnConstraints>
<rowConstraints>
<RowConstraints minHeight="10.0" valignment="TOP" vgrow="SOMETIMES"/>
<RowConstraints minHeight="10.0" valignment="TOP" vgrow="ALWAYS"/>
</rowConstraints>
<children>
<Label fx:id="titleLabel" styleClass="notification-title" text="Eclair Notification" GridPane.columnIndex="1"/>
<ImageView fx:id="icon" fitWidth="25.0" pickOnBounds="true" preserveRatio="true" GridPane.rowSpan="2">
<image>
<Image url="@../commons/images/eclair-square.png"/>
</image>
</ImageView>
<Button fx:id="closeButton" maxHeight="18.0" maxWidth="18.0" minHeight="18.0" minWidth="18.0" mnemonicParsing="false"
styleClass="notification-close" text="" GridPane.columnIndex="2" />
<Label fx:id="messageLabel" styleClass="notification-message" text="N/A" wrapText="false" GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.rowIndex="1"/>
</children>
<styleClass>
<String fx:value="grid"/>
<String fx:value="notification-pane"/>
</styleClass>
<stylesheets>
<URL value="@../commons/globals.css"/>
<URL value="@main.css"/>
</stylesheets>
</GridPane>

View file

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import java.net.URL?>
<?import javafx.scene.layout.VBox?>
<VBox fx:id="notifsVBox" spacing="10.0"
style="-fx-background-color: transparent" styleClass="notifications-box"
xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
<stylesheets>
<URL value="@../commons/globals.css" />
<URL value="@main.css" />
</stylesheets>
</VBox>

View file

@ -1,64 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.String?>
<?import java.net.URL?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Text?>
<?import javafx.scene.text.TextFlow?>
<GridPane prefWidth="500.0" prefHeight="200.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
<columnConstraints>
<ColumnConstraints halignment="LEFT" hgrow="SOMETIMES" minWidth="10.0" maxWidth="120.0" />
<ColumnConstraints halignment="LEFT" hgrow="SOMETIMES" minWidth="10.0" prefWidth="180.0" />
</columnConstraints>
<children>
<ImageView fitHeight="120.0" fitWidth="120.0" pickOnBounds="true" preserveRatio="true" GridPane.halignment="CENTER">
<image>
<Image url="@../commons/images/eclair-square.png" />
</image>
</ImageView>
<VBox spacing="10.0" styleClass="about-content" GridPane.columnIndex="1">
<children>
<TextFlow>
<children>
<Text strokeType="OUTSIDE" strokeWidth="0.0" styleClass="text-strong" text="Eclair v" />
<Text fx:id="version" strokeType="OUTSIDE" strokeWidth="0.0" styleClass="text-strong" text="Unknown" />
<Text strokeType="OUTSIDE" styleClass="text-sm" strokeWidth="0.0" text=" brought to you by " />
<Text onMouseClicked="#openACINQPage" strokeType="OUTSIDE" strokeWidth="0.0" styleClass="link" text="ACINQ" />
</children>
</TextFlow>
<TextFlow layoutX="10.0" layoutY="90.0">
<children>
<Text strokeType="OUTSIDE" strokeWidth="0.0" text="Eclair follows " />
<Text onMouseClicked="#openLNRFCPage" strokeType="OUTSIDE" strokeWidth="0.0" styleClass="link" text="the Lightning Network specifications" />
<Text strokeType="OUTSIDE" strokeWidth="0.0" text="." />
</children>
</TextFlow>
<TextFlow layoutX="10.0" layoutY="10.0">
<children>
<Text strokeType="OUTSIDE" strokeWidth="0.0" text="The source code is available from " />
<Text onMouseClicked="#openGithubPage" strokeType="OUTSIDE" strokeWidth="0.0" styleClass="link" text="GitHub" />
<Text strokeType="OUTSIDE" strokeWidth="0.0" text="." />
</children>
</TextFlow>
<TextFlow layoutX="10.0" layoutY="90.0" styleClass="">
<children>
<Text strokeType="OUTSIDE" strokeWidth="0.0" text="Licensed under " />
<Text onMouseClicked="#openApacheLicencePage" strokeType="OUTSIDE" strokeWidth="0.0" styleClass="link" text="the Apache 2 License" />
<Text strokeType="OUTSIDE" strokeWidth="0.0" text="." />
</children>
</TextFlow>
</children>
</VBox>
</children>
<styleClass>
<String fx:value="grid" />
</styleClass>
<stylesheets>
<URL value="@../commons/globals.css" />
</stylesheets>
</GridPane>

View file

@ -1,89 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.String?>
<?import java.net.URL?>
<?import javafx.collections.FXCollections?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ComboBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Separator?>
<?import javafx.scene.control.CheckBox?>
<GridPane styleClass="grid" prefWidth="550.0" prefHeight="350.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" maxWidth="180.0" minWidth="10.0" prefWidth="180.0" halignment="RIGHT" />
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="180.0" />
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="160.0" />
</columnConstraints>
<rowConstraints>
<RowConstraints vgrow="SOMETIMES" />
<RowConstraints vgrow="SOMETIMES" maxHeight="10.0"/>
<RowConstraints vgrow="SOMETIMES" />
<RowConstraints vgrow="SOMETIMES" minHeight="30.0" valignment="BOTTOM"/>
<RowConstraints vgrow="SOMETIMES" />
<RowConstraints vgrow="SOMETIMES" />
</rowConstraints>
<children>
<VBox alignment="CENTER_RIGHT" GridPane.rowIndex="0">
<children>
<Label styleClass="text-strong" text="Target Node URI" />
<Label styleClass="label-description" text="Address of the node" textAlignment="RIGHT" wrapText="true" />
</children>
</VBox>
<TextField fx:id="host" prefWidth="313.0" promptText="pubkey@host:port"
GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.rowIndex="0" />
<Label fx:id="hostError" opacity="0.0" styleClass="text-error, text-error-downward" text="Generic Invalid URI" mouseTransparent="true"
GridPane.rowIndex="0" GridPane.columnIndex="1" GridPane.columnSpan="2" />
<CheckBox fx:id="simpleConnection" mnemonicParsing="false" text="Simple connection (no channel)" styleClass="text-sm"
GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.rowIndex="1" />
<VBox alignment="CENTER_RIGHT" GridPane.rowIndex="2">
<children>
<Label styleClass="text-strong" text="Capacity" />
<Label styleClass="label-description" text="Funding capacity of the channel" textAlignment="RIGHT" wrapText="true" />
</children>
</VBox>
<TextField fx:id="fundingSatoshis" prefWidth="313.0" GridPane.columnIndex="1" GridPane.rowIndex="2" />
<ComboBox fx:id="unit" prefWidth="150.0" GridPane.columnIndex="2" GridPane.rowIndex="2">
<items>
<FXCollections fx:factory="observableArrayList">
<String fx:id="milliBTC" fx:value="milliBTC" />
<String fx:id="Satoshi" fx:value="Satoshi" />
<String fx:id="milliSatoshi" fx:value="milliSatoshi" />
</FXCollections>
</items>
</ComboBox>
<Label fx:id="fundingSatoshisError" opacity="0.0" styleClass="text-error, text-error-downward" text="Generic Invalid Funding"
GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.rowIndex="2" />
<Label styleClass="text-muted" text="Optional Parameters" wrapText="true" GridPane.columnIndex="0" GridPane.rowIndex="3" />
<Separator styleClass="options-separator" GridPane.columnIndex="1" GridPane.rowIndex="3" GridPane.columnSpan="2"/>
<VBox alignment="CENTER_RIGHT" GridPane.rowIndex="4">
<children>
<Label styleClass="text-strong" text="Push Amount (msat)" />
<Label styleClass="label-description" text="Sent when opening channel" textAlignment="RIGHT" wrapText="true" />
</children>
</VBox>
<TextField fx:id="pushMsat" prefWidth="313.0" GridPane.columnIndex="1" GridPane.rowIndex="4" />
<Label fx:id="pushMsatError" opacity="0.0" styleClass="text-error, text-error-downward" text="Generic Invalid Push"
GridPane.columnIndex="1" GridPane.rowIndex="4" />
<CheckBox fx:id="publicChannel" mnemonicParsing="true" selected="true" styleClass="text-sm" text="Public Channel"
GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.rowIndex="5" />
<Button fx:id="button" defaultButton="true" mnemonicParsing="false" onAction="#handleOpen" text="Connect"
GridPane.columnIndex="1" GridPane.rowIndex="6" GridPane.valignment="BOTTOM" />
<Button cancelButton="true" mnemonicParsing="false" onAction="#handleClose" styleClass="cancel" text="Cancel"
GridPane.columnIndex="2" GridPane.halignment="RIGHT" GridPane.rowIndex="6" GridPane.valignment="BOTTOM" />
</children>
<stylesheets>
<URL value="@../commons/globals.css" />
</stylesheets>
</GridPane>

View file

@ -1,87 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.collections.FXCollections?>
<?import javafx.scene.control.*?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.*?>
<?import java.lang.String?>
<?import java.net.URL?>
<VBox xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" prefWidth="590.0">
<children>
<GridPane styleClass="grid">
<columnConstraints>
<ColumnConstraints halignment="RIGHT" hgrow="SOMETIMES" maxWidth="250.0" minWidth="10.0" prefWidth="250.0"/>
<ColumnConstraints hgrow="ALWAYS" minWidth="10.0" prefWidth="120.0"/>
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="120.0"/>
</columnConstraints>
<rowConstraints>
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
</rowConstraints>
<children>
<VBox alignment="TOP_RIGHT" GridPane.rowIndex="0">
<children>
<Label styleClass="text-strong" text="Amount to receive" />
<Label styleClass="label-description" wrapText="true" textAlignment="RIGHT" text="Maximum of ~0.042 BTC" />
</children>
</VBox>
<TextField fx:id="amount" GridPane.columnIndex="1" GridPane.rowIndex="0"/>
<ComboBox fx:id="unit" GridPane.columnIndex="2" GridPane.rowIndex="0" GridPane.halignment="RIGHT">
<items>
<FXCollections fx:factory="observableArrayList">
<String fx:id="milliBTC" fx:value="milliBTC" />
<String fx:id="Satoshi" fx:value="Satoshi" />
<String fx:id="milliSatoshi" fx:value="milliSatoshi" />
</FXCollections>
</items>
</ComboBox>
<Label fx:id="amountError" opacity="0.0" styleClass="text-error, text-error-downward" text="Generic Invalid Amount"
mouseTransparent="true" GridPane.rowIndex="0" GridPane.columnIndex="1" GridPane.columnSpan="2"/>
<VBox alignment="TOP_RIGHT" GridPane.rowIndex="1" GridPane.columnIndex="0">
<children>
<Label styleClass="text-strong" text="Optional description" />
<Label styleClass="label-description" wrapText="true" textAlignment="RIGHT" text="Can be left empty" />
</children>
</VBox>
<TextArea fx:id="description" GridPane.columnIndex="1" GridPane.rowIndex="1" GridPane.columnSpan="2" wrapText="true" prefHeight="50.0" />
<Button defaultButton="true" mnemonicParsing="false" onAction="#handleGenerate" prefHeight="29.0"
prefWidth="95.0" text="Generate" GridPane.columnIndex="1" GridPane.rowIndex="2"/>
<Button cancelButton="true" mnemonicParsing="false" onAction="#handleClose" styleClass="cancel" text="Close"
GridPane.columnIndex="2" GridPane.halignment="RIGHT" GridPane.rowIndex="2" opacity="0" focusTraversable="false"/>
</children>
</GridPane>
<GridPane fx:id="resultBox" styleClass="grid, result-box" visible="false">
<columnConstraints>
<ColumnConstraints halignment="LEFT" hgrow="ALWAYS" maxWidth="250.0" minWidth="10.0" prefWidth="250.0"/>
<ColumnConstraints hgrow="ALWAYS" minWidth="200.0" prefWidth="240.0"/>
</columnConstraints>
<rowConstraints>
<RowConstraints minHeight="10.0" vgrow="ALWAYS"/>
</rowConstraints>
<children>
<ImageView fx:id="paymentRequestQRCode" fitWidth="250.0" pickOnBounds="true" preserveRatio="true"
GridPane.rowIndex="0" GridPane.columnIndex="0"></ImageView>
<VBox spacing="10.0" GridPane.rowIndex="0" GridPane.columnIndex="1">
<children>
<HBox spacing="10.0" alignment="CENTER_LEFT">
<children>
<Label text="Invoice:" styleClass="text-strong" />
<Button mnemonicParsing="false" onAction="#handleCopyInvoice" styleClass="copy-clipboard"
text="Copy to Clipboard" GridPane.columnIndex="1" GridPane.rowIndex="2" />
</children>
</HBox>
<TextArea fx:id="paymentRequestTextArea" prefHeight="200.0" editable="false" styleClass="noteditable, text-sm, text-mono" wrapText="true" />
</children>
</VBox>
</children>
</GridPane>
</children>
<stylesheets>
<URL value="@../commons/globals.css"/>
<URL value="@../main/main.css"/>
</stylesheets>
</VBox>

View file

@ -1,53 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import java.lang.String?>
<?import java.net.URL?>
<GridPane fx:id="nodeId" prefWidth="450.0" prefHeight="450.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
<columnConstraints>
<ColumnConstraints halignment="LEFT" hgrow="SOMETIMES" minWidth="10.0" prefWidth="110.0"/>
<ColumnConstraints halignment="LEFT" hgrow="ALWAYS" minWidth="10.0" prefWidth="250.0"/>
</columnConstraints>
<rowConstraints>
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
<RowConstraints minHeight="1.0" prefHeight="3.0" vgrow="SOMETIMES"/>
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
<RowConstraints minHeight="1.0" vgrow="SOMETIMES"/>
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
</rowConstraints>
<children>
<Label styleClass="text-strong" text="Enter a Payment Request below" GridPane.columnSpan="2"
GridPane.valignment="TOP"/>
<TextArea fx:id="paymentRequest" minHeight="150.0" prefHeight="150.0" styleClass="ta" wrapText="true"
GridPane.columnSpan="2" GridPane.rowIndex="1" GridPane.vgrow="ALWAYS"/>
<Label fx:id="paymentRequestError" opacity="0.0" text="Generic Invalid Payment Request" mouseTransparent="true"
styleClass="text-error" GridPane.columnSpan="2" GridPane.rowIndex="2"/>
<Label styleClass="text-muted" text="Amount (msat)" GridPane.halignment="RIGHT" GridPane.rowIndex="3"/>
<TextField fx:id="amountField" focusTraversable="false" editable="false" styleClass="noteditable" text="0" GridPane.columnIndex="1" GridPane.rowIndex="3"/>
<Label styleClass="text-muted" text="Node Id" GridPane.halignment="RIGHT" GridPane.rowIndex="4"/>
<TextField fx:id="nodeIdField" focusTraversable="false" editable="false" styleClass="noteditable" text="N/A" GridPane.columnIndex="1" GridPane.rowIndex="4"/>
<Label styleClass="text-muted" text="hash" GridPane.halignment="RIGHT" GridPane.rowIndex="5"/>
<TextField fx:id="hashField" focusTraversable="false" editable="false" styleClass="noteditable" text="N/A" GridPane.columnIndex="1" GridPane.rowIndex="5"/>
<Separator GridPane.columnSpan="2" GridPane.rowIndex="6"/>
<Button fx:id="sendButton" defaultButton="true" mnemonicParsing="false" onAction="#handleSend" text="Send"
GridPane.rowIndex="7"/>
<Button cancelButton="true" mnemonicParsing="false" onAction="#handleClose" styleClass="cancel" text="Cancel"
GridPane.columnIndex="1" GridPane.halignment="RIGHT" GridPane.rowIndex="7"/>
</children>
<styleClass>
<String fx:value="grid"/>
<String fx:value="modal"/>
</styleClass>
<stylesheets>
<URL value="@../commons/globals.css"/>
</stylesheets>
</GridPane>

View file

@ -1,18 +0,0 @@
.label.splash-error-label {
-fx-padding: .25em;
}
.error-box {
-fx-padding: 1em;
-fx-border-width: 1px;
-fx-border-color: #dddddd;
-fx-background-color: #f6f6f6;
}
.error-box .label {
-fx-font-size: 12px;
}
.error-box .button {
-fx-font-size: 12px;
-fx-faint-focus-color: transparent;
}

View file

@ -1,49 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.effect.BoxBlur?>
<?import javafx.scene.image.*?>
<?import javafx.scene.layout.*?>
<?import java.net.URL?>
<?import javafx.scene.effect.DropShadow?>
<Pane fx:id="splash" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity"
prefHeight="457.0" prefWidth="760.0" style="-fx-background-color: transparent"
xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
<children>
<ImageView fx:id="imgBlurred" fitHeight="0" fitWidth="300.0" layoutX="176.0" layoutY="115.0" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@../commons/images/eclair-fit.png" />
</image>
<effect>
<BoxBlur height="114.75" width="92.44" />
</effect>
</ImageView>
<ImageView fx:id="img" fitHeight="0" fitWidth="409.0" layoutX="176.0" layoutY="114.0" opacity="0.0" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@../commons/images/eclair-fit.png" />
</image>
</ImageView>
<VBox fx:id="errorBox" opacity="0.0" alignment="CENTER" layoutX="196.0" prefWidth="370.0" prefHeight="457.0">
<children>
<VBox prefWidth="370.0" styleClass="error-box" spacing="10">
<effect>
<DropShadow offsetX="5.0" offsetY="5.0" radius="25.0" color="rgba(0,0,0,.4)" blurType="GAUSSIAN"/>
</effect>
<children>
<VBox fx:id="logBox" VBox.vgrow="ALWAYS" styleClass="log-box">
<children>
</children>
</VBox>
<Label onMouseClicked="#openGithubPage" VBox.vgrow="NEVER" styleClass="link" text="Consult our readme to get started." />
<Button fx:id="closeButton" VBox.vgrow="NEVER" mnemonicParsing="false" onAction="#closeAndKill" text="Close" cancelButton="true" />
</children>
</VBox>
</children>
</VBox>
</children>
<stylesheets>
<URL value="@../commons/globals.css" />
<URL value="@splash.css" />
</stylesheets>
</Pane>

View file

@ -1,39 +0,0 @@
package fr.acinq.eclair
import java.io.File
import com.sun.javafx.application.LauncherImpl
import fr.acinq.eclair.gui.{FxApp, FxPreloader}
import grizzled.slf4j.Logging
/**
* Created by PM on 25/01/2016.
*/
object JavafxBoot extends App with Logging {
case class CmdLineConfig(datadir: File = new File(System.getProperty("user.home"), ".eclair"), headless: Boolean = false)
val parser = new scopt.OptionParser[CmdLineConfig]("eclair") {
head("eclair gui", s"${getClass.getPackage.getImplementationVersion} (commit: ${getClass.getPackage.getSpecificationVersion})")
help("help").abbr("h").text("display usage text")
opt[File]("datadir").optional().valueName("<file>").action((x, c) => c.copy(datadir = x)).text("optional data directory, default is ~/.eclair")
opt[Unit]("headless").optional().action((_, c) => c.copy(headless = true)).text("runs eclair without a gui")
}
try {
parser.parse(args, CmdLineConfig()) match {
case Some(config) if config.headless =>
LogSetup.logTo(config.datadir)
new Setup(config.datadir).bootstrap
case Some(config) =>
LogSetup.logTo(config.datadir)
LauncherImpl.launchApplication(classOf[FxApp], classOf[FxPreloader], Array(config.datadir.getAbsolutePath))
case None => System.exit(0)
}
} catch {
case t: Throwable =>
System.err.println(s"fatal error: ${t.getMessage}")
logger.error(s"fatal error: ${t.getMessage}")
System.exit(1)
}
}

View file

@ -1,131 +0,0 @@
package fr.acinq.eclair.gui
import java.io.File
import javafx.application.Preloader.ErrorNotification
import javafx.application.{Application, Platform}
import javafx.event.EventHandler
import javafx.fxml.FXMLLoader
import javafx.scene.image.Image
import javafx.scene.{Parent, Scene}
import javafx.stage.{Popup, Screen, Stage, WindowEvent}
import akka.actor.{ActorSystem, Props, SupervisorStrategy}
import fr.acinq.eclair._
import fr.acinq.eclair.blockchain.zmq.ZMQEvents
import fr.acinq.eclair.channel.ChannelEvent
import fr.acinq.eclair.gui.controllers.{MainController, NotificationsController}
import fr.acinq.eclair.payment.PaymentEvent
import fr.acinq.eclair.router.NetworkEvent
import grizzled.slf4j.Logging
import scala.concurrent.Promise
import scala.util.{Failure, Success}
/**
* Created by PM on 16/08/2016.
*/
class FxApp extends Application with Logging {
override def init = {
logger.debug("initializing application...")
}
def onError(t: Throwable): Unit = t match {
case TCPBindException(port) =>
notifyPreloader(new ErrorNotification("Setup", s"Could not bind to port $port", null))
case BitcoinRPCConnectionException =>
notifyPreloader(new ErrorNotification("Setup", "Could not connect to Bitcoin Core using JSON-RPC.", null))
notifyPreloader(new AppNotification(InfoAppNotification, "Make sure that Bitcoin Core is up and running and RPC parameters are correct."))
case BitcoinZMQConnectionTimeoutException =>
notifyPreloader(new ErrorNotification("Setup", "Could not connect to Bitcoin Core using ZMQ.", null))
notifyPreloader(new AppNotification(InfoAppNotification, "Make sure that Bitcoin Core is up and running and ZMQ parameters are correct."))
case t: Throwable =>
notifyPreloader(new ErrorNotification("Setup", s"Internal error: ${t.toString}", t))
}
override def start(primaryStage: Stage): Unit = {
new Thread(new Runnable {
override def run(): Unit = {
try {
val icon = new Image(getClass.getResource("/gui/commons/images/eclair-square.png").toExternalForm, false)
primaryStage.getIcons.add(icon)
val mainFXML = new FXMLLoader(getClass.getResource("/gui/main/main.fxml"))
val pKit = Promise[Kit]()
val handlers = new Handlers(pKit.future)
val controller = new MainController(handlers, getHostServices)
mainFXML.setController(controller)
val mainRoot = mainFXML.load[Parent]
val datadir = new File(getParameters.getUnnamed.get(0))
implicit val system = ActorSystem("system")
val setup = new Setup(datadir)
val guiUpdater = setup.system.actorOf(SimpleSupervisor.props(Props(classOf[GUIUpdater], controller), "gui-updater", SupervisorStrategy.Resume))
setup.system.eventStream.subscribe(guiUpdater, classOf[ChannelEvent])
setup.system.eventStream.subscribe(guiUpdater, classOf[NetworkEvent])
setup.system.eventStream.subscribe(guiUpdater, classOf[PaymentEvent])
setup.system.eventStream.subscribe(guiUpdater, classOf[ZMQEvents])
pKit.completeWith(setup.bootstrap)
import scala.concurrent.ExecutionContext.Implicits.global
pKit.future.onComplete {
case Success(_) =>
Platform.runLater(new Runnable {
override def run(): Unit = {
val scene = new Scene(mainRoot)
primaryStage.setTitle("Eclair")
primaryStage.setMinWidth(600)
primaryStage.setWidth(960)
primaryStage.setMinHeight(400)
primaryStage.setHeight(640)
primaryStage.setOnCloseRequest(new EventHandler[WindowEvent] {
override def handle(event: WindowEvent) {
System.exit(0)
}
})
controller.initInfoFields(setup)
primaryStage.setScene(scene)
primaryStage.show
notifyPreloader(new AppNotification(SuccessAppNotification, "Init successful"))
initNotificationStage(primaryStage, handlers)
}
})
case Failure(t) => onError(t)
}
} catch {
case t: Throwable => onError(t)
}
}
}).start
}
/**
* Initialize the notification stage and assign it to the handler class.
*
* @param owner stage owning the notification stage
* @param notifhandlers Handles the notifications
*/
private def initNotificationStage(owner: Stage, notifhandlers: Handlers) = {
// get fxml/controller
val notifFXML = new FXMLLoader(getClass.getResource("/gui/main/notifications.fxml"))
val notifsController = new NotificationsController
notifFXML.setController(notifsController)
val root = notifFXML.load[Parent]
Platform.runLater(new Runnable() {
override def run = {
// create scene
val popup = new Popup
popup.setHideOnEscape(false)
popup.setAutoFix(false)
val margin = 10
val width = 300
popup.setWidth(margin + width)
popup.getContent.add(root)
// positioning the popup @ TOP RIGHT of screen
val screenBounds = Screen.getPrimary.getVisualBounds
popup.show(owner, screenBounds.getMaxX - (margin + width), screenBounds.getMinY + margin)
notifhandlers.initNotifications(notifsController)
}
})
}
}

View file

@ -1,74 +0,0 @@
package fr.acinq.eclair.gui
import javafx.application.Preloader
import javafx.application.Preloader.{ErrorNotification, PreloaderNotification}
import javafx.fxml.FXMLLoader
import javafx.scene.image.Image
import javafx.scene.paint.Color
import javafx.scene.{Parent, Scene}
import javafx.stage.{Stage, StageStyle}
import fr.acinq.eclair.gui.controllers.SplashController
import grizzled.slf4j.Logging
sealed trait AppNotificationType
case object SuccessAppNotification extends AppNotificationType
case object InfoAppNotification extends AppNotificationType
case class AppNotification(notificationType: AppNotificationType, message: String) extends PreloaderNotification
/**
* Created by DPA on 15/03/2017.
*/
class FxPreloader extends Preloader with Logging {
var controller: Option[SplashController] = None
var stage: Option[Stage] = None
override def start(primaryStage: Stage) = {
setupStage(primaryStage)
primaryStage.show
stage = Option(primaryStage)
}
private def setupStage(stage: Stage) = {
val icon = new Image(getClass.getResource("/gui/commons/images/eclair-square.png").toExternalForm, false)
stage.getIcons.add(icon)
// set stage props
stage.initStyle(StageStyle.TRANSPARENT)
stage.setResizable(false)
// get fxml/controller
val splashController = new SplashController(getHostServices)
val splash = new FXMLLoader(getClass.getResource("/gui/splash/splash.fxml"))
splash.setController(splashController)
val root = splash.load[Parent]
// create scene
val scene = new Scene(root)
scene.setFill(Color.TRANSPARENT)
stage.setScene(scene)
controller = Option(splashController)
}
override def handleApplicationNotification(info: PreloaderNotification) = {
info match {
case n: ErrorNotification =>
logger.debug(s"Preloader error notification => ${n.getDetails}")
logger.error(s"An error has occured", n.getCause)
controller.map(_.addError(n.getDetails))
controller.map(_.showErrorBox)
case n: AppNotification =>
logger.debug(s"Preloader app notification => ${n.notificationType}, ${n.message}")
n.notificationType match {
case SuccessAppNotification => stage.map(_.close)
case InfoAppNotification => controller.map(_.addLog(n.message))
case _ =>
}
case _ =>
logger.debug(s"Notification ${info}")
}
}
}

View file

@ -1,188 +0,0 @@
package fr.acinq.eclair.gui
import java.time.LocalDateTime
import java.util.function.Predicate
import javafx.application.Platform
import javafx.event.{ActionEvent, EventHandler}
import javafx.fxml.FXMLLoader
import javafx.scene.layout.VBox
import akka.actor.{Actor, ActorLogging, ActorRef, Terminated}
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin._
import fr.acinq.eclair.blockchain.zmq.{ZMQConnected, ZMQDisconnected}
import fr.acinq.eclair.channel._
import fr.acinq.eclair.gui.controllers._
import fr.acinq.eclair.payment.{PaymentReceived, PaymentRelayed, PaymentSent}
import fr.acinq.eclair.router._
import fr.acinq.eclair.wire.NodeAnnouncement
import scala.collection.JavaConversions._
/**
* Created by PM on 16/08/2016.
*/
class GUIUpdater(mainController: MainController) extends Actor with ActorLogging {
def receive: Receive = main(Map())
def createChannelPanel(channel: ActorRef, peer: ActorRef, remoteNodeId: PublicKey, isFunder: Boolean, temporaryChannelId: BinaryData): (ChannelPaneController, VBox) = {
log.info(s"new channel: $channel")
val loader = new FXMLLoader(getClass.getResource("/gui/main/channelPane.fxml"))
val channelPaneController = new ChannelPaneController(s"$remoteNodeId")
loader.setController(channelPaneController)
val root = loader.load[VBox]
channelPaneController.nodeId.setText(s"$remoteNodeId")
channelPaneController.channelId.setText(s"$temporaryChannelId")
channelPaneController.funder.setText(if (isFunder) "Yes" else "No")
channelPaneController.close.setOnAction(new EventHandler[ActionEvent] {
override def handle(event: ActionEvent) = channel ! CMD_CLOSE(None)
})
// set the node alias if the node has already been announced
mainController.networkNodesList
.find(na => na.nodeId.toString.equals(remoteNodeId.toString))
.map(na => channelPaneController.updateRemoteNodeAlias(na.alias))
(channelPaneController, root)
}
def updateBalance(channelPaneController: ChannelPaneController, commitments: Commitments) = {
val spec = commitments.localCommit.spec
channelPaneController.capacity.setText(s"${millisatoshi2millibtc(MilliSatoshi(spec.totalFunds)).amount}")
channelPaneController.amountUs.setText(s"${millisatoshi2millibtc(MilliSatoshi(spec.toLocalMsat)).amount}")
channelPaneController.balanceBar.setProgress(spec.toLocalMsat.toDouble / spec.totalFunds)
}
def main(m: Map[ActorRef, ChannelPaneController]): Receive = {
case ChannelCreated(channel, peer, remoteNodeId, isFunder, temporaryChannelId) =>
context.watch(channel)
val (channelPaneController, root) = createChannelPanel(channel, peer, remoteNodeId, isFunder, temporaryChannelId)
Platform.runLater(new Runnable() {
override def run = mainController.channelBox.getChildren.addAll(root)
})
context.become(main(m + (channel -> channelPaneController)))
case ChannelRestored(channel, peer, remoteNodeId, isFunder, channelId, currentData) =>
context.watch(channel)
val (channelPaneController, root) = createChannelPanel(channel, peer, remoteNodeId, isFunder, channelId)
currentData match {
case d: HasCommitments => updateBalance(channelPaneController, d.commitments)
case _ => {}
}
Platform.runLater(new Runnable() {
override def run = {
mainController.channelBox.getChildren.addAll(root)
}
})
context.become(main(m + (channel -> channelPaneController)))
case ChannelIdAssigned(channel, temporaryChannelId, channelId) if m.contains(channel) =>
val channelPaneController = m(channel)
Platform.runLater(new Runnable() {
override def run = {
channelPaneController.channelId.setText(s"$channelId")
}
})
case ChannelStateChanged(channel, _, _, _, currentState, currentData) if m.contains(channel) =>
val channelPaneController = m(channel)
Platform.runLater(new Runnable() {
override def run = {
channelPaneController.close.setText( if (OFFLINE == currentState) "Force close" else "Close")
channelPaneController.state.setText(currentState.toString)
}
})
case ChannelSignatureReceived(channel, commitments) if m.contains(channel) =>
val channelPaneController = m(channel)
Platform.runLater(new Runnable() {
override def run = updateBalance(channelPaneController, commitments)
})
case Terminated(actor) if m.contains(actor) =>
val channelPaneController = m(actor)
log.debug(s"channel=${channelPaneController.channelId.getText} to be removed from gui")
Platform.runLater(new Runnable() {
override def run = {
mainController.channelBox.getChildren.remove(channelPaneController.root)
}
})
case NodeDiscovered(nodeAnnouncement) =>
log.debug(s"peer node discovered with node id=${nodeAnnouncement.nodeId}")
if(!mainController.networkNodesList.exists(na => na.nodeId == nodeAnnouncement.nodeId)) {
mainController.networkNodesList.add(nodeAnnouncement)
m.foreach(f => if (nodeAnnouncement.nodeId.toString.equals(f._2.theirNodeIdValue)) {
Platform.runLater(new Runnable() {
override def run = f._2.updateRemoteNodeAlias(nodeAnnouncement.alias)
})
})
}
case NodeLost(nodeId) =>
log.debug(s"peer node lost with node id=${nodeId}")
mainController.networkNodesList.removeIf(new Predicate[NodeAnnouncement] {
override def test(na: NodeAnnouncement) = na.nodeId.equals(nodeId)
})
case NodeUpdated(nodeAnnouncement) =>
log.debug(s"peer node with id=${nodeAnnouncement.nodeId} has been updated")
val idx = mainController.networkNodesList.indexWhere(na => na.nodeId == nodeAnnouncement.nodeId)
if (idx >= 0) {
mainController.networkNodesList.update(idx, nodeAnnouncement)
m.foreach(f => if (nodeAnnouncement.nodeId.toString.equals(f._2.theirNodeIdValue)) {
Platform.runLater(new Runnable() {
override def run = f._2.updateRemoteNodeAlias(nodeAnnouncement.alias)
})
})
}
case ChannelDiscovered(channelAnnouncement, _) =>
log.debug(s"peer channel discovered with channel id=${channelAnnouncement.shortChannelId}")
if(!mainController.networkChannelsList.exists(c => c.announcement.shortChannelId == channelAnnouncement.shortChannelId)) {
mainController.networkChannelsList.add(new ChannelInfo(channelAnnouncement, None, None))
}
case ChannelLost(shortChannelId) =>
log.debug(s"peer channel lost with channel id=${shortChannelId}")
mainController.networkChannelsList.removeIf(new Predicate[ChannelInfo] {
override def test(c: ChannelInfo) = c.announcement.shortChannelId == shortChannelId
})
case ChannelUpdateReceived(channelUpdate) =>
log.debug(s"peer channel with id=${channelUpdate.shortChannelId} has been updated")
val idx = mainController.networkChannelsList.indexWhere(c => c.announcement.shortChannelId == channelUpdate.shortChannelId)
if (idx >= 0) {
val c = mainController.networkChannelsList.get(idx)
if (Announcements.isNode1(channelUpdate.flags)) {
c.isNode1Enabled = Some(Announcements.isEnabled(channelUpdate.flags))
} else {
c.isNode2Enabled = Some(Announcements.isEnabled(channelUpdate.flags))
}
mainController.networkChannelsList.update(idx, c)
}
case p: PaymentSent =>
log.debug(s"payment sent with h=${p.paymentHash}, amount=${p.amount}, fees=${p.feesPaid}")
mainController.paymentSentList.prepend(new PaymentSentRecord(p, LocalDateTime.now()))
case p: PaymentReceived =>
log.debug(s"payment received with h=${p.paymentHash}, amount=${p.amount}")
mainController.paymentReceivedList.prepend(new PaymentReceivedRecord(p, LocalDateTime.now()))
case p: PaymentRelayed =>
log.debug(s"payment relayed with h=${p.paymentHash}, amount=${p.amount}, feesEarned=${p.feesEarned}")
mainController.paymentRelayedList.prepend(new PaymentRelayedRecord(p, LocalDateTime.now()))
case ZMQConnected =>
log.debug("ZMQ connection online")
mainController.hideBlockerModal
case ZMQDisconnected =>
log.debug("ZMQ connection lost")
mainController.showBlockerModal
}
}

View file

@ -1,115 +0,0 @@
package fr.acinq.eclair.gui
import java.io.{File, FileWriter}
import java.net.InetSocketAddress
import java.text.NumberFormat
import java.util.Locale
import akka.pattern.ask
import akka.util.Timeout
import fr.acinq.bitcoin.{BinaryData, MilliSatoshi}
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair._
import fr.acinq.eclair.gui.controllers._
import fr.acinq.eclair.gui.utils.GUIValidators
import fr.acinq.eclair.io.Switchboard.{NewChannel, NewConnection}
import fr.acinq.eclair.payment._
import grizzled.slf4j.Logging
import scala.concurrent.{ExecutionContext, Future}
import scala.concurrent.duration._
import scala.util.{Failure, Success}
/**
* Created by PM on 16/08/2016.
*/
class Handlers(fKit: Future[Kit])(implicit ec: ExecutionContext = ExecutionContext.Implicits.global) extends Logging {
implicit val timeout = Timeout(30 seconds)
private var notifsController: Option[NotificationsController] = None
def initNotifications (controller: NotificationsController) = {
notifsController = Option(controller)
}
/**
* Opens a connection to a node. If the channel option exists this will also open a channel with the node, with a
* `fundingSatoshis` capacity and `pushMsat` amount.
*
* @param hostPort
* @param channel
*/
def open(hostPort: String, channel: Option[NewChannel]) = {
hostPort match {
case GUIValidators.hostRegex(remoteNodeId, host, port) =>
logger.info(s"opening a channel with remoteNodeId=$remoteNodeId")
(for {
address <- Future(new InetSocketAddress(host, port.toInt))
pubkey = PublicKey(remoteNodeId)
kit <- fKit
conn <- kit.switchboard ? NewConnection(pubkey, address, channel)
} yield conn) onFailure {
case t =>
notification("Connection failed", s"$host:$port", NOTIFICATION_ERROR)
}
case _ => {}
}
}
def send(nodeId: PublicKey, paymentHash: BinaryData, amountMsat: Long) = {
logger.info(s"sending $amountMsat to $paymentHash @ $nodeId")
(for {
kit <- fKit
res <- (kit.paymentInitiator ? SendPayment(amountMsat, paymentHash, nodeId)).mapTo[PaymentResult]
} yield res)
.onComplete {
case Success(_: PaymentSucceeded) =>
val message = s"${NumberFormat.getInstance(Locale.getDefault).format(amountMsat/1000)} satoshis"
notification("Payment Sent", message, NOTIFICATION_SUCCESS)
case Success(PaymentFailed(_, failures)) =>
val message = s"${failures.lastOption match {
case Some(LocalFailure(t)) => t.getMessage
case Some(RemoteFailure(_, e)) => e.failureMessage
case _ => "Unknown error"
}} (${failures.size} attempts)"
notification("Payment Failed", message, NOTIFICATION_ERROR)
case Failure(t) =>
val message = t.getMessage
notification("Payment Failed", message, NOTIFICATION_ERROR)
}
}
def receive(amountMsat: MilliSatoshi, description: String): Future[String] = for {
kit <- fKit
res <- (kit.paymentHandler ? ReceivePayment(amountMsat, description)).mapTo[PaymentRequest].map(PaymentRequest.write(_))
} yield res
def exportToDot(file: File) = for {
kit <- fKit
dot <- (kit.router ? 'dot).mapTo[String]
_ = printToFile(file)(writer => writer.write(dot))
} yield {}
private def printToFile(f: java.io.File)(op: java.io.FileWriter => Unit) {
val p = new FileWriter(f)
try {
op(p)
} finally {
p.close
}
}
/**
* Displays a system notification if the system supports it.
*
* @param title Title of the notification
* @param message main message of the notification, will not wrap
* @param notificationType type of the message, default to NONE
* @param showAppName true if you want the notification title to be preceded by "Eclair - ". True by default
*/
def notification (title: String, message: String, notificationType: NotificationType = NOTIFICATION_NONE, showAppName: Boolean = true) = {
notifsController.map(_.addNotification(if (showAppName) s"Eclair - $title" else title, message, notificationType))
}
}

View file

@ -1,21 +0,0 @@
package fr.acinq.eclair.gui.controllers
import javafx.fxml.FXML
import javafx.application.HostServices
import javafx.scene.text.Text
import grizzled.slf4j.Logging
/**
* Created by DPA on 28/09/2016.
*/
class AboutController(hostServices: HostServices) extends Logging {
@FXML var version: Text = _
@FXML def initialize = {
version.setText(getClass.getPackage.getImplementationVersion)
}
@FXML def openApacheLicencePage = hostServices.showDocument("https://www.apache.org/licenses/LICENSE-2.0")
@FXML def openACINQPage = hostServices.showDocument("https://acinq.co")
@FXML def openGithubPage = hostServices.showDocument("https://github.com/ACINQ/eclair")
@FXML def openLNRFCPage = hostServices.showDocument("https://github.com/lightningnetwork/lightning-rfc")
}

View file

@ -1,67 +0,0 @@
package fr.acinq.eclair.gui.controllers
import javafx.application.Platform
import javafx.beans.value.{ChangeListener, ObservableValue}
import javafx.fxml.FXML
import javafx.scene.control._
import javafx.scene.input.{ContextMenuEvent, MouseEvent}
import javafx.scene.layout.VBox
import fr.acinq.eclair.gui.utils.{ContextMenuUtils, CopyAction}
import grizzled.slf4j.Logging
/**
* Created by DPA on 23/09/2016.
*/
class ChannelPaneController(val theirNodeIdValue: String) extends Logging {
@FXML var root: VBox = _
@FXML var channelId: TextField = _
@FXML var balanceBar: ProgressBar = _
@FXML var amountUs: TextField = _
@FXML var nodeId: TextField = _
@FXML var capacity: TextField = _
@FXML var funder: TextField = _
@FXML var state: TextField = _
@FXML var close: Button = _
var contextMenu: ContextMenu = _
private def buildChannelContextMenu = {
Platform.runLater(new Runnable() {
override def run = {
contextMenu = ContextMenuUtils.buildCopyContext(List(
new CopyAction("Copy Channel Id", channelId.getText),
new CopyAction("Copy Peer Pubkey", theirNodeIdValue)
))
contextMenu.getStyleClass.add("context-channel")
channelId.setContextMenu(contextMenu)
amountUs.setContextMenu(contextMenu)
nodeId.setContextMenu(contextMenu)
capacity.setContextMenu(contextMenu)
funder.setContextMenu(contextMenu)
state.setContextMenu(contextMenu)
}
})
}
@FXML def initialize = {
channelId.textProperty.addListener(new ChangeListener[String] {
override def changed(observable: ObservableValue[_ <: String], oldValue: String, newValue: String) = buildChannelContextMenu
})
buildChannelContextMenu
}
@FXML def openChannelContext(event: ContextMenuEvent) {
contextMenu.show(channelId, event.getScreenX, event.getScreenY)
event.consume
}
@FXML def closeChannelContext(event: MouseEvent) {
if (contextMenu != null) contextMenu.hide
}
def updateRemoteNodeAlias (alias: String) {
Option(nodeId).map((n: TextField) => n.setText(s"$theirNodeIdValue ($alias)"))
}
}

View file

@ -1,510 +0,0 @@
package fr.acinq.eclair.gui.controllers
import java.text.NumberFormat
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.Locale
import javafx.animation.{FadeTransition, ParallelTransition, SequentialTransition, TranslateTransition}
import javafx.application.{HostServices, Platform}
import javafx.beans.property._
import javafx.beans.value.{ChangeListener, ObservableValue}
import javafx.collections.ListChangeListener.Change
import javafx.collections.{FXCollections, ListChangeListener, ObservableList}
import javafx.event.{ActionEvent, EventHandler}
import javafx.fxml.FXML
import javafx.scene.control.TableColumn.CellDataFeatures
import javafx.scene.control._
import javafx.scene.image.{Image, ImageView}
import javafx.scene.input.ContextMenuEvent
import javafx.scene.layout.{AnchorPane, HBox, StackPane, VBox}
import javafx.scene.paint.Color
import javafx.scene.shape.Rectangle
import javafx.stage.FileChooser.ExtensionFilter
import javafx.stage._
import javafx.util.{Callback, Duration}
import fr.acinq.eclair.Setup
import fr.acinq.eclair.gui.Handlers
import fr.acinq.eclair.gui.stages._
import fr.acinq.eclair.gui.utils.{ContextMenuUtils, CopyAction}
import fr.acinq.eclair.payment.{PaymentEvent, PaymentReceived, PaymentRelayed, PaymentSent}
import fr.acinq.eclair.wire.{ChannelAnnouncement, NodeAnnouncement}
import grizzled.slf4j.Logging
case class ChannelInfo(announcement: ChannelAnnouncement, var isNode1Enabled: Option[Boolean], var isNode2Enabled: Option[Boolean])
sealed trait Record {
val event: PaymentEvent
val date: LocalDateTime
}
case class PaymentSentRecord(event: PaymentSent, date: LocalDateTime) extends Record
case class PaymentReceivedRecord(event: PaymentReceived, date: LocalDateTime) extends Record
case class PaymentRelayedRecord(event: PaymentRelayed, date: LocalDateTime) extends Record
/**
* Created by DPA on 22/09/2016.
*/
class MainController(val handlers: Handlers, val hostServices: HostServices) extends Logging {
@FXML var root: AnchorPane = _
var contextMenu: ContextMenu = _
// menu
@FXML var menuOpen: MenuItem = _
@FXML var menuSend: MenuItem = _
@FXML var menuReceive: MenuItem = _
// status bar elements
@FXML var labelNodeId: Label = _
@FXML var rectRGB: Rectangle = _
@FXML var labelAlias: Label = _
@FXML var labelApi: Label = _
@FXML var labelServer: Label = _
@FXML var bitcoinVersion: Label = _
@FXML var bitcoinChain: Label = _
// channels tab elements
@FXML var channelInfo: VBox = _
@FXML var channelBox: VBox = _
@FXML var channelsTab: Tab = _
// all nodes tab
val networkNodesList: ObservableList[NodeAnnouncement] = FXCollections.observableArrayList[NodeAnnouncement]()
@FXML var networkNodesTab: Tab = _
@FXML var networkNodesTable: TableView[NodeAnnouncement] = _
@FXML var networkNodesIdColumn: TableColumn[NodeAnnouncement, String] = _
@FXML var networkNodesAliasColumn: TableColumn[NodeAnnouncement, String] = _
@FXML var networkNodesRGBColumn: TableColumn[NodeAnnouncement, String] = _
@FXML var networkNodesIPColumn: TableColumn[NodeAnnouncement, String] = _
// all channels
val networkChannelsList: ObservableList[ChannelInfo] = FXCollections.observableArrayList[ChannelInfo]()
@FXML var networkChannelsTab: Tab = _
@FXML var networkChannelsTable: TableView[ChannelInfo] = _
@FXML var networkChannelsIdColumn: TableColumn[ChannelInfo, String] = _
@FXML var networkChannelsNode1Column: TableColumn[ChannelInfo, String] = _
@FXML var networkChannelsDirectionsColumn: TableColumn[ChannelInfo, ChannelInfo] = _
@FXML var networkChannelsNode2Column: TableColumn[ChannelInfo, String] = _
// payment sent table
val paymentSentList = FXCollections.observableArrayList[PaymentSentRecord]()
@FXML var paymentSentTab: Tab = _
@FXML var paymentSentTable: TableView[PaymentSentRecord] = _
@FXML var paymentSentAmountColumn: TableColumn[PaymentSentRecord, Number] = _
@FXML var paymentSentFeesColumn: TableColumn[PaymentSentRecord, Number] = _
@FXML var paymentSentHashColumn: TableColumn[PaymentSentRecord, String] = _
@FXML var paymentSentDateColumn: TableColumn[PaymentSentRecord, String] = _
// payment received table
val paymentReceivedList = FXCollections.observableArrayList[PaymentReceivedRecord]()
@FXML var paymentReceivedTab: Tab = _
@FXML var paymentReceivedTable: TableView[PaymentReceivedRecord] = _
@FXML var paymentReceivedAmountColumn: TableColumn[PaymentReceivedRecord, Number] = _
@FXML var paymentReceivedHashColumn: TableColumn[PaymentReceivedRecord, String] = _
@FXML var paymentReceivedDateColumn: TableColumn[PaymentReceivedRecord, String] = _
// payment relayed table
val paymentRelayedList = FXCollections.observableArrayList[PaymentRelayedRecord]()
@FXML var paymentRelayedTab: Tab = _
@FXML var paymentRelayedTable: TableView[PaymentRelayedRecord] = _
@FXML var paymentRelayedAmountColumn: TableColumn[PaymentRelayedRecord, Number] = _
@FXML var paymentRelayedFeesColumn: TableColumn[PaymentRelayedRecord, Number] = _
@FXML var paymentRelayedHashColumn: TableColumn[PaymentRelayedRecord, String] = _
@FXML var paymentRelayedDateColumn: TableColumn[PaymentRelayedRecord, String] = _
@FXML var blocker: StackPane = _
@FXML var blockerDialog: HBox = _
val PAYMENT_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
val moneyFormatter = NumberFormat.getInstance(Locale.getDefault)
/**
* Initialize the main window.
*
* - Set content in status bar labels (node id, host, ...)
* - init the channels tab with a 'No channels found' message
* - init the 'nodes in network' and 'channels in network' tables
*/
@FXML def initialize = {
// init channels tab
if (channelBox.getChildren.size() > 0) {
channelInfo.setScaleY(0)
channelInfo.setOpacity(0)
}
channelBox.heightProperty().addListener(new ChangeListener[Number] {
override def changed(observable: ObservableValue[_ <: Number], oldValue: Number, newValue: Number): Unit = {
if (channelBox.getChildren.size() > 0) {
channelInfo.setScaleY(0)
channelInfo.setOpacity(0)
} else {
channelInfo.setScaleY(1)
channelInfo.setOpacity(1)
}
channelsTab.setText(s"Local Channels (${channelBox.getChildren.size})")
}
})
// init all nodes
networkNodesTable.setItems(networkNodesList)
networkNodesList.addListener(new ListChangeListener[NodeAnnouncement] {
override def onChanged(c: Change[_ <: NodeAnnouncement]) = updateTabHeader(networkNodesTab, "All Nodes", networkNodesList)
})
networkNodesIdColumn.setCellValueFactory(new Callback[CellDataFeatures[NodeAnnouncement, String], ObservableValue[String]]() {
def call(pn: CellDataFeatures[NodeAnnouncement, String]) = new SimpleStringProperty(pn.getValue.nodeId.toString)
})
networkNodesAliasColumn.setCellValueFactory(new Callback[CellDataFeatures[NodeAnnouncement, String], ObservableValue[String]]() {
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)})")
})
networkNodesIPColumn.setCellValueFactory(new Callback[CellDataFeatures[NodeAnnouncement, String], ObservableValue[String]]() {
def call(pn: CellDataFeatures[NodeAnnouncement, String]) = {
val address = pn.getValue.addresses.map(a => s"${a.getHostString}:${a.getPort}").mkString(",")
new SimpleStringProperty(address)
}
})
networkNodesRGBColumn.setCellFactory(new Callback[TableColumn[NodeAnnouncement, String], TableCell[NodeAnnouncement, String]]() {
def call(pn: TableColumn[NodeAnnouncement, String]) = {
new TableCell[NodeAnnouncement, String]() {
override def updateItem(item: String, empty: Boolean): Unit = {
super.updateItem(item, empty)
if (empty || item == null) {
setText(null)
setGraphic(null)
setStyle(null)
} else {
setStyle("-fx-background-color:" + item)
}
}
}
}
})
networkNodesTable.setRowFactory(new Callback[TableView[NodeAnnouncement], TableRow[NodeAnnouncement]]() {
override def call(table: TableView[NodeAnnouncement]): TableRow[NodeAnnouncement] = setupPeerNodeContextMenu
})
// init all channels
networkChannelsTable.setItems(networkChannelsList)
networkChannelsList.addListener(new ListChangeListener[ChannelInfo] {
override def onChanged(c: Change[_ <: ChannelInfo]) = updateTabHeader(networkChannelsTab, "All Channels", networkChannelsList)
})
networkChannelsIdColumn.setCellValueFactory(new Callback[CellDataFeatures[ChannelInfo, String], ObservableValue[String]]() {
def call(pc: CellDataFeatures[ChannelInfo, String]) = new SimpleStringProperty(pc.getValue.announcement.shortChannelId.toHexString)
})
networkChannelsNode1Column.setCellValueFactory(new Callback[CellDataFeatures[ChannelInfo, String], ObservableValue[String]]() {
def call(pc: CellDataFeatures[ChannelInfo, String]) = new SimpleStringProperty(pc.getValue.announcement.nodeId1.toString)
})
networkChannelsNode2Column.setCellValueFactory(new Callback[CellDataFeatures[ChannelInfo, String], ObservableValue[String]]() {
def call(pc: CellDataFeatures[ChannelInfo, String]) = new SimpleStringProperty(pc.getValue.announcement.nodeId2.toString)
})
networkChannelsTable.setRowFactory(new Callback[TableView[ChannelInfo], TableRow[ChannelInfo]]() {
override def call(table: TableView[ChannelInfo]): TableRow[ChannelInfo] = setupPeerChannelContextMenu
})
networkChannelsDirectionsColumn.setCellValueFactory(new Callback[CellDataFeatures[ChannelInfo, ChannelInfo], ObservableValue[ChannelInfo]]() {
def call(pc: CellDataFeatures[ChannelInfo, ChannelInfo]) = new SimpleObjectProperty[ChannelInfo](pc.getValue)
})
networkChannelsDirectionsColumn.setCellFactory(new Callback[TableColumn[ChannelInfo, ChannelInfo], TableCell[ChannelInfo, ChannelInfo]]() {
def call(pn: TableColumn[ChannelInfo, ChannelInfo]) = {
new TableCell[ChannelInfo, ChannelInfo]() {
val directionImage = new ImageView
directionImage.setFitWidth(20)
directionImage.setFitHeight(20)
override def updateItem(item: ChannelInfo, empty: Boolean): Unit = {
super.updateItem(item, empty)
if (item == null || empty) {
setGraphic(null)
setText(null)
} else {
item match {
case ChannelInfo(_ , Some(true), Some(true)) =>
directionImage.setImage(new Image("/gui/commons/images/in-out-11.png", false))
setTooltip(new Tooltip("Both Node 1 and Node 2 are enabled"))
setGraphic(directionImage)
case ChannelInfo(_ , Some(true), Some(false)) =>
directionImage.setImage(new Image("/gui/commons/images/in-out-10.png", false))
setTooltip(new Tooltip("Node 1 is enabled, but not Node 2"))
setGraphic(directionImage)
case ChannelInfo(_ , Some(false), Some(true)) =>
directionImage.setImage(new Image("/gui/commons/images/in-out-01.png", false))
setTooltip(new Tooltip("Node 2 is enabled, but not Node 1"))
setGraphic(directionImage)
case ChannelInfo(_ , Some(false), Some(false)) =>
directionImage.setImage(new Image("/gui/commons/images/in-out-00.png", false))
setTooltip(new Tooltip("Neither Node 1 nor Node 2 is enabled"))
setGraphic(directionImage)
case _ =>
setText("?")
setGraphic(null)
}
}
}
}
}
})
// init payment sent
paymentSentTable.setItems(paymentSentList)
paymentSentList.addListener(new ListChangeListener[PaymentSentRecord] {
override def onChanged(c: Change[_ <: PaymentSentRecord]) = updateTabHeader(paymentSentTab, "Sent", paymentSentList)
})
paymentSentAmountColumn.setCellValueFactory(new Callback[CellDataFeatures[PaymentSentRecord, Number], ObservableValue[Number]]() {
def call(record: CellDataFeatures[PaymentSentRecord, Number]) = new SimpleLongProperty(record.getValue.event.amount.amount)
})
paymentSentAmountColumn.setCellFactory(new Callback[TableColumn[PaymentSentRecord, Number], TableCell[PaymentSentRecord, Number]]() {
def call(record: TableColumn[PaymentSentRecord, Number]) = buildMoneyTableCell
})
paymentSentFeesColumn.setCellValueFactory(new Callback[CellDataFeatures[PaymentSentRecord, Number], ObservableValue[Number]]() {
def call(record: CellDataFeatures[PaymentSentRecord, Number]) = new SimpleLongProperty(record.getValue.event.feesPaid.amount)
})
paymentSentFeesColumn.setCellFactory(new Callback[TableColumn[PaymentSentRecord, Number], TableCell[PaymentSentRecord, Number]]() {
def call(record: TableColumn[PaymentSentRecord, Number]) = buildMoneyTableCell
})
paymentSentHashColumn.setCellValueFactory(paymentHashCellValueFactory)
paymentSentDateColumn.setCellValueFactory(paymentDateCellValueFactory)
paymentSentTable.setRowFactory(paymentRowFactory)
// init payment received
paymentReceivedTable.setItems(paymentReceivedList)
paymentReceivedList.addListener(new ListChangeListener[PaymentReceivedRecord] {
override def onChanged(c: Change[_ <: PaymentReceivedRecord]) = updateTabHeader(paymentReceivedTab, "Received", paymentReceivedList)
})
paymentReceivedAmountColumn.setCellValueFactory(new Callback[CellDataFeatures[PaymentReceivedRecord, Number], ObservableValue[Number]]() {
def call(p: CellDataFeatures[PaymentReceivedRecord, Number]) = new SimpleLongProperty(p.getValue.event.amount.amount)
})
paymentReceivedAmountColumn.setCellFactory(new Callback[TableColumn[PaymentReceivedRecord, Number], TableCell[PaymentReceivedRecord, Number]]() {
def call(pn: TableColumn[PaymentReceivedRecord, Number]) = buildMoneyTableCell
})
paymentReceivedHashColumn.setCellValueFactory(paymentHashCellValueFactory)
paymentReceivedDateColumn.setCellValueFactory(paymentDateCellValueFactory)
paymentReceivedTable.setRowFactory(paymentRowFactory)
// init payment relayed
paymentRelayedTable.setItems(paymentRelayedList)
paymentRelayedList.addListener(new ListChangeListener[PaymentRelayedRecord] {
override def onChanged(c: Change[_ <: PaymentRelayedRecord]) = updateTabHeader(paymentRelayedTab, "Relayed", paymentRelayedList)
})
paymentRelayedAmountColumn.setCellValueFactory(new Callback[CellDataFeatures[PaymentRelayedRecord, Number], ObservableValue[Number]]() {
def call(p: CellDataFeatures[PaymentRelayedRecord, Number]) = new SimpleLongProperty(p.getValue.event.amount.amount)
})
paymentRelayedAmountColumn.setCellFactory(new Callback[TableColumn[PaymentRelayedRecord, Number], TableCell[PaymentRelayedRecord, Number]]() {
def call(pn: TableColumn[PaymentRelayedRecord, Number]) = buildMoneyTableCell
})
paymentRelayedFeesColumn.setCellValueFactory(new Callback[CellDataFeatures[PaymentRelayedRecord, Number], ObservableValue[Number]]() {
def call(p: CellDataFeatures[PaymentRelayedRecord, Number]) = new SimpleLongProperty(p.getValue.event.feesEarned.amount)
})
paymentRelayedFeesColumn.setCellFactory(new Callback[TableColumn[PaymentRelayedRecord, Number], TableCell[PaymentRelayedRecord, Number]]() {
def call(pn: TableColumn[PaymentRelayedRecord, Number]) = buildMoneyTableCell
})
paymentRelayedHashColumn.setCellValueFactory(paymentHashCellValueFactory)
paymentRelayedDateColumn.setCellValueFactory(paymentDateCellValueFactory)
paymentRelayedTable.setRowFactory(paymentRowFactory)
}
def initInfoFields(setup: Setup) = {
// 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))
labelApi.setText(s"${setup.config.getInt("api.port")}")
labelServer.setText(s"${setup.config.getInt("server.port")}")
bitcoinVersion.setText(s"v0.0.0")
//bitcoinVersion.setText(s"v${setup.bitcoinVersion}")
bitcoinChain.setText(s"${setup.chain.toUpperCase()}")
bitcoinChain.getStyleClass.add(setup.chain)
contextMenu = ContextMenuUtils.buildCopyContext(
List(
Some(new CopyAction("Copy Pubkey", s"${setup.nodeParams.privateKey.publicKey}")),
setup.nodeParams.publicAddresses.headOption.map(address => new CopyAction("Copy URI", s"${setup.nodeParams.privateKey.publicKey}@${address.getHostString}:${address.getPort}"))
).flatten)
}
private def updateTabHeader(tab: Tab, prefix: String, items: ObservableList[_]) = {
Platform.runLater(new Runnable() {
override def run = tab.setText(s"$prefix (${items.size})")
})
}
private def paymentHashCellValueFactory[T <: Record] = new Callback[CellDataFeatures[T, String], ObservableValue[String]]() {
def call(p: CellDataFeatures[T, String]) = new SimpleStringProperty(p.getValue.event.paymentHash.toString)
}
private def buildMoneyTableCell[T <: Record] = new TableCell[T, Number]() {
override def updateItem(item: Number, empty: Boolean) = {
super.updateItem(item, empty)
if (empty || item == null) {
setText(null)
setGraphic(null)
} else {
setText(moneyFormatter.format(item))
}
}
}
private def paymentDateCellValueFactory[T <: Record] = new Callback[CellDataFeatures[T, String], ObservableValue[String]]() {
def call(p: CellDataFeatures[T, String]) = new SimpleStringProperty(p.getValue.date.format(PAYMENT_DATE_FORMAT))
}
private def paymentRowFactory[T <: Record] = new Callback[TableView[T], TableRow[T]]() {
override def call(table: TableView[T]): TableRow[T] = {
val row = new TableRow[T]
val rowContextMenu = new ContextMenu
val copyHash = new MenuItem("Copy Payment Hash")
copyHash.setOnAction(new EventHandler[ActionEvent] {
override def handle(event: ActionEvent) = Option(row.getItem) match {
case Some(p) => ContextMenuUtils.copyToClipboard(p.event.paymentHash.toString)
case None =>
}
})
rowContextMenu.getItems.addAll(copyHash)
row.setContextMenu(rowContextMenu)
row
}
}
/**
* Create a row for a node with context actions (copy node uri and id).
*
* @return TableRow the created row
*/
private def setupPeerNodeContextMenu(): TableRow[NodeAnnouncement] = {
val row = new TableRow[NodeAnnouncement]
val rowContextMenu = new ContextMenu
val copyPubkey = new MenuItem("Copy Pubkey")
copyPubkey.setOnAction(new EventHandler[ActionEvent] {
override def handle(event: ActionEvent) = Option(row.getItem) match {
case Some(pn) => ContextMenuUtils.copyToClipboard(pn.nodeId.toString)
case None =>
}
})
val copyURI = new MenuItem("Copy first known URI")
copyURI.setOnAction(new EventHandler[ActionEvent] {
override def handle(event: ActionEvent) = Option(row.getItem) match {
case Some(pn) => ContextMenuUtils.copyToClipboard(
if (pn.addresses.nonEmpty) s"${pn.nodeId.toString}@${pn.addresses.head.getHostString}:${pn.addresses.head.getPort}"
else "no URI Known")
case None =>
}
})
rowContextMenu.getItems.addAll(copyPubkey, copyURI)
row.setContextMenu(rowContextMenu)
row
}
/**
* Create a row for a PeerChannel with Copy context actions.
*
* @return TableRow the created row
*/
private def setupPeerChannelContextMenu(): TableRow[ChannelInfo] = {
val row = new TableRow[ChannelInfo]
val rowContextMenu = new ContextMenu
val copyChannelId = new MenuItem("Copy Channel Id")
copyChannelId.setOnAction(new EventHandler[ActionEvent] {
override def handle(event: ActionEvent) = Option(row.getItem) match {
case Some(pc) => ContextMenuUtils.copyToClipboard(pc.announcement.shortChannelId.toHexString)
case None =>
}
})
val copyNode1 = new MenuItem("Copy Node 1")
copyNode1.setOnAction(new EventHandler[ActionEvent] {
override def handle(event: ActionEvent) = Option(row.getItem) match {
case Some(pc) => ContextMenuUtils.copyToClipboard(pc.announcement.nodeId1.toString)
case None =>
}
})
val copyNode2 = new MenuItem("Copy Node 2")
copyNode2.setOnAction(new EventHandler[ActionEvent] {
override def handle(event: ActionEvent) = Option(row.getItem) match {
case Some(pc) => ContextMenuUtils.copyToClipboard(pc.announcement.nodeId2.toString)
case None =>
}
})
rowContextMenu.getItems.addAll(copyChannelId, copyNode1, copyNode2)
row.setContextMenu(rowContextMenu)
row
}
@FXML def handleExportDot = {
val fileChooser = new FileChooser
fileChooser.setTitle("Save as")
fileChooser.getExtensionFilters.addAll(new ExtensionFilter("DOT File (*.dot)", "*.dot"))
val file = fileChooser.showSaveDialog(getWindow.getOrElse(null))
if (file != null) handlers.exportToDot(file)
}
@FXML def handleOpenChannel = {
val openChannelStage = new OpenChannelStage(handlers)
openChannelStage.initOwner(getWindow.getOrElse(null))
positionAtCenter(openChannelStage)
openChannelStage.show
}
@FXML def handleSendPayment = {
val sendPaymentStage = new SendPaymentStage(handlers)
sendPaymentStage.initOwner(getWindow.getOrElse(null))
positionAtCenter(sendPaymentStage)
sendPaymentStage.show
}
@FXML def handleReceivePayment = {
val receiveStage = new ReceivePaymentStage(handlers)
receiveStage.initOwner(getWindow.getOrElse(null))
positionAtCenter(receiveStage)
receiveStage.show
}
def showBlockerModal = {
val fadeTransition = new FadeTransition(Duration.millis(300))
fadeTransition.setFromValue(0)
fadeTransition.setToValue(1)
val translateTransition = new TranslateTransition(Duration.millis(300))
translateTransition.setFromY(20)
translateTransition.setToY(0)
blocker.setVisible(true)
val ftCover = new FadeTransition(Duration.millis(200), blocker)
ftCover.setFromValue(0)
ftCover.setToValue(1)
ftCover.play
val t = new ParallelTransition(blockerDialog, fadeTransition, translateTransition)
t.setDelay(Duration.millis(200))
t.play
}
def hideBlockerModal = {
val ftCover = new FadeTransition(Duration.millis(400))
ftCover.setFromValue(1)
ftCover.setToValue(0)
val s = new SequentialTransition(blocker, ftCover)
s.setOnFinished(new EventHandler[ActionEvent]() {
override def handle(event: ActionEvent): Unit = blocker.setVisible(false)
})
s.play
}
private def getWindow: Option[Window] = {
Option(root).map(_.getScene.getWindow)
}
@FXML def handleCloseRequest = getWindow.map(_.fireEvent(new WindowEvent(getWindow.get, WindowEvent.WINDOW_CLOSE_REQUEST)))
@FXML def handleOpenAbout = {
val aboutStage = new AboutStage(hostServices)
aboutStage.initOwner(getWindow.getOrElse(null))
positionAtCenter(aboutStage)
aboutStage.show
}
@FXML def openNodeIdContext(event: ContextMenuEvent) = contextMenu.show(labelNodeId, event.getScreenX, event.getScreenY)
def positionAtCenter(childStage: Stage) = {
childStage.setX(getWindow.map(w => w.getX + w.getWidth / 2 - childStage.getWidth / 2).getOrElse(0))
childStage.setY(getWindow.map(w => w.getY + w.getHeight / 2 - childStage.getHeight / 2).getOrElse(0))
}
}

View file

@ -1,26 +0,0 @@
package fr.acinq.eclair.gui.controllers
import javafx.fxml.FXML
import javafx.scene.control.{Button, Label}
import javafx.scene.image.ImageView
import javafx.scene.input.MouseEvent
import javafx.scene.layout.GridPane
/**
* Created by DPA on 17/02/2017.
*/
class NotificationPaneController {
@FXML var rootPane: GridPane = _
@FXML var titleLabel: Label = _
@FXML var messageLabel: Label = _
@FXML var icon: ImageView = _
@FXML var closeButton: Button = _
@FXML def handleMouseEnter(event: MouseEvent) = {
rootPane.setOpacity(1)
}
@FXML def handleMouseExit(event: MouseEvent) = {
rootPane.setOpacity(0.95)
}
}

View file

@ -1,109 +0,0 @@
package fr.acinq.eclair.gui.controllers
import javafx.animation._
import javafx.application.Platform
import javafx.event.{ActionEvent, EventHandler}
import javafx.fxml.{FXML, FXMLLoader}
import javafx.scene.Parent
import javafx.scene.image.Image
import javafx.scene.layout.{GridPane, VBox}
import javafx.util.Duration
import grizzled.slf4j.Logging
sealed trait NotificationType
case object NOTIFICATION_NONE extends NotificationType
case object NOTIFICATION_SUCCESS extends NotificationType
case object NOTIFICATION_ERROR extends NotificationType
case object NOTIFICATION_INFO extends NotificationType
/**
* Created by DPA on 17/02/2017.
*/
class NotificationsController extends Logging {
@FXML var notifsVBox: VBox = _
val successIcon: Image = new Image(getClass.getResource("/gui/commons/images/success_icon.png").toExternalForm, true)
val errorIcon: Image = new Image(getClass.getResource("/gui/commons/images/warning_icon.png").toExternalForm, true)
val infoIcon: Image = new Image(getClass.getResource("/gui/commons/images/info_icon.png").toExternalForm, true)
/**
* Adds a notification panel to the notifications stage
*
* @param title Title of the notification, should not be too long
* @param message Main message of the notification
* @param notificationType type of the notification (error, warning, success, info...)
*/
def addNotification (title: String, message: String, notificationType: NotificationType) = {
val loader = new FXMLLoader(getClass.getResource("/gui/main/notificationPane.fxml"))
val notifPaneController = new NotificationPaneController
loader.setController(notifPaneController)
Platform.runLater(new Runnable() {
override def run = {
val root = loader.load[GridPane]
notifsVBox.getChildren.add(root)
// set notification content
notifPaneController.titleLabel.setText(title)
notifPaneController.messageLabel.setText(message.capitalize)
notificationType match {
case NOTIFICATION_SUCCESS => {
notifPaneController.rootPane.setStyle("-fx-border-color: #28d087")
notifPaneController.icon.setImage(successIcon)
}
case NOTIFICATION_ERROR => {
notifPaneController.rootPane.setStyle("-fx-border-color: #d43c4e")
notifPaneController.icon.setImage(errorIcon)
}
case NOTIFICATION_INFO => {
notifPaneController.rootPane.setStyle("-fx-border-color: #409be6")
notifPaneController.icon.setImage(infoIcon)
}
case _ =>
}
// in/out animations
val showAnimation = getShowAnimation(notifPaneController.rootPane)
val dismissAnimation = getDismissAnimation(notifPaneController.rootPane)
dismissAnimation.setOnFinished(new EventHandler[ActionEvent] {
override def handle(event: ActionEvent) = notifsVBox.getChildren.remove(root)
})
notifPaneController.closeButton.setOnAction(new EventHandler[ActionEvent] {
override def handle(event: ActionEvent) = {
dismissAnimation.stop
dismissAnimation.setDelay(Duration.ZERO)
dismissAnimation.play
}
})
showAnimation.play
dismissAnimation.setDelay(Duration.seconds(12))
dismissAnimation.play
}
})
}
private def getDismissAnimation(element: Parent): Transition = {
val fadeOutTransition = new FadeTransition(Duration.millis(200))
fadeOutTransition.setFromValue(1)
fadeOutTransition.setToValue(0)
val translateRevTransition = new TranslateTransition(Duration.millis(450))
translateRevTransition.setFromX(0)
translateRevTransition.setToX(150)
val scaleTransition = new ScaleTransition(Duration.millis(350))
scaleTransition.setFromY(1)
scaleTransition.setToY(0)
new ParallelTransition(element, fadeOutTransition, translateRevTransition, scaleTransition)
}
private def getShowAnimation(element: Parent): Transition = {
val fadeTransition = new FadeTransition(Duration.millis(400))
fadeTransition.setFromValue(0)
fadeTransition.setToValue(.95)
val translateTransition = new TranslateTransition(Duration.millis(500))
translateTransition.setFromX(150)
translateTransition.setToX(0)
new ParallelTransition(element, fadeTransition, translateTransition)
}
}

View file

@ -1,89 +0,0 @@
package fr.acinq.eclair.gui.controllers
import java.lang.Boolean
import javafx.beans.value.{ChangeListener, ObservableValue}
import javafx.event.ActionEvent
import javafx.fxml.FXML
import javafx.scene.control._
import javafx.stage.Stage
import fr.acinq.bitcoin.{MilliSatoshi, Satoshi}
import fr.acinq.eclair.Setup
import fr.acinq.eclair.channel.ChannelFlags
import fr.acinq.eclair.gui.Handlers
import fr.acinq.eclair.gui.utils.GUIValidators
import fr.acinq.eclair.io.Switchboard.NewChannel
import grizzled.slf4j.Logging
/**
* Created by DPA on 23/09/2016.
*/
class OpenChannelController(val handlers: Handlers, val stage: Stage) extends Logging {
/**
* Funding must be less than {@code 2^24} satoshi.
* PushMsat must not be greater than 1000 * Max funding
*
* https://github.com/lightningnetwork/lightning-rfc/blob/master/02-peer-protocol.md#requirements
*/
val maxFunding = 16777216L
val maxPushMsat = 1000L * maxFunding
@FXML var host: TextField = _
@FXML var hostError: Label = _
@FXML var simpleConnection: CheckBox = _
@FXML var fundingSatoshis: TextField = _
@FXML var fundingSatoshisError: Label = _
@FXML var pushMsat: TextField = _
@FXML var pushMsatError: Label = _
@FXML var publicChannel: CheckBox = _
@FXML var unit: ComboBox[String] = _
@FXML var button: Button = _
@FXML def initialize = {
unit.setValue(unit.getItems.get(0))
simpleConnection.selectedProperty.addListener(new ChangeListener[Boolean] {
override def changed(observable: ObservableValue[_ <: Boolean], oldValue: Boolean, newValue: Boolean) = {
fundingSatoshis.setDisable(newValue)
pushMsat.setDisable(newValue)
unit.setDisable(newValue)
}
})
}
@FXML def handleOpen(event: ActionEvent) = {
if (GUIValidators.validate(host.getText, hostError, "Please use a valid url (pubkey@host:port)", GUIValidators.hostRegex)) {
if (simpleConnection.isSelected) {
handlers.open(host.getText, None)
stage.close
} else {
if (GUIValidators.validate(fundingSatoshis.getText, fundingSatoshisError, "Funding must be numeric", GUIValidators.amountRegex)
&& GUIValidators.validate(fundingSatoshisError, "Funding must be greater than 0", fundingSatoshis.getText.toLong > 0)) {
val rawFunding = fundingSatoshis.getText.toLong
val smartFunding = unit.getValue match {
case "milliBTC" => Satoshi(rawFunding * 100000L)
case "Satoshi" => Satoshi(rawFunding)
case "milliSatoshi" => Satoshi(rawFunding / 1000L)
}
if (GUIValidators.validate(fundingSatoshisError, "Funding must be 16 777 216 satoshis (~0.167 BTC) or less", smartFunding.toLong < maxFunding)) {
if (!pushMsat.getText.isEmpty) {
// pushMsat is optional, so we validate field only if it isn't empty
if (GUIValidators.validate(pushMsat.getText, pushMsatError, "Push msat must be numeric", GUIValidators.amountRegex)
&& GUIValidators.validate(pushMsatError, "Push msat must be 16 777 216 000 msat (~0.167 BTC) or less", pushMsat.getText.toLong <= maxPushMsat)) {
val channelFlags = if(publicChannel.isSelected) ChannelFlags.AnnounceChannel else ChannelFlags.Empty
handlers.open(host.getText, Some(NewChannel(smartFunding, MilliSatoshi(pushMsat.getText.toLong), Some(channelFlags))))
stage.close
}
} else {
handlers.open(host.getText, Some(NewChannel(smartFunding, MilliSatoshi(0), None)))
stage.close
}
}
}
}
}
}
@FXML def handleClose(event: ActionEvent) = stage.close
}

View file

@ -1,130 +0,0 @@
package fr.acinq.eclair.gui.controllers
import javafx.application.Platform
import javafx.event.ActionEvent
import javafx.fxml.FXML
import javafx.scene.control.{ComboBox, Label, TextArea, TextField}
import javafx.scene.image.{ImageView, WritableImage}
import javafx.scene.layout.GridPane
import javafx.scene.paint.Color
import javafx.stage.Stage
import com.google.zxing.qrcode.QRCodeWriter
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import com.google.zxing.{BarcodeFormat, EncodeHintType}
import fr.acinq.bitcoin.MilliSatoshi
import fr.acinq.eclair.Setup
import fr.acinq.eclair.gui.Handlers
import fr.acinq.eclair.gui.utils.{ContextMenuUtils, GUIValidators}
import fr.acinq.eclair.payment.PaymentRequest
import grizzled.slf4j.Logging
import scala.util.{Failure, Success, Try}
/**
* Created by DPA on 23/09/2016.
*/
class ReceivePaymentController(val handlers: Handlers, val stage: Stage) extends Logging {
@FXML var amount: TextField = _
@FXML var amountError: Label = _
@FXML var unit: ComboBox[String] = _
@FXML var description: TextArea = _
@FXML var resultBox: GridPane = _
// the content of this field is generated and readonly
@FXML var paymentRequestTextArea: TextArea = _
@FXML var paymentRequestQRCode: ImageView = _
@FXML def initialize = {
unit.setValue(unit.getItems.get(0))
resultBox.managedProperty().bind(resultBox.visibleProperty())
stage.sizeToScene()
}
@FXML def handleCopyInvoice(event: ActionEvent) = ContextMenuUtils.copyToClipboard(paymentRequestTextArea.getText)
@FXML def handleGenerate(event: ActionEvent) = {
if ((("milliBTC".equals(unit.getValue) || "Satoshi".equals(unit.getValue))
&& GUIValidators.validate(amount.getText, amountError, "Amount must be numeric", GUIValidators.amountDecRegex))
|| ("milliSatoshi".equals(unit.getValue) && GUIValidators.validate(amount.getText, amountError, "Amount must be numeric (no decimal msat)", GUIValidators.amountRegex))) {
try {
val Array(parsedInt, parsedDec) = if (amount.getText.contains(".")) amount.getText.split("\\.") else Array(amount.getText, "0")
val amountDec = parsedDec.length match {
case 0 => "000"
case 1 => parsedDec.concat("00")
case 2 => parsedDec.concat("0")
case 3 => parsedDec
case _ =>
// amount has too many decimals, regex validation has failed somehow
throw new NumberFormatException("incorrect amount")
}
val smartAmount = unit.getValue match {
case "milliBTC" => MilliSatoshi(parsedInt.toLong * 100000000L + amountDec.toLong * 100000L)
case "Satoshi" => MilliSatoshi(parsedInt.toLong * 1000L + amountDec.toLong)
case "milliSatoshi" => MilliSatoshi(amount.getText.toLong)
}
if (GUIValidators.validate(amountError, "Amount must be greater than 0", smartAmount.amount > 0)
&& GUIValidators.validate(amountError, f"Amount must be less than ${PaymentRequest.maxAmount.amount}%,d msat (~${PaymentRequest.maxAmount.amount / 1e11}%.3f BTC)", smartAmount < PaymentRequest.maxAmount)
&& GUIValidators.validate(amountError, "Description is too long, max 256 chars.", description.getText().size < 256)) {
import scala.concurrent.ExecutionContext.Implicits.global
handlers.receive(smartAmount, description.getText) onComplete {
case Success(s) =>
Try(createQRCode(s)) match {
case Success(wImage) => displayPaymentRequest(s, Some(wImage))
case Failure(t) => displayPaymentRequest(s, None)
}
case Failure(t) => Platform.runLater(new Runnable {
def run = GUIValidators.validate(amountError, "The payment request could not be generated", false)
})
}
}
} catch {
case e: NumberFormatException =>
logger.debug(s"Could not generate payment request for amount = ${amount.getText}")
paymentRequestTextArea.setText("")
amountError.setText("Amount is incorrect")
amountError.setOpacity(1)
}
}
}
private def displayPaymentRequest(pr: String, image: Option[WritableImage]) = Platform.runLater(new Runnable {
def run = {
paymentRequestTextArea.setText(pr)
if ("".equals(pr)) {
resultBox.setVisible(false)
resultBox.setMaxHeight(0)
} else {
resultBox.setVisible(true)
resultBox.setMaxHeight(Double.MaxValue)
}
image.map(paymentRequestQRCode.setImage(_))
stage.sizeToScene()
}
})
private def createQRCode(data: String, width: Int = 250, height: Int = 250, margin: Int = -1): WritableImage = {
import scala.collection.JavaConversions._
val hintMap = collection.mutable.Map[EncodeHintType, Object]()
hintMap.put(EncodeHintType.CHARACTER_SET, "UTF-8")
hintMap.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L)
hintMap.put(EncodeHintType.MARGIN, margin.toString)
val qrWriter = new QRCodeWriter
val byteMatrix = qrWriter.encode(data, BarcodeFormat.QR_CODE, width, height, hintMap)
val writableImage = new WritableImage(width, height)
val pixelWriter = writableImage.getPixelWriter
for (i <- 0 to byteMatrix.getWidth - 1) {
for (j <- 0 to byteMatrix.getWidth - 1) {
if (byteMatrix.get(i, j)) {
pixelWriter.setColor(i, j, Color.BLACK)
} else {
pixelWriter.setColor(i, j, Color.WHITE)
}
}
}
writableImage
}
@FXML def handleClose(event: ActionEvent) = stage.close
}

View file

@ -1,80 +0,0 @@
package fr.acinq.eclair.gui.controllers
import javafx.beans.value.{ChangeListener, ObservableValue}
import javafx.event.{ActionEvent, EventHandler}
import javafx.fxml.FXML
import javafx.scene.control.{Button, Label, TextArea, TextField}
import javafx.scene.input.KeyCode.{ENTER, TAB}
import javafx.scene.input.KeyEvent
import javafx.stage.Stage
import fr.acinq.bitcoin.BinaryData
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair.Setup
import fr.acinq.eclair.gui.Handlers
import fr.acinq.eclair.gui.utils.GUIValidators
import fr.acinq.eclair.payment.PaymentRequest
import grizzled.slf4j.Logging
import scala.util.{Failure, Success, Try}
/**
* Created by DPA on 23/09/2016.
*/
class SendPaymentController(val handlers: Handlers, val stage: Stage) extends Logging {
@FXML var paymentRequest: TextArea = _
@FXML var paymentRequestError: Label = _
@FXML var nodeIdField: TextField = _
@FXML var amountField: TextField = _
@FXML var hashField: TextField = _
@FXML var sendButton: Button = _
@FXML def initialize(): Unit = {
// ENTER or TAB events in the paymentRequest textarea insted fire or focus sendButton
paymentRequest.addEventFilter(KeyEvent.KEY_PRESSED, new EventHandler[KeyEvent] {
def handle(event: KeyEvent) = {
event.getCode match {
case ENTER =>
sendButton.fire
event.consume
case TAB =>
sendButton.requestFocus()
event.consume
case _ =>
}
}
})
paymentRequest.textProperty.addListener(new ChangeListener[String] {
def changed(observable: ObservableValue[_ <: String], oldValue: String, newValue: String) = {
Try(PaymentRequest.read(paymentRequest.getText)) match {
case Success(pr) =>
pr.amount.foreach(amount => amountField.setText(amount.amount.toString))
nodeIdField.setText(pr.nodeId.toString)
hashField.setText(pr.paymentHash.toString)
case Failure(f) =>
GUIValidators.validate(paymentRequestError, "Please use a valid payment request", false)
amountField.setText("0")
nodeIdField.setText("N/A")
hashField.setText("N/A")
}
}
})
}
@FXML def handleSend(event: ActionEvent) = {
Try(PaymentRequest.read(paymentRequest.getText)) match {
case Success(pr) =>
Try(handlers.send(pr.nodeId, pr.paymentHash, pr.amount.get.amount)) match {
case Success(s) => stage.close
case Failure(f) => GUIValidators.validate(paymentRequestError, s"Invalid Payment Request: ${f.getMessage}", false)
}
case Failure(f) => GUIValidators.validate(paymentRequestError, "cannot parse payment request", false)
}
}
@FXML def handleClose(event: ActionEvent) = {
stage.close
}
}

View file

@ -1,70 +0,0 @@
package fr.acinq.eclair.gui.controllers
import javafx.animation._
import javafx.application.HostServices
import javafx.fxml.FXML
import javafx.scene.control.{Button, Label}
import javafx.scene.image.ImageView
import javafx.scene.layout.{HBox, Pane, VBox}
import javafx.util.Duration
import grizzled.slf4j.Logging
/**
* Created by DPA on 22/09/2016.
*/
class SplashController(hostServices: HostServices) extends Logging {
@FXML var splash: Pane = _
@FXML var img: ImageView = _
@FXML var imgBlurred: ImageView = _
@FXML var closeButton: Button = _
@FXML var errorBox: VBox = _
@FXML var logBox: VBox = _
/**
* Start an animation when the splash window is initialized
*/
@FXML def initialize = {
val timeline = new Timeline(
new KeyFrame(Duration.ZERO,
new KeyValue(img.opacityProperty, double2Double(0), Interpolator.EASE_IN),
new KeyValue(imgBlurred.opacityProperty, double2Double(1.0), Interpolator.EASE_IN)),
new KeyFrame(Duration.millis(1000.0d),
new KeyValue(img.opacityProperty, double2Double(1.0), Interpolator.EASE_OUT),
new KeyValue(imgBlurred.opacityProperty, double2Double(0), Interpolator.EASE_OUT)))
timeline.play()
}
@FXML def closeAndKill = System.exit(0)
@FXML def openGithubPage = hostServices.showDocument("https://github.com/ACINQ/eclair/blob/master/README.md")
def addLog(message: String) = {
val l = new Label
l.setText(message)
l.setWrapText(true)
logBox.getChildren.add(l)
}
def addError(message: String) = {
val l = new Label
l.setText(message)
l.setWrapText(true)
l.getStyleClass.add("text-error")
logBox.getChildren.add(l)
}
/**
* Shows the error Box with a fade+translate transition.
*/
def showErrorBox = {
val fadeTransition = new FadeTransition(Duration.millis(400))
fadeTransition.setFromValue(0)
fadeTransition.setToValue(1)
val translateTransition = new TranslateTransition(Duration.millis(500))
translateTransition.setFromY(20)
translateTransition.setToY(0)
val t = new ParallelTransition(errorBox, fadeTransition, translateTransition)
t.play
}
}

View file

@ -1,46 +0,0 @@
package fr.acinq.eclair.gui.stages
import javafx.application.HostServices
import javafx.event.EventHandler
import javafx.fxml.FXMLLoader
import javafx.scene.image.Image
import javafx.scene.input.KeyCode._
import javafx.scene.input.KeyEvent
import javafx.scene.{Parent, Scene}
import javafx.stage.{Modality, Stage, StageStyle}
import fr.acinq.eclair.gui.controllers.AboutController
/**
* Created by DPA on 28/09/2016.
*/
class AboutStage(hostServices: HostServices) extends Stage() {
initModality(Modality.WINDOW_MODAL)
initStyle(StageStyle.DECORATED)
getIcons().add(new Image("/gui/commons/images/eclair-square.png", false))
setTitle("About Eclair")
setResizable(false)
setWidth(500)
setHeight(200)
// get fxml/controller
val openFXML = new FXMLLoader(getClass.getResource("/gui/modals/about.fxml"))
openFXML.setController(new AboutController(hostServices))
val root = openFXML.load[Parent]
// create scene
val scene = new Scene(root)
val self = this
scene.addEventFilter(KeyEvent.KEY_PRESSED, new EventHandler[KeyEvent]() {
def handle(event: KeyEvent) = {
event.getCode match {
case ESCAPE =>
self.close
case _ =>
}
}
})
setScene(scene)
}

View file

@ -1,33 +0,0 @@
package fr.acinq.eclair.gui.stages
import javafx.fxml.FXMLLoader
import javafx.scene.image.Image
import javafx.scene.{Parent, Scene}
import javafx.stage.{Modality, Stage, StageStyle}
import fr.acinq.eclair.Setup
import fr.acinq.eclair.gui.Handlers
import fr.acinq.eclair.gui.controllers.OpenChannelController
/**
* Created by PM on 16/08/2016.
*/
class OpenChannelStage(handlers: Handlers) extends Stage() {
initModality(Modality.WINDOW_MODAL)
initStyle(StageStyle.DECORATED)
getIcons().add(new Image("/gui/commons/images/eclair-square.png", false))
setTitle("Open a new channel")
setMinWidth(550)
setWidth(550)
setMinHeight(350)
setHeight(350)
// get fxml/controller
val openFXML = new FXMLLoader(getClass.getResource("/gui/modals/openChannel.fxml"))
openFXML.setController(new OpenChannelController(handlers, this))
val root = openFXML.load[Parent]
// create scene
val scene = new Scene(root)
setScene(scene)
}

View file

@ -1,34 +0,0 @@
package fr.acinq.eclair.gui.stages
import javafx.fxml.FXMLLoader
import javafx.scene.image.Image
import javafx.scene.{Parent, Scene}
import javafx.stage.{Modality, Stage, StageStyle}
import fr.acinq.eclair.Setup
import fr.acinq.eclair.gui.Handlers
import fr.acinq.eclair.gui.controllers.ReceivePaymentController
/**
* Created by PM on 16/08/2016.
*/
class ReceivePaymentStage(handlers: Handlers) extends Stage() {
initModality(Modality.WINDOW_MODAL)
initStyle(StageStyle.DECORATED)
getIcons().add(new Image("/gui/commons/images/eclair-square.png", false))
setTitle("Receive a Payment")
setMinWidth(590)
setWidth(590)
setMinHeight(200)
setHeight(200)
setResizable(false)
// get fxml/controller
val receivePayment = new FXMLLoader(getClass.getResource("/gui/modals/receivePayment.fxml"))
receivePayment.setController(new ReceivePaymentController(handlers, this))
val root = receivePayment.load[Parent]
// create scene
val scene = new Scene(root)
setScene(scene)
}

View file

@ -1,34 +0,0 @@
package fr.acinq.eclair.gui.stages
import javafx.fxml.FXMLLoader
import javafx.scene.image.Image
import javafx.scene.{Parent, Scene}
import javafx.stage.{Modality, Stage, StageStyle}
import fr.acinq.eclair.Setup
import fr.acinq.eclair.gui.Handlers
import fr.acinq.eclair.gui.controllers.SendPaymentController
import grizzled.slf4j.Logging
/**
* Created by PM on 16/08/2016.
*/
class SendPaymentStage(handlers: Handlers) extends Stage() with Logging {
initModality(Modality.WINDOW_MODAL)
initStyle(StageStyle.DECORATED)
getIcons().add(new Image("/gui/commons/images/eclair-square.png", false))
setTitle("Send a Payment Request")
setMinWidth(450)
setWidth(450)
setMinHeight(450)
setHeight(450)
// get fxml/controller
val receivePayment = new FXMLLoader(getClass.getResource("/gui/modals/sendPayment.fxml"))
receivePayment.setController(new SendPaymentController(handlers, this))
val root = receivePayment.load[Parent]
// create scene
val scene = new Scene(root)
setScene(scene)
}

View file

@ -1,47 +0,0 @@
package fr.acinq.eclair.gui.utils
import javafx.event.{ActionEvent, EventHandler}
import javafx.scene.control.{ContextMenu, MenuItem}
import javafx.scene.input.{Clipboard, ClipboardContent}
import scala.collection.immutable.List
/**
* Created by DPA on 28/09/2016.
*/
/**
* Action to copy a value
*
* @param label label of the copy action in the context menu, defaults to copy value
* @param value the value to copy
*/
case class CopyAction(label: String = "Copy Value", value: String)
object ContextMenuUtils {
val clip = Clipboard.getSystemClipboard
/**
* Builds a Context Menu containing a list of copy actions.
*
* @param actions list of copy action (label + value)
* @return javafx context menu
*/
def buildCopyContext (actions: List[CopyAction]): ContextMenu = {
val context = new ContextMenu()
for (action <- actions ) {
val copyItem = new MenuItem(action.label)
copyItem.setOnAction(new EventHandler[ActionEvent] {
override def handle(event: ActionEvent) = copyToClipboard(action.value)
})
context.getItems.addAll(copyItem)
}
context
}
def copyToClipboard (value: String) = {
val clipContent = new ClipboardContent
clipContent.putString(value)
clip.setContent(clipContent)
}
}

View file

@ -1,46 +0,0 @@
package fr.acinq.eclair.gui.utils
import javafx.scene.control.Label
import scala.util.matching.Regex
/**
* Created by DPA on 27/09/2016.
*/
object GUIValidators {
val hostRegex = """([a-fA-F0-9]{66})@([a-zA-Z0-9:\.\-_]+):([0-9]+)""".r
val amountRegex = """\d+""".r
val amountDecRegex = """(\d+)|(\d+\.[\d]{1,3})""".r // accepts 3 decimals at most
val paymentRequestRegex = """([a-zA-Z0-9]+):([a-zA-Z0-9]+):([a-zA-Z0-9]+)""".r
val hexRegex = """[0-9a-fA-F]+""".r
/**
* Validate a field against a regex. If field does not match the regex, validatorLabel is shown.
*
* @param field String content of the field to validate
* @param validatorLabel JFX label associated to the field.
* @param validatorMessage Message displayed if the field is invalid. It should describe the cause of
* the validation failure
* @param regex Scala regex that the field must match
* @return true if field is valid, false otherwise
*/
def validate(field: String, validatorLabel: Label, validatorMessage: String, regex: Regex): Boolean = {
return field match {
case regex(_*) => validate(validatorLabel, validatorMessage, true)
case _ => validate(validatorLabel, validatorMessage, false)
}
}
/**
* Displays a label with an error message.
*
* @param errorLabel JFX label containing an error messsage
* @param validCondition if true the label is hidden, else it is shown
* @return true if field is valid, false otherwise
*/
def validate(errorLabel: Label, errorMessage: String, validCondition: Boolean): Boolean = {
errorLabel.setOpacity( if (validCondition) 0 else 1 )
errorLabel.setText(errorMessage)
validCondition
}
}

View file

@ -1,38 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" debug="false">
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<target>System.out</target>
<encoder>
<pattern>%date{HH:mm:ss.SSS} %highlight(%-5level) %X{akkaSource} - %msg%ex{12}%n</pattern>
</encoder>
</appender>
<!--appender name="CONSOLEWARN" class="ch.qos.logback.core.ConsoleAppender">
<target>System.out</target>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>WARN</level>
</filter>
<encoder>
<pattern>%-5level %X{akkaSource} - %msg%ex{12}%n</pattern>
</encoder>
</appender-->
<!--appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>eclair.log</file>
<append>false</append>
<encoder>
<pattern>%-5level %X{akkaSource} - %msg%ex{12}%n</pattern>
</encoder>
</appender-->
<logger name="fr.acinq.eclair.Pipe" level="DEBUG" />
<logger name="fr.acinq.eclair.crypto.TransportHandler" level="DEBUG" />
<root level="INFO">
<!--appender-ref ref="FILE"/>
<appender-ref ref="CONSOLEWARN"/-->
<appender-ref ref="CONSOLE"/>
</root>
</configuration>

View file

@ -5,7 +5,7 @@
<parent>
<groupId>fr.acinq.eclair</groupId>
<artifactId>eclair_2.11</artifactId>
<version>0.2-spv-SNAPSHOT</version>
<version>0.2-android-SNAPSHOT</version>
</parent>
<artifactId>eclair-node_2.11</artifactId>
@ -91,5 +91,10 @@
<artifactId>janino</artifactId>
<version>2.5.10</version>
</dependency>
<dependency>
<groupId>com.googlecode.lanterna</groupId>
<artifactId>lanterna</artifactId>
<version>3.0.0-rc1</version>
</dependency>
</dependencies>
</project>

View file

@ -2,6 +2,8 @@ package fr.acinq.eclair
import java.io.File
import fr.acinq.eclair.blockchain.spv.BitcoinjKit2
import fr.acinq.eclair.blockchain.wallet.BitcoinjWallet
import grizzled.slf4j.Logging
/**
@ -9,20 +11,27 @@ import grizzled.slf4j.Logging
*/
object Boot extends App with Logging {
case class CmdLineConfig(datadir: File = new File(System.getProperty("user.home"), ".eclair"))
case class CmdLineConfig(datadir: File = new File(System.getProperty("user.home"), ".eclair"), textui: Boolean = false)
val parser = new scopt.OptionParser[CmdLineConfig]("eclair") {
head("eclair", s"${getClass.getPackage.getImplementationVersion} (commit: ${getClass.getPackage.getSpecificationVersion})")
help("help").abbr("h").text("display usage text")
opt[File]("datadir").optional().valueName("<file>").action((x, c) => c.copy(datadir = x)).text("optional data directory, default is ~/.eclair")
opt[Unit]("textui").optional().action((_, c) => c.copy(textui = true)).text("runs eclair with a text-based ui")
}
try {
parser.parse(args, CmdLineConfig()) match {
case Some(config) =>
LogSetup.logTo(config.datadir)
val kit = new BitcoinjKit2("test", config.datadir)
kit.startAsync()
import scala.concurrent.ExecutionContext.Implicits.global
new Setup(config.datadir).bootstrap onFailure {
val wallet = new BitcoinjWallet(kit.initialized.map(_ => kit.wallet()))
val setup = new Setup(config.datadir, wallet_opt = Some(wallet))
setup.bootstrap.collect {
case kit if (config.textui) => new Textui(kit)
} onFailure {
case t: Throwable => onError(t)
}
case None => System.exit(0)

View file

@ -0,0 +1,141 @@
package fr.acinq.eclair
import java.net.InetSocketAddress
import java.util.concurrent.atomic.AtomicBoolean
import akka.actor.{ActorRef, Props, SupervisorStrategy}
import com.googlecode.lanterna.gui2.dialogs.TextInputDialogBuilder
import com.googlecode.lanterna.input.KeyStroke
import com.googlecode.lanterna.{TerminalPosition, TerminalSize}
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{BinaryData, MilliBtc, MilliSatoshi, Satoshi}
import fr.acinq.eclair.channel.State
import fr.acinq.eclair.io.Switchboard.{NewChannel, NewConnection}
import fr.acinq.eclair.payment.{PaymentRequest, SendPayment}
import grizzled.slf4j.Logging
import scala.collection.JavaConversions._
/**
* Created by PM on 05/06/2017.
*/
class Textui(kit: Kit) extends Logging {
import com.googlecode.lanterna.TextColor
import com.googlecode.lanterna.gui2._
import com.googlecode.lanterna.screen.TerminalScreen
import com.googlecode.lanterna.terminal.DefaultTerminalFactory
// Setup terminal and screen layers// Setup terminal and screen layers
val terminal = new DefaultTerminalFactory().createTerminal
val screen = new TerminalScreen(terminal)
screen.startScreen()
// Create panel to hold components
val mainPanel = new Panel()
mainPanel.setLayoutManager(new BorderLayout())
val channelsPanel = new Panel()
channelsPanel.setLayoutManager(new LinearLayout(Direction.VERTICAL))
channelsPanel.setLayoutData(BorderLayout.Location.TOP)
mainPanel.addComponent(channelsPanel)
channelsPanel.addComponent(new Label("channels"))
val channels = collection.mutable.Map[ActorRef, Panel]()
def addChannel(channel: ActorRef, channelId: BinaryData, remoteNodeId: PublicKey, state: State, balance: Satoshi, capacity: Satoshi): Unit = {
val channelPanel = new Panel()
channelPanel.setLayoutManager(new LinearLayout(Direction.HORIZONTAL))
val channelDataPanel = new Panel()
channelDataPanel.setLayoutManager(new GridLayout(2))
channelDataPanel.addComponent(new Label(s"$channelId"))
channelDataPanel.addComponent(new Label(s"${state.toString}"))
channelDataPanel.addComponent(new Label(s"$remoteNodeId"))
channelDataPanel.addComponent(new EmptySpace(new TerminalSize(0, 0))) // Empty space underneath labels
channelDataPanel.addComponent(new Separator(Direction.HORIZONTAL)) // Empty space underneath labels
channelPanel.addComponent(channelDataPanel)
val pb = new ProgressBar(0, 100)
pb.setLabelFormat(s"$balance")
pb.setValue((balance.amount * 100 / capacity.amount).toInt)
pb.setPreferredWidth(100)
channelPanel.addComponent(pb)
channelsPanel.addComponent(channelPanel)
channels.put(channel, channelPanel)
}
def updateState(channel: ActorRef, state: State): Unit = {
val panel = channels(channel)
val channelDataPanel = panel.getChildren.iterator().next().asInstanceOf[Panel]
channelDataPanel.getChildren.toList(1).asInstanceOf[Label].setText(s"$state")
}
/*val shortcutsPanel = new Panel()
shortcutsPanel.setLayoutManager(new LinearLayout(Direction.HORIZONTAL))
shortcutsPanel.addComponent(new Label("(N)ew channel"))
shortcutsPanel.addComponent(new Separator(Direction.VERTICAL))
shortcutsPanel.setLayoutData(BorderLayout.Location.BOTTOM)
mainPanel.addComponent(shortcutsPanel)*/
//addChannel(randomBytes(32), randomKey.publicKey, NORMAL, Satoshi(Random.nextInt(1000)), Satoshi(1000))
//addChannel(randomBytes(32), randomKey.publicKey, NORMAL, Satoshi(Random.nextInt(1000)), Satoshi(1000))
//addChannel(randomBytes(32), randomKey.publicKey, NORMAL, Satoshi(Random.nextInt(1000)), Satoshi(1000))
//val theme = new SimpleTheme(TextColor.ANSI.DEFAULT, TextColor.ANSI.BLACK)
// Create window to hold the panel
val window = new BasicWindow
window.setComponent(mainPanel)
//window.setTheme(theme)
window.setHints(/*Window.Hint.FULL_SCREEN :: */ Window.Hint.NO_DECORATIONS :: Nil)
val textuiUpdater = kit.system.actorOf(SimpleSupervisor.props(Props(classOf[TextuiUpdater], this), "textui-updater", SupervisorStrategy.Resume))
// Create gui and start gui
val runnable = new Runnable {
override def run(): Unit = {
val gui = new MultiWindowTextGUI(screen, new DefaultWindowManager, new EmptySpace(TextColor.ANSI.BLUE))
window.addWindowListener(new WindowListener {
override def onMoved(window: Window, terminalPosition: TerminalPosition, terminalPosition1: TerminalPosition): Unit = {}
override def onResized(window: Window, terminalSize: TerminalSize, terminalSize1: TerminalSize): Unit = {}
override def onUnhandledInput(t: Window, keyStroke: KeyStroke, atomicBoolean: AtomicBoolean): Unit = {}
override def onInput(t: Window, keyStroke: KeyStroke, atomicBoolean: AtomicBoolean): Unit = {
if (keyStroke.getCharacter == 'n') {
val input = new TextInputDialogBuilder()
.setTitle("Open a new channel")
.setDescription("Node URI:")
//.setValidationPattern(Pattern.compile("[0-9]"), "You didn't enter a single number!")
.build()
.showDialog(gui)
val hostRegex = """([a-fA-F0-9]{66})@([a-zA-Z0-9:\.\-_]+):([0-9]+)""".r
try {
val hostRegex(nodeId, host, port) = input
kit.switchboard ! NewConnection(PublicKey(BinaryData(nodeId)), new InetSocketAddress(host, port.toInt), Some(NewChannel(MilliBtc(30), MilliSatoshi(0), None)))
} catch {
case t: Throwable => logger.error("", t)
}
} else if (keyStroke.getCharacter == 's') {
val input = new TextInputDialogBuilder()
.setTitle("Send a payment")
.setDescription("Payment request:")
//.setValidationPattern(Pattern.compile("[0-9]"), "You didn't enter a single number!")
.build()
.showDialog(gui)
try {
val paymentRequest = PaymentRequest.read(input)
kit.paymentInitiator ! SendPayment(paymentRequest.amount.getOrElse(MilliSatoshi(1000000)).amount, paymentRequest.paymentHash, paymentRequest.nodeId)
} catch {
case t: Throwable => logger.error("", t)
}
}
}
})
gui.addWindowAndWait(window)
kit.system.shutdown()
}
}
new Thread(runnable).start()
}

View file

@ -0,0 +1,27 @@
package fr.acinq.eclair
import akka.actor.Actor
import fr.acinq.bitcoin.Satoshi
import fr.acinq.eclair.channel._
import fr.acinq.eclair.payment.PaymentEvent
import fr.acinq.eclair.router.NetworkEvent
/**
* Created by PM on 31/05/2017.
*/
class TextuiUpdater(textui: Textui) extends Actor {
context.system.eventStream.subscribe(self, classOf[ChannelEvent])
context.system.eventStream.subscribe(self, classOf[NetworkEvent])
context.system.eventStream.subscribe(self, classOf[PaymentEvent])
override def receive: Receive = {
case ChannelCreated(channel, _, remoteNodeId, _, temporaryChannelId) =>
textui.addChannel(channel, temporaryChannelId, remoteNodeId, WAIT_FOR_INIT_INTERNAL, Satoshi(0), Satoshi(1))
case ChannelRestored(channel, _, remoteNodeId, _, channelId, data) =>
textui.addChannel(channel, channelId, remoteNodeId, OFFLINE, Satoshi(33), Satoshi(100))
case ChannelStateChanged(channel, _, _, _, state, _) =>
textui.updateState(channel, state)
}
}

11
pom.xml
View file

@ -4,13 +4,12 @@
<groupId>fr.acinq.eclair</groupId>
<artifactId>eclair_2.11</artifactId>
<version>0.2-spv-SNAPSHOT</version>
<version>0.2-android-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>eclair-core</module>
<module>eclair-node</module>
<module>eclair-node-gui</module>
</modules>
<description>A scala implementation of the Lightning Network</description>
@ -42,13 +41,13 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
<scala.version>2.11.11</scala.version>
<scala.version.short>2.11</scala.version.short>
<akka.version>2.4.18</akka.version>
<akka.version>2.3.14</akka.version>
<bitcoinlib.version>0.9.13</bitcoinlib.version>
<bitcoinj.version>0.15-ACINQ-rc6</bitcoinj.version>
<bitcoinj.version>0.15-rc1</bitcoinj.version>
</properties>
<build>