diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index 7c425328c4..82ab7ba769 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -27,15 +27,17 @@ import fr.acinq.eclair.channel.Register.{Forward, ForwardShortId} import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.{IncomingPayment, NetworkFee, OutgoingPayment, Stats} import fr.acinq.eclair.io.Peer.{GetPeerInfo, PeerInfo} -import fr.acinq.eclair.io.{NodeURI, Peer, Switchboard} +import fr.acinq.eclair.io.{NodeURI, Peer} import fr.acinq.eclair.payment.PaymentLifecycle._ import fr.acinq.eclair.router.{ChannelDesc, RouteRequest, RouteResponse, Router} import scodec.bits.ByteVector + import scala.concurrent.Future import scala.concurrent.duration._ import fr.acinq.eclair.payment.{PaymentReceived, PaymentRelayed, PaymentRequest, PaymentSent} import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAddress, NodeAnnouncement} import TimestampQueryFilters._ +import fr.acinq.eclair.payment.Relayer.OutgoingChannel case class GetInfoResponse(nodeId: PublicKey, alias: String, chainHash: ByteVector32, blockHeight: Int, publicAddresses: Seq[NodeAddress]) @@ -105,6 +107,7 @@ trait Eclair { def getInfoResponse()(implicit timeout: Timeout): Future[GetInfoResponse] + def usableBalances()(implicit timeout: Timeout): Future[Iterable[OutgoingChannel]] } class EclairImpl(appKit: Kit) extends Eclair { @@ -269,4 +272,5 @@ class EclairImpl(appKit: Kit) extends Eclair { publicAddresses = appKit.nodeParams.publicAddresses) ) + override def usableBalances()(implicit timeout: Timeout): Future[Iterable[OutgoingChannel]] = (appKit.relayer ? 'usableBalances).mapTo[Iterable[OutgoingChannel]] } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala index f816aa8be3..e7f2a26e53 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -281,6 +281,9 @@ trait Service extends ExtraDirectives with Logging { } ~ path("channelstats") { complete(eclairApi.channelStats()) + } ~ + path("usablebalances") { + complete(eclairApi.usableBalances()) } } ~ get { path("ws") { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index ee0e47f09c..a4f71b7bde 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -40,6 +40,7 @@ case class PublishableTxs(commitTx: CommitTx, htlcTxsAndSigs: List[HtlcTxAndSigs case class LocalCommit(index: Long, spec: CommitmentSpec, publishableTxs: PublishableTxs) case class RemoteCommit(index: Long, spec: CommitmentSpec, txid: ByteVector32, remotePerCommitmentPoint: Point) case class WaitingForRevocation(nextRemoteCommit: RemoteCommit, sent: CommitSig, sentAfterLocalCommitIndex: Long, reSignAsap: Boolean = false) +case class UsableBalances(localMsat: Long, remoteMsat: Long, isPublic: Boolean) // @formatter:on /** @@ -71,12 +72,14 @@ case class Commitments(localParams: LocalParams, remoteParams: RemoteParams, def addRemoteProposal(proposal: UpdateMessage): Commitments = Commitments.addRemoteProposal(this, proposal) - def announceChannel: Boolean = (channelFlags & 0x01) != 0 + val announceChannel: Boolean = (channelFlags & 0x01) != 0 - def availableBalanceForSendMsat: Long = { + def usableBalances: UsableBalances = { val reduced = CommitmentSpec.reduce(remoteCommit.spec, remoteChanges.acked, localChanges.proposed) - val feesMsat = if (localParams.isFunder) Transactions.commitTxFee(Satoshi(remoteParams.dustLimitSatoshis), reduced).amount * 1000 else 0 - reduced.toRemoteMsat - remoteParams.channelReserveSatoshis * 1000 - feesMsat + val commitTxFee = Transactions.commitTxFee(Satoshi(remoteParams.dustLimitSatoshis), reduced).amount * 1000 + val localBalance = reduced.toRemoteMsat - remoteParams.channelReserveSatoshis * 1000 - { if (localParams.isFunder) commitTxFee else 0 } + val remoteBalance = reduced.toLocalMsat - localParams.channelReserveSatoshis * 1000 - { if (localParams.isFunder) 0 else commitTxFee } + UsableBalances(localMsat = localBalance, remoteMsat = remoteBalance, isPublic = announceChannel) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala index 0d0b55fe4a..a967e61634 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala @@ -70,9 +70,12 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR def main(channelUpdates: Map[ShortChannelId, OutgoingChannel], node2channels: mutable.HashMap[PublicKey, mutable.Set[ShortChannelId]] with mutable.MultiMap[PublicKey, ShortChannelId]): Receive = { + case 'usableBalances => + sender ! channelUpdates.values + case LocalChannelUpdate(_, channelId, shortChannelId, remoteNodeId, _, channelUpdate, commitments) => log.debug(s"updating local channel info for channelId=$channelId shortChannelId=$shortChannelId remoteNodeId=$remoteNodeId channelUpdate={} commitments={}", channelUpdate, commitments) - context become main(channelUpdates + (channelUpdate.shortChannelId -> OutgoingChannel(remoteNodeId, channelUpdate, commitments.availableBalanceForSendMsat)), node2channels.addBinding(remoteNodeId, channelUpdate.shortChannelId)) + context become main(channelUpdates + (channelUpdate.shortChannelId -> OutgoingChannel(remoteNodeId, channelUpdate, commitments.usableBalances)), node2channels.addBinding(remoteNodeId, channelUpdate.shortChannelId)) case LocalChannelDown(_, channelId, shortChannelId, remoteNodeId) => log.debug(s"removed local channel info for channelId=$channelId shortChannelId=$shortChannelId") @@ -80,7 +83,7 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR case AvailableBalanceChanged(_, _, shortChannelId, _, commitments) => val channelUpdates1 = channelUpdates.get(shortChannelId) match { - case Some(c: OutgoingChannel) => channelUpdates + (shortChannelId -> c.copy(availableBalanceMsat = commitments.availableBalanceForSendMsat)) + case Some(c: OutgoingChannel) => channelUpdates + (shortChannelId -> c.copy(usableBalances = commitments.usableBalances)) case None => channelUpdates // we only consider the balance if we have the channel_update } context become main(channelUpdates1, node2channels) @@ -197,7 +200,7 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR object Relayer { def props(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorRef) = Props(classOf[Relayer], nodeParams, register, paymentHandler) - case class OutgoingChannel(nextNodeId: PublicKey, channelUpdate: ChannelUpdate, availableBalanceMsat: Long) + case class OutgoingChannel(nextNodeId: PublicKey, channelUpdate: ChannelUpdate, usableBalances: UsableBalances) // @formatter:off sealed trait NextPayload @@ -302,10 +305,10 @@ object Relayer { val channelInfo_opt = channelUpdates.get(shortChannelId) val channelUpdate_opt = channelInfo_opt.map(_.channelUpdate) val relayResult = relayOrFail(relayPayload, channelUpdate_opt) - log.debug(s"candidate channel for htlc #${add.id} paymentHash=${add.paymentHash}: shortChannelId={} balanceMsat={} channelUpdate={} relayResult={}", shortChannelId, channelInfo_opt.map(_.availableBalanceMsat).getOrElse(""), channelUpdate_opt.getOrElse(""), relayResult) + log.debug(s"candidate channel for htlc #${add.id} paymentHash=${add.paymentHash}: shortChannelId={} balanceMsat={} channelUpdate={} relayResult={}", shortChannelId, channelInfo_opt.map(_.usableBalances.localMsat).getOrElse(""), channelUpdate_opt.getOrElse(""), relayResult) (shortChannelId, channelInfo_opt, relayResult) } - .collect { case (shortChannelId, Some(channelInfo), Right(_)) => (shortChannelId, channelInfo.availableBalanceMsat) } + .collect { case (shortChannelId, Some(channelInfo), Right(_)) => (shortChannelId, channelInfo.usableBalances.localMsat) } .filter(_._2 > relayPayload.payload.amtToForward) // we only keep channels that have enough balance to handle this payment .toList // needed for ordering .sortBy(_._2) // we want to use the channel with the lowest available balance that can process the payment diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala index bcab510f4d..9ff0f11c70 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala @@ -103,7 +103,7 @@ trait StateTestsHelperMethods extends TestKitBase { bob2blockchain.expectMsgType[WatchConfirmed] // deeply buried awaitCond(alice.stateName == NORMAL) awaitCond(bob.stateName == NORMAL) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.availableBalanceForSendMsat == pushMsat - TestConstants.Alice.channelParams.channelReserveSatoshis * 1000) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.usableBalances.localMsat == pushMsat - TestConstants.Alice.channelParams.channelReserveSatoshis * 1000) // x2 because alice and bob share the same relayer channelUpdateListener.expectMsgType[LocalChannelUpdate] channelUpdateListener.expectMsgType[LocalChannelUpdate] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala index 7a7508ef03..556218296d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.payment import fr.acinq.bitcoin.Block import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.eclair.channel.{AddHtlcFailed, CMD_ADD_HTLC, CMD_FAIL_HTLC} +import fr.acinq.eclair.channel.{AddHtlcFailed, UsableBalances, CMD_ADD_HTLC, CMD_FAIL_HTLC} import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.payment.Relayer.{OutgoingChannel, RelayPayload} import fr.acinq.eclair.router.Announcements @@ -81,11 +81,11 @@ class ChannelSelectionSpec extends FunSuite { val channelUpdate = dummyUpdate(ShortChannelId(12345), 10, 100, 1000, 100, 10000000, true) val channelUpdates = Map( - ShortChannelId(11111) -> OutgoingChannel(a, channelUpdate, 100000000), - ShortChannelId(12345) -> OutgoingChannel(a, channelUpdate, 20000000), - ShortChannelId(22222) -> OutgoingChannel(a, channelUpdate, 10000000), - ShortChannelId(33333) -> OutgoingChannel(a, channelUpdate, 100000), - ShortChannelId(44444) -> OutgoingChannel(b, channelUpdate, 1000000) + ShortChannelId(11111) -> OutgoingChannel(a, channelUpdate, UsableBalances(100000000, 0, isPublic = true)), + ShortChannelId(12345) -> OutgoingChannel(a, channelUpdate, UsableBalances(20000000, 0, isPublic = true)), + ShortChannelId(22222) -> OutgoingChannel(a, channelUpdate, UsableBalances(10000000, 0, isPublic = false)), + ShortChannelId(33333) -> OutgoingChannel(a, channelUpdate, UsableBalances(100000, 0, isPublic = false)), + ShortChannelId(44444) -> OutgoingChannel(b, channelUpdate, UsableBalances(1000000, 0, isPublic = true)) ) val node2channels = new mutable.HashMap[PublicKey, mutable.Set[ShortChannelId]] with mutable.MultiMap[PublicKey, ShortChannelId] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala index a71efb14ee..d717c7d3e1 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala @@ -58,9 +58,8 @@ class RelayerSpec extends TestkitBaseClass { val channelId_ab = randomBytes32 val channelId_bc = randomBytes32 - def makeCommitments(channelId: ByteVector32, availableBalanceMsat: Long = 50000000L) = new Commitments(null, null, 0.toByte, null, null, - null, null, 0, 0, Map.empty, null, null, null, channelId) { - override def availableBalanceForSendMsat: Long = availableBalanceMsat + def makeCommitments(channelId: ByteVector32, availableBalanceMsat: Long = 50000000L) = new Commitments(null, null, 0.toByte, null, null, null, null, 0, 0, Map.empty, null, null, null, channelId) { + override def usableBalances: UsableBalances = UsableBalances(availableBalanceMsat, 0, isPublic = true) } test("relay an htlc-add") { f =>