mirror of
https://github.com/ACINQ/eclair.git
synced 2025-03-14 03:48:13 +01:00
Merge branch 'master' into wip-android
This commit is contained in:
commit
734ec9d914
18 changed files with 226 additions and 91 deletions
11
Dockerfile
11
Dockerfile
|
@ -45,5 +45,12 @@ FROM openjdk:8u151-jre-slim
|
|||
WORKDIR /app
|
||||
# Eclair only needs the eclair-node-*.jar to run
|
||||
COPY --from=BUILD /usr/src/eclair-node/target/eclair-node-*.jar .
|
||||
RUN ln `ls` eclair-node
|
||||
ENTRYPOINT [ "java", "-jar", "eclair-node" ]
|
||||
RUN ln `ls` eclair-node.jar
|
||||
|
||||
ENV ECLAIR_DATADIR=/data
|
||||
ENV JAVA_OPTS=
|
||||
|
||||
RUN mkdir -p "$ECLAIR_DATADIR"
|
||||
VOLUME [ "/data" ]
|
||||
|
||||
ENTRYPOINT java $JAVA_OPTS -Declair.datadir=$ECLAIR_DATADIR -jar eclair-node.jar
|
17
README.md
17
README.md
|
@ -140,6 +140,23 @@ java -Declair.datadir=/tmp/node1 -jar eclair-node-gui-<version>-<commit_id>.jar
|
|||
close | channelId, scriptPubKey (optional) | close a channel and send the funds to the given scriptPubKey
|
||||
help | | display available methods
|
||||
|
||||
## Docker
|
||||
|
||||
A [Dockerfile](Dockerfile) images are built on each commit on [docker hub](https://hub.docker.com/r/ACINQ/eclair) for running a dockerized eclair-node.
|
||||
|
||||
You can use the `JAVA_OPTS` environment variable to set arguments to `eclair-node`.
|
||||
|
||||
```
|
||||
docker run -ti --rm -e "JAVA_OPTS=-Xmx512m -Declair.api.binding-ip=0.0.0.0 -Declair.node-alias=node-pm -Declair.printToConsole" acinq\eclair
|
||||
```
|
||||
|
||||
If you want to persist the data directory, you can make the volume to your host with the `-v` argument, as the following example:
|
||||
|
||||
```
|
||||
docker run -ti --rm -v "/path_on_host:/data" -e "JAVA_OPTS=-Declair.printToConsole" acinq\eclair
|
||||
```
|
||||
|
||||
|
||||
## Resources
|
||||
- [1] [The Bitcoin Lightning Network: Scalable Off-Chain Instant Payments](https://lightning.network/lightning-network-paper.pdf) by Joseph Poon and Thaddeus Dryja
|
||||
- [2] [Reaching The Ground With Lightning](https://github.com/ElementsProject/lightning/raw/master/doc/deployable-lightning.pdf) by Rusty Russell
|
||||
|
|
|
@ -18,6 +18,9 @@ case $1 in
|
|||
"channels")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"channels\", \"params\" : [] }' $URL" | jq ".result[]"
|
||||
;;
|
||||
"channelsto")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"channelsto\", \"params\" : [\"${2?"missing node id"}\"] }' $URL" | jq ".result[]"
|
||||
;;
|
||||
"channel")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"channel\", \"params\" : [\"${2?"missing channel id"}\"] }' $URL" | jq ".result | { nodeid, channelId, state, balanceMsat: .data.commitments.localCommit.spec.toLocalMsat, capacitySat: .data.commitments.commitInput.txOut.amount.amount }"
|
||||
;;
|
||||
|
|
|
@ -62,8 +62,8 @@ eclair {
|
|||
mindepth-blocks = 2
|
||||
expiry-delta-blocks = 144
|
||||
|
||||
fee-base-msat = 546000
|
||||
fee-proportional-millionths = 10
|
||||
fee-base-msat = 10000
|
||||
fee-proportional-millionths = 100 // fee charged per transferred satoshi in millionths of a satoshi (100 = 0.1%)
|
||||
|
||||
// maximum local vs remote feerate mismatch; 1.0 means 100%
|
||||
// actual check is abs((local feerate - remote fee rate) / (local fee rate + remote fee rate)/2) > fee rate mismatch
|
||||
|
|
|
@ -266,8 +266,8 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
|
|||
signature = localSigOfRemoteTx
|
||||
)
|
||||
val channelId = toLongId(fundingTx.hash, fundingTxOutputIndex)
|
||||
context.parent ! ChannelIdAssigned(self, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages
|
||||
context.system.eventStream.publish(ChannelIdAssigned(self, temporaryChannelId, channelId))
|
||||
context.parent ! ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages
|
||||
context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId))
|
||||
goto(WAIT_FOR_FUNDING_SIGNED) using DATA_WAIT_FOR_FUNDING_SIGNED(channelId, localParams, remoteParams, fundingTx, localSpec, localCommitTx, RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint), open.channelFlags, fundingCreated) sending fundingCreated
|
||||
|
||||
case Event(Status.Failure(t), d: DATA_WAIT_FOR_FUNDING_INTERNAL) =>
|
||||
|
@ -314,8 +314,8 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
|
|||
originChannels = Map.empty,
|
||||
remoteNextCommitInfo = Right(randomKey.publicKey), // we will receive their next per-commitment point in the next message, so we temporarily put a random byte array,
|
||||
commitInput, ShaChain.init, channelId = channelId)
|
||||
context.parent ! ChannelIdAssigned(self, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages
|
||||
context.system.eventStream.publish(ChannelIdAssigned(self, temporaryChannelId, channelId))
|
||||
context.parent ! ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages
|
||||
context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId))
|
||||
context.system.eventStream.publish(ChannelSignatureReceived(self, commitments))
|
||||
log.info(s"waiting for them to publish the funding tx for channelId=$channelId fundingTxid=${commitInput.outPoint.txid}")
|
||||
blockchain ! WatchSpent(self, commitInput.outPoint.txid, commitInput.outPoint.index.toInt, commitments.commitInput.txOut.publicKeyScript, BITCOIN_FUNDING_SPENT) // TODO: should we wait for an acknowledgment from the watcher?
|
||||
|
@ -764,7 +764,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
|
|||
}
|
||||
relayer ! channelUpdate
|
||||
d.commitments.localChanges.proposed.collect {
|
||||
case add: UpdateAddHtlc => relayer ! AddHtlcFailed(d.channelId, ChannelUnavailable(d.channelId), d.commitments.originChannels(add.id), Some(channelUpdate))
|
||||
case add: UpdateAddHtlc => relayer ! Status.Failure(AddHtlcFailed(d.channelId, ChannelUnavailable(d.channelId), d.commitments.originChannels(add.id), Some(channelUpdate)))
|
||||
}
|
||||
goto(OFFLINE) using d.copy(channelUpdate = channelUpdate)
|
||||
|
||||
|
@ -1066,7 +1066,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
|
|||
case add if ripemd160(add.paymentHash) == extracted =>
|
||||
val origin = d.commitments.originChannels(add.id)
|
||||
log.warning(s"found a match between paymentHash160=$extracted and origin=$origin: htlc timed out")
|
||||
relayer ! AddHtlcFailed(d.channelId, HtlcTimedout(d.channelId), origin, None)
|
||||
relayer ! Status.Failure(AddHtlcFailed(d.channelId, HtlcTimedout(d.channelId), origin, None))
|
||||
}
|
||||
// TODO: should we handle local htlcs here as well? currently timed out htlcs that we sent will never have an answer
|
||||
// TODO: we do not handle the case where htlcs transactions end up being unconfirmed this can happen if an htlc-success tx is published right before a htlc timed out
|
||||
|
|
|
@ -14,7 +14,7 @@ case class ChannelCreated(channel: ActorRef, peer: ActorRef, remoteNodeId: Publi
|
|||
|
||||
case class ChannelRestored(channel: ActorRef, peer: ActorRef, remoteNodeId: PublicKey, isFunder: Boolean, channelId: BinaryData, currentData: HasCommitments) extends ChannelEvent
|
||||
|
||||
case class ChannelIdAssigned(channel: ActorRef, temporaryChannelId: BinaryData, channelId: BinaryData) extends ChannelEvent
|
||||
case class ChannelIdAssigned(channel: ActorRef, remoteNodeId: PublicKey, temporaryChannelId: BinaryData, channelId: BinaryData) extends ChannelEvent
|
||||
|
||||
case class ShortChannelIdAssigned(channel: ActorRef, channelId: BinaryData, shortChannelId: Long) extends ChannelEvent
|
||||
|
||||
|
|
|
@ -3,7 +3,8 @@ package fr.acinq.eclair.channel
|
|||
import akka.actor.Status.Failure
|
||||
import akka.actor.{Actor, ActorLogging, ActorRef, Terminated}
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
import fr.acinq.eclair.channel.Register.{Forward, ForwardFailure, ForwardShortId, ForwardShortIdFailure}
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.eclair.channel.Register._
|
||||
|
||||
/**
|
||||
* Created by PM on 26/01/2016.
|
||||
|
@ -16,32 +17,34 @@ class Register extends Actor with ActorLogging {
|
|||
context.system.eventStream.subscribe(self, classOf[ChannelIdAssigned])
|
||||
context.system.eventStream.subscribe(self, classOf[ShortChannelIdAssigned])
|
||||
|
||||
override def receive: Receive = main(Map.empty, Map.empty)
|
||||
override def receive: Receive = main(Map.empty, Map.empty, Map.empty)
|
||||
|
||||
def main(channels: Map[BinaryData, ActorRef], shortIds: Map[Long, BinaryData]): Receive = {
|
||||
case ChannelCreated(channel, _, _, _, temporaryChannelId) =>
|
||||
def main(channels: Map[BinaryData, ActorRef], shortIds: Map[Long, BinaryData], channelsTo: Map[BinaryData, PublicKey]): Receive = {
|
||||
case ChannelCreated(channel, _, remoteNodeId, _, temporaryChannelId) =>
|
||||
context.watch(channel)
|
||||
context become main(channels + (temporaryChannelId -> channel), shortIds)
|
||||
context become main(channels + (temporaryChannelId -> channel), shortIds, channelsTo + (temporaryChannelId -> remoteNodeId))
|
||||
|
||||
case ChannelRestored(channel, _, _, _, channelId, _) =>
|
||||
case ChannelRestored(channel, _, remoteNodeId, _, channelId, _) =>
|
||||
context.watch(channel)
|
||||
context become main(channels + (channelId -> channel), shortIds)
|
||||
context become main(channels + (channelId -> channel), shortIds, channelsTo + (channelId -> remoteNodeId))
|
||||
|
||||
case ChannelIdAssigned(channel, temporaryChannelId, channelId) =>
|
||||
context become main(channels + (channelId -> channel) - temporaryChannelId, shortIds)
|
||||
case ChannelIdAssigned(channel, remoteNodeId, temporaryChannelId, channelId) =>
|
||||
context become main(channels + (channelId -> channel) - temporaryChannelId, shortIds, channelsTo + (channelId -> remoteNodeId) - temporaryChannelId)
|
||||
|
||||
case ShortChannelIdAssigned(channel, channelId, shortChannelId) =>
|
||||
context become main(channels, shortIds + (shortChannelId -> channelId))
|
||||
case ShortChannelIdAssigned(_, channelId, shortChannelId) =>
|
||||
context become main(channels, shortIds + (shortChannelId -> channelId), channelsTo)
|
||||
|
||||
case Terminated(actor) if channels.values.toSet.contains(actor) =>
|
||||
val channelId = channels.find(_._2 == actor).get._1
|
||||
val shortChannelId = shortIds.find(_._2 == channelId).map(_._1).getOrElse(0L)
|
||||
context become main(channels - channelId, shortIds - shortChannelId)
|
||||
context become main(channels - channelId, shortIds - shortChannelId, channelsTo - channelId)
|
||||
|
||||
case 'channels => sender ! channels
|
||||
|
||||
case 'shortIds => sender ! shortIds
|
||||
|
||||
case 'channelsTo => sender ! channelsTo
|
||||
|
||||
case fwd@Forward(channelId, msg) =>
|
||||
channels.get(channelId) match {
|
||||
case Some(channel) => channel forward msg
|
||||
|
@ -63,6 +66,6 @@ object Register {
|
|||
case class ForwardShortId[T](shortChannelId: Long, message: T)
|
||||
|
||||
case class ForwardFailure[T](fwd: Forward[T]) extends RuntimeException(s"channel ${fwd.channelId} not found")
|
||||
case class ForwardShortIdFailure[T](fwd: ForwardShortId[T]) extends RuntimeException(s"channel ${fwd.shortChannelId} not found")
|
||||
case class ForwardShortIdFailure[T](fwd: ForwardShortId[T]) extends RuntimeException(s"channel ${fwd.shortChannelId.toHexString} not found")
|
||||
// @formatter:on
|
||||
}
|
|
@ -195,7 +195,7 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, address_opt: Option[
|
|||
}
|
||||
stay
|
||||
|
||||
case Event(ChannelIdAssigned(channel, temporaryChannelId, channelId), d@ConnectedData(_, _, channels)) if channels.contains(TemporaryChannelId(temporaryChannelId)) =>
|
||||
case Event(ChannelIdAssigned(channel, _, temporaryChannelId, channelId), d@ConnectedData(_, _, channels)) if channels.contains(TemporaryChannelId(temporaryChannelId)) =>
|
||||
log.info(s"channel id switch: previousId=$temporaryChannelId nextId=$channelId")
|
||||
// NB: we keep the temporary channel id because the switch is not always acknowledged at this point (see https://github.com/lightningnetwork/lightning-rfc/pull/151)
|
||||
// we won't clean it up, but we won't remember the temporary id on channel termination
|
||||
|
|
|
@ -21,9 +21,9 @@ object PaymentHop {
|
|||
* @param msat an amount to send to a payment recipient
|
||||
* @return a sequence of extra hops with a pre-calculated fee for a given msat amount
|
||||
*/
|
||||
def buildExtra(reversePath: Seq[Hop], msat: Long): Seq[ExtraHop] = (List.empty[ExtraHop] /: reversePath) {
|
||||
case (Nil, hop) => ExtraHop(hop.nodeId, hop.shortChannelId, hop.nextFee(msat), hop.cltvExpiryDelta) :: Nil
|
||||
case (head :: rest, hop) => ExtraHop(hop.nodeId, hop.shortChannelId, hop.nextFee(msat + head.fee), hop.cltvExpiryDelta) :: head :: rest
|
||||
def buildExtra(reversePath: Seq[Hop], msat: Long): Seq[ExtraHop] = reversePath.foldLeft(List.empty[ExtraHop]) {
|
||||
case (Nil, hop) => ExtraHop(hop.nodeId, hop.shortChannelId, hop.feeBaseMsat, hop.feeProportionalMillionths, hop.cltvExpiryDelta) :: Nil
|
||||
case (head :: rest, hop) => ExtraHop(hop.nodeId, hop.shortChannelId, hop.feeBaseMsat, hop.feeProportionalMillionths, hop.cltvExpiryDelta) :: head :: rest
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -40,6 +40,10 @@ trait PaymentHop {
|
|||
case class Hop(nodeId: PublicKey, nextNodeId: PublicKey, lastUpdate: ChannelUpdate) extends PaymentHop {
|
||||
def nextFee(msat: Long): Long = PaymentHop.nodeFee(lastUpdate.feeBaseMsat, lastUpdate.feeProportionalMillionths, msat)
|
||||
|
||||
def feeBaseMsat: Long = lastUpdate.feeBaseMsat
|
||||
|
||||
def feeProportionalMillionths: Long = lastUpdate.feeProportionalMillionths
|
||||
|
||||
def cltvExpiryDelta: Int = lastUpdate.cltvExpiryDelta
|
||||
|
||||
def shortChannelId: Long = lastUpdate.shortChannelId
|
||||
|
|
|
@ -15,7 +15,7 @@ import scala.util.Try
|
|||
|
||||
/**
|
||||
* Lightning Payment Request
|
||||
* see https://github.com/lightningnetwork/lightning-rfc/pull/183
|
||||
* see https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md
|
||||
*
|
||||
* @param prefix currency prefix; lnbc for bitcoin, lntb for bitcoin testnet
|
||||
* @param amount amount to pay (empty string means no amount is specified)
|
||||
|
@ -217,15 +217,16 @@ object PaymentRequest {
|
|||
*
|
||||
* @param nodeId node id
|
||||
* @param shortChannelId channel id
|
||||
* @param fee node fee
|
||||
* @param feeBaseMast node fixed fee
|
||||
* @param feeProportionalMillionths node proportional fee
|
||||
* @param cltvExpiryDelta node cltv expiry delta
|
||||
*/
|
||||
case class ExtraHop(nodeId: PublicKey, shortChannelId: Long, fee: Long, cltvExpiryDelta: Int) extends PaymentHop {
|
||||
case class ExtraHop(nodeId: PublicKey, shortChannelId: Long, feeBaseMast: Long, feeProportionalMillionths: Long, cltvExpiryDelta: Int) extends PaymentHop {
|
||||
def pack: Seq[Byte] = nodeId.toBin ++ Protocol.writeUInt64(shortChannelId, ByteOrder.BIG_ENDIAN) ++
|
||||
Protocol.writeUInt64(fee, ByteOrder.BIG_ENDIAN) ++ Protocol.writeUInt16(cltvExpiryDelta, ByteOrder.BIG_ENDIAN)
|
||||
Protocol.writeUInt32(feeBaseMast, ByteOrder.BIG_ENDIAN) ++ Protocol.writeUInt32(feeProportionalMillionths, ByteOrder.BIG_ENDIAN) ++ Protocol.writeUInt16(cltvExpiryDelta, ByteOrder.BIG_ENDIAN)
|
||||
|
||||
// Fee is already pre-calculated for extra hops
|
||||
def nextFee(msat: Long): Long = fee
|
||||
def nextFee(msat: Long): Long = PaymentHop.nodeFee(feeBaseMast, feeProportionalMillionths, msat)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -244,15 +245,16 @@ object PaymentRequest {
|
|||
def parse(data: Seq[Byte]) = {
|
||||
val pubkey = data.slice(0, 33)
|
||||
val shortChannelId = Protocol.uint64(data.slice(33, 33 + 8), ByteOrder.BIG_ENDIAN)
|
||||
val fee = Protocol.uint64(data.slice(33 + 8, 33 + 8 + 8), ByteOrder.BIG_ENDIAN)
|
||||
val fee_base_msat = Protocol.uint32(data.slice(33 + 8, 33 + 8 + 4), ByteOrder.BIG_ENDIAN)
|
||||
val fee_proportional_millionths = Protocol.uint32(data.slice(33 + 8 + 4, 33 + 8 + 8), ByteOrder.BIG_ENDIAN)
|
||||
val cltv = Protocol.uint16(data.slice(33 + 8 + 8, chunkLength), ByteOrder.BIG_ENDIAN)
|
||||
ExtraHop(PublicKey(pubkey), shortChannelId, fee, cltv)
|
||||
ExtraHop(PublicKey(pubkey), shortChannelId, fee_base_msat, fee_proportional_millionths, cltv)
|
||||
}
|
||||
|
||||
def parseAll(data: Seq[Byte]): Seq[ExtraHop] =
|
||||
data.grouped(chunkLength).map(parse).toList
|
||||
|
||||
val chunkLength: Int = 33 + 8 + 8 + 2
|
||||
val chunkLength: Int = 33 + 8 + 4 + 4 + 2
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -88,7 +88,7 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR
|
|||
case Some(channelUpdate) if add.expiry < Globals.blockCount.get() + 3 => // TODO: hardcoded value
|
||||
sender ! CMD_FAIL_HTLC(add.id, Right(ExpiryTooSoon(channelUpdate)), commit = true)
|
||||
case _ =>
|
||||
log.info(s"forwarding htlc #${add.id} to shortChannelId=${perHopPayload.channel_id}")
|
||||
log.info(s"forwarding htlc #${add.id} to shortChannelId=${perHopPayload.channel_id.toHexString}")
|
||||
register ! Register.ForwardShortId(perHopPayload.channel_id, CMD_ADD_HTLC(perHopPayload.amtToForward, add.paymentHash, perHopPayload.outgoingCltvValue, nextPacket.serialize, upstream_opt = Some(add), commit = true))
|
||||
}
|
||||
case Success((Attempt.Failure(cause), _, _)) =>
|
||||
|
@ -100,14 +100,14 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR
|
|||
sender ! CMD_FAIL_MALFORMED_HTLC(add.id, Crypto.sha256(add.onionRoutingPacket), failureCode = FailureMessageCodecs.BADONION, commit = true)
|
||||
}
|
||||
|
||||
case Register.ForwardShortIdFailure(Register.ForwardShortId(shortChannelId, CMD_ADD_HTLC(_, _, _, _, Some(add), _))) =>
|
||||
case Status.Failure(Register.ForwardShortIdFailure(Register.ForwardShortId(shortChannelId, CMD_ADD_HTLC(_, _, _, _, Some(add), _)))) =>
|
||||
log.warning(s"couldn't resolve downstream channel $shortChannelId, failing htlc #${add.id}")
|
||||
register ! Register.Forward(add.channelId, CMD_FAIL_HTLC(add.id, Right(UnknownNextPeer), commit = true))
|
||||
|
||||
case AddHtlcFailed(_, error, Local(Some(sender)), _) =>
|
||||
case Status.Failure(AddHtlcFailed(_, error, Local(Some(sender)), _)) =>
|
||||
sender ! Status.Failure(error)
|
||||
|
||||
case AddHtlcFailed(_, error, Relayed(originChannelId, originHtlcId, _, _), channelUpdate_opt) =>
|
||||
case Status.Failure(AddHtlcFailed(_, error, Relayed(originChannelId, originHtlcId, _, _), channelUpdate_opt)) =>
|
||||
val failure = (error, channelUpdate_opt) match {
|
||||
case (_: ChannelUnavailable, Some(channelUpdate)) if !Announcements.isEnabled(channelUpdate.flags) => ChannelDisabled(channelUpdate.flags, channelUpdate)
|
||||
case (_: InsufficientFunds, Some(channelUpdate)) => TemporaryChannelFailure(channelUpdate)
|
||||
|
@ -145,6 +145,8 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR
|
|||
case ForwardFailMalformed(fail, Relayed(originChannelId, originHtlcId, _, _)) =>
|
||||
val cmd = CMD_FAIL_MALFORMED_HTLC(originHtlcId, fail.onionHash, fail.failureCode, commit = true)
|
||||
register ! Register.Forward(originChannelId, cmd)
|
||||
|
||||
case "ok" => () // ignoring responses from channels
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -249,12 +249,12 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSM[State, Data]
|
|||
case Event(WatchEventSpentBasic(BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT(shortChannelId)), d)
|
||||
if d.channels.containsKey(shortChannelId) =>
|
||||
val lostChannel = d.channels(shortChannelId)
|
||||
log.info(s"funding tx of channelId=$shortChannelId has been spent")
|
||||
log.info(s"funding tx of channelId=${shortChannelId.toHexString} has been spent")
|
||||
// we need to remove nodes that aren't tied to any channels anymore
|
||||
val channels1 = d.channels - lostChannel.shortChannelId
|
||||
val lostNodes = Seq(lostChannel.nodeId1, lostChannel.nodeId2).filterNot(nodeId => hasChannels(nodeId, channels1.values))
|
||||
// let's clean the db and send the events
|
||||
log.info(s"pruning shortChannelId=$shortChannelId (spent)")
|
||||
log.info(s"pruning shortChannelId=${shortChannelId.toHexString} (spent)")
|
||||
db.removeChannel(shortChannelId) // NB: this also removes channel updates
|
||||
context.system.eventStream.publish(ChannelLost(shortChannelId))
|
||||
lostNodes.foreach {
|
||||
|
@ -287,7 +287,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSM[State, Data]
|
|||
// let's clean the db and send the events
|
||||
staleChannels.foreach {
|
||||
case shortChannelId =>
|
||||
log.info(s"pruning shortChannelId=$shortChannelId (stale)")
|
||||
log.info(s"pruning shortChannelId=${shortChannelId.toHexString} (stale)")
|
||||
db.removeChannel(shortChannelId) // NB: this also removes channel updates
|
||||
context.system.eventStream.publish(ChannelLost(shortChannelId))
|
||||
}
|
||||
|
@ -301,12 +301,12 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSM[State, Data]
|
|||
|
||||
case Event(ExcludeChannel(desc@ChannelDesc(shortChannelId, nodeId, _)), d) =>
|
||||
val banDuration = nodeParams.channelExcludeDuration
|
||||
log.info(s"excluding shortChannelId=$shortChannelId from nodeId=$nodeId for duration=$banDuration")
|
||||
log.info(s"excluding shortChannelId=${shortChannelId.toHexString} from nodeId=$nodeId for duration=$banDuration")
|
||||
context.system.scheduler.scheduleOnce(banDuration, self, LiftChannelExclusion(desc))
|
||||
stay using d.copy(excludedChannels = d.excludedChannels + desc)
|
||||
|
||||
case Event(LiftChannelExclusion(desc@ChannelDesc(shortChannelId, nodeId, _)), d) =>
|
||||
log.info(s"reinstating shortChannelId=$shortChannelId from nodeId=$nodeId")
|
||||
log.info(s"reinstating shortChannelId=${shortChannelId.toHexString} from nodeId=$nodeId")
|
||||
stay using d.copy(excludedChannels = d.excludedChannels - desc)
|
||||
|
||||
case Event('nodes, d) =>
|
||||
|
|
|
@ -142,7 +142,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
|||
|
||||
test("connect nodes") {
|
||||
//
|
||||
// A ---- B ---- C ---- D
|
||||
// A ---- B ---- C ==== D
|
||||
// | / \
|
||||
// --E--' F{1,2,3,4,5}
|
||||
//
|
||||
|
@ -154,15 +154,16 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
|||
connect(nodes("A"), nodes("B"), 10000000, 0)
|
||||
connect(nodes("B"), nodes("C"), 2000000, 0)
|
||||
connect(nodes("C"), nodes("D"), 5000000, 0)
|
||||
connect(nodes("B"), nodes("E"), 5000000, 0)
|
||||
connect(nodes("E"), nodes("C"), 5000000, 0)
|
||||
connect(nodes("C"), nodes("D"), 5000000, 0)
|
||||
connect(nodes("B"), nodes("E"), 10000000, 0)
|
||||
connect(nodes("E"), nodes("C"), 10000000, 0)
|
||||
connect(nodes("C"), nodes("F1"), 5000000, 0)
|
||||
connect(nodes("C"), nodes("F2"), 5000000, 0)
|
||||
connect(nodes("C"), nodes("F3"), 5000000, 0)
|
||||
connect(nodes("C"), nodes("F4"), 5000000, 0)
|
||||
connect(nodes("C"), nodes("F5"), 5000000, 0)
|
||||
|
||||
val numberOfChannels = 10
|
||||
val numberOfChannels = 11
|
||||
val channelEndpointsCount = 2 * numberOfChannels
|
||||
|
||||
// we make sure all channels have set up their WatchConfirmed for the funding tx
|
||||
|
@ -211,7 +212,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
|||
// generating more blocks so that all funding txes are buried under at least 6 blocks
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 4))
|
||||
sender.expectMsgType[JValue]
|
||||
awaitAnnouncements(nodes, 10, 10, 20)
|
||||
awaitAnnouncements(nodes, 10, 11, 22)
|
||||
}
|
||||
|
||||
test("send an HTLC A->D") {
|
||||
|
@ -226,14 +227,15 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
|||
sender.expectMsgType[PaymentSucceeded]
|
||||
}
|
||||
|
||||
test("send an HTLC A->D with an invalid expiry delta for C") {
|
||||
// TODO: reenable this test when we support route hints
|
||||
ignore("send an HTLC A->D with an invalid expiry delta for B") {
|
||||
val sender = TestProbe()
|
||||
// to simulate this, we will update C's relay params
|
||||
// first we find out the short channel id for channel C-D, easiest way is to ask D's register which has only one channel
|
||||
sender.send(nodes("D").register, 'shortIds)
|
||||
val shortIdCD = sender.expectMsgType[Map[Long, BinaryData]].keys.head
|
||||
val channelUpdateCD = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, nodes("C").nodeParams.privateKey, nodes("D").nodeParams.privateKey.publicKey, shortIdCD, nodes("D").nodeParams.expiryDeltaBlocks + 1, nodes("D").nodeParams.htlcMinimumMsat, nodes("D").nodeParams.feeBaseMsat, nodes("D").nodeParams.feeProportionalMillionth)
|
||||
sender.send(nodes("C").relayer, channelUpdateCD)
|
||||
// to simulate this, we will update B's relay params
|
||||
// first we find out the short channel id for channel B-C
|
||||
sender.send(nodes("B").router, 'channels)
|
||||
val shortIdBC = sender.expectMsgType[Iterable[ChannelAnnouncement]].find(c => Set(c.nodeId1, c.nodeId2) == Set(nodes("B").nodeParams.privateKey.publicKey, nodes("C").nodeParams.privateKey.publicKey)).get.shortChannelId
|
||||
val channelUpdateBC = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, nodes("B").nodeParams.privateKey, nodes("C").nodeParams.privateKey.publicKey, shortIdBC, nodes("B").nodeParams.expiryDeltaBlocks + 1, nodes("C").nodeParams.htlcMinimumMsat, nodes("B").nodeParams.feeBaseMsat, nodes("B").nodeParams.feeProportionalMillionth)
|
||||
sender.send(nodes("B").relayer, channelUpdateBC)
|
||||
// first we retrieve a payment hash from D
|
||||
val amountMsat = MilliSatoshi(4200000)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
|
||||
|
@ -241,23 +243,22 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
|||
// then we make the actual payment
|
||||
val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey)
|
||||
sender.send(nodes("A").paymentInitiator, sendReq)
|
||||
// A will receive an error from C that include the updated channel update, then will retry the payment
|
||||
// A will receive an error from B that include the updated channel update, then will retry the payment
|
||||
sender.expectMsgType[PaymentSucceeded](5 seconds)
|
||||
// in the meantime, the router will have updated its state
|
||||
awaitCond({
|
||||
sender.send(nodes("A").router, 'updates)
|
||||
sender.expectMsgType[Iterable[ChannelUpdate]].toSeq.contains(channelUpdateCD)
|
||||
sender.expectMsgType[Iterable[ChannelUpdate]].toSeq.contains(channelUpdateBC)
|
||||
}, max = 20 seconds, interval = 1 second)
|
||||
// finally we retry the same payment, this time successfully
|
||||
}
|
||||
|
||||
test("send an HTLC A->D with an amount greater than capacity of C-D") {
|
||||
test("send an HTLC A->D with an amount greater than capacity of B-C") {
|
||||
val sender = TestProbe()
|
||||
// first we retrieve a payment hash from D
|
||||
val amountMsat = MilliSatoshi(300000000L)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
// then we make the payment (C-D has a smaller capacity than A-B and B-C)
|
||||
// then we make the payment (B-C has a smaller capacity than A-B and C-D)
|
||||
val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey)
|
||||
sender.send(nodes("A").paymentInitiator, sendReq)
|
||||
// A will first receive an error from C, then retry and route around C: A->B->E->C->D
|
||||
|
@ -269,7 +270,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
|||
val pr = SendPayment(100000000L, "42" * 32, nodes("D").nodeParams.privateKey.publicKey)
|
||||
sender.send(nodes("A").paymentInitiator, pr)
|
||||
|
||||
// A will first receive an error from C, then retry and route around C: A->B->E->C->D
|
||||
// A will receive an error from D and won't retry
|
||||
val failed = sender.expectMsgType[PaymentFailed]
|
||||
assert(failed.paymentHash === pr.paymentHash)
|
||||
assert(failed.failures.size === 1)
|
||||
|
@ -325,6 +326,22 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
|||
sender.expectMsgType[PaymentSucceeded]
|
||||
}
|
||||
|
||||
test("send multiple HTLCs A->D with a failover when a channel gets exhausted") {
|
||||
val sender = TestProbe()
|
||||
// there are two C-D channels with 5000000 sat, so we should be able to make 7 payments worth 1000000 sat each
|
||||
for (i <- 0 until 7) {
|
||||
// first we retrieve a payment hash from D for 2 mBTC
|
||||
val amountMsat = MilliSatoshi(1000000000L)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 payment"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
|
||||
// A send payment of 3 mBTC, more than asked but it should still be accepted
|
||||
val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey)
|
||||
sender.send(nodes("A").paymentInitiator, sendReq)
|
||||
sender.expectMsgType[PaymentSucceeded]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We currently use p2pkh script Helpers.getFinalScriptPubKey
|
||||
*
|
||||
|
@ -405,7 +422,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
|||
val receivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString])
|
||||
(receivedByC diff previouslyReceivedByC).size == 1
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
awaitAnnouncements(nodes.filter(_._1 == "A"), 9, 9, 18)
|
||||
awaitAnnouncements(nodes.filter(_._1 == "A"), 9, 10, 20)
|
||||
}
|
||||
|
||||
test("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (remote commit)") {
|
||||
|
@ -470,7 +487,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
|||
val receivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString])
|
||||
(receivedByC diff previouslyReceivedByC).size == 1
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
awaitAnnouncements(nodes.filter(_._1 == "A"), 8, 8, 16)
|
||||
awaitAnnouncements(nodes.filter(_._1 == "A"), 8, 9, 18)
|
||||
}
|
||||
|
||||
test("propagate a failure upstream when a downstream htlc times out (local commit)") {
|
||||
|
@ -516,7 +533,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
|||
val receivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString])
|
||||
(receivedByC diff previouslyReceivedByC).size == 2
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
awaitAnnouncements(nodes.filter(_._1 == "A"), 7, 7, 14)
|
||||
awaitAnnouncements(nodes.filter(_._1 == "A"), 7, 8, 16)
|
||||
}
|
||||
|
||||
test("propagate a failure upstream when a downstream htlc times out (remote commit)") {
|
||||
|
@ -564,7 +581,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
|||
val receivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString])
|
||||
(receivedByC diff previouslyReceivedByC).size == 2
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
awaitAnnouncements(nodes.filter(_._1 == "A"), 6, 6, 12)
|
||||
awaitAnnouncements(nodes.filter(_._1 == "A"), 6, 7, 14)
|
||||
}
|
||||
|
||||
test("punish a node that has published a revoked commit tx") {
|
||||
|
@ -616,7 +633,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
|||
(receivedByC diff previouslyReceivedByC).size == 2
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
// this will remove the channel
|
||||
awaitAnnouncements(nodes.filter(_._1 == "A"), 5, 5, 10)
|
||||
awaitAnnouncements(nodes.filter(_._1 == "A"), 5, 6, 12)
|
||||
}
|
||||
|
||||
test("generate and validate lots of channels") {
|
||||
|
@ -640,7 +657,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
|||
announcements.foreach(ann => nodes("A").router ! ann)
|
||||
awaitCond({
|
||||
sender.send(nodes("D").router, 'channels)
|
||||
sender.expectMsgType[Iterable[ChannelAnnouncement]](5 seconds).size == channels.size + 5 // 5 remaining channels because D->F{1-F4} have disappeared
|
||||
sender.expectMsgType[Iterable[ChannelAnnouncement]](5 seconds).size == channels.size + 6 // 6 remaining channels because D->F{1-F4} have disappeared
|
||||
}, max = 120 seconds, interval = 1 second)
|
||||
}
|
||||
|
||||
|
|
|
@ -97,7 +97,7 @@ class PaymentRequestSpec extends FunSuite {
|
|||
}
|
||||
|
||||
test("On mainnet, with fallback address 1RustyRX2oai4EYYDpQGWvEL62BBGqN9T with extra routing info to go via nodes 029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255 then 039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255") {
|
||||
val ref = "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqqqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqqqqqqq7qqzqfnlkwydm8rg30gjku7wmxmk06sevjp53fmvrcfegvwy7d5443jvyhxsel0hulkstws7vqv400q4j3wgpk4crg49682hr4scqvmad43cqd5m7tf"
|
||||
val ref = "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzqj9n4evl6mr5aj9f58zp6fyjzup6ywn3x6sk8akg5v4tgn2q8g4fhx05wf6juaxu9760yp46454gpg5mtzgerlzezqcqvjnhjh8z3g2qqdhhwkj"
|
||||
val pr = PaymentRequest.read(ref)
|
||||
assert(pr.prefix == "lnbc")
|
||||
assert(pr.amount === Some(MilliSatoshi(2000000000L)))
|
||||
|
@ -106,9 +106,12 @@ class PaymentRequestSpec extends FunSuite {
|
|||
assert(pr.nodeId == PublicKey(BinaryData("03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")))
|
||||
assert(pr.description == Right(Crypto.sha256("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))
|
||||
assert(pr.fallbackAddress === Some("1RustyRX2oai4EYYDpQGWvEL62BBGqN9T"))
|
||||
assert(pr.routingInfo() === List(RoutingInfoTag(List(ExtraHop(PublicKey("029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255"), 72623859790382856L, 20, 3), ExtraHop(PublicKey("039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255"), 217304205466536202L, 30, 4)))))
|
||||
assert(BinaryData(Protocol.writeUInt64(72623859790382856L, ByteOrder.BIG_ENDIAN)) == BinaryData("0102030405060708"))
|
||||
assert(BinaryData(Protocol.writeUInt64(217304205466536202L, ByteOrder.BIG_ENDIAN)) == BinaryData("030405060708090a"))
|
||||
assert(pr.routingInfo() === List(RoutingInfoTag(List(
|
||||
ExtraHop(PublicKey("029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255"), 72623859790382856L, 1, 20, 3),
|
||||
ExtraHop(PublicKey("039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255"), 217304205466536202L, 2, 30, 4)
|
||||
))))
|
||||
assert(BinaryData(Protocol.writeUInt64(0x0102030405060708L, ByteOrder.BIG_ENDIAN)) == BinaryData("0102030405060708"))
|
||||
assert(BinaryData(Protocol.writeUInt64(0x030405060708090aL, ByteOrder.BIG_ENDIAN)) == BinaryData("030405060708090a"))
|
||||
assert(pr.tags.size == 4)
|
||||
assert(PaymentRequest.write(pr.sign(priv)) == ref)
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
package fr.acinq.eclair.payment
|
||||
|
||||
import akka.actor.ActorRef
|
||||
import akka.actor.{ActorRef, Status}
|
||||
import akka.testkit.TestProbe
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.bitcoin.{BinaryData, Crypto, MilliSatoshi}
|
||||
import fr.acinq.eclair.randomBytes
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.crypto.Sphinx
|
||||
import fr.acinq.eclair.payment.PaymentLifecycle.buildCommand
|
||||
|
@ -37,12 +38,8 @@ class RelayerSpec extends TestkitBaseClass {
|
|||
}
|
||||
}
|
||||
|
||||
// node c is the next node in the route
|
||||
val nodeId_a = PublicKey(a)
|
||||
val nodeId_c = PublicKey(c)
|
||||
val channelId_ab: BinaryData = "65514354" * 8
|
||||
val channelId_bc: BinaryData = "64864544" * 8
|
||||
val channel_flags = 0x00.toByte
|
||||
val channelId_ab: BinaryData = randomBytes(32)
|
||||
val channelId_bc: BinaryData = randomBytes(32)
|
||||
|
||||
def makeCommitments(channelId: BinaryData) = Commitments(null, null, 0.toByte, null, null, null, null, 0, 0, Map.empty, null, null, null, channelId)
|
||||
|
||||
|
@ -83,6 +80,32 @@ class RelayerSpec extends TestkitBaseClass {
|
|||
paymentHandler.expectNoMsg(500 millis)
|
||||
}
|
||||
|
||||
test("fail to relay an htlc-add when register returns an error") { case (relayer, register, paymentHandler) =>
|
||||
val sender = TestProbe()
|
||||
|
||||
// we use this to build a valid onion
|
||||
val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops)
|
||||
// and then manually build an htlc
|
||||
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.expiry, cmd.onion)
|
||||
relayer ! channelUpdate_bc
|
||||
|
||||
sender.send(relayer, ForwardAdd(add_ab))
|
||||
|
||||
val fwd1 = register.expectMsgType[Register.ForwardShortId[CMD_ADD_HTLC]]
|
||||
assert(fwd1.shortChannelId === channelUpdate_bc.shortChannelId)
|
||||
assert(fwd1.message.upstream_opt === Some(add_ab))
|
||||
|
||||
sender.send(relayer, Status.Failure(Register.ForwardShortIdFailure(fwd1)))
|
||||
|
||||
val fwd2 = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]]
|
||||
assert(fwd2.channelId === channelId_ab)
|
||||
assert(fwd2.message.id === add_ab.id)
|
||||
assert(fwd2.message.reason === Right(UnknownNextPeer))
|
||||
|
||||
register.expectNoMsg(500 millis)
|
||||
paymentHandler.expectNoMsg(500 millis)
|
||||
}
|
||||
|
||||
test("fail to relay an htlc-add when the requested channel is disabled") { case (relayer, register, paymentHandler) =>
|
||||
val sender = TestProbe()
|
||||
|
||||
|
@ -232,7 +255,7 @@ class RelayerSpec extends TestkitBaseClass {
|
|||
assert(fwd.shortChannelId === channelUpdate_bc.shortChannelId)
|
||||
assert(fwd.message.upstream_opt === Some(add_ab))
|
||||
|
||||
sender.send(relayer, AddHtlcFailed(channelId_bc, new InsufficientFunds(channelId_bc, cmd.amountMsat, 100, 0, 0), Relayed(add_ab.channelId, add_ab.id, add_ab.amountMsat, cmd.amountMsat), Some(channelUpdate_bc)))
|
||||
sender.send(relayer, Status.Failure(AddHtlcFailed(channelId_bc, new InsufficientFunds(channelId_bc, cmd.amountMsat, 100, 0, 0), Relayed(add_ab.channelId, add_ab.id, add_ab.amountMsat, cmd.amountMsat), Some(channelUpdate_bc))))
|
||||
|
||||
val fail = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
|
||||
assert(fail.id === add_ab.id)
|
||||
|
@ -256,7 +279,7 @@ class RelayerSpec extends TestkitBaseClass {
|
|||
assert(fwd.shortChannelId === channelUpdate_bc.shortChannelId)
|
||||
assert(fwd.message.upstream_opt === Some(add_ab))
|
||||
|
||||
sender.send(relayer, AddHtlcFailed(channelId_bc, new TooManyAcceptedHtlcs(channelId_bc, 30), Relayed(add_ab.channelId, add_ab.id, add_ab.amountMsat, cmd.amountMsat), Some(channelUpdate_bc)))
|
||||
sender.send(relayer, Status.Failure(AddHtlcFailed(channelId_bc, new TooManyAcceptedHtlcs(channelId_bc, 30), Relayed(add_ab.channelId, add_ab.id, add_ab.amountMsat, cmd.amountMsat), Some(channelUpdate_bc))))
|
||||
|
||||
val fail = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
|
||||
assert(fail.id === add_ab.id)
|
||||
|
@ -280,7 +303,7 @@ class RelayerSpec extends TestkitBaseClass {
|
|||
assert(fwd.shortChannelId === channelUpdate_bc.shortChannelId)
|
||||
assert(fwd.message.upstream_opt === Some(add_ab))
|
||||
|
||||
sender.send(relayer, AddHtlcFailed(channelId_bc, new HtlcTimedout(channelId_bc), Relayed(add_ab.channelId, add_ab.id, add_ab.amountMsat, cmd.amountMsat), Some(channelUpdate_bc)))
|
||||
sender.send(relayer, Status.Failure(AddHtlcFailed(channelId_bc, new HtlcTimedout(channelId_bc), Relayed(add_ab.channelId, add_ab.id, add_ab.amountMsat, cmd.amountMsat), Some(channelUpdate_bc))))
|
||||
|
||||
val fail = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
|
||||
assert(fail.id === add_ab.id)
|
||||
|
|
|
@ -200,8 +200,10 @@ class RouteCalculationSpec extends FunSuite {
|
|||
val reverseRoute = List(hopBA, hopCB)
|
||||
val extraRoute = PaymentHop.buildExtra(reverseRoute, amount.amount)
|
||||
|
||||
assert(extraRoute === List(ExtraHop(PublicKey("02f0b230e53723ccc331db140edc518be1ee5ab29a508104a4be2f5be922c928e8"), 24412456671576064L, 547005, 144),
|
||||
ExtraHop(PublicKey("032b4af42b5e8089a7a06005ead9ac4667527390ee39c998b7b0307f0d81d7f4ac"), 23366821113626624L, 547000, 144)))
|
||||
assert(extraRoute === List(
|
||||
ExtraHop(PublicKey("02f0b230e53723ccc331db140edc518be1ee5ab29a508104a4be2f5be922c928e8"), 24412456671576064L, 546000, 10, 144),
|
||||
ExtraHop(PublicKey("032b4af42b5e8089a7a06005ead9ac4667527390ee39c998b7b0307f0d81d7f4ac"), 23366821113626624L, 546000, 10, 144))
|
||||
)
|
||||
|
||||
// Sender side
|
||||
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
package fr.acinq.eclair.gui.utils
|
||||
|
||||
import fr.acinq.bitcoin.MilliSatoshi
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.FunSuite
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class CoinUtilsSpec extends FunSuite {
|
||||
|
||||
test("Convert string amount to the correct BtcAmount") {
|
||||
val am_btc: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1", CoinUtils.BTC_LABEL)
|
||||
assert(am_btc == MilliSatoshi(100000000000L))
|
||||
val am_mbtc: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1", CoinUtils.MILLI_BTC_LABEL)
|
||||
assert(am_mbtc == MilliSatoshi(100000000L))
|
||||
val am_sat: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1", CoinUtils.SATOSHI_LABEL)
|
||||
assert(am_sat == MilliSatoshi(1000))
|
||||
val am_msat: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1", CoinUtils.MILLI_SATOSHI_LABEL)
|
||||
assert(am_msat == MilliSatoshi(1))
|
||||
val am_zero: MilliSatoshi = CoinUtils.convertStringAmountToMsat("0", CoinUtils.MILLI_BTC_LABEL)
|
||||
assert(am_zero == MilliSatoshi(0))
|
||||
}
|
||||
|
||||
test("Convert decimal string amount to the correct BtcAmount") {
|
||||
val am_btc_dec: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1.23456789876", CoinUtils.BTC_LABEL)
|
||||
assert(am_btc_dec == MilliSatoshi(123456789876L))
|
||||
val am_mbtc_dec: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1.23456789", CoinUtils.MILLI_BTC_LABEL)
|
||||
assert(am_mbtc_dec == MilliSatoshi(123456789L))
|
||||
val am_sat_dec: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1.23456789", CoinUtils.SATOSHI_LABEL)
|
||||
assert(am_sat_dec == MilliSatoshi(1234))
|
||||
val am_msat_dec: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1.234", CoinUtils.MILLI_SATOSHI_LABEL)
|
||||
assert(am_msat_dec == MilliSatoshi(1))
|
||||
}
|
||||
|
||||
test("Convert string amount with unknown unit") {
|
||||
intercept[IllegalArgumentException](CoinUtils.convertStringAmountToMsat("1.23456789876", "foo"))
|
||||
}
|
||||
|
||||
test("Convert string amount with a non numerical amount") {
|
||||
intercept[NumberFormatException](CoinUtils.convertStringAmountToMsat("1.abcd", CoinUtils.MILLI_BTC_LABEL))
|
||||
}
|
||||
|
||||
test("Convert string amount with an empty amount") {
|
||||
intercept[NumberFormatException](CoinUtils.convertStringAmountToMsat("", CoinUtils.MILLI_BTC_LABEL))
|
||||
}
|
||||
|
||||
test("Convert string amount with a invalid numerical amount") {
|
||||
intercept[NumberFormatException](CoinUtils.convertStringAmountToMsat("1.23.45", CoinUtils.MILLI_BTC_LABEL))
|
||||
}
|
||||
|
||||
test("Convert string amount with a negative numerical amount") {
|
||||
intercept[IllegalArgumentException](CoinUtils.convertStringAmountToMsat("-1", CoinUtils.MILLI_BTC_LABEL))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
7
pom.xml
7
pom.xml
|
@ -66,11 +66,6 @@
|
|||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>3.0.2</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>pl.project13.maven</groupId>
|
||||
<artifactId>git-commit-id-plugin</artifactId>
|
||||
<version>2.2.3</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>com.github.chrisdchristo</groupId>
|
||||
<artifactId>capsule-maven-plugin</artifactId>
|
||||
|
@ -87,7 +82,7 @@
|
|||
<plugin>
|
||||
<groupId>pl.project13.maven</groupId>
|
||||
<artifactId>git-commit-id-plugin</artifactId>
|
||||
<version>2.2.2</version>
|
||||
<version>2.2.3</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
|
|
Loading…
Add table
Reference in a new issue