diff --git a/contrib/eclair-cli.bash-completion b/contrib/eclair-cli.bash-completion index b6bb784e5..47b76ecd7 100644 --- a/contrib/eclair-cli.bash-completion +++ b/contrib/eclair-cli.bash-completion @@ -21,7 +21,7 @@ _eclair-cli() *) # works fine, but is too slow at the moment. # allopts=$($eclaircli help 2>&1 | awk '$1 ~ /^"/ { sub(/,/, ""); print $1}' | sed 's/[":]//g') - allopts="getinfo connect open close forceclose updaterelayfee peers channels channel allnodes allchannels allupdates findroute findroutetonode findroutebetweennodes parseinvoice payinvoice sendtonode getsentinfo createinvoice getinvoice listinvoices listpendinginvoices getreceivedinfo audit networkfees channelstats" + allopts="getinfo connect open close forceclose updaterelayfee peers channels channel allnodes allchannels allupdates findroute findroutetonode findroutebetweennodes parseinvoice payinvoice sendtonode getsentinfo createinvoice getinvoice listinvoices listpendinginvoices listreceivedpayments getreceivedinfo audit networkfees channelstats" if ! [[ " $allopts " =~ " $prev " ]]; then # prevent double arguments if [[ -z "$cur" || "$cur" =~ ^[a-z] ]]; then diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index 3a25a4945..d6e2172fd 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -38,6 +38,7 @@ All this data is signed and encrypted so that it can not be read or forged by th - `channel-created` is a new websocket event that is published when a channel's funding transaction has been broadcast (#2567) - `channel-opened` websocket event was updated to contain the final `channel_id` and be published when a channel is ready to process payments (#2567) - `getsentinfo` can now be used with `--offer` to list payments sent to a specific offer. +- `listreceivedpayments` lists payments received by your node (#2607) ### Miscellaneous improvements and bug fixes 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 447b55ead..40f76f30c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -113,6 +113,8 @@ trait Eclair { def receivedInfo(paymentHash: ByteVector32)(implicit timeout: Timeout): Future[Option[IncomingPayment]] + def receivedPayments(from: TimestampSecond, to: TimestampSecond, paginated_opt: Option[Paginated])(implicit timeout: Timeout): Future[Seq[IncomingPayment]] + def send(externalId_opt: Option[String], amount: MilliSatoshi, invoice: Bolt11Invoice, maxAttempts_opt: Option[Int] = None, maxFeeFlat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None, pathFindingExperimentName_opt: Option[String] = None)(implicit timeout: Timeout): Future[UUID] def sendBlocking(externalId_opt: Option[String], amount: MilliSatoshi, invoice: Bolt11Invoice, maxAttempts_opt: Option[Int] = None, maxFeeFlat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None, pathFindingExperimentName_opt: Option[String] = None)(implicit timeout: Timeout): Future[PaymentEvent] @@ -460,6 +462,10 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { appKit.nodeParams.db.payments.listPendingIncomingPayments(from.toTimestampMilli, to.toTimestampMilli, paginated_opt).map(_.invoice) } + override def receivedPayments(from: TimestampSecond, to: TimestampSecond, paginated_opt: Option[Paginated])(implicit timeout: Timeout): Future[Seq[IncomingPayment]] = Future { + appKit.nodeParams.db.payments.listReceivedIncomingPayments(from.toTimestampMilli, to.toTimestampMilli, paginated_opt) + } + override def getInvoice(paymentHash: ByteVector32)(implicit timeout: Timeout): Future[Option[Invoice]] = Future { appKit.nodeParams.db.payments.getIncomingPayment(paymentHash).map(_.invoice) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala index ca018f60a..43d9182aa 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala @@ -314,14 +314,14 @@ case class DualPaymentsDb(primary: PaymentsDb, secondary: PaymentsDb) extends Pa primary.listPendingIncomingPayments(from, to, paginated_opt) } - override def listExpiredIncomingPayments(from: TimestampMilli, to: TimestampMilli): Seq[IncomingPayment] = { - runAsync(secondary.listExpiredIncomingPayments(from, to)) - primary.listExpiredIncomingPayments(from, to) + override def listExpiredIncomingPayments(from: TimestampMilli, to: TimestampMilli, paginated_opt: Option[Paginated]): Seq[IncomingPayment] = { + runAsync(secondary.listExpiredIncomingPayments(from, to, paginated_opt)) + primary.listExpiredIncomingPayments(from, to, paginated_opt) } - override def listReceivedIncomingPayments(from: TimestampMilli, to: TimestampMilli): Seq[IncomingPayment] = { - runAsync(secondary.listReceivedIncomingPayments(from, to)) - primary.listReceivedIncomingPayments(from, to) + override def listReceivedIncomingPayments(from: TimestampMilli, to: TimestampMilli, paginated_opt: Option[Paginated]): Seq[IncomingPayment] = { + runAsync(secondary.listReceivedIncomingPayments(from, to, paginated_opt)) + primary.listReceivedIncomingPayments(from, to, paginated_opt) } override def addOutgoingPayment(outgoingPayment: OutgoingPayment): Unit = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index 88a25ce88..26cd0fa51 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -60,10 +60,10 @@ trait IncomingPaymentsDb { def listPendingIncomingPayments(from: TimestampMilli, to: TimestampMilli, paginated_opt: Option[Paginated]): Seq[IncomingPayment] /** List all expired (not paid) incoming payments in the given time range (milli-seconds). */ - def listExpiredIncomingPayments(from: TimestampMilli, to: TimestampMilli): Seq[IncomingPayment] + def listExpiredIncomingPayments(from: TimestampMilli, to: TimestampMilli, paginated_opt: Option[Paginated]): Seq[IncomingPayment] /** List all received (paid) incoming payments in the given time range (milli-seconds). */ - def listReceivedIncomingPayments(from: TimestampMilli, to: TimestampMilli): Seq[IncomingPayment] + def listReceivedIncomingPayments(from: TimestampMilli, to: TimestampMilli, paginated_opt: Option[Paginated]): Seq[IncomingPayment] } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala index 26570fc0b..d78883a29 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala @@ -364,9 +364,9 @@ class PgPaymentsDb(implicit ds: DataSource, lock: PgLock) extends PaymentsDb wit } } - override def listReceivedIncomingPayments(from: TimestampMilli, to: TimestampMilli): Seq[IncomingPayment] = withMetrics("payments/list-incoming-received", DbBackends.Postgres) { + override def listReceivedIncomingPayments(from: TimestampMilli, to: TimestampMilli, paginated_opt: Option[Paginated]): Seq[IncomingPayment] = withMetrics("payments/list-incoming-received", DbBackends.Postgres) { withLock { pg => - using(pg.prepareStatement("SELECT * FROM payments.received WHERE received_msat > 0 AND created_at > ? AND created_at < ? ORDER BY created_at")) { statement => + using(pg.prepareStatement(limited("SELECT * FROM payments.received WHERE received_msat > 0 AND 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 @@ -385,9 +385,9 @@ class PgPaymentsDb(implicit ds: DataSource, lock: PgLock) extends PaymentsDb wit } } - override def listExpiredIncomingPayments(from: TimestampMilli, to: TimestampMilli): Seq[IncomingPayment] = withMetrics("payments/list-incoming-expired", DbBackends.Postgres) { + override def listExpiredIncomingPayments(from: TimestampMilli, to: TimestampMilli, paginated_opt: Option[Paginated]): Seq[IncomingPayment] = withMetrics("payments/list-incoming-expired", DbBackends.Postgres) { withLock { pg => - using(pg.prepareStatement("SELECT * FROM payments.received WHERE received_msat IS NULL AND created_at > ? AND created_at < ? AND expire_at < ? ORDER BY created_at")) { statement => + using(pg.prepareStatement(limited("SELECT * FROM payments.received WHERE received_msat IS NULL AND created_at > ? AND created_at < ? AND expire_at < ? ORDER BY created_at", paginated_opt))) { statement => statement.setTimestamp(1, from.toSqlTimestamp) statement.setTimestamp(2, to.toSqlTimestamp) statement.setTimestamp(3, Timestamp.from(Instant.now())) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index 2d3dbf832..d08008388 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -367,8 +367,8 @@ class SqlitePaymentsDb(val sqlite: Connection) extends PaymentsDb with Logging { } } - override def listReceivedIncomingPayments(from: TimestampMilli, to: TimestampMilli): Seq[IncomingPayment] = withMetrics("payments/list-incoming-received", DbBackends.Sqlite) { - using(sqlite.prepareStatement("SELECT * FROM received_payments WHERE received_msat > 0 AND created_at > ? AND created_at < ? ORDER BY created_at")) { statement => + override def listReceivedIncomingPayments(from: TimestampMilli, to: TimestampMilli, paginated_opt: Option[Paginated]): Seq[IncomingPayment] = withMetrics("payments/list-incoming-received", DbBackends.Sqlite) { + using(sqlite.prepareStatement(limited("SELECT * FROM received_payments WHERE received_msat > 0 AND 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 @@ -384,8 +384,8 @@ class SqlitePaymentsDb(val sqlite: Connection) extends PaymentsDb with Logging { } } - override def listExpiredIncomingPayments(from: TimestampMilli, to: TimestampMilli): Seq[IncomingPayment] = withMetrics("payments/list-incoming-expired", DbBackends.Sqlite) { - using(sqlite.prepareStatement("SELECT * FROM received_payments WHERE received_msat IS NULL AND created_at > ? AND created_at < ? AND expire_at < ? ORDER BY created_at")) { statement => + override def listExpiredIncomingPayments(from: TimestampMilli, to: TimestampMilli, paginated_opt: Option[Paginated]): Seq[IncomingPayment] = withMetrics("payments/list-incoming-expired", DbBackends.Sqlite) { + using(sqlite.prepareStatement(limited("SELECT * FROM received_payments WHERE received_msat IS NULL AND created_at > ? AND created_at < ? AND expire_at < ? ORDER BY created_at", paginated_opt))) { statement => statement.setLong(1, from.toLong) statement.setLong(2, to.toLong) statement.setLong(3, TimestampMilli.now().toLong) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/InvoicePurger.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/InvoicePurger.scala index 28d96061c..978a9680f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/InvoicePurger.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/InvoicePurger.scala @@ -58,7 +58,7 @@ class InvoicePurger private(paymentsDb: IncomingPaymentsDb, context: ActorContex case TickPurge => val now = TimestampMilli.now() val start = if (fullScan) 0 unixms else now - 15.days - val expiredPayments = paymentsDb.listExpiredIncomingPayments(start, now) + val expiredPayments = paymentsDb.listExpiredIncomingPayments(start, now, None) // purge expired payments expiredPayments.foreach(p => paymentsDb.removeIncomingPayment(p.invoice.paymentHash)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala index 25ae6fc94..8589fbd13 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala @@ -205,7 +205,7 @@ class PaymentsDbSpec extends AnyFunSuite { assert(db.listOutgoingPayments(1 unixms, 2000 unixms) == Seq(ps1, ps2, ps3, ps4, ps5, ps6)) assert(db.listIncomingPayments(1 unixms, TimestampMilli.now(), None) == Seq(pr1, pr2, pr3)) - assert(db.listExpiredIncomingPayments(1 unixms, 2000 unixms) == Seq(pr2)) + assert(db.listExpiredIncomingPayments(1 unixms, 2000 unixms, None) == Seq(pr2)) }) } @@ -509,9 +509,9 @@ class PaymentsDbSpec extends AnyFunSuite { 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) + assert(db.listExpiredIncomingPayments(TimestampMilli(Instant.parse("2020-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2100-12-31T23:59:59.00Z").toEpochMilli), None) == 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), None) == 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), None) == Seq.empty) assert(db.listOutgoingPayments(TimestampMilli(Instant.parse("2020-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2021-12-31T23:59:59.00Z").toEpochMilli)) == Seq(ps2, ps1, ps3)) assert(db.listOutgoingPayments(TimestampMilli(Instant.parse("2010-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2021-01-15T23:59:59.00Z").toEpochMilli)) == Seq(ps2, ps1)) @@ -690,8 +690,8 @@ class PaymentsDbSpec extends AnyFunSuite { val now = TimestampMilli.now() assert(db.listIncomingPayments(0 unixms, now, None) == Seq(expiredPayment1, expiredPayment2, pendingPayment1, pendingPayment2, payment1.copy(status = IncomingPaymentStatus.Pending), payment2.copy(status = IncomingPaymentStatus.Pending), payment3)) - assert(db.listExpiredIncomingPayments(0 unixms, now) == Seq(expiredPayment1, expiredPayment2)) - assert(db.listReceivedIncomingPayments(0 unixms, now) == Seq(payment3)) + assert(db.listExpiredIncomingPayments(0 unixms, now, None) == Seq(expiredPayment1, expiredPayment2)) + assert(db.listReceivedIncomingPayments(0 unixms, now, None) == Seq(payment3)) assert(db.listPendingIncomingPayments(0 unixms, now, None) == Seq(pendingPayment1, pendingPayment2, payment1.copy(status = IncomingPaymentStatus.Pending), payment2.copy(status = IncomingPaymentStatus.Pending))) db.receiveIncomingPayment(paidInvoice1.paymentHash, 461 msat, receivedAt1) @@ -712,7 +712,7 @@ class PaymentsDbSpec extends AnyFunSuite { assert(db.listIncomingPayments(0 unixms, now, Some(Paginated(2, 2))) == Seq(pendingPayment1, pendingPayment2)) assert(db.listPendingIncomingPayments(0 unixms, now, None) == Seq(pendingPayment1, pendingPayment2)) assert(db.listPendingIncomingPayments(0 unixms, now, Some(Paginated(1, 1))) == Seq(pendingPayment2)) - assert(db.listReceivedIncomingPayments(0 unixms, now) == Seq(payment1, payment2, payment4)) + assert(db.listReceivedIncomingPayments(0 unixms, now, None) == Seq(payment1, payment2, payment4)) assert(db.removeIncomingPayment(paidInvoice1.paymentHash).isFailure) db.removeIncomingPayment(paidInvoice1.paymentHash).failed.foreach(e => assert(e.getMessage == "Cannot remove a received incoming payment")) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/receive/InvoicePurgerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/receive/InvoicePurgerSpec.scala index 0093fb5bd..e019b0bb0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/receive/InvoicePurgerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/receive/InvoicePurgerSpec.scala @@ -63,8 +63,8 @@ class InvoicePurgerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("ap 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, None) == pendingPayments) - assert(db.listReceivedIncomingPayments(0 unixms, now) == paidPayments) - assert(db.listExpiredIncomingPayments(0 unixms, now) == expiredPayments) + assert(db.listReceivedIncomingPayments(0 unixms, now, None) == paidPayments) + assert(db.listExpiredIncomingPayments(0 unixms, now, None) == expiredPayments) val probe = testKit.createTestProbe[PurgeEvent]() system.eventStream ! EventStream.Subscribe(probe.ref) @@ -74,7 +74,7 @@ class InvoicePurgerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("ap // check that purge runs before the default first interval of 24 hours probe.expectMessage(5 seconds, PurgeCompleted) probe.expectNoMessage() - assert(db.listExpiredIncomingPayments(0 unixms, now).isEmpty) + assert(db.listExpiredIncomingPayments(0 unixms, now, None).isEmpty) assert(db.listIncomingPayments(0 unixms, now, None) == pendingPayments ++ paidPayments) testKit.stop(purger) @@ -105,7 +105,7 @@ class InvoicePurgerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("ap // check that the initial purge scanned the entire database probe.expectMessage(10 seconds, PurgeCompleted) probe.expectNoMessage() - assert(db.listExpiredIncomingPayments(0 unixms, TimestampMilli.now()).isEmpty) + assert(db.listExpiredIncomingPayments(0 unixms, TimestampMilli.now(), None).isEmpty) // add an expired invoice from before the 15 days look back period val expiredInvoice3 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(100 msat), randomBytes32(), alicePriv, Left("expired invoice3"), CltvExpiryDelta(18), @@ -122,7 +122,7 @@ class InvoicePurgerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("ap // check that subsequent purge runs do not go back > 15 days probe.expectMessage(10 seconds, PurgeCompleted) probe.expectNoMessage() - assert(db.listExpiredIncomingPayments(0 unixms, TimestampMilli.now()) == Seq(expiredPayment3)) + assert(db.listExpiredIncomingPayments(0 unixms, TimestampMilli.now(), None) == Seq(expiredPayment3)) testKit.stop(purger) } diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Payment.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Payment.scala index ea920a53c..c55a368b9 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Payment.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Payment.scala @@ -95,6 +95,14 @@ trait Payment { } } + val listReceivedPayments: Route = postRequest("listreceivedpayments") { implicit t => + withPaginated { paginated_opt => + formFields(fromFormParam(), toFormParam()) { (from, to) => + complete(eclairApi.receivedPayments(from, to, paginated_opt)) + } + } + } + val payOffer: Route = postRequest("payoffer") { implicit t => formFields(offerFormParam, amountMsatFormParam, "quantity".as[Long].?, "maxAttempts".as[Int].?, "maxFeeFlatSat".as[Satoshi].?, "maxFeePct".as[Double].?, "externalId".?, "pathFindingExperimentName".?, "blocking".as[Boolean].?) { case (offer, amountMsat, quantity_opt, maxAttempts_opt, maxFeeFlat_opt, maxFeePct_opt, externalId_opt, pathFindingExperimentName_opt, blocking_opt) => @@ -105,6 +113,6 @@ trait Payment { } } - val paymentRoutes: Route = usableBalances ~ payInvoice ~ sendToNode ~ sendToRoute ~ getSentInfo ~ getReceivedInfo ~ payOffer + val paymentRoutes: Route = usableBalances ~ payInvoice ~ sendToNode ~ sendToRoute ~ getSentInfo ~ getReceivedInfo ~ listReceivedPayments ~ payOffer }