Merge pull request #236 from bitromortac/2603-fwd-prep-7

db: implement sql tables and store for channel events
This commit is contained in:
bitromortac 2026-04-02 10:22:04 +02:00 committed by GitHub
commit 0af64175a9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 961 additions and 4 deletions

View file

@ -50,10 +50,28 @@ jobs:
- name: run imports check
run: make fmt
- name: run JS stubs check
run: make rpc-js-compile
#######################
# sql model generation
#######################
sqlc-check:
name: Sqlc check
runs-on: ubuntu-latest
steps:
- name: git checkout
uses: actions/checkout@v2
- name: setup go ${{ env.GO_VERSION }}
uses: actions/setup-go@v2
with:
go-version: '${{ env.GO_VERSION }}'
- name: Generate sql models
run: make sqlc-check
########################
# lint code
########################

109
chanevents/chanevents.go Normal file
View file

@ -0,0 +1,109 @@
// Package chanevents contains functions for monitoring and storing channel
// events such as online/offline and balance updates.
package chanevents
import (
"time"
"github.com/btcsuite/btcd/btcutil"
"github.com/lightningnetwork/lnd/fn/v2"
)
// EventType is an enum for the different types of channel events.
type EventType int16
const (
// EventTypeUnknown is the unknown event type.
EventTypeUnknown = 0
// EventTypeOnline is the online event type.
EventTypeOnline = 1
// EventTypeOffline is the offline event type.
EventTypeOffline = 2
// EventTypeUpdate is the balance update event type.
EventTypeUpdate = 3
)
// String returns the string representation of the event type.
func (e EventType) String() string {
switch e {
case EventTypeOnline:
return "online"
case EventTypeOffline:
return "offline"
case EventTypeUpdate:
return "update"
default:
return "unknown"
}
}
// EventTypeFromString returns the event type from a string.
func EventTypeFromString(s string) EventType {
switch s {
case "online":
return EventTypeOnline
case "offline":
return EventTypeOffline
case "update":
return EventTypeUpdate
default:
return EventTypeUnknown
}
}
// Peer is the application-level representation of a peer.
type Peer struct {
// ID is the database ID of the peer.
ID int64
// PubKey is the public key of the peer.
PubKey string
}
// Channel is the application-level representation of a channel.
type Channel struct {
// ID is the database ID of the channel.
ID int64
// ChannelPoint is the channel point of the channel.
ChannelPoint string
// ShortChannelID is the short channel ID of the channel.
ShortChannelID uint64
// PeerID is the database ID of the peer that this channel is with.
PeerID int64
}
// ChannelEvent is the application-level representation of a channel event.
type ChannelEvent struct {
// ID is the database ID of the event.
ID int64
// ChannelID is the database ID of the channel that this event is
// associated with.
ChannelID int64
// EventType is the type of the event.
EventType EventType
// Timestamp is the time that the event occurred.
Timestamp time.Time
// LocalBalance is the local balance of the channel at the time of the
// event. This is only populated for balance update events.
LocalBalance fn.Option[btcutil.Amount]
// RemoteBalance is the remote balance of the channel at the time of the
// event. This is only populated for balance update events.
RemoteBalance fn.Option[btcutil.Amount]
}

273
chanevents/store.go Normal file
View file

@ -0,0 +1,273 @@
package chanevents
import (
"context"
"database/sql"
"errors"
"fmt"
"time"
"github.com/btcsuite/btcd/btcutil"
"github.com/lightninglabs/faraday/db/sqlc"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/fn/v2"
"github.com/lightningnetwork/lnd/sqldb/v2"
)
var (
errUnknownPeer = errors.New("unknown peer")
errUnknownChannel = errors.New("unknown channel")
)
// Queries is a subset of the sqlc.Queries interface that can be used to
// interact with the peers, channels and channel_events tables.
type Queries interface {
InsertPeer(ctx context.Context, pubkey string) (int64, error)
GetPeerByPubKey(ctx context.Context, pubkey string) (sqlc.Peer, error)
InsertChannel(ctx context.Context,
arg sqlc.InsertChannelParams) (int64, error)
GetChannelByChanPoint(ctx context.Context,
channelPoint string) (sqlc.Channel, error)
GetChannelByShortChanID(ctx context.Context,
shortChannelID int64) (sqlc.Channel, error)
InsertChannelEvent(ctx context.Context,
arg sqlc.InsertChannelEventParams) error
GetChannelEvents(ctx context.Context,
arg sqlc.GetChannelEventsParams) ([]sqlc.ChannelEvent, error)
}
// Store provides access to the db for channel events.
type Store struct {
// db is all the higher level queries that the SQLStore has access to in
// order to implement all its CRUD logic.
db BatchedSQLQueries
// BaseDB represents the underlying database connection.
*sqldb.BaseDB
clock clock.Clock
}
// BatchedSQLQueries combines the SQLQueries interface with the BatchedTx
// interface, allowing for multiple queries to be executed in single SQL
// transaction.
type BatchedSQLQueries interface {
SQLQueries
sqldb.BatchedTx[SQLQueries]
}
// SQLQueries is a subset of the sqlc.Queries interface that can be used to
// interact with various chanevents tables.
type SQLQueries interface {
sqldb.BaseQuerier
Queries
}
type SQLQueriesExecutor[T sqldb.BaseQuerier] struct {
*sqldb.TransactionExecutor[T]
SQLQueries
}
// NewStore creates a new SQLStore instance given an open SQLQueries storage
// backend.
func NewStore(sqlDB *sqldb.BaseDB, queries *sqlc.Queries,
clock clock.Clock) *Store {
txExecutor := sqldb.NewTransactionExecutor(
sqlDB,
func(tx *sql.Tx) SQLQueries {
return queries.WithTx(tx)
},
)
executor := &SQLQueriesExecutor[SQLQueries]{
TransactionExecutor: txExecutor,
SQLQueries: queries,
}
return &Store{
db: executor,
BaseDB: sqlDB,
clock: clock,
}
}
// AddPeer adds a new peer to the database.
func (s *Store) AddPeer(ctx context.Context, pubkey string) (int64, error) {
id, err := s.db.InsertPeer(ctx, pubkey)
if err != nil {
return 0, fmt.Errorf("failed to insert peer: %w", err)
}
return id, nil
}
// GetPeer retrieves a peer by their public key.
func (s *Store) GetPeer(ctx context.Context, pubkey string) (*Peer, error) {
dbPeer, err := s.db.GetPeerByPubKey(ctx, pubkey)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, errUnknownPeer
}
return nil, fmt.Errorf("failed to get peer: %w", err)
}
return &Peer{
ID: dbPeer.ID,
PubKey: dbPeer.Pubkey,
}, nil
}
// int64ToSCID converts an int64 to a uint64 ShortChannelID. The BOLT spec
// encodes SCIDs as uint64, but SQL only supports signed int64. We preserve the
// bits, which means SCIDs with the high bit set will appear negative in the
// database. Direct SQL queries (e.g. ORDER BY short_channel_id) will not sort
// these correctly, but round-tripping through Go preserves the value.
func int64ToSCID(i int64) uint64 {
return uint64(i)
}
// scidToInt64 converts a uint64 ShortChannelID to an int64 for SQL storage.
func scidToInt64(u uint64) int64 {
return int64(u)
}
// AddChannel adds a new channel for a peer.
func (s *Store) AddChannel(ctx context.Context, channelPoint string,
shortChannelID uint64, peerID int64) (int64, error) {
id, err := s.db.InsertChannel(
ctx, sqlc.InsertChannelParams{
ChannelPoint: channelPoint,
ShortChannelID: scidToInt64(shortChannelID),
PeerID: peerID,
},
)
if err != nil {
return 0, fmt.Errorf("failed to insert channel: %w", err)
}
return id, nil
}
// GetChannel retrieves a channel by its channel point.
func (s *Store) GetChannel(ctx context.Context, channelPoint string) (*Channel,
error) {
dbChannel, err := s.db.GetChannelByChanPoint(ctx, channelPoint)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, errUnknownChannel
}
return nil, fmt.Errorf("failed to get channel: %w", err)
}
return &Channel{
ID: dbChannel.ID,
ChannelPoint: dbChannel.ChannelPoint,
ShortChannelID: int64ToSCID(dbChannel.ShortChannelID),
PeerID: dbChannel.PeerID,
}, nil
}
// AddChannelEvent adds a new channel event.
func (s *Store) AddChannelEvent(ctx context.Context,
event *ChannelEvent) error {
var localBalance sql.NullInt64
event.LocalBalance.WhenSome(
func(b btcutil.Amount) {
localBalance.Int64 = int64(b)
localBalance.Valid = true
},
)
var remoteBalance sql.NullInt64
event.RemoteBalance.WhenSome(
func(b btcutil.Amount) {
remoteBalance.Int64 = int64(b)
remoteBalance.Valid = true
},
)
timestamp := event.Timestamp.UTC()
if timestamp.IsZero() {
timestamp = s.clock.Now().UTC()
}
err := s.db.InsertChannelEvent(
ctx, sqlc.InsertChannelEventParams{
ChannelID: event.ChannelID,
EventType: int16(event.EventType),
Timestamp: timestamp,
LocalBalanceSat: localBalance,
RemoteBalanceSat: remoteBalance,
},
)
if err != nil {
return fmt.Errorf("failed to insert channel event: %w", err)
}
return nil
}
// GetChannelEvents retrieves all events for a channel within a given time
// range.
// TODO: Add pagination support (LIMIT/OFFSET) to prevent OOM on high-traffic
// channels.
func (s *Store) GetChannelEvents(ctx context.Context, channelID int64,
startTime, endTime time.Time) ([]*ChannelEvent, error) {
dbEvents, err := s.db.GetChannelEvents(
ctx, sqlc.GetChannelEventsParams{
ChannelID: channelID,
Timestamp: startTime.UTC(),
Timestamp_2: endTime.UTC(),
},
)
if err != nil {
return nil, fmt.Errorf("failed to get channel events: %w", err)
}
events := make([]*ChannelEvent, len(dbEvents))
for i, dbEvent := range dbEvents {
events[i] = marshalChannelEvent(dbEvent)
}
return events, nil
}
// marshalChannelEvent converts a db channel event into our internal type.
func marshalChannelEvent(dbEvent sqlc.ChannelEvent) *ChannelEvent {
var localBalance fn.Option[btcutil.Amount]
if dbEvent.LocalBalanceSat.Valid {
amt := btcutil.Amount(dbEvent.LocalBalanceSat.Int64)
localBalance = fn.Some(amt)
}
var remoteBalance fn.Option[btcutil.Amount]
if dbEvent.RemoteBalanceSat.Valid {
amt := btcutil.Amount(dbEvent.RemoteBalanceSat.Int64)
remoteBalance = fn.Some(amt)
}
return &ChannelEvent{
ID: dbEvent.ID,
ChannelID: dbEvent.ChannelID,
EventType: EventType(dbEvent.EventType),
Timestamp: dbEvent.Timestamp.UTC(),
LocalBalance: localBalance,
RemoteBalance: remoteBalance,
}
}

128
chanevents/store_test.go Normal file
View file

@ -0,0 +1,128 @@
package chanevents
import (
"context"
"testing"
"time"
"github.com/btcsuite/btcd/btcutil"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/fn/v2"
"github.com/stretchr/testify/require"
)
var (
testPubKey = "028d4c6347426f2e3f5e2b8e4a1c3b9f1" +
"c4e5d6f7a8b9c0d1e2f3a4b5c6d7e8f9"
testChanPoint1 = "test_txid:0"
testChanPoint2 = "test_txid:1"
testShortChanID1 uint64 = 123
testShortChanID2 uint64 = 456
testTime = time.Unix(1, 0)
)
// TestStore tests the chanevents store.
func TestStore(t *testing.T) {
t.Parallel()
// First, create a new test database.
clock := clock.NewTestClock(testTime)
store := NewTestDB(t, clock)
ctx := context.Background()
// *** Peers *** Add a peer.
peer := &Peer{PubKey: testPubKey}
peerID, err := store.AddPeer(ctx, peer.PubKey)
require.NoError(t, err)
require.NotZero(t, peerID)
// Adding the same peer again violates the unique constraint.
_, err = store.AddPeer(ctx, peer.PubKey)
require.Error(t, err)
dbPeer, err := store.GetPeer(ctx, "non_existent_pubkey")
require.ErrorIs(t, err, errUnknownPeer)
require.Nil(t, dbPeer)
// Get the peer and assert it is the same.
dbPeer, err = store.GetPeer(ctx, peer.PubKey)
require.NoError(t, err)
require.Equal(t, peer.PubKey, dbPeer.PubKey)
// *** Channels *** Add a channel for an unknown peer and assert an
// error is returned.
channelID, err := store.AddChannel(
ctx, testChanPoint1, testShortChanID1, 9999,
)
require.Error(t, err)
require.Zero(t, channelID)
// Add a channel for the peer.
channelID, err = store.AddChannel(
ctx, testChanPoint1, testShortChanID1, peerID,
)
require.NoError(t, err)
require.NotZero(t, channelID)
// Get a non-existent channel and assert an error is returned.
dbChannel, err := store.GetChannel(ctx, "non-existent-chan-point")
require.ErrorIs(t, err, errUnknownChannel)
require.Nil(t, dbChannel)
// Get the channel and assert it is the same.
dbChannel, err = store.GetChannel(ctx, testChanPoint1)
require.NoError(t, err)
require.Equal(t, testChanPoint1, dbChannel.ChannelPoint)
require.Equal(t, testShortChanID1, dbChannel.ShortChannelID)
require.Equal(t, peerID, dbChannel.PeerID)
// Add a second channel for the same peer.
channel2ID, err := store.AddChannel(
ctx, testChanPoint2, testShortChanID2, peerID,
)
require.NoError(t, err)
require.NotZero(t, channel2ID)
// Add an online event for the channel.
onlineEvent := &ChannelEvent{
ChannelID: channelID,
EventType: EventTypeOnline,
}
err = store.AddChannelEvent(ctx, onlineEvent)
require.NoError(t, err)
// Advance the clock for the next event.
clock.SetTime(testTime.Add(time.Second))
// Add an update event for the channel.
localBalance := btcutil.Amount(1000)
remoteBalance := btcutil.Amount(2000)
updateEvent := &ChannelEvent{
ChannelID: channelID,
EventType: EventTypeUpdate,
LocalBalance: fn.Some(localBalance),
RemoteBalance: fn.Some(remoteBalance),
}
err = store.AddChannelEvent(ctx, updateEvent)
require.NoError(t, err)
// Get the channel events and assert they are correct.
events, err := store.GetChannelEvents(
ctx, channelID, time.Unix(0, 0), time.Unix(3, 0),
)
require.NoError(t, err)
require.Len(t, events, 2)
require.Equal(t, onlineEvent.EventType, events[0].EventType)
require.Equal(t, testTime.Unix(), events[0].Timestamp.Unix())
require.True(t, events[0].LocalBalance.IsNone())
require.True(t, events[0].RemoteBalance.IsNone())
require.Equal(t, updateEvent.EventType, events[1].EventType)
require.Equal(
t, testTime.Add(time.Second).Unix(), events[1].Timestamp.Unix(),
)
require.Equal(t, updateEvent.LocalBalance, events[1].LocalBalance)
require.Equal(t, updateEvent.RemoteBalance, events[1].RemoteBalance)
}

View file

@ -0,0 +1,29 @@
//go:build test_db_postgres
package chanevents
import (
"testing"
"github.com/lightninglabs/faraday/db"
"github.com/lightningnetwork/lnd/clock"
"github.com/stretchr/testify/require"
)
// NewTestDB creates a new test chanevents.Store backed by a postgres DB.
func NewTestDB(t *testing.T, clock clock.Clock) *Store {
// We'll create a new test database. The call to NewTestPostgresDB will
// automatically create the DB and apply the migrations.
testDB := db.NewTestPostgresDB(t)
// Now, we'll create the FaradayDB instance from the test database. The
// FaradayDB is the main database object that holds the connection and
// the generated querier.
faradayDB := createStore(t, testDB.BaseDB, clock)
t.Cleanup(func() {
require.NoError(t, faradayDB.Close())
})
return faradayDB
}

18
chanevents/test_sql.go Normal file
View file

@ -0,0 +1,18 @@
package chanevents
import (
"testing"
"github.com/lightninglabs/faraday/db/sqlc"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/sqldb/v2"
)
// createStore is a helper function that creates a new Store.
func createStore(t *testing.T, sqlDB *sqldb.BaseDB, clock clock.Clock) *Store {
queries := sqlc.NewForType(sqlDB, sqlDB.BackendType)
store := NewStore(sqlDB, queries, clock)
return store
}

30
chanevents/test_sqlite.go Normal file
View file

@ -0,0 +1,30 @@
//go:build !test_db_postgres
package chanevents
import (
"testing"
"github.com/lightninglabs/faraday/db"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/sqldb/v2"
"github.com/stretchr/testify/require"
)
// NewTestDB creates a new test chanevents.Store backed by a sqlite DB.
func NewTestDB(t *testing.T, clock clock.Clock) *Store {
// We'll create a new test database. The call to NewTestSqliteDB will
// automatically create the DB and apply the migrations.
testDB := sqldb.NewTestSqliteDB(t, db.FaradayMigrationSets)
// Now, we'll create the FaradayDB instance from the test database. The
// FaradayDB is the main database object that holds the connection and
// the generated querier.
faradayDB := createStore(t, testDB.BaseDB, clock)
t.Cleanup(func() {
require.NoError(t, faradayDB.Close())
})
return faradayDB
}

View file

@ -6,5 +6,5 @@ const (
// daemon.
//
// NOTE: This MUST be updated when a new migration is added.
LatestMigrationVersion = 0
LatestMigrationVersion = 1
)

41
db/migrations_test.go Normal file
View file

@ -0,0 +1,41 @@
package db
import (
"strconv"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
// TestLatestMigrationVersion ensures that LatestMigrationVersion stays in sync
// with the highest-numbered .up.sql file in the migrations directory. Each
// migration — whether pure SQL or programmatic (with a dummy SQL file) — gets
// its own numbered file pair, so the max file number must equal the constant.
func TestLatestMigrationVersion(t *testing.T) {
entries, err := sqlSchemas.ReadDir("sqlc/migrations")
require.NoError(t, err)
var maxVersion uint
for _, entry := range entries {
if !strings.HasSuffix(entry.Name(), ".up.sql") {
continue
}
parts := strings.SplitN(entry.Name(), "_", 2)
require.NotEmpty(t, parts)
v, err := strconv.ParseUint(parts[0], 10, 64)
require.NoError(t, err)
if uint(v) > maxVersion {
maxVersion = uint(v)
}
}
require.EqualValues(
t, maxVersion, LatestMigrationVersion,
"LatestMigrationVersion is out of date, update "+
"db/migrations.go",
)
}

View file

@ -5,4 +5,5 @@ import (
_ "embed"
)
//go:embed sqlc/migrations/*.*.sql
var sqlSchemas embed.FS

150
db/sqlc/chanevents.sql.go Normal file
View file

@ -0,0 +1,150 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// source: chanevents.sql
package sqlc
import (
"context"
"database/sql"
"time"
)
const getChannelByChanPoint = `-- name: GetChannelByChanPoint :one
SELECT id, channel_point, short_channel_id, peer_id FROM channels WHERE channel_point = $1
`
func (q *Queries) GetChannelByChanPoint(ctx context.Context, channelPoint string) (Channel, error) {
row := q.db.QueryRowContext(ctx, getChannelByChanPoint, channelPoint)
var i Channel
err := row.Scan(
&i.ID,
&i.ChannelPoint,
&i.ShortChannelID,
&i.PeerID,
)
return i, err
}
const getChannelByShortChanID = `-- name: GetChannelByShortChanID :one
SELECT id, channel_point, short_channel_id, peer_id FROM channels WHERE short_channel_id = $1
`
func (q *Queries) GetChannelByShortChanID(ctx context.Context, shortChannelID int64) (Channel, error) {
row := q.db.QueryRowContext(ctx, getChannelByShortChanID, shortChannelID)
var i Channel
err := row.Scan(
&i.ID,
&i.ChannelPoint,
&i.ShortChannelID,
&i.PeerID,
)
return i, err
}
const getChannelEvents = `-- name: GetChannelEvents :many
SELECT id, channel_id, event_type, timestamp, local_balance_sat, remote_balance_sat FROM channel_events
WHERE channel_id = $1 AND timestamp >= $2 AND timestamp < $3
ORDER BY timestamp ASC, id ASC
`
type GetChannelEventsParams struct {
ChannelID int64
Timestamp time.Time
Timestamp_2 time.Time
}
func (q *Queries) GetChannelEvents(ctx context.Context, arg GetChannelEventsParams) ([]ChannelEvent, error) {
rows, err := q.db.QueryContext(ctx, getChannelEvents, arg.ChannelID, arg.Timestamp, arg.Timestamp_2)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ChannelEvent
for rows.Next() {
var i ChannelEvent
if err := rows.Scan(
&i.ID,
&i.ChannelID,
&i.EventType,
&i.Timestamp,
&i.LocalBalanceSat,
&i.RemoteBalanceSat,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getPeerByPubKey = `-- name: GetPeerByPubKey :one
SELECT id, pubkey FROM peers WHERE pubkey = $1
`
func (q *Queries) GetPeerByPubKey(ctx context.Context, pubkey string) (Peer, error) {
row := q.db.QueryRowContext(ctx, getPeerByPubKey, pubkey)
var i Peer
err := row.Scan(&i.ID, &i.Pubkey)
return i, err
}
const insertChannel = `-- name: InsertChannel :one
INSERT INTO channels (channel_point, short_channel_id, peer_id) VALUES ($1, $2, $3) RETURNING id
`
type InsertChannelParams struct {
ChannelPoint string
ShortChannelID int64
PeerID int64
}
func (q *Queries) InsertChannel(ctx context.Context, arg InsertChannelParams) (int64, error) {
row := q.db.QueryRowContext(ctx, insertChannel, arg.ChannelPoint, arg.ShortChannelID, arg.PeerID)
var id int64
err := row.Scan(&id)
return id, err
}
const insertChannelEvent = `-- name: InsertChannelEvent :exec
INSERT INTO channel_events (
channel_id, event_type, timestamp, local_balance_sat, remote_balance_sat
) VALUES ($1, $2, $3, $4, $5)
`
type InsertChannelEventParams struct {
ChannelID int64
EventType int16
Timestamp time.Time
LocalBalanceSat sql.NullInt64
RemoteBalanceSat sql.NullInt64
}
func (q *Queries) InsertChannelEvent(ctx context.Context, arg InsertChannelEventParams) error {
_, err := q.db.ExecContext(ctx, insertChannelEvent,
arg.ChannelID,
arg.EventType,
arg.Timestamp,
arg.LocalBalanceSat,
arg.RemoteBalanceSat,
)
return err
}
const insertPeer = `-- name: InsertPeer :one
INSERT INTO peers (pubkey) VALUES ($1) RETURNING id
`
func (q *Queries) InsertPeer(ctx context.Context, pubkey string) (int64, error) {
row := q.db.QueryRowContext(ctx, insertPeer, pubkey)
var id int64
err := row.Scan(&id)
return id, err
}

34
db/sqlc/db_custom.go Normal file
View file

@ -0,0 +1,34 @@
// Package sqlc provides a set of custom database queries and utilities
// for interacting with the SQL database used in the application. It includes
// generated code from sqlc as well as custom wrappers to handle different
// database backends.
package sqlc
import (
"github.com/lightningnetwork/lnd/sqldb/v2"
)
// wrappedTX is a wrapper around a DBTX that also stores the database backend
// type.
type wrappedTX struct {
DBTX
backendType sqldb.BackendType
}
// Backend returns the type of database backend we're using.
func (q *Queries) Backend() sqldb.BackendType {
wtx, ok := q.db.(*wrappedTX)
if !ok {
// Shouldn't happen unless a new database backend type is added
// but not initialized correctly.
return sqldb.BackendTypeUnknown
}
return wtx.backendType
}
// NewForType creates a new Queries instance for the given database type.
func NewForType(db DBTX, typ sqldb.BackendType) *Queries {
return &Queries{db: &wrappedTX{db, typ}}
}

View file

@ -0,0 +1,5 @@
DROP INDEX IF EXISTS channel_events_chan_id_ts_idx;
DROP TABLE IF EXISTS channel_events;
DROP INDEX IF EXISTS channel_peer_idx;
DROP TABLE IF EXISTS channels;
DROP TABLE IF EXISTS peers;

View file

@ -0,0 +1,45 @@
-- The peers table stores all the peers that we have channels with.
CREATE TABLE IF NOT EXISTS peers (
-- The auto incrementing primary key.
id INTEGER PRIMARY KEY,
-- The public key of the peer.
pubkey TEXT NOT NULL UNIQUE
);
-- The channels table stores all the channels that we have with our peers.
CREATE TABLE IF NOT EXISTS channels (
-- The auto incrementing primary key.
id INTEGER PRIMARY KEY,
-- The channel point, as a 'txid:output_index' string.
channel_point TEXT NOT NULL UNIQUE,
-- The short channel ID.
short_channel_id BIGINT NOT NULL UNIQUE,
-- The peer that this channel is with.
peer_id BIGINT NOT NULL REFERENCES peers(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS channel_peer_idx ON channels (peer_id);
-- The channel_events table stores all the events that are associated with a
-- particular channel.
CREATE TABLE IF NOT EXISTS channel_events (
-- The auto incrementing primary key.
id INTEGER PRIMARY KEY,
-- The channel that this event is associated with.
channel_id BIGINT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
-- The type of event.
event_type SMALLINT NOT NULL,
-- The time the event occurred.
timestamp TIMESTAMP NOT NULL,
-- The local balance of the channel at the time of the event.
-- This is only populated for balance update events.
local_balance_sat BIGINT CHECK (local_balance_sat >= 0),
-- The remote balance of the channel at the time of the event.
-- This is only populated for balance update events.
remote_balance_sat BIGINT CHECK (remote_balance_sat >= 0)
);
-- This composite index is crucial for efficiently querying the event history
-- of a specific channel. It allows the database to quickly locate relevant rows
-- for a given channel, sorted by time. This is useful for fetching events
-- within a time range, and for finding the latest event before a certain time.
CREATE INDEX IF NOT EXISTS channel_events_chan_id_ts_idx ON channel_events (channel_id, timestamp);

31
db/sqlc/models.go Normal file
View file

@ -0,0 +1,31 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
package sqlc
import (
"database/sql"
"time"
)
type Channel struct {
ID int64
ChannelPoint string
ShortChannelID int64
PeerID int64
}
type ChannelEvent struct {
ID int64
ChannelID int64
EventType int16
Timestamp time.Time
LocalBalanceSat sql.NullInt64
RemoteBalanceSat sql.NullInt64
}
type Peer struct {
ID int64
Pubkey string
}

21
db/sqlc/querier.go Normal file
View file

@ -0,0 +1,21 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
package sqlc
import (
"context"
)
type Querier interface {
GetChannelByChanPoint(ctx context.Context, channelPoint string) (Channel, error)
GetChannelByShortChanID(ctx context.Context, shortChannelID int64) (Channel, error)
GetChannelEvents(ctx context.Context, arg GetChannelEventsParams) ([]ChannelEvent, error)
GetPeerByPubKey(ctx context.Context, pubkey string) (Peer, error)
InsertChannel(ctx context.Context, arg InsertChannelParams) (int64, error)
InsertChannelEvent(ctx context.Context, arg InsertChannelEventParams) error
InsertPeer(ctx context.Context, pubkey string) (int64, error)
}
var _ Querier = (*Queries)(nil)

View file

@ -0,0 +1,24 @@
-- name: InsertPeer :one
INSERT INTO peers (pubkey) VALUES ($1) RETURNING id;
-- name: GetPeerByPubKey :one
SELECT * FROM peers WHERE pubkey = $1;
-- name: InsertChannel :one
INSERT INTO channels (channel_point, short_channel_id, peer_id) VALUES ($1, $2, $3) RETURNING id;
-- name: GetChannelByChanPoint :one
SELECT * FROM channels WHERE channel_point = $1;
-- name: GetChannelByShortChanID :one
SELECT * FROM channels WHERE short_channel_id = $1;
-- name: InsertChannelEvent :exec
INSERT INTO channel_events (
channel_id, event_type, timestamp, local_balance_sat, remote_balance_sat
) VALUES ($1, $2, $3, $4, $5);
-- name: GetChannelEvents :many
SELECT * FROM channel_events
WHERE channel_id = $1 AND timestamp >= $2 AND timestamp < $3
ORDER BY timestamp ASC, id ASC;

4
go.mod
View file

@ -13,6 +13,8 @@ require (
github.com/lightninglabs/lndclient v1.0.1-0.20260224134629-de7b65bb4c60
github.com/lightningnetwork/lnd v0.20.0-beta.rc4.0.20260223110936-dd65ba2b0106
github.com/lightningnetwork/lnd/cert v1.2.2
github.com/lightningnetwork/lnd/clock v1.1.1
github.com/lightningnetwork/lnd/fn/v2 v2.0.9
github.com/lightningnetwork/lnd/kvdb v1.4.16
github.com/lightningnetwork/lnd/sqldb/v2 v2.0.0-20260326184657-f7cc56305bae
github.com/shopspring/decimal v1.2.0
@ -100,8 +102,6 @@ require (
github.com/lightninglabs/neutrino v0.16.1 // indirect
github.com/lightninglabs/neutrino/cache v1.1.2 // indirect
github.com/lightningnetwork/lightning-onion v1.2.1-0.20240815225420-8b40adf04ab9 // indirect
github.com/lightningnetwork/lnd/clock v1.1.1 // indirect
github.com/lightningnetwork/lnd/fn/v2 v2.0.9 // indirect
github.com/lightningnetwork/lnd/healthcheck v1.2.6 // indirect
github.com/lightningnetwork/lnd/queue v1.1.1 // indirect
github.com/lightningnetwork/lnd/sqldb v1.0.13-0.20260223110936-dd65ba2b0106 // indirect