Skip to content
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
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 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
Expand Down
1 change: 1 addition & 0 deletions docs/release-notes/eclair-vnext.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions eclair-core/eclair-cli
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ and COMMAND is one of the available commands:
- forceclose
- channel
- channels
- closedchannels
- allchannels
- allupdates
- channelstats
Expand Down
10 changes: 10 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 @@ -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]]
Expand Down Expand Up @@ -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)))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)]
Expand Down
Original file line number Diff line number Diff line change
@@ -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}
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@ 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
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

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

Expand Down Expand Up @@ -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 {
Comment thread
rorp marked this conversation as resolved.
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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._
Expand Down Expand Up @@ -127,6 +127,14 @@ trait Channel {
}
}

val closedChannels: Route = postRequest("closedchannels") { implicit t =>
Comment thread
rorp marked this conversation as resolved.
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())
}
Expand All @@ -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

}