mirror of
https://github.com/ACINQ/eclair.git
synced 2025-02-20 13:34:35 +01:00
Merge branch 'master' into peer-ratelimit
This commit is contained in:
commit
ad24fdb94b
31 changed files with 849 additions and 296 deletions
|
@ -103,7 +103,7 @@ name | description
|
|||
eclair.bitcoind.rpcpassword | Bitcoin Core RPC password | bar
|
||||
eclair.bitcoind.zmqblock | Bitcoin Core ZMQ block address | "tcp://127.0.0.1:29000"
|
||||
eclair.bitcoind.zmqtx | Bitcoin Core ZMQ tx address | "tcp://127.0.0.1:29000"
|
||||
eclair.gui.unit | Unit in which amounts are displayed (possible values: msat, sat, mbtc, btc) | btc
|
||||
eclair.gui.unit | Unit in which amounts are displayed (possible values: msat, sat, bits, mbtc, btc) | btc
|
||||
|
||||
Quotes are not required unless the value contains special characters. Full syntax guide [here](https://github.com/lightbend/config/blob/master/HOCON.md).
|
||||
|
||||
|
@ -155,8 +155,10 @@ java -Dlogback.configurationFile=/path/to/logback-custom.xml -jar eclair-node-gu
|
|||
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
|
||||
receive | amountMsat, description, expirySeconds | generate a payment request for a given amount that expires after given number of seconds
|
||||
checkinvoice | paymentRequest | returns node, amount and payment hash in an invoice/paymentRequest
|
||||
findroute | paymentRequest|paymentRequest, amountMsat|nodeId, amountMsat | given a payment request or nodeID and an amount checks if there is a valid payment route returns JSON with attempts, nodes and channels of route
|
||||
parseinvoice | paymentRequest | returns node, amount and payment hash in a payment request
|
||||
findroute | paymentRequest | returns nodes and channels of the route for this payment request if there is any
|
||||
findroute | paymentRequest, amountMsat | returns nodes and channels of the route for this payment request and amount, if there is any
|
||||
findroute | nodeId, amountMsat | returns nodes and channels of the route to the nodeId, if there is any
|
||||
send | amountMsat, paymentHash, nodeId | send a payment to a lightning node
|
||||
send | paymentRequest | send a payment to a lightning node using a BOLT11 payment request
|
||||
send | paymentRequest, amountMsat | send a payment to a lightning node using a BOLT11 payment request and a custom amount
|
||||
|
|
|
@ -21,7 +21,7 @@ _eclair-cli()
|
|||
*)
|
||||
# works fine, but is too slow at the moment.
|
||||
# allopts=$($eclaircli help 2>&1 | awk '$1 ~ /^"/ { sub(/,/, ""); print $1}' | sed 's/[":]//g')
|
||||
allopts="connect open peers channels channel allnodes allchannels allupdates receive send close checkpayment getinfo help"
|
||||
allopts="connect open peers channels channel allnodes allchannels allupdates receive send close audit findroute updaterelayfee parseinvoice forceclose networkfees channelstats checkpayment getinfo help"
|
||||
|
||||
if ! [[ " $allopts " =~ " $prev " ]]; then # prevent double arguments
|
||||
if [[ -z "$cur" || "$cur" =~ ^[a-z] ]]; then
|
||||
|
|
|
@ -94,4 +94,6 @@ eclair {
|
|||
max-pending-payment-requests = 10000000
|
||||
max-payment-fee = 0.03 // max total fee for outgoing payments, in percentage: sending a payment will not be attempted if the cheapest route found is more expensive than that
|
||||
min-funding-satoshis = 100000
|
||||
|
||||
autoprobe-count = 0 // number of parallel tasks that send test payments to detect invalid channels
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
package fr.acinq.eclair
|
||||
|
||||
import java.text.DecimalFormat
|
||||
import java.text.{DecimalFormat, NumberFormat}
|
||||
|
||||
import fr.acinq.bitcoin.{Btc, BtcAmount, MilliBtc, MilliSatoshi, Satoshi}
|
||||
import grizzled.slf4j.Logging
|
||||
|
@ -93,19 +93,44 @@ case object BtcUnit extends CoinUnit {
|
|||
|
||||
object CoinUtils extends Logging {
|
||||
|
||||
val COIN_PATTERN = "###,###,###,##0.###########"
|
||||
var COIN_FORMAT = new DecimalFormat(COIN_PATTERN)
|
||||
// msat pattern, no decimals allowed
|
||||
val MILLI_SAT_PATTERN = "#,###,###,###,###,###,##0"
|
||||
|
||||
def setCoinPattern(pattern: String) = {
|
||||
// sat pattern decimals are optional
|
||||
val SAT_PATTERN = "#,###,###,###,###,##0.###"
|
||||
|
||||
// bits pattern always shows 2 decimals (msat optional)
|
||||
val BITS_PATTERN = "##,###,###,###,##0.00###"
|
||||
|
||||
// milli btc pattern always shows 5 decimals (msat optional)
|
||||
val MILLI_BTC_PATTERN = "##,###,###,##0.00000###"
|
||||
|
||||
// btc pattern always shows 8 decimals (msat optional). This is the default pattern.
|
||||
val BTC_PATTERN = "##,###,##0.00000000###"
|
||||
|
||||
var COIN_FORMAT: NumberFormat = new DecimalFormat(BTC_PATTERN)
|
||||
|
||||
def setCoinPattern(pattern: String): Unit = {
|
||||
COIN_FORMAT = new DecimalFormat(pattern)
|
||||
}
|
||||
|
||||
def getPatternFromUnit(unit: CoinUnit): String = {
|
||||
unit match {
|
||||
case MSatUnit => MILLI_SAT_PATTERN
|
||||
case SatUnit => SAT_PATTERN
|
||||
case BitUnit => BITS_PATTERN
|
||||
case MBtcUnit => MILLI_BTC_PATTERN
|
||||
case BtcUnit => BTC_PATTERN
|
||||
case _ => throw new IllegalArgumentException("unhandled unit")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, milliBTC, BTC.
|
||||
* @param unit bitcoin unit, can be milliSatoshi, Satoshi, Bits, milliBTC, BTC.
|
||||
* @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.
|
||||
|
@ -132,7 +157,7 @@ object CoinUtils extends Logging {
|
|||
fr.acinq.bitcoin.millisatoshi2satoshi(CoinUtils.convertStringAmountToMsat(amount, unit))
|
||||
|
||||
/**
|
||||
* Only BtcUnit, MBtcUnit, SatUnit and MSatUnit codes or label are supported.
|
||||
* Only BtcUnit, MBtcUnit, BitUnit, SatUnit and MSatUnit codes or label are supported.
|
||||
* @param unit
|
||||
* @return
|
||||
*/
|
||||
|
@ -199,13 +224,13 @@ object CoinUtils extends Logging {
|
|||
}
|
||||
|
||||
/**
|
||||
* Converts the amount to the user preferred unit and returns the Long value.
|
||||
* Converts the amount to the user preferred unit and returns the BigDecimal value.
|
||||
* This method is useful to feed numeric text input without formatting.
|
||||
*
|
||||
* Returns -1 if the given amount can not be converted.
|
||||
*
|
||||
* @param amount BtcAmount
|
||||
* @return Long value of the BtcAmount
|
||||
* @return BigDecimal value of the BtcAmount
|
||||
*/
|
||||
def rawAmountInUnit(amount: BtcAmount, unit: CoinUnit): BigDecimal = Try(convertAmountToGUIUnit(amount, unit) match {
|
||||
case a: BtcAmountGUILossless => BigDecimal(a.amount_msat) / a.unit.factorToMsat
|
||||
|
|
|
@ -83,6 +83,7 @@ case class NodeParams(metrics: MetricRegistry = new MetricRegistry(),
|
|||
maxPendingPaymentRequests: Int,
|
||||
maxPaymentFee: Double,
|
||||
minFundingSatoshis: Long) {
|
||||
|
||||
val privateKey = keyManager.nodeKey.privateKey
|
||||
val nodeId = keyManager.nodeId
|
||||
}
|
||||
|
@ -172,6 +173,9 @@ object NodeParams {
|
|||
val offeredCLTV = config.getInt("to-remote-delay-blocks")
|
||||
require(maxToLocalCLTV <= Channel.MAX_TO_SELF_DELAY && offeredCLTV <= Channel.MAX_TO_SELF_DELAY, s"CLTV delay values too high, max is ${Channel.MAX_TO_SELF_DELAY}")
|
||||
|
||||
val nodeAlias = config.getString("node-alias")
|
||||
require(nodeAlias.getBytes("UTF-8").length <= 32, "invalid alias, too long (max allowed 32 bytes)")
|
||||
|
||||
val overrideFeatures: Map[PublicKey, (BinaryData, BinaryData)] = config.getConfigList("override-features").map { e =>
|
||||
val p = PublicKey(e.getString("nodeid"))
|
||||
val gf = BinaryData(e.getString("global-features"))
|
||||
|
@ -181,7 +185,7 @@ object NodeParams {
|
|||
|
||||
NodeParams(
|
||||
keyManager = keyManager,
|
||||
alias = config.getString("node-alias").take(32),
|
||||
alias = nodeAlias,
|
||||
color = Color(color.data(0), color.data(1), color.data(2)),
|
||||
publicAddresses = config.getStringList("server.public-ips").toList.map(ip => new InetSocketAddress(InetAddresses.forString(ip), config.getInt("server.port"))),
|
||||
globalFeatures = BinaryData(config.getString("global-features")),
|
||||
|
|
|
@ -259,6 +259,7 @@ class Setup(datadir: File,
|
|||
switchboard = system.actorOf(SimpleSupervisor.props(Switchboard.props(nodeParams, authenticator, watcher, router, relayer, wallet), "switchboard", SupervisorStrategy.Resume))
|
||||
server = system.actorOf(SimpleSupervisor.props(Server.props(nodeParams, authenticator, new InetSocketAddress(config.getString("server.binding-ip"), config.getInt("server.port")), Some(tcpBound)), "server", SupervisorStrategy.Restart))
|
||||
paymentInitiator = system.actorOf(SimpleSupervisor.props(PaymentInitiator.props(nodeParams.nodeId, router, register), "payment-initiator", SupervisorStrategy.Restart))
|
||||
_ = for (i <- 0 until config.getInt("autoprobe-count")) yield system.actorOf(SimpleSupervisor.props(Autoprobe.props(nodeParams, router, paymentInitiator), s"payment-autoprobe-$i", SupervisorStrategy.Restart))
|
||||
|
||||
kit = Kit(
|
||||
nodeParams = nodeParams,
|
||||
|
|
|
@ -23,6 +23,7 @@ import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, PublicKey, Scalar}
|
|||
import fr.acinq.bitcoin.{BinaryData, MilliSatoshi, OutPoint, Transaction}
|
||||
import fr.acinq.eclair.channel.State
|
||||
import fr.acinq.eclair.crypto.ShaChain
|
||||
import fr.acinq.eclair.payment.PaymentRequest
|
||||
import fr.acinq.eclair.router.RouteResponse
|
||||
import fr.acinq.eclair.transactions.Direction
|
||||
import fr.acinq.eclair.transactions.Transactions.{InputInfo, TransactionWithInputInfo}
|
||||
|
@ -132,3 +133,17 @@ class DirectionSerializer extends CustomSerializer[Direction](format => ({ null
|
|||
case d: Direction => JString(d.toString)
|
||||
}))
|
||||
|
||||
class PaymentRequestSerializer extends CustomSerializer[PaymentRequest](format => ({ null },{
|
||||
case p: PaymentRequest => JObject(JField("prefix", JString(p.prefix)) ::
|
||||
JField("amount", if (p.amount.isDefined) JLong(p.amount.get.toLong) else JNull) ::
|
||||
JField("timestamp", JLong(p.timestamp)) ::
|
||||
JField("nodeId", JString(p.nodeId.toString())) ::
|
||||
JField("description", JString(p.description match {
|
||||
case Left(l) => l.toString()
|
||||
case Right(r) => r.toString()
|
||||
})) ::
|
||||
JField("paymentHash", JString(p.paymentHash.toString())) ::
|
||||
JField("expiry", if (p.expiry.isDefined) JLong(p.expiry.get) else JNull) ::
|
||||
JField("minFinalCltvExpiry", if (p.minFinalCltvExpiry.isDefined) JLong(p.minFinalCltvExpiry.get) else JNull) ::
|
||||
Nil)
|
||||
}))
|
||||
|
|
|
@ -74,7 +74,7 @@ trait Service extends Logging {
|
|||
def scheduler: Scheduler
|
||||
|
||||
implicit val serialization = jackson.Serialization
|
||||
implicit val formats = org.json4s.DefaultFormats + new BinaryDataSerializer + new UInt64Serializer + new MilliSatoshiSerializer + new ShortChannelIdSerializer + new StateSerializer + new ShaChainSerializer + new PublicKeySerializer + new PrivateKeySerializer + new ScalarSerializer + new PointSerializer + new TransactionSerializer + new TransactionWithInputInfoSerializer + new InetSocketAddressSerializer + new OutPointSerializer + new OutPointKeySerializer + new InputInfoSerializer + new ColorSerializer + new RouteResponseSerializer + new ThrowableSerializer + new FailureMessageSerializer + new NodeAddressSerializer + new DirectionSerializer
|
||||
implicit val formats = org.json4s.DefaultFormats + new BinaryDataSerializer + new UInt64Serializer + new MilliSatoshiSerializer + new ShortChannelIdSerializer + new StateSerializer + new ShaChainSerializer + new PublicKeySerializer + new PrivateKeySerializer + new ScalarSerializer + new PointSerializer + new TransactionSerializer + new TransactionWithInputInfoSerializer + new InetSocketAddressSerializer + new OutPointSerializer + new OutPointKeySerializer + new InputInfoSerializer + new ColorSerializer + new RouteResponseSerializer + new ThrowableSerializer + new FailureMessageSerializer + new NodeAddressSerializer + new DirectionSerializer +new PaymentRequestSerializer
|
||||
implicit val timeout = Timeout(60 seconds)
|
||||
implicit val shouldWritePretty: ShouldWritePretty = ShouldWritePretty.True
|
||||
|
||||
|
@ -234,7 +234,8 @@ trait Service extends Logging {
|
|||
case _ => reject(UnknownParamsRejection(req.id, "[description] or [amount, description] or [amount, description, expiryDuration]"))
|
||||
}
|
||||
|
||||
case "checkinvoice" => req.params match {
|
||||
// checkinvoice deprecated.
|
||||
case "parseinvoice" | "checkinvoice" => req.params match {
|
||||
case JString(paymentRequest) :: Nil => Try(PaymentRequest.read(paymentRequest)) match {
|
||||
case Success(pr) => completeRpc(req.id,pr)
|
||||
case Failure(t) => reject(RpcValidationRejection(req.id, s"invalid payment request ${t.getMessage}"))
|
||||
|
@ -384,8 +385,10 @@ trait Service extends Logging {
|
|||
"allupdates (nodeId): list all channels updates for this nodeId",
|
||||
"receive (amountMsat, description): generate a payment request for a given amount",
|
||||
"receive (amountMsat, description, expirySeconds): generate a payment request for a given amount with a description and a number of seconds till it expires",
|
||||
"checkinvoice (paymentRequest): returns node, amount and payment hash in an invoice/paymentRequest",
|
||||
"findroute (paymentRequest|nodeId): given a payment request or nodeID checks if there is a valid payment route returns JSON with attempts, nodes and channels of route",
|
||||
"parseinvoice (paymentRequest): returns node, amount and payment hash in a payment request",
|
||||
"findroute (paymentRequest): returns nodes and channels of the route if there is any",
|
||||
"findroute (paymentRequest, amountMsat): returns nodes and channels of the route if there is any",
|
||||
"findroute (nodeId, amountMsat): returns nodes and channels of the route if there is any",
|
||||
"send (amountMsat, paymentHash, nodeId): send a payment to a lightning node",
|
||||
"send (paymentRequest): send a payment to a lightning node using a BOLT11 payment request",
|
||||
"send (paymentRequest, amountMsat): send a payment to a lightning node using a BOLT11 payment request and a custom amount",
|
||||
|
|
|
@ -86,7 +86,8 @@ class ElectrumClient(serverAddress: InetSocketAddress, ssl: SSL)(implicit val ec
|
|||
})
|
||||
|
||||
// Start the client.
|
||||
log.info(s"connecting to $serverAddress")
|
||||
log.info("connecting to server={}", serverAddress)
|
||||
|
||||
val channelOpenFuture = b.connect(serverAddress.getHostName, serverAddress.getPort)
|
||||
|
||||
def close() = {
|
||||
|
@ -95,7 +96,7 @@ class ElectrumClient(serverAddress: InetSocketAddress, ssl: SSL)(implicit val ec
|
|||
}
|
||||
|
||||
def errorHandler(t: Throwable) = {
|
||||
log.info(s"connection error (reason=${t.getMessage})")
|
||||
log.info("server={} connection error (reason={})", serverAddress, t.getMessage)
|
||||
close()
|
||||
}
|
||||
|
||||
|
@ -109,7 +110,7 @@ class ElectrumClient(serverAddress: InetSocketAddress, ssl: SSL)(implicit val ec
|
|||
if (!future.isSuccess) {
|
||||
errorHandler(future.cause())
|
||||
} else {
|
||||
log.info(s"channel closed: " + future.channel())
|
||||
log.info("server={} channel closed: {}", serverAddress, future.channel())
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
@ -168,7 +169,7 @@ class ElectrumClient(serverAddress: InetSocketAddress, ssl: SSL)(implicit val ec
|
|||
import org.json4s._
|
||||
import org.json4s.jackson.JsonMethods._
|
||||
|
||||
log.info(s"sending $request")
|
||||
log.debug("sending {} to {}", request, serverAddress)
|
||||
val json = ("method" -> request.method) ~ ("params" -> request.params.map {
|
||||
case s: String => new JString(s)
|
||||
case b: BinaryData => new JString(b.toString())
|
||||
|
@ -202,7 +203,7 @@ class ElectrumClient(serverAddress: InetSocketAddress, ssl: SSL)(implicit val ec
|
|||
var addressSubscriptions = Map.empty[String, Set[ActorRef]]
|
||||
var scriptHashSubscriptions = Map.empty[BinaryData, Set[ActorRef]]
|
||||
val headerSubscriptions = collection.mutable.HashSet.empty[ActorRef]
|
||||
val version = ServerVersion("3.3.2", "1.4")
|
||||
val version = ServerVersion(CLIENT_NAME, PROTOCOL_VERSION)
|
||||
val statusListeners = collection.mutable.HashSet.empty[ActorRef]
|
||||
val keepHeaders = 100
|
||||
|
||||
|
@ -223,7 +224,7 @@ class ElectrumClient(serverAddress: InetSocketAddress, ssl: SSL)(implicit val ec
|
|||
|
||||
case PingResponse => ()
|
||||
|
||||
case _ => log.warning(s"unhandled $message")
|
||||
case _ => log.warning("server={} unhandled message {}", serverAddress, message)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -254,7 +255,7 @@ class ElectrumClient(serverAddress: InetSocketAddress, ssl: SSL)(implicit val ec
|
|||
|
||||
def disconnected: Receive = {
|
||||
case ctx: ChannelHandlerContext =>
|
||||
log.info(s"connected to $serverAddress")
|
||||
log.info("connected to server={}", serverAddress)
|
||||
send(ctx, version)
|
||||
context become waitingForVersion(ctx)
|
||||
|
||||
|
@ -263,12 +264,17 @@ class ElectrumClient(serverAddress: InetSocketAddress, ssl: SSL)(implicit val ec
|
|||
|
||||
def waitingForVersion(ctx: ChannelHandlerContext): Receive = {
|
||||
case Right(json: JsonRPCResponse) =>
|
||||
val serverVersion = parseJsonResponse(version, json)
|
||||
log.debug(s"serverVersion=$serverVersion")
|
||||
send(ctx, HeaderSubscription(self))
|
||||
headerSubscriptions += self
|
||||
log.debug("waiting for tip")
|
||||
context become waitingForTip(ctx)
|
||||
(parseJsonResponse(version, json): @unchecked) match {
|
||||
case ServerVersionResponse(clientName, protocolVersion) =>
|
||||
log.info("server={} clientName={} protocolVersion={}", serverAddress, clientName, protocolVersion)
|
||||
send(ctx, HeaderSubscription(self))
|
||||
headerSubscriptions += self
|
||||
log.debug("waiting for tip from server={}", serverAddress)
|
||||
context become waitingForTip(ctx)
|
||||
case ServerError(request, error) =>
|
||||
log.error("server={} sent error={} while processing request={}, disconnecting", serverAddress, error, request)
|
||||
close()
|
||||
}
|
||||
|
||||
case AddStatusListener(actor) => statusListeners += actor
|
||||
}
|
||||
|
@ -276,7 +282,7 @@ class ElectrumClient(serverAddress: InetSocketAddress, ssl: SSL)(implicit val ec
|
|||
def waitingForTip(ctx: ChannelHandlerContext): Receive = {
|
||||
case Right(json: JsonRPCResponse) =>
|
||||
val (height, header) = parseBlockHeader(json.result)
|
||||
log.debug(s"connected, tip = ${header.hash} height = $height")
|
||||
log.debug("connected to server={}, tip={} height={}", serverAddress, header.hash, height)
|
||||
statusListeners.map(_ ! ElectrumReady(height, header, serverAddress))
|
||||
context become connected(ctx, height, header, "", Map())
|
||||
|
||||
|
@ -310,10 +316,10 @@ class ElectrumClient(serverAddress: InetSocketAddress, ssl: SSL)(implicit val ec
|
|||
requests.get(json.id) match {
|
||||
case Some((request, requestor)) =>
|
||||
val response = parseJsonResponse(request, json)
|
||||
log.debug(s"got response for reqId=${json.id} request=$request response=$response")
|
||||
log.debug("server={} sent response for reqId={} request={} response={}", serverAddress, json.id, request, response)
|
||||
requestor ! response
|
||||
case None =>
|
||||
log.warning(s"could not find requestor for reqId=${json.id} response=$json")
|
||||
log.warning("server={} could not find requestor for reqId=${} response={}", serverAddress, json.id, json)
|
||||
}
|
||||
context become connected(ctx, height, tip, buffer, requests - json.id)
|
||||
|
||||
|
@ -324,12 +330,14 @@ class ElectrumClient(serverAddress: InetSocketAddress, ssl: SSL)(implicit val ec
|
|||
case Left(response: ScriptHashSubscriptionResponse) => scriptHashSubscriptions.get(response.scriptHash).map(listeners => listeners.map(_ ! response))
|
||||
|
||||
case HeaderSubscriptionResponse(height, newtip) =>
|
||||
log.info(s"new tip $newtip")
|
||||
log.info("server={} new tip={}", serverAddress, newtip)
|
||||
context become connected(ctx, height, newtip, buffer, requests)
|
||||
}
|
||||
}
|
||||
|
||||
object ElectrumClient {
|
||||
val CLIENT_NAME = "3.3.2" // client name that we will include in our "version" message
|
||||
val PROTOCOL_VERSION = "1.4" // version of the protocol that we require
|
||||
|
||||
// this is expensive and shared with all clients
|
||||
val workerGroup = new NioEventLoopGroup()
|
||||
|
|
|
@ -19,7 +19,7 @@ package fr.acinq.eclair.blockchain.electrum
|
|||
import java.io.InputStream
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
import akka.actor.{Actor, ActorRef, FSM, Props, Terminated}
|
||||
import akka.actor.{Actor, ActorRef, FSM, OneForOneStrategy, Props, SupervisorStrategy, Terminated}
|
||||
import fr.acinq.bitcoin.BlockHeader
|
||||
import fr.acinq.eclair.Globals
|
||||
import fr.acinq.eclair.blockchain.CurrentBlockCount
|
||||
|
@ -38,12 +38,19 @@ class ElectrumClientPool(serverAddresses: Set[ElectrumServerAddress])(implicit v
|
|||
val statusListeners = collection.mutable.HashSet.empty[ActorRef]
|
||||
val addresses = collection.mutable.Map.empty[ActorRef, InetSocketAddress]
|
||||
|
||||
|
||||
// on startup, we attempt to connect to a number of electrum clients
|
||||
// they will send us an `ElectrumReady` message when they're connected, or
|
||||
// terminate if they cannot connect
|
||||
(0 until Math.min(MAX_CONNECTION_COUNT, serverAddresses.size)) foreach (_ => self ! Connect)
|
||||
|
||||
log.debug(s"starting electrum pool with serverAddresses={}", serverAddresses)
|
||||
log.debug("starting electrum pool with serverAddresses={}", serverAddresses)
|
||||
|
||||
// custom supervision strategy: always stop Electrum clients when there's a problem, we will automatically reconnect
|
||||
// to another client
|
||||
override def supervisorStrategy: SupervisorStrategy = OneForOneStrategy(loggingEnabled = true) {
|
||||
case _ => SupervisorStrategy.stop
|
||||
}
|
||||
|
||||
startWith(Disconnected, DisconnectedData)
|
||||
|
||||
|
@ -130,14 +137,17 @@ class ElectrumClientPool(serverAddresses: Set[ElectrumServerAddress])(implicit v
|
|||
data match {
|
||||
case None =>
|
||||
// as soon as we have a connection to an electrum server, we select it as master
|
||||
log.info(s"selecting master $remoteAddress} at $tip")
|
||||
log.info("selecting master {} at {}", remoteAddress, tip)
|
||||
statusListeners.foreach(_ ! ElectrumClient.ElectrumReady(height, tip, remoteAddress))
|
||||
context.system.eventStream.publish(ElectrumClient.ElectrumReady(height, tip, remoteAddress))
|
||||
goto(Connected) using ConnectedData(connection, Map(connection -> (height, tip)))
|
||||
case Some(d) if height >= d.blockHeight + 2L =>
|
||||
case Some(d) if connection != d.master && height >= d.blockHeight + 2L =>
|
||||
// we only switch to a new master if there is a significant difference with our current master, because
|
||||
// we don't want to switch to a new master every time a new block arrives (some servers will be notified before others)
|
||||
log.info(s"switching to master $remoteAddress at $tip")
|
||||
// we check that the current connection is not our master because on regtest when you generate several blocks at once
|
||||
// (and maybe on testnet in some pathological cases where there's a block every second) it may seen like our master
|
||||
// skipped a block and is suddenly at height + 2
|
||||
log.info("switching to master {} at {}", remoteAddress, tip)
|
||||
// we've switched to a new master, treat this as a disconnection/reconnection
|
||||
// so users (wallet, watcher, ...) will reset their subscriptions
|
||||
statusListeners.foreach(_ ! ElectrumClient.ElectrumDisconnected)
|
||||
|
@ -146,7 +156,7 @@ class ElectrumClientPool(serverAddresses: Set[ElectrumServerAddress])(implicit v
|
|||
context.system.eventStream.publish(ElectrumClient.ElectrumReady(height, tip, remoteAddress))
|
||||
goto(Connected) using d.copy(master = connection, tips = d.tips + (connection -> (height, tip)))
|
||||
case Some(d) =>
|
||||
log.debug(s"received tip from $remoteAddress} $tip at $height")
|
||||
log.debug("received tip {} from {} at {}", tip, remoteAddress, height)
|
||||
stay using d.copy(tips = d.tips + (connection -> (height, tip)))
|
||||
}
|
||||
}
|
||||
|
@ -154,7 +164,7 @@ class ElectrumClientPool(serverAddresses: Set[ElectrumServerAddress])(implicit v
|
|||
private def updateBlockCount(blockCount: Long): Unit = {
|
||||
// when synchronizing we don't want to advertise previous blocks
|
||||
if (Globals.blockCount.get() < blockCount) {
|
||||
log.debug(s"current blockchain height=$blockCount")
|
||||
log.debug("current blockchain height={}", blockCount)
|
||||
context.system.eventStream.publish(CurrentBlockCount(blockCount))
|
||||
Globals.blockCount.set(blockCount)
|
||||
}
|
||||
|
|
|
@ -763,8 +763,8 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
|
|||
}
|
||||
}
|
||||
|
||||
case Event(c@CurrentBlockCount(count), d: DATA_NORMAL) if d.commitments.hasTimedoutOutgoingHtlcs(count) =>
|
||||
handleLocalError(HtlcTimedout(d.channelId), d, Some(c))
|
||||
case Event(c@CurrentBlockCount(count), d: DATA_NORMAL) if d.commitments.timedoutOutgoingHtlcs(count).nonEmpty =>
|
||||
handleLocalError(HtlcTimedout(d.channelId, d.commitments.timedoutOutgoingHtlcs(count)), d, Some(c))
|
||||
|
||||
case Event(c@CurrentFeerates(feeratesPerKw), d: DATA_NORMAL) =>
|
||||
val networkFeeratePerKw = feeratesPerKw.blocks_2
|
||||
|
@ -1023,8 +1023,8 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
|
|||
|
||||
case Event(r: RevocationTimeout, d: DATA_SHUTDOWN) => handleRevocationTimeout(r, d)
|
||||
|
||||
case Event(c@CurrentBlockCount(count), d: DATA_SHUTDOWN) if d.commitments.hasTimedoutOutgoingHtlcs(count) =>
|
||||
handleLocalError(HtlcTimedout(d.channelId), d, Some(c))
|
||||
case Event(c@CurrentBlockCount(count), d: DATA_SHUTDOWN) if d.commitments.timedoutOutgoingHtlcs(count).nonEmpty =>
|
||||
handleLocalError(HtlcTimedout(d.channelId, d.commitments.timedoutOutgoingHtlcs(count)), d, Some(c))
|
||||
|
||||
case Event(c@CurrentFeerates(feerates), d: DATA_SHUTDOWN) =>
|
||||
val networkFeeratePerKw = feerates.blocks_2
|
||||
|
@ -1204,7 +1204,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
|
|||
d.commitments.originChannels.get(add.id) match {
|
||||
case Some(origin) =>
|
||||
log.info(s"failing htlc #${add.id} paymentHash=${add.paymentHash} origin=$origin: htlc timed out")
|
||||
relayer ! Status.Failure(AddHtlcFailed(d.channelId, add.paymentHash, HtlcTimedout(d.channelId), origin, None, None))
|
||||
relayer ! Status.Failure(AddHtlcFailed(d.channelId, add.paymentHash, HtlcTimedout(d.channelId, Set(add)), origin, None, None))
|
||||
case None =>
|
||||
// same as for fulfilling the htlc (no big deal)
|
||||
log.info(s"cannot fail timedout htlc #${add.id} paymentHash=${add.paymentHash} (origin not found)")
|
||||
|
@ -1318,11 +1318,11 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
|
|||
|
||||
goto(SYNCING) using d1 sending channelReestablish
|
||||
|
||||
case Event(c@CurrentBlockCount(count), d: HasCommitments) if d.commitments.hasTimedoutOutgoingHtlcs(count) =>
|
||||
case Event(c@CurrentBlockCount(count), d: HasCommitments) if d.commitments.timedoutOutgoingHtlcs(count).nonEmpty =>
|
||||
// note: this can only happen if state is NORMAL or SHUTDOWN
|
||||
// -> in NEGOTIATING there are no more htlcs
|
||||
// -> in CLOSING we either have mutual closed (so no more htlcs), or already have unilaterally closed (so no action required), and we can't be in OFFLINE state anyway
|
||||
handleLocalError(HtlcTimedout(d.channelId), d, Some(c))
|
||||
handleLocalError(HtlcTimedout(d.channelId, d.commitments.timedoutOutgoingHtlcs(count)), d, Some(c))
|
||||
|
||||
case Event(CMD_UPDATE_RELAY_FEE(feeBaseMsat, feeProportionalMillionths), d: DATA_NORMAL) =>
|
||||
log.info(s"updating relay fees: prevFeeBaseMsat={} nextFeeBaseMsat={} prevFeeProportionalMillionths={} nextFeeProportionalMillionths={}", d.channelUpdate.feeBaseMsat, feeBaseMsat, d.channelUpdate.feeProportionalMillionths, feeProportionalMillionths)
|
||||
|
@ -1446,7 +1446,8 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
|
|||
goto(NEGOTIATING) using d.copy(closingTxProposed = closingTxProposed1) sending d.localShutdown
|
||||
}
|
||||
|
||||
case Event(c@CurrentBlockCount(count), d: HasCommitments) if d.commitments.hasTimedoutOutgoingHtlcs(count) => handleLocalError(HtlcTimedout(d.channelId), d, Some(c))
|
||||
case Event(c@CurrentBlockCount(count), d: HasCommitments) if d.commitments.timedoutOutgoingHtlcs(count).nonEmpty =>
|
||||
handleLocalError(HtlcTimedout(d.channelId, d.commitments.timedoutOutgoingHtlcs(count)), d, Some(c))
|
||||
|
||||
// just ignore this, we will put a new watch when we reconnect, and we'll be notified again
|
||||
case Event(WatchEventConfirmed(BITCOIN_FUNDING_DEPTHOK | BITCOIN_FUNDING_DEEPLYBURIED, _, _), _) => stay
|
||||
|
|
|
@ -48,7 +48,7 @@ case class ChannelUnavailable (override val channelId: BinaryDa
|
|||
case class InvalidFinalScript (override val channelId: BinaryData) extends ChannelException(channelId, "invalid final script")
|
||||
case class FundingTxTimedout (override val channelId: BinaryData) extends ChannelException(channelId, "funding tx timed out")
|
||||
case class FundingTxSpent (override val channelId: BinaryData, spendingTx: Transaction) extends ChannelException(channelId, s"funding tx has been spent by txid=${spendingTx.txid}")
|
||||
case class HtlcTimedout (override val channelId: BinaryData) extends ChannelException(channelId, "one or more htlcs timed out")
|
||||
case class HtlcTimedout (override val channelId: BinaryData, htlcs: Set[UpdateAddHtlc]) extends ChannelException(channelId, s"one or more htlcs timed out: ids=${htlcs.take(10).map(_.id).mkString}") // we only display the first 10 ids
|
||||
case class HtlcOverridenByLocalCommit (override val channelId: BinaryData) extends ChannelException(channelId, "htlc was overriden by local commit")
|
||||
case class FeerateTooSmall (override val channelId: BinaryData, remoteFeeratePerKw: Long) extends ChannelException(channelId, s"remote fee rate is too small: remoteFeeratePerKw=$remoteFeeratePerKw")
|
||||
case class FeerateTooDifferent (override val channelId: BinaryData, localFeeratePerKw: Long, remoteFeeratePerKw: Long) extends ChannelException(channelId, s"local/remote feerates are too different: remoteFeeratePerKw=$remoteFeeratePerKw localFeeratePerKw=$localFeeratePerKw")
|
||||
|
|
|
@ -61,10 +61,10 @@ case class Commitments(localParams: LocalParams, remoteParams: RemoteParams,
|
|||
|
||||
def hasNoPendingHtlcs: Boolean = localCommit.spec.htlcs.isEmpty && remoteCommit.spec.htlcs.isEmpty && remoteNextCommitInfo.isRight
|
||||
|
||||
def hasTimedoutOutgoingHtlcs(blockheight: Long): Boolean =
|
||||
localCommit.spec.htlcs.exists(htlc => htlc.direction == OUT && blockheight >= htlc.add.cltvExpiry) ||
|
||||
remoteCommit.spec.htlcs.exists(htlc => htlc.direction == IN && blockheight >= htlc.add.cltvExpiry) ||
|
||||
remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.spec.htlcs.exists(htlc => htlc.direction == IN && blockheight >= htlc.add.cltvExpiry)).getOrElse(false)
|
||||
def timedoutOutgoingHtlcs(blockheight: Long): Set[UpdateAddHtlc] =
|
||||
(localCommit.spec.htlcs.filter(htlc => htlc.direction == OUT && blockheight >= htlc.add.cltvExpiry) ++
|
||||
remoteCommit.spec.htlcs.filter(htlc => htlc.direction == IN && blockheight >= htlc.add.cltvExpiry) ++
|
||||
remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.spec.htlcs.filter(htlc => htlc.direction == IN && blockheight >= htlc.add.cltvExpiry)).getOrElse(Set.empty[DirectedHtlc])).map(_.add)
|
||||
|
||||
def addLocalProposal(proposal: UpdateMessage): Commitments = Commitments.addLocalProposal(this, proposal)
|
||||
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
package fr.acinq.eclair.payment
|
||||
|
||||
import akka.actor.{Actor, ActorLogging, ActorRef, Props}
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.eclair.crypto.Sphinx.ErrorPacket
|
||||
import fr.acinq.eclair.payment.PaymentLifecycle.{PaymentFailed, PaymentResult, RemoteFailure, SendPayment}
|
||||
import fr.acinq.eclair.{NodeParams, randomBytes, secureRandom}
|
||||
import fr.acinq.eclair.router.{Announcements, ChannelDesc, Data}
|
||||
import fr.acinq.eclair.wire.{ChannelUpdate, UnknownPaymentHash}
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
/**
|
||||
* This actor periodically probes the network by sending payments to random nodes. The payments will eventually fail
|
||||
* because the recipient doesn't know the preimage, but it allows us to test channels and improve routing for real payments.
|
||||
*/
|
||||
class Autoprobe(nodeParams: NodeParams, router: ActorRef, paymentInitiator: ActorRef) extends Actor with ActorLogging {
|
||||
|
||||
import Autoprobe._
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
||||
// refresh our map of channel_updates regularly from the router
|
||||
context.system.scheduler.schedule(0 seconds, ROUTING_TABLE_REFRESH_INTERVAL, router, 'data)
|
||||
|
||||
override def receive: Receive = {
|
||||
case routingData: Data =>
|
||||
scheduleProbe()
|
||||
context become main(routingData)
|
||||
}
|
||||
|
||||
def main(routingData: Data): Receive = {
|
||||
case routingData: Data =>
|
||||
context become main(routingData)
|
||||
|
||||
case TickProbe =>
|
||||
pickPaymentDestination(nodeParams.nodeId, routingData) match {
|
||||
case Some(targetNodeId) =>
|
||||
val paymentHash = randomBytes(32) // we don't even know the preimage (this needs to be a secure random!)
|
||||
log.info(s"sending payment probe to node=$targetNodeId payment_hash=$paymentHash")
|
||||
paymentInitiator ! SendPayment(PAYMENT_AMOUNT_MSAT, paymentHash, targetNodeId, maxAttempts = 1)
|
||||
case None =>
|
||||
log.info(s"could not find a destination, re-scheduling")
|
||||
scheduleProbe()
|
||||
}
|
||||
|
||||
case paymentResult: PaymentResult =>
|
||||
paymentResult match {
|
||||
case PaymentFailed(_, _ :+ RemoteFailure(_, ErrorPacket(targetNodeId, UnknownPaymentHash))) =>
|
||||
log.info(s"payment probe successful to node=$targetNodeId")
|
||||
case _ =>
|
||||
log.info(s"payment probe failed with paymentResult=$paymentResult")
|
||||
}
|
||||
scheduleProbe()
|
||||
}
|
||||
|
||||
def scheduleProbe() = context.system.scheduler.scheduleOnce(PROBING_INTERVAL, self, TickProbe)
|
||||
|
||||
|
||||
}
|
||||
|
||||
object Autoprobe {
|
||||
|
||||
def props(nodeParams: NodeParams, router: ActorRef, paymentInitiator: ActorRef) = Props(classOf[Autoprobe], nodeParams, router, paymentInitiator)
|
||||
|
||||
val ROUTING_TABLE_REFRESH_INTERVAL = 10 minutes
|
||||
|
||||
val PROBING_INTERVAL = 20 seconds
|
||||
|
||||
val PAYMENT_AMOUNT_MSAT = 100 * 1000 // this is below dust_limit so there won't be an output in the commitment tx
|
||||
|
||||
object TickProbe
|
||||
|
||||
def pickPaymentDestination(nodeId: PublicKey, routingData: Data): Option[PublicKey] = {
|
||||
// we only pick direct peers with enabled public channels
|
||||
val peers = routingData.updates
|
||||
.collect {
|
||||
case (desc, u) if desc.a == nodeId && Announcements.isEnabled(u.channelFlags) && routingData.channels.contains(u.shortChannelId) => desc.b // we only consider outgoing channels that are enabled and announced
|
||||
}
|
||||
if (peers.isEmpty) {
|
||||
None
|
||||
} else {
|
||||
peers.drop(secureRandom.nextInt(peers.size)).headOption
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -9,9 +9,8 @@ import Router._
|
|||
|
||||
object Graph {
|
||||
|
||||
import DirectedGraph._
|
||||
|
||||
case class WeightedNode(key: PublicKey, weight: Long)
|
||||
case class WeightedPath(path: Seq[GraphEdge], weight: Long)
|
||||
|
||||
/**
|
||||
* This comparator must be consistent with the "equals" behavior, thus for two weighted nodes with
|
||||
|
@ -25,6 +24,99 @@ object Graph {
|
|||
}
|
||||
}
|
||||
|
||||
implicit object PathComparator extends Ordering[WeightedPath] {
|
||||
override def compare(x: WeightedPath, y: WeightedPath): Int = y.weight.compareTo(x.weight)
|
||||
}
|
||||
/**
|
||||
* Yen's algorithm to find the k-shortest (loopless) paths in a graph, uses dijkstra as search algo. Is guaranteed to terminate finding
|
||||
* at most @pathsToFind paths sorted by cost (the cheapest is in position 0).
|
||||
* @param graph
|
||||
* @param sourceNode
|
||||
* @param targetNode
|
||||
* @param amountMsat
|
||||
* @param pathsToFind
|
||||
* @return
|
||||
*/
|
||||
def yenKshortestPaths(graph: DirectedGraph, sourceNode: PublicKey, targetNode: PublicKey, amountMsat: Long, ignoredEdges: Set[ChannelDesc], extraEdges: Set[GraphEdge], pathsToFind: Int): Seq[WeightedPath] = {
|
||||
|
||||
var allSpurPathsFound = false
|
||||
|
||||
// stores the shortest paths
|
||||
val shortestPaths = new mutable.MutableList[WeightedPath]
|
||||
// stores the candidates for k(K +1) shortest paths, sorted by path cost
|
||||
val candidates = new mutable.PriorityQueue[WeightedPath]
|
||||
|
||||
// find the shortest path, k = 0
|
||||
val shortestPath = dijkstraShortestPath(graph, sourceNode, targetNode, amountMsat, ignoredEdges, extraEdges)
|
||||
shortestPaths += WeightedPath(shortestPath, pathCost(shortestPath, amountMsat))
|
||||
|
||||
// main loop
|
||||
for(k <- 1 until pathsToFind) {
|
||||
|
||||
if ( !allSpurPathsFound ) {
|
||||
|
||||
// for every edge in the path
|
||||
for (i <- shortestPaths(k - 1).path.indices) {
|
||||
|
||||
val prevShortestPath = shortestPaths(k - 1).path
|
||||
|
||||
// select the spur node as the i-th element of the k-th previous shortest path (k -1)
|
||||
val spurEdge = prevShortestPath(i)
|
||||
|
||||
// select the subpath from the source to the spur node of the k-th previous shortest path
|
||||
val rootPathEdges = if(i == 0) prevShortestPath.head :: Nil else prevShortestPath.take(i)
|
||||
|
||||
// links to be removed that are part of the previous shortest path and which share the same root path
|
||||
val edgesToIgnore = shortestPaths.flatMap { weightedPath =>
|
||||
if ( (i == 0 && (weightedPath.path.head :: Nil) == rootPathEdges) || weightedPath.path.take(i) == rootPathEdges ) {
|
||||
weightedPath.path(i).desc :: Nil
|
||||
} else {
|
||||
Nil
|
||||
}
|
||||
}
|
||||
|
||||
// find the "spur" path, a subpath going from the spur edge to the target avoiding previously found subpaths
|
||||
val spurPath = dijkstraShortestPath(graph, spurEdge.desc.a, targetNode, amountMsat, ignoredEdges ++ edgesToIgnore.toSet, extraEdges)
|
||||
|
||||
// if there wasn't a path the spur will be empty
|
||||
if(spurPath.nonEmpty) {
|
||||
|
||||
// candidate k-shortest path is made of the rootPath and the new spurPath
|
||||
val totalPath = rootPathEdges.head.desc.a == spurPath.head.desc.a match {
|
||||
case true => rootPathEdges.tail ++ spurPath // if the heads are the same node, drop it from the rootPath
|
||||
case false => rootPathEdges ++ spurPath
|
||||
}
|
||||
|
||||
//val totalPath = concat(rootPathEdges, spurPath.toList)
|
||||
val candidatePath = WeightedPath(totalPath, pathCost(totalPath, amountMsat))
|
||||
|
||||
if (!shortestPaths.contains(candidatePath) && !candidates.exists(_ == candidatePath)) {
|
||||
candidates.enqueue(candidatePath)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(candidates.isEmpty) {
|
||||
// handles the case of having exhausted all possible spur paths and it's impossible to reach the target from the source
|
||||
allSpurPathsFound = true
|
||||
} else {
|
||||
// move the best candidate to the shortestPaths container
|
||||
shortestPaths += candidates.dequeue()
|
||||
}
|
||||
}
|
||||
|
||||
shortestPaths
|
||||
}
|
||||
|
||||
// Calculates the total cost of a path (amount + fees), direct channels with the source will have a cost of 0 (pay no fees)
|
||||
def pathCost(path: Seq[GraphEdge], amountMsat: Long): Long = {
|
||||
path.drop(1).foldRight(amountMsat) { (edge, cost) =>
|
||||
edgeWeight(edge, cost, isNeighborTarget = false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the shortest path in the graph, uses a modified version of Dijsktra's algorithm that computes
|
||||
* the shortest path from the target to the source (this is because we want to calculate the weight of the
|
||||
|
@ -38,9 +130,6 @@ object Graph {
|
|||
* @param extraEdges a list of extra edges we want to consider but are not currently in the graph
|
||||
* @return
|
||||
*/
|
||||
def shortestPath(g: DirectedGraph, sourceNode: PublicKey, targetNode: PublicKey, amountMsat: Long, ignoredEdges: Set[ChannelDesc], extraEdges: Set[GraphEdge]): Seq[Hop] = {
|
||||
dijkstraShortestPath(g, sourceNode, targetNode, amountMsat, ignoredEdges, extraEdges).map(graphEdgeToHop)
|
||||
}
|
||||
|
||||
def dijkstraShortestPath(g: DirectedGraph, sourceNode: PublicKey, targetNode: PublicKey, amountMsat: Long, ignoredEdges: Set[ChannelDesc], extraEdges: Set[GraphEdge]): Seq[GraphEdge] = {
|
||||
|
||||
|
|
|
@ -26,7 +26,9 @@ import fr.acinq.eclair.channel._
|
|||
import fr.acinq.eclair.crypto.TransportHandler
|
||||
import fr.acinq.eclair.io.Peer.{ChannelClosed, InvalidSignature, NonexistingChannel, PeerRoutingMessage}
|
||||
import fr.acinq.eclair.payment.PaymentRequest.ExtraHop
|
||||
import fr.acinq.eclair.router.Graph.GraphStructure.DirectedGraph.graphEdgeToHop
|
||||
import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge}
|
||||
import fr.acinq.eclair.router.Graph.WeightedPath
|
||||
import fr.acinq.eclair.transactions.Scripts
|
||||
import fr.acinq.eclair.wire._
|
||||
|
||||
|
@ -35,7 +37,7 @@ import scala.collection.{SortedSet, mutable}
|
|||
import scala.compat.Platform
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{ExecutionContext, Promise}
|
||||
import scala.util.Try
|
||||
import scala.util.{Random, Try}
|
||||
|
||||
// @formatter:off
|
||||
|
||||
|
@ -367,6 +369,10 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom
|
|||
sender ! (d.updates ++ d.privateUpdates)
|
||||
stay
|
||||
|
||||
case Event('data, d) =>
|
||||
sender ! d
|
||||
stay
|
||||
|
||||
case Event(RouteRequest(start, end, amount, assistedRoutes, ignoreNodes, ignoreChannels), d) =>
|
||||
// we convert extra routing info provided in the payment request to fake channel_update
|
||||
// it takes precedence over all other channel_updates we know
|
||||
|
@ -376,7 +382,8 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom
|
|||
val ignoredUpdates = getIgnoredChannelDesc(d.updates ++ d.privateUpdates ++ assistedUpdates, ignoreNodes) ++ ignoreChannels ++ d.excludedChannels
|
||||
log.info(s"finding a route $start->$end with assistedChannels={} ignoreNodes={} ignoreChannels={} excludedChannels={}", assistedUpdates.keys.mkString(","), ignoreNodes.map(_.toBin).mkString(","), ignoreChannels.mkString(","), d.excludedChannels.mkString(","))
|
||||
val extraEdges = assistedUpdates.map { case (c, u) => GraphEdge(c, u) }.toSet
|
||||
findRoute(d.graph, start, end, amount, extraEdges = extraEdges, ignoredEdges = ignoredUpdates.toSet)
|
||||
// we ask the router to make a random selection among the three best routes, numRoutes = 3
|
||||
findRoute(d.graph, start, end, amount, numRoutes = DEFAULT_ROUTES_COUNT, extraEdges = extraEdges, ignoredEdges = ignoredUpdates.toSet)
|
||||
.map(r => sender ! RouteResponse(r, ignoreNodes, ignoreChannels))
|
||||
.recover { case t => sender ! Status.Failure(t) }
|
||||
stay
|
||||
|
@ -797,24 +804,42 @@ object Router {
|
|||
*/
|
||||
val ROUTE_MAX_LENGTH = 20
|
||||
|
||||
// The default amount of routes we'll search for when findRoute is called
|
||||
val DEFAULT_ROUTES_COUNT = 3
|
||||
|
||||
// The default allowed 'spread' between the cheapest route found an the others
|
||||
// routes exceeding this difference won't be considered as a valid result
|
||||
val DEFAULT_ALLOWED_SPREAD = 0.1D
|
||||
|
||||
/**
|
||||
* Find a route in the graph between localNodeId and targetNodeId, returns the route and its cost
|
||||
* Find a route in the graph between localNodeId and targetNodeId, returns the route.
|
||||
* Will perform a k-shortest path selection given the @param numRoutes and randomly select one of the result,
|
||||
* the 'route-set' from where we select the result is made of the k-shortest path given that none of them
|
||||
* exceeds a 10% spread with the cheapest route
|
||||
*
|
||||
* @param g
|
||||
* @param localNodeId
|
||||
* @param targetNodeId
|
||||
* @param amountMsat the amount that will be sent along this route
|
||||
* @param numRoutes the number of shortest-paths to find
|
||||
* @param extraEdges a set of extra edges we want to CONSIDER during the search
|
||||
* @param ignoredEdges a set of extra edges we want to IGNORE during the search
|
||||
* @return the computed route to the destination @targetNodeId
|
||||
*/
|
||||
def findRoute(g: DirectedGraph, localNodeId: PublicKey, targetNodeId: PublicKey, amountMsat: Long, extraEdges: Set[GraphEdge] = Set.empty, ignoredEdges: Set[ChannelDesc] = Set.empty): Try[Seq[Hop]] = Try {
|
||||
def findRoute(g: DirectedGraph, localNodeId: PublicKey, targetNodeId: PublicKey, amountMsat: Long, numRoutes: Int, extraEdges: Set[GraphEdge] = Set.empty, ignoredEdges: Set[ChannelDesc] = Set.empty): Try[Seq[Hop]] = Try {
|
||||
if (localNodeId == targetNodeId) throw CannotRouteToSelf
|
||||
|
||||
Graph.shortestPath(g, localNodeId, targetNodeId, amountMsat, ignoredEdges, extraEdges) match {
|
||||
val foundRoutes = Graph.yenKshortestPaths(g, localNodeId, targetNodeId, amountMsat, ignoredEdges, extraEdges, numRoutes).toList match {
|
||||
case Nil => throw RouteNotFound
|
||||
case path => path
|
||||
case route :: Nil if route.path.isEmpty => throw RouteNotFound
|
||||
case foundRoutes => foundRoutes
|
||||
}
|
||||
|
||||
// minimum cost
|
||||
val minimumCost = foundRoutes.head.weight
|
||||
|
||||
// routes paying at most minimumCost + 10%
|
||||
val eligibleRoutes = foundRoutes.filter(_.weight <= (minimumCost + minimumCost * DEFAULT_ALLOWED_SPREAD).round)
|
||||
Random.shuffle(eligibleRoutes).head.path.map(graphEdgeToHop)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"result" : [ "connect (uri): open a secure connection to a lightning node", "connect (nodeId, host, port): open a secure connection to a lightning node", "open (nodeId, fundingSatoshis, pushMsat = 0, feerateSatPerByte = ?, channelFlags = 0x01): open a channel with another lightning node, by default push = 0, feerate for the funding tx targets 6 blocks, and channel is announced", "updaterelayfee (channelId, feeBaseMsat, feeProportionalMillionths): update relay fee for payments going through this channel", "peers: list existing local peers", "channels: list existing local channels", "channels (nodeId): list existing local channels to a particular nodeId", "channel (channelId): retrieve detailed information about a given channel", "channelstats: retrieves statistics about channel usage (fees, number and average amount of payments)", "allnodes: list all known nodes", "allchannels: list all known channels", "allupdates: list all channels updates", "allupdates (nodeId): list all channels updates for this nodeId", "receive (amountMsat, description): generate a payment request for a given amount", "receive (amountMsat, description, expirySeconds): generate a payment request for a given amount with a description and a number of seconds till it expires", "checkinvoice (paymentRequest): returns node, amount and payment hash in an invoice/paymentRequest", "findroute (paymentRequest|nodeId): given a payment request or nodeID checks if there is a valid payment route returns JSON with attempts, nodes and channels of route", "send (amountMsat, paymentHash, nodeId): send a payment to a lightning node", "send (paymentRequest): send a payment to a lightning node using a BOLT11 payment request", "send (paymentRequest, amountMsat): send a payment to a lightning node using a BOLT11 payment request and a custom amount", "close (channelId): close a channel", "close (channelId, scriptPubKey): close a channel and send the funds to the given scriptPubKey", "forceclose (channelId): force-close a channel by publishing the local commitment tx (careful: this is more expensive than a regular close and will incur a delay before funds are spendable)", "checkpayment (paymentHash): returns true if the payment has been received, false otherwise", "checkpayment (paymentRequest): returns true if the payment has been received, false otherwise", "audit: list all send/received/relayed payments", "audit (from, to): list send/received/relayed payments in that interval (from <= timestamp < to)", "networkfees: list all network fees paid to the miners, by transaction", "networkfees (from, to): list network fees paid to the miners, by transaction, in that interval (from <= timestamp < to)", "getinfo: returns info about the blockchain and this node", "help: display this message" ],
|
||||
"result" : [ "connect (uri): open a secure connection to a lightning node", "connect (nodeId, host, port): open a secure connection to a lightning node", "open (nodeId, fundingSatoshis, pushMsat = 0, feerateSatPerByte = ?, channelFlags = 0x01): open a channel with another lightning node, by default push = 0, feerate for the funding tx targets 6 blocks, and channel is announced", "updaterelayfee (channelId, feeBaseMsat, feeProportionalMillionths): update relay fee for payments going through this channel", "peers: list existing local peers", "channels: list existing local channels", "channels (nodeId): list existing local channels to a particular nodeId", "channel (channelId): retrieve detailed information about a given channel", "channelstats: retrieves statistics about channel usage (fees, number and average amount of payments)", "allnodes: list all known nodes", "allchannels: list all known channels", "allupdates: list all channels updates", "allupdates (nodeId): list all channels updates for this nodeId", "receive (amountMsat, description): generate a payment request for a given amount", "receive (amountMsat, description, expirySeconds): generate a payment request for a given amount with a description and a number of seconds till it expires", "parseinvoice (paymentRequest): returns node, amount and payment hash in a payment request", "findroute (paymentRequest): returns nodes and channels of the route if there is any", "findroute (paymentRequest, amountMsat): returns nodes and channels of the route if there is any", "findroute (nodeId, amountMsat): returns nodes and channels of the route if there is any", "send (amountMsat, paymentHash, nodeId): send a payment to a lightning node", "send (paymentRequest): send a payment to a lightning node using a BOLT11 payment request", "send (paymentRequest, amountMsat): send a payment to a lightning node using a BOLT11 payment request and a custom amount", "close (channelId): close a channel", "close (channelId, scriptPubKey): close a channel and send the funds to the given scriptPubKey", "forceclose (channelId): force-close a channel by publishing the local commitment tx (careful: this is more expensive than a regular close and will incur a delay before funds are spendable)", "checkpayment (paymentHash): returns true if the payment has been received, false otherwise", "checkpayment (paymentRequest): returns true if the payment has been received, false otherwise", "audit: list all send/received/relayed payments", "audit (from, to): list send/received/relayed payments in that interval (from <= timestamp < to)", "networkfees: list all network fees paid to the miners, by transaction", "networkfees (from, to): list network fees paid to the miners, by transaction, in that interval (from <= timestamp < to)", "getinfo: returns info about the blockchain and this node", "help: display this message" ],
|
||||
"id" : "eclair-node"
|
||||
}
|
|
@ -27,6 +27,8 @@ class CoinUtilsSpec extends FunSuite {
|
|||
assert(am_btc == MilliSatoshi(100000000000L))
|
||||
val am_mbtc: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1", MBtcUnit.code)
|
||||
assert(am_mbtc == MilliSatoshi(100000000L))
|
||||
val am_bits: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1", BitUnit.code)
|
||||
assert(am_bits == MilliSatoshi(100000))
|
||||
val am_sat: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1", SatUnit.code)
|
||||
assert(am_sat == MilliSatoshi(1000))
|
||||
val am_msat: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1", MSatUnit.code)
|
||||
|
@ -42,6 +44,8 @@ class CoinUtilsSpec extends FunSuite {
|
|||
assert(am_mbtc_dec_nozero == MilliSatoshi(25000000L))
|
||||
val am_mbtc_dec: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1.23456789", MBtcUnit.code)
|
||||
assert(am_mbtc_dec == MilliSatoshi(123456789L))
|
||||
val am_bits_dec: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1.23456789", BitUnit.code)
|
||||
assert(am_bits_dec == MilliSatoshi(123456))
|
||||
val am_sat_dec: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1.23456789", SatUnit.code)
|
||||
assert(am_sat_dec == MilliSatoshi(1234))
|
||||
val am_msat_dec: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1.234", MSatUnit.code)
|
||||
|
@ -80,19 +84,23 @@ class CoinUtilsSpec extends FunSuite {
|
|||
assert(CoinUtils.rawAmountInUnit(MilliSatoshi(123), SatUnit) == BigDecimal(0.123))
|
||||
assert(CoinUtils.rawAmountInUnit(MilliSatoshi(123), MSatUnit) == BigDecimal(123))
|
||||
assert(CoinUtils.rawAmountInUnit(MilliSatoshi(12345678), BtcUnit) == BigDecimal(0.00012345678))
|
||||
assert(CoinUtils.rawAmountInUnit(MilliSatoshi(1234567), BitUnit) == BigDecimal(12.34567))
|
||||
|
||||
assert(CoinUtils.rawAmountInUnit(Satoshi(123), BtcUnit) == BigDecimal(0.00000123))
|
||||
assert(CoinUtils.rawAmountInUnit(Satoshi(123), MBtcUnit) == BigDecimal(0.00123))
|
||||
assert(CoinUtils.rawAmountInUnit(Satoshi(123), BitUnit) == BigDecimal(1.23))
|
||||
assert(CoinUtils.rawAmountInUnit(Satoshi(123), SatUnit) == BigDecimal(123))
|
||||
assert(CoinUtils.rawAmountInUnit(Satoshi(123), MSatUnit) == BigDecimal(123000))
|
||||
|
||||
assert(CoinUtils.rawAmountInUnit(MilliBtc(123.456), BtcUnit) == BigDecimal(0.123456))
|
||||
assert(CoinUtils.rawAmountInUnit(MilliBtc(123.456), MBtcUnit) == BigDecimal(123.456))
|
||||
assert(CoinUtils.rawAmountInUnit(MilliBtc(123.45678), BitUnit) == BigDecimal(123456.78))
|
||||
assert(CoinUtils.rawAmountInUnit(MilliBtc(123.456789), SatUnit) == BigDecimal(12345678.9))
|
||||
assert(CoinUtils.rawAmountInUnit(MilliBtc(123.45678987), MSatUnit) == BigDecimal(12345678987L))
|
||||
|
||||
assert(CoinUtils.rawAmountInUnit(Btc(123.456), BtcUnit) == BigDecimal(123.456))
|
||||
assert(CoinUtils.rawAmountInUnit(Btc(123.45678987654), MBtcUnit) == BigDecimal(123456.78987654))
|
||||
assert(CoinUtils.rawAmountInUnit(Btc(123.456789876), BitUnit) == BigDecimal(123456789.876))
|
||||
assert(CoinUtils.rawAmountInUnit(Btc(1.22233333444), SatUnit) == BigDecimal(122233333.444))
|
||||
assert(CoinUtils.rawAmountInUnit(Btc(0.00011111222), MSatUnit) == BigDecimal(11111222L))
|
||||
}
|
||||
|
|
52
eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala
Normal file
52
eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala
Normal file
|
@ -0,0 +1,52 @@
|
|||
package fr.acinq.eclair
|
||||
|
||||
import java.io.{File, IOException}
|
||||
import java.nio.file.attribute.BasicFileAttributes
|
||||
import java.nio.file._
|
||||
|
||||
import com.typesafe.config.{Config, ConfigFactory}
|
||||
import fr.acinq.bitcoin.Block
|
||||
import fr.acinq.eclair.crypto.LocalKeyManager
|
||||
import org.scalatest.FunSuite
|
||||
|
||||
import scala.util.Try
|
||||
|
||||
class StartupSpec extends FunSuite {
|
||||
|
||||
test("NodeParams should fail if the alias is illegal (over 32 bytes)") {
|
||||
|
||||
val threeBytesUTFChar = '\u20AC' // €
|
||||
val baseUkraineAlias = "BitcoinLightningNodeUkraine"
|
||||
|
||||
assert(baseUkraineAlias.length === 27)
|
||||
assert(baseUkraineAlias.getBytes.length === 27)
|
||||
|
||||
// we add 2 UTF-8 chars, each is 3-bytes long -> total new length 33 bytes!
|
||||
val goUkraineGo = threeBytesUTFChar+"BitcoinLightningNodeUkraine"+threeBytesUTFChar
|
||||
|
||||
assert(goUkraineGo.length === 29)
|
||||
assert(goUkraineGo.getBytes.length === 33) // too long for the alias, should be truncated
|
||||
|
||||
val illegalAliasConf = ConfigFactory.parseString(s"node-alias = $goUkraineGo")
|
||||
val conf = illegalAliasConf.withFallback(ConfigFactory.parseResources("reference.conf").getConfig("eclair"))
|
||||
val tempConfParentDir = new File("temp-test.conf")
|
||||
|
||||
val keyManager = new LocalKeyManager(seed = randomKey.toBin, chainHash = Block.TestnetGenesisBlock.hash)
|
||||
|
||||
// try to create a NodeParams instance with a conf that contains an illegal alias
|
||||
val nodeParamsAttempt = Try(NodeParams.makeNodeParams(tempConfParentDir, conf, keyManager))
|
||||
assert(nodeParamsAttempt.isFailure && nodeParamsAttempt.failed.get.getMessage.contains("alias, too long"))
|
||||
|
||||
// destroy conf files after the test
|
||||
Files.walkFileTree(tempConfParentDir.toPath, new SimpleFileVisitor[Path]() {
|
||||
override def visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult = {
|
||||
Files.deleteIfExists(file)
|
||||
FileVisitResult.CONTINUE
|
||||
}
|
||||
})
|
||||
|
||||
tempConfParentDir.listFiles.foreach(_.delete())
|
||||
tempConfParentDir.deleteOnExit()
|
||||
}
|
||||
|
||||
}
|
|
@ -19,6 +19,7 @@ package fr.acinq.eclair.api
|
|||
import java.net.{InetAddress, InetSocketAddress}
|
||||
|
||||
import fr.acinq.bitcoin.{BinaryData, OutPoint}
|
||||
import fr.acinq.eclair.payment.PaymentRequest
|
||||
import fr.acinq.eclair.transactions.{IN, OUT}
|
||||
import fr.acinq.eclair.wire.NodeAddress
|
||||
import org.json4s.jackson.Serialization
|
||||
|
@ -61,4 +62,9 @@ class JsonSerializersSpec extends FunSuite with Matchers {
|
|||
Serialization.write(OUT)(org.json4s.DefaultFormats + new DirectionSerializer) shouldBe s""""OUT""""
|
||||
}
|
||||
|
||||
test("Payment Request") {
|
||||
val ref = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp"
|
||||
val pr = PaymentRequest.read(ref)
|
||||
Serialization.write(pr)(org.json4s.DefaultFormats + new PaymentRequestSerializer) shouldBe """{"prefix":"lnbc","amount":250000000,"timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"minFinalCltvExpiry":null}"""
|
||||
}
|
||||
}
|
||||
|
|
|
@ -322,10 +322,10 @@ class RelayerSpec extends TestkitBaseClass {
|
|||
sender.send(relayer, Status.Failure(AddHtlcFailed(channelId_bc, paymentHash, ChannelUnavailable(channelId_bc), origin, Some(channelUpdate_bc_disabled), None)))
|
||||
assert(register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message.reason === Right(ChannelDisabled(channelUpdate_bc_disabled.messageFlags, channelUpdate_bc_disabled.channelFlags, channelUpdate_bc_disabled)))
|
||||
|
||||
sender.send(relayer, Status.Failure(AddHtlcFailed(channelId_bc, paymentHash, HtlcTimedout(channelId_bc), origin, None, None)))
|
||||
sender.send(relayer, Status.Failure(AddHtlcFailed(channelId_bc, paymentHash, HtlcTimedout(channelId_bc, Set.empty), origin, None, None)))
|
||||
assert(register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message.reason === Right(PermanentChannelFailure))
|
||||
|
||||
sender.send(relayer, Status.Failure(AddHtlcFailed(channelId_bc, paymentHash, HtlcTimedout(channelId_bc), origin, Some(channelUpdate_bc), None)))
|
||||
sender.send(relayer, Status.Failure(AddHtlcFailed(channelId_bc, paymentHash, HtlcTimedout(channelId_bc, Set.empty), origin, Some(channelUpdate_bc), None)))
|
||||
assert(register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message.reason === Right(PermanentChannelFailure))
|
||||
|
||||
register.expectNoMsg(100 millis)
|
||||
|
|
|
@ -19,11 +19,11 @@ package fr.acinq.eclair.router
|
|||
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
|
||||
import fr.acinq.bitcoin.{BinaryData, Block, Crypto}
|
||||
import fr.acinq.eclair.payment.PaymentRequest.ExtraHop
|
||||
import fr.acinq.eclair.router.Graph.GraphStructure.DirectedGraph.graphEdgeToHop
|
||||
import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge}
|
||||
import fr.acinq.eclair.wire._
|
||||
import fr.acinq.eclair.{ShortChannelId, nodeFee, randomKey}
|
||||
import fr.acinq.eclair.{ShortChannelId, randomKey}
|
||||
import org.scalatest.FunSuite
|
||||
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
/**
|
||||
|
@ -36,14 +36,6 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
val (a, b, c, d, e) = (randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey)
|
||||
|
||||
|
||||
// the total fee cost for this path
|
||||
def pathCost(path: Seq[Hop], amountMsat: Long): Long = {
|
||||
path.drop(1).reverse.foldLeft(amountMsat) { (fee, hop) =>
|
||||
fee + nodeFee(hop.lastUpdate.feeBaseMsat, hop.lastUpdate.feeProportionalMillionths, fee)
|
||||
}
|
||||
}
|
||||
|
||||
test("calculate simple route") {
|
||||
|
||||
val updates = List(
|
||||
|
@ -55,7 +47,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
val g = makeGraph(updates)
|
||||
|
||||
val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT)
|
||||
val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
|
||||
|
||||
assert(route.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil))
|
||||
}
|
||||
|
@ -93,16 +85,16 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
val graph = makeGraph(updates)
|
||||
|
||||
val Success(route) = Router.findRoute(graph, a, d, amountMsat)
|
||||
val Success(route) = Router.findRoute(graph, a, d, amountMsat, numRoutes = 1)
|
||||
|
||||
assert(hops2Ids(route) === 4 :: 5 :: 6 :: Nil)
|
||||
assert(pathCost(route, amountMsat) === expectedCost)
|
||||
assert(Graph.pathCost(hops2Edges(route), amountMsat) === expectedCost)
|
||||
|
||||
// now channel 5 could route the amount (10000) but not the amount + fees (10007)
|
||||
val (desc, update) = makeUpdate(5L, e, f, feeBaseMsat = 1, feeProportionalMillionth = 400, minHtlcMsat = 0, maxHtlcMsat = Some(10005))
|
||||
val graph1 = graph.addEdge(desc, update)
|
||||
|
||||
val Success(route1) = Router.findRoute(graph1, a, d, amountMsat)
|
||||
val Success(route1) = Router.findRoute(graph1, a, d, amountMsat, numRoutes = 1)
|
||||
|
||||
assert(hops2Ids(route1) === 1 :: 2 :: 3 :: Nil)
|
||||
}
|
||||
|
@ -117,7 +109,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
).toMap
|
||||
|
||||
val g = makeGraph(updates)
|
||||
val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT)
|
||||
val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
|
||||
|
||||
assert(route.map(hops2Ids) === Success(2 :: 5 :: Nil))
|
||||
}
|
||||
|
@ -133,11 +125,11 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
val g = makeGraph(updates)
|
||||
|
||||
val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT)
|
||||
val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
|
||||
assert(route1.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil))
|
||||
|
||||
val graphWithRemovedEdge = g.removeEdge(ChannelDesc(ShortChannelId(3L), c, d))
|
||||
val route2 = Router.findRoute(graphWithRemovedEdge, a, e, DEFAULT_AMOUNT_MSAT)
|
||||
val route2 = Router.findRoute(graphWithRemovedEdge, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
|
||||
assert(route2.map(hops2Ids) === Failure(RouteNotFound))
|
||||
}
|
||||
|
||||
|
@ -159,7 +151,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
val graph = makeGraph(updates)
|
||||
|
||||
val route = Router.findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT)
|
||||
val route = Router.findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
|
||||
assert(route.map(hops2Ids) === Success(4 :: 3 :: Nil))
|
||||
|
||||
}
|
||||
|
@ -182,7 +174,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
val graph = makeGraph(updates)
|
||||
|
||||
val route = Router.findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT)
|
||||
val route = Router.findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
|
||||
assert(route.map(hops2Ids) === Success(4 :: 3 :: Nil))
|
||||
}
|
||||
|
||||
|
@ -204,7 +196,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
val graph = makeGraph(updates)
|
||||
|
||||
val route = Router.findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT)
|
||||
val route = Router.findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
|
||||
assert(route.map(hops2Ids) === Success(1 :: 6 :: 3 :: Nil))
|
||||
}
|
||||
|
||||
|
@ -220,7 +212,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
val g = makeGraph(updates)
|
||||
|
||||
val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT)
|
||||
val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
|
||||
assert(route.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil))
|
||||
}
|
||||
|
||||
|
@ -233,7 +225,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
val g = makeGraph(updates)
|
||||
|
||||
val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT)
|
||||
val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
|
||||
assert(route.map(hops2Ids) === Failure(RouteNotFound))
|
||||
}
|
||||
|
||||
|
@ -247,7 +239,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
val g = makeGraph(updates)
|
||||
|
||||
val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT)
|
||||
val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
|
||||
assert(route.map(hops2Ids) === Failure(RouteNotFound))
|
||||
}
|
||||
|
||||
|
@ -260,7 +252,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
val g = makeGraph(updates).addVertex(a)
|
||||
|
||||
val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT)
|
||||
val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
|
||||
assert(route.map(hops2Ids) === Failure(RouteNotFound))
|
||||
}
|
||||
|
||||
|
@ -274,7 +266,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
val g = makeGraph(updates)
|
||||
|
||||
val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT)
|
||||
val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
|
||||
assert(route.map(hops2Ids) === Failure(RouteNotFound))
|
||||
}
|
||||
|
||||
|
@ -288,7 +280,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
val g = makeGraph(updates)
|
||||
|
||||
val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT)
|
||||
val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
|
||||
assert(route.map(hops2Ids) === Failure(RouteNotFound))
|
||||
}
|
||||
|
||||
|
@ -304,7 +296,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
val g = makeGraph(updates)
|
||||
|
||||
val route = Router.findRoute(g, a, d, highAmount)
|
||||
val route = Router.findRoute(g, a, d, highAmount, numRoutes = 1)
|
||||
assert(route.map(hops2Ids) === Failure(RouteNotFound))
|
||||
|
||||
}
|
||||
|
@ -321,7 +313,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
val g = makeGraph(updates)
|
||||
|
||||
val route = Router.findRoute(g, a, d, lowAmount)
|
||||
val route = Router.findRoute(g, a, d, lowAmount, numRoutes = 1)
|
||||
assert(route.map(hops2Ids) === Failure(RouteNotFound))
|
||||
|
||||
}
|
||||
|
@ -336,7 +328,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
val g = makeGraph(updates)
|
||||
|
||||
val route = Router.findRoute(g, a, a, DEFAULT_AMOUNT_MSAT)
|
||||
val route = Router.findRoute(g, a, a, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
|
||||
assert(route.map(hops2Ids) === Failure(CannotRouteToSelf))
|
||||
}
|
||||
|
||||
|
@ -351,7 +343,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
val g = makeGraph(updates)
|
||||
|
||||
val route = Router.findRoute(g, a, b, DEFAULT_AMOUNT_MSAT)
|
||||
val route = Router.findRoute(g, a, b, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
|
||||
assert(route.map(hops2Ids) === Success(1 :: Nil))
|
||||
}
|
||||
|
||||
|
@ -367,10 +359,10 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
val g = makeGraph(updates)
|
||||
|
||||
val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT)
|
||||
val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
|
||||
assert(route1.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil))
|
||||
|
||||
val route2 = Router.findRoute(g, e, a, DEFAULT_AMOUNT_MSAT)
|
||||
val route2 = Router.findRoute(g, e, a, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
|
||||
assert(route2.map(hops2Ids) === Failure(RouteNotFound))
|
||||
}
|
||||
|
||||
|
@ -407,7 +399,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
val g = makeGraph(updates)
|
||||
|
||||
val hops = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT).get
|
||||
val hops = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1).get
|
||||
|
||||
assert(hops === Hop(a, b, uab) :: Hop(b, c, ubc) :: Hop(c, d, ucd) :: Hop(d, e, ude) :: Nil)
|
||||
}
|
||||
|
@ -447,7 +439,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
val g = makeGraph(updates)
|
||||
|
||||
val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, ignoredEdges = Set(ChannelDesc(ShortChannelId(3L), c, d)))
|
||||
val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1 , ignoredEdges = Set(ChannelDesc(ShortChannelId(3L), c, d)))
|
||||
assert(route1.map(hops2Ids) === Failure(RouteNotFound))
|
||||
|
||||
// verify that we left the graph untouched
|
||||
|
@ -456,7 +448,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
assert(g.containsVertex(d))
|
||||
|
||||
// make sure we can find a route if without the blacklist
|
||||
val route2 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT)
|
||||
val route2 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
|
||||
assert(route2.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil))
|
||||
}
|
||||
|
||||
|
@ -469,14 +461,14 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
val g = makeGraph(updates)
|
||||
|
||||
val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT)
|
||||
val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
|
||||
assert(route.map(hops2Ids) === Failure(RouteNotFound))
|
||||
|
||||
// now we add the missing edge to reach the destination
|
||||
val (extraDesc, extraUpdate) = makeUpdate(4L, d, e, 5, 5)
|
||||
val extraGraphEdges = Set(GraphEdge(extraDesc, extraUpdate))
|
||||
|
||||
val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, extraEdges = extraGraphEdges)
|
||||
val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, extraEdges = extraGraphEdges)
|
||||
assert(route1.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil))
|
||||
}
|
||||
|
||||
|
@ -491,7 +483,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
val g = makeGraph(updates)
|
||||
|
||||
val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT)
|
||||
val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
|
||||
assert(route1.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil))
|
||||
assert(route1.get(1).lastUpdate.feeBaseMsat == 10)
|
||||
|
||||
|
@ -499,7 +491,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
val extraGraphEdges = Set(GraphEdge(extraDesc, extraUpdate))
|
||||
|
||||
val route2 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, extraEdges = extraGraphEdges)
|
||||
val route2 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, extraEdges = extraGraphEdges)
|
||||
assert(route2.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil))
|
||||
assert(route2.get(1).lastUpdate.feeBaseMsat == 5)
|
||||
}
|
||||
|
@ -557,10 +549,10 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
val g = makeGraph(updates)
|
||||
|
||||
assert(Router.findRoute(g, nodes(0), nodes(18), DEFAULT_AMOUNT_MSAT).map(hops2Ids) === Success(0 until 18))
|
||||
assert(Router.findRoute(g, nodes(0), nodes(19), DEFAULT_AMOUNT_MSAT).map(hops2Ids) === Success(0 until 19))
|
||||
assert(Router.findRoute(g, nodes(0), nodes(20), DEFAULT_AMOUNT_MSAT).map(hops2Ids) === Success(0 until 20))
|
||||
assert(Router.findRoute(g, nodes(0), nodes(21), DEFAULT_AMOUNT_MSAT).map(hops2Ids) === Failure(RouteNotFound))
|
||||
assert(Router.findRoute(g, nodes(0), nodes(18), DEFAULT_AMOUNT_MSAT, numRoutes = 1).map(hops2Ids) === Success(0 until 18))
|
||||
assert(Router.findRoute(g, nodes(0), nodes(19), DEFAULT_AMOUNT_MSAT, numRoutes = 1).map(hops2Ids) === Success(0 until 19))
|
||||
assert(Router.findRoute(g, nodes(0), nodes(20), DEFAULT_AMOUNT_MSAT, numRoutes = 1).map(hops2Ids) === Success(0 until 20))
|
||||
assert(Router.findRoute(g, nodes(0), nodes(21), DEFAULT_AMOUNT_MSAT, numRoutes = 1).map(hops2Ids) === Failure(RouteNotFound))
|
||||
}
|
||||
|
||||
test("ignore cheaper route when it has more than 20 hops") {
|
||||
|
@ -577,7 +569,7 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
val g = makeGraph(updates2)
|
||||
|
||||
val route = Router.findRoute(g, nodes(0), nodes(49), DEFAULT_AMOUNT_MSAT)
|
||||
val route = Router.findRoute(g, nodes(0), nodes(49), DEFAULT_AMOUNT_MSAT, numRoutes = 1)
|
||||
assert(route.map(hops2Ids) === Success(0 :: 1 :: 99 :: 48 :: Nil))
|
||||
}
|
||||
|
||||
|
@ -593,10 +585,145 @@ class RouteCalculationSpec extends FunSuite {
|
|||
|
||||
val g = makeGraph(updates)
|
||||
|
||||
val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT)
|
||||
val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1)
|
||||
assert(route1.map(hops2Ids) === Success(1 :: 2 :: 4 :: 5 :: Nil))
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* +---+ +---+ +---+
|
||||
* | A +-----+ | B +----------> | C |
|
||||
* +-+-+ | +-+-+ +-+-+
|
||||
* ^ | ^ |
|
||||
* | | | |
|
||||
* | v----> + | |
|
||||
* +-+-+ <-+-+ +-+-+
|
||||
* | D +----------> | E +----------> | F |
|
||||
* +---+ +---+ +---+
|
||||
*
|
||||
*/
|
||||
test("find the k-shortest paths in a graph, k=4") {
|
||||
|
||||
val (a, b, c, d, e, f) = (
|
||||
PublicKey("02999fa724ec3c244e4da52b4a91ad421dc96c9a810587849cd4b2469313519c73"), //a
|
||||
PublicKey("03f1cb1af20fe9ccda3ea128e27d7c39ee27375c8480f11a87c17197e97541ca6a"), //b
|
||||
PublicKey("0358e32d245ff5f5a3eb14c78c6f69c67cea7846bdf9aeeb7199e8f6fbb0306484"), //c
|
||||
PublicKey("029e059b6780f155f38e83601969919aae631ddf6faed58fe860c72225eb327d7c"), //d
|
||||
PublicKey("02f38f4e37142cc05df44683a83e22dea608cf4691492829ff4cf99888c5ec2d3a"), //e
|
||||
PublicKey("03fc5b91ce2d857f146fd9b986363374ffe04dc143d8bcd6d7664c8873c463cdfc") //f
|
||||
)
|
||||
|
||||
|
||||
val edges = Seq(
|
||||
makeUpdate(1L, d, a, 1, 0),
|
||||
makeUpdate(2L, d, e, 1, 0),
|
||||
makeUpdate(3L, a, e, 1, 0),
|
||||
makeUpdate(4L, e, b, 1, 0),
|
||||
makeUpdate(5L, e, f, 1, 0),
|
||||
makeUpdate(6L, b, c, 1, 0),
|
||||
makeUpdate(7L, c, f, 1, 0)
|
||||
).toMap
|
||||
|
||||
val graph = DirectedGraph.makeGraph(edges)
|
||||
|
||||
val fourShortestPaths = Graph.yenKshortestPaths(graph, d, f, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, pathsToFind = 4)
|
||||
|
||||
assert(fourShortestPaths.size === 4)
|
||||
assert(hops2Ids(fourShortestPaths(0).path.map(graphEdgeToHop)) === 2 :: 5 :: Nil) // D -> E -> F
|
||||
assert(hops2Ids(fourShortestPaths(1).path.map(graphEdgeToHop)) === 1 :: 3 :: 5 :: Nil) // D -> A -> E -> F
|
||||
assert(hops2Ids(fourShortestPaths(2).path.map(graphEdgeToHop)) === 2 :: 4 :: 6 :: 7 :: Nil) // D -> E -> B -> C -> F
|
||||
assert(hops2Ids(fourShortestPaths(3).path.map(graphEdgeToHop)) === 1 :: 3 :: 4 :: 6 :: 7 :: Nil) // D -> A -> E -> B -> C -> F
|
||||
}
|
||||
|
||||
test("find the k shortest path (wikipedia example)") {
|
||||
val (c, d, e, f, g, h) = (
|
||||
PublicKey("02999fa724ec3c244e4da52b4a91ad421dc96c9a810587849cd4b2469313519c73"), //c
|
||||
PublicKey("03f1cb1af20fe9ccda3ea128e27d7c39ee27375c8480f11a87c17197e97541ca6a"), //d
|
||||
PublicKey("0358e32d245ff5f5a3eb14c78c6f69c67cea7846bdf9aeeb7199e8f6fbb0306484"), //e
|
||||
PublicKey("029e059b6780f155f38e83601969919aae631ddf6faed58fe860c72225eb327d7c"), //f
|
||||
PublicKey("02f38f4e37142cc05df44683a83e22dea608cf4691492829ff4cf99888c5ec2d3a"), //g
|
||||
PublicKey("03fc5b91ce2d857f146fd9b986363374ffe04dc143d8bcd6d7664c8873c463cdfc") //h
|
||||
)
|
||||
|
||||
|
||||
val edges = Seq(
|
||||
makeUpdate(10L, c, e, 2, 0),
|
||||
makeUpdate(20L, c, d, 3, 0),
|
||||
makeUpdate(30L, d, f, 4, 5), // D- > F has a higher cost to distinguish it from the 2nd cheapest route
|
||||
makeUpdate(40L, e, d, 1, 0),
|
||||
makeUpdate(50L, e, f, 2, 0),
|
||||
makeUpdate(60L, e, g, 3, 0),
|
||||
makeUpdate(70L, f, g, 2, 0),
|
||||
makeUpdate(80L, f, h, 1, 0),
|
||||
makeUpdate(90L, g, h, 2, 0)
|
||||
)
|
||||
|
||||
val graph = DirectedGraph().addEdges(edges)
|
||||
|
||||
val twoShortestPaths = Graph.yenKshortestPaths(graph, c, h, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, pathsToFind = 2)
|
||||
|
||||
assert(twoShortestPaths.size === 2)
|
||||
val shortest = twoShortestPaths(0)
|
||||
assert(hops2Ids(shortest.path.map(graphEdgeToHop)) === 10 :: 50 :: 80 :: Nil) // C -> E -> F -> H
|
||||
|
||||
val secondShortest = twoShortestPaths(1)
|
||||
assert(hops2Ids(secondShortest.path.map(graphEdgeToHop)) === 10 :: 60 :: 90 :: Nil) // C -> E -> G -> H
|
||||
}
|
||||
|
||||
test("terminate looking for k-shortest path if there are no more alternative paths than k"){
|
||||
|
||||
val f = randomKey.publicKey
|
||||
|
||||
// simple graph with only 2 possible paths from A to F
|
||||
val edges = Seq(
|
||||
makeUpdate(1L, a, b, 1, 0),
|
||||
makeUpdate(2L, b, c, 1, 0),
|
||||
makeUpdate(3L, c, f, 1, 0),
|
||||
makeUpdate(4L, c, d, 1, 0),
|
||||
makeUpdate(5L, d, e, 1, 0),
|
||||
makeUpdate(6L, e, f, 1, 0)
|
||||
)
|
||||
|
||||
val graph = DirectedGraph().addEdges(edges)
|
||||
|
||||
//we ask for 3 shortest paths but only 2 can be found
|
||||
val foundPaths = Graph.yenKshortestPaths(graph, a, f, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, pathsToFind = 3)
|
||||
|
||||
assert(foundPaths.size === 2)
|
||||
assert(hops2Ids(foundPaths(0).path.map(graphEdgeToHop)) === 1 :: 2 :: 3 :: Nil) // A -> B -> C -> F
|
||||
assert(hops2Ids(foundPaths(1).path.map(graphEdgeToHop)) === 1 :: 2 :: 4 :: 5 :: 6 :: Nil) // A -> B -> C -> D -> E -> F
|
||||
}
|
||||
|
||||
test("select a random route below the allowed fee spread") {
|
||||
|
||||
val f = randomKey.publicKey
|
||||
|
||||
// A -> B -> C -> D has total cost of 10000005
|
||||
// A -> E -> C -> D has total cost of 11080003 !!
|
||||
// A -> E -> F -> D has total cost of 10000006
|
||||
val g = makeGraph(List(
|
||||
makeUpdate(1L, a, b, feeBaseMsat = 1, 0),
|
||||
makeUpdate(4L, a, e, feeBaseMsat = 1, 0),
|
||||
makeUpdate(2L, b, c, feeBaseMsat = 2, 0),
|
||||
makeUpdate(3L, c, d, feeBaseMsat = 3, 0),
|
||||
makeUpdate(5L, e, f, feeBaseMsat = 3, 0),
|
||||
makeUpdate(6L, f, d, feeBaseMsat = 3, 0),
|
||||
makeUpdate(7L, e, c, feeBaseMsat = 90000, 99000)
|
||||
).toMap)
|
||||
|
||||
(for { _ <- 0 to 10 } yield Router.findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 3)).map {
|
||||
case Failure(_) => assert(false)
|
||||
case Success(someRoute) =>
|
||||
|
||||
val routeCost = Graph.pathCost(hops2Edges(someRoute), DEFAULT_AMOUNT_MSAT)
|
||||
val allowedSpread = DEFAULT_AMOUNT_MSAT + (DEFAULT_AMOUNT_MSAT * Router.DEFAULT_ALLOWED_SPREAD)
|
||||
|
||||
// over the three routes we could only get the 2 cheapest because the third is too expensive (over 10% of the cheapest)
|
||||
assert(routeCost == 10000005 || routeCost == 10000006)
|
||||
assert(routeCost < allowedSpread)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object RouteCalculationSpec {
|
||||
|
@ -632,4 +759,6 @@ object RouteCalculationSpec {
|
|||
|
||||
def hops2Ids(route: Seq[Hop]) = route.map(hop => hop.lastUpdate.shortChannelId.toLong)
|
||||
|
||||
def hops2Edges(route: Seq[Hop]) = route.map(hop => GraphEdge(ChannelDesc(hop.lastUpdate.shortChannelId, hop.nodeId, hop.nextNodeId), hop.lastUpdate))
|
||||
|
||||
}
|
||||
|
|
|
@ -15,10 +15,26 @@
|
|||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.text-xs {
|
||||
-fx-font-size: 10px;
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
-fx-font-size: 12px;
|
||||
}
|
||||
|
||||
.text-md {
|
||||
-fx-font-size: 14px;
|
||||
}
|
||||
|
||||
.text-lg {
|
||||
-fx-font-size: 16px;
|
||||
}
|
||||
|
||||
.text-xl {
|
||||
-fx-font-size: 18px;
|
||||
}
|
||||
|
||||
.text-error {
|
||||
-fx-text-fill: rgb(216,31,74);
|
||||
-fx-font-size: 11px;
|
||||
|
@ -115,15 +131,13 @@
|
|||
/* ---------- Progress Bar ---------- */
|
||||
|
||||
.bar {
|
||||
-fx-background-color: rgb(63,179,234);
|
||||
-fx-background-color: rgb(114,193,229);
|
||||
-fx-background-insets: 0;
|
||||
-fx-background-radius: 0;
|
||||
}
|
||||
|
||||
.track {
|
||||
-fx-background-color: rgb(206,230,255);
|
||||
-fx-background-color: rgb(211,227,234);
|
||||
-fx-background-insets: 0;
|
||||
-fx-background-radius: 0;
|
||||
}
|
||||
|
||||
/* ---------- Forms ----------- */
|
||||
|
|
|
@ -1,8 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import java.net.URL?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.ProgressBar?>
|
||||
<?import javafx.scene.control.TextField?>
|
||||
<?import javafx.scene.layout.ColumnConstraints?>
|
||||
<?import javafx.scene.layout.GridPane?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.RowConstraints?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
|
||||
<!--
|
||||
~ Copyright 2018 ACINQ SAS
|
||||
~
|
||||
|
@ -19,67 +28,66 @@
|
|||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<VBox fx:id="root" styleClass="channel" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"
|
||||
onContextMenuRequested="#openChannelContext">
|
||||
<VBox fx:id="root" onContextMenuRequested="#openChannelContext" styleClass="channel" xmlns="http://javafx.com/javafx/8.0.172-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<stylesheets>
|
||||
<URL value="@../commons/globals.css"/>
|
||||
<URL value="@./main.css"/>
|
||||
<URL value="@../commons/globals.css" />
|
||||
<URL value="@./main.css" />
|
||||
</stylesheets>
|
||||
<GridPane styleClass="grid" prefWidth="400.0">
|
||||
<GridPane prefWidth="600.0" styleClass="grid">
|
||||
<columnConstraints>
|
||||
<ColumnConstraints hgrow="SOMETIMES" minWidth="140.0" prefWidth="150.0" maxWidth="180.0"/>
|
||||
<ColumnConstraints hgrow="ALWAYS" minWidth="30.0" prefWidth="30.0"/>
|
||||
<ColumnConstraints hgrow="SOMETIMES" minWidth="40.0" prefWidth="60.0" maxWidth="60.0"/>
|
||||
<ColumnConstraints hgrow="ALWAYS" minWidth="30.0" prefWidth="40.0"/>
|
||||
<ColumnConstraints hgrow="SOMETIMES" maxWidth="380.0" minWidth="100.0" prefWidth="100.0" />
|
||||
<ColumnConstraints hgrow="NEVER" maxWidth="1.0" minWidth="1.0" prefWidth="1.0" />
|
||||
<ColumnConstraints hgrow="NEVER" maxWidth="90.0" minWidth="90.0" prefWidth="90.0" />
|
||||
<ColumnConstraints hgrow="SOMETIMES" prefWidth="100.0" />
|
||||
<ColumnConstraints hgrow="NEVER" maxWidth="50.0" minWidth="50.0" prefWidth="50.0" />
|
||||
<ColumnConstraints hgrow="NEVER" />
|
||||
</columnConstraints>
|
||||
<rowConstraints>
|
||||
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
|
||||
<RowConstraints minHeight="4.0" vgrow="SOMETIMES"/>
|
||||
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
|
||||
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
|
||||
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
|
||||
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
|
||||
<RowConstraints minHeight="4.0" vgrow="SOMETIMES" />
|
||||
<RowConstraints minHeight="4.0" vgrow="SOMETIMES" />
|
||||
<RowConstraints minHeight="4.0" vgrow="SOMETIMES" />
|
||||
<RowConstraints minHeight="4.0" vgrow="SOMETIMES" />
|
||||
<RowConstraints minHeight="4.0" vgrow="SOMETIMES" />
|
||||
</rowConstraints>
|
||||
|
||||
<HBox GridPane.columnSpan="4" GridPane.columnIndex="0" alignment="CENTER" spacing="5.0">
|
||||
<TextField fx:id="channelId" text="N/A" editable="false" styleClass="noteditable, text-strong"
|
||||
HBox.hgrow="ALWAYS" GridPane.valignment="BOTTOM" focusTraversable="false"/>
|
||||
<HBox GridPane.columnIndex="4" GridPane.halignment="RIGHT" alignment="CENTER_RIGHT" HBox.hgrow="NEVER" spacing="5">
|
||||
<Button fx:id="close" mnemonicParsing="false" styleClass="close-channel" text="Close" visible="false"/>
|
||||
<Button fx:id="forceclose" mnemonicParsing="false" styleClass="forceclose-channel" text="Force close" visible="false"/>
|
||||
</HBox>
|
||||
<VBox alignment="CENTER_RIGHT" GridPane.columnIndex="0" GridPane.rowIndex="0" GridPane.rowSpan="2147483647">
|
||||
<Label styleClass="text-muted, text-xs" text="BALANCE" />
|
||||
<Label fx:id="amountUs" alignment="CENTER_RIGHT" styleClass="text-lg, channel-balance" text="N/A" />
|
||||
<ProgressBar fx:id="balanceBar" maxWidth="120.0" minHeight="4.0" prefHeight="4.0" progress="0.0" snapToPixel="false">
|
||||
<VBox.margin>
|
||||
<Insets top="3.0" />
|
||||
</VBox.margin>
|
||||
</ProgressBar>
|
||||
</VBox>
|
||||
|
||||
<HBox alignment="BOTTOM_LEFT" spacing="5" GridPane.columnIndex="2" GridPane.columnSpan="3" GridPane.rowIndex="0" GridPane.valignment="BOTTOM">
|
||||
<Label styleClass="text-strong, text-md" text="With" />
|
||||
<Label fx:id="nodeAlias" maxWidth="120.0" styleClass="text-md, channel-peer-alias" visible="false"/>
|
||||
<TextField fx:id="nodeId" editable="false" focusTraversable="false" styleClass="noteditable, text-strong, text-md" text="N/A" HBox.hgrow="ALWAYS" />
|
||||
</HBox>
|
||||
|
||||
<ProgressBar fx:id="balanceBar" minHeight="4.0" prefHeight="4.0" maxWidth="1.7976931348623157E308"
|
||||
progress="0.0" snapToPixel="false"
|
||||
GridPane.columnSpan="4" GridPane.hgrow="ALWAYS" GridPane.rowIndex="1"/>
|
||||
<HBox alignment="CENTER_RIGHT" spacing="5" GridPane.columnIndex="5" GridPane.halignment="RIGHT" GridPane.rowIndex="0" HBox.hgrow="NEVER">
|
||||
<Button fx:id="close" mnemonicParsing="false" styleClass="close-channel" text="Close" visible="false" />
|
||||
<Button fx:id="forceclose" mnemonicParsing="false" styleClass="forceclose-channel" text="Force close" visible="false" />
|
||||
</HBox>
|
||||
|
||||
<Label styleClass="text-muted" text="Funding tx id" GridPane.columnIndex="0" GridPane.rowIndex="2"/>
|
||||
<TextField fx:id="txId" text="N/A" focusTraversable="false" editable="false" styleClass="noteditable"
|
||||
GridPane.columnIndex="1" GridPane.columnSpan="3" GridPane.rowIndex="2"/>
|
||||
<Label styleClass="text-muted" text="State" GridPane.columnIndex="2" GridPane.rowIndex="1" />
|
||||
<TextField fx:id="state" editable="false" focusTraversable="false" styleClass="noteditable" text="N/A" GridPane.columnIndex="3" GridPane.columnSpan="3" GridPane.rowIndex="1" />
|
||||
|
||||
<Label styleClass="text-muted" text="Remote node id" GridPane.columnIndex="0" GridPane.rowIndex="3"/>
|
||||
<TextField fx:id="nodeId" text="N/A" focusTraversable="false" editable="false" styleClass="noteditable"
|
||||
GridPane.columnIndex="1" GridPane.columnSpan="3" GridPane.rowIndex="3"/>
|
||||
<Label styleClass="text-muted" text="Funder" GridPane.columnIndex="4" GridPane.halignment="RIGHT" GridPane.rowIndex="4" />
|
||||
<TextField fx:id="funder" editable="false" focusTraversable="false" styleClass="noteditable" text="N/A" GridPane.columnIndex="5" GridPane.rowIndex="4" />
|
||||
|
||||
<Label styleClass="text-muted" text="Your balance" GridPane.rowIndex="4"/>
|
||||
<TextField fx:id="amountUs" text="N/A" focusTraversable="false" editable="false"
|
||||
styleClass="noteditable"
|
||||
GridPane.columnIndex="1" GridPane.rowIndex="4"/>
|
||||
<Label styleClass="text-muted" text="Channel id" GridPane.columnIndex="2" GridPane.rowIndex="2" />
|
||||
<TextField fx:id="channelId" editable="false" focusTraversable="false" styleClass="noteditable" text="N/A" GridPane.columnIndex="3" GridPane.columnSpan="3" GridPane.rowIndex="2" />
|
||||
|
||||
<Label styleClass="text-muted" text="Capacity" GridPane.rowIndex="5"/>
|
||||
<TextField fx:id="capacity" text="N/A" focusTraversable="false" editable="false"
|
||||
styleClass="noteditable"
|
||||
GridPane.columnIndex="1" GridPane.rowIndex="5"/>
|
||||
<Label styleClass="text-muted" text="Short channel id" GridPane.columnIndex="2" GridPane.rowIndex="3" />
|
||||
<TextField fx:id="shortChannelId" editable="false" focusTraversable="false" styleClass="noteditable" text="N/A" GridPane.columnIndex="3" GridPane.columnSpan="3" GridPane.rowIndex="3" />
|
||||
|
||||
<Label styleClass="text-muted" text="Funder" GridPane.columnIndex="2" GridPane.rowIndex="4"/>
|
||||
<TextField fx:id="funder" text="N/A" focusTraversable="false" editable="false" styleClass="noteditable"
|
||||
GridPane.columnIndex="3" GridPane.rowIndex="4"/>
|
||||
<Label styleClass="text-muted" text="Funding tx id" GridPane.columnIndex="2" GridPane.rowIndex="4" />
|
||||
<TextField fx:id="txId" editable="false" focusTraversable="false" styleClass="noteditable" text="N/A" GridPane.columnIndex="3" GridPane.rowIndex="4" />
|
||||
|
||||
<Label styleClass="text-muted" text="State" GridPane.columnIndex="2" GridPane.rowIndex="5"/>
|
||||
<TextField fx:id="state" text="N/A" focusTraversable="false" editable="false" styleClass="noteditable"
|
||||
GridPane.columnIndex="3" GridPane.rowIndex="5"/>
|
||||
<HBox prefHeight="100.0" prefWidth="1.0" styleClass="channel-balance-separator" GridPane.columnIndex="1" GridPane.rowSpan="2147483647" />
|
||||
|
||||
</GridPane>
|
||||
<HBox styleClass="channel-separator"/>
|
||||
<HBox styleClass="channel-separator" />
|
||||
</VBox>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* ---------- Status Bar ---------- */
|
||||
|
||||
.status-bar {
|
||||
-fx-padding: .5em 1em;
|
||||
-fx-padding: .5em .7em;
|
||||
-fx-background-color: rgb(221,221,221);
|
||||
-fx-border-width: 1px 0 0 0;
|
||||
-fx-border-color: rgb(181,181,181);
|
||||
|
@ -50,18 +50,28 @@
|
|||
}
|
||||
|
||||
.channel .grid {
|
||||
-fx-padding: 10px;
|
||||
-fx-padding: 14px;
|
||||
-fx-vgap: 3px;
|
||||
-fx-hgap: 5px;
|
||||
-fx-hgap: 1em;
|
||||
-fx-font-size: 12px;
|
||||
}
|
||||
|
||||
.channel-separator {
|
||||
-fx-background-color: rgb(220,220,220);
|
||||
-fx-background-color: rgb(190,200,210);
|
||||
-fx-pref-height: 1px;
|
||||
-fx-padding: 0 -.25em;
|
||||
}
|
||||
|
||||
.channel-balance-separator {
|
||||
-fx-background-color: rgb(225,230,235);
|
||||
}
|
||||
|
||||
.channel-peer-alias {
|
||||
-fx-background-color: rgb(232,233,235);
|
||||
-fx-background-radius: 2;
|
||||
-fx-label-padding: 0 3px;
|
||||
}
|
||||
|
||||
.channels-info {
|
||||
-fx-padding: 4em 0 0 0;
|
||||
}
|
||||
|
|
|
@ -209,51 +209,40 @@
|
|||
</center>
|
||||
<bottom>
|
||||
<HBox fx:id="statusBarBox" styleClass="status-bar" spacing="10">
|
||||
<children>
|
||||
<HBox alignment="CENTER_LEFT" HBox.hgrow="ALWAYS" onContextMenuRequested="#openNodeIdContext">
|
||||
<children>
|
||||
<ImageView fitHeight="16.0" fitWidth="27.0" opacity="0.52" pickOnBounds="true"
|
||||
preserveRatio="true">
|
||||
<image>
|
||||
<Image url="@../commons/images/eclair-shape.png"/>
|
||||
</image>
|
||||
</ImageView>
|
||||
<Label fx:id="labelNodeId" text="N/A"/>
|
||||
</children>
|
||||
</HBox>
|
||||
<HBox alignment="CENTER_LEFT" HBox.hgrow="SOMETIMES" minWidth="80.0">
|
||||
<children>
|
||||
<Separator orientation="VERTICAL"/>
|
||||
<Rectangle fx:id="rectRGB" width="7" height="7" fill="transparent"/>
|
||||
<Label fx:id="labelAlias" text="N/A"/>
|
||||
</children>
|
||||
</HBox>
|
||||
<HBox alignment="CENTER_LEFT" HBox.hgrow="NEVER" minWidth="85.0">
|
||||
<children>
|
||||
<Separator orientation="VERTICAL"/>
|
||||
<Label text="HTTP" styleClass="badge, badge-http"/>
|
||||
<Label fx:id="labelApi" styleClass="value" text="N/A"/>
|
||||
</children>
|
||||
</HBox>
|
||||
<HBox alignment="CENTER_LEFT" HBox.hgrow="NEVER" minWidth="85.0">
|
||||
<children>
|
||||
<Separator orientation="VERTICAL"/>
|
||||
<Label text="TCP" styleClass="badge, badge-tcp"/>
|
||||
<Label fx:id="labelServer" text="N/A"/>
|
||||
</children>
|
||||
</HBox>
|
||||
<HBox alignment="CENTER_LEFT" HBox.hgrow="NEVER" minWidth="6.0">
|
||||
<children>
|
||||
<Separator orientation="VERTICAL"/>
|
||||
</children>
|
||||
</HBox>
|
||||
<HBox alignment="CENTER_RIGHT" HBox.hgrow="SOMETIMES" minWidth="195.0">
|
||||
<children>
|
||||
<Label fx:id="bitcoinWallet" text="N/A" textAlignment="RIGHT" textOverrun="CLIP"/>
|
||||
<Label fx:id="bitcoinChain" styleClass="chain" text="(N/A)" textOverrun="CLIP"/>
|
||||
</children>
|
||||
</HBox>
|
||||
</children>
|
||||
<HBox alignment="CENTER_LEFT" HBox.hgrow="ALWAYS" onContextMenuRequested="#openNodeIdContext">
|
||||
<ImageView fitHeight="16.0" fitWidth="27.0" opacity="0.52" pickOnBounds="true"
|
||||
preserveRatio="true">
|
||||
<Image url="@../commons/images/eclair-shape.png" />
|
||||
</ImageView>
|
||||
<Label fx:id="labelNodeId" text="N/A" />
|
||||
</HBox>
|
||||
<HBox alignment="CENTER_LEFT" HBox.hgrow="SOMETIMES" minWidth="160.0">
|
||||
<Separator orientation="VERTICAL" />
|
||||
<Label text="TOTAL" styleClass="badge" />
|
||||
<Label fx:id="statusBalanceLabel" styleClass="value" text="N/A" />
|
||||
</HBox>
|
||||
<HBox alignment="CENTER_LEFT" HBox.hgrow="SOMETIMES" minWidth="85.0">
|
||||
<Separator orientation="VERTICAL" />
|
||||
<Rectangle fx:id="rectRGB" width="7" height="7" fill="transparent" />
|
||||
<Label fx:id="labelAlias" text="N/A" />
|
||||
</HBox>
|
||||
<HBox alignment="CENTER_LEFT" HBox.hgrow="NEVER" minWidth="85.0">
|
||||
<Separator orientation="VERTICAL" />
|
||||
<Label text="HTTP" styleClass="badge, badge-http" />
|
||||
<Label fx:id="labelApi" styleClass="value" text="N/A" />
|
||||
</HBox>
|
||||
<HBox alignment="CENTER_LEFT" HBox.hgrow="NEVER" minWidth="80.0">
|
||||
<Separator orientation="VERTICAL" />
|
||||
<Label text="TCP" styleClass="badge, badge-tcp" />
|
||||
<Label fx:id="labelServer" text="N/A" />
|
||||
</HBox>
|
||||
<HBox alignment="CENTER_LEFT" HBox.hgrow="NEVER" minWidth="6.0">
|
||||
<Separator orientation="VERTICAL" />
|
||||
</HBox>
|
||||
<HBox alignment="CENTER_RIGHT" HBox.hgrow="SOMETIMES" minWidth="155.0">
|
||||
<Label fx:id="bitcoinWallet" text="N/A" textAlignment="RIGHT" textOverrun="CLIP" />
|
||||
<Label fx:id="bitcoinChain" styleClass="chain" text="(N/A)" textOverrun="CLIP" />
|
||||
</HBox>
|
||||
</HBox>
|
||||
</bottom>
|
||||
<top>
|
||||
|
|
|
@ -89,10 +89,11 @@ class FxApp extends Application with Logging {
|
|||
val unitConf = setup.config.getString("gui.unit")
|
||||
FxApp.unit = Try(CoinUtils.getUnitFromString(unitConf)) match {
|
||||
case Failure(_) =>
|
||||
logger.warn(s"$unitConf is not a valid gui unit, must be msat, sat, mbtc or btc. Defaulting to btc.")
|
||||
logger.warn(s"$unitConf is not a valid gui unit, must be msat, sat, bits, mbtc or btc. Defaulting to btc.")
|
||||
BtcUnit
|
||||
case Success(u) => u
|
||||
}
|
||||
CoinUtils.setCoinPattern(CoinUtils.getPatternFromUnit(FxApp.unit))
|
||||
|
||||
val guiUpdater = system.actorOf(SimpleSupervisor.props(Props(classOf[GUIUpdater], controller), "gui-updater", SupervisorStrategy.Resume))
|
||||
system.eventStream.subscribe(guiUpdater, classOf[ChannelEvent])
|
||||
|
@ -108,8 +109,8 @@ class FxApp extends Application with Logging {
|
|||
override def run(): Unit = {
|
||||
val scene = new Scene(mainRoot)
|
||||
primaryStage.setTitle("Eclair")
|
||||
primaryStage.setMinWidth(600)
|
||||
primaryStage.setWidth(960)
|
||||
primaryStage.setMinWidth(750)
|
||||
primaryStage.setWidth(980)
|
||||
primaryStage.setMinHeight(400)
|
||||
primaryStage.setHeight(640)
|
||||
primaryStage.setOnCloseRequest(new EventHandler[WindowEvent] {
|
||||
|
|
|
@ -18,12 +18,6 @@ package fr.acinq.eclair.gui
|
|||
|
||||
import java.time.LocalDateTime
|
||||
import java.util.function.Predicate
|
||||
import javafx.application.Platform
|
||||
import javafx.event.{ActionEvent, EventHandler}
|
||||
import javafx.fxml.FXMLLoader
|
||||
import javafx.scene.control.Alert.AlertType
|
||||
import javafx.scene.control.{Alert, ButtonType}
|
||||
import javafx.scene.layout.VBox
|
||||
|
||||
import akka.actor.{Actor, ActorLogging, ActorRef, Terminated}
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
|
@ -31,12 +25,18 @@ import fr.acinq.bitcoin._
|
|||
import fr.acinq.eclair.CoinUtils
|
||||
import fr.acinq.eclair.blockchain.bitcoind.zmq.ZMQActor.{ZMQConnected, ZMQDisconnected}
|
||||
import fr.acinq.eclair.blockchain.electrum.ElectrumClient.{ElectrumDisconnected, ElectrumReady}
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.channel.{Data, _}
|
||||
import fr.acinq.eclair.gui.controllers._
|
||||
import fr.acinq.eclair.payment.PaymentLifecycle.{LocalFailure, PaymentFailed, PaymentSucceeded, RemoteFailure}
|
||||
import fr.acinq.eclair.payment._
|
||||
import fr.acinq.eclair.router.{NORMAL => _, _}
|
||||
import fr.acinq.eclair.wire.NodeAnnouncement
|
||||
import javafx.application.Platform
|
||||
import javafx.event.{ActionEvent, EventHandler}
|
||||
import javafx.fxml.FXMLLoader
|
||||
import javafx.scene.control.Alert.AlertType
|
||||
import javafx.scene.control.{Alert, ButtonType}
|
||||
import javafx.scene.layout.VBox
|
||||
|
||||
import scala.collection.JavaConversions._
|
||||
|
||||
|
@ -60,43 +60,23 @@ class GUIUpdater(mainController: MainController) extends Actor with ActorLogging
|
|||
|
||||
def receive: Receive = main(Map())
|
||||
|
||||
def createChannelPanel(channel: ActorRef, peer: ActorRef, remoteNodeId: PublicKey, isFunder: Boolean, temporaryChannelId: BinaryData): (ChannelPaneController, VBox) = {
|
||||
def createChannelPanel(channel: ActorRef, peer: ActorRef, remoteNodeId: PublicKey, isFunder: Boolean, channelId: BinaryData): (ChannelPaneController, VBox) = {
|
||||
log.info(s"new channel: $channel")
|
||||
val loader = new FXMLLoader(getClass.getResource("/gui/main/channelPane.fxml"))
|
||||
val channelPaneController = new ChannelPaneController(s"$remoteNodeId")
|
||||
val channelPaneController = new ChannelPaneController(channel, remoteNodeId.toString())
|
||||
loader.setController(channelPaneController)
|
||||
val root = loader.load[VBox]
|
||||
channelPaneController.nodeId.setText(remoteNodeId.toString())
|
||||
channelPaneController.channelId.setText(temporaryChannelId.toString())
|
||||
channelPaneController.channelId.setText(channelId.toString())
|
||||
channelPaneController.funder.setText(if (isFunder) "Yes" else "No")
|
||||
channelPaneController.close.setOnAction(new EventHandler[ActionEvent] {
|
||||
override def handle(event: ActionEvent) = channel ! CMD_CLOSE(scriptPubKey = None)
|
||||
})
|
||||
channelPaneController.forceclose.setOnAction(new EventHandler[ActionEvent] {
|
||||
override def handle(event: ActionEvent) = {
|
||||
val alert = new Alert(AlertType.WARNING, "Careful: force-close is more expensive than a regular close and will incur a delay before funds are spendable.\n\nAre you sure you want to proceed?", ButtonType.YES, ButtonType.NO)
|
||||
alert.showAndWait
|
||||
if (alert.getResult eq ButtonType.YES) {
|
||||
channel ! CMD_FORCECLOSE
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// set the node alias if the node has already been announced
|
||||
mainController.networkNodesList
|
||||
.find(na => na.nodeId.toString.equals(remoteNodeId.toString))
|
||||
.map(na => channelPaneController.updateRemoteNodeAlias(na.alias))
|
||||
.foreach(na => channelPaneController.updateRemoteNodeAlias(na.alias))
|
||||
|
||||
(channelPaneController, root)
|
||||
}
|
||||
|
||||
def updateBalance(channelPaneController: ChannelPaneController, commitments: Commitments) = {
|
||||
val spec = commitments.localCommit.spec
|
||||
channelPaneController.capacity.setText(CoinUtils.formatAmountInUnit(MilliSatoshi(spec.totalFunds), FxApp.getUnit, withUnit = true))
|
||||
channelPaneController.amountUs.setText(CoinUtils.formatAmountInUnit(MilliSatoshi(spec.toLocalMsat), FxApp.getUnit, withUnit = true))
|
||||
channelPaneController.balanceBar.setProgress(spec.toLocalMsat.toDouble / spec.totalFunds)
|
||||
}
|
||||
|
||||
def main(m: Map[ActorRef, ChannelPaneController]): Receive = {
|
||||
|
||||
case ChannelCreated(channel, peer, remoteNodeId, isFunder, temporaryChannelId) =>
|
||||
|
@ -108,29 +88,32 @@ class GUIUpdater(mainController: MainController) extends Actor with ActorLogging
|
|||
case ChannelRestored(channel, peer, remoteNodeId, isFunder, channelId, currentData) =>
|
||||
context.watch(channel)
|
||||
val (channelPaneController, root) = createChannelPanel(channel, peer, remoteNodeId, isFunder, channelId)
|
||||
currentData match {
|
||||
case d: HasCommitments =>
|
||||
updateBalance(channelPaneController, d.commitments)
|
||||
channelPaneController.txId.setText(d.commitments.commitInput.outPoint.txid.toString())
|
||||
case _ => {}
|
||||
}
|
||||
runInGuiThread(() => mainController.channelBox.getChildren.addAll(root))
|
||||
context.become(main(m + (channel -> channelPaneController)))
|
||||
channelPaneController.updateBalance(currentData.commitments)
|
||||
val m1 = m + (channel -> channelPaneController)
|
||||
val totalBalance = MilliSatoshi(m1.values.map(_.getBalance.amount).sum)
|
||||
runInGuiThread(() => {
|
||||
channelPaneController.refreshBalance()
|
||||
mainController.refreshTotalBalance(totalBalance)
|
||||
channelPaneController.txId.setText(currentData.commitments.commitInput.outPoint.txid.toString())
|
||||
mainController.channelBox.getChildren.addAll(root)
|
||||
})
|
||||
context.become(main(m1))
|
||||
|
||||
case ShortChannelIdAssigned(channel, channelId, shortChannelId) if m.contains(channel) =>
|
||||
val channelPaneController = m(channel)
|
||||
runInGuiThread(() => channelPaneController.shortChannelId.setText(shortChannelId.toString))
|
||||
|
||||
case ChannelIdAssigned(channel, _, _, channelId) if m.contains(channel) =>
|
||||
val channelPaneController = m(channel)
|
||||
runInGuiThread(() => channelPaneController.channelId.setText(s"$channelId"))
|
||||
runInGuiThread(() => channelPaneController.channelId.setText(channelId.toString()))
|
||||
|
||||
case ChannelStateChanged(channel, _, _, _, currentState, currentData) if m.contains(channel) =>
|
||||
case ChannelStateChanged(channel, _, remoteNodeId, _, currentState, currentData) if m.contains(channel) =>
|
||||
val channelPaneController = m(channel)
|
||||
runInGuiThread { () =>
|
||||
|
||||
(currentState, currentData) match {
|
||||
case (WAIT_FOR_FUNDING_CONFIRMED, d: HasCommitments) =>
|
||||
channelPaneController.txId.setText(d.commitments.commitInput.outPoint.txid.toString())
|
||||
case (WAIT_FOR_FUNDING_CONFIRMED, d: HasCommitments) => channelPaneController.txId.setText(d.commitments.commitInput.outPoint.txid.toString())
|
||||
case _ => {}
|
||||
}
|
||||
|
||||
channelPaneController.close.setVisible(STATE_MUTUAL_CLOSE.contains(currentState))
|
||||
channelPaneController.forceclose.setVisible(STATE_FORCE_CLOSE.contains(currentState))
|
||||
channelPaneController.state.setText(currentState.toString)
|
||||
|
@ -138,19 +121,30 @@ class GUIUpdater(mainController: MainController) extends Actor with ActorLogging
|
|||
|
||||
case ChannelSignatureReceived(channel, commitments) if m.contains(channel) =>
|
||||
val channelPaneController = m(channel)
|
||||
runInGuiThread(() => updateBalance(channelPaneController, commitments))
|
||||
channelPaneController.updateBalance(commitments)
|
||||
val totalBalance = MilliSatoshi(m.values.map(_.getBalance.amount).sum)
|
||||
runInGuiThread(() => {
|
||||
channelPaneController.refreshBalance()
|
||||
mainController.refreshTotalBalance(totalBalance)
|
||||
})
|
||||
|
||||
case Terminated(actor) if m.contains(actor) =>
|
||||
val channelPaneController = m(actor)
|
||||
log.debug(s"channel=${channelPaneController.channelId.getText} to be removed from gui")
|
||||
runInGuiThread(() => mainController.channelBox.getChildren.remove(channelPaneController.root))
|
||||
val m1 = m - actor
|
||||
val totalBalance = MilliSatoshi(m1.values.map(_.getBalance.amount).sum)
|
||||
runInGuiThread(() => {
|
||||
mainController.refreshTotalBalance(totalBalance)
|
||||
})
|
||||
context.become(main(m1))
|
||||
|
||||
case NodeDiscovered(nodeAnnouncement) =>
|
||||
log.debug(s"peer node discovered with node id=${nodeAnnouncement.nodeId}")
|
||||
runInGuiThread { () =>
|
||||
if (!mainController.networkNodesList.exists(na => na.nodeId == nodeAnnouncement.nodeId)) {
|
||||
mainController.networkNodesList.add(nodeAnnouncement)
|
||||
m.foreach(f => if (nodeAnnouncement.nodeId.toString.equals(f._2.theirNodeIdValue)) {
|
||||
m.foreach(f => if (nodeAnnouncement.nodeId.toString.equals(f._2.peerNodeId)) {
|
||||
f._2.updateRemoteNodeAlias(nodeAnnouncement.alias)
|
||||
})
|
||||
}
|
||||
|
@ -170,7 +164,7 @@ class GUIUpdater(mainController: MainController) extends Actor with ActorLogging
|
|||
val idx = mainController.networkNodesList.indexWhere(na => na.nodeId == nodeAnnouncement.nodeId)
|
||||
if (idx >= 0) {
|
||||
mainController.networkNodesList.update(idx, nodeAnnouncement)
|
||||
m.foreach(f => if (nodeAnnouncement.nodeId.toString.equals(f._2.theirNodeIdValue)) {
|
||||
m.foreach(f => if (nodeAnnouncement.nodeId.toString.equals(f._2.peerNodeId)) {
|
||||
f._2.updateRemoteNodeAlias(nodeAnnouncement.alias)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -16,48 +16,59 @@
|
|||
|
||||
package fr.acinq.eclair.gui.controllers
|
||||
|
||||
import akka.actor.ActorRef
|
||||
import com.google.common.base.Strings
|
||||
import fr.acinq.bitcoin.MilliSatoshi
|
||||
import fr.acinq.eclair.CoinUtils
|
||||
import fr.acinq.eclair.channel.{CMD_CLOSE, CMD_FORCECLOSE, Commitments}
|
||||
import fr.acinq.eclair.gui.FxApp
|
||||
import javafx.application.Platform
|
||||
import javafx.beans.value.{ChangeListener, ObservableValue}
|
||||
import javafx.fxml.FXML
|
||||
import javafx.scene.control._
|
||||
import javafx.scene.input.{ContextMenuEvent, MouseEvent}
|
||||
import javafx.scene.layout.VBox
|
||||
|
||||
import fr.acinq.eclair.gui.utils.{ContextMenuUtils, CopyAction}
|
||||
import grizzled.slf4j.Logging
|
||||
import javafx.event.{ActionEvent, EventHandler}
|
||||
import javafx.scene.control.Alert.AlertType
|
||||
|
||||
/**
|
||||
* Created by DPA on 23/09/2016.
|
||||
*/
|
||||
class ChannelPaneController(val theirNodeIdValue: String) extends Logging {
|
||||
class ChannelPaneController(val channelRef: ActorRef, val peerNodeId: String) extends Logging {
|
||||
|
||||
@FXML var root: VBox = _
|
||||
@FXML var channelId: TextField = _
|
||||
@FXML var shortChannelId: TextField = _
|
||||
@FXML var txId: TextField = _
|
||||
@FXML var balanceBar: ProgressBar = _
|
||||
@FXML var amountUs: TextField = _
|
||||
@FXML var amountUs: Label = _
|
||||
@FXML var nodeAlias: Label = _
|
||||
@FXML var nodeId: TextField = _
|
||||
@FXML var capacity: TextField = _
|
||||
@FXML var funder: TextField = _
|
||||
@FXML var state: TextField = _
|
||||
@FXML var funder: TextField = _
|
||||
@FXML var close: Button = _
|
||||
@FXML var forceclose: Button = _
|
||||
|
||||
var contextMenu: ContextMenu = _
|
||||
private var contextMenu: ContextMenu = _
|
||||
private var balance: MilliSatoshi = MilliSatoshi(0)
|
||||
private var capacity: MilliSatoshi = MilliSatoshi(0)
|
||||
|
||||
private def buildChannelContextMenu() = {
|
||||
private def buildChannelContextMenu(): Unit = {
|
||||
Platform.runLater(new Runnable() {
|
||||
override def run() = {
|
||||
contextMenu = ContextMenuUtils.buildCopyContext(List(
|
||||
CopyAction("Copy Channel Id", channelId.getText),
|
||||
CopyAction("Copy Peer Pubkey", theirNodeIdValue),
|
||||
CopyAction("Copy Tx Id", txId.getText())
|
||||
CopyAction("Copy channel id", channelId.getText),
|
||||
CopyAction("Copy peer pubkey", peerNodeId),
|
||||
CopyAction("Copy tx id", txId.getText())
|
||||
))
|
||||
contextMenu.getStyleClass.add("context-channel")
|
||||
channelId.setContextMenu(contextMenu)
|
||||
shortChannelId.setContextMenu(contextMenu)
|
||||
txId.setContextMenu(contextMenu)
|
||||
amountUs.setContextMenu(contextMenu)
|
||||
nodeId.setContextMenu(contextMenu)
|
||||
capacity.setContextMenu(contextMenu)
|
||||
funder.setContextMenu(contextMenu)
|
||||
state.setContextMenu(contextMenu)
|
||||
}
|
||||
|
@ -68,6 +79,38 @@ class ChannelPaneController(val theirNodeIdValue: String) extends Logging {
|
|||
channelId.textProperty.addListener(new ChangeListener[String] {
|
||||
override def changed(observable: ObservableValue[_ <: String], oldValue: String, newValue: String) = buildChannelContextMenu()
|
||||
})
|
||||
nodeId.setText(peerNodeId)
|
||||
nodeAlias.managedProperty.bind(nodeAlias.visibleProperty)
|
||||
close.setOnAction(new EventHandler[ActionEvent] {
|
||||
override def handle(event: ActionEvent) = {
|
||||
val alert = new Alert(AlertType.CONFIRMATION,
|
||||
s"""
|
||||
|Are you sure you want to close this channel?
|
||||
|
|
||||
|$getChannelDetails
|
||||
|""".stripMargin, ButtonType.YES, ButtonType.NO)
|
||||
alert.showAndWait
|
||||
if (alert.getResult eq ButtonType.YES) {
|
||||
channelRef ! CMD_CLOSE(scriptPubKey = None)
|
||||
}
|
||||
}
|
||||
})
|
||||
forceclose.setOnAction(new EventHandler[ActionEvent] {
|
||||
override def handle(event: ActionEvent) = {
|
||||
val alert = new Alert(AlertType.WARNING,
|
||||
s"""
|
||||
|Careful: force-close is more expensive than a regular close and will incur a delay before funds are spendable.
|
||||
|
|
||||
|Are you sure you want to forcibly close this channel?
|
||||
|
|
||||
|$getChannelDetails
|
||||
""".stripMargin, ButtonType.YES, ButtonType.NO)
|
||||
alert.showAndWait
|
||||
if (alert.getResult eq ButtonType.YES) {
|
||||
channelRef ! CMD_FORCECLOSE
|
||||
}
|
||||
}
|
||||
})
|
||||
buildChannelContextMenu()
|
||||
}
|
||||
|
||||
|
@ -81,6 +124,31 @@ class ChannelPaneController(val theirNodeIdValue: String) extends Logging {
|
|||
}
|
||||
|
||||
def updateRemoteNodeAlias(alias: String) {
|
||||
Option(nodeId).map((n: TextField) => n.setText(s"$theirNodeIdValue ($alias)"))
|
||||
nodeAlias.setText(alias)
|
||||
nodeAlias.setVisible(!Strings.isNullOrEmpty(alias))
|
||||
}
|
||||
|
||||
def updateBalance(commitments: Commitments) {
|
||||
balance = MilliSatoshi(commitments.localCommit.spec.toLocalMsat)
|
||||
capacity = MilliSatoshi(commitments.localCommit.spec.totalFunds)
|
||||
}
|
||||
|
||||
def refreshBalance(): Unit = {
|
||||
amountUs.setText(s"${CoinUtils.formatAmountInUnit(balance, FxApp.getUnit)} / ${CoinUtils.formatAmountInUnit(capacity, FxApp.getUnit, withUnit = true)}")
|
||||
balanceBar.setProgress(balance.amount.toDouble / capacity.amount)
|
||||
}
|
||||
|
||||
def getBalance: MilliSatoshi = balance
|
||||
|
||||
def getCapacity: MilliSatoshi = capacity
|
||||
|
||||
def getChannelDetails: String =
|
||||
s"""
|
||||
|Channel details:
|
||||
|---
|
||||
|Id: ${channelId.getText().substring(0, 18)}...
|
||||
|Peer: ${peerNodeId.toString().substring(0, 18)}...
|
||||
|Balance: ${CoinUtils.formatAmountInUnit(getBalance, FxApp.getUnit, withUnit = true)}
|
||||
|State: ${state.getText}
|
||||
|"""
|
||||
}
|
||||
|
|
|
@ -20,6 +20,17 @@ import java.text.NumberFormat
|
|||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Locale
|
||||
|
||||
import com.google.common.net.HostAndPort
|
||||
import fr.acinq.bitcoin.{MilliSatoshi, Satoshi}
|
||||
import fr.acinq.eclair.NodeParams.{BITCOIND, ELECTRUM}
|
||||
import fr.acinq.eclair.gui.stages._
|
||||
import fr.acinq.eclair.gui.utils.{ContextMenuUtils, CopyAction}
|
||||
import fr.acinq.eclair.gui.{FxApp, Handlers}
|
||||
import fr.acinq.eclair.payment.{PaymentEvent, PaymentReceived, PaymentRelayed, PaymentSent}
|
||||
import fr.acinq.eclair.wire.{ChannelAnnouncement, NodeAnnouncement}
|
||||
import fr.acinq.eclair.{CoinUtils, Setup}
|
||||
import grizzled.slf4j.Logging
|
||||
import javafx.animation.{FadeTransition, ParallelTransition, SequentialTransition, TranslateTransition}
|
||||
import javafx.application.{HostServices, Platform}
|
||||
import javafx.beans.property._
|
||||
|
@ -36,21 +47,9 @@ import javafx.scene.layout.{AnchorPane, HBox, StackPane, VBox}
|
|||
import javafx.scene.paint.Color
|
||||
import javafx.scene.shape.Rectangle
|
||||
import javafx.scene.text.Text
|
||||
import javafx.stage.FileChooser.ExtensionFilter
|
||||
import javafx.stage._
|
||||
import javafx.util.{Callback, Duration}
|
||||
|
||||
import com.google.common.net.HostAndPort
|
||||
import fr.acinq.bitcoin.{MilliSatoshi, Satoshi}
|
||||
import fr.acinq.eclair.NodeParams.{BITCOIND, ELECTRUM}
|
||||
import fr.acinq.eclair.gui.stages._
|
||||
import fr.acinq.eclair.gui.utils.{ContextMenuUtils, CopyAction}
|
||||
import fr.acinq.eclair.gui.{FxApp, Handlers}
|
||||
import fr.acinq.eclair.payment.{PaymentEvent, PaymentReceived, PaymentRelayed, PaymentSent}
|
||||
import fr.acinq.eclair.wire.{ChannelAnnouncement, NodeAnnouncement}
|
||||
import fr.acinq.eclair.{CoinUtils, Setup}
|
||||
import grizzled.slf4j.Logging
|
||||
|
||||
case class ChannelInfo(announcement: ChannelAnnouncement,
|
||||
var feeBaseMsatNode1_opt: Option[Long], var feeBaseMsatNode2_opt: Option[Long],
|
||||
var feeProportionalMillionthsNode1_opt: Option[Long], var feeProportionalMillionthsNode2_opt: Option[Long],
|
||||
|
@ -82,6 +81,7 @@ class MainController(val handlers: Handlers, val hostServices: HostServices) ext
|
|||
|
||||
// status bar elements
|
||||
@FXML var labelNodeId: Label = _
|
||||
@FXML var statusBalanceLabel: Label = _
|
||||
@FXML var rectRGB: Rectangle = _
|
||||
@FXML var labelAlias: Label = _
|
||||
@FXML var labelApi: Label = _
|
||||
|
@ -240,16 +240,15 @@ class MainController(val handlers: Handlers, val hostServices: HostServices) ext
|
|||
networkChannelsFeeBaseMsatNode2Column.setCellValueFactory(new Callback[CellDataFeatures[ChannelInfo, String], ObservableValue[String]]() {
|
||||
def call(pc: CellDataFeatures[ChannelInfo, String]) = new SimpleStringProperty(
|
||||
pc.getValue.feeBaseMsatNode2_opt.map(f => CoinUtils.formatAmountInUnit(MilliSatoshi(f), FxApp.getUnit, withUnit = true)).getOrElse("?"))
|
||||
// CoinUtils.formatAmountInUnit(MilliSatoshi(pc.getValue.feeBaseMsatNode2), FxApp.getUnit, withUnit = true))
|
||||
})
|
||||
// feeProportionalMillionths is fee per satoshi in millionths of a satoshi
|
||||
networkChannelsFeeProportionalMillionthsNode1Column.setCellValueFactory(new Callback[CellDataFeatures[ChannelInfo, String], ObservableValue[String]]() {
|
||||
def call(pc: CellDataFeatures[ChannelInfo, String]) = new SimpleStringProperty(
|
||||
pc.getValue.feeProportionalMillionthsNode1_opt.map(f => s"${CoinUtils.COIN_FORMAT.format(f.toDouble / 1000000 * 100)}%").getOrElse("?"))
|
||||
pc.getValue.feeProportionalMillionthsNode1_opt.map(f => s"${NumberFormat.getInstance().format(f.toDouble / 1000000 * 100)}%").getOrElse("?"))
|
||||
})
|
||||
networkChannelsFeeProportionalMillionthsNode2Column.setCellValueFactory(new Callback[CellDataFeatures[ChannelInfo, String], ObservableValue[String]]() {
|
||||
def call(pc: CellDataFeatures[ChannelInfo, String]) = new SimpleStringProperty(
|
||||
pc.getValue.feeProportionalMillionthsNode2_opt.map(f => s"${CoinUtils.COIN_FORMAT.format(f.toDouble / 1000000 * 100)}%").getOrElse("?"))
|
||||
pc.getValue.feeProportionalMillionthsNode2_opt.map(f => s"${NumberFormat.getInstance().format(f.toDouble / 1000000 * 100)}%").getOrElse("?"))
|
||||
})
|
||||
networkChannelsCapacityColumn.setCellValueFactory(new Callback[CellDataFeatures[ChannelInfo, String], ObservableValue[String]]() {
|
||||
def call(pc: CellDataFeatures[ChannelInfo, String]) = new SimpleStringProperty(
|
||||
|
@ -572,4 +571,8 @@ class MainController(val handlers: Handlers, val hostServices: HostServices) ext
|
|||
childStage.setX(getWindow.map(w => w.getX + w.getWidth / 2 - childStage.getWidth / 2).getOrElse(0))
|
||||
childStage.setY(getWindow.map(w => w.getY + w.getHeight / 2 - childStage.getHeight / 2).getOrElse(0))
|
||||
}
|
||||
|
||||
def refreshTotalBalance(total: MilliSatoshi): Unit = {
|
||||
statusBalanceLabel.setText(CoinUtils.formatAmountInUnit(total, FxApp.getUnit, withUnit = true))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,10 +17,9 @@
|
|||
package fr.acinq.eclair.gui.utils
|
||||
|
||||
import javafx.collections.FXCollections
|
||||
|
||||
import fr.acinq.eclair.{BtcUnit, MBtcUnit, MSatUnit, SatUnit}
|
||||
import fr.acinq.eclair._
|
||||
|
||||
object Constants {
|
||||
val FX_UNITS_ARRAY_NO_MSAT = FXCollections.observableArrayList(SatUnit.label, MBtcUnit.label, BtcUnit.label)
|
||||
val FX_UNITS_ARRAY = FXCollections.observableArrayList(MSatUnit.label, SatUnit.label, MBtcUnit.label, BtcUnit.label)
|
||||
val FX_UNITS_ARRAY_NO_MSAT = FXCollections.observableArrayList(SatUnit.label, BitUnit.label, MBtcUnit.label, BtcUnit.label)
|
||||
val FX_UNITS_ARRAY = FXCollections.observableArrayList(MSatUnit.label, SatUnit.label, BitUnit.label, MBtcUnit.label, BtcUnit.label)
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue