mirror of
https://github.com/ACINQ/eclair.git
synced 2025-03-13 11:35:47 +01:00
Handling optional amount in a Payment Request (#241)
* Enable generation of a payment request without amount The amount field in a `PaymentRequest` was already optional but eclair did not permit the generation of such a request. Added a new `receive` service with no required amount field. In the GUI, the parsing of the amount field and its conversion to `MilliSatoshi` are reworked to better handle decimals. * (gui) Amount's can be overriden when sending a payment request The amount of a payment request can be changed and it is up to the receiving node to accept or deny the payment according to its implementation. This also enables the user to pay through the GUI a payment request where the amount has not been set, such as a donation. The amount is still required! The description field has also been added in the GUI. It is empty if the description has not been set. * (gui) Properly parse amounts from open channel form * (gui) added optional `lightning:` scheme to payment request
This commit is contained in:
parent
ffc4172e70
commit
44e7c3ba31
22 changed files with 287 additions and 156 deletions
|
@ -131,6 +131,7 @@ java -Declair.datadir=/tmp/node1 -jar eclair-node-gui-<version>-<commit_id>.jar
|
|||
channel | channelId | retrieve detailed information about a given channel
|
||||
allnodes | | list all known nodes
|
||||
allchannels | | list all known channels
|
||||
receive | description | generate a payment request without a required amount (can be useful for donations)
|
||||
receive | amountMsat, description | generate a payment request for a given amount
|
||||
send | amountMsat, paymentHash, nodeId | send a payment to a lightning node
|
||||
send | paymentRequest | send a payment to a lightning node using a BOLT11 payment request
|
||||
|
|
|
@ -95,8 +95,10 @@ trait Service extends Logging {
|
|||
(router ? 'nodes).mapTo[Iterable[NodeAnnouncement]].map(_.map(_.nodeId))
|
||||
case JsonRPCBody(_, _, "allchannels", _) =>
|
||||
(router ? 'channels).mapTo[Iterable[ChannelAnnouncement]].map(_.map(c => ChannelInfo(c.shortChannelId.toHexString, c.nodeId1, c.nodeId2)))
|
||||
case JsonRPCBody(_, _, "receive", JString(description) :: Nil) =>
|
||||
(paymentHandler ? ReceivePayment(None, description)).mapTo[PaymentRequest].map(PaymentRequest.write)
|
||||
case JsonRPCBody(_, _, "receive", JInt(amountMsat) :: JString(description) :: Nil) =>
|
||||
(paymentHandler ? ReceivePayment(MilliSatoshi(amountMsat.toLong), description)).mapTo[PaymentRequest].map(PaymentRequest.write)
|
||||
(paymentHandler ? ReceivePayment(Some(MilliSatoshi(amountMsat.toLong)), description)).mapTo[PaymentRequest].map(PaymentRequest.write)
|
||||
case JsonRPCBody(_, _, "send", JInt(amountMsat) :: JString(paymentHash) :: JString(nodeId) :: Nil) =>
|
||||
(paymentInitiator ? SendPayment(amountMsat.toLong, paymentHash, PublicKey(nodeId))).mapTo[PaymentResult]
|
||||
case JsonRPCBody(_, _, "send", JString(paymentRequest) :: rest) =>
|
||||
|
|
|
@ -17,14 +17,14 @@ class LocalPaymentHandler(nodeParams: NodeParams) extends Actor with ActorLoggin
|
|||
|
||||
def run(h2r: Map[BinaryData, (BinaryData, PaymentRequest)]): Receive = {
|
||||
|
||||
case ReceivePayment(amount, desc) =>
|
||||
case ReceivePayment(amount_opt, desc) =>
|
||||
Try {
|
||||
val paymentPreimage = randomBytes(32)
|
||||
val paymentHash = Crypto.sha256(paymentPreimage)
|
||||
(paymentPreimage, paymentHash, PaymentRequest(nodeParams.chainHash, Some(amount), paymentHash, nodeParams.privateKey, desc))
|
||||
(paymentPreimage, paymentHash, PaymentRequest(nodeParams.chainHash, amount_opt, paymentHash, nodeParams.privateKey, desc))
|
||||
} match {
|
||||
case Success((r, h, pr)) =>
|
||||
log.debug(s"generated payment request=${PaymentRequest.write(pr)} from amount=$amount")
|
||||
log.debug(s"generated payment request=${PaymentRequest.write(pr)} from amount=$amount_opt")
|
||||
sender ! pr
|
||||
context.become(run(h2r + (h -> (r, pr))))
|
||||
case Failure(t) =>
|
||||
|
|
|
@ -12,7 +12,7 @@ import fr.acinq.eclair.wire._
|
|||
import scodec.Attempt
|
||||
|
||||
// @formatter:off
|
||||
case class ReceivePayment(amountMsat: MilliSatoshi, description: String)
|
||||
case class ReceivePayment(amountMsat_opt: Option[MilliSatoshi], description: String)
|
||||
case class SendPayment(amountMsat: Long, paymentHash: BinaryData, targetNodeId: PublicKey, minFinalCltvExpiry: Long = PaymentLifecycle.defaultMinFinalCltvExpiry, maxAttempts: Int = 5)
|
||||
|
||||
sealed trait PaymentResult
|
||||
|
|
|
@ -26,7 +26,7 @@ import scala.util.Try
|
|||
*/
|
||||
case class PaymentRequest(prefix: String, amount: Option[MilliSatoshi], timestamp: Long, nodeId: PublicKey, tags: List[PaymentRequest.Tag], signature: BinaryData) {
|
||||
|
||||
amount.map(a => require(a > MilliSatoshi(0) && a <= PaymentRequest.maxAmount, s"amount is not valid"))
|
||||
amount.map(a => require(a.amount > 0 && a.amount <= PaymentRequest.MAX_AMOUNT.amount, s"amount is not valid"))
|
||||
require(tags.collect { case _: PaymentRequest.PaymentHashTag => {} }.size == 1, "there must be exactly one payment hash tag")
|
||||
require(tags.collect { case PaymentRequest.DescriptionTag(_) | PaymentRequest.DescriptionHashTag(_) => {} }.size == 1, "there must be exactly one description tag or one description hash tag")
|
||||
|
||||
|
@ -102,7 +102,7 @@ case class PaymentRequest(prefix: String, amount: Option[MilliSatoshi], timestam
|
|||
object PaymentRequest {
|
||||
|
||||
// https://github.com/lightningnetwork/lightning-rfc/blob/master/02-peer-protocol.md#adding-an-htlc-update_add_htlc
|
||||
val maxAmount = MilliSatoshi(4294967296L)
|
||||
val MAX_AMOUNT = MilliSatoshi(4294967296L)
|
||||
|
||||
def apply(chainHash: BinaryData, amount: Option[MilliSatoshi], paymentHash: BinaryData, privateKey: PrivateKey,
|
||||
description: String, fallbackAddress: Option[String] = None, expirySeconds: Option[Long] = None,
|
||||
|
|
|
@ -83,7 +83,7 @@ class FuzzySpec extends TestkitBaseClass with StateTestsHelperMethods with Loggi
|
|||
if (stopping) {
|
||||
context stop self
|
||||
} else {
|
||||
paymentHandler ! ReceivePayment(MilliSatoshi(requiredAmount), "One coffee")
|
||||
paymentHandler ! ReceivePayment(Some(MilliSatoshi(requiredAmount)), "One coffee")
|
||||
context become waitingForPaymentRequest
|
||||
}
|
||||
|
||||
|
|
|
@ -255,7 +255,7 @@ class BasicIntegrationSpvSpec extends TestKit(ActorSystem("test")) with FunSuite
|
|||
val sender = TestProbe()
|
||||
val amountMsat = MilliSatoshi(4200000)
|
||||
// first we retrieve a payment hash from D
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
// then we make the actual payment
|
||||
sender.send(nodes("A").paymentInitiator,
|
||||
|
@ -273,7 +273,7 @@ class BasicIntegrationSpvSpec extends TestKit(ActorSystem("test")) with FunSuite
|
|||
sender.send(nodes("C").relayer, channelUpdateCD)
|
||||
// first we retrieve a payment hash from D
|
||||
val amountMsat = MilliSatoshi(4200000)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
// then we make the actual payment
|
||||
val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey)
|
||||
|
@ -292,7 +292,7 @@ class BasicIntegrationSpvSpec extends TestKit(ActorSystem("test")) with FunSuite
|
|||
val sender = TestProbe()
|
||||
// first we retrieve a payment hash from D
|
||||
val amountMsat = MilliSatoshi(300000000L)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
// then we make the payment (C-D has a smaller capacity than A-B and B-C)
|
||||
val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey)
|
||||
|
@ -317,7 +317,7 @@ class BasicIntegrationSpvSpec extends TestKit(ActorSystem("test")) with FunSuite
|
|||
val sender = TestProbe()
|
||||
// first we retrieve a payment hash from D for 2 mBTC
|
||||
val amountMsat = MilliSatoshi(200000000L)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
|
||||
// A send payment of only 1 mBTC
|
||||
|
@ -335,7 +335,7 @@ class BasicIntegrationSpvSpec extends TestKit(ActorSystem("test")) with FunSuite
|
|||
val sender = TestProbe()
|
||||
// first we retrieve a payment hash from D for 2 mBTC
|
||||
val amountMsat = MilliSatoshi(200000000L)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
|
||||
// A send payment of 6 mBTC
|
||||
|
@ -353,7 +353,7 @@ class BasicIntegrationSpvSpec extends TestKit(ActorSystem("test")) with FunSuite
|
|||
val sender = TestProbe()
|
||||
// first we retrieve a payment hash from D for 2 mBTC
|
||||
val amountMsat = MilliSatoshi(200000000L)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
|
||||
// A send payment of 3 mBTC, more than asked but it should still be accepted
|
||||
|
|
|
@ -217,7 +217,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
|||
val sender = TestProbe()
|
||||
val amountMsat = MilliSatoshi(4200000)
|
||||
// first we retrieve a payment hash from D
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
// then we make the actual payment
|
||||
sender.send(nodes("A").paymentInitiator,
|
||||
|
@ -235,7 +235,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
|||
sender.send(nodes("C").relayer, channelUpdateCD)
|
||||
// first we retrieve a payment hash from D
|
||||
val amountMsat = MilliSatoshi(4200000)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
// then we make the actual payment
|
||||
val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey)
|
||||
|
@ -254,7 +254,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
|||
val sender = TestProbe()
|
||||
// first we retrieve a payment hash from D
|
||||
val amountMsat = MilliSatoshi(300000000L)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
// then we make the payment (C-D has a smaller capacity than A-B and B-C)
|
||||
val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey)
|
||||
|
@ -279,7 +279,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
|||
val sender = TestProbe()
|
||||
// first we retrieve a payment hash from D for 2 mBTC
|
||||
val amountMsat = MilliSatoshi(200000000L)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
|
||||
// A send payment of only 1 mBTC
|
||||
|
@ -297,7 +297,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
|||
val sender = TestProbe()
|
||||
// first we retrieve a payment hash from D for 2 mBTC
|
||||
val amountMsat = MilliSatoshi(200000000L)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
|
||||
// A send payment of 6 mBTC
|
||||
|
@ -315,7 +315,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
|||
val sender = TestProbe()
|
||||
// first we retrieve a payment hash from D for 2 mBTC
|
||||
val amountMsat = MilliSatoshi(200000000L)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
|
||||
// A send payment of 3 mBTC, more than asked but it should still be accepted
|
||||
|
@ -574,7 +574,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
|||
awaitCond(Globals.blockCount.get() == currentBlockCount, max = 20 seconds, interval = 1 second)
|
||||
// first we send 3 mBTC to F so that it has a balance
|
||||
val amountMsat = MilliSatoshi(300000000L)
|
||||
sender.send(nodes("F5").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
sender.send(nodes("F5").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
val sendReq = SendPayment(300000000L, pr.paymentHash, nodes("F5").nodeParams.privateKey.publicKey)
|
||||
sender.send(nodes("A").paymentInitiator, sendReq)
|
||||
|
@ -592,7 +592,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
|||
val localCommitTxF = sender.expectMsgType[DATA_NORMAL].commitments.localCommit.publishableTxs
|
||||
// we now send some more money to F so that it creates a new commitment tx
|
||||
val amountMsat1 = MilliSatoshi(100000000L)
|
||||
sender.send(nodes("F5").paymentHandler, ReceivePayment(amountMsat1, "1 coffee"))
|
||||
sender.send(nodes("F5").paymentHandler, ReceivePayment(Some(amountMsat1), "1 coffee"))
|
||||
val pr1 = sender.expectMsgType[PaymentRequest]
|
||||
val sendReq1 = SendPayment(100000000L, pr1.paymentHash, nodes("F5").nodeParams.privateKey.publicKey)
|
||||
sender.send(nodes("A").paymentInitiator, sendReq1)
|
||||
|
|
|
@ -24,7 +24,7 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike
|
|||
system.eventStream.subscribe(eventListener.ref, classOf[PaymentReceived])
|
||||
|
||||
val amountMsat = MilliSatoshi(42000)
|
||||
sender.send(handler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
sender.send(handler, ReceivePayment(Some(amountMsat), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
|
||||
val add = UpdateAddHtlc("11" * 32, 0, amountMsat.amount, pr.paymentHash, 0, "")
|
||||
|
@ -40,23 +40,34 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike
|
|||
system.eventStream.subscribe(eventListener.ref, classOf[PaymentReceived])
|
||||
|
||||
// negative amount should fail
|
||||
sender.send(handler, ReceivePayment(MilliSatoshi(-50), "1 coffee"))
|
||||
sender.send(handler, ReceivePayment(Some(MilliSatoshi(-50)), "1 coffee"))
|
||||
val negativeError = sender.expectMsgType[Failure]
|
||||
assert(negativeError.cause.getMessage.contains("amount is not valid"))
|
||||
|
||||
// amount = 0 should fail
|
||||
sender.send(handler, ReceivePayment(MilliSatoshi(0), "1 coffee"))
|
||||
sender.send(handler, ReceivePayment(Some(MilliSatoshi(0)), "1 coffee"))
|
||||
val zeroError = sender.expectMsgType[Failure]
|
||||
assert(zeroError.cause.getMessage.contains("amount is not valid"))
|
||||
|
||||
// large amount should fail (> 42.95 mBTC)
|
||||
sender.send(handler, ReceivePayment(Satoshi(1) + PaymentRequest.maxAmount, "1 coffee"))
|
||||
sender.send(handler, ReceivePayment(Some(Satoshi(1) + PaymentRequest.MAX_AMOUNT), "1 coffee"))
|
||||
val largeAmountError = sender.expectMsgType[Failure]
|
||||
assert(largeAmountError.cause.getMessage.contains("amount is not valid"))
|
||||
|
||||
// success with 1 mBTC
|
||||
sender.send(handler, ReceivePayment(MilliSatoshi(100000000L), "1 coffee"))
|
||||
sender.send(handler, ReceivePayment(Some(MilliSatoshi(100000000L)), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
assert(pr.amount == Some(MilliSatoshi(100000000L)) && pr.nodeId.toString == Alice.nodeParams.privateKey.publicKey.toString)
|
||||
}
|
||||
|
||||
test("Payment request generation should succeed when the amount is not set") {
|
||||
val handler = system.actorOf(LocalPaymentHandler.props(Alice.nodeParams))
|
||||
val sender = TestProbe()
|
||||
val eventListener = TestProbe()
|
||||
system.eventStream.subscribe(eventListener.ref, classOf[PaymentReceived])
|
||||
|
||||
sender.send(handler, ReceivePayment(None, "This is a donation PR"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
assert(pr.amount == None && pr.nodeId.toString == Alice.nodeParams.privateKey.publicKey.toString)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -207,4 +207,12 @@
|
|||
}
|
||||
.button.copy-clipboard:pressed {
|
||||
-fx-background-color: rgb(220,222,225);
|
||||
}
|
||||
|
||||
/* --------- send modal ---------- */
|
||||
|
||||
.text-field.description-text, .text-area.description-text {
|
||||
-fx-background-color: rgb(235,235,235);
|
||||
-fx-text-fill: rgb(100,100,100);
|
||||
-fx-padding: 4px;
|
||||
}
|
|
@ -30,8 +30,7 @@
|
|||
</VBox>
|
||||
<TextField fx:id="host" prefWidth="313.0" promptText="pubkey@host:port"
|
||||
GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.rowIndex="0"/>
|
||||
<Label fx:id="hostError" opacity="0.0" styleClass="text-error, text-error-downward" text="Generic Invalid URI"
|
||||
mouseTransparent="true"
|
||||
<Label fx:id="hostError" styleClass="text-error, text-error-downward" mouseTransparent="true"
|
||||
GridPane.rowIndex="0" GridPane.columnIndex="1" GridPane.columnSpan="2"/>
|
||||
|
||||
<CheckBox fx:id="simpleConnection" mnemonicParsing="false" text="Simple connection (no channel)"
|
||||
|
@ -55,8 +54,7 @@
|
|||
</FXCollections>
|
||||
</items>
|
||||
</ComboBox>
|
||||
<Label fx:id="fundingSatoshisError" opacity="0.0" styleClass="text-error, text-error-downward"
|
||||
text="Generic Invalid Funding"
|
||||
<Label fx:id="fundingSatoshisError" styleClass="text-error, text-error-downward"
|
||||
GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.rowIndex="2"/>
|
||||
|
||||
<Label styleClass="text-muted" text="Optional Parameters" wrapText="true" GridPane.columnIndex="0"
|
||||
|
@ -71,10 +69,9 @@
|
|||
wrapText="true"/>
|
||||
</children>
|
||||
</VBox>
|
||||
<TextField fx:id="pushMsat" prefWidth="313.0" GridPane.columnIndex="1" GridPane.rowIndex="4"/>
|
||||
<Label fx:id="pushMsatError" opacity="0.0" styleClass="text-error, text-error-downward"
|
||||
text="Generic Invalid Push" GridPane.columnSpan="2"
|
||||
GridPane.columnIndex="1" GridPane.rowIndex="4"/>
|
||||
<TextField fx:id="pushMsatField" prefWidth="313.0" GridPane.columnIndex="1" GridPane.rowIndex="4"/>
|
||||
<Label fx:id="pushMsatError" styleClass="text-error, text-error-downward"
|
||||
GridPane.columnIndex="1" GridPane.rowIndex="4" GridPane.columnSpan="2"/>
|
||||
<CheckBox fx:id="publicChannel" mnemonicParsing="true" selected="true" styleClass="text-sm"
|
||||
text="Public Channel"
|
||||
GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.rowIndex="5"/>
|
||||
|
|
|
@ -19,11 +19,12 @@
|
|||
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
|
||||
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
|
||||
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
|
||||
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
|
||||
</rowConstraints>
|
||||
<children>
|
||||
<VBox alignment="TOP_RIGHT" GridPane.rowIndex="0">
|
||||
<children>
|
||||
<Label styleClass="text-strong" text="Amount to receive"/>
|
||||
<Label styleClass="text-strong" text="Optional amount to receive"/>
|
||||
<Label styleClass="label-description" wrapText="true" textAlignment="RIGHT"
|
||||
text="Maximum of ~0.042 BTC"/>
|
||||
</children>
|
||||
|
@ -52,11 +53,18 @@
|
|||
<TextArea fx:id="description" GridPane.columnIndex="1" GridPane.rowIndex="1" GridPane.columnSpan="2"
|
||||
wrapText="true" prefHeight="50.0"/>
|
||||
|
||||
<VBox alignment="TOP_RIGHT" GridPane.rowIndex="2" GridPane.columnIndex="0">
|
||||
<children>
|
||||
<Label styleClass="text-strong" text="Add the lightning: prefix"/>
|
||||
</children>
|
||||
</VBox>
|
||||
<CheckBox fx:id="prependPrefixCheckbox" selected="false" GridPane.rowIndex="2" GridPane.columnIndex="1"/>
|
||||
|
||||
<Button defaultButton="true" mnemonicParsing="false" onAction="#handleGenerate" prefHeight="29.0"
|
||||
prefWidth="95.0" text="Generate" GridPane.columnIndex="1" GridPane.rowIndex="2"/>
|
||||
prefWidth="95.0" text="Generate" GridPane.columnIndex="1" GridPane.rowIndex="3"/>
|
||||
<Button cancelButton="true" mnemonicParsing="false" onAction="#handleClose" styleClass="cancel"
|
||||
text="Close"
|
||||
GridPane.columnIndex="2" GridPane.halignment="RIGHT" GridPane.rowIndex="2" opacity="0"
|
||||
GridPane.columnIndex="2" GridPane.halignment="RIGHT" GridPane.rowIndex="3" opacity="0"
|
||||
focusTraversable="false"/>
|
||||
|
||||
</children>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<?import javafx.scene.layout.*?>
|
||||
<?import java.lang.String?>
|
||||
<?import java.net.URL?>
|
||||
<GridPane fx:id="nodeId" prefWidth="450.0" prefHeight="450.0" xmlns="http://javafx.com/javafx/8"
|
||||
<GridPane fx:id="nodeId" prefWidth="550.0" prefHeight="550.0" xmlns="http://javafx.com/javafx/8"
|
||||
xmlns:fx="http://javafx.com/fxml/1">
|
||||
<columnConstraints>
|
||||
<ColumnConstraints halignment="LEFT" hgrow="SOMETIMES" minWidth="10.0" prefWidth="110.0"/>
|
||||
|
@ -19,33 +19,47 @@
|
|||
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
|
||||
<RowConstraints minHeight="1.0" vgrow="SOMETIMES"/>
|
||||
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
|
||||
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
|
||||
</rowConstraints>
|
||||
<children>
|
||||
<Label styleClass="text-strong" text="Enter a Payment Request below" GridPane.columnSpan="2"
|
||||
GridPane.valignment="TOP"/>
|
||||
<TextArea fx:id="paymentRequest" minHeight="150.0" prefHeight="150.0" styleClass="ta" wrapText="true"
|
||||
GridPane.columnSpan="2" GridPane.rowIndex="1" GridPane.vgrow="ALWAYS"/>
|
||||
<Label fx:id="paymentRequestError" opacity="0.0" text="Generic Invalid Payment Request" mouseTransparent="true"
|
||||
styleClass="text-error" GridPane.columnSpan="2" GridPane.rowIndex="2"/>
|
||||
|
||||
<Label styleClass="text-muted" text="Amount (msat)" GridPane.halignment="RIGHT" GridPane.rowIndex="3"/>
|
||||
<TextField fx:id="amountField" focusTraversable="false" editable="false" styleClass="noteditable" text="0"
|
||||
<Label fx:id="paymentRequestError" mouseTransparent="true" styleClass="text-error" GridPane.columnSpan="2"
|
||||
GridPane.rowIndex="2"/>
|
||||
|
||||
<Label text="Node Id" GridPane.halignment="RIGHT" GridPane.rowIndex="3"/>
|
||||
<TextField fx:id="nodeIdField" focusTraversable="false" editable="false" styleClass="description-text"
|
||||
text="N/A"
|
||||
GridPane.columnIndex="1" GridPane.rowIndex="3"/>
|
||||
|
||||
<Label styleClass="text-muted" text="Node Id" GridPane.halignment="RIGHT" GridPane.rowIndex="4"/>
|
||||
<TextField fx:id="nodeIdField" focusTraversable="false" editable="false" styleClass="noteditable" text="N/A"
|
||||
<Label text="Payment Hash" GridPane.halignment="RIGHT" GridPane.rowIndex="4"/>
|
||||
<TextField fx:id="paymentHashField" focusTraversable="false" editable="false" styleClass="description-text"
|
||||
text="N/A"
|
||||
GridPane.columnIndex="1" GridPane.rowIndex="4"/>
|
||||
|
||||
<Label styleClass="text-muted" text="hash" GridPane.halignment="RIGHT" GridPane.rowIndex="5"/>
|
||||
<TextField fx:id="hashField" focusTraversable="false" editable="false" styleClass="noteditable" text="N/A"
|
||||
GridPane.columnIndex="1" GridPane.rowIndex="5"/>
|
||||
<Label fx:id="descriptionLabel" text="Description" GridPane.halignment="RIGHT" GridPane.valignment="BASELINE"
|
||||
GridPane.rowIndex="5"/>
|
||||
<TextArea fx:id="descriptionField" focusTraversable="false" editable="false"
|
||||
styleClass="noteditable, description-text" text="N/A"
|
||||
prefHeight="80.0" maxHeight="80.0" GridPane.columnIndex="1" GridPane.rowIndex="5"/>
|
||||
|
||||
<Separator GridPane.columnSpan="2" GridPane.rowIndex="6"/>
|
||||
<Label text="Amount (msat)" GridPane.halignment="RIGHT" GridPane.valignment="BASELINE" GridPane.rowIndex="6"/>
|
||||
<VBox GridPane.columnIndex="1" GridPane.rowIndex="6">
|
||||
<children>
|
||||
<TextField fx:id="amountField"/>
|
||||
<Label fx:id="amountFieldError" mouseTransparent="true" styleClass="text-error"/>
|
||||
</children>
|
||||
</VBox>
|
||||
|
||||
<Separator GridPane.columnSpan="2" GridPane.rowIndex="7"/>
|
||||
|
||||
<Button fx:id="sendButton" defaultButton="true" mnemonicParsing="false" onAction="#handleSend" text="Send"
|
||||
GridPane.rowIndex="7"/>
|
||||
GridPane.rowIndex="8"/>
|
||||
<Button cancelButton="true" mnemonicParsing="false" onAction="#handleClose" styleClass="cancel" text="Cancel"
|
||||
GridPane.columnIndex="1" GridPane.halignment="RIGHT" GridPane.rowIndex="7"/>
|
||||
GridPane.columnIndex="1" GridPane.halignment="RIGHT" GridPane.rowIndex="8"/>
|
||||
</children>
|
||||
<styleClass>
|
||||
<String fx:value="grid"/>
|
||||
|
@ -53,5 +67,6 @@
|
|||
</styleClass>
|
||||
<stylesheets>
|
||||
<URL value="@../commons/globals.css"/>
|
||||
<URL value="@../main/main.css"/>
|
||||
</stylesheets>
|
||||
</GridPane>
|
||||
|
|
|
@ -14,7 +14,7 @@ import fr.acinq.eclair.blockchain.bitcoind.zmq.ZMQActor.{ZMQConnected, ZMQDiscon
|
|||
import fr.acinq.eclair.blockchain.electrum.ElectrumClient.{ElectrumConnected, ElectrumDisconnected}
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.gui.controllers._
|
||||
import fr.acinq.eclair.gui.utils.CoinFormat
|
||||
import fr.acinq.eclair.gui.utils.CoinUtils
|
||||
import fr.acinq.eclair.payment.{PaymentReceived, PaymentRelayed, PaymentSent}
|
||||
import fr.acinq.eclair.router._
|
||||
import fr.acinq.eclair.wire.NodeAnnouncement
|
||||
|
@ -52,8 +52,8 @@ class GUIUpdater(mainController: MainController) extends Actor with ActorLogging
|
|||
|
||||
def updateBalance(channelPaneController: ChannelPaneController, commitments: Commitments) = {
|
||||
val spec = commitments.localCommit.spec
|
||||
channelPaneController.capacity.setText(s"${CoinFormat.MILLI_BTC_FORMAT.format(millisatoshi2millibtc(MilliSatoshi(spec.totalFunds)).amount)}")
|
||||
channelPaneController.amountUs.setText(s"${CoinFormat.MILLI_BTC_FORMAT.format(millisatoshi2millibtc(MilliSatoshi(spec.toLocalMsat)).amount)}")
|
||||
channelPaneController.capacity.setText(s"${CoinUtils.MILLI_BTC_FORMAT.format(millisatoshi2millibtc(MilliSatoshi(spec.totalFunds)).amount)}")
|
||||
channelPaneController.amountUs.setText(s"${CoinUtils.MILLI_BTC_FORMAT.format(millisatoshi2millibtc(MilliSatoshi(spec.toLocalMsat)).amount)}")
|
||||
channelPaneController.balanceBar.setProgress(spec.toLocalMsat.toDouble / spec.totalFunds)
|
||||
}
|
||||
|
||||
|
|
|
@ -86,9 +86,9 @@ class Handlers(fKit: Future[Kit])(implicit ec: ExecutionContext = ExecutionConte
|
|||
}
|
||||
}
|
||||
|
||||
def receive(amountMsat: MilliSatoshi, description: String): Future[String] = for {
|
||||
def receive(amountMsat_opt: Option[MilliSatoshi], description: String): Future[String] = for {
|
||||
kit <- fKit
|
||||
res <- (kit.paymentHandler ? ReceivePayment(amountMsat, description)).mapTo[PaymentRequest].map(PaymentRequest.write(_))
|
||||
res <- (kit.paymentHandler ? ReceivePayment(amountMsat_opt, description)).mapTo[PaymentRequest].map(PaymentRequest.write(_))
|
||||
} yield res
|
||||
|
||||
|
||||
|
|
|
@ -7,13 +7,14 @@ import javafx.fxml.FXML
|
|||
import javafx.scene.control._
|
||||
import javafx.stage.Stage
|
||||
|
||||
import fr.acinq.bitcoin.{MilliSatoshi, Satoshi}
|
||||
import fr.acinq.eclair.channel.{Channel, ChannelFlags}
|
||||
import fr.acinq.eclair.gui.Handlers
|
||||
import fr.acinq.eclair.gui.utils.GUIValidators
|
||||
import fr.acinq.eclair.gui.utils.{CoinUtils, GUIValidators}
|
||||
import fr.acinq.eclair.io.Switchboard.NewChannel
|
||||
import grizzled.slf4j.Logging
|
||||
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
/**
|
||||
* Created by DPA on 23/09/2016.
|
||||
*/
|
||||
|
@ -24,7 +25,7 @@ class OpenChannelController(val handlers: Handlers, val stage: Stage) extends Lo
|
|||
@FXML var simpleConnection: CheckBox = _
|
||||
@FXML var fundingSatoshis: TextField = _
|
||||
@FXML var fundingSatoshisError: Label = _
|
||||
@FXML var pushMsat: TextField = _
|
||||
@FXML var pushMsatField: TextField = _
|
||||
@FXML var pushMsatError: Label = _
|
||||
@FXML var publicChannel: CheckBox = _
|
||||
@FXML var unit: ComboBox[String] = _
|
||||
|
@ -36,45 +37,61 @@ class OpenChannelController(val handlers: Handlers, val stage: Stage) extends Lo
|
|||
simpleConnection.selectedProperty.addListener(new ChangeListener[Boolean] {
|
||||
override def changed(observable: ObservableValue[_ <: Boolean], oldValue: Boolean, newValue: Boolean) = {
|
||||
fundingSatoshis.setDisable(newValue)
|
||||
pushMsat.setDisable(newValue)
|
||||
pushMsatField.setDisable(newValue)
|
||||
unit.setDisable(newValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@FXML def handleOpen(event: ActionEvent) = {
|
||||
clearErrors()
|
||||
if (GUIValidators.validate(host.getText, hostError, "Please use a valid url (pubkey@host:port)", GUIValidators.hostRegex)) {
|
||||
if (simpleConnection.isSelected) {
|
||||
handlers.open(host.getText, None)
|
||||
stage.close
|
||||
} else {
|
||||
if (GUIValidators.validate(fundingSatoshis.getText, fundingSatoshisError, "Funding must be numeric", GUIValidators.amountRegex)
|
||||
&& GUIValidators.validate(fundingSatoshisError, "Funding must be greater than 0", fundingSatoshis.getText.toLong > 0)) {
|
||||
val rawFunding = fundingSatoshis.getText.toLong
|
||||
val smartFunding = unit.getValue match {
|
||||
case "milliBTC" => Satoshi(rawFunding * 100000L)
|
||||
case "Satoshi" => Satoshi(rawFunding)
|
||||
case "milliSatoshi" => Satoshi(rawFunding / 1000L)
|
||||
}
|
||||
if (GUIValidators.validate(fundingSatoshisError, f"Capacity must be less than ${Channel.MAX_FUNDING_SATOSHIS}%d satoshis", smartFunding.toLong < Channel.MAX_FUNDING_SATOSHIS)) {
|
||||
if (!pushMsat.getText.isEmpty) {
|
||||
// pushMsat is optional, so we validate field only if it isn't empty
|
||||
import fr.acinq.bitcoin._
|
||||
if (GUIValidators.validate(pushMsat.getText, pushMsatError, "Push msat must be numeric", GUIValidators.amountRegex)
|
||||
&& GUIValidators.validate(pushMsatError, "Push msat must be less or equal to capacity", pushMsat.getText.toLong <= satoshi2millisatoshi(smartFunding).amount)) {
|
||||
val channelFlags = if (publicChannel.isSelected) ChannelFlags.AnnounceChannel else ChannelFlags.Empty
|
||||
handlers.open(host.getText, Some(NewChannel(smartFunding, MilliSatoshi(pushMsat.getText.toLong), Some(channelFlags))))
|
||||
stage.close
|
||||
}
|
||||
} else {
|
||||
handlers.open(host.getText, Some(NewChannel(smartFunding, MilliSatoshi(0), None)))
|
||||
stage.close
|
||||
import fr.acinq.bitcoin._
|
||||
fundingSatoshis.getText match {
|
||||
case GUIValidators.amountDecRegex(_*) =>
|
||||
Try(CoinUtils.convertStringAmountToSat(fundingSatoshis.getText, unit.getValue)) match {
|
||||
case Success(capacitySat) if capacitySat.amount < 0 =>
|
||||
fundingSatoshisError.setText("Capacity must be greater than 0")
|
||||
case Success(capacitySat) if capacitySat.amount >= Channel.MAX_FUNDING_SATOSHIS =>
|
||||
fundingSatoshisError.setText(f"Capacity must be less than ${Channel.MAX_FUNDING_SATOSHIS}%,d sat")
|
||||
case Success(capacitySat) =>
|
||||
pushMsatField.getText match {
|
||||
case "" =>
|
||||
handlers.open(host.getText, Some(NewChannel(capacitySat, MilliSatoshi(0), None)))
|
||||
stage close()
|
||||
case GUIValidators.amountRegex(_*) =>
|
||||
Try(MilliSatoshi(pushMsatField.getText.toLong)) match {
|
||||
case Success(pushMsat) if pushMsat.amount > satoshi2millisatoshi(capacitySat).amount =>
|
||||
pushMsatError.setText("Push must be less or equal to capacity")
|
||||
case Success(pushMsat) =>
|
||||
val channelFlags = if (publicChannel.isSelected) ChannelFlags.AnnounceChannel else ChannelFlags.Empty
|
||||
handlers.open(host.getText, Some(NewChannel(capacitySat, pushMsat, Some(channelFlags))))
|
||||
stage close()
|
||||
case Failure(t) =>
|
||||
logger.error("Could not parse push amount", t)
|
||||
pushMsatError.setText("Push amount is not valid")
|
||||
}
|
||||
case _ => pushMsatError.setText("Push amount is not valid")
|
||||
}
|
||||
case Failure(t) =>
|
||||
logger.error("Could not parse capacity amount", t)
|
||||
fundingSatoshisError.setText("Capacity is not valid")
|
||||
}
|
||||
}
|
||||
case _ => fundingSatoshisError.setText("Capacity is not valid")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def clearErrors() = {
|
||||
hostError.setText("")
|
||||
fundingSatoshisError.setText("")
|
||||
pushMsatError.setText("")
|
||||
}
|
||||
|
||||
@FXML def handleClose(event: ActionEvent) = stage.close
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ package fr.acinq.eclair.gui.controllers
|
|||
import javafx.application.Platform
|
||||
import javafx.event.ActionEvent
|
||||
import javafx.fxml.FXML
|
||||
import javafx.scene.control.{ComboBox, Label, TextArea, TextField}
|
||||
import javafx.scene.control._
|
||||
import javafx.scene.image.{ImageView, WritableImage}
|
||||
import javafx.scene.layout.GridPane
|
||||
import javafx.scene.paint.Color
|
||||
|
@ -14,10 +14,11 @@ import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
|
|||
import com.google.zxing.{BarcodeFormat, EncodeHintType}
|
||||
import fr.acinq.bitcoin.MilliSatoshi
|
||||
import fr.acinq.eclair.gui.Handlers
|
||||
import fr.acinq.eclair.gui.utils.{ContextMenuUtils, GUIValidators}
|
||||
import fr.acinq.eclair.gui.utils.{CoinUtils, ContextMenuUtils, GUIValidators}
|
||||
import fr.acinq.eclair.payment.PaymentRequest
|
||||
import grizzled.slf4j.Logging
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
/**
|
||||
|
@ -29,6 +30,7 @@ class ReceivePaymentController(val handlers: Handlers, val stage: Stage) extends
|
|||
@FXML var amountError: Label = _
|
||||
@FXML var unit: ComboBox[String] = _
|
||||
@FXML var description: TextArea = _
|
||||
@FXML var prependPrefixCheckbox: CheckBox = _
|
||||
|
||||
@FXML var resultBox: GridPane = _
|
||||
// the content of this field is generated and readonly
|
||||
|
@ -43,52 +45,77 @@ class ReceivePaymentController(val handlers: Handlers, val stage: Stage) extends
|
|||
|
||||
@FXML def handleCopyInvoice(event: ActionEvent) = ContextMenuUtils.copyToClipboard(paymentRequestTextArea.getText)
|
||||
|
||||
/**
|
||||
* Generates a payment request from the amount/unit set in form. Displays an error if the generation fails.
|
||||
* Amount field content must obviously be numeric. It is also validated against minimal/maximal HTLC values.
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
@FXML def handleGenerate(event: ActionEvent) = {
|
||||
if ((("milliBTC".equals(unit.getValue) || "Satoshi".equals(unit.getValue))
|
||||
&& GUIValidators.validate(amount.getText, amountError, "Amount must be numeric", GUIValidators.amountDecRegex))
|
||||
|| ("milliSatoshi".equals(unit.getValue) && GUIValidators.validate(amount.getText, amountError, "Amount must be numeric (no decimal msat)", GUIValidators.amountRegex))) {
|
||||
try {
|
||||
val Array(parsedInt, parsedDec) = if (amount.getText.contains(".")) amount.getText.split("\\.") else Array(amount.getText, "0")
|
||||
val amountDec = parsedDec.length match {
|
||||
case 0 => "000"
|
||||
case 1 => parsedDec.concat("00")
|
||||
case 2 => parsedDec.concat("0")
|
||||
case 3 => parsedDec
|
||||
case _ =>
|
||||
// amount has too many decimals, regex validation has failed somehow
|
||||
throw new NumberFormatException("incorrect amount")
|
||||
clearError()
|
||||
amount.getText match {
|
||||
case "" => createPaymentRequest(None)
|
||||
case GUIValidators.amountDecRegex(_*) =>
|
||||
Try(CoinUtils.convertStringAmountToMsat(amount.getText, unit.getValue)) match {
|
||||
case Success(amountMsat) if amountMsat.amount < 0 =>
|
||||
handleError("Amount must be greater than 0")
|
||||
case Success(amountMsat) if amountMsat.amount >= PaymentRequest.MAX_AMOUNT.amount =>
|
||||
handleError(f"Amount must be less than ${PaymentRequest.MAX_AMOUNT.amount}%,d msat (~${PaymentRequest.MAX_AMOUNT.amount / 1e11}%.3f BTC)")
|
||||
case Failure(_) =>
|
||||
handleError("Amount is incorrect")
|
||||
case Success(amountMsat) => createPaymentRequest(Some(amountMsat))
|
||||
}
|
||||
val smartAmount = unit.getValue match {
|
||||
case "milliBTC" => MilliSatoshi(parsedInt.toLong * 100000000L + amountDec.toLong * 100000L)
|
||||
case "Satoshi" => MilliSatoshi(parsedInt.toLong * 1000L + amountDec.toLong)
|
||||
case "milliSatoshi" => MilliSatoshi(amount.getText.toLong)
|
||||
}
|
||||
if (GUIValidators.validate(amountError, "Amount must be greater than 0", smartAmount.amount > 0)
|
||||
&& GUIValidators.validate(amountError, f"Amount must be less than ${PaymentRequest.maxAmount.amount}%,d msat (~${PaymentRequest.maxAmount.amount / 1e11}%.3f BTC)", smartAmount < PaymentRequest.maxAmount)
|
||||
&& GUIValidators.validate(amountError, "Description is too long, max 256 chars.", description.getText().size < 256)) {
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
handlers.receive(smartAmount, description.getText) onComplete {
|
||||
case Success(s) =>
|
||||
Try(createQRCode(s)) match {
|
||||
case Success(wImage) => displayPaymentRequest(s, Some(wImage))
|
||||
case Failure(t) => displayPaymentRequest(s, None)
|
||||
}
|
||||
case Failure(t) => Platform.runLater(new Runnable {
|
||||
def run = GUIValidators.validate(amountError, "The payment request could not be generated", false)
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
case e: NumberFormatException =>
|
||||
logger.debug(s"Could not generate payment request for amount = ${amount.getText}")
|
||||
paymentRequestTextArea.setText("")
|
||||
amountError.setText("Amount is incorrect")
|
||||
amountError.setOpacity(1)
|
||||
}
|
||||
case _ => handleError("Amount must be a number")
|
||||
}
|
||||
}
|
||||
|
||||
private def displayPaymentRequest(pr: String, image: Option[WritableImage]) = Platform.runLater(new Runnable {
|
||||
/**
|
||||
* Display error message
|
||||
*
|
||||
* @param message
|
||||
*/
|
||||
private def handleError(message: String): Unit = {
|
||||
paymentRequestTextArea.setText("")
|
||||
amountError.setText(message)
|
||||
amountError.setOpacity(1)
|
||||
}
|
||||
|
||||
private def clearError(): Unit = {
|
||||
paymentRequestTextArea.setText("")
|
||||
amountError.setText("")
|
||||
amountError.setOpacity(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask eclair-core to create a Payment Request. If successful a QR code is generated and displayed, otherwise
|
||||
* an error message is shown.
|
||||
*
|
||||
* @param amount_opt optional amount of the payment request, in millisatoshi
|
||||
*/
|
||||
private def createPaymentRequest(amount_opt: Option[MilliSatoshi]) = {
|
||||
logger.debug(s"generate payment request for amount_opt=${amount_opt.getOrElse("N/A")} description=${description.getText()}")
|
||||
handlers.receive(amount_opt, description.getText) onComplete {
|
||||
case Success(s) =>
|
||||
val pr = if (prependPrefixCheckbox.isSelected) s"lightning:$s" else s
|
||||
Try(createQRCode(pr)) match {
|
||||
case Success(wImage) => displayPaymentRequestQR(pr, Some(wImage))
|
||||
case Failure(t) => displayPaymentRequestQR(pr, None)
|
||||
}
|
||||
case Failure(t) =>
|
||||
logger.error("Could not generate payment request", t)
|
||||
Platform.runLater(new Runnable {
|
||||
def run = GUIValidators.validate(amountError, "The payment request could not be generated", false)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a QR Code from a QR code image.
|
||||
*
|
||||
* @param pr payment request described by the QR code
|
||||
* @param image QR code source image
|
||||
*/
|
||||
private def displayPaymentRequestQR(pr: String, image: Option[WritableImage]) = Platform.runLater(new Runnable {
|
||||
def run = {
|
||||
paymentRequestTextArea.setText(pr)
|
||||
if ("".equals(pr)) {
|
||||
|
|
|
@ -8,8 +8,8 @@ import javafx.scene.input.KeyCode.{ENTER, TAB}
|
|||
import javafx.scene.input.KeyEvent
|
||||
import javafx.stage.Stage
|
||||
|
||||
import fr.acinq.bitcoin.MilliSatoshi
|
||||
import fr.acinq.eclair.gui.Handlers
|
||||
import fr.acinq.eclair.gui.utils.GUIValidators
|
||||
import fr.acinq.eclair.payment.PaymentRequest
|
||||
import grizzled.slf4j.Logging
|
||||
|
||||
|
@ -24,12 +24,15 @@ class SendPaymentController(val handlers: Handlers, val stage: Stage) extends Lo
|
|||
@FXML var paymentRequest: TextArea = _
|
||||
@FXML var paymentRequestError: Label = _
|
||||
@FXML var nodeIdField: TextField = _
|
||||
@FXML var descriptionLabel: Label = _
|
||||
@FXML var descriptionField: TextArea = _
|
||||
@FXML var amountField: TextField = _
|
||||
@FXML var hashField: TextField = _
|
||||
@FXML var amountFieldError: Label = _
|
||||
@FXML var paymentHashField: TextField = _
|
||||
@FXML var sendButton: Button = _
|
||||
|
||||
@FXML def initialize(): Unit = {
|
||||
// ENTER or TAB events in the paymentRequest textarea insted fire or focus sendButton
|
||||
// ENTER or TAB events in the paymentRequest textarea instead fire or focus sendButton
|
||||
paymentRequest.addEventFilter(KeyEvent.KEY_PRESSED, new EventHandler[KeyEvent] {
|
||||
def handle(event: KeyEvent) = {
|
||||
event.getCode match {
|
||||
|
@ -45,33 +48,43 @@ class SendPaymentController(val handlers: Handlers, val stage: Stage) extends Lo
|
|||
})
|
||||
paymentRequest.textProperty.addListener(new ChangeListener[String] {
|
||||
def changed(observable: ObservableValue[_ <: String], oldValue: String, newValue: String) = {
|
||||
clearErrors()
|
||||
Try(PaymentRequest.read(paymentRequest.getText)) match {
|
||||
case Success(pr) =>
|
||||
pr.amount.foreach(amount => amountField.setText(amount.amount.toString))
|
||||
pr.description match {
|
||||
case Left(s) => descriptionField.setText(s)
|
||||
case Right(hash) =>
|
||||
descriptionLabel.setText("Description's Hash")
|
||||
descriptionField.setText(hash.toString())
|
||||
}
|
||||
nodeIdField.setText(pr.nodeId.toString)
|
||||
hashField.setText(pr.paymentHash.toString)
|
||||
paymentHashField.setText(pr.paymentHash.toString)
|
||||
case Failure(f) =>
|
||||
GUIValidators.validate(paymentRequestError, "Please use a valid payment request", false)
|
||||
amountField.setText("0")
|
||||
nodeIdField.setText("N/A")
|
||||
hashField.setText("N/A")
|
||||
paymentRequestError.setText("Could not read this payment request")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@FXML def handleSend(event: ActionEvent) = {
|
||||
Try(PaymentRequest.read(paymentRequest.getText)) match {
|
||||
case Success(pr) =>
|
||||
Try(handlers.send(pr.nodeId, pr.paymentHash, pr.amount.get.amount, pr.minFinalCltvExpiry)) match {
|
||||
(Try(MilliSatoshi(amountField.getText().toLong)), Try(PaymentRequest.read(paymentRequest.getText))) match {
|
||||
case (Success(amountMsat), Success(pr)) =>
|
||||
Try(handlers.send(pr.nodeId, pr.paymentHash, amountMsat.amount, pr.minFinalCltvExpiry)) match {
|
||||
case Success(s) => stage.close
|
||||
case Failure(f) => GUIValidators.validate(paymentRequestError, s"Invalid Payment Request: ${f.getMessage}", false)
|
||||
case Failure(f) => paymentRequestError.setText(s"Invalid Payment Request: ${f.getMessage}")
|
||||
}
|
||||
case Failure(f) => GUIValidators.validate(paymentRequestError, "cannot parse payment request", false)
|
||||
case (_, Success(_)) => amountFieldError.setText("Invalid amount")
|
||||
case (_, Failure(f)) => paymentRequestError.setText("Could not read this payment request")
|
||||
}
|
||||
}
|
||||
|
||||
@FXML def handleClose(event: ActionEvent) = {
|
||||
stage.close
|
||||
}
|
||||
|
||||
private def clearErrors(): Unit = {
|
||||
paymentRequestError.setText("")
|
||||
amountFieldError.setText("")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,8 +19,8 @@ class SendPaymentStage(handlers: Handlers) extends Stage() with Logging {
|
|||
setTitle("Send a Payment Request")
|
||||
setMinWidth(450)
|
||||
setWidth(450)
|
||||
setMinHeight(450)
|
||||
setHeight(450)
|
||||
setMinHeight(550)
|
||||
setHeight(550)
|
||||
|
||||
// get fxml/controller
|
||||
val receivePayment = new FXMLLoader(getClass.getResource("/gui/modals/sendPayment.fxml"))
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
package fr.acinq.eclair.gui.utils
|
||||
|
||||
import java.text.DecimalFormat
|
||||
|
||||
object CoinFormat {
|
||||
/**
|
||||
* Always 5 decimals
|
||||
*/
|
||||
val MILLI_BTC_PATTERN = "###,##0.00000"
|
||||
|
||||
/**
|
||||
* Localized formatter for milli-bitcoin amounts. Uses `MILLI_BTC_PATTERN`.
|
||||
*/
|
||||
val MILLI_BTC_FORMAT: DecimalFormat = new DecimalFormat(MILLI_BTC_PATTERN)
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package fr.acinq.eclair.gui.utils
|
||||
|
||||
import java.text.DecimalFormat
|
||||
|
||||
import fr.acinq.bitcoin.{MilliSatoshi, Satoshi}
|
||||
import grizzled.slf4j.Logging
|
||||
import fr.acinq.bitcoin._
|
||||
|
||||
object CoinUtils extends Logging {
|
||||
/**
|
||||
* Always 5 decimals
|
||||
*/
|
||||
val MILLI_BTC_PATTERN = "###,##0.00000"
|
||||
|
||||
/**
|
||||
* Localized formatter for milli-bitcoin amounts. Uses `MILLI_BTC_PATTERN`.
|
||||
*/
|
||||
val MILLI_BTC_FORMAT: DecimalFormat = new DecimalFormat(MILLI_BTC_PATTERN)
|
||||
|
||||
val MILLI_SATOSHI_LABEL = "milliSatoshi"
|
||||
val SATOSHI_LABEL = "satoshi"
|
||||
val MILLI_BTC_LABEL = "milliBTC"
|
||||
|
||||
/**
|
||||
* Converts a string amount denominated in a bitcoin unit to a Millisatoshi amount. The amount might be truncated if
|
||||
* it has too many decimals because MilliSatoshi only accepts Long amount.
|
||||
*
|
||||
* @param amount numeric String, can be decimal.
|
||||
* @param unit bitcoin unit, can be milliSatoshi, Satoshi or milliBTC.
|
||||
* @return amount as a MilliSatoshi object.
|
||||
* @throws NumberFormatException if the amount parameter is not numeric.
|
||||
* @throws IllegalArgumentException if the unit is not equals to milliSatoshi, Satoshi or milliBTC.
|
||||
*/
|
||||
def convertStringAmountToMsat(amount: String, unit: String): MilliSatoshi = {
|
||||
val amountDecimal = BigDecimal(amount)
|
||||
logger.debug(s"amount=$amountDecimal with unit=$unit")
|
||||
unit match {
|
||||
case MILLI_SATOSHI_LABEL => MilliSatoshi(amountDecimal.longValue())
|
||||
case SATOSHI_LABEL => MilliSatoshi((amountDecimal * 1000).longValue())
|
||||
case MILLI_BTC_LABEL => MilliSatoshi((amountDecimal * 1000 * 100000).longValue())
|
||||
case _ => throw new IllegalArgumentException("unknown unit")
|
||||
}
|
||||
}
|
||||
|
||||
def convertStringAmountToSat(amount: String, unit: String): Satoshi =
|
||||
millisatoshi2satoshi(CoinUtils.convertStringAmountToMsat(amount, unit))
|
||||
}
|
|
@ -10,7 +10,7 @@ import scala.util.matching.Regex
|
|||
object GUIValidators {
|
||||
val hostRegex = """([a-fA-F0-9]{66})@([a-zA-Z0-9:\.\-_]+):([0-9]+)""".r
|
||||
val amountRegex = """\d+""".r
|
||||
val amountDecRegex = """(\d+)|(\d+\.[\d]{1,3})""".r // accepts 3 decimals at most
|
||||
val amountDecRegex = """(\d+)|(\d+\.[\d]{1,})""".r
|
||||
val paymentRequestRegex =
|
||||
"""([a-zA-Z0-9]+):([a-zA-Z0-9]+):([a-zA-Z0-9]+)""".r
|
||||
val hexRegex = """[0-9a-fA-F]+""".r
|
||||
|
|
Loading…
Add table
Reference in a new issue