mirror of
https://github.com/ACINQ/eclair.git
synced 2025-01-18 13:23:46 +01:00
Add closedchannels
RPC (#2642)
This RPC allows to access the historic channel data without relying on third party services like LN explorers. Note that when the `remoteNodeId` filter is not provided, this query may be expensive on nodes with a lot of closed channels.
This commit is contained in:
parent
46d1c73889
commit
37eb1420dc
@ -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 cpfpbumpfees 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"
|
||||
allopts="getinfo connect open cpfpbumpfees close forceclose updaterelayfee peers channels channel closedchannels 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
|
||||
|
@ -48,6 +48,7 @@ Node operators that use Postgres as database backend and make SQL queries on cha
|
||||
- `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)
|
||||
- `closedchannels` lists closed channels. It accepts `--count` and `--skip` parameters to limit the number of retrieved items as well (#2642)
|
||||
- `cpfpbumpfees` can be used to unblock chains of unconfirmed transactions by creating a child transaction that pays a high fee (#1783)
|
||||
|
||||
### Miscellaneous improvements and bug fixes
|
||||
|
@ -45,6 +45,7 @@ and COMMAND is one of the available commands:
|
||||
- forceclose
|
||||
- channel
|
||||
- channels
|
||||
- closedchannels
|
||||
- allchannels
|
||||
- allupdates
|
||||
- channelstats
|
||||
|
@ -104,6 +104,8 @@ trait Eclair {
|
||||
|
||||
def channelInfo(channel: ApiTypes.ChannelIdentifier)(implicit timeout: Timeout): Future[CommandResponse[CMD_GET_CHANNEL_INFO]]
|
||||
|
||||
def closedChannels(nodeId_opt: Option[PublicKey], paginated_opt: Option[Paginated])(implicit timeout: Timeout): Future[Iterable[RES_GET_CHANNEL_INFO]]
|
||||
|
||||
def peers()(implicit timeout: Timeout): Future[Iterable[PeerInfo]]
|
||||
|
||||
def node(nodeId: PublicKey)(implicit timeout: Timeout): Future[Option[Router.PublicNode]]
|
||||
@ -288,6 +290,14 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
|
||||
sendToChannel[CMD_GET_CHANNEL_INFO, CommandResponse[CMD_GET_CHANNEL_INFO]](channel, CMD_GET_CHANNEL_INFO(ActorRef.noSender))
|
||||
}
|
||||
|
||||
override def closedChannels(nodeId_opt: Option[PublicKey], paginated_opt: Option[Paginated])(implicit timeout: Timeout): Future[Iterable[RES_GET_CHANNEL_INFO]] = {
|
||||
Future {
|
||||
appKit.nodeParams.db.channels.listClosedChannels(nodeId_opt, paginated_opt).map { data =>
|
||||
RES_GET_CHANNEL_INFO(nodeId = data.remoteNodeId, channelId = data.channelId, state = CLOSED, data = data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override def allChannels()(implicit timeout: Timeout): Future[Iterable[ChannelDesc]] = {
|
||||
(appKit.router ? Router.GetChannels).mapTo[Iterable[ChannelAnnouncement]].map(_.map(c => ChannelDesc(c.shortChannelId, c.nodeId1, c.nodeId2)))
|
||||
}
|
||||
|
@ -17,7 +17,8 @@
|
||||
package fr.acinq.eclair.db
|
||||
|
||||
import fr.acinq.bitcoin.scalacompat.ByteVector32
|
||||
import fr.acinq.eclair.CltvExpiry
|
||||
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
|
||||
import fr.acinq.eclair.{CltvExpiry, Paginated, TimestampSecond}
|
||||
import fr.acinq.eclair.channel.PersistentChannelData
|
||||
import fr.acinq.eclair.db.DbEventHandler.ChannelEvent
|
||||
|
||||
@ -33,6 +34,8 @@ trait ChannelsDb {
|
||||
|
||||
def listLocalChannels(): Seq[PersistentChannelData]
|
||||
|
||||
def listClosedChannels(remoteNodeId_opt: Option[PublicKey], paginated_opt: Option[Paginated]): Seq[PersistentChannelData]
|
||||
|
||||
def addHtlcInfo(channelId: ByteVector32, commitmentNumber: Long, paymentHash: ByteVector32, cltvExpiry: CltvExpiry): Unit
|
||||
|
||||
def listHtlcInfos(channelId: ByteVector32, commitmentNumber: Long): Seq[(ByteVector32, CltvExpiry)]
|
||||
|
@ -1,6 +1,7 @@
|
||||
package fr.acinq.eclair.db
|
||||
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder
|
||||
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
|
||||
import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, Satoshi}
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.db.Databases.{FileBackup, PostgresDatabases, SqliteDatabases}
|
||||
@ -10,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, Paginated, RealShortChannelId, ShortChannelId, TimestampMilli}
|
||||
import fr.acinq.eclair.{CltvExpiry, MilliSatoshi, Paginated, RealShortChannelId, ShortChannelId, TimestampMilli, TimestampSecond}
|
||||
import grizzled.slf4j.Logging
|
||||
|
||||
import java.io.File
|
||||
@ -229,6 +230,11 @@ case class DualChannelsDb(primary: ChannelsDb, secondary: ChannelsDb) extends Ch
|
||||
primary.listLocalChannels()
|
||||
}
|
||||
|
||||
override def listClosedChannels(remoteNodeId_opt: Option[PublicKey], paginated_opt: Option[Paginated]): Seq[PersistentChannelData] = {
|
||||
runAsync(secondary.listClosedChannels(remoteNodeId_opt, paginated_opt))
|
||||
primary.listClosedChannels(remoteNodeId_opt, paginated_opt)
|
||||
}
|
||||
|
||||
override def addHtlcInfo(channelId: ByteVector32, commitmentNumber: Long, paymentHash: ByteVector32, cltvExpiry: CltvExpiry): Unit = {
|
||||
runAsync(secondary.addHtlcInfo(channelId, commitmentNumber, paymentHash, cltvExpiry))
|
||||
primary.addHtlcInfo(channelId, commitmentNumber, paymentHash, cltvExpiry)
|
||||
|
@ -18,7 +18,7 @@ package fr.acinq.eclair.db.pg
|
||||
|
||||
import com.zaxxer.hikari.util.IsolationLevel
|
||||
import fr.acinq.bitcoin.scalacompat.ByteVector32
|
||||
import fr.acinq.eclair.CltvExpiry
|
||||
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
|
||||
import fr.acinq.eclair.channel.PersistentChannelData
|
||||
import fr.acinq.eclair.db.ChannelsDb
|
||||
import fr.acinq.eclair.db.DbEventHandler.ChannelEvent
|
||||
@ -26,6 +26,7 @@ import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics
|
||||
import fr.acinq.eclair.db.Monitoring.Tags.DbBackends
|
||||
import fr.acinq.eclair.db.pg.PgUtils.PgLock
|
||||
import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.channelDataCodec
|
||||
import fr.acinq.eclair.{CltvExpiry, Paginated}
|
||||
import grizzled.slf4j.Logging
|
||||
import scodec.bits.BitVector
|
||||
|
||||
@ -246,6 +247,19 @@ class PgChannelsDb(implicit ds: DataSource, lock: PgLock) extends ChannelsDb wit
|
||||
}
|
||||
}
|
||||
|
||||
override def listClosedChannels(remoteNodeId_opt: Option[PublicKey], paginated_opt: Option[Paginated]): Seq[PersistentChannelData] = withMetrics("channels/list-closed-channels", DbBackends.Postgres) {
|
||||
val sql = remoteNodeId_opt match {
|
||||
case None => "SELECT data FROM local.channels WHERE is_closed=TRUE ORDER BY closed_timestamp DESC"
|
||||
case Some(remoteNodeId) => s"SELECT data FROM local.channels WHERE is_closed=TRUE AND remote_node_id = '${remoteNodeId.toHex}' ORDER BY closed_timestamp DESC"
|
||||
}
|
||||
withLock { pg =>
|
||||
using(pg.prepareStatement(limited(sql, paginated_opt))) { statement =>
|
||||
statement.executeQuery()
|
||||
.mapCodec(channelDataCodec).toSeq
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override def addHtlcInfo(channelId: ByteVector32, commitmentNumber: Long, paymentHash: ByteVector32, cltvExpiry: CltvExpiry): Unit = withMetrics("channels/add-htlc-info", DbBackends.Postgres) {
|
||||
withLock { pg =>
|
||||
using(pg.prepareStatement("INSERT INTO local.htlc_infos VALUES (?, ?, ?, ?)")) { statement =>
|
||||
|
@ -17,13 +17,14 @@
|
||||
package fr.acinq.eclair.db.sqlite
|
||||
|
||||
import fr.acinq.bitcoin.scalacompat.ByteVector32
|
||||
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
|
||||
import fr.acinq.eclair.channel.PersistentChannelData
|
||||
import fr.acinq.eclair.db.ChannelsDb
|
||||
import fr.acinq.eclair.db.DbEventHandler.ChannelEvent
|
||||
import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics
|
||||
import fr.acinq.eclair.db.Monitoring.Tags.DbBackends
|
||||
import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.channelDataCodec
|
||||
import fr.acinq.eclair.{CltvExpiry, TimestampMilli}
|
||||
import fr.acinq.eclair.{CltvExpiry, Paginated, TimestampMilli, TimestampSecond}
|
||||
import grizzled.slf4j.Logging
|
||||
import scodec.bits.BitVector
|
||||
|
||||
@ -170,6 +171,27 @@ class SqliteChannelsDb(val sqlite: Connection) extends ChannelsDb with Logging {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override def listClosedChannels(remoteNodeId_opt: Option[PublicKey], paginated_opt: Option[Paginated]): Seq[PersistentChannelData] = withMetrics("channels/list-closed-channels", DbBackends.Sqlite) {
|
||||
val sql = "SELECT data FROM local_channels WHERE is_closed=1 ORDER BY closed_timestamp DESC"
|
||||
remoteNodeId_opt match {
|
||||
case None =>
|
||||
using(sqlite.prepareStatement(limited(sql, paginated_opt))) { statement =>
|
||||
statement.executeQuery().mapCodec(channelDataCodec).toSeq
|
||||
}
|
||||
case Some(nodeId) =>
|
||||
using(sqlite.prepareStatement(sql)) { statement =>
|
||||
val filtered = statement.executeQuery()
|
||||
.mapCodec(channelDataCodec).filter(_.remoteNodeId == nodeId)
|
||||
val limited = paginated_opt match {
|
||||
case None => filtered
|
||||
case Some(p) => filtered.slice(p.skip, p.skip + p.count)
|
||||
}
|
||||
limited.toSeq
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override def addHtlcInfo(channelId: ByteVector32, commitmentNumber: Long, paymentHash: ByteVector32, cltvExpiry: CltvExpiry): Unit = withMetrics("channels/add-htlc-info", DbBackends.Sqlite) {
|
||||
using(sqlite.prepareStatement("INSERT INTO htlc_infos VALUES (?, ?, ?, ?)")) { statement =>
|
||||
statement.setBytes(1, channelId.toArray)
|
||||
|
@ -18,6 +18,7 @@ package fr.acinq.eclair.db
|
||||
|
||||
import com.softwaremill.quicklens._
|
||||
import fr.acinq.bitcoin.scalacompat.ByteVector32
|
||||
import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey
|
||||
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
|
||||
import fr.acinq.eclair.TestDatabases.{TestPgDatabases, TestSqliteDatabases, migrationCheck}
|
||||
import fr.acinq.eclair.channel.RealScidStatus
|
||||
@ -30,7 +31,7 @@ import fr.acinq.eclair.db.sqlite.SqliteChannelsDb
|
||||
import fr.acinq.eclair.db.sqlite.SqliteUtils.ExtendedResultSet._
|
||||
import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.channelDataCodec
|
||||
import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec
|
||||
import fr.acinq.eclair.{CltvExpiry, RealShortChannelId, TestDatabases, randomBytes32, randomKey}
|
||||
import fr.acinq.eclair.{CltvExpiry, RealShortChannelId, TestDatabases, TimestampSecond, randomBytes32, randomKey}
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
@ -90,9 +91,13 @@ class ChannelsDbSpec extends AnyFunSuite {
|
||||
assert(db.listHtlcInfos(channel1.channelId, commitNumber).toList.toSet == Set((paymentHash1, cltvExpiry1), (paymentHash2, cltvExpiry2)))
|
||||
assert(db.listHtlcInfos(channel1.channelId, 43).toList == Nil)
|
||||
|
||||
assert(db.listClosedChannels(None, None).isEmpty)
|
||||
db.removeChannel(channel1.channelId)
|
||||
assert(db.getChannel(channel1.channelId).isEmpty)
|
||||
assert(db.listLocalChannels() == List(channel2b))
|
||||
assert(db.listClosedChannels(None, None) == List(channel1))
|
||||
assert(db.listClosedChannels(Some(channel1.remoteNodeId), None) == List(channel1))
|
||||
assert(db.listClosedChannels(Some(PrivateKey(randomBytes32()).publicKey), None) == Nil)
|
||||
assert(db.listHtlcInfos(channel1.channelId, commitNumber).toList == Nil)
|
||||
db.removeChannel(channel2b.channelId)
|
||||
assert(db.getChannel(channel2b.channelId).isEmpty)
|
||||
|
@ -19,7 +19,7 @@ package fr.acinq.eclair.api.handlers
|
||||
import akka.http.scaladsl.server.{MalformedFormFieldRejection, Route}
|
||||
import akka.util.Timeout
|
||||
import fr.acinq.bitcoin.scalacompat.{Satoshi, Script}
|
||||
import fr.acinq.eclair.MilliSatoshi
|
||||
import fr.acinq.eclair.{MilliSatoshi, Paginated}
|
||||
import fr.acinq.eclair.api.Service
|
||||
import fr.acinq.eclair.api.directives.EclairDirectives
|
||||
import fr.acinq.eclair.api.serde.FormParamExtractors._
|
||||
@ -127,6 +127,14 @@ trait Channel {
|
||||
}
|
||||
}
|
||||
|
||||
val closedChannels: Route = postRequest("closedchannels") { implicit t =>
|
||||
withPaginated { paginated_opt =>
|
||||
formFields(nodeIdFormParam.?) { toRemoteNodeId_opt =>
|
||||
complete(eclairApi.closedChannels(toRemoteNodeId_opt, paginated_opt.orElse(Some(Paginated(count = 10, skip = 0)))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val allChannels: Route = postRequest("allchannels") { implicit t =>
|
||||
complete(eclairApi.allChannels())
|
||||
}
|
||||
@ -147,6 +155,6 @@ trait Channel {
|
||||
complete(eclairApi.channelBalances())
|
||||
}
|
||||
|
||||
val channelRoutes: Route = open ~ rbfOpen ~ spliceIn ~ spliceOut ~ close ~ forceClose ~ channel ~ channels ~ allChannels ~ allUpdates ~ channelStats ~ channelBalances
|
||||
val channelRoutes: Route = open ~ rbfOpen ~ spliceIn ~ spliceOut ~ close ~ forceClose ~ channel ~ channels ~ closedChannels ~ allChannels ~ allUpdates ~ channelStats ~ channelBalances
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user