mirror of
https://github.com/ACINQ/eclair.git
synced 2025-02-23 06:35:11 +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:
parent
2872d876d0
commit
9358e5e1f5
11 changed files with 71 additions and 31 deletions
|
@ -8,7 +8,7 @@
|
|||
|
||||
### API changes
|
||||
|
||||
<insert changes>
|
||||
- `channelbalances` Retrieves information about the balances of all local channels. (#2196)
|
||||
|
||||
### Miscellaneous improvements and bug fixes
|
||||
|
||||
|
|
|
@ -44,6 +44,7 @@ and COMMAND is one of the available commands:
|
|||
- allchannels
|
||||
- allupdates
|
||||
- channelstats
|
||||
- channelbalances
|
||||
|
||||
=== Fees ===
|
||||
- networkfees
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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])
|
||||
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
1
eclair-node/src/test/resources/api/channelbalances
Normal file
1
eclair-node/src/test/resources/api/channelbalances
Normal 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}]
|
|
@ -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}]
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Reference in a new issue