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 listreceivedpayments RPC call #2607

Merged
merged 7 commits into from
Apr 4, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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 contrib/eclair-cli.bash-completion
Original file line number Diff line number Diff line change
Expand Up @@ -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 listpaidinvoices listexpiredinvoices getreceivedinfo audit networkfees channelstats"

if ! [[ " $allopts " =~ " $prev " ]]; then # prevent double arguments
if [[ -z "$cur" || "$cur" =~ ^[a-z] ]]; then
Expand Down
2 changes: 2 additions & 0 deletions docs/release-notes/eclair-vnext.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ Offers are still experimental and some details could still change before they ar
- `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` allos to query received paid (#2607)
- `listexpiredinvoices` returns expired invoices (#2607)

### Miscellaneous improvements and bug fixes

Expand Down
12 changes: 12 additions & 0 deletions eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -139,6 +141,8 @@ trait Eclair {

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

def expiredInvoices(from: TimestampSecond, to: TimestampSecond, paginated_opt: Option[Paginated])(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 @@ -461,6 +465,14 @@ 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 expiredInvoices(from: TimestampSecond, to: TimestampSecond, paginated_opt: Option[Paginated])(implicit timeout: Timeout): Future[Seq[Invoice]] = Future {
appKit.nodeParams.db.payments.listExpiredIncomingPayments(from.toTimestampMilli, to.toTimestampMilli, paginated_opt).map(_.invoice)
}

override def getInvoice(paymentHash: ByteVector32)(implicit timeout: Timeout): Future[Option[Invoice]] = Future {
appKit.nodeParams.db.payments.getIncomingPayment(paymentHash).map(_.invoice)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -316,14 +316,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 = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,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]

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -363,9 +363,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
Expand All @@ -384,9 +384,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()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -360,8 +360,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
Expand All @@ -377,8 +377,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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,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))
})
}

Expand Down Expand Up @@ -511,9 +511,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))
Expand Down Expand Up @@ -701,8 +701,8 @@ class PaymentsDbSpec extends AnyFunSuite {

val now = TimestampMilli.now()
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.listExpiredIncomingPayments(0 unixms, now, None) == Seq(expiredPayment1, expiredPayment2, expiredPayment3))
assert(db.listReceivedIncomingPayments(0 unixms, now, None) == Nil)
assert(db.listPendingIncomingPayments(0 unixms, now, None) == Seq(pendingPayment1, pendingPayment2, pendingPayment3, payment1.copy(status = IncomingPaymentStatus.Pending), payment2.copy(status = IncomingPaymentStatus.Pending), payment3.copy(status = IncomingPaymentStatus.Pending)))

db.receiveIncomingPayment(paidInvoice1.paymentHash, 461 msat, receivedAt1)
Expand All @@ -722,7 +722,7 @@ class PaymentsDbSpec extends AnyFunSuite {
assert(db.listIncomingPayments(0 unixms, now, Some(Paginated(3, 3))) == Seq(pendingPayment1, pendingPayment2, pendingPayment3))
assert(db.listPendingIncomingPayments(0 unixms, now, None) == Seq(pendingPayment1, pendingPayment2, pendingPayment3))
assert(db.listPendingIncomingPayments(0 unixms, now, Some(Paginated(1, 1))) == Seq(pendingPayment2))
assert(db.listReceivedIncomingPayments(0 unixms, now) == Seq(payment1, payment2, payment3))
assert(db.listReceivedIncomingPayments(0 unixms, now, None) == 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"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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),
Expand All @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ trait Invoice {
}
}

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

val parseInvoice: Route = postRequest("parseinvoice") { implicit t =>
formFields(invoiceFormParam) { invoice =>
complete(invoice)
Expand All @@ -69,6 +77,6 @@ trait Invoice {
}
}

val invoiceRoutes: Route = createInvoice ~ getInvoice ~ listInvoices ~ listPendingInvoices ~ parseInvoice ~ deleteInvoice
val invoiceRoutes: Route = createInvoice ~ getInvoice ~ listInvoices ~ listPendingInvoices ~ listExpiredInvoices ~ parseInvoice ~ deleteInvoice

}
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand All @@ -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

}