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:
parent
0416784f08
commit
17acf77a65
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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) =>
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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) =>
|
||||
|
@ -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>
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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"))
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user