1
0
mirror of https://github.com/ACINQ/eclair.git synced 2025-01-19 05:33:59 +01:00

Persisted channel capacity an added fees and capacity to Channel GUI (#416)

* (gui) added channel fees (base and proportional) and capacity to the list 
of channels in network

* (gui) fixed issues with gui being updated from wrong threads

* channel capacity is now saved in network DB along with the tx id when
a channel is discovered. `ChannelDiscovered` now contains the capacity.

A compatibility check for the network DB is added in startup. This check is
separated from the node DB check because a network DB check failure is
less severe and the network DB file can be safely removed with no impact
on the node.
This commit is contained in:
n1bor 2018-02-12 16:28:21 +00:00 committed by Dominique
parent 0416784f08
commit 17acf77a65
13 changed files with 177 additions and 123 deletions

View File

@ -65,7 +65,7 @@ eclair {
expiry-delta-blocks = 144
fee-base-msat = 10000
fee-proportional-millionths = 100 // fee charged per transferred satoshi in millionths of a satoshi (100 = 0.1%)
fee-proportional-millionths = 100 // fee charged per transferred satoshi in millionths of a satoshi (100 = 0.01%)
// maximum local vs remote feerate mismatch; 1.0 means 100%
// actual check is abs((local feerate - remote fee rate) / (local fee rate + remote fee rate)/2) > fee rate mismatch

View File

@ -16,6 +16,15 @@ object DBCompatChecker extends Logging {
case Success(_) => {}
case Failure(_) => throw IncompatibleDBException
}
}
case object IncompatibleDBException extends RuntimeException("DB files are not compatible with this version of eclair.")
/**
* Tests if the network database is readable.
*
* @param nodeParams
*/
def checkNetworkDBCompatibility(nodeParams: NodeParams): Unit =
Try(nodeParams.networkDb.listChannels(), nodeParams.networkDb.listNodes(), nodeParams.networkDb.listChannelUpdates()) match {
case Success(_) => {}
case Failure(_) => throw IncompatibleNetworkDBException
}
}

View File

@ -50,6 +50,7 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act
// early checks
DBCompatChecker.checkDBCompatibility(nodeParams)
DBCompatChecker.checkNetworkDBCompatibility(nodeParams)
PortChecker.checkAvailable(config.getString("server.binding-ip"), config.getInt("server.port"))
logger.info(s"nodeid=${nodeParams.privateKey.publicKey.toBin} alias=${nodeParams.alias}")
@ -245,3 +246,7 @@ case object BitcoinRPCConnectionException extends RuntimeException("could not co
case object BitcoinWalletDisabledException extends RuntimeException("bitcoind must have wallet support enabled")
case object EmptyAPIPasswordException extends RuntimeException("must set a password for the json-rpc api")
case object IncompatibleDBException extends RuntimeException("database is not compatible with this version of eclair")
case object IncompatibleNetworkDBException extends RuntimeException("network database is not compatible with this version of eclair")

View File

@ -1,6 +1,6 @@
package fr.acinq.eclair.db
import fr.acinq.bitcoin.BinaryData
import fr.acinq.bitcoin.{BinaryData, Satoshi}
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement}
@ -14,7 +14,7 @@ trait NetworkDb {
def listNodes(): List[NodeAnnouncement]
def addChannel(c: ChannelAnnouncement, txid: BinaryData)
def addChannel(c: ChannelAnnouncement, txid: BinaryData, capacity: Satoshi)
/**
* This method removes 1 channel announcement and 2 channel updates (at both ends of the same channel)
@ -24,7 +24,7 @@ trait NetworkDb {
*/
def removeChannel(shortChannelId: Long)
def listChannels(): Map[ChannelAnnouncement, BinaryData]
def listChannels(): Map[ChannelAnnouncement, (BinaryData, Satoshi)]
def addChannelUpdate(u: ChannelUpdate)

View File

@ -2,7 +2,7 @@ package fr.acinq.eclair.db.sqlite
import java.sql.Connection
import fr.acinq.bitcoin.{BinaryData, Crypto}
import fr.acinq.bitcoin.{BinaryData, Crypto, Satoshi}
import fr.acinq.eclair.db.NetworkDb
import fr.acinq.eclair.router.Announcements
import fr.acinq.eclair.wire.LightningMessageCodecs.{channelAnnouncementCodec, channelUpdateCodec, nodeAnnouncementCodec}
@ -16,7 +16,7 @@ class SqliteNetworkDb(sqlite: Connection) extends NetworkDb {
using(sqlite.createStatement()) { statement =>
statement.execute("PRAGMA foreign_keys = ON")
statement.executeUpdate("CREATE TABLE IF NOT EXISTS nodes (node_id BLOB NOT NULL PRIMARY KEY, data BLOB NOT NULL)")
statement.executeUpdate("CREATE TABLE IF NOT EXISTS channels (short_channel_id INTEGER NOT NULL PRIMARY KEY, txid STRING NOT NULL, data BLOB NOT NULL)")
statement.executeUpdate("CREATE TABLE IF NOT EXISTS channels (short_channel_id INTEGER NOT NULL PRIMARY KEY, txid STRING NOT NULL, data BLOB NOT NULL, capacity_sat INTEGER NOT NULL)")
statement.executeUpdate("CREATE TABLE IF NOT EXISTS channel_updates (short_channel_id INTEGER NOT NULL, node_flag INTEGER NOT NULL, data BLOB NOT NULL, PRIMARY KEY(short_channel_id, node_flag), FOREIGN KEY(short_channel_id) REFERENCES channels(short_channel_id))")
statement.executeUpdate("CREATE INDEX IF NOT EXISTS channel_updates_idx ON channel_updates(short_channel_id)")
}
@ -51,11 +51,12 @@ class SqliteNetworkDb(sqlite: Connection) extends NetworkDb {
}
}
override def addChannel(c: ChannelAnnouncement, txid: BinaryData): Unit = {
using(sqlite.prepareStatement("INSERT OR IGNORE INTO channels VALUES (?, ?, ?)")) { statement =>
override def addChannel(c: ChannelAnnouncement, txid: BinaryData, capacity: Satoshi): Unit = {
using(sqlite.prepareStatement("INSERT OR IGNORE INTO channels VALUES (?, ?, ?, ?)")) { statement =>
statement.setLong(1, c.shortChannelId)
statement.setString(2, txid.toString())
statement.setBytes(3, channelAnnouncementCodec.encode(c).require.toByteArray)
statement.setLong(4, capacity.amount)
statement.executeUpdate()
}
}
@ -69,12 +70,13 @@ class SqliteNetworkDb(sqlite: Connection) extends NetworkDb {
}
}
override def listChannels(): Map[ChannelAnnouncement, BinaryData] = {
override def listChannels(): Map[ChannelAnnouncement, (BinaryData, Satoshi)] = {
using(sqlite.createStatement()) { statement =>
val rs = statement.executeQuery("SELECT data, txid FROM channels")
var l: Map[ChannelAnnouncement, BinaryData] = Map()
val rs = statement.executeQuery("SELECT data, txid, capacity_sat FROM channels")
var l: Map[ChannelAnnouncement, (BinaryData, Satoshi)] = Map()
while (rs.next()) {
l = l + (channelAnnouncementCodec.decode(BitVector(rs.getBytes("data"))).require.value -> BinaryData(rs.getString("txid")))
l = l + (channelAnnouncementCodec.decode(BitVector(rs.getBytes("data"))).require.value ->
(BinaryData(rs.getString("txid")), Satoshi(rs.getLong("capacity_sat"))))
}
l
}

View File

@ -15,7 +15,7 @@ case class NodeUpdated(ann: NodeAnnouncement) extends NetworkEvent
case class NodeLost(nodeId: PublicKey) extends NetworkEvent
case class ChannelDiscovered(ann: ChannelAnnouncement) extends NetworkEvent
case class ChannelDiscovered(ann: ChannelAnnouncement, capacity: Satoshi) extends NetworkEvent
case class ChannelLost(channelId: Long) extends NetworkEvent

View File

@ -4,7 +4,7 @@ import java.io.StringWriter
import akka.actor.{ActorRef, FSM, Props, Terminated}
import akka.pattern.pipe
import fr.acinq.bitcoin.{BinaryData, Satoshi}
import fr.acinq.bitcoin.BinaryData
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.Script.{pay2wsh, write}
import fr.acinq.eclair._
@ -88,7 +88,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSM[State, Data]
val updates = db.listChannelUpdates()
// let's prune the db (maybe eclair was stopped for a long time)
val staleChannels = getStaleChannels(channels.keys, updates)
if (staleChannels.size > 0) {
if (staleChannels.nonEmpty) {
log.info(s"dropping ${staleChannels.size} stale channels pre-validation")
staleChannels.foreach(shortChannelId => db.removeChannel(shortChannelId)) // this also removes updates
}
@ -101,13 +101,13 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSM[State, Data]
val initNodes = remainingNodes.map(n => (n.nodeId -> n)).toMap
// send events for these channels/nodes
remainingChannels.foreach(c => context.system.eventStream.publish(ChannelDiscovered(c)))
remainingChannels.foreach(c => context.system.eventStream.publish(ChannelDiscovered(c, channels(c)._2)))
remainingNodes.foreach(n => context.system.eventStream.publish(NodeDiscovered(n)))
// watch the funding tx of all these channels
// note: some of them may already have been spent, in that case we will receive the watchh event immediately
// note: some of them may already have been spent, in that case we will receive the watch event immediately
remainingChannels.foreach { c =>
val txid = channels(c)
val txid = channels(c)._1
val (_, _, outputIndex) = fromShortId(c.shortChannelId)
val fundingOutputScript = write(pay2wsh(Scripts.multiSig2of2(PublicKey(c.bitcoinKey1), PublicKey(c.bitcoinKey2))))
watcher ! WatchSpentBasic(self, txid, outputIndex, fundingOutputScript, BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT(c.shortChannelId))
@ -158,8 +158,9 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSM[State, Data]
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.toHexString}")
context.system.eventStream.publish(ChannelDiscovered(c))
db.addChannel(c, tx.txid)
val capacity = tx.txOut(outputIndex).amount
context.system.eventStream.publish(ChannelDiscovered(c, capacity))
db.addChannel(c, tx.txid, capacity)
Some(c)
}
case IndividualResult(c, Some(tx), false) =>

View File

@ -3,7 +3,7 @@ package fr.acinq.eclair.db
import java.net.{InetAddress, InetSocketAddress}
import java.sql.DriverManager
import fr.acinq.bitcoin.{Block, Crypto}
import fr.acinq.bitcoin.{Block, Crypto, Satoshi}
import fr.acinq.eclair.db.sqlite.SqliteNetworkDb
import fr.acinq.eclair.randomKey
import fr.acinq.eclair.router.Announcements
@ -57,16 +57,17 @@ class SqliteNetworkDbSpec extends FunSuite {
val txid_1 = randomKey.toBin
val txid_2 = randomKey.toBin
val txid_3 = randomKey.toBin
val capacity = Satoshi(10000)
assert(db.listChannels().toSet === Set.empty)
db.addChannel(channel_1, txid_1)
db.addChannel(channel_1, txid_1) // duplicate is ignored
db.addChannel(channel_1, txid_1, capacity)
db.addChannel(channel_1, txid_1, capacity) // duplicate is ignored
assert(db.listChannels().size === 1)
db.addChannel(channel_2, txid_2)
db.addChannel(channel_3, txid_3)
assert(db.listChannels().toSet === Set((channel_1, txid_1), (channel_2, txid_2), (channel_3, txid_3)))
db.addChannel(channel_2, txid_2, capacity)
db.addChannel(channel_3, txid_3, capacity)
assert(db.listChannels().toSet === Set((channel_1, (txid_1, capacity)), (channel_2, (txid_2, capacity)), (channel_3, (txid_3, capacity))))
db.removeChannel(channel_2.shortChannelId)
assert(db.listChannels().toSet === Set((channel_1, txid_1), (channel_3, txid_3)))
assert(db.listChannels().toSet === Set((channel_1, (txid_1, capacity)), (channel_3, (txid_3, capacity))))
val channel_update_1 = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, randomKey, randomKey.publicKey, 42, 5, 7000000, 50000, 100, true)
val channel_update_2 = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, randomKey, randomKey.publicKey, 43, 5, 7000000, 50000, 100, true)
@ -79,7 +80,7 @@ class SqliteNetworkDbSpec extends FunSuite {
intercept[SQLiteException](db.addChannelUpdate(channel_update_2))
db.addChannelUpdate(channel_update_3)
db.removeChannel(channel_3.shortChannelId)
assert(db.listChannels().toSet === Set((channel_1, txid_1)))
assert(db.listChannels().toSet === Set((channel_1, (txid_1, capacity))))
assert(db.listChannelUpdates().toSet === Set(channel_update_1))
db.updateChannelUpdate(channel_update_1)
}

View File

@ -63,7 +63,7 @@ class RouterSpec extends BaseRouterSpec {
watcher.expectMsgType[WatchSpentBasic]
watcher.expectNoMsg(1 second)
eventListener.expectMsg(ChannelDiscovered(chan_ac))
eventListener.expectMsg(ChannelDiscovered(chan_ac, Satoshi(1000000)))
}
test("properly announce lost channels and nodes") { case (router, watcher) =>

View File

@ -68,13 +68,21 @@
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY"/>
</columnResizePolicy>
<columns>
<TableColumn fx:id="networkChannelsIdColumn" minWidth="120.0"
prefWidth="170.0" maxWidth="300.0"
<TableColumn fx:id="networkChannelsIdColumn"
minWidth="120.0" prefWidth="170.0" maxWidth="200.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="networkChannelsDirectionsColumn"
minWidth="30.0" prefWidth="30.0" maxWidth="30.0"/>
<TableColumn fx:id="networkChannelsNode2Column" text="Node 2"/>
<TableColumn fx:id="networkChannelsCapacityColumn" text="Capacity"
minWidth="80.0" prefWidth="120.0" maxWidth="200.0"/>
<TableColumn fx:id="networkChannelsFeeBaseMsatColumn"
minWidth="100.0" prefWidth="120.0" maxWidth="200.0"
text="Base Fee"/>
<TableColumn fx:id="networkChannelsFeeProportionalMillionthsColumn"
minWidth="60.0" prefWidth="120.0" maxWidth="200.0"
text="Proportional Fee"/>
</columns>
</TableView>
</children>
@ -231,26 +239,33 @@
<menus>
<Menu mnemonicParsing="false" text="Channels">
<items>
<MenuItem fx:id="menuOpen" mnemonicParsing="false" onAction="#handleOpenChannel" text="Open channel...">
<MenuItem fx:id="menuOpen" mnemonicParsing="false" onAction="#handleOpenChannel"
text="Open channel...">
<accelerator>
<KeyCodeCombination code="O" control="DOWN" alt="UP" meta="UP" shift="UP" shortcut="UP" />
<KeyCodeCombination code="O" control="DOWN" alt="UP" meta="UP" shift="UP"
shortcut="UP"/>
</accelerator>
</MenuItem>
<SeparatorMenuItem mnemonicParsing="false"/>
<MenuItem fx:id="menuSend" mnemonicParsing="false" onAction="#handleSendPayment" text="Send Payment...">
<MenuItem fx:id="menuSend" mnemonicParsing="false" onAction="#handleSendPayment"
text="Send Payment...">
<accelerator>
<KeyCodeCombination code="P" control="DOWN" alt="UP" meta="UP" shift="UP" shortcut="UP" />
<KeyCodeCombination code="P" control="DOWN" alt="UP" meta="UP" shift="UP"
shortcut="UP"/>
</accelerator>
</MenuItem>
<MenuItem fx:id="menuReceive" mnemonicParsing="false" onAction="#handleReceivePayment" text="Receive Payment...">
<MenuItem fx:id="menuReceive" mnemonicParsing="false" onAction="#handleReceivePayment"
text="Receive Payment...">
<accelerator>
<KeyCodeCombination code="N" control="DOWN" alt="UP" meta="UP" shift="UP" shortcut="UP" />
<KeyCodeCombination code="N" control="DOWN" alt="UP" meta="UP" shift="UP"
shortcut="UP"/>
</accelerator>
</MenuItem>
<SeparatorMenuItem mnemonicParsing="false"/>
<MenuItem mnemonicParsing="false" onAction="#handleCloseRequest" text="Close">
<accelerator>
<KeyCodeCombination code="Q" control="DOWN" alt="UP" meta="UP" shift="UP" shortcut="UP" />
<KeyCodeCombination code="Q" control="DOWN" alt="UP" meta="UP" shift="UP"
shortcut="UP"/>
</accelerator>
</MenuItem>
</items>

View File

@ -47,6 +47,9 @@ class FxApp extends Application with Logging {
notifyPreloader(new ErrorNotification("Setup", "Breaking changes!", e))
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 e@IncompatibleNetworkDBException =>
notifyPreloader(new ErrorNotification("Setup", "Unreadable network database!", e))
notifyPreloader(new AppNotification(InfoAppNotification, "Could not read the network database. Please remove the file and restart."))
case t: Throwable =>
notifyPreloader(new ErrorNotification("Setup", s"Error: ${t.getLocalizedMessage}", t))
}

View File

@ -27,6 +27,15 @@ import scala.collection.JavaConversions._
*/
class GUIUpdater(mainController: MainController) extends Actor with ActorLogging {
/**
* Needed to stop JavaFX complaining about updates from non GUI thread
*/
private def runInGuiThread(f: () => Unit): Unit = {
Platform.runLater(new Runnable() {
@Override def run() = f()
})
}
def receive: Receive = main(Map())
def createChannelPanel(channel: ActorRef, peer: ActorRef, remoteNodeId: PublicKey, isFunder: Boolean, temporaryChannelId: BinaryData): (ChannelPaneController, VBox) = {
@ -62,9 +71,7 @@ class GUIUpdater(mainController: MainController) extends Actor with ActorLogging
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)
})
runInGuiThread(() => mainController.channelBox.getChildren.addAll(root))
context.become(main(m + (channel -> channelPaneController)))
case ChannelRestored(channel, peer, remoteNodeId, isFunder, channelId, currentData) =>
@ -76,130 +83,123 @@ class GUIUpdater(mainController: MainController) extends Actor with ActorLogging
channelPaneController.txId.setText(d.commitments.commitInput.outPoint.txid.toString())
case _ => {}
}
Platform.runLater(new Runnable() {
override def run = {
mainController.channelBox.getChildren.addAll(root)
}
})
runInGuiThread(() => mainController.channelBox.getChildren.addAll(root))
context.become(main(m + (channel -> channelPaneController)))
case ChannelIdAssigned(channel, _, _, channelId) if m.contains(channel) =>
val channelPaneController = m(channel)
Platform.runLater(new Runnable() {
override def run = {
channelPaneController.channelId.setText(s"$channelId")
}
})
runInGuiThread(() => channelPaneController.channelId.setText(s"$channelId"))
case ChannelStateChanged(channel, _, _, _, currentState, _) if m.contains(channel) =>
val channelPaneController = m(channel)
Platform.runLater(new Runnable() {
override def run = {
if (currentState == CLOSING || currentState == CLOSED) {
channelPaneController.close.setVisible(false)
} else {
channelPaneController.close.setVisible(true)
}
channelPaneController.close.setText(if (OFFLINE == currentState) "Force close" else "Close")
channelPaneController.state.setText(currentState.toString)
runInGuiThread { () =>
if (currentState == CLOSING || currentState == CLOSED) {
channelPaneController.close.setVisible(false)
} else {
channelPaneController.close.setVisible(true)
}
})
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)
})
runInGuiThread(() => 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)
}
})
runInGuiThread(() => 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)
runInGuiThread { () =>
if (!mainController.networkNodesList.exists(na => na.nodeId == nodeAnnouncement.nodeId)) {
mainController.networkNodesList.add(nodeAnnouncement)
m.foreach(f => if (nodeAnnouncement.nodeId.toString.equals(f._2.theirNodeIdValue)) {
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)
})
runInGuiThread { () =>
mainController.networkNodesList.removeIf(new Predicate[NodeAnnouncement] {
override def test(na: NodeAnnouncement) = na.nodeId.equals(nodeId)
})
}
case ChannelDiscovered(channelAnnouncement) =>
case NodeUpdated(nodeAnnouncement) =>
log.debug(s"peer node with id=${nodeAnnouncement.nodeId} has been updated")
runInGuiThread { () =>
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)) {
f._2.updateRemoteNodeAlias(nodeAnnouncement.alias)
})
}
}
case ChannelDiscovered(channelAnnouncement, capacity) =>
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))
runInGuiThread { () =>
if (!mainController.networkChannelsList.exists(c => c.announcement.shortChannelId == channelAnnouncement.shortChannelId)) {
mainController.networkChannelsList.add(new ChannelInfo(channelAnnouncement, -1, -1, capacity, 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
})
runInGuiThread { () =>
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))
runInGuiThread { () =>
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))
}
c.feeBaseMsat = channelUpdate.feeBaseMsat
c.feeProportionalMillionths = channelUpdate.feeProportionalMillionths
mainController.networkChannelsList.update(idx, c)
}
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()))
runInGuiThread(() => 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()))
runInGuiThread(() => 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()))
runInGuiThread(() => mainController.paymentRelayedList.prepend(new PaymentRelayedRecord(p, LocalDateTime.now())))
case ZMQConnected =>
log.debug("ZMQ connection UP")
mainController.hideBlockerModal
runInGuiThread(() => mainController.hideBlockerModal)
case ZMQDisconnected =>
log.debug("ZMQ connection DOWN")
mainController.showBlockerModal("Bitcoin Core")
runInGuiThread(() => mainController.showBlockerModal("Bitcoin Core"))
case ElectrumConnected =>
log.debug("Electrum connection UP")
mainController.hideBlockerModal
runInGuiThread(() => mainController.hideBlockerModal)
case ElectrumDisconnected =>
log.debug("Electrum connection DOWN")
mainController.showBlockerModal("Electrum")
runInGuiThread(() => mainController.showBlockerModal("Electrum"))
}
}

View File

@ -25,17 +25,18 @@ import javafx.stage._
import javafx.util.{Callback, Duration}
import com.google.common.net.HostAndPort
import fr.acinq.bitcoin.MilliSatoshi
import fr.acinq.bitcoin.{MilliSatoshi, Satoshi}
import fr.acinq.eclair.NodeParams.{BITCOIND, BITCOINJ, ELECTRUM}
import fr.acinq.eclair.Setup
import fr.acinq.eclair.gui.{FxApp, Handlers}
import fr.acinq.eclair.gui.stages._
import fr.acinq.eclair.gui.utils.{CoinUtils, ContextMenuUtils, CopyAction}
import fr.acinq.eclair.gui.{FxApp, Handlers}
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])
case class ChannelInfo(announcement: ChannelAnnouncement, var feeBaseMsat: Long, var feeProportionalMillionths: Long,
capacity: Satoshi, var isNode1Enabled: Option[Boolean], var isNode2Enabled: Option[Boolean])
sealed trait Record {
val event: PaymentEvent
@ -92,6 +93,9 @@ class MainController(val handlers: Handlers, val hostServices: HostServices) ext
@FXML var networkChannelsNode1Column: TableColumn[ChannelInfo, String] = _
@FXML var networkChannelsDirectionsColumn: TableColumn[ChannelInfo, ChannelInfo] = _
@FXML var networkChannelsNode2Column: TableColumn[ChannelInfo, String] = _
@FXML var networkChannelsFeeBaseMsatColumn: TableColumn[ChannelInfo, String] = _
@FXML var networkChannelsFeeProportionalMillionthsColumn: TableColumn[ChannelInfo, String] = _
@FXML var networkChannelsCapacityColumn: TableColumn[ChannelInfo, String] = _
// payment sent table
val paymentSentList = FXCollections.observableArrayList[PaymentSentRecord]()
@ -208,6 +212,20 @@ class MainController(val handlers: Handlers, val hostServices: HostServices) ext
networkChannelsNode2Column.setCellValueFactory(new Callback[CellDataFeatures[ChannelInfo, String], ObservableValue[String]]() {
def call(pc: CellDataFeatures[ChannelInfo, String]) = new SimpleStringProperty(pc.getValue.announcement.nodeId2.toString)
})
networkChannelsFeeBaseMsatColumn.setCellValueFactory(new Callback[CellDataFeatures[ChannelInfo, String], ObservableValue[String]]() {
def call(pc: CellDataFeatures[ChannelInfo, String]) = new SimpleStringProperty(
CoinUtils.formatAmountInUnit(MilliSatoshi(pc.getValue.feeBaseMsat), FxApp.getUnit, withUnit = true))
})
// feeProportionalMillionths is fee per satoshi in millionths of a satoshi
networkChannelsFeeProportionalMillionthsColumn.setCellValueFactory(new Callback[CellDataFeatures[ChannelInfo, String], ObservableValue[String]]() {
def call(pc: CellDataFeatures[ChannelInfo, String]) = new SimpleStringProperty(
s"${CoinUtils.COIN_FORMAT.format(pc.getValue.feeProportionalMillionths.toDouble / 1000000 * 100)}%")
})
networkChannelsCapacityColumn.setCellValueFactory(new Callback[CellDataFeatures[ChannelInfo, String], ObservableValue[String]]() {
def call(pc: CellDataFeatures[ChannelInfo, String]) = new SimpleStringProperty(
CoinUtils.formatAmountInUnit(pc.getValue.capacity, FxApp.getUnit, withUnit = true))
})
networkChannelsTable.setRowFactory(new Callback[TableView[ChannelInfo], TableRow[ChannelInfo]]() {
override def call(table: TableView[ChannelInfo]): TableRow[ChannelInfo] = setupPeerChannelContextMenu()
})
@ -228,19 +246,19 @@ class MainController(val handlers: Handlers, val hostServices: HostServices) ext
setText(null)
} else {
item match {
case ChannelInfo(_, Some(true), Some(true)) =>
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)) =>
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)) =>
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)) =>
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)
@ -339,8 +357,8 @@ class MainController(val handlers: Handlers, val hostServices: HostServices) ext
}
private def updateTabHeader(tab: Tab, prefix: String, items: ObservableList[_]) = Platform.runLater(new Runnable() {
override def run(): Unit = tab.setText(s"$prefix (${items.size})")
})
override def run(): Unit = 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)