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

Add pagination on listInvoices #2474

Merged
merged 3 commits into from
Nov 14, 2022
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
1 change: 1 addition & 0 deletions docs/release-notes/eclair-vnext.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ upgrading to this release.
- `channelbalances` and `usablebalances` return a `shortIds` object instead of a single `shortChannelId` (#2323)
- `stop` stops eclair: please note that the recommended way of stopping eclair is simply to kill its process (#2233)
- `rbfopen` lets the initiator of a dual-funded channel RBF the funding transaction (#2275)
- `listinvoices` now accepts `--count` and `--skip` parameters to limit the number of retrieved items (#2474)

### 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 @@ -137,7 +137,7 @@ trait Eclair {

def pendingInvoices(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[Invoice]]

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

def deleteInvoice(paymentHash: ByteVector32): Future[String]

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

override def allInvoices(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[Invoice]] = Future {
appKit.nodeParams.db.payments.listIncomingPayments(from.toTimestampMilli, to.toTimestampMilli).map(_.invoice)
override def allInvoices(from: TimestampSecond, to: TimestampSecond, paginated_opt: Option[Paginated])(implicit timeout: Timeout): Future[Seq[Invoice]] = Future {
appKit.nodeParams.db.payments.listIncomingPayments(from.toTimestampMilli, to.toTimestampMilli, paginated_opt).map(_.invoice)
}

override def pendingInvoices(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[Invoice]] = Future {
Expand Down
27 changes: 27 additions & 0 deletions eclair-core/src/main/scala/fr/acinq/eclair/Paginated.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2022 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package fr.acinq.eclair

/**
* Created by rorp on 03/11/2022.
*
* Simple class for pagination database and API queries.
*/
case class Paginated(count: Int, skip: Int) {
require(count >= 0, "count must be a positive number")
require(skip >= 0, "skip must be a positive number")
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import fr.acinq.eclair.payment._
import fr.acinq.eclair.payment.relay.Relayer.RelayFees
import fr.acinq.eclair.router.Router
import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate, NodeAddress, NodeAnnouncement}
import fr.acinq.eclair.{CltvExpiry, MilliSatoshi, RealShortChannelId, ShortChannelId, TimestampMilli}
import fr.acinq.eclair.{CltvExpiry, MilliSatoshi, Paginated, RealShortChannelId, ShortChannelId, TimestampMilli}
import grizzled.slf4j.Logging
import scodec.bits.ByteVector

Expand Down Expand Up @@ -306,9 +306,9 @@ case class DualPaymentsDb(primary: PaymentsDb, secondary: PaymentsDb) extends Pa
primary.removeIncomingPayment(paymentHash)
}

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

override def listPendingIncomingPayments(from: TimestampMilli, to: TimestampMilli): Seq[IncomingPayment] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import fr.acinq.bitcoin.scalacompat.ByteVector32
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.eclair.payment._
import fr.acinq.eclair.router.Router.{ChannelHop, Hop, NodeHop}
import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, TimestampMilli}
import fr.acinq.eclair.{MilliSatoshi, Paginated, ShortChannelId, TimestampMilli}
import scodec.bits.ByteVector

import java.util.UUID
Expand Down Expand Up @@ -52,7 +52,7 @@ trait IncomingPaymentsDb {
def removeIncomingPayment(paymentHash: ByteVector32): Try[Unit]

/** List all incoming payments (pending, expired and succeeded) in the given time range (milli-seconds). */
def listIncomingPayments(from: TimestampMilli, to: TimestampMilli): Seq[IncomingPayment]
def listIncomingPayments(from: TimestampMilli, to: TimestampMilli, paginated_opt: Option[Paginated]): Seq[IncomingPayment]

/** List all pending (not paid, not expired) incoming payments in the given time range (milli-seconds). */
def listPendingIncomingPayments(from: TimestampMilli, to: TimestampMilli): Seq[IncomingPayment]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
package fr.acinq.eclair.db.jdbc

import fr.acinq.bitcoin.scalacompat.ByteVector32
import fr.acinq.eclair.MilliSatoshi
import fr.acinq.eclair.{MilliSatoshi, Paginated}
import grizzled.slf4j.Logger
import org.sqlite.SQLiteConnection
import scodec.Decoder
Expand Down Expand Up @@ -214,6 +214,11 @@ trait JdbcUtils {
implicit def conv(rs: ResultSet): ExtendedResultSet = ExtendedResultSet(rs)
}

def limited(sql: String, paginated_opt: Option[Paginated]): String = paginated_opt match {
case Some(paginated) => s"$sql LIMIT ${paginated.count} OFFSET ${paginated.skip}"
case None => sql
}

}

object JdbcUtils extends JdbcUtils
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import fr.acinq.eclair.db.PaymentsDb._
import fr.acinq.eclair.db._
import fr.acinq.eclair.db.pg.PgUtils.PgLock
import fr.acinq.eclair.payment._
import fr.acinq.eclair.{MilliSatoshi, TimestampMilli, TimestampMilliLong}
import fr.acinq.eclair.{MilliSatoshi, Paginated, TimestampMilli, TimestampMilliLong}
import grizzled.slf4j.Logging
import scodec.bits.{BitVector, ByteVector}

Expand Down Expand Up @@ -326,9 +326,9 @@ class PgPaymentsDb(implicit ds: DataSource, lock: PgLock) extends PaymentsDb wit
}
}

override def listIncomingPayments(from: TimestampMilli, to: TimestampMilli): Seq[IncomingPayment] = withMetrics("payments/list-incoming", DbBackends.Postgres) {
override def listIncomingPayments(from: TimestampMilli, to: TimestampMilli, paginated_opt: Option[Paginated]): Seq[IncomingPayment] = withMetrics("payments/list-incoming", DbBackends.Postgres) {
withLock { pg =>
using(pg.prepareStatement("SELECT * FROM payments.received WHERE created_at > ? AND created_at < ? ORDER BY created_at")) { statement =>
using(pg.prepareStatement(limited("SELECT * FROM payments.received WHERE created_at > ? AND created_at < ? ORDER BY created_at", paginated_opt))) { statement =>
statement.setTimestamp(1, from.toSqlTimestamp)
statement.setTimestamp(2, to.toSqlTimestamp)
statement.executeQuery().flatMap(parseIncomingPayment).toSeq
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import fr.acinq.eclair.db.PaymentsDb._
import fr.acinq.eclair.db._
import fr.acinq.eclair.db.sqlite.SqliteUtils._
import fr.acinq.eclair.payment._
import fr.acinq.eclair.{MilliSatoshi, TimestampMilli, TimestampMilliLong}
import fr.acinq.eclair.{MilliSatoshi, Paginated, TimestampMilli, TimestampMilliLong}
import grizzled.slf4j.Logging
import scodec.bits.{BitVector, ByteVector}

Expand Down Expand Up @@ -328,8 +328,8 @@ class SqlitePaymentsDb(val sqlite: Connection) extends PaymentsDb with Logging {
}
}

override def listIncomingPayments(from: TimestampMilli, to: TimestampMilli): Seq[IncomingPayment] = withMetrics("payments/list-incoming", DbBackends.Sqlite) {
using(sqlite.prepareStatement("SELECT * FROM received_payments WHERE created_at > ? AND created_at < ? ORDER BY created_at")) { statement =>
override def listIncomingPayments(from: TimestampMilli, to: TimestampMilli, paginated_opt: Option[Paginated]): Seq[IncomingPayment] = withMetrics("payments/list-incoming", DbBackends.Sqlite) {
using(sqlite.prepareStatement(limited("SELECT * FROM received_payments WHERE created_at > ? AND created_at < ? ORDER BY created_at", paginated_opt))) { statement =>
statement.setLong(1, from.toLong)
statement.setLong(2, to.toLong)
statement.executeQuery().flatMap(parseIncomingPayment).toSeq
Expand Down
23 changes: 14 additions & 9 deletions eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import fr.acinq.eclair.payment._
import fr.acinq.eclair.router.Router.{ChannelHop, ChannelRelayParams, NodeHop}
import fr.acinq.eclair.wire.protocol.OfferTypes._
import fr.acinq.eclair.wire.protocol.{ChannelUpdate, TlvStream, UnknownNextPeer}
import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshiLong, ShortChannelId, TimestampMilli, TimestampMilliLong, TimestampSecond, TimestampSecondLong, randomBytes32, randomBytes64, randomKey}
import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshiLong, Paginated, ShortChannelId, TimestampMilli, TimestampMilliLong, TimestampSecond, TimestampSecondLong, randomBytes32, randomBytes64, randomKey}
import org.scalatest.funsuite.AnyFunSuite
import scodec.bits.HexStringSyntax

Expand Down Expand Up @@ -88,7 +88,7 @@ class PaymentsDbSpec extends AnyFunSuite {
db.addIncomingPayment(i1, preimage1)
db.receiveIncomingPayment(i1.paymentHash, 550 msat, 1100 unixms)

assert(db.listIncomingPayments(1 unixms, 1500 unixms) == Seq(pr1))
assert(db.listIncomingPayments(1 unixms, 1500 unixms, None) == Seq(pr1))
assert(db.listOutgoingPayments(1 unixms, 1500 unixms) == Seq(ps1))
}
)
Expand Down Expand Up @@ -204,7 +204,7 @@ class PaymentsDbSpec extends AnyFunSuite {
db.updateOutgoingPayment(PaymentFailed(ps6.id, ps6.paymentHash, Nil, 1300 unixms))

assert(db.listOutgoingPayments(1 unixms, 2000 unixms) == Seq(ps1, ps2, ps3, ps4, ps5, ps6))
assert(db.listIncomingPayments(1 unixms, TimestampMilli.now()) == Seq(pr1, pr2, pr3))
assert(db.listIncomingPayments(1 unixms, TimestampMilli.now(), None) == Seq(pr1, pr2, pr3))
assert(db.listExpiredIncomingPayments(1 unixms, 2000 unixms) == Seq(pr2))
})
}
Expand Down Expand Up @@ -436,9 +436,9 @@ class PaymentsDbSpec extends AnyFunSuite {

assert(db.getIncomingPayment(i1.paymentHash).contains(pr1))
assert(db.getIncomingPayment(i2.paymentHash).contains(pr2))
assert(db.listIncomingPayments(TimestampMilli(Instant.parse("2020-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2100-12-31T23:59:59.00Z").toEpochMilli)) == Seq(pr2, pr1))
assert(db.listIncomingPayments(TimestampMilli(Instant.parse("2020-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2020-12-31T23:59:59.00Z").toEpochMilli)) == Seq(pr2))
assert(db.listIncomingPayments(TimestampMilli(Instant.parse("2010-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2011-12-31T23:59:59.00Z").toEpochMilli)) == Seq.empty)
assert(db.listIncomingPayments(TimestampMilli(Instant.parse("2020-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2100-12-31T23:59:59.00Z").toEpochMilli), None) == Seq(pr2, pr1))
assert(db.listIncomingPayments(TimestampMilli(Instant.parse("2020-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2020-12-31T23:59:59.00Z").toEpochMilli), None) == Seq(pr2))
assert(db.listIncomingPayments(TimestampMilli(Instant.parse("2010-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2011-12-31T23:59:59.00Z").toEpochMilli), None) == Seq.empty)
assert(db.listExpiredIncomingPayments(TimestampMilli(Instant.parse("2020-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2100-12-31T23:59:59.00Z").toEpochMilli)) == Seq(pr2))
assert(db.listExpiredIncomingPayments(TimestampMilli(Instant.parse("2020-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2020-12-31T23:59:59.00Z").toEpochMilli)) == Seq(pr2))
assert(db.listExpiredIncomingPayments(TimestampMilli(Instant.parse("2010-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2011-12-31T23:59:59.00Z").toEpochMilli)) == Seq.empty)
Expand Down Expand Up @@ -556,7 +556,7 @@ class PaymentsDbSpec extends AnyFunSuite {
assert(db.getIncomingPayment(paidInvoice3.paymentHash).contains(payment3.copy(status = IncomingPaymentStatus.Pending)))

val now = TimestampMilli.now()
assert(db.listIncomingPayments(0 unixms, now) == Seq(expiredPayment1, expiredPayment2, expiredPayment3, pendingPayment1, pendingPayment2, pendingPayment3, payment1.copy(status = IncomingPaymentStatus.Pending), payment2.copy(status = IncomingPaymentStatus.Pending), payment3.copy(status = IncomingPaymentStatus.Pending)))
assert(db.listIncomingPayments(0 unixms, now, None) == Seq(expiredPayment1, expiredPayment2, expiredPayment3, pendingPayment1, pendingPayment2, pendingPayment3, payment1.copy(status = IncomingPaymentStatus.Pending), payment2.copy(status = IncomingPaymentStatus.Pending), payment3.copy(status = IncomingPaymentStatus.Pending)))
assert(db.listExpiredIncomingPayments(0 unixms, now) == Seq(expiredPayment1, expiredPayment2, expiredPayment3))
assert(db.listReceivedIncomingPayments(0 unixms, now) == Nil)
assert(db.listPendingIncomingPayments(0 unixms, now) == Seq(pendingPayment1, pendingPayment2, pendingPayment3, payment1.copy(status = IncomingPaymentStatus.Pending), payment2.copy(status = IncomingPaymentStatus.Pending), payment3.copy(status = IncomingPaymentStatus.Pending)))
Expand All @@ -570,11 +570,16 @@ class PaymentsDbSpec extends AnyFunSuite {
assert(db.getIncomingPayment(paidInvoice1.paymentHash).contains(payment1))
assert(db.getIncomingPayment(paidInvoice3.paymentHash).contains(payment3))

assert(db.listIncomingPayments(0 unixms, now) == Seq(expiredPayment1, expiredPayment2, expiredPayment3, pendingPayment1, pendingPayment2, pendingPayment3, payment1, payment2, payment3))
assert(db.listIncomingPayments(now - 60.seconds, now) == Seq(pendingPayment1, pendingPayment2, pendingPayment3, payment1, payment2, payment3))
assert(db.listIncomingPayments(0 unixms, now, None) == Seq(expiredPayment1, expiredPayment2, expiredPayment3, pendingPayment1, pendingPayment2, pendingPayment3, payment1, payment2, payment3))
assert(db.listIncomingPayments(now - 60.seconds, now, None) == Seq(pendingPayment1, pendingPayment2, pendingPayment3, payment1, payment2, payment3))
assert(db.listIncomingPayments(0 unixms, now, Some(Paginated(0,0))) == Seq())
assert(db.listIncomingPayments(0 unixms, now, Some(Paginated(0,3))) == Seq())
assert(db.listIncomingPayments(0 unixms, now, Some(Paginated(3,0))) == Seq(expiredPayment1, expiredPayment2, expiredPayment3))
assert(db.listIncomingPayments(0 unixms, now, Some(Paginated(3,3))) == Seq(pendingPayment1, pendingPayment2, pendingPayment3))
assert(db.listPendingIncomingPayments(0 unixms, now) == Seq(pendingPayment1, pendingPayment2, pendingPayment3))
assert(db.listReceivedIncomingPayments(0 unixms, now) == Seq(payment1, payment2, payment3))


assert(db.removeIncomingPayment(paidInvoice1.paymentHash).isFailure)
db.removeIncomingPayment(paidInvoice1.paymentHash).failed.foreach(e => assert(e.getMessage == "Cannot remove a received incoming payment"))
assert(db.removeIncomingPayment(paidInvoice3.paymentHash).isFailure)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ class InvoicePurgerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("ap
})

val now = TimestampMilli.now()
assert(db.listIncomingPayments(0 unixms, now) == expiredPayments ++ pendingPayments ++ paidPayments)
assert(db.listIncomingPayments(now - 100.days, now) == pendingPayments ++ paidPayments)
assert(db.listIncomingPayments(0 unixms, now, None) == expiredPayments ++ pendingPayments ++ paidPayments)
assert(db.listIncomingPayments(now - 100.days, now, None) == pendingPayments ++ paidPayments)
assert(db.listPendingIncomingPayments(0 unixms, now) == pendingPayments)
assert(db.listReceivedIncomingPayments(0 unixms, now) == paidPayments)
assert(db.listExpiredIncomingPayments(0 unixms, now) == expiredPayments)
Expand All @@ -75,7 +75,7 @@ class InvoicePurgerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("ap
probe.expectMessage(5 seconds, PurgeCompleted)
probe.expectNoMessage()
assert(db.listExpiredIncomingPayments(0 unixms, now).isEmpty)
assert(db.listIncomingPayments(0 unixms, now) == pendingPayments ++ paidPayments)
assert(db.listIncomingPayments(0 unixms, now, None) == pendingPayments ++ paidPayments)

testKit.stop(purger)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import fr.acinq.eclair.ApiTypes.ChannelIdentifier
import fr.acinq.eclair.api.serde.FormParamExtractors._
import fr.acinq.eclair.api.serde.JsonSupport._
import fr.acinq.eclair.payment.Bolt11Invoice
import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, TimestampSecond}
import fr.acinq.eclair.{MilliSatoshi, Paginated, ShortChannelId, TimestampSecond}

import scala.concurrent.Future
import scala.concurrent.duration.DurationInt
Expand All @@ -52,6 +52,8 @@ trait ExtraDirectives extends Directives {
val ignoreNodeIdsFormParam: NameUnmarshallerReceptacle[List[PublicKey]] = "ignoreNodeIds".as[List[PublicKey]](pubkeyListUnmarshaller)
val ignoreShortChannelIdsFormParam: NameUnmarshallerReceptacle[List[ShortChannelId]] = "ignoreShortChannelIds".as[List[ShortChannelId]](shortChannelIdsUnmarshaller)
val maxFeeMsatFormParam: NameReceptacle[MilliSatoshi] = "maxFeeMsat".as[MilliSatoshi]
val countFormParam: NameReceptacle[Int] = "count".as[Int]
val skipFormParam: NameReceptacle[Int] = "skip".as[Int]

// custom directive to fail with HTTP 404 (and JSON response) if the element was not found
def completeOrNotFound[T](fut: Future[Option[T]])(implicit marshaller: ToResponseMarshaller[T]): Route = onComplete(fut) {
Expand All @@ -61,6 +63,13 @@ trait ExtraDirectives extends Directives {
case Failure(_) => reject
}

def withPaginated: Directive1[Option[Paginated]] = formFields(countFormParam.?, skipFormParam.?).tflatMap {
case (Some(count), Some(skip)) => provide(Some(Paginated(count = count, skip = skip)))
case (Some(count), None) => provide(Some(Paginated(count = count, skip = 0)))
case (None, Some(_)) => reject(MalformedFormFieldRejection("skip", "Must specify count"))
case (None, None) => provide(None)
}

def withChannelIdentifier: Directive1[ChannelIdentifier] = formFields(channelIdFormParam.?, shortChannelIdFormParam.?).tflatMap {
case (Some(channelId), None) => provide(Left(channelId))
case (None, Some(shortChannelId)) => provide(Right(shortChannelId))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,10 @@ trait Invoice {
}

val listInvoices: Route = postRequest("listinvoices") { implicit t =>
formFields(fromFormParam, toFormParam) { (from, to) =>
complete(eclairApi.allInvoices(from, to))
withPaginated { paginated_opt =>
formFields(fromFormParam, toFormParam) { (from, to) =>
complete(eclairApi.allInvoices(from, to, paginated_opt))
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,12 @@ trait OnChain {
}

val onChainTransactions: Route = postRequest("onchaintransactions") { implicit t =>
formFields("count".as[Int].?, "skip".as[Int].?) { (count_opt, skip_opt) =>
complete(eclairApi.onChainTransactions(count_opt.getOrElse(10), skip_opt.getOrElse(0)))
withPaginated { paginated_opt =>
formFields(countFormParam, skipFormParam) { (count_opt, skip_opt) =>
val count = paginated_opt.map(_.count).getOrElse(10)
val skip = paginated_opt.map(_.skip).getOrElse(0)
complete(eclairApi.onChainTransactions(count, skip))
}
}
}

Expand Down