1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-02-23 14:40:34 +01:00

Added an activity tab to the GUI (#52)

* (gui) added Activity tab (payment sent, received, relayed)
* GUIUpdater listens to PaymentEvent
* payments are listed in separate tableviews (sent, received, relayed)
* added Payment[Sent, Relayed, Received] events
* (gui) Handling Relayed and Sent payments in Activity Tab
* (gui) fixed amount columns in activity
* (gui) Added formatting to monetary columns of activity tables
* (gui) payments are prepended to activity tables
This commit is contained in:
dpad85 2017-03-31 20:03:36 +02:00 committed by Pierre-Marie Padiou
parent c3bfbf4a14
commit 34677f0ed6
24 changed files with 353 additions and 309 deletions

View file

@ -48,11 +48,17 @@
-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);
@ -64,6 +70,11 @@
-fx-padding: 2px 0;
}
.menu-bar .context-menu {
/* font size in menu context popup is standard */
-fx-font-size: 14px;
}
/* ---------- Grid Structure ---------- */
.grid {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 728 B

View file

@ -127,3 +127,43 @@
.button.notification-close:pressed {
-fx-background-color: #7e7e7e;
}
/* ------------- 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;
}

View file

@ -6,7 +6,7 @@
<?import javafx.scene.layout.*?>
<?import java.net.URL?>
<?import javafx.scene.shape.Rectangle?>
<BorderPane fx:id="root" minHeight="300.0" styleClass="root" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
<BorderPane fx:id="root" minHeight="300.0" prefHeight="400.0" styleClass="root" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
<center>
<TabPane tabClosingPolicy="UNAVAILABLE" BorderPane.alignment="CENTER">
<tabs>
@ -31,7 +31,7 @@
</StackPane>
</content>
</Tab>
<Tab text="All Nodes" fx:id="networkNodesTab">
<Tab text="All Nodes" fx:id="networkNodesTab" closable="false">
<content>
<VBox spacing="10.0" styleClass="grid">
<children>
@ -48,7 +48,7 @@
</VBox>
</content>
</Tab>
<Tab text="All Channels" fx:id="networkChannelsTab">
<Tab text="All Channels" fx:id="networkChannelsTab" closable="false">
<content>
<VBox spacing="10.0" styleClass="grid">
<children>
@ -64,6 +64,59 @@
</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 erased when the node is shutdown." />
</children>
</AnchorPane>
</content>
</Tab>
</tabs>
</TabPane>
</center>

View file

@ -63,7 +63,7 @@
<VBox alignment="CENTER_RIGHT" GridPane.rowIndex="3">
<children>
<Label styleClass="text-strong" text="Push Amount (mSat)" />
<Label styleClass="text-strong" text="Push Amount (msat)" />
<Label styleClass="label-description" text="Sent when opening channel" textAlignment="RIGHT" wrapText="true" />
</children>
</VBox>

View file

@ -27,7 +27,7 @@
<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"/>
<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"/>

View file

@ -1,185 +0,0 @@
/* ---------- Global ---------- */
.root {
-fx-font-size: 14px;
-fx-font-family: "Open Sans", "Arial", "sans-serif";
-fx-base: rgb(255, 255, 255);
-fx-background: rgb(255, 255, 255);
-fx-text-fill: rgb(80, 82, 84);
}
.label {
-fx-text-fill: rgb(80, 82, 84);
}
.label.text-muted {
-fx-text-fill: rgb(156,159,161);
}
/* ---------- Buttons ---------- */
.button {
-fx-padding: .5em 1em;
-fx-border-insets: 0;
-fx-border-width: 0;
-fx-background-insets: 0;
-fx-background-radius: 0;
-fx-background-color: rgb(63,179,234);
-fx-cursor: hand;
-fx-border-color: rgb(63,179,234);
-fx-border-width: 1px;
}
.button .text {
-fx-fill: white;
}
.button:hover,
.button:focused {
-fx-background-color: rgb(24,158,222);
-fx-border-color: rgb(24,158,222);
}
.button:pressed {
-fx-background-color: rgb(0,131,195);
-fx-border-color: rgb(0,131,195);
}
.button.close {
-fx-background-color: rgb(232,65,90);
}
.button.cancel {
-fx-background-color: white;
-fx-border-color: rgb(233,190,198);
-fx-border-width: 1px;
}
.button.cancel:hover,
.button.cancel:focused,
.button.cancel:pressed {
-fx-border-color: rgb(217,144,158);
}
.button.cancel .text {
-fx-fill: rgb(212,125,140);
}
.button.cancel:hover .text,
.button.cancel:focused .text,
.button.cancel:pressed .text {
-fx-fill: rgb(191,68,92);
}
.button.grey {
-fx-background-color: rgb(230,230,230);
-fx-border-color: rgb(230,230,230);
}
.button.grey:hover,
.button.grey:pressed {
-fx-background-color: rgb(220,220,220);
-fx-border-color: rgb(220,220,220);
}
.button.grey .text {
-fx-fill: rgb(95,95,95);
}
.button.close-channel {
-fx-background-color: white;
-fx-background-image: url("../commons/images/close.png");
-fx-background-repeat: no-repeat;
-fx-background-size: 5px;
-fx-background-position: 8px center;
-fx-border-color: transparent;
-fx-padding: 0px 5px 0px 17px;
}
.button.close-channel:hover {
-fx-background-color: rgb(249,243,243);
-fx-border-color: rgb(249,243,243);
}
.button.close-channel .text {
-fx-fill: rgb(232,65,90);
-fx-font-size: 12px;
}
/* ------------- TEXT FIELD ------------- */
.text-field, .text-area {
-fx-padding: .6em;
-fx-background-insets: 0;
-fx-border-color: rgb(220,220,220);
-fx-border-width: 1px;
-fx-background-radius: 0;
-fx-background-color: rgb(249,249,249);
}
.text-field:focused {
-fx-border-color: rgb(63,179,234);
}
/* ------------- TEXT AREA ------------- */
.ta.text-area {
-fx-border-width: 1px;
-fx-padding: .4em .2em;
-fx-font-family: "Consolas", "Lucida Console", Monaco, monospace;
}
.ta.text-area *.scroll-pane > *.viewport,
.ta.text-area *.scroll-pane > *.viewport *.content {
-fx-background-color: transparent;
}
/* ------------- Status Bar ------------- */
.status-bar {
-fx-background-color: rgb(240,245,246);
}
.status-bar .separator:vertical .line {
-fx-background-color: rgb(226,227,229);
}
/* ------------- About modal ------------- */
.about-text {
-fx-fill: rgb(80,80,80);
}
/* ------------- Splash Error Box ------------- */
.error-box {
-fx-background-color: rgb(255,255,255);
-fx-text-fill: white;
-fx-border-color: rgb(180,180,180);
}
/* ------------- Menu ------------- */
.menu-bar .menu-button:hover,
.menu-bar .menu-button:focused,
.menu-bar .menu-button:showing,
.menu-bar .menu-item:hover,
.menu-bar .menu-item:focused {
-fx-background-color: rgb(109,197,239);
}
/* ------------- Tabs ------------- */
.tab-pane > *.tab-header-area {
-fx-padding: .7em 1em 0;
}
.tab-pane > *.tab-header-area > *.tab-header-background {
-fx-base: rgb(25,170,240);
-fx-background-color: rgb(228,235,238);
}
.tab:top {
-fx-padding: 0.25em 1em;
-fx-background-color: rgba(255,255,255, .5);
-fx-background-radius: 0;
-fx-focus-color: transparent;
-fx-faint-focus-color: transparent;
-fx-background-insets: 0 0 0 0;
}
.tab:top:selected {
-fx-background-color: white;
-fx-base: rgb(25,170,240);
}
.tab-content-area {
-fx-background-color: white;
}

View file

@ -383,7 +383,7 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A
case Event(c@CMD_ADD_HTLC(amountMsat, rHash, expiry, route, downstream_opt, do_commit), d@DATA_NORMAL(commitments, _)) =>
Try(Commitments.sendAdd(commitments, c)) match {
case Success(Right((commitments1, add))) =>
val origin = downstream_opt.map(Relayed(_)).getOrElse(Local(sender))
val origin = downstream_opt.map(u => Relayed(sender, u)).getOrElse(Local(sender))
relayer ! AddHtlcSucceeded(add, origin)
if (do_commit) self ! CMD_SIGN
handleCommandSuccess(sender, d.copy(commitments = commitments1))
@ -895,7 +895,7 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A
log.info(s"we are disconnected so we just include the add in our commitments")
Try(Commitments.sendAdd(commitments, c)) match {
case Success(Right((commitments1, add))) =>
val origin = downstream_opt.map(Relayed(_)).getOrElse(Local(sender))
val origin = downstream_opt.map(u => Relayed(sender, u)).getOrElse(Local(sender))
relayer ! AddHtlcSucceeded(add, origin)
sender ! "ok"
goto(stateName) using d.copy(commitments = commitments1)

View file

@ -13,6 +13,7 @@ import akka.actor.{Props, SupervisorStrategy}
import akka.stream.StreamTcpException
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 fr.acinq.eclair.{Setup, SimpleSupervisor, TCPBindException}
import grizzled.slf4j.Logging
@ -42,6 +43,7 @@ class FxApp extends Application with Logging {
val guiUpdater = setup.system.actorOf(SimpleSupervisor.props(Props(classOf[GUIUpdater], controller, setup), "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])
Platform.runLater(new Runnable {
override def run(): Unit = {
@ -51,7 +53,7 @@ class FxApp extends Application with Logging {
val scene = new Scene(mainRoot)
primaryStage.setTitle("Eclair")
primaryStage.setMinWidth(570)
primaryStage.setMinWidth(600)
primaryStage.setWidth(650)
primaryStage.setMinHeight(400)
primaryStage.setHeight(400)

View file

@ -5,7 +5,6 @@ import javafx.application.Platform
import javafx.event.{ActionEvent, EventHandler}
import javafx.fxml.FXMLLoader
import javafx.scene.layout.VBox
import collection.JavaConversions._
import akka.actor.{Actor, ActorLogging, ActorRef, Terminated}
import fr.acinq.bitcoin.Crypto.PublicKey
@ -14,10 +13,13 @@ import fr.acinq.eclair.Setup
import fr.acinq.eclair.channel._
import fr.acinq.eclair.gui.controllers.{ChannelPaneController, MainController}
import fr.acinq.eclair.io.Reconnect
import fr.acinq.eclair.payment.{PaymentReceived, PaymentRelayed, PaymentSent}
import fr.acinq.eclair.router.{ChannelDiscovered, ChannelLost, NodeDiscovered, NodeLost}
import fr.acinq.eclair.wire.{ChannelAnnouncement, NodeAnnouncement}
import org.jgrapht.graph.{DefaultEdge, SimpleGraph}
import scala.collection.JavaConversions._
/**
* Created by PM on 16/08/2016.
@ -139,5 +141,17 @@ class GUIUpdater(mainController: MainController, setup: Setup) extends Actor wit
mainController.networkChannelsList.removeIf(new Predicate[ChannelAnnouncement] {
override def test(ca: ChannelAnnouncement) = ca.shortChannelId == shortChannelId
})
case p: PaymentSent =>
log.debug(s"payment sent with h=${p.paymentHash}, amount=${p.amount}, fees=${p.feesPaid}")
mainController.paymentSentList.prepend(p)
case p: PaymentReceived =>
log.debug(s"payment received with h=${p.paymentHash}, amount=${p.amount}")
mainController.paymentReceivedList.prepend(p)
case p: PaymentRelayed =>
log.debug(s"payment relayed with h=${p.paymentHash}, amount=${p.amount}, feesEarned=${p.feesEarned}")
mainController.paymentRelayedList.prepend(p)
}
}

View file

@ -52,13 +52,13 @@ class Handlers(setup: Setup) extends Logging {
logger.info(s"sending $amountMsat to $paymentHash @ $nodeId")
(paymentInitiator ? CreatePayment(amountMsat, paymentHash, nodeId)).mapTo[PaymentResult].onComplete {
case Success(PaymentSucceeded(_)) =>
val message = s"Amount (mSat): $amountMsat\nH: $paymentHash"
val message = s"Amount (msat): $amountMsat\nH: $paymentHash"
notification("Payment Successful", message, NOTIFICATION_SUCCESS)
case Success(PaymentFailed(_, reason)) =>
val message = s"Cause: ${reason.getOrElse("unknown")}\nAmount (mSat): $amountMsat\nH: $paymentHash"
val message = s"Cause: ${reason.getOrElse("unknown")}\nAmount (msat): $amountMsat\nH: $paymentHash"
notification("Payment Failed", message, NOTIFICATION_ERROR)
case Failure(t) =>
val message = s"Cause: ${t.getMessage}\nAmount (mSat): $amountMsat\nH: $paymentHash"
val message = s"Cause: ${t.getMessage}\nAmount (msat): $amountMsat\nH: $paymentHash"
notification("Payment Failed", message, NOTIFICATION_ERROR)
}
}

View file

@ -1,5 +1,9 @@
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.application.{HostServices, Platform}
import javafx.beans.property._
import javafx.beans.value.{ChangeListener, ObservableValue}
@ -21,6 +25,7 @@ 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
@ -52,7 +57,7 @@ class MainController(val handlers: Handlers, val setup: Setup, val hostServices:
@FXML var channelsTab: Tab = _
// all nodes tab
val networkNodesList:ObservableList[NodeAnnouncement] = FXCollections.observableArrayList[NodeAnnouncement]()
val networkNodesList: ObservableList[NodeAnnouncement] = FXCollections.observableArrayList[NodeAnnouncement]()
@FXML var networkNodesTab: Tab = _
@FXML var networkNodesTable: TableView[NodeAnnouncement] = _
@FXML var networkNodesIdColumn: TableColumn[NodeAnnouncement, String] = _
@ -61,13 +66,42 @@ class MainController(val handlers: Handlers, val setup: Setup, val hostServices:
@FXML var networkNodesIPColumn: TableColumn[NodeAnnouncement, String] = _
// all channels
val networkChannelsList:ObservableList[ChannelAnnouncement] = FXCollections.observableArrayList[ChannelAnnouncement]()
val networkChannelsList: ObservableList[ChannelAnnouncement] = FXCollections.observableArrayList[ChannelAnnouncement]()
@FXML var networkChannelsTab: Tab = _
@FXML var networkChannelsTable: TableView[ChannelAnnouncement] = _
@FXML var networkChannelsIdColumn: TableColumn[ChannelAnnouncement, Number] = _
@FXML var networkChannelsNode1Column: TableColumn[ChannelAnnouncement, String] = _
@FXML var networkChannelsNode2Column: TableColumn[ChannelAnnouncement, String] = _
// payment sent table
val paymentSentList = FXCollections.observableArrayList[PaymentSent]()
@FXML var paymentSentTab: Tab = _
@FXML var paymentSentTable: TableView[PaymentSent] = _
@FXML var paymentSentAmountColumn: TableColumn[PaymentSent, Number] = _
@FXML var paymentSentFeesColumn: TableColumn[PaymentSent, Number] = _
@FXML var paymentSentHashColumn: TableColumn[PaymentSent, String] = _
@FXML var paymentSentDateColumn: TableColumn[PaymentSent, String] = _
// payment received table
val paymentReceivedList = FXCollections.observableArrayList[PaymentReceived]()
@FXML var paymentReceivedTab: Tab = _
@FXML var paymentReceivedTable: TableView[PaymentReceived] = _
@FXML var paymentReceivedAmountColumn: TableColumn[PaymentReceived, Number] = _
@FXML var paymentReceivedHashColumn: TableColumn[PaymentReceived, String] = _
@FXML var paymentReceivedDateColumn: TableColumn[PaymentReceived, String] = _
// payment relayed table
val paymentRelayedList = FXCollections.observableArrayList[PaymentRelayed]()
@FXML var paymentRelayedTab: Tab = _
@FXML var paymentRelayedTable: TableView[PaymentRelayed] = _
@FXML var paymentRelayedAmountColumn: TableColumn[PaymentRelayed, Number] = _
@FXML var paymentRelayedFeesColumn: TableColumn[PaymentRelayed, Number] = _
@FXML var paymentRelayedHashColumn: TableColumn[PaymentRelayed, String] = _
@FXML var paymentRelayedDateColumn: TableColumn[PaymentRelayed, String] = _
val PAYMENT_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
val moneyFormatter = NumberFormat.getInstance(Locale.getDefault)
/**
* Initialize the main window.
*
@ -90,7 +124,7 @@ class MainController(val handlers: Handlers, val setup: Setup, val hostServices:
// init context
contextMenu = ContextMenuUtils.buildCopyContext(List(
new CopyAction("Copy Pubkey", s"${setup.nodeParams.privateKey.publicKey}"),
new CopyAction("Copy URI", s"${setup.nodeParams.privateKey.publicKey}@${setup.nodeParams.address.getHostString}:${setup.nodeParams.address.getPort}" )))
new CopyAction("Copy URI", s"${setup.nodeParams.privateKey.publicKey}@${setup.nodeParams.address.getHostString}:${setup.nodeParams.address.getPort}")))
// init channels tab
if (channelBox.getChildren.size() > 0) {
@ -113,11 +147,7 @@ class MainController(val handlers: Handlers, val setup: Setup, val hostServices:
// init all nodes
networkNodesTable.setItems(networkNodesList)
networkNodesList.addListener(new ListChangeListener[NodeAnnouncement] {
override def onChanged(c: Change[_ <: NodeAnnouncement]) = {
Platform.runLater(new Runnable() {
override def run = networkNodesTab.setText(s"All Nodes (${networkNodesList.size})")
})
}
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)
@ -135,7 +165,7 @@ class MainController(val handlers: Handlers, val setup: Setup, val hostServices:
})
networkNodesRGBColumn.setCellFactory(new Callback[TableColumn[NodeAnnouncement, String], TableCell[NodeAnnouncement, String]]() {
def call(pn: TableColumn[NodeAnnouncement, String]) = {
new TableCell[NodeAnnouncement, String] () {
new TableCell[NodeAnnouncement, String]() {
override def updateItem(item: String, empty: Boolean): Unit = {
super.updateItem(item, empty)
setStyle("-fx-background-color:" + item)
@ -150,11 +180,7 @@ class MainController(val handlers: Handlers, val setup: Setup, val hostServices:
// init all channels
networkChannelsTable.setItems(networkChannelsList)
networkChannelsList.addListener(new ListChangeListener[ChannelAnnouncement] {
override def onChanged(c: Change[_ <: ChannelAnnouncement]) = {
Platform.runLater(new Runnable() {
override def run = networkChannelsTab.setText(s"All Channels (${networkChannelsList.size})")
})
}
override def onChanged(c: Change[_ <: ChannelAnnouncement]) = updateTabHeader(networkChannelsTab, "All Channels", networkChannelsList)
})
networkChannelsIdColumn.setCellValueFactory(new Callback[CellDataFeatures[ChannelAnnouncement, Number], ObservableValue[Number]]() {
def call(pc: CellDataFeatures[ChannelAnnouncement, Number]) = new SimpleLongProperty(pc.getValue.shortChannelId)
@ -168,10 +194,106 @@ class MainController(val handlers: Handlers, val setup: Setup, val hostServices:
networkChannelsTable.setRowFactory(new Callback[TableView[ChannelAnnouncement], TableRow[ChannelAnnouncement]]() {
override def call(table: TableView[ChannelAnnouncement]): TableRow[ChannelAnnouncement] = setupPeerChannelContextMenu
})
// init payment sent
paymentSentTable.setItems(paymentSentList)
paymentSentList.addListener(new ListChangeListener[PaymentSent] {
override def onChanged(c: Change[_ <: PaymentSent]) = updateTabHeader(paymentSentTab, "Sent", paymentSentList)
})
paymentSentAmountColumn.setCellValueFactory(new Callback[CellDataFeatures[PaymentSent, Number], ObservableValue[Number]]() {
def call(p: CellDataFeatures[PaymentSent, Number]) = new SimpleLongProperty(p.getValue.amount.amount)
})
paymentSentAmountColumn.setCellFactory(new Callback[TableColumn[PaymentSent, Number], TableCell[PaymentSent, Number]]() {
def call(pn: TableColumn[PaymentSent, Number]) = buildMoneyTableCell
})
paymentSentFeesColumn.setCellValueFactory(new Callback[CellDataFeatures[PaymentSent, Number], ObservableValue[Number]]() {
def call(p: CellDataFeatures[PaymentSent, Number]) = new SimpleLongProperty(p.getValue.feesPaid.amount)
})
paymentSentFeesColumn.setCellFactory(new Callback[TableColumn[PaymentSent, Number], TableCell[PaymentSent, Number]]() {
def call(pn: TableColumn[PaymentSent, Number]) = buildMoneyTableCell
})
paymentSentHashColumn.setCellValueFactory(paymentHashCellValueFactory)
paymentSentDateColumn.setCellValueFactory(paymentDateCellValueFactory)
paymentSentTable.setRowFactory(paymentRowFactory)
// init payment received
paymentReceivedTable.setItems(paymentReceivedList)
paymentReceivedList.addListener(new ListChangeListener[PaymentReceived] {
override def onChanged(c: Change[_ <: PaymentReceived]) = updateTabHeader(paymentReceivedTab, "Received", paymentReceivedList)
})
paymentReceivedAmountColumn.setCellValueFactory(new Callback[CellDataFeatures[PaymentReceived, Number], ObservableValue[Number]]() {
def call(p: CellDataFeatures[PaymentReceived, Number]) = new SimpleLongProperty(p.getValue.amount.amount)
})
paymentReceivedAmountColumn.setCellFactory(new Callback[TableColumn[PaymentReceived, Number], TableCell[PaymentReceived, Number]]() {
def call(pn: TableColumn[PaymentReceived, Number]) = buildMoneyTableCell
})
paymentReceivedHashColumn.setCellValueFactory(paymentHashCellValueFactory)
paymentReceivedDateColumn.setCellValueFactory(paymentDateCellValueFactory)
paymentReceivedTable.setRowFactory(paymentRowFactory)
// init payment relayed
paymentRelayedTable.setItems(paymentRelayedList)
paymentRelayedList.addListener(new ListChangeListener[PaymentRelayed] {
override def onChanged(c: Change[_ <: PaymentRelayed]) = updateTabHeader(paymentRelayedTab, "Relayed", paymentRelayedList)
})
paymentRelayedAmountColumn.setCellValueFactory(new Callback[CellDataFeatures[PaymentRelayed, Number], ObservableValue[Number]]() {
def call(p: CellDataFeatures[PaymentRelayed, Number]) = new SimpleLongProperty(p.getValue.amount.amount)
})
paymentRelayedAmountColumn.setCellFactory(new Callback[TableColumn[PaymentRelayed, Number], TableCell[PaymentRelayed, Number]]() {
def call(pn: TableColumn[PaymentRelayed, Number]) = buildMoneyTableCell
})
paymentRelayedFeesColumn.setCellValueFactory(new Callback[CellDataFeatures[PaymentRelayed, Number], ObservableValue[Number]]() {
def call(p: CellDataFeatures[PaymentRelayed, Number]) = new SimpleLongProperty(p.getValue.feesEarned.amount)
})
paymentRelayedFeesColumn.setCellFactory(new Callback[TableColumn[PaymentRelayed, Number], TableCell[PaymentRelayed, Number]]() {
def call(pn: TableColumn[PaymentRelayed, Number]) = buildMoneyTableCell
})
paymentRelayedHashColumn.setCellValueFactory(paymentHashCellValueFactory)
paymentRelayedDateColumn.setCellValueFactory(paymentDateCellValueFactory)
paymentRelayedTable.setRowFactory(paymentRowFactory)
}
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 <: PaymentEvent] = new Callback[CellDataFeatures[T, String], ObservableValue[String]]() {
def call(p: CellDataFeatures[T, String]) = new SimpleStringProperty(p.getValue.paymentHash.toString)
}
private def buildMoneyTableCell[T <: PaymentEvent] = new TableCell[T, Number]() {
override def updateItem(item: Number, empty: Boolean) = {
super.updateItem(item, empty)
if (item != null && !empty) setText(moneyFormatter.format(item))
}
}
private def paymentDateCellValueFactory[T <: PaymentEvent] = new Callback[CellDataFeatures[T, String], ObservableValue[String]]() {
def call(p: CellDataFeatures[T, String]) = new SimpleStringProperty(LocalDateTime.now.format(PAYMENT_DATE_FORMAT))
}
private def paymentRowFactory[T <: PaymentEvent] = 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.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] = {
@ -200,6 +322,7 @@ class MainController(val handlers: Handlers, val setup: Setup, val hostServices:
/**
* Create a row for a PeerChannel with Copy context actions.
*
* @return TableRow the created row
*/
private def setupPeerChannelContextMenu(): TableRow[ChannelAnnouncement] = {

View file

@ -51,8 +51,8 @@ class OpenChannelController(val handlers: Handlers, val stage: Stage, val setup:
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)) {
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)) {
handlers.open(host.getText, smartFunding, MilliSatoshi(pushMsat.getText.toLong))
stage.close()
}

View file

@ -28,7 +28,7 @@ class ReceivePaymentController(val handlers: Handlers, val stage: Stage, val set
@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))) {
|| ("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 {
@ -48,7 +48,7 @@ class ReceivePaymentController(val handlers: Handlers, val stage: Stage, val set
}
logger.debug(s"Final amount for payment request = $smartAmount")
if (GUIValidators.validate(amountError, "Amount must be greater than 0", smartAmount.amount > 0)
&& GUIValidators.validate(amountError, "Must be less than 4 294 967 295 mSat (~0.042 BTC)", smartAmount.amount < 4294967295L)) {
&& GUIValidators.validate(amountError, "Must be less than 4 294 967 295 msat (~0.042 BTC)", smartAmount.amount < 4294967295L)) {
handlers.getPaymentRequest(smartAmount.amount, paymentRequest)
}
} catch {

View file

@ -68,7 +68,7 @@ class SendPaymentController(val handlers: Handlers, val stage: Stage, val setup:
Try(amount.toLong) match {
case Success(amountLong) =>
if (GUIValidators.validate(paymentRequestError, "Amount must be greater than 0", amountLong > 0)
&& GUIValidators.validate(paymentRequestError, "Amount must be less than 4 294 967 295 mSat (~0.042 BTC)", amountLong < 4294967295L)) {
&& GUIValidators.validate(paymentRequestError, "Amount must be less than 4 294 967 295 msat (~0.042 BTC)", amountLong < 4294967295L)) {
Try (handlers.send(PublicKey(nodeId), BinaryData(hash), amountLong)) match {
case Success(s) => stage.close
case Failure(f) => GUIValidators.validate(paymentRequestError, s"Invalid Payment Request: ${f.getMessage}", false)

View file

@ -26,20 +26,13 @@ class SplashController(hostServices: HostServices) extends Logging {
* Start an animation when the splash window is initialized
*/
@FXML def initialize = {
val t = new HBox()
t.prefHeightProperty()
val timeline = new Timeline()
val startKF = new KeyFrame(Duration.ZERO,
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))
val endKF = new KeyFrame(Duration.millis(1000.0d),
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.getKeyFrames.addAll(startKF, endKF)
new KeyValue(imgBlurred.opacityProperty, double2Double(0), Interpolator.EASE_OUT)))
timeline.play()
}

View file

@ -1,7 +1,7 @@
package fr.acinq.eclair.payment
import akka.actor.{Actor, ActorLogging}
import fr.acinq.bitcoin.{BinaryData, Crypto}
import fr.acinq.bitcoin.{BinaryData, Crypto, MilliSatoshi}
import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC}
import fr.acinq.eclair.wire.{UnknownPaymentHash, UpdateAddHtlc}
@ -36,7 +36,7 @@ class LocalPaymentHandler extends Actor with ActorLogging {
case htlc: UpdateAddHtlc if h2r.contains(htlc.paymentHash) =>
val r = h2r(htlc.paymentHash)
sender ! CMD_FULFILL_HTLC(htlc.id, r, commit = true)
context.system.eventStream.publish(PaymentReceived(self, htlc.paymentHash))
context.system.eventStream.publish(PaymentReceived(MilliSatoshi(htlc.amountMsat), htlc.paymentHash))
context.become(run(h2r - htlc.paymentHash))
case htlc: UpdateAddHtlc =>

View file

@ -1,15 +1,16 @@
package fr.acinq.eclair.payment
import akka.actor.ActorRef
import fr.acinq.bitcoin.BinaryData
import fr.acinq.bitcoin.{BinaryData, MilliSatoshi}
/**
* Created by PM on 01/02/2017.
*/
class PaymentEvent
sealed trait PaymentEvent {
val paymentHash: BinaryData
}
//case class PaymentSent(channel: ActorRef, h: BinaryData) extends PaymentEvent
case class PaymentSent(amount: MilliSatoshi, feesPaid: MilliSatoshi, paymentHash: BinaryData) extends PaymentEvent
//case class PaymentFailed(channel: ActorRef, h: BinaryData, reason: String) extends PaymentEvent
case class PaymentRelayed(amount: MilliSatoshi, feesEarned: MilliSatoshi, paymentHash: BinaryData) extends PaymentEvent
case class PaymentReceived(channel: ActorRef, h: BinaryData) extends PaymentEvent
case class PaymentReceived(amount: MilliSatoshi, paymentHash: BinaryData) extends PaymentEvent

View file

@ -2,7 +2,7 @@ package fr.acinq.eclair.payment
import akka.actor.Status.Failure
import akka.actor.{ActorRef, FSM, LoggingFSM, Props, Status}
import fr.acinq.bitcoin.BinaryData
import fr.acinq.bitcoin.{BinaryData, MilliSatoshi}
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair._
import fr.acinq.eclair.channel.{CMD_ADD_HTLC, Register}
@ -23,7 +23,7 @@ case class PaymentFailed(paymentHash: BinaryData, error: Option[ErrorPacket]) ex
sealed trait Data
case object WaitingForRequest extends Data
case class WaitingForRoute(sender: ActorRef, c: CreatePayment, attempts: Int) extends Data
case class WaitingForComplete(sender: ActorRef, c: CreatePayment, attempts: Int, sharedSecrets: Seq[(BinaryData, PublicKey)], ignoreNodes: Set[PublicKey], ignoreChannels: Set[Long], hops: Seq[Hop]) extends Data
case class WaitingForComplete(sender: ActorRef, c: CreatePayment, cmd: CMD_ADD_HTLC, attempts: Int, sharedSecrets: Seq[(BinaryData, PublicKey)], ignoreNodes: Set[PublicKey], ignoreChannels: Set[Long], hops: Seq[Hop]) extends Data
sealed trait State
case object WAITING_FOR_REQUEST extends State
@ -52,7 +52,7 @@ class PaymentLifecycle(sourceNodeId: PublicKey, router: ActorRef, register: Acto
val firstHop = hops.head
val (cmd, sharedSecrets) = buildCommand(c.amountMsat, c.paymentHash, hops, Globals.blockCount.get().toInt)
register ! Register.ForwardShortId(firstHop.lastUpdate.shortChannelId, cmd)
goto(WAITING_FOR_PAYMENT_COMPLETE) using WaitingForComplete(s, c, attempts + 1, sharedSecrets, ignoreNodes, ignoreChannels, hops)
goto(WAITING_FOR_PAYMENT_COMPLETE) using WaitingForComplete(s, c, cmd, attempts + 1, sharedSecrets, ignoreNodes, ignoreChannels, hops)
case Event(f@Failure(t), WaitingForRoute(s, c, _)) =>
s ! PaymentFailed(c.paymentHash, error = None)
@ -68,9 +68,10 @@ class PaymentLifecycle(sourceNodeId: PublicKey, router: ActorRef, register: Acto
case Event(fulfill: UpdateFulfillHtlc, w: WaitingForComplete) =>
w.sender ! PaymentSucceeded(fulfill.paymentPreimage)
context.system.eventStream.publish(PaymentSent(MilliSatoshi(w.c.amountMsat), MilliSatoshi(w.cmd.amountMsat - w.c.amountMsat), w.cmd.paymentHash))
stop(FSM.Normal)
case Event(fail: UpdateFailHtlc, WaitingForComplete(s, c, attempts, sharedSecrets, ignoreNodes, ignoreChannels, hops)) =>
case Event(fail: UpdateFailHtlc, WaitingForComplete(s, c, _, attempts, sharedSecrets, ignoreNodes, ignoreChannels, hops)) =>
Sphinx.parseErrorPacket(fail.reason, sharedSecrets) match {
case e@Some(ErrorPacket(nodeId, failureMessage)) if nodeId == c.targetNodeId =>
// TODO: spec says: that MAY retry the payment in certain conditions, see https://github.com/lightningnetwork/lightning-rfc/blob/master/04-onion-routing.md#receiving-failure-codes

View file

@ -2,7 +2,7 @@ package fr.acinq.eclair.payment
import akka.actor.{Actor, ActorLogging, ActorRef, Props}
import fr.acinq.bitcoin.Crypto.{PrivateKey, ripemd160, sha256}
import fr.acinq.bitcoin.{BinaryData, Crypto, ScriptWitness, Transaction}
import fr.acinq.bitcoin.{BinaryData, Crypto, MilliSatoshi, ScriptWitness, Transaction}
import fr.acinq.eclair.Globals
import fr.acinq.eclair.blockchain.WatchEventSpent
import fr.acinq.eclair.channel._
@ -20,7 +20,7 @@ case class OutgoingChannel(channelId: BinaryData, channel: ActorRef, nodeAddress
sealed trait Origin
case class Local(sender: ActorRef) extends Origin
case class Relayed(upstream: UpdateAddHtlc) extends Origin
case class Relayed(upstream: ActorRef, htlcIn: UpdateAddHtlc) extends Origin
case class AddHtlcSucceeded(add: UpdateAddHtlc, origin: Origin)
case class AddHtlcFailed(add: CMD_ADD_HTLC, failure: FailureMessage)
@ -42,9 +42,7 @@ class Relayer(nodeSecret: PrivateKey, paymentHandler: ActorRef) extends Actor wi
override def receive: Receive = main(Set(), Map(), Map(), Map())
case class DownstreamHtlcId(channelId: BinaryData, htlcId: Long)
def main(channels: Set[OutgoingChannel], bindings: Map[DownstreamHtlcId, Origin], shortIds: Map[BinaryData, Long], channelUpdates: Map[Long, ChannelUpdate]): Receive = {
def main(channels: Set[OutgoingChannel], bindings: Map[UpdateAddHtlc, Origin], shortIds: Map[BinaryData, Long], channelUpdates: Map[Long, ChannelUpdate]): Receive = {
case ChannelStateChanged(channel, _, remoteNodeId, _, NORMAL, d: DATA_NORMAL) =>
import d.commitments.channelId
@ -95,7 +93,7 @@ class Relayer(nodeSecret: PrivateKey, paymentHandler: ActorRef) extends Actor wi
case _ =>
val downstream = outgoingChannel.channel
log.info(s"forwarding htlc #${add.id} to downstream=$downstream")
downstream ! CMD_ADD_HTLC(perHopPayload.amt_to_forward, add.paymentHash, perHopPayload.outgoing_cltv_value, nextPacket, upstream_opt = Some(add), commit = true)
downstream forward CMD_ADD_HTLC(perHopPayload.amt_to_forward, add.paymentHash, perHopPayload.outgoing_cltv_value, nextPacket, upstream_opt = Some(add), commit = true)
}
case Success((Attempt.Successful(DecodeResult(_, _)), nextNodeAddress, _, sharedSecret)) =>
log.warning(s"couldn't resolve downstream node address $nextNodeAddress, failing htlc #${add.id}")
@ -112,57 +110,50 @@ class Relayer(nodeSecret: PrivateKey, paymentHandler: ActorRef) extends Actor wi
case AddHtlcSucceeded(downstream, origin) =>
origin match {
case Local(_) => log.info(s"we are the origin of htlc ${downstream.channelId}/${downstream.id}")
case Relayed(upstream) => log.info(s"relayed htlc ${upstream.channelId}/${upstream.id} to ${downstream}/${downstream.id}")
case Relayed(_, upstream) => log.info(s"relayed htlc ${upstream.channelId}/${upstream.id} to ${downstream.channelId}/${downstream.id}")
}
context become main(channels, bindings + (DownstreamHtlcId(downstream.channelId, downstream.id) -> origin), shortIds, channelUpdates)
context become main(channels, bindings + (downstream -> origin), shortIds, channelUpdates)
case AddHtlcFailed(CMD_ADD_HTLC(_, _, _, onion, Some(updateAddHtlc), _), failure) if channels.exists(_.channelId == updateAddHtlc.channelId) =>
val upstream = channels.find(_.channelId == updateAddHtlc.channelId).get.channel
upstream ! CMD_FAIL_HTLC(updateAddHtlc.id, Right(failure), commit = true)
case ForwardFulfill(fulfill) =>
val downstream = DownstreamHtlcId(fulfill.channelId, fulfill.id)
bindings.get(downstream) match {
case Some(Relayed(origin)) if channels.exists(_.channelId == origin.channelId) =>
val upstream = channels.find(_.channelId == origin.channelId).get.channel
upstream ! CMD_FULFILL_HTLC(origin.id, fulfill.paymentPreimage, commit = true)
case Some(Relayed(origin)) =>
log.warning(s"origin channel ${origin.channelId} has disappeared in the meantime")
case Some(Local(origin)) =>
bindings.find(b => b._1.channelId == fulfill.channelId && b._1.id == fulfill.id) match {
case Some((htlcOut, Relayed(upstream, htlcIn))) =>
upstream ! CMD_FULFILL_HTLC(htlcIn.id, fulfill.paymentPreimage, commit = true)
context.system.eventStream.publish(PaymentRelayed(MilliSatoshi(htlcIn.amountMsat), MilliSatoshi(htlcIn.amountMsat - htlcOut.amountMsat), htlcIn.paymentHash))
context become main(channels, bindings - htlcOut, shortIds, channelUpdates)
case Some((htlcOut, Local(origin))) =>
log.info(s"we were the origin payer for htlc #${fulfill.id}")
origin ! fulfill
context become main(channels, bindings - htlcOut, shortIds, channelUpdates)
case None =>
log.warning(s"no origin found for htlc ${fulfill.channelId}/${fulfill.id}")
}
context become (main(channels, bindings - downstream, shortIds, channelUpdates))
case ForwardFail(fail) =>
val downstream = DownstreamHtlcId(fail.channelId, fail.id)
bindings.get(downstream) match {
case Some(Relayed(origin)) if channels.exists(_.channelId == origin.channelId) =>
val upstream = channels.find(_.channelId == origin.channelId).get.channel
upstream ! CMD_FAIL_HTLC(origin.id, Left(fail.reason), commit = true)
case Some(Relayed(origin)) =>
log.warning(s"origin channel ${origin.channelId} has disappeared in the meantime")
case Some(Local(origin)) =>
bindings.find(b => b._1.channelId == fail.channelId && b._1.id == fail.id) match {
case Some((htlcOut, Relayed(upstream, htlcIn))) =>
upstream ! CMD_FAIL_HTLC(htlcIn.id, Left(fail.reason), commit = true)
context become main(channels, bindings - htlcOut, shortIds, channelUpdates)
case Some((htlcOut, Local(origin))) =>
log.info(s"we were the origin payer for htlc #${fail.id}")
origin ! fail
context become main(channels, bindings - htlcOut, shortIds, channelUpdates)
case None =>
log.warning(s"no origin found for htlc ${fail.channelId}/${fail.id}")
}
context become (main(channels, bindings - downstream, shortIds, channelUpdates))
case ForwardFailMalformed(fail) =>
val downstream = DownstreamHtlcId(fail.channelId, fail.id)
bindings.get(downstream) match {
case Some(Relayed(origin)) if channels.exists(_.channelId == origin.channelId) =>
val upstream = channels.find(_.channelId == origin.channelId).get.channel
upstream ! CMD_FAIL_MALFORMED_HTLC(origin.id, fail.onionHash, fail.failureCode, commit = true)
case Some(Relayed(origin)) =>
log.warning(s"origin channel ${origin.channelId} has disappeared in the meantime")
case Some(Local(origin)) =>
bindings.find(b => b._1.channelId == fail.channelId && b._1.id == fail.id) match {
case Some((htlcOut, Relayed(upstream, htlcIn))) =>
upstream ! CMD_FAIL_MALFORMED_HTLC(htlcIn.id, fail.onionHash, fail.failureCode, commit = true)
context become main(channels, bindings - htlcOut, shortIds, channelUpdates)
case Some((htlcOut, Local(origin))) =>
log.info(s"we were the origin payer for htlc #${fail.id}")
origin ! fail
context become main(channels, bindings - htlcOut, shortIds, channelUpdates)
case None =>
log.warning(s"no origin found for htlc ${fail.channelId}/${fail.id}")
}
@ -190,30 +181,19 @@ class Relayer(nodeSecret: PrivateKey, paymentHandler: ActorRef) extends Actor wi
log.warning(s"extracted paymentHash160=$paymentHash160 from tx=${Transaction.write(tx)} (claim-htlc-timeout)")
paymentHash160
}
val downstreams = bindings.collect {
case b@(downstream, Relayed(origin)) if origin.paymentHash == sha256(extracted) =>
log.warning(s"found a match between preimage=$extracted and origin htlc=$origin")
channels.find(_.channelId == origin.channelId) match {
case Some(outgoingChannel) =>
val upstream = outgoingChannel.channel
upstream ! CMD_FULFILL_HTLC(origin.id, extracted, commit = true)
case None => log.warning(s"could not find channel for channelId=${origin.channelId}")
val htlcsOut = bindings.collect {
case b@(htlcOut, Relayed(upstream, htlcIn)) if htlcIn.paymentHash == sha256(extracted) =>
log.warning(s"found a match between preimage=$extracted and origin htlc=$htlcIn")
upstream ! CMD_FULFILL_HTLC(htlcIn.id, extracted, commit = true)
htlcOut
case b@(htlcOut, Relayed(upstream, htlcIn)) if ripemd160(htlcIn.paymentHash) == extracted =>
log.warning(s"found a match between paymentHash160=$extracted and origin htlc=$htlcIn")
upstream ! CMD_FAIL_HTLC(htlcIn.id, Right(PermanentChannelFailure), commit = true)
htlcOut
}
downstream
case b@(downstream, Relayed(origin)) if ripemd160(origin.paymentHash) == extracted =>
log.warning(s"found a match between paymentHash160=$extracted and origin htlc=$origin")
channels.find(_.channelId == origin.channelId) match {
case Some(outgoingChannel) =>
val upstream = outgoingChannel.channel
upstream ! CMD_FAIL_HTLC(origin.id, Right(PermanentChannelFailure), commit = true)
case None => log.warning(s"could not find channel for channelId=${origin.channelId}")
}
downstream
}
context become (main(channels, bindings -- downstreams, shortIds, channelUpdates))
context become main(channels, bindings -- htlcsOut, shortIds, channelUpdates)
case 'channels
=> sender ! channels
case 'channels => sender ! channels
}
}

View file

@ -85,7 +85,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
localNextHtlcId = 1,
localChanges = initialState.commitments.localChanges.copy(proposed = htlc :: Nil),
unackedMessages = initialState.commitments.unackedMessages :+ htlc)))
relayer.expectMsg(AddHtlcSucceeded(htlc, origin = Relayed(originHtlc)))
relayer.expectMsg(AddHtlcSucceeded(htlc, origin = Relayed(sender.ref, originHtlc)))
}
}

View file

@ -2,7 +2,7 @@ package fr.acinq.eclair.payment
import akka.actor.{ActorSystem, Props}
import akka.testkit.{TestKit, TestProbe}
import fr.acinq.bitcoin.BinaryData
import fr.acinq.bitcoin.{BinaryData, MilliSatoshi}
import fr.acinq.eclair.channel.CMD_FULFILL_HTLC
import fr.acinq.eclair.wire.UpdateAddHtlc
import org.junit.runner.RunWith
@ -25,9 +25,12 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike
sender.send(handler, 'genh)
val paymentHash = sender.expectMsgType[BinaryData]
sender.send(handler, UpdateAddHtlc("11" * 32, 0, 0, 0, paymentHash, ""))
val add = UpdateAddHtlc("11" * 32, 0, 42000, 0, paymentHash, "")
sender.send(handler, add)
sender.expectMsgType[CMD_FULFILL_HTLC]
eventListener.expectMsgType[PaymentReceived]
eventListener.expectMsg(PaymentReceived(MilliSatoshi(add.amountMsat), add.paymentHash))
}
}

View file

@ -2,6 +2,7 @@ package fr.acinq.eclair.payment
import akka.actor.FSM.{CurrentState, SubscribeTransitionCallBack, Transition}
import akka.testkit.{TestFSMRef, TestProbe}
import fr.acinq.bitcoin.MilliSatoshi
import fr.acinq.eclair.Globals
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.router.BaseRouterSpec
@ -45,7 +46,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
sender.send(paymentFSM, request)
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE)
awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE)
val WaitingForComplete(_, add, _, sharedSecrets, _, _, _) = paymentFSM.stateData
val WaitingForComplete(_, c, cmd, _, sharedSecrets, _, _, _) = paymentFSM.stateData
sender.send(paymentFSM, UpdateFailHtlc("00" * 32, 0, Sphinx.createErrorPacket(sharedSecrets(0)._1, TemporaryChannelFailure)))
@ -70,6 +71,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
sender.send(paymentFSM, UpdateFulfillHtlc("00" * 32, 0, "42" * 32))
sender.expectMsgType[PaymentSucceeded]
val PaymentSent(MilliSatoshi(request.amountMsat), feesPaid, request.paymentHash) = eventListener.expectMsgType[PaymentSent]
assert(feesPaid.amount > 0)
}

View file

@ -3,7 +3,7 @@ package fr.acinq.eclair.payment
import akka.actor.ActorRef
import akka.testkit.TestProbe
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{BinaryData, Crypto, OutPoint, Transaction, TxIn}
import fr.acinq.bitcoin.{BinaryData, Crypto, MilliSatoshi, OutPoint, Transaction, TxIn}
import fr.acinq.eclair.TestkitBaseClass
import fr.acinq.eclair.blockchain.WatchEventSpent
import fr.acinq.eclair.channel._
@ -101,6 +101,7 @@ class RelayerSpec extends TestkitBaseClass {
sender.expectNoMsg(1 second)
val cmd_bc = channel_bc.expectMsgType[CMD_ADD_HTLC]
paymentHandler.expectNoMsg(1 second)
assert(cmd_bc.upstream_opt === Some(add_ab))
@ -198,6 +199,9 @@ class RelayerSpec extends TestkitBaseClass {
val sender = TestProbe()
val channel_ab = TestProbe()
val channel_bc = TestProbe()
val eventListener = TestProbe()
system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent])
val add_ab = {
val (cmd, _) = buildCommand(finalAmountMsat, paymentHash, hops, currentBlockCount)
@ -211,15 +215,16 @@ class RelayerSpec extends TestkitBaseClass {
sender.send(relayer, ForwardAdd(add_ab))
val cmd_bc = channel_bc.expectMsgType[CMD_ADD_HTLC]
val add_bc = UpdateAddHtlc(channelId = channelId_bc, id = 987451, amountMsat = cmd_bc.amountMsat, expiry = cmd_bc.expiry, paymentHash = cmd_bc.paymentHash, onionRoutingPacket = cmd_bc.onion)
sender.send(relayer, AddHtlcSucceeded(add_bc, Relayed(add_ab)))
sender.send(relayer, AddHtlcSucceeded(add_bc, Relayed(channel_ab.ref, add_ab)))
// preimage is wrong, does not matter here
val fulfill_cb = UpdateFulfillHtlc(channelId = add_bc.channelId, id = add_bc.id, paymentPreimage = "00" * 32)
sender.send(relayer, ForwardFulfill(fulfill_cb))
val fulfill_ba = channel_ab.expectMsgType[CMD_FULFILL_HTLC]
assert(fulfill_ba.id === add_ab.id)
eventListener.expectMsg(PaymentRelayed(MilliSatoshi(add_ab.amountMsat), MilliSatoshi(add_ab.amountMsat - cmd_bc.amountMsat), add_ab.paymentHash))
assert(fulfill_ba.id === add_ab.id)
}
test("relay an htlc-fail") { case (relayer, paymentHandler) =>
@ -239,7 +244,7 @@ class RelayerSpec extends TestkitBaseClass {
sender.send(relayer, ForwardAdd(add_ab))
val cmd_bc = channel_bc.expectMsgType[CMD_ADD_HTLC]
val add_bc = UpdateAddHtlc(channelId = channelId_bc, id = 987451, amountMsat = cmd_bc.amountMsat, expiry = cmd_bc.expiry, paymentHash = cmd_bc.paymentHash, onionRoutingPacket = cmd_bc.onion)
sender.send(relayer, AddHtlcSucceeded(add_bc, Relayed(add_ab)))
sender.send(relayer, AddHtlcSucceeded(add_bc, Relayed(channel_ab.ref, add_ab)))
val fail_cb = UpdateFailHtlc(channelId = add_bc.channelId, id = add_bc.id, reason = Sphinx.createErrorPacket(BinaryData("01" * 32), TemporaryChannelFailure))
sender.send(relayer, ForwardFail(fail_cb))
@ -266,7 +271,7 @@ class RelayerSpec extends TestkitBaseClass {
sender.send(relayer, ForwardAdd(add_ab))
val cmd_bc = channel_bc.expectMsgType[CMD_ADD_HTLC]
val add_bc = UpdateAddHtlc(channelId = channelId_bc, id = 987451, amountMsat = cmd_bc.amountMsat, expiry = cmd_bc.expiry, paymentHash = cmd_bc.paymentHash, onionRoutingPacket = cmd_bc.onion)
sender.send(relayer, AddHtlcSucceeded(add_bc, Relayed(add_ab)))
sender.send(relayer, AddHtlcSucceeded(add_bc, Relayed(channel_ab.ref, add_ab)))
// actual test starts here
val tx = Transaction(version = 0, txIn = TxIn(outPoint = OutPoint("22" * 32, 0), signatureScript = "", sequence = 0, witness = Scripts.witnessHtlcSuccess("11" * 70, "22" * 70, paymentPreimage, "33" * 130)) :: Nil, txOut = Nil, lockTime = 0)
@ -294,7 +299,7 @@ class RelayerSpec extends TestkitBaseClass {
sender.send(relayer, ForwardAdd(add_ab))
val cmd_bc = channel_bc.expectMsgType[CMD_ADD_HTLC]
val add_bc = UpdateAddHtlc(channelId = channelId_bc, id = 987451, amountMsat = cmd_bc.amountMsat, expiry = cmd_bc.expiry, paymentHash = cmd_bc.paymentHash, onionRoutingPacket = cmd_bc.onion)
sender.send(relayer, AddHtlcSucceeded(add_bc, Relayed(add_ab)))
sender.send(relayer, AddHtlcSucceeded(add_bc, Relayed(channel_ab.ref, add_ab)))
// actual test starts here
val tx = Transaction(version = 0, txIn = TxIn(outPoint = OutPoint("22" * 32, 0), signatureScript = "", sequence = 0, witness = Scripts.witnessClaimHtlcSuccessFromCommitTx("11" * 70, paymentPreimage, "33" * 130)) :: Nil, txOut = Nil, lockTime = 0)