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

Add channelbalances API call (#2196)

The `channelbalances` API call retrieves information about the balances of all local channels, not just those with usable outgoing balances that are enabled for sending.

This change also adds the `isEnabled` attribute to the json results for both the new `channelbalances` and old `usablebalances` API calls.
This commit is contained in:
Richard Myers 2022-03-29 11:10:42 +02:00 committed by GitHub
parent 2872d876d0
commit 9358e5e1f5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 71 additions and 31 deletions

View file

@ -8,7 +8,7 @@
### API changes
<insert changes>
- `channelbalances` Retrieves information about the balances of all local channels. (#2196)
### Miscellaneous improvements and bug fixes

View file

@ -44,6 +44,7 @@ and COMMAND is one of the available commands:
- allchannels
- allupdates
- channelstats
- channelbalances
=== Fees ===
- networkfees

View file

@ -19,9 +19,6 @@ package fr.acinq.eclair
import akka.actor.ActorRef
import akka.actor.typed.scaladsl.AskPattern.Askable
import akka.actor.typed.scaladsl.adapter.ClassicSchedulerOps
import akka.actor.{ActorRef, typed}
import akka.actor.typed.scaladsl.AskPattern.{Askable, schedulerFromActorSystem}
import akka.actor.typed.scaladsl.adapter.{ClassicActorSystemOps, ClassicSchedulerOps}
import akka.pattern._
import akka.util.Timeout
import com.softwaremill.quicklens.ModifyPimp
@ -43,7 +40,7 @@ import fr.acinq.eclair.io._
import fr.acinq.eclair.message.{OnionMessages, Postman}
import fr.acinq.eclair.payment._
import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment
import fr.acinq.eclair.payment.relay.Relayer.{GetOutgoingChannels, OutgoingChannels, RelayFees, UsableBalance}
import fr.acinq.eclair.payment.relay.Relayer.{ChannelBalance, GetOutgoingChannels, OutgoingChannels, RelayFees}
import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.PreimageReceived
import fr.acinq.eclair.payment.send.PaymentInitiator._
import fr.acinq.eclair.router.Router
@ -147,7 +144,9 @@ trait Eclair {
def getInfo()(implicit timeout: Timeout): Future[GetInfoResponse]
def usableBalances()(implicit timeout: Timeout): Future[Iterable[UsableBalance]]
def usableBalances()(implicit timeout: Timeout): Future[Iterable[ChannelBalance]]
def channelBalances()(implicit timeout: Timeout): Future[Iterable[ChannelBalance]]
def onChainBalance(): Future[OnChainBalance]
@ -485,8 +484,11 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
instanceId = appKit.nodeParams.instanceId.toString)
)
override def usableBalances()(implicit timeout: Timeout): Future[Iterable[UsableBalance]] =
(appKit.relayer ? GetOutgoingChannels()).mapTo[OutgoingChannels].map(_.channels.map(_.toUsableBalance))
override def usableBalances()(implicit timeout: Timeout): Future[Iterable[ChannelBalance]] =
(appKit.relayer ? GetOutgoingChannels()).mapTo[OutgoingChannels].map(_.channels.map(_.toChannelBalance))
override def channelBalances()(implicit timeout: Timeout): Future[Iterable[ChannelBalance]] =
(appKit.relayer ? GetOutgoingChannels(enabledOnly = false)).mapTo[OutgoingChannels].map(_.channels.map(_.toChannelBalance))
override def globalBalance()(implicit timeout: Timeout): Future[GlobalBalance] = {
for {

View file

@ -132,7 +132,7 @@ object Relayer extends Logging {
}
case class RelayForward(add: UpdateAddHtlc)
case class UsableBalance(remoteNodeId: PublicKey, shortChannelId: ShortChannelId, canSend: MilliSatoshi, canReceive: MilliSatoshi, isPublic: Boolean)
case class ChannelBalance(remoteNodeId: PublicKey, shortChannelId: ShortChannelId, canSend: MilliSatoshi, canReceive: MilliSatoshi, isPublic: Boolean, isEnabled: Boolean)
/**
* Get the list of local outgoing channels.
@ -141,12 +141,13 @@ object Relayer extends Logging {
*/
case class GetOutgoingChannels(enabledOnly: Boolean = true)
case class OutgoingChannel(nextNodeId: PublicKey, channelUpdate: ChannelUpdate, prevChannelUpdate: Option[ChannelUpdate], commitments: AbstractCommitments) {
def toUsableBalance: UsableBalance = UsableBalance(
def toChannelBalance: ChannelBalance = ChannelBalance(
remoteNodeId = nextNodeId,
shortChannelId = channelUpdate.shortChannelId,
canSend = commitments.availableBalanceForSend,
canReceive = commitments.availableBalanceForReceive,
isPublic = commitments.announceChannel)
isPublic = commitments.announceChannel,
isEnabled = channelUpdate.channelFlags.isEnabled)
}
case class OutgoingChannels(channels: Seq[OutgoingChannel])

View file

@ -30,13 +30,12 @@ import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw}
import fr.acinq.eclair.channel._
import fr.acinq.eclair.db._
import fr.acinq.eclair.io.Peer.OpenChannel
import fr.acinq.eclair.payment.Bolt11Invoice
import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop
import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment
import fr.acinq.eclair.payment.receive.PaymentHandler
import fr.acinq.eclair.payment.relay.Relayer.RelayFees
import fr.acinq.eclair.payment.relay.Relayer.{GetOutgoingChannels, RelayFees}
import fr.acinq.eclair.payment.send.PaymentInitiator._
import fr.acinq.eclair.payment.{PaymentFailed, Invoice}
import fr.acinq.eclair.payment.{Bolt11Invoice, Invoice, PaymentFailed}
import fr.acinq.eclair.router.RouteCalculationSpec.makeUpdateShort
import fr.acinq.eclair.router.Router.{PredefinedNodeRoute, PublicChannel}
import fr.acinq.eclair.router.{Announcements, Router}
@ -56,7 +55,7 @@ import scala.concurrent.duration._
class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with IdiomaticMockito with ParallelTestExecution {
implicit val timeout: Timeout = Timeout(30 seconds)
case class FixtureParam(register: TestProbe, router: TestProbe, paymentInitiator: TestProbe, switchboard: TestProbe, paymentHandler: TestProbe, sender: TestProbe, kit: Kit)
case class FixtureParam(register: TestProbe, relayer: TestProbe, router: TestProbe, paymentInitiator: TestProbe, switchboard: TestProbe, paymentHandler: TestProbe, sender: TestProbe, kit: Kit)
override def withFixture(test: OneArgTest): Outcome = {
val watcher = TestProbe()
@ -86,7 +85,7 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I
postman.ref.toTyped,
new DummyOnChainWallet()
)
withFixture(test.toNoArgTest(FixtureParam(register, router, paymentInitiator, switchboard, paymentHandler, TestProbe(), kit)))
withFixture(test.toNoArgTest(FixtureParam(register, relayer, router, paymentInitiator, switchboard, paymentHandler, TestProbe(), kit)))
}
test("convert fee rate properly") { f =>
@ -611,4 +610,15 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I
peersDb.addOrUpdateRelayFees(b, RelayFees(999 msat, 1234)).wasCalled(once)
}
test("channelBalances asks for all channels, usableBalances only for enabled ones") { f =>
import f._
val eclair = new EclairImpl(kit)
eclair.channelBalances().pipeTo(sender.ref)
relayer.expectMsg(GetOutgoingChannels(enabledOnly=false))
eclair.usableBalances().pipeTo(sender.ref)
relayer.expectMsg(GetOutgoingChannels())
}
}

View file

@ -699,8 +699,8 @@ class PaymentIntegrationSpec extends IntegrationSpec {
val channels1 = sender.expectMsgType[Relayer.OutgoingChannels]
val channels2 = sender.expectMsgType[Relayer.OutgoingChannels]
logger.info(channels1.channels.map(_.toUsableBalance))
logger.info(channels2.channels.map(_.toUsableBalance))
logger.info(channels1.channels.map(_.toChannelBalance))
logger.info(channels2.channels.map(_.toChannelBalance))
}
}

View file

@ -463,9 +463,9 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a
val channels1 = getOutgoingChannels(true)
assert(channels1.size === 2)
assert(channels1.head.channelUpdate === channelUpdate_ab)
assert(channels1.head.toUsableBalance === Relayer.UsableBalance(a, channelUpdate_ab.shortChannelId, 0 msat, 300000 msat, isPublic = false))
assert(channels1.head.toChannelBalance === Relayer.ChannelBalance(a, channelUpdate_ab.shortChannelId, 0 msat, 300000 msat, isPublic = false, isEnabled = true))
assert(channels1.last.channelUpdate === channelUpdate_bc)
assert(channels1.last.toUsableBalance === Relayer.UsableBalance(c, channelUpdate_bc.shortChannelId, 400000 msat, 0 msat, isPublic = false))
assert(channels1.last.toChannelBalance === Relayer.ChannelBalance(c, channelUpdate_bc.shortChannelId, 400000 msat, 0 msat, isPublic = false, isEnabled = true))
channelRelayer ! WrappedAvailableBalanceChanged(AvailableBalanceChanged(null, channelId_bc, channelUpdate_bc.shortChannelId, makeCommitments(channelId_bc, 200000 msat, 500000 msat)))
val channels2 = getOutgoingChannels(true)

View file

@ -106,6 +106,10 @@ trait Channel {
}
}
val channelRoutes: Route = open ~ close ~ forceClose ~ channel ~ channels ~ allChannels ~ allUpdates ~ channelStats
val channelBalances: Route = postRequest("channelbalances") { implicit t =>
complete(eclairApi.channelBalances())
}
val channelRoutes: Route = open ~ close ~ forceClose ~ channel ~ channels ~ allChannels ~ allUpdates ~ channelStats ~ channelBalances
}

View file

@ -0,0 +1 @@
[{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortChannelId":"0x0x1","canSend":100000000,"canReceive":20000000,"isPublic":true,"isEnabled":true},{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortChannelId":"0x0x2","canSend":0,"canReceive":30000000,"isPublic":false,"isEnabled":false}]

View file

@ -1 +1 @@
[{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortChannelId":"0x0x1","canSend":100000000,"canReceive":20000000,"isPublic":true},{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortChannelId":"0x0x2","canSend":400000000,"canReceive":30000000,"isPublic":false}]
[{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortChannelId":"0x0x1","canSend":100000000,"canReceive":20000000,"isPublic":true,"isEnabled":true},{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortChannelId":"0x0x2","canSend":400000000,"canReceive":30000000,"isPublic":false,"isEnabled":true}]

View file

@ -42,12 +42,13 @@ import fr.acinq.eclair.io.Peer.PeerInfo
import fr.acinq.eclair.io.{NodeURI, Peer}
import fr.acinq.eclair.message.OnionMessages
import fr.acinq.eclair.payment._
import fr.acinq.eclair.payment.relay.Relayer.UsableBalance
import fr.acinq.eclair.payment.relay.Relayer.ChannelBalance
import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.PreimageReceived
import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentToRouteResponse
import fr.acinq.eclair.router.Router
import fr.acinq.eclair.router.Router.PredefinedNodeRoute
import fr.acinq.eclair.wire.protocol._
import org.json4s.{Formats, Serialization}
import org.mockito.scalatest.IdiomaticMockito
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
@ -62,12 +63,12 @@ import scala.util.Try
class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticMockito with Matchers {
implicit val formats = JsonSupport.formats
implicit val serialization = JsonSupport.serialization
implicit val routeTestTimeout = RouteTestTimeout(3 seconds)
implicit val formats: Formats = JsonSupport.formats
implicit val serialization: Serialization = JsonSupport.serialization
implicit val routeTestTimeout: RouteTestTimeout = RouteTestTimeout(3 seconds)
val aliceNodeId = PublicKey(hex"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0")
val bobNodeId = PublicKey(hex"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585")
val aliceNodeId: PublicKey = PublicKey(hex"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0")
val bobNodeId: PublicKey = PublicKey(hex"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585")
object PluginApi extends RouteProvider {
override def route(directives: EclairDirectives): Route = {
@ -200,11 +201,11 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM
}
}
test("'usablebalances' asks relayer for current usable balances") {
test("'usablebalances' returns expected balance json only for enabled channels") {
val eclair = mock[Eclair]
eclair.usableBalances()(any[Timeout]) returns Future.successful(List(
UsableBalance(aliceNodeId, ShortChannelId(1), 100000000 msat, 20000000 msat, isPublic = true),
UsableBalance(aliceNodeId, ShortChannelId(2), 400000000 msat, 30000000 msat, isPublic = false)
ChannelBalance(aliceNodeId, ShortChannelId(1), 100000000 msat, 20000000 msat, isPublic = true, isEnabled = true),
ChannelBalance(aliceNodeId, ShortChannelId(2), 400000000 msat, 30000000 msat, isPublic = false, isEnabled = true)
))
val mockService = mockApi(eclair)
@ -220,6 +221,26 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM
}
}
test("'channelbalances' returns expected balance json for all channels") {
val eclair = mock[Eclair]
eclair.channelBalances()(any[Timeout]) returns Future.successful(List(
ChannelBalance(aliceNodeId, ShortChannelId(1), 100000000 msat, 20000000 msat, isPublic = true, isEnabled = true),
ChannelBalance(aliceNodeId, ShortChannelId(2), 0 msat, 30000000 msat, isPublic = false, isEnabled = false)
))
val mockService = mockApi(eclair)
Post("/channelbalances") ~>
addCredentials(BasicHttpCredentials("", mockApi().password)) ~>
Route.seal(mockService.channelBalances) ~>
check {
assert(handled)
assert(status == OK)
val response = entityAs[String]
eclair.channelBalances()(any[Timeout]).wasCalled(once)
matchTestJson("channelbalances", response)
}
}
test("'getinfo' response should include this node ID") {
val eclair = mock[Eclair]
val mockService = new MockService(eclair)