1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-03-12 19:01:39 +01:00

Add listreceivedpayments RPC call (#2607)

Add a new RPC to list payments received by the node.
This commit is contained in:
rorp 2023-04-04 10:44:45 -07:00 committed by GitHub
parent dcedeccb05
commit e383d81de8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 46 additions and 31 deletions

View file

@ -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

View file

@ -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

View file

@ -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)
}

View file

@ -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 = {

View file

@ -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]
}

View file

@ -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()))

View file

@ -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)

View file

@ -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))

View file

@ -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"))

View file

@ -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)
}

View file

@ -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
}