lightning-terminal/firewalldb/actions_sql.go
Viktor Torstensson 6d350d6270
firewalldb: use sqldb/v2 in firewalldb package
Update the firewalldb package to use the `sqldb/v2` package instead of
the older version.
2026-02-06 20:14:35 +01:00

428 lines
11 KiB
Go

package firewalldb
import (
"context"
"database/sql"
"encoding/binary"
"errors"
"fmt"
"math"
"github.com/lightninglabs/lightning-terminal/accounts"
"github.com/lightninglabs/lightning-terminal/db"
"github.com/lightninglabs/lightning-terminal/db/sqlc"
"github.com/lightninglabs/lightning-terminal/session"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/sqldb/v2"
)
// SQLAccountQueries is a subset of the sqlc.Queries interface that can be used
// to interact with the accounts table.
type SQLAccountQueries interface {
GetAccount(ctx context.Context, id int64) (sqlc.Account, error)
GetAccountIDByAlias(ctx context.Context, alias int64) (int64, error)
}
// SQLActionQueries is a subset of the sqlc.Queries interface that can be used
// to interact with action related tables.
//
//nolint:ll
type SQLActionQueries interface {
SQLSessionQueries
SQLAccountQueries
InsertAction(ctx context.Context, arg sqlc.InsertActionParams) (int64, error)
SetActionState(ctx context.Context, arg sqlc.SetActionStateParams) error
ListActions(ctx context.Context, arg sqlc.ListActionsParams) ([]sqlc.Action, error)
CountActions(ctx context.Context, arg sqlc.ActionQueryParams) (int64, error)
GetAction(ctx context.Context, id int64) (sqlc.Action, error)
}
// sqlActionLocator helps us find an action in the SQL DB.
type sqlActionLocator struct {
// id is the DB level ID of the action.
id int64
}
func (s *sqlActionLocator) isActionLocator() {}
// A compile-time check to ensure sqlActionLocator implements the ActionLocator
// interface.
var _ ActionLocator = (*sqlActionLocator)(nil)
// GetActionsReadDB is a method on DB that constructs an ActionsReadDB.
//
// NOTE: This is part of the ActionDB interface.
func (s *SQLDB) GetActionsReadDB(groupID session.ID,
featureName string) ActionsReadDB {
return &allActionsReadDB{
db: s,
groupID: groupID,
featureName: featureName,
}
}
// AddAction persists the given action to the database.
//
// NOTE: This is a part of the ActionDB interface.
func (s *SQLDB) AddAction(ctx context.Context,
req *AddActionReq) (ActionLocator, error) {
var (
writeTxOpts db.QueriesTxOptions
locator sqlActionLocator
actor = sql.NullString{
String: req.ActorName,
Valid: req.ActorName != "",
}
feature = sql.NullString{
String: req.FeatureName,
Valid: req.FeatureName != "",
}
trigger = sql.NullString{
String: req.Trigger,
Valid: req.Trigger != "",
}
intent = sql.NullString{
String: req.Intent,
Valid: req.Intent != "",
}
)
err := s.db.ExecTx(ctx, &writeTxOpts, func(db SQLQueries) error {
// Do best effort to see if this action is linked to a session,
// and/or an action or none.
var (
sessionID sql.NullInt64
accountID sql.NullInt64
)
// First check session DB.
var sessErr error
req.SessionID.WhenSome(func(alias session.ID) {
sessID, err := db.GetSessionIDByAlias(ctx, alias[:])
if errors.Is(err, sql.ErrNoRows) {
sessErr = session.ErrSessionNotFound
return
} else if err != nil {
sessErr = err
return
}
sessionID = sqldb.SQLInt64(sessID)
})
if sessErr != nil {
return sessErr
}
// If an account ID was provided, then it must exist in our DB.
var getAcctErr error
req.AccountID.WhenSome(func(alias accounts.AccountID) {
aliasInt, err := alias.ToInt64()
if err != nil {
getAcctErr = err
return
}
acctID, err := db.GetAccountIDByAlias(ctx, aliasInt)
if errors.Is(err, sql.ErrNoRows) {
getAcctErr = accounts.ErrAccNotFound
return
} else if err != nil {
getAcctErr = err
return
}
accountID = sqldb.SQLInt64(acctID)
})
if getAcctErr != nil {
return getAcctErr
}
var macID []byte
req.MacaroonRootKeyID.WhenSome(func(rootKeyID uint64) {
rootKeyBytes := make([]byte, 8)
binary.BigEndian.PutUint64(rootKeyBytes[:], rootKeyID)
macID = rootKeyBytes
})
id, err := db.InsertAction(ctx, sqlc.InsertActionParams{
SessionID: sessionID,
AccountID: accountID,
ActorName: actor,
MacaroonIdentifier: macID,
FeatureName: feature,
ActionTrigger: trigger,
Intent: intent,
StructuredJsonData: []byte(req.StructuredJsonData),
RpcMethod: req.RPCMethod,
RpcParamsJson: req.RPCParamsJson,
CreatedAt: s.clock.Now().UTC(),
ActionState: int16(ActionStateInit),
})
if err != nil {
return err
}
locator = sqlActionLocator{
id: id,
}
return nil
}, sqldb.NoOpReset)
if err != nil {
return nil, err
}
return &locator, nil
}
// SetActionState finds the action specified by the ActionLocator and sets its
// state to the given state.
//
// NOTE: This is a part of the ActionDB interface.
func (s *SQLDB) SetActionState(ctx context.Context, al ActionLocator,
state ActionState, errReason string) error {
if errReason != "" && state != ActionStateError {
return fmt.Errorf("error reason should only be set for " +
"ActionStateError")
}
locator, ok := al.(*sqlActionLocator)
if !ok {
return fmt.Errorf("expected sqlActionLocator, got %T", al)
}
var writeTxOpts db.QueriesTxOptions
return s.db.ExecTx(ctx, &writeTxOpts, func(db SQLQueries) error {
return db.SetActionState(ctx, sqlc.SetActionStateParams{
ID: locator.id,
ActionState: int16(state),
ErrorReason: sql.NullString{
String: errReason,
Valid: errReason != "",
},
})
}, sqldb.NoOpReset)
}
// ListActions returns a list of Actions. The query IndexOffset and MaxNum
// params can be used to control the number of actions returned.
// ListActionOptions may be used to filter on specific Action values. The return
// values are the list of actions, the last index and the total count (iff
// query.CountTotal is set).
//
// NOTE: This is part of the ActionDB interface.
func (s *SQLDB) ListActions(ctx context.Context,
query *ListActionsQuery, options ...ListActionOption) ([]*Action,
uint64, uint64, error) {
opts := newListActionOptions()
for _, o := range options {
o(opts)
}
var (
readTxOpts = db.NewQueryReadTx()
actions []*Action
lastIndex uint64
totalCount int64
)
err := s.db.ExecTx(ctx, &readTxOpts, func(db SQLQueries) error {
var (
actorName = sql.NullString{
String: opts.actorName,
Valid: opts.actorName != "",
}
feature = sql.NullString{
String: opts.featureName,
Valid: opts.featureName != "",
}
rpcMethod = sql.NullString{
String: opts.methodName,
Valid: opts.methodName != "",
}
actionState = sql.NullInt16{
Int16: int16(opts.state),
Valid: opts.state != 0,
}
startTime = sql.NullTime{
Time: opts.startTime,
Valid: !opts.startTime.IsZero(),
}
endTime = sql.NullTime{
Time: opts.endTime,
Valid: !opts.endTime.IsZero(),
}
)
var sessionID sql.NullInt64
if opts.sessionID != session.EmptyID {
sID, err := db.GetSessionIDByAlias(
ctx, opts.sessionID[:],
)
if errors.Is(err, sql.ErrNoRows) {
return session.ErrSessionNotFound
} else if err != nil {
return fmt.Errorf("unable to get DB ID for "+
"legacy session ID %x: %w",
opts.sessionID, err)
}
sessionID = sqldb.SQLInt64(sID)
}
var groupID sql.NullInt64
if opts.groupID != session.EmptyID {
gID, err := db.GetSessionIDByAlias(ctx, opts.groupID[:])
if errors.Is(err, sql.ErrNoRows) {
return session.ErrUnknownGroup
} else if err != nil {
return fmt.Errorf("unable to get DB ID for "+
"legacy group ID %x: %w", opts.groupID,
err)
}
groupID = sqldb.SQLInt64(gID)
}
var (
dbActions []sqlc.Action
err error
)
actionQueryParams := sqlc.ActionQueryParams{
SessionID: sessionID,
GroupID: groupID,
FeatureName: feature,
ActorName: actorName,
RpcMethod: rpcMethod,
State: actionState,
EndTime: endTime,
StartTime: startTime,
}
queryParams := sqlc.ListActionsParams{
ActionQueryParams: actionQueryParams,
Reversed: false,
}
if query != nil {
queryParams.Reversed = query.Reversed
queryParams.Pagination = &sqlc.Pagination{
NumLimit: func() int32 {
if query.MaxNum == 0 {
return int32(math.MaxInt32)
}
return int32(query.MaxNum)
}(),
NumOffset: int32(query.IndexOffset),
}
}
dbActions, err = db.ListActions(ctx, queryParams)
if err != nil {
return fmt.Errorf("unable to list actions: %w", err)
}
// If pagination was used, then the number of results returned
// won't necessarily match the total number of actions that
// match the query. So, if pagination was used and the CountAll
// flag is set, then we need to count the total number of
// actions that match the query.
if query != nil && query.CountAll {
totalCount, err = db.CountActions(
ctx, actionQueryParams,
)
if err != nil {
return fmt.Errorf("unable to count actions: %w",
err)
}
}
actions = make([]*Action, len(dbActions))
for i, dbAction := range dbActions {
action, err := unmarshalAction(ctx, db, dbAction)
if err != nil {
return fmt.Errorf("unable to unmarshal "+
"action: %w", err)
}
actions[i] = action
lastIndex = uint64(dbAction.ID)
}
return nil
}, sqldb.NoOpReset)
return actions, lastIndex, uint64(totalCount), err
}
func unmarshalAction(ctx context.Context, db SQLActionQueries,
dbAction sqlc.Action) (*Action, error) {
var legacySessID fn.Option[session.ID]
if dbAction.SessionID.Valid {
legacySessIDB, err := db.GetAliasBySessionID(
ctx, dbAction.SessionID.Int64,
)
if err != nil {
return nil, fmt.Errorf("unable to get legacy "+
"session ID for session ID %d: %w",
dbAction.SessionID.Int64, err)
}
sessID, err := session.IDFromBytes(legacySessIDB)
if err != nil {
return nil, err
}
legacySessID = fn.Some(sessID)
}
var legacyAcctID fn.Option[accounts.AccountID]
if dbAction.AccountID.Valid {
acct, err := db.GetAccount(ctx, dbAction.AccountID.Int64)
if err != nil {
return nil, err
}
acctID, err := accounts.AccountIDFromInt64(acct.Alias)
if err != nil {
return nil, fmt.Errorf("unable to get account ID: %w",
err)
}
legacyAcctID = fn.Some(acctID)
}
// Note that we export the full 8 byte macaroon root key ID in the sql
// actions DB, while the kvdb version persists and exports stored the
// last 4 bytes only.
var macRootKeyID fn.Option[uint64]
if len(dbAction.MacaroonIdentifier) >= 8 {
macRootKeyID = fn.Some(
binary.BigEndian.Uint64(dbAction.MacaroonIdentifier),
)
}
return &Action{
AddActionReq: AddActionReq{
MacaroonRootKeyID: macRootKeyID,
AccountID: legacyAcctID,
SessionID: legacySessID,
ActorName: dbAction.ActorName.String,
FeatureName: dbAction.FeatureName.String,
Trigger: dbAction.ActionTrigger.String,
Intent: dbAction.Intent.String,
StructuredJsonData: string(dbAction.StructuredJsonData),
RPCMethod: dbAction.RpcMethod,
RPCParamsJson: dbAction.RpcParamsJson,
},
AttemptedAt: dbAction.CreatedAt,
State: ActionState(dbAction.ActionState),
ErrorReason: dbAction.ErrorReason.String,
}, nil
}