Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pagination for channelstats RPC #2890

Merged
merged 3 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/release-notes/eclair-vnext.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Eclair will not allow remote peers to open new obsolete channels that do not sup

### API changes

<insert changes>
- `channelstats` now takes optional parameters `--count` and `--skip` to control pagination. By default, it will return first 10 entries. (#2890)

### Miscellaneous improvements and bug fixes

Expand Down
6 changes: 3 additions & 3 deletions eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ trait Eclair {

def networkFees(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[NetworkFee]]

def channelStats(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[Stats]]
def channelStats(from: TimestampSecond, to: TimestampSecond, paginated_opt: Option[Paginated])(implicit timeout: Timeout): Future[Seq[Stats]]

def getInvoice(paymentHash: ByteVector32)(implicit timeout: Timeout): Future[Option[Invoice]]

Expand Down Expand Up @@ -525,8 +525,8 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
Future(appKit.nodeParams.db.audit.listNetworkFees(from.toTimestampMilli, to.toTimestampMilli))
}

override def channelStats(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[Stats]] = {
Future(appKit.nodeParams.db.audit.stats(from.toTimestampMilli, to.toTimestampMilli))
override def channelStats(from: TimestampSecond, to: TimestampSecond, paginated_opt: Option[Paginated])(implicit timeout: Timeout): Future[Seq[Stats]] = {
Future(appKit.nodeParams.db.audit.stats(from.toTimestampMilli, to.toTimestampMilli, paginated_opt))
}

override def allInvoices(from: TimestampSecond, to: TimestampSecond, paginated_opt: Option[Paginated])(implicit timeout: Timeout): Future[Seq[Invoice]] = Future {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ trait AuditDb {

def listNetworkFees(from: TimestampMilli, to: TimestampMilli): Seq[NetworkFee]

def stats(from: TimestampMilli, to: TimestampMilli): Seq[Stats]
def stats(from: TimestampMilli, to: TimestampMilli, paginated_opt: Option[Paginated] = None): Seq[Stats]

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,9 +201,9 @@ case class DualAuditDb(primary: AuditDb, secondary: AuditDb) extends AuditDb {
primary.listNetworkFees(from, to)
}

override def stats(from: TimestampMilli, to: TimestampMilli): Seq[AuditDb.Stats] = {
runAsync(secondary.stats(from, to))
primary.stats(from, to)
override def stats(from: TimestampMilli, to: TimestampMilli, paginated_opt: Option[Paginated]): Seq[AuditDb.Stats] = {
runAsync(secondary.stats(from, to, paginated_opt))
primary.stats(from, to, paginated_opt)
}
}

Expand Down
10 changes: 7 additions & 3 deletions eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala
Original file line number Diff line number Diff line change
Expand Up @@ -479,7 +479,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging {
}
}

override def stats(from: TimestampMilli, to: TimestampMilli): Seq[Stats] = {
override def stats(from: TimestampMilli, to: TimestampMilli, paginated_opt: Option[Paginated]): Seq[Stats] = {
val networkFees = listNetworkFees(from, to).foldLeft(Map.empty[ByteVector32, Satoshi]) { (feeByChannelId, f) =>
feeByChannelId + (f.channelId -> (feeByChannelId.getOrElse(f.channelId, 0 sat) + f.fee))
}
Expand All @@ -505,7 +505,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging {
}
// Channels opened by our peers won't have any network fees paid by us, but we still want to compute stats for them.
val allChannels = networkFees.keySet ++ relayed.keySet
allChannels.toSeq.flatMap(channelId => {
val result = allChannels.toSeq.flatMap(channelId => {
val networkFee = networkFees.getOrElse(channelId, 0 sat)
val (in, out) = relayed.getOrElse(channelId, Nil).partition(_.direction == "IN")
((in, "IN") :: (out, "OUT") :: Nil).map { case (r, direction) =>
Expand All @@ -518,6 +518,10 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging {
Stats(channelId, direction, avgPaymentAmount.truncateToSatoshi, paymentCount, relayFee.truncateToSatoshi, networkFee)
}
}
})
}).sortBy(s => s.channelId.toHex + s.direction)
paginated_opt match {
case Some(paginated) => result.slice(paginated.skip, paginated.skip + paginated.count)
case None => result
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ class SqliteAuditDb(val sqlite: Connection) extends AuditDb with Logging {
}.toSeq
}

override def stats(from: TimestampMilli, to: TimestampMilli): Seq[Stats] = {
override def stats(from: TimestampMilli, to: TimestampMilli, paginated_opt: Option[Paginated]): Seq[Stats] = {
val networkFees = listNetworkFees(from, to).foldLeft(Map.empty[ByteVector32, Satoshi]) { (feeByChannelId, f) =>
feeByChannelId + (f.channelId -> (feeByChannelId.getOrElse(f.channelId, 0 sat) + f.fee))
}
Expand All @@ -474,7 +474,7 @@ class SqliteAuditDb(val sqlite: Connection) extends AuditDb with Logging {
}
// Channels opened by our peers won't have any network fees paid by us, but we still want to compute stats for them.
val allChannels = networkFees.keySet ++ relayed.keySet
allChannels.toSeq.flatMap(channelId => {
val result = allChannels.toSeq.flatMap(channelId => {
val networkFee = networkFees.getOrElse(channelId, 0 sat)
val (in, out) = relayed.getOrElse(channelId, Nil).partition(_.direction == "IN")
((in, "IN") :: (out, "OUT") :: Nil).map { case (r, direction) =>
Expand All @@ -487,6 +487,11 @@ class SqliteAuditDb(val sqlite: Connection) extends AuditDb with Logging {
Stats(channelId, direction, avgPaymentAmount.truncateToSatoshi, paymentCount, relayFee.truncateToSatoshi, networkFee)
}
}
})
}).sortBy(s => s.channelId.toHex + s.direction)
paginated_opt match {
case Some(paginated) => result.slice(paginated.skip, paginated.skip + paginated.count)
case None => result
}

}
}
18 changes: 11 additions & 7 deletions eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,12 @@ class AuditDbSpec extends AnyFunSuite {
val n3 = randomKey().publicKey
val n4 = randomKey().publicKey

val c1 = randomBytes32()
val c2 = randomBytes32()
val c3 = randomBytes32()
val c4 = randomBytes32()
val c5 = randomBytes32()
val c6 = randomBytes32()
val c1 = ByteVector32.One
val c2 = c1.copy(bytes = 0x02b +: c1.tail)
val c3 = c1.copy(bytes = 0x03b +: c1.tail)
val c4 = c1.copy(bytes = 0x04b +: c1.tail)
val c5 = c1.copy(bytes = 0x05b +: c1.tail)
val c6 = c1.copy(bytes = 0x06b +: c1.tail)

db.add(ChannelPaymentRelayed(46000 msat, 44000 msat, randomBytes32(), c6, c1, 1000 unixms, 1001 unixms))
db.add(ChannelPaymentRelayed(41000 msat, 40000 msat, randomBytes32(), c6, c1, 1002 unixms, 1003 unixms))
Expand Down Expand Up @@ -174,7 +174,7 @@ class AuditDbSpec extends AnyFunSuite {
assert(db.listPublished(c4).map(_.desc) == Seq("funding", "funding"))

// NB: we only count a relay fee for the outgoing channel, no the incoming one.
assert(db.stats(0 unixms, TimestampMilli.now() + 1.milli).toSet == Set(
assert(db.stats(0 unixms, TimestampMilli.now() + 1.milli) == Seq(
Stats(channelId = c1, direction = "IN", avgPaymentAmount = 0 sat, paymentCount = 0, relayFee = 0 sat, networkFee = 0 sat),
Stats(channelId = c1, direction = "OUT", avgPaymentAmount = 42 sat, paymentCount = 3, relayFee = 4 sat, networkFee = 0 sat),
Stats(channelId = c2, direction = "IN", avgPaymentAmount = 0 sat, paymentCount = 0, relayFee = 0 sat, networkFee = 500 sat),
Expand All @@ -188,6 +188,10 @@ class AuditDbSpec extends AnyFunSuite {
Stats(channelId = c6, direction = "IN", avgPaymentAmount = 39 sat, paymentCount = 4, relayFee = 0 sat, networkFee = 0 sat),
Stats(channelId = c6, direction = "OUT", avgPaymentAmount = 40 sat, paymentCount = 1, relayFee = 5 sat, networkFee = 0 sat),
))
assert(db.stats(0 unixms, TimestampMilli.now() + 1.milli, Some(Paginated(2, 3))) == Seq(
Stats(channelId = c2, direction = "OUT", avgPaymentAmount = 28 sat, paymentCount = 2, relayFee = 4 sat, networkFee = 500 sat),
Stats(channelId = c3, direction = "IN", avgPaymentAmount = 0 sat, paymentCount = 0, relayFee = 0 sat, networkFee = 400 sat),
))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,10 @@ trait Channel {
}

val channelStats: Route = postRequest("channelstats") { implicit t =>
formFields(fromFormParam(), toFormParam()) { (from, to) =>
complete(eclairApi.channelStats(from, to))
withPaginated { paginated_opt =>
formFields(fromFormParam(), toFormParam()) { (from, to) =>
complete(eclairApi.channelStats(from, to, paginated_opt.orElse(Some(Paginated(count = 10, skip = 0)))))
}
}
}

Expand Down
Loading