Compare commits

..

38 Commits

Author SHA1 Message Date
pm47
68b494d283 Merge branch 'master' into wip-android 2017-11-23 11:31:46 +01:00
dpad85
e400fb5eb4 set version to 0.2-android-alpha8 2017-11-21 19:26:41 +01:00
Pierre-Marie Padiou
ca46c0e9cf
[Android] Support for electrumx API (#214)
* back to 0.2-SNAPHOT (#166)

use scala plugin 3.3.1 (mvn scala:console now works)
add Dominique to the list of developpers

* Process `update_fail_malformed_htlc` properly (#168)

* add a test that fails and shows that we don't process `update_fail_malformed` properly
* remove HTLCs failed with update_fail_malformed
* fixes #167

* Re-send htlc/sigs after `funding_locked` (#169)

We previously skipped the `handleSync` function when we had to re-send
`funding_locked` messages on reconnection. This didn't take into account
the fact that we might have been disconnected right after sending the
very first `commit_sig` in the channel. In that case we need to first
re-send `funding_locked`, then re-send whatever updates were included in
the lost signature, and finally re-send the same `commit_sig`.

Note that the specification doesn't require to re-send the exact same
updates and signatures on reconnection. But doing this allows for a single
commitment history and allows us not to keep track of all signatures
sent to the other party.

Closes #165

* Handle `update_fail_malformed_htlc` in payment FSM (#170)

* handling `update_fail_malformed` messages in payment fsm

* added check on failure code for malformed htlc errors

Spec says that the `update_fail_malformed_htlc`.`failure_code`
must have the BADONION bit set.

* removed hard-coded actor names in fuzzy tests

* Added an integration test on revoked tx handling (#172)

The scenario was already tested at a lower level, but this is
more realistic, with a real bitcoin core.

Note that we currently only steal the counterparty's *main output*,
we ignore pending htlcs. From an incentive point-of-view, it is an
acceptable tradeoff because the amount of in-flight htlcs should
typically be far less than the main outputs (and can be configured
with `max-htlc-value-in-flight-msat`).

* Added ACK-based TCP write back-pressure (#174)

Current implementation was simplistic, which resulted in writes
being rejected when OS buffer was full. This happened especially
right after connection, when dumping a large routing table.

It is not clear whether we need read throttling too.

* Added check amount>dust on `ClaimDelayedOutputTx` (#177)

The current trimming logic [1] as defined in the spec only takes
care of 2nd level txes, making sure that their outputs are greater
than the configured dust limit. However, it is very possible that
depending on the current `feeRatePerKw`, 3rd level transactions
(such as those claiming delayed outputs) are below the dust limit.

Current implementation wasn't taking care of that, and was happily
generating transactions with negative amounts, as reported in #164.

This fixes #164 by rejecting transactions with an amount below the
dust limit, similarly to what happens when the parent output is
trimmed for 2nd level txes.

[1] https://github.com/lightningnetwork/lightning-rfc/blob/master/03-transactions.md#trimmed-outputs

* Reworked handling of `shutdown` messages (#176)

Current version attempted to do everything at once, and would always
leave the NORMAL state after processing the `shutdown` message. In
addition to being overly complicated, it had bugs because it is just
not always possible to do so: for example when we have unsigned outgoing
`update_add_htlc` and are already in the process of signing older changes,
the only thing we can do is wait for the counterparty's `revoke_and_ack`
and sign again right away when we receive it. Only after that can we
send our `shutdown` message, and we can't accept new `update_add_htlc`
in the meantime.

Another issue with the current implementation was that we considered
unsigned outgoing *changes*, when only unsigned outgoing `update_add_htlc`
are relevant in the context of `shutdown` logic.

We now also fail to route htlcs in SHUTDOWN state, as recommended by BOLT 2.

This fixes #173.

* Base checks in `sendAdd` on the *last* sent sig (#180)

fixes #175

* Store state when a sig is received in SHUTDOWN (#181)

This fixes #173 (for good this time)

* Support multiple hops in RoutingInfoTag (#185)

* Support multiple hops in RoutingInfoTag

* Change `HiddenHop` to `ExtraHop`, `channelId: BinaryData` to `shortChannelId: Long`

* payment request: encode expiry as a varlen unsigned long (#188)

* payment request expiry encoding: add Anton's test
it shows that we don't encode/decode values which would take up more than 2 5-bits value

* payment request: encode expiry as a varlen unsigned value
fixes #186

* Have channels subscribe to blockchain events at creation (#195)

Instead of only subscribing to those events when we reach certain states,
we now always subscribe to them at startup. It doesn't cost a lot because
we receive an event only when a new block appears, and is much simpler.

Note that we previously didn't even bother unsubscribing when we weren't
interested anymore in the events, and were ignoring the messages in the
`whenUnhandled` block. So it is more consistent to have the same behavior
during the whole lifetime of the channel.

This fixes #187.

* Add primary key to channel_updates table (#199)

This table was missing a primary key, which caused it to grow
indefinitely.

Also added duplication tests to other tables.

* Always store channel state when a rev is received (#201)

The way the store data currently doesn't allow for easy testing of this.
It will be improved in a later iteration.

This fixes #200.

* Add an optional 'minimum htlc expiry' tag (#202)

* Support building payments with extra hops (#198)

* Support building of outgoing payments with optional extra hops from payment requests

* Add test for route calculation with extra hops

* Simplify pattern matching in `buildExtra`

* `buildPayment` now uses a reverse Seq[Hop] to create a Seq[ExtraHop]

Since `Router` currently stores `ChannelUpdate`'s for non-public channels, it is possible to use it not only to get a route from payer to payee but also to get a "reverse" assisted route from payee when creating a `PaymentRequest`.

In principle this could be used to even generate a full reverse path to a payer which does not have an access to routing table for some reason.

* Can create `PaymentRequest`s with `RoutingInfoTag`s

* Bugfix and update test with data from live payment testing

* Move ExtraHop to PaymentRequest.scala

* Use `min_final_cltv_expiry` included in payment request (if any) (#210)

* Lock utxos when creating a funding transaction with Bitcoin Core (#211)

This mitigates issues when opening channels in parallel and having unpublished transactions reuse utxos that are already used by other txs.

Note that utxo will stay locked if counterparty disconnects after the `funding_created` message is sent, and before a `funding_signed` is received. In that case we should release locked utxos by calling `lockunspent` RPC function.

* Readme: added help for options syntax (#212)

* (README) updated link to release readme
* (README) added a link to HOCON readme for options syntax

This closes #209

* Rework preimage handling (#183)

* properly handle new htlc requests when closing

When in NORMAL state and a `shutdown` message has already been
sent or received, then any subsequent `CMD_ADD_HTLC` fails and
the relayer is notified of the failure.

Same in SHUTDOWN state.

This fixes a possible race condition when a channel just switched
to SHUTDOWN, and the relayer keeps sending it new htlcs before
being notified of the state change.

* renamed Htlc->DirectedHtlc + cleanup

* storing origin of htlcs in the channel state

Currently this information is handled in the relayer, which is not
persisted. As a consequence, if eclair is shut down and there are
pending (signed) incoming htlcs, those will always expire (time out
and fail) even if the corresponding outgoing htlc is fulfilled, because
we lose the lookup table (the relayer's `bindings` map).

Storing the origin in the channel (as opposed to persisting the state
of the relayer) makes sense because we want to store the origin if and
only if an outgoing htlc was successfully sent and signed in a channel.

It is also probably more performant because we only need to do one disk
operation (which we have to do at signing anyway) instead of two
distinct operations.

* removed bindings from relayer

Instead, we rely on the origin stored in the actor state.

* preimages are now persisted and acknowledged

Upon reception of an `UpdateFulfillHtlc`, the relayer forwards it
immediately to the origin channel, *and* it stores the preimage in
a `PreimagesDb`.

When the origin channel has irrevocably committed the fulfill in a
`CommitSig`, it sends an `AckFulfillCmd` back to the relayer, which
will then remove the preimage from its database.

In addition to that, the relayer will re-send all pending fulfills
when it is notified that a channel reaches NORMAL, SHUTDOWN, or
CLOSING state. That way we make sure that the origin channel will
always get the fulfill eventually, even if it currently OFFLINE for
example. This fixes #146.

Also, the relayer now relies on the register to forward messages to
channels based on `channelId` or `shortChannelId`. This simplifies
the relayer but adds one hop when forwarding messages.

* modified `PaymentRelayed` event

Replaced `amountIn` and `feesEarned` by more explicit `amountIn`
and `amountOut`. `feesEarned` are simply the difference.

TODO:
- when local/remote closing a channel, we currently do not wait
for htlc-related transactions, we consider the channel CLOSED when
the commitment transactions has been buried deeply enough; this is
wrong because it wouldn't let us time to extract payment preimages
in certain cases

* Reworked channel closing logic (#204)

When doing an unilateral close (local or remote), we previously weren't
watching htlc outputs to decide whether the commit was finished or not.
This was incorrect because we didn't make sure the htlc-related
transactions had indeed been confirmed on the blockchain, making us
potentially lose money.

This is not trivial, because htlc transactions may be double-spent by the
counterparty, dependending on scenarios (ex: `htlc-timeout` vs
`claim-success`). On top of that, there may be several different kind of
commits in competition at the same time.

With this change, we now:
- put `WatchConfirm` watches on the commitment tx, and on all outputs only
  us control (eg: our main output) ;
- put `WatchSpent` watches on the outputs that may be double spent by the
  counterparty; when such an output is spent, we put a `WatchConfirm` on
  the corresponding transaction and keep track of all outpoints spent ;
- every time a new transaction is confirmed, we find out if there are some
  remaining transactions waiting for confirmation, taking into account the
  fact that some 2nd/3rd-stage txs may never confirm because their input
  has been doublespent.

We also don't rely anymore on dedicated `BITCOIN_CLOSE_DONE`,
`BITCOIN_LOCALCOMMIT_DONE`, ... events.

* Better handle big routing table (#194)

* increased tcp send buffer x100

* throttle announcement messages when dumping the table

* set router throttling to chunkSize=10 delay=50ms

* Handle remote `error` in `SYNCING` state (#205)

This closes #203.

* added support for electrumx wallet/watcher

This is a rework of #184 with numerous improvements and bugfixes.

* re-enabled `WatchSpentBasic`

* fixed several issues in watcher

* fixed pattern matching for INPUT_RECONNECTED event in CLOSING

* reduced logback_colors log level

* disabled WatchSpentBasic on android

* moved bitcoin init to the bootstrap future

This way the name resolution when loading electrum server addresses does
not occur on android's main thread.

* improved fees management

* main feerate source is now earn.com (21.co) instead of bitpay insight
* if main feerate source is unavailable, we now fallback to default values
* we retrieve feerates for a set of block delays instead of just one
* we now use different block delays depending on transactions:
  - `block_delay`=`1` for txes that compete with others (eg: commitment
    tx, htlc tx, penalty tx)
  - `block_delay`=`6` for other txes (eg: funding tx, closing tx, delayed
    output tx)

TODO:
- set sensible default values for feerates

* WalletTransactionReceive contains the tx fee as an option

* connect txes even if they arrive out of order

* use manual transitions because of limitation in akka 2.3 FSM

* Delay `announcement_signatures` when received early (#217)

* delay `announcement_signatures` in state `WAIT_FOR_FUNDING_LOCKED`
* delay `announcement_signatures` in state `WAIT_FOR_FUNDING_CONFIRMED`
* always re-send our `announcement_signatures` in response to theirs

* Use a separate `htlc_key` to sign 2nd stage htlc txs (#213)

We previously used the `payment_key` both for our main output, and to sign
the local `htlc_success`/`htlc_timeout` transactions.

With this change we can keep the `payment_privkey` offline, which is far
better from a security point of view because an attacker getting control
of a node wouldn't be able to just publish the current commitment
transaction and get the funds. The attacker would only be able to get our
`htlc_privkey`, which we only use in a 2-of-2 multisig with our
counterparty, so it is useless except if the attacker and the counterparty
are the same person, and even in that case only the pending htlcs would be
at risk.

Note that this implementation is a first step and actually keeps the
payment key to spend our outputs in non-mutual close scenarios.

* wallet: send confidence event as soon as a tx is confirmed

* fixed 5985148f2fc727dadbe9ffece596ed61331435b6 and improve events

* added `NewWalletReceiveAddress` event

* cleaned up electrum testnet seeds

* added a test on dumping routing state

* routing table dump is now disabled in the router instead of peer

* removed WAIT_FOR_FUNDING_PUBLISHED state and clarified funding tx publish assumptions

* wallet: use BIP49 derivation and 24 words mnemonic codes
we use segwit with p2sh-of-p2wkh so we should use BIP49 derivation
instead of BIP44 (same path with m/49'/... instead of m/44'/...)

* added a rollback function to `EclairWallet`

This rollback is called whenever we know we won't publish the funding tx,
so that we tell the wallet to release locks on utxos.

* fundee now checks feerates at `open_channel` reception

* proper handling of electrum connection/disconnection

* moved bitcoinj test to its own package

* Improved fees management (#216)

* main feerate source is now earn.com (21.co) instead of bitpay insight
* if main feerate source is unavailable, we now fallback to default values
* we retrieve feerates for a set of block delays instead of just one
* we now use different block delays depending on transactions:
  - `block_delay`=`1` for txes that compete with others (eg: commitment
    tx, htlc tx, penalty tx)
  - `block_delay`=`6` for other txes (eg: funding tx, closing tx, delayed
    output tx)

* make electrum wallet advertise address at startup

* Prune stale network announcements (#219)

See https://github.com/lightningnetwork/lightning-rfc/blob/master/07-routing-gossip.md#recommendation-on-pruning-stale-entries.

* send a new `channel_update` every 24h as keepalive

* use case object instead of symbol for ticks

* minor improvements in router init

* prune stale channels

Note that we don't want to prune brand new channels for which we didn't
yet receive a channel update, so we consider stale a channel that:
(1) is older than 2 weeks (2*7*144 = 2016 blocks)
AND
(2) didn't have an update during the last 2 weeks.

Pruning is triggered every day.

Also renamed event `BITCOIN_FUNDING_OTHER_CHANNEL_SPENT` to
`BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT`.

* filter out duplicate announcements before checking sig

* changed routing table dump parameters

* set version to 0.2-android-SNAPSHOT
2017-11-21 18:25:27 +01:00
pm47
435c1e424c db hotfix, set version to 0.2-android-alpha7
Table `channel_updates` is missing a primary key and keeps growing
everytime eclair wallet is restarted. As a temporary quick fix, we just
delete duplicates on startup.
2017-10-24 18:52:51 +02:00
pm47
4786066bb7 set version to 0.2-android-alpha6 2017-10-24 14:36:39 +02:00
Pierre-Marie Padiou
8a05ae2b63 Improved performance of Android router (#192)
* mitigate race condition at router startup

When there are a lot of network announcements to load, channels
could start before the router had time to subscribe to
`ChannelStateChanged` and missed events. This resulted in local
channels not being detected by the router, which in turn led to 
`RouteNotFound` errors.

* optimized router for android

When initializing the router, the stored announcements are directly
loaded in memory and don't go to the 'normal' process involving signature
verification, events and all.

We also do not broadcast announcements anymore.
2017-10-24 14:30:53 +02:00
sstone
c3a1858e13 set version to 0.2-android-SNAPSHOT
and upgrade to mave scala plugin 3.3.1
2017-09-25 14:03:51 +02:00
pm47
778e4e6cc9 set version to 0.2-android-alpha5 2017-09-14 19:04:07 +02:00
pm47
2dcf8edf0b added public accessor for ChannelException.channelId 2017-09-13 18:55:16 +02:00
pm47
3844ba2cf7 Merge branch 'master' into wip-android 2017-09-13 18:43:43 +02:00
pm47
d74f943a2a Merge branch 'master' into wip-android 2017-09-12 15:08:07 +02:00
pm47
0510401ce2 merged from master 2017-09-08 18:17:18 +02:00
pm47
dd91a6a5f7 merged from master 2017-09-08 15:10:33 +02:00
pm47
7511fbfc24 merged from master 2017-09-07 00:22:17 +02:00
pm47
183addb1c8 using bitcoinj 0.15-rc2 2017-09-07 00:04:36 +02:00
pm47
9fbf669ad5 allow setting static peers for bitcoinj in configuration 2017-09-06 23:59:22 +02:00
pm47
e6747f2a3f merged from wip-spv 2017-09-06 20:05:26 +02:00
pm47
82959934ad merged from master 2017-09-06 19:16:10 +02:00
pm47
e8382f7c8c Merge branch 'wip-spv' into wip-android 2017-09-06 16:54:40 +02:00
pm47
fe3fd47d82 removed deprecated YesRouter 2017-09-06 16:54:20 +02:00
pm47
13afa1ff95 merged from wip-spv 2017-09-06 16:51:55 +02:00
pm47
d637a4cb25 merged from master 2017-09-06 16:05:11 +02:00
pm47
be2cdf44c5 merged from master, added tests for spv mode 2017-09-06 14:44:14 +02:00
dpad85
476d07ee60 Added a way to check DB compatibility 2017-08-25 17:42:03 +02:00
pm47
50729674e4 make eclair run on android 2017-08-25 12:24:54 +02:00
pm47
4d1ea8e402 minor typo 2017-08-24 18:44:17 +02:00
pm47
b32eb2cf5c re-enabled travis checks, updated to scala 2.11.11 2017-08-24 18:34:58 +02:00
pm47
5a18cedd3e now using bitcoinj 0.15-ACINQ-rc6, back to SNAPSHOT 2017-08-24 17:05:02 +02:00
pm47
368ee7b9a8 merged from master 2017-08-24 15:49:54 +02:00
pm47
9f060a5d27 Bugfix: regression in init
Eclair wasn't stopping anymore when two instances were started with the
same ports.

Note: we should probably go one step further and put a lock in the datadir
directory.

Set version to 0.2-spv-d
2017-08-21 18:29:34 +02:00
pm47
b4b1d75c53 using logback's MDC to display channelId in logs
and set version to 0.2-spv-c
2017-08-21 16:31:38 +02:00
pm47
eded5170aa setting version to 0.2-spv-b 2017-07-26 19:02:42 +02:00
pm47
ca9ecb946e Merge branch 'master' into wip-spv 2017-07-26 19:01:13 +02:00
pm47
fe306194d6 fees are now retrieved every 10 minutes instead of just one time 2017-07-25 16:29:45 +02:00
pm47
c2866dd55d reworked payment lifecycle
* fixed retry logic (infinite loop in some cases)
* check update signature
* keep track of the list of errors and routes tried
2017-07-24 19:13:09 +02:00
pm47
04d08cfc01 adds a way to tell when we know the current height
This is because we need to use correct block height when sending
htcs, otherwise counterparties will unilaterally close the channel
2017-07-24 16:19:57 +02:00
pm47
bcf43b8a76 disable travis (until custom bitcoinj lib is published) 2017-07-24 14:21:40 +02:00
pm47
8245f61f1f added spv support to eclair with bitcoinj 2017-07-24 13:54:45 +02:00
78 changed files with 431 additions and 3761 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-SNAPSHOT</version>
<version>0.2-android-SNAPSHOT</version>
</parent>
<artifactId>eclair-core_2.11</artifactId>
@ -121,10 +121,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>
@ -132,11 +133,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>
@ -180,12 +176,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

@ -13,7 +13,7 @@ eclair {
port = 8080
}
watcher-type = "bitcoind" // other *experimental* values include "bitcoinj" or "electrum"
watcher-type = "electrum" // other *experimental* values include "bitcoinj" or "electrum"
bitcoind {
host = "localhost"
@ -47,7 +47,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
@ -81,4 +81,21 @@ eclair {
auto-reconnect = true
payment-handler = "local"
}
akka {
loggers = ["akka.event.slf4j.Slf4jLogger"]
loglevel = "DEBUG"
actor {
debug {
# enable DEBUG logging of all LoggingFSMs for events, transitions and timers
fsm = on
}
}
http {
host-connection-pool {
max-open-requests = 64
}
}
}

View File

@ -1,6 +1,5 @@
package fr.acinq.eclair
import java.util.BitSet
import fr.acinq.bitcoin.BinaryData

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,10 +2,10 @@ package fr.acinq.eclair
import java.io.File
import java.net.InetSocketAddress
import java.nio.file.Files
import java.sql.DriverManager
import java.util.concurrent.TimeUnit
import com.google.common.io.Files
import com.typesafe.config.{Config, ConfigFactory}
import fr.acinq.bitcoin.Crypto.PrivateKey
import fr.acinq.bitcoin.DeterministicWallet.ExtendedPrivateKey
@ -83,10 +83,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)
@ -138,15 +138,15 @@ object NodeParams {
peersDb = peersDb,
networkDb = networkDb,
preimagesDb = preimagesDb,
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"),
chainHash = chainHash,
channelFlags = config.getInt("channel-flags").toByte,
channelExcludeDuration = FiniteDuration(config.getDuration("channel-exclude-duration").getSeconds, TimeUnit.SECONDS),
channelExcludeDuration = FiniteDuration(config.getDuration("channel-exclude-duration", TimeUnit.SECONDS), TimeUnit.SECONDS),
watcherType = watcherType)
}
}

View File

@ -4,35 +4,29 @@ 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.NodeParams.{BITCOIND, BITCOINJ, ELECTRUM}
import fr.acinq.eclair.api.{GetInfoResponse, Service}
import fr.acinq.eclair.blockchain.bitcoind.rpc.{BitcoinJsonRPCClient, ExtendedBitcoinClient}
import fr.acinq.eclair.blockchain.bitcoind.zmq.ZMQActor
import fr.acinq.eclair.blockchain.bitcoind.{BitcoinCoreWallet, ZmqWatcher}
import fr.acinq.bitcoin.Block
import fr.acinq.eclair.NodeParams.{BITCOINJ, ELECTRUM}
import fr.acinq.eclair.blockchain.bitcoinj.{BitcoinjKit, BitcoinjWallet, BitcoinjWatcher}
import fr.acinq.eclair.blockchain.electrum.{ElectrumClient, ElectrumEclairWallet, ElectrumWallet, ElectrumWatcher}
import fr.acinq.eclair.blockchain.fee.{ConstantFeeProvider, _}
import fr.acinq.eclair.blockchain.{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.collection.JavaConversions._
import scala.concurrent.duration._
import scala.concurrent.{Await, ExecutionContext, Future, Promise}
import scala.concurrent.{Await, 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}")
@ -41,65 +35,37 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act
val nodeParams = NodeParams.makeNodeParams(datadir, config)
val chain = config.getString("chain")
// early checks
DBCompatChecker.checkDBCompatibility(nodeParams)
PortChecker.checkAvailable(config.getString("server.binding-ip"), config.getInt("server.port"))
logger.info(s"nodeid=${nodeParams.privateKey.publicKey.toBin} alias=${nodeParams.alias}")
logger.info(s"using chain=$chain chainHash=${nodeParams.chainHash}")
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
val bitcoin = nodeParams.watcherType match {
case BITCOIND =>
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").recover { case _ => throw BitcoinRPCConnectionException }
progress = (json \ "verificationprogress").extract[Double]
chainHash <- bitcoinClient.rpcClient.invoke("getblockhash", 0).map(_.extract[String]).map(BinaryData(_)).map(x => BinaryData(x.reverse))
bitcoinVersion <- bitcoinClient.rpcClient.invoke("getnetworkinfo").map(json => (json \ "version")).map(_.extract[String])
} yield (progress, chainHash, bitcoinVersion)
// blocking sanity checks
val (progress, chainHash, bitcoinVersion) = Await.result(future, 10 seconds)
assert(chainHash == nodeParams.chainHash, s"chainHash mismatch (conf=${nodeParams.chainHash} != bitcoind=$chainHash)")
assert(progress > 0.99, "bitcoind should be synchronized")
// TODO: add a check on bitcoin version?
Bitcoind(bitcoinClient)
case BITCOINJ =>
logger.warn("EXPERIMENTAL BITCOINJ MODE ENABLED!!!")
val staticPeers = config.getConfigList("bitcoinj.static-peers").map(c => new InetSocketAddress(c.getString("host"), c.getInt("port"))).toList
logger.info(s"using staticPeers=$staticPeers")
val bitcoinjKit = new BitcoinjKit(chain, datadir, staticPeers)
bitcoinjKit.startAsync()
Await.ready(bitcoinjKit.initialized, 10 seconds)
Bitcoinj(bitcoinjKit)
case ELECTRUM =>
logger.warn("EXPERIMENTAL ELECTRUM MODE ENABLED!!!")
val addressesFile = chain match {
case "test" => "/electrum/servers_testnet.json"
case "regtest" => "/electrum/servers_regtest.json"
}
val stream = classOf[Setup].getResourceAsStream(addressesFile)
val addresses = ElectrumClient.readServerAddresses(stream)
val electrumClient = system.actorOf(SimpleSupervisor.props(Props(new ElectrumClient(addresses)), "electrum-client", SupervisorStrategy.Resume))
Electrum(electrumClient)
}
def bootstrap: Future[Kit] = Future {
def bootstrap: Future[Kit] = {
val zmqConnected = Promise[Boolean]()
val tcpBound = Promise[Unit]()
val bitcoin = nodeParams.watcherType match {
case BITCOINJ =>
logger.warn("EXPERIMENTAL BITCOINJ MODE ENABLED!!!")
val staticPeers = config.getConfigList("bitcoinj.static-peers").map(c => new InetSocketAddress(c.getString("host"), c.getInt("port"))).toList
logger.info(s"using staticPeers=$staticPeers")
val bitcoinjKit = new BitcoinjKit(chain, datadir, staticPeers)
bitcoinjKit.startAsync()
Await.ready(bitcoinjKit.initialized, 10 seconds)
Bitcoinj(bitcoinjKit)
case ELECTRUM =>
logger.warn("EXPERIMENTAL ELECTRUM MODE ENABLED!!!")
val addressesFile = chain match {
case "test" => "/electrum/servers_testnet.json"
case "regtest" => "/electrum/servers_regtest.json"
}
val stream = classOf[Setup].getResourceAsStream(addressesFile)
val addresses = ElectrumClient.readServerAddresses(stream)
val electrumClient = system.actorOf(SimpleSupervisor.props(Props(new ElectrumClient(addresses)), "electrum-client", SupervisorStrategy.Resume))
Electrum(electrumClient)
case _ => ???
}
val defaultFeerates = FeeratesPerByte(block_1 = config.getLong("default-feerates.delay-blocks.1"), blocks_2 = config.getLong("default-feerates.delay-blocks.2"), blocks_6 = config.getLong("default-feerates.delay-blocks.6"), blocks_12 = config.getLong("default-feerates.delay-blocks.12"), blocks_36 = config.getLong("default-feerates.delay-blocks.36"), blocks_72 = config.getLong("default-feerates.delay-blocks.72"))
Globals.feeratesPerByte.set(defaultFeerates)
@ -107,7 +73,6 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act
logger.info(s"initial feeratesPerByte=${Globals.feeratesPerByte.get()}")
val feeProvider = (chain, bitcoin) match {
case ("regtest", _) => new ConstantFeeProvider(defaultFeerates)
case (_, Bitcoind(client)) => new FallbackFeeProvider(new EarnDotComFeeProvider() :: new BitcoinCoreFeeProvider(client.rpcClient, defaultFeerates) :: new ConstantFeeProvider(defaultFeerates) :: Nil) // order matters!
case _ => new FallbackFeeProvider(new EarnDotComFeeProvider() :: new ConstantFeeProvider(defaultFeerates) :: Nil) // order matters!
}
system.scheduler.schedule(0 seconds, 10 minutes)(feeProvider.getFeerates.map {
@ -119,25 +84,23 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act
})
val watcher = bitcoin match {
case Bitcoind(bitcoinClient) =>
system.actorOf(SimpleSupervisor.props(Props(new ZMQActor(config.getString("bitcoind.zmq"), Some(zmqConnected))), "zmq", SupervisorStrategy.Restart))
system.actorOf(SimpleSupervisor.props(ZmqWatcher.props(bitcoinClient), "watcher", SupervisorStrategy.Resume))
case Bitcoinj(bitcoinj) =>
zmqConnected.success(true)
system.actorOf(SimpleSupervisor.props(BitcoinjWatcher.props(bitcoinj), "watcher", SupervisorStrategy.Resume))
case Electrum(electrumClient) =>
zmqConnected.success(true)
system.actorOf(SimpleSupervisor.props(Props(new ElectrumWatcher(electrumClient)), "watcher", SupervisorStrategy.Resume))
case _ => ???
}
val wallet = bitcoin match {
case Bitcoind(bitcoinClient) => new BitcoinCoreWallet(bitcoinClient.rpcClient, watcher)
case _ if wallet_opt.isDefined => wallet_opt.get
case Bitcoinj(bitcoinj) => new BitcoinjWallet(bitcoinj.initialized.map(_ => bitcoinj.wallet()))
case Electrum(electrumClient) =>
val electrumSeedPath = new File(datadir, "electrum_seed.dat")
val electrumWallet = system.actorOf(ElectrumWallet.props(electrumSeedPath, electrumClient, ElectrumWallet.WalletParameters(Block.RegtestGenesisBlock.hash, allowSpendUnconfirmed = true)), "electrum-wallet")
new ElectrumEclairWallet(electrumWallet)
case _ => ???
}
wallet.getFinalAddress.map {
case address => logger.info(s"initial wallet address=$address")
}
@ -151,7 +114,6 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act
val router = 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,
@ -163,40 +125,18 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act
router = router,
switchboard = switchboard,
paymentInitiator = paymentInitiator,
server = server,
wallet = wallet)
val api = new Service {
override def getInfoResponse: Future[GetInfoResponse] = Future.successful(GetInfoResponse(nodeId = nodeParams.privateKey.publicKey, alias = nodeParams.alias, port = config.getInt("server.port"), chainHash = nodeParams.chainHash, blockHeight = Globals.blockCount.intValue()))
override def appKit = kit
}
val httpBound = Http().bindAndHandle(api.route, config.getString("api.binding-ip"), config.getInt("api.port")).recover {
case _: BindFailedException => throw TCPBindException(config.getInt("api.port"))
}
val zmqTimeout = after(5 seconds, using = system.scheduler)(Future.failed(BitcoinZMQConnectionTimeoutException))
val tcpTimeout = after(5 seconds, using = system.scheduler)(Future.failed(TCPBindException(config.getInt("server.port"))))
val httpTimeout = after(5 seconds, using = system.scheduler)(Future.failed(TCPBindException(config.getInt("api.port"))))
for {
_ <- Future.firstCompletedOf(zmqConnected.future :: zmqTimeout :: Nil)
_ <- Future.firstCompletedOf(tcpBound.future :: tcpTimeout :: Nil)
_ <- Future.firstCompletedOf(httpBound :: httpTimeout :: Nil)
} yield kit
kit
}
}
// @formatter:off
sealed trait Bitcoin
case class Bitcoind(extendedBitcoinClient: ExtendedBitcoinClient) extends Bitcoin
case class Bitcoinj(bitcoinjKit: BitcoinjKit) extends Bitcoin
case class Electrum(electrumClient: ActorRef) extends Bitcoin
// @formatter:on
case class Kit(nodeParams: NodeParams,
system: ActorSystem,
@ -207,9 +147,6 @@ case class Kit(nodeParams: NodeParams,
router: ActorRef,
switchboard: ActorRef,
paymentInitiator: ActorRef,
server: ActorRef,
wallet: EclairWallet)
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")

View File

@ -1,76 +0,0 @@
package fr.acinq.eclair.api
import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, PublicKey, Scalar}
import fr.acinq.bitcoin.{BinaryData, Transaction}
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,148 +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(nodeId) :: JString(host) :: JInt(port) :: 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) :: rest) =>
for {
req <- Future(PaymentRequest.read(paymentRequest))
amount = (req.amount, rest) match {
case (Some(_), JInt(amt) :: Nil) => amt.toLong // overriding payment request amount with the one provided
case (Some(amt), _) => amt.amount
case (None, JInt(amt) :: Nil) => amt.toLong // amount wasn't specified in request, using custom one
case (None, _) => throw new RuntimeException("you need to manually specify an amount for this payment request")
}
sendPayment = req.minFinalCltvExpiry match {
case None => SendPayment(amount, req.paymentHash, req.nodeId)
case Some(value) => SendPayment(amount, req.paymentHash, req.nodeId, value)
}
res <- (paymentInitiator ? sendPayment).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 (nodeId, host, port): 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",
"send (paymentRequest, amountMsat): send a payment to a lightning node using a BOLT11 payment request and a custom amount",
"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

@ -3,20 +3,13 @@ package fr.acinq.eclair.blockchain.bitcoind.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, OverflowStrategy, QueueOfferResult}
import akka.stream.scaladsl.{Keep, Sink, Source}
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, Promise}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Failure, Success}
// @formatter:off
case class JsonRPCRequest(jsonrpc: String = "1.0", id: String = "scala-client", method: String, params: Seq[Any])
@ -25,57 +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) {
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)
val scheme = if (ssl) "https" else "http"
val uri = Uri(s"$scheme://$host:$port")
implicit val serialization = jackson.Serialization
implicit val formats = DefaultFormats
implicit val materializer = ActorMaterializer()
val httpClientFlow = Http().cachedHostConnectionPool[Promise[HttpResponse]](host, port)
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)
}
val queueSize = 512
val queue = Source.queue[(HttpRequest, Promise[HttpResponse])](queueSize, OverflowStrategy.dropNew)
.via(httpClientFlow)
.toMat(Sink.foreach({
case ((Success(resp), p)) => p.success(resp)
case ((Failure(e), p)) => p.failure(e)
}))(Keep.left)
.run()
def queueRequest(request: HttpRequest): Future[HttpResponse] = {
val responsePromise = Promise[HttpResponse]()
queue.offer(request -> responsePromise).flatMap {
case QueueOfferResult.Enqueued => responsePromise.future
case QueueOfferResult.Dropped => Future.failed(new RuntimeException("Queue overflowed. Try again later."))
case QueueOfferResult.Failure(ex) => Future.failed(ex)
case QueueOfferResult.QueueClosed => Future.failed(new RuntimeException("Queue was closed (pool shut down) while running the request. Try again later."))
}
override def onThrowable(t: Throwable): Unit = promise.failure(t)
})
promise.future
}
def invoke(method: String, params: Any*)(implicit ec: ExecutionContext): Future[JValue] =
for {
entity <- Marshal(JsonRPCRequest(method = method, params = params)).to[RequestEntity]
httpRes <- queueRequest(HttpRequest(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(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 <- queueRequest(HttpRequest(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)
def invoke(request: Seq[(String, Seq[Any])])(implicit ec: ExecutionContext): Future[Seq[JValue]] = ???
}

View File

@ -0,0 +1,39 @@
package fr.acinq.eclair.blockchain.bitcoinj
import java.io.File
import fr.acinq.eclair.blockchain.bitcoinj.BitcoinjKit._
import grizzled.slf4j.Logging
import org.bitcoinj.core.listeners.PeerConnectedEventListener
import org.bitcoinj.core.{Peer, VersionMessage}
import org.bitcoinj.kits.WalletAppKit
import scala.concurrent.Promise
/**
* 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 = {
peerGroup().setMinRequiredProtocolVersion(70015) // bitcoin core 0.13
peerGroup().addConnectedEventListener(new PeerConnectedEventListener {
override def onPeerConnected(peer: Peer, peerCount: Int): Unit = {
if ((peer.getPeerVersionMessage.localServices & VersionMessage.NODE_WITNESS) == 0) {
peer.close()
}
}
})
initializedPromise.success(true)
}
}

View File

@ -126,6 +126,7 @@ class ElectrumWallet(mnemonics: Seq[String], client: ActorRef, params: ElectrumW
status = data.status + (scriptHash -> status),
pendingHistoryRequests = data.pendingHistoryRequests + scriptHash)
manualTransition(data1)
goto(stateName) using data1 // goto instead of stay because we want to fire transitions
case Event(ElectrumClient.GetScriptHashHistoryResponse(scriptHash, history), data) =>
@ -158,6 +159,7 @@ class ElectrumWallet(mnemonics: Seq[String], client: ActorRef, params: ElectrumW
}
}
val data1 = data.copy(heights = heights1, history = data.history + (scriptHash -> history), pendingHistoryRequests = data.pendingHistoryRequests - scriptHash, pendingTransactionRequests = pendingTransactionRequests1)
manualTransition(data1)
goto(stateName) using data1 // goto instead of stay because we want to fire transitions
case Event(GetTransactionResponse(tx), data) =>
@ -169,6 +171,7 @@ class ElectrumWallet(mnemonics: Seq[String], client: ActorRef, params: ElectrumW
// when we have successfully processed a new tx, we retry all pending txes to see if they can be added now
data.pendingTransactions.foreach(self ! GetTransactionResponse(_))
val data1 = data.copy(transactions = data.transactions + (tx.txid -> tx), pendingTransactionRequests = data.pendingTransactionRequests - tx.txid, pendingTransactions = Nil)
manualTransition(data1)
goto(stateName) using data1 // goto instead of stay because we want to fire transitions
case None =>
// missing parents
@ -192,6 +195,7 @@ class ElectrumWallet(mnemonics: Seq[String], client: ActorRef, params: ElectrumW
val (received, sent, Some(fee)) = data.computeTransactionDelta(tx).get
// we notify here because the tx won't be downloaded again (it has been added to the state at commit)
context.system.eventStream.publish(TransactionReceived(tx, data1.computeTransactionDepth(tx.txid), received, sent, Some(fee)))
manualTransition(data1)
goto(stateName) using data1 replying CommitTransactionResponse(tx) // goto instead of stay because we want to fire transitions
case Event(CancelTransaction(tx), data) =>
@ -222,12 +226,16 @@ class ElectrumWallet(mnemonics: Seq[String], client: ActorRef, params: ElectrumW
case Event(ElectrumClient.BroadcastTransaction(tx), _) => stay replying ElectrumClient.BroadcastTransactionResponse(tx, Some(Error(-1, "wallet is not connected")))
}
onTransition {
case _ -> _ if nextStateData.isReady(params.swipeRange) =>
/**
* Bug in akka 2.3 onTransition won't fire on same-state transitions
*/
def manualTransition(nextStateData: Data) = {
if (nextStateData.isReady(params.swipeRange)) {
val ready = nextStateData.readyMessage
log.info(s"wallet is ready with $ready")
context.system.eventStream.publish(ready)
context.system.eventStream.publish(NewWalletReceiveAddress(nextStateData.currentReceiveAddress))
}
}
initialize()

View File

@ -1,13 +1,8 @@
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 de.heikoseeberger.akkahttpjson4s.Json4sSupport._
import fr.acinq.eclair.HttpHelper.get
import org.json4s.JsonAST.{JArray, JInt, JValue}
import org.json4s.{DefaultFormats, jackson}
import scala.concurrent.{ExecutionContext, Future}
@ -18,15 +13,9 @@ class EarnDotComFeeProvider(implicit system: ActorSystem, ec: ExecutionContext)
import EarnDotComFeeProvider._
implicit val materializer = ActorMaterializer()
val httpClient = Http(system)
implicit val serialization = jackson.Serialization
implicit val formats = DefaultFormats
override def getFeerates: Future[FeeratesPerByte] =
for {
httpRes <- httpClient.singleRequest(HttpRequest(uri = Uri("https://bitcoinfees.earn.com/api/v1/fees/list"), method = HttpMethods.GET))
json <- Unmarshal(httpRes).to[JValue]
json <- get("https://bitcoinfees.earn.com/api/v1/fees/list")
feeRanges = parseFeeRanges(json)
} yield extractFeerates(feeRanges)
}

View File

@ -7,6 +7,7 @@ import fr.acinq.bitcoin.Crypto.{PublicKey, ripemd160, sha256}
import fr.acinq.bitcoin._
import fr.acinq.eclair.NodeParams.BITCOINJ
import fr.acinq.eclair._
import fr.acinq.eclair.blockchain.WatchConfirmed.extractPublicKeyScript
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.channel.Helpers.{Closing, Funding}
import fr.acinq.eclair.crypto.{Generators, ShaChain, Sphinx}

View File

@ -55,6 +55,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, _)) =>

View File

@ -1,7 +1,5 @@
package fr.acinq.eclair.router
import java.io.StringWriter
import akka.actor.{ActorRef, FSM, Props}
import akka.pattern.pipe
import fr.acinq.bitcoin.BinaryData
@ -14,9 +12,8 @@ import fr.acinq.eclair.io.Peer
import fr.acinq.eclair.payment.Hop
import fr.acinq.eclair.transactions.Scripts
import fr.acinq.eclair.wire._
import org.jgrapht.alg.shortestpath.DijkstraShortestPath
import org.jgrapht.ext._
import org.jgrapht.graph.{DefaultDirectedGraph, DefaultEdge, SimpleGraph}
import org.jgrapht.alg.DijkstraShortestPath
import org.jgrapht.graph.{DefaultDirectedGraph, DefaultEdge}
import scala.collection.JavaConversions._
import scala.compat.Platform
@ -72,20 +69,17 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSM[State, Data]
val db = nodeParams.networkDb
// Note: We go through the whole validation process instead of directly loading into memory, because the channels
// could have been closed while we were shutdown, and if someone connects to us right after startup we don't want to
// advertise invalid channels. We could optimize this (at least not fetch txes from the blockchain, and not check sigs)
log.info(s"loading network announcements from db...")
db.listChannels().map(self ! _)
db.listNodes().map(self ! _)
db.listChannelUpdates().map(self ! _)
if (db.listChannels().size > 0) {
val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, Platform.currentTime / 1000)
self ! nodeAnn
{
log.info(s"loading network announcements from db...")
val initChannels = db.listChannels().map(c => (c.shortChannelId -> c)).toMap
val initNodes = (db.listNodes() match {
case Nil => Nil
case l => l :+ Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, Platform.currentTime / 1000)
}).map(n => (n.nodeId -> n)).toMap
val initChannelUpdates = db.listChannelUpdates().map(u => (getDesc(u, initChannels(u.shortChannelId)) -> u)).toMap
log.info(s"starting state machine")
startWith(NORMAL, Data(initNodes, initChannels, initChannelUpdates, Nil, Nil, Nil, Map.empty, Map.empty, Set.empty))
}
log.info(s"starting state machine")
startWith(NORMAL, Data(Map.empty, Map.empty, Map.empty, Nil, Nil, Nil, Map.empty, Map.empty, Set.empty))
when(NORMAL) {
case Event(TickValidate, d) =>
@ -120,7 +114,9 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSM[State, Data]
log.error(s"invalid script for shortChannelId=${c.shortChannelId} txid=${tx.txid} ann=$c")
None
} else {
watcher ! WatchSpentBasic(self, tx, outputIndex, BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT(c.shortChannelId))
// On Android we disable the ability to detect when external channels die. If we try to use them during a
// payment, we simply will get an error from the node that is just before the missing channel.
//watcher ! WatchSpentBasic(self, tx, outputIndex, BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT(c.shortChannelId))
// TODO: check feature bit set
log.debug(s"added channel channelId=${c.shortChannelId}")
context.system.eventStream.publish(ChannelDiscovered(c, tx.txOut(outputIndex).amount))
@ -158,9 +154,10 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSM[State, Data]
case Event(_: ChannelStateChanged, _) => stay
case Event(SendRoutingState(remote), Data(nodes, channels, updates, _, _, _, _, _, _)) =>
log.debug(s"info sending all announcements to $remote: channels=${channels.size} nodes=${nodes.size} updates=${updates.size}")
// disabled on Android for performance reasons
//log.debug(s"info sending all announcements to $remote: channels=${channels.size} nodes=${nodes.size} updates=${updates.size}")
// we group and add delays to leave room for channel messages
context.actorOf(ThrottleForwarder.props(remote, channels.values ++ nodes.values ++ updates.values, 100, 100 millis))
//context.actorOf(ThrottleForwarder.props(remote, channels.values ++ nodes.values ++ updates.values, 100, 100 millis))
stay
case Event(c: ChannelAnnouncement, d) =>
@ -261,8 +258,8 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSM[State, Data]
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)
//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)
}
@ -395,7 +392,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
}
}
@ -405,39 +402,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

@ -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

@ -7,6 +7,7 @@ import fr.acinq.bitcoin.{Block, Crypto}
import fr.acinq.eclair.db.sqlite.SqliteNetworkDb
import fr.acinq.eclair.randomKey
import fr.acinq.eclair.router.Announcements
import fr.acinq.eclair.wire.LightningMessageCodecs.channelAnnouncementCodec
import org.junit.runner.RunWith
import org.scalatest.FunSuite
import org.scalatest.junit.JUnitRunner

View File

@ -21,7 +21,7 @@ import fr.acinq.eclair.io.Switchboard.{NewChannel, NewConnection}
import fr.acinq.eclair.payment.{State => _, _}
import fr.acinq.eclair.router.{Announcements, AnnouncementsBatchValidationSpec}
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{Bitcoinj, Globals, Kit, Setup}
import fr.acinq.eclair.{Globals, Kit, Setup}
import grizzled.slf4j.Logging
import org.bitcoinj.core.Transaction
import org.json4s.DefaultFormats
@ -87,7 +87,7 @@ class BasicIntegrationSpvSpec extends TestKit(ActorSystem("test")) with FunSuite
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
@ -115,7 +115,7 @@ class BasicIntegrationSpvSpec extends TestKit(ActorSystem("test")) with FunSuite
}
val setup = new Setup(datadir, actorSystem = ActorSystem(s"system-$name"))
val kit = Await.result(setup.bootstrap, 10 seconds)
setup.bitcoin.asInstanceOf[Bitcoinj].bitcoinjKit.awaitRunning()
//TODO setup.bitcoin.asInstanceOf[Bitcoinj].bitcoinjKit.awaitRunning()
nodes = nodes + (name -> kit)
}

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}"
@ -78,7 +79,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

@ -11,6 +11,7 @@ import fr.acinq.eclair.router._
import fr.acinq.eclair.wire._
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
import org.scalatest.matchers.FailureMessage
/**
* Created by PM on 29/08/2016.

View File

@ -97,10 +97,10 @@ abstract class BaseRouterSpec extends TestkitBaseClass {
IndividualResult(chan_ef, Some(Transaction(version = 0, txIn = Nil, txOut = TxOut(Satoshi(1000000), write(pay2wsh(Scripts.multiSig2of2(funding_e, funding_f)))) :: Nil, lockTime = 0)), true) :: Nil
))
// watcher receives watch-spent request
watcher.expectMsgType[WatchSpentBasic]
watcher.expectMsgType[WatchSpentBasic]
watcher.expectMsgType[WatchSpentBasic]
watcher.expectMsgType[WatchSpentBasic]
//watcher.expectMsgType[WatchSpentBasic]
//watcher.expectMsgType[WatchSpentBasic]
//watcher.expectMsgType[WatchSpentBasic]
//watcher.expectMsgType[WatchSpentBasic]
// then nodes
router ! ann_a
router ! ann_b

View File

@ -8,7 +8,7 @@ import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.channel.BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT
import fr.acinq.eclair.router.Announcements.makeChannelUpdate
import fr.acinq.eclair.transactions.Scripts
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, Error, NodeAnnouncement}
import fr.acinq.eclair.wire.Error
import fr.acinq.eclair.{randomKey, toShortId}
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
@ -49,7 +49,7 @@ class RouterSpec extends BaseRouterSpec {
IndividualResult(chan_ax, None, false) ::
IndividualResult(chan_ay, Some(Transaction(version = 0, txIn = Nil, txOut = TxOut(Satoshi(1000000), write(pay2wsh(Scripts.multiSig2of2(funding_a, randomKey.publicKey)))) :: Nil, lockTime = 0)), true) ::
IndividualResult(chan_az, Some(Transaction(version = 0, txIn = Nil, txOut = TxOut(Satoshi(1000000), write(pay2wsh(Scripts.multiSig2of2(funding_a, priv_funding_z.publicKey)))) :: Nil, lockTime = 0)), false) :: Nil))
watcher.expectMsgType[WatchSpentBasic]
//watcher.expectMsgType[WatchSpentBasic]
watcher.expectNoMsg(1 second)
eventListener.expectMsg(ChannelDiscovered(chan_ac, Satoshi(1000000)))
@ -162,7 +162,7 @@ class RouterSpec extends BaseRouterSpec {
sender.expectMsgType[RouteResponse]
}
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]
@ -180,9 +180,7 @@ class RouterSpec extends BaseRouterSpec {
val sender = TestProbe()
val receiver = TestProbe()
sender.send(router, SendRoutingState(receiver.ref))
for (_ <- 0 until 4) receiver.expectMsgType[ChannelAnnouncement]
for (_ <- 0 until 6) receiver.expectMsgType[NodeAnnouncement]
for (_ <- 0 until 8) receiver.expectMsgType[ChannelUpdate]
receiver.expectNoMsg(1 second)
}
}

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-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.0 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,71 +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,301 +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 javafx.scene.shape.Rectangle?>
<?import javafx.scene.text.*?>
<?import java.net.URL?>
<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>
<TextFlow>
<children>
<Text strokeType="OUTSIDE" strokeWidth="0.0" styleClass="text-strong"
text="Lost connection to "/>
<Text fx:id="blockerDialogTitleEngineName" strokeType="OUTSIDE"
strokeWidth="0.0" styleClass="text-strong"/>
<Text strokeType="OUTSIDE" styleClass="text-strong" strokeWidth="0.0"
text="..."/>
</children>
</TextFlow>
<TextFlow>
<children>
<Text strokeType="OUTSIDE" strokeWidth="0.0" styleClass="text-sm"
text="Please check your connection."/>
</children>
</TextFlow>
</children>
</VBox>
</children>
</HBox>
</children>
</StackPane>
</children>
<stylesheets>
<URL value="@main.css"/>
<URL value="@../commons/globals.css"/>
</stylesheets>
</AnchorPane>

View File

@ -1,43 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.*?>
<?import java.lang.String?>
<?import java.net.URL?>
<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,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.VBox?>
<?import java.net.URL?>
<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,66 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<?import java.lang.String?>
<?import java.net.URL?>
<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,91 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.collections.FXCollections?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import java.lang.String?>
<?import java.net.URL?>
<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,96 +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,57 +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,55 +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.effect.DropShadow?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.*?>
<?import java.net.URL?>
<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,29 +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 {
val datadir = new File(System.getProperty("eclair.datadir", System.getProperty("user.home") + "/.eclair"))
try {
val headless = System.getProperty("eclair.headless") != null
if (headless) {
new Setup(datadir).bootstrap
} else {
LauncherImpl.launchApplication(classOf[FxApp], classOf[FxPreloader], Array(datadir.getAbsolutePath))
}
} 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,137 +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.bitcoind.zmq.ZMQActor._
import fr.acinq.eclair.blockchain.electrum.ElectrumClient.ElectrumEvent
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 IncompatibleDBException =>
notifyPreloader(new ErrorNotification("Setup", "Breaking changes!", null))
notifyPreloader(new AppNotification(InfoAppNotification, "Eclair is still in alpha, and under heavy development. Last update was not backward compatible."))
notifyPreloader(new AppNotification(InfoAppNotification, "Please reset your datadir."))
case t: Throwable =>
notifyPreloader(new ErrorNotification("Setup", s"Internal error: ${t.toString}", t))
}
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[ZMQEvent])
setup.system.eventStream.subscribe(guiUpdater, classOf[ElectrumEvent])
pKit.completeWith(setup.bootstrap)
import scala.concurrent.ExecutionContext.Implicits.global
pKit.future.onComplete {
case Success(_) =>
Platform.runLater(new Runnable {
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,76 +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,198 +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.bitcoind.zmq.ZMQActor.{ZMQConnected, ZMQDisconnected}
import fr.acinq.eclair.blockchain.electrum.ElectrumClient.{ElectrumConnected, ElectrumDisconnected}
import fr.acinq.eclair.channel._
import fr.acinq.eclair.gui.controllers._
import fr.acinq.eclair.gui.utils.CoinFormat
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"${CoinFormat.MILLI_BTC_FORMAT.format(millisatoshi2millibtc(MilliSatoshi(spec.totalFunds)).amount)}")
channelPaneController.amountUs.setText(s"${CoinFormat.MILLI_BTC_FORMAT.format(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.amountIn}, feesEarned=${p.amountOut}")
mainController.paymentRelayedList.prepend(new PaymentRelayedRecord(p, LocalDateTime.now()))
case ZMQConnected =>
log.debug("ZMQ connection UP")
mainController.hideBlockerModal
case ZMQDisconnected =>
log.debug("ZMQ connection DOWN")
mainController.showBlockerModal("Bitcoin Core")
case ElectrumConnected =>
log.debug("Electrum connection UP")
mainController.hideBlockerModal
case ElectrumDisconnected =>
log.debug("Electrum connection DOWN")
mainController.showBlockerModal("Electrum")
}
}

View File

@ -1,121 +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.Crypto.PublicKey
import fr.acinq.bitcoin.{BinaryData, MilliSatoshi}
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.duration._
import scala.concurrent.{ExecutionContext, Future}
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, minFinalCltvExpiry: Option[Long]) = {
logger.info(s"sending $amountMsat to $paymentHash @ $nodeId")
val request = minFinalCltvExpiry match {
case None => SendPayment(amountMsat, paymentHash, nodeId)
case Some(value) => SendPayment(amountMsat, paymentHash, nodeId, value)
}
(for {
kit <- fKit
res <- (kit.paymentInitiator ? request).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,27 +0,0 @@
package fr.acinq.eclair.gui.controllers
import javafx.application.HostServices
import javafx.fxml.FXML
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,514 +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.scene.text.Text
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 = _
@FXML var blockerDialogTitleEngineName: Text = _
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.amountIn.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.amountIn.amount - p.getValue.event.amountOut.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(backendName: String) = {
blockerDialogTitleEngineName.setText(backendName)
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,27 +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,113 +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,88 +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.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,129 +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.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,77 +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.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, pr.minFinalCltvExpiry)) 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,71 +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.{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,32 +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.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,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.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,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.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,15 +0,0 @@
package fr.acinq.eclair.gui.utils
import java.text.DecimalFormat
object CoinFormat {
/**
* Always 5 decimals
*/
val MILLI_BTC_PATTERN = "###,##0.00000"
/**
* Localized formatter for milli-bitcoin amounts. Uses `MILLI_BTC_PATTERN`.
*/
val MILLI_BTC_FORMAT: DecimalFormat = new DecimalFormat(MILLI_BTC_PATTERN)
}

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,47 +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-SNAPSHOT</version>
<version>0.2-android-SNAPSHOT</version>
</parent>
<artifactId>eclair-node_2.11</artifactId>
@ -80,5 +80,10 @@
<artifactId>janino</artifactId>
<version>3.0.7</version>
</dependency>
<dependency>
<groupId>com.googlecode.lanterna</groupId>
<artifactId>lanterna</artifactId>
<version>3.0.0-rc1</version>
</dependency>
</dependencies>
</project>

View File

@ -1,17 +0,0 @@
akka {
loggers = ["akka.event.slf4j.Slf4jLogger"]
loglevel = "DEBUG"
actor {
debug {
# enable DEBUG logging of all LoggingFSMs for events, transitions and timers
fsm = on
}
}
http {
host-connection-pool {
max-open-requests = 64
}
}
}

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)
}
}

View File

@ -4,13 +4,12 @@
<groupId>fr.acinq.eclair</groupId>
<artifactId>eclair_2.11</artifactId>
<version>0.2-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>
@ -45,11 +44,11 @@
<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-rc4</bitcoinj.version>
</properties>