scripting: fix LND RPC authentication using lndclient wrappers

This commit fixes LND RPC authentication by switching from raw gRPC
clients to lndclient wrappers that provide proper macaroon authentication.

Changes:
- Update LNDClients to use lndclient.LightningClient and
  lndclient.RouterClient instead of raw gRPC clients
- Add getRawLightningClient() and getRawRouterClient() helpers that use
  RawClientWithMacAuth() for authenticated raw client access
- Update all 21 LND builtin methods to use the new pattern
- Add InMemoryStore implementation for testing
- Wire up LND clients in terminal.go after LND connects
- Fix integration test function signatures

All 7 scripts integration tests now pass:
- scripts_basic_crud
- scripts_validation
- scripts_execution
- scripts_kv_store
- scripts_lnd_access
- scripts_builtins
- scripts_kv_builtins
This commit is contained in:
sputn1ck 2026-01-30 23:27:15 +01:00
parent 7b9c2419e7
commit 43df8e2d7d
7 changed files with 292 additions and 104 deletions

View file

@ -15,8 +15,9 @@ import (
)
// testScriptBasicCRUD tests basic script create, read, update, delete operations.
func testScriptBasicCRUD(t *harnessTest, net *NetworkHarness) {
ctx := context.Background()
func testScriptBasicCRUD(ctx context.Context, net *NetworkHarness,
t *harnessTest) {
ctxt, cancel := context.WithTimeout(ctx, defaultTimeout)
defer cancel()
@ -98,8 +99,9 @@ def main():
}
// testScriptValidation tests script syntax validation.
func testScriptValidation(t *harnessTest, net *NetworkHarness) {
ctx := context.Background()
func testScriptValidation(ctx context.Context, net *NetworkHarness,
t *harnessTest) {
ctxt, cancel := context.WithTimeout(ctx, defaultTimeout)
defer cancel()
@ -148,8 +150,9 @@ def helper():
}
// testScriptExecution tests basic script execution.
func testScriptExecution(t *harnessTest, net *NetworkHarness) {
ctx := context.Background()
func testScriptExecution(ctx context.Context, net *NetworkHarness,
t *harnessTest) {
ctxt, cancel := context.WithTimeout(ctx, defaultTimeout)
defer cancel()
@ -220,8 +223,9 @@ def main(x=10, y=20):
}
// testScriptKVStore tests the script KV store operations.
func testScriptKVStore(t *harnessTest, net *NetworkHarness) {
ctx := context.Background()
func testScriptKVStore(ctx context.Context, net *NetworkHarness,
t *harnessTest) {
ctxt, cancel := context.WithTimeout(ctx, defaultTimeout)
defer cancel()
@ -298,8 +302,9 @@ func testScriptKVStore(t *harnessTest, net *NetworkHarness) {
}
// testScriptWithLNDAccess tests scripts that access LND RPCs.
func testScriptWithLNDAccess(t *harnessTest, net *NetworkHarness) {
ctx := context.Background()
func testScriptWithLNDAccess(ctx context.Context, net *NetworkHarness,
t *harnessTest) {
ctxt, cancel := context.WithTimeout(ctx, defaultTimeout)
defer cancel()
@ -350,8 +355,9 @@ def main():
}
// testScriptBuiltins tests the built-in functions.
func testScriptBuiltins(t *harnessTest, net *NetworkHarness) {
ctx := context.Background()
func testScriptBuiltins(ctx context.Context, net *NetworkHarness,
t *harnessTest) {
ctxt, cancel := context.WithTimeout(ctx, defaultTimeout)
defer cancel()
@ -411,8 +417,9 @@ def main():
}
// testScriptWithKVBuiltins tests scripts using built-in KV functions.
func testScriptWithKVBuiltins(t *harnessTest, net *NetworkHarness) {
ctx := context.Background()
func testScriptWithKVBuiltins(ctx context.Context, net *NetworkHarness,
t *harnessTest) {
ctxt, cancel := context.WithTimeout(ctx, defaultTimeout)
defer cancel()

View file

@ -5,19 +5,17 @@ import (
"encoding/hex"
"fmt"
"github.com/lightninglabs/lndclient"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"go.starlark.net/starlark"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
// LNDClients holds all the LND RPC clients needed for script execution.
// These are the lndclient wrappers that provide authenticated access.
type LNDClients struct {
Lightning lnrpc.LightningClient
Router routerrpc.RouterClient
Invoices invoicesrpc.InvoicesClient
Lightning lndclient.LightningClient
Router lndclient.RouterClient
}
// lndBuiltins provides LND RPC access to Starlark scripts.
@ -82,14 +80,20 @@ func (e *Engine) registerLNDBuiltins(predeclared starlark.StringDict, clients *L
predeclared["lnd"] = lndModule.Struct()
}
// contextWithMacaroon creates a context with the script's macaroon.
func (lb *lndBuiltins) contextWithMacaroon() context.Context {
ctx := lb.engine.sandbox.Context()
if lb.macaroon != "" {
md := metadata.Pairs("macaroon", lb.macaroon)
ctx = metadata.NewOutgoingContext(ctx, md)
}
return ctx
// getRawLightningClient returns an authenticated context and the raw
// Lightning client for making RPC calls.
func (lb *lndBuiltins) getRawLightningClient() (context.Context, lnrpc.LightningClient) {
parentCtx := lb.engine.sandbox.Context()
ctx, _, client := lb.clients.Lightning.RawClientWithMacAuth(parentCtx)
return ctx, client
}
// getRawRouterClient returns an authenticated context and the raw
// Router client for making RPC calls.
func (lb *lndBuiltins) getRawRouterClient() (context.Context, routerrpc.RouterClient) {
parentCtx := lb.engine.sandbox.Context()
ctx, _, client := lb.clients.Router.RawClientWithMacAuth(parentCtx)
return ctx, client
}
// getInfo implements lnd.get_info().
@ -100,23 +104,23 @@ func (lb *lndBuiltins) getInfo(thread *starlark.Thread, fn *starlark.Builtin,
return nil, err
}
ctx := lb.contextWithMacaroon()
resp, err := lb.clients.Lightning.GetInfo(ctx, &lnrpc.GetInfoRequest{})
ctx, client := lb.getRawLightningClient()
resp, err := client.GetInfo(ctx, &lnrpc.GetInfoRequest{})
if err != nil {
return nil, fmt.Errorf("get_info failed: %w", err)
}
return lb.protoToDict(map[string]any{
"identity_pubkey": resp.IdentityPubkey,
"alias": resp.Alias,
"num_active_channels": resp.NumActiveChannels,
"identity_pubkey": resp.IdentityPubkey,
"alias": resp.Alias,
"num_active_channels": resp.NumActiveChannels,
"num_inactive_channels": resp.NumInactiveChannels,
"num_pending_channels": resp.NumPendingChannels,
"num_peers": resp.NumPeers,
"block_height": resp.BlockHeight,
"synced_to_chain": resp.SyncedToChain,
"synced_to_graph": resp.SyncedToGraph,
"version": resp.Version,
"num_pending_channels": resp.NumPendingChannels,
"num_peers": resp.NumPeers,
"block_height": resp.BlockHeight,
"synced_to_chain": resp.SyncedToChain,
"synced_to_graph": resp.SyncedToGraph,
"version": resp.Version,
})
}
@ -131,8 +135,8 @@ func (lb *lndBuiltins) getNodeInfo(thread *starlark.Thread, fn *starlark.Builtin
return nil, err
}
ctx := lb.contextWithMacaroon()
resp, err := lb.clients.Lightning.GetNodeInfo(ctx, &lnrpc.NodeInfoRequest{
ctx, client := lb.getRawLightningClient()
resp, err := client.GetNodeInfo(ctx, &lnrpc.NodeInfoRequest{
PubKey: pubkey,
IncludeChannels: includeChannels,
})
@ -170,8 +174,8 @@ func (lb *lndBuiltins) listChannels(thread *starlark.Thread, fn *starlark.Builti
return nil, err
}
ctx := lb.contextWithMacaroon()
resp, err := lb.clients.Lightning.ListChannels(ctx, &lnrpc.ListChannelsRequest{
ctx, client := lb.getRawLightningClient()
resp, err := client.ListChannels(ctx, &lnrpc.ListChannelsRequest{
ActiveOnly: activeOnly,
InactiveOnly: inactiveOnly,
PublicOnly: publicOnly,
@ -211,8 +215,8 @@ func (lb *lndBuiltins) channelBalance(thread *starlark.Thread, fn *starlark.Buil
return nil, err
}
ctx := lb.contextWithMacaroon()
resp, err := lb.clients.Lightning.ChannelBalance(ctx, &lnrpc.ChannelBalanceRequest{})
ctx, client := lb.getRawLightningClient()
resp, err := client.ChannelBalance(ctx, &lnrpc.ChannelBalanceRequest{})
if err != nil {
return nil, fmt.Errorf("channel_balance failed: %w", err)
}
@ -235,8 +239,8 @@ func (lb *lndBuiltins) pendingChannels(thread *starlark.Thread, fn *starlark.Bui
return nil, err
}
ctx := lb.contextWithMacaroon()
resp, err := lb.clients.Lightning.PendingChannels(ctx, &lnrpc.PendingChannelsRequest{})
ctx, client := lb.getRawLightningClient()
resp, err := client.PendingChannels(ctx, &lnrpc.PendingChannelsRequest{})
if err != nil {
return nil, fmt.Errorf("pending_channels failed: %w", err)
}
@ -258,8 +262,8 @@ func (lb *lndBuiltins) closedChannels(thread *starlark.Thread, fn *starlark.Buil
return nil, err
}
ctx := lb.contextWithMacaroon()
resp, err := lb.clients.Lightning.ClosedChannels(ctx, &lnrpc.ClosedChannelsRequest{})
ctx, client := lb.getRawLightningClient()
resp, err := client.ClosedChannels(ctx, &lnrpc.ClosedChannelsRequest{})
if err != nil {
return nil, fmt.Errorf("closed_channels failed: %w", err)
}
@ -314,8 +318,8 @@ func (lb *lndBuiltins) updateChannelPolicy(thread *starlark.Thread, fn *starlark
req.Scope = &lnrpc.PolicyUpdateRequest_Global{Global: true}
}
ctx := lb.contextWithMacaroon()
resp, err := lb.clients.Lightning.UpdateChannelPolicy(ctx, req)
ctx, client := lb.getRawLightningClient()
resp, err := client.UpdateChannelPolicy(ctx, req)
if err != nil {
return nil, fmt.Errorf("update_channel_policy failed: %w", err)
}
@ -338,8 +342,8 @@ func (lb *lndBuiltins) walletBalance(thread *starlark.Thread, fn *starlark.Built
return nil, err
}
ctx := lb.contextWithMacaroon()
resp, err := lb.clients.Lightning.WalletBalance(ctx, &lnrpc.WalletBalanceRequest{})
ctx, client := lb.getRawLightningClient()
resp, err := client.WalletBalance(ctx, &lnrpc.WalletBalanceRequest{})
if err != nil {
return nil, fmt.Errorf("wallet_balance failed: %w", err)
}
@ -366,8 +370,8 @@ func (lb *lndBuiltins) listUnspent(thread *starlark.Thread, fn *starlark.Builtin
maxConfs = int32Max
}
ctx := lb.contextWithMacaroon()
resp, err := lb.clients.Lightning.ListUnspent(ctx, &lnrpc.ListUnspentRequest{
ctx, client := lb.getRawLightningClient()
resp, err := client.ListUnspent(ctx, &lnrpc.ListUnspentRequest{
MinConfs: int32(minConfs),
MaxConfs: int32(maxConfs),
})
@ -410,8 +414,8 @@ func (lb *lndBuiltins) newAddress(thread *starlark.Thread, fn *starlark.Builtin,
return nil, fmt.Errorf("unknown address type: %s", addrType)
}
ctx := lb.contextWithMacaroon()
resp, err := lb.clients.Lightning.NewAddress(ctx, &lnrpc.NewAddressRequest{
ctx, client := lb.getRawLightningClient()
resp, err := client.NewAddress(ctx, &lnrpc.NewAddressRequest{
Type: at,
})
if err != nil {
@ -437,8 +441,8 @@ func (lb *lndBuiltins) sendCoins(thread *starlark.Thread, fn *starlark.Builtin,
return nil, err
}
ctx := lb.contextWithMacaroon()
resp, err := lb.clients.Lightning.SendCoins(ctx, &lnrpc.SendCoinsRequest{
ctx, client := lb.getRawLightningClient()
resp, err := client.SendCoins(ctx, &lnrpc.SendCoinsRequest{
Addr: addr,
Amount: amount,
SatPerVbyte: uint64(satPerVbyte),
@ -467,8 +471,8 @@ func (lb *lndBuiltins) addInvoice(thread *starlark.Thread, fn *starlark.Builtin,
expiry = 3600 // 1 hour default
}
ctx := lb.contextWithMacaroon()
resp, err := lb.clients.Lightning.AddInvoice(ctx, &lnrpc.Invoice{
ctx, client := lb.getRawLightningClient()
resp, err := client.AddInvoice(ctx, &lnrpc.Invoice{
Memo: memo,
Value: valueSat,
Expiry: expiry,
@ -499,8 +503,8 @@ func (lb *lndBuiltins) lookupInvoice(thread *starlark.Thread, fn *starlark.Built
return nil, fmt.Errorf("invalid r_hash: %w", err)
}
ctx := lb.contextWithMacaroon()
resp, err := lb.clients.Lightning.LookupInvoice(ctx, &lnrpc.PaymentHash{
ctx, client := lb.getRawLightningClient()
resp, err := client.LookupInvoice(ctx, &lnrpc.PaymentHash{
RHash: rHash,
})
if err != nil {
@ -528,8 +532,8 @@ func (lb *lndBuiltins) listInvoices(thread *starlark.Thread, fn *starlark.Builti
numMaxInvoices = 100
}
ctx := lb.contextWithMacaroon()
resp, err := lb.clients.Lightning.ListInvoices(ctx, &lnrpc.ListInvoiceRequest{
ctx, client := lb.getRawLightningClient()
resp, err := client.ListInvoices(ctx, &lnrpc.ListInvoiceRequest{
PendingOnly: pendingOnly,
IndexOffset: uint64(indexOffset),
NumMaxInvoices: uint64(numMaxInvoices),
@ -561,8 +565,8 @@ func (lb *lndBuiltins) decodePayReq(thread *starlark.Thread, fn *starlark.Builti
return nil, err
}
ctx := lb.contextWithMacaroon()
resp, err := lb.clients.Lightning.DecodePayReq(ctx, &lnrpc.PayReqString{
ctx, client := lb.getRawLightningClient()
resp, err := client.DecodePayReq(ctx, &lnrpc.PayReqString{
PayReq: payReq,
})
if err != nil {
@ -614,8 +618,8 @@ func (lb *lndBuiltins) sendPayment(thread *starlark.Thread, fn *starlark.Builtin
req.Amt = amtSat
}
ctx := lb.contextWithMacaroon()
stream, err := lb.clients.Router.SendPaymentV2(ctx, req)
ctx, routerClient := lb.getRawRouterClient()
stream, err := routerClient.SendPaymentV2(ctx, req)
if err != nil {
return nil, fmt.Errorf("send_payment failed: %w", err)
}
@ -672,8 +676,8 @@ func (lb *lndBuiltins) listPayments(thread *starlark.Thread, fn *starlark.Builti
maxPayments = 100
}
ctx := lb.contextWithMacaroon()
resp, err := lb.clients.Lightning.ListPayments(ctx, &lnrpc.ListPaymentsRequest{
ctx, client := lb.getRawLightningClient()
resp, err := client.ListPayments(ctx, &lnrpc.ListPaymentsRequest{
IncludeIncomplete: includeIncomplete,
IndexOffset: uint64(indexOffset),
MaxPayments: uint64(maxPayments),
@ -709,8 +713,8 @@ func (lb *lndBuiltins) listPeers(thread *starlark.Thread, fn *starlark.Builtin,
return nil, err
}
ctx := lb.contextWithMacaroon()
resp, err := lb.clients.Lightning.ListPeers(ctx, &lnrpc.ListPeersRequest{})
ctx, client := lb.getRawLightningClient()
resp, err := client.ListPeers(ctx, &lnrpc.ListPeersRequest{})
if err != nil {
return nil, fmt.Errorf("list_peers failed: %w", err)
}
@ -743,8 +747,8 @@ func (lb *lndBuiltins) connectPeer(thread *starlark.Thread, fn *starlark.Builtin
return nil, err
}
ctx := lb.contextWithMacaroon()
_, err := lb.clients.Lightning.ConnectPeer(ctx, &lnrpc.ConnectPeerRequest{
ctx, client := lb.getRawLightningClient()
_, err := client.ConnectPeer(ctx, &lnrpc.ConnectPeerRequest{
Addr: &lnrpc.LightningAddress{
Pubkey: pubkey,
Host: host,
@ -767,8 +771,8 @@ func (lb *lndBuiltins) disconnectPeer(thread *starlark.Thread, fn *starlark.Buil
return nil, err
}
ctx := lb.contextWithMacaroon()
_, err := lb.clients.Lightning.DisconnectPeer(ctx, &lnrpc.DisconnectPeerRequest{
ctx, client := lb.getRawLightningClient()
_, err := client.DisconnectPeer(ctx, &lnrpc.DisconnectPeerRequest{
PubKey: pubkey,
})
if err != nil {
@ -797,8 +801,8 @@ func (lb *lndBuiltins) forwardingHistory(thread *starlark.Thread, fn *starlark.B
numMaxEvents = 100
}
ctx := lb.contextWithMacaroon()
resp, err := lb.clients.Lightning.ForwardingHistory(ctx, &lnrpc.ForwardingHistoryRequest{
ctx, client := lb.getRawLightningClient()
resp, err := client.ForwardingHistory(ctx, &lnrpc.ForwardingHistoryRequest{
StartTime: uint64(startTime),
EndTime: uint64(endTime),
IndexOffset: uint32(indexOffset),
@ -843,8 +847,8 @@ func (lb *lndBuiltins) estimateFee(thread *starlark.Thread, fn *starlark.Builtin
targetConf = 6
}
ctx := lb.contextWithMacaroon()
resp, err := lb.clients.Lightning.EstimateFee(ctx, &lnrpc.EstimateFeeRequest{
ctx, client := lb.getRawLightningClient()
resp, err := client.EstimateFee(ctx, &lnrpc.EstimateFeeRequest{
AddrToAmount: map[string]int64{
"bcrt1qw508d6qejxtdg4y5r3zarvary0c5xw7kygt080": 10000,
},
@ -891,11 +895,10 @@ func (e *Engine) SetLNDClients(clients *LNDClients) {
e.registerLNDBuiltins(e.predeclared, clients)
}
// NewLNDClientsFromConn creates LND clients from a gRPC connection.
func NewLNDClientsFromConn(conn *grpc.ClientConn) *LNDClients {
// NewLNDClientsFromServices creates LND clients from lndclient services.
func NewLNDClientsFromServices(services *lndclient.LndServices) *LNDClients {
return &LNDClients{
Lightning: lnrpc.NewLightningClient(conn),
Router: routerrpc.NewRouterClient(conn),
Invoices: invoicesrpc.NewInvoicesClient(conn),
Lightning: services.Client,
Router: services.Router,
}
}

View file

@ -48,6 +48,9 @@ type EngineConfig struct {
// OutputCallback is called when the script produces output.
OutputCallback OutputCallback
// LNDClients provides access to LND RPCs.
LNDClients *LNDClients
}
// Engine executes Starlark scripts in a sandboxed environment.
@ -62,6 +65,7 @@ type Engine struct {
kvStore KVStore
rpcCaller RPCCaller
outputCallback OutputCallback
lndClients *LNDClients
// thread is the Starlark execution thread.
thread *starlark.Thread
@ -96,6 +100,7 @@ func NewEngine(ctx context.Context, cfg EngineConfig) *Engine {
kvStore: cfg.KVStore,
rpcCaller: cfg.RPCCaller,
outputCallback: cfg.OutputCallback,
lndClients: cfg.LNDClients,
subscriptionHandlers: make(map[string]starlark.Callable),
}
@ -104,6 +109,7 @@ func NewEngine(ctx context.Context, cfg EngineConfig) *Engine {
e.registerStandardBuiltins(e.predeclared)
e.registerHTTPBuiltins(e.predeclared)
e.registerKVBuiltins(e.predeclared)
e.registerLNDBuiltins(e.predeclared, cfg.LNDClients)
return e
}

View file

@ -20,10 +20,11 @@ type MacaroonBaker interface {
// Manager handles script CRUD operations and macaroon management.
type Manager struct {
store Store
kvStore KVStore
macBaker MacaroonBaker
rpcCaller RPCCaller
store Store
kvStore KVStore
macBaker MacaroonBaker
rpcCaller RPCCaller
lndClients *LNDClients
// runners tracks currently running scripts.
runners map[string]*Runner
@ -41,6 +42,17 @@ func NewManager(store Store, kvStore KVStore, macBaker MacaroonBaker, rpcCaller
}
}
// SetMacaroonBaker sets the macaroon baker after initialization.
// This is used when the baker needs to be set after LND connects.
func (m *Manager) SetMacaroonBaker(baker MacaroonBaker) {
m.macBaker = baker
}
// SetLNDClients sets the LND clients for script RPC access.
func (m *Manager) SetLNDClients(clients *LNDClients) {
m.lndClients = clients
}
// CreateScript creates a new script with the specified permissions.
func (m *Manager) CreateScript(ctx context.Context, req *litrpc.CreateScriptRequest) (*Script, error) {
// Validate the source.
@ -247,7 +259,7 @@ func (m *Manager) StartScript(ctx context.Context, name string, argsJSON string)
}
// Create and start runner.
runner := NewRunner(m, script, exec.ID, m.kvStore, m.rpcCaller)
runner := NewRunner(m, script, exec.ID, m.kvStore, m.rpcCaller, m.lndClients)
m.runnersMu.Lock()
m.runners[name] = runner

View file

@ -12,6 +12,7 @@ type Runner struct {
executionID int64
kvStore KVStore
rpcCaller RPCCaller
lndClients *LNDClients
engine *Engine
ctx context.Context
@ -29,7 +30,7 @@ type ScriptOutput struct {
}
// NewRunner creates a new script runner.
func NewRunner(manager *Manager, script *Script, executionID int64, kvStore KVStore, rpcCaller RPCCaller) *Runner {
func NewRunner(manager *Manager, script *Script, executionID int64, kvStore KVStore, rpcCaller RPCCaller, lndClients *LNDClients) *Runner {
ctx, cancel := context.WithCancel(context.Background())
return &Runner{
@ -38,6 +39,7 @@ func NewRunner(manager *Manager, script *Script, executionID int64, kvStore KVSt
executionID: executionID,
kvStore: kvStore,
rpcCaller: rpcCaller,
lndClients: lndClients,
ctx: ctx,
cancel: cancel,
outputs: make([]ScriptOutput, 0),
@ -67,8 +69,9 @@ func (r *Runner) Run(ctx context.Context, args map[string]interface{}) *Executio
MaxMemoryBytes: r.script.MaxMemoryBytes,
TimeoutSecs: r.script.TimeoutSecs,
},
KVStore: r.kvStore,
RPCCaller: r.rpcCaller,
KVStore: r.kvStore,
RPCCaller: r.rpcCaller,
LNDClients: r.lndClients,
OutputCallback: func(level, message string) {
r.mu.Lock()
r.outputs = append(r.outputs, ScriptOutput{

View file

@ -2,6 +2,7 @@ package scripting
import (
"context"
"fmt"
"time"
)
@ -94,6 +95,154 @@ const (
StateStopped = "stopped"
)
// InMemoryStore implements Store with in-memory storage.
// This is useful for testing and development.
type InMemoryStore struct {
scripts map[string]*Script
executions []*ScriptExecution
running map[int64]*RunningScript
nextID int64
nextExecID int64
}
// NewInMemoryStore creates a new in-memory script store.
func NewInMemoryStore() *InMemoryStore {
return &InMemoryStore{
scripts: make(map[string]*Script),
running: make(map[int64]*RunningScript),
nextID: 1,
}
}
func (s *InMemoryStore) CreateScript(ctx context.Context, script *Script) error {
if _, exists := s.scripts[script.Name]; exists {
return fmt.Errorf("script %s already exists", script.Name)
}
script.ID = s.nextID
s.nextID++
s.scripts[script.Name] = script
return nil
}
func (s *InMemoryStore) UpdateScript(ctx context.Context, script *Script) error {
if _, exists := s.scripts[script.Name]; !exists {
return fmt.Errorf("script %s not found", script.Name)
}
s.scripts[script.Name] = script
return nil
}
func (s *InMemoryStore) DeleteScript(ctx context.Context, name string) error {
delete(s.scripts, name)
return nil
}
func (s *InMemoryStore) GetScript(ctx context.Context, name string) (*Script, error) {
script, exists := s.scripts[name]
if !exists {
return nil, fmt.Errorf("script %s not found", name)
}
return script, nil
}
func (s *InMemoryStore) GetScriptByID(ctx context.Context, id int64) (*Script, error) {
for _, script := range s.scripts {
if script.ID == id {
return script, nil
}
}
return nil, fmt.Errorf("script with ID %d not found", id)
}
func (s *InMemoryStore) ListScripts(ctx context.Context) ([]*Script, error) {
result := make([]*Script, 0, len(s.scripts))
for _, script := range s.scripts {
result = append(result, script)
}
return result, nil
}
func (s *InMemoryStore) CreateExecution(ctx context.Context, exec *ScriptExecution) error {
s.nextExecID++
exec.ID = s.nextExecID
s.executions = append(s.executions, exec)
return nil
}
func (s *InMemoryStore) UpdateExecution(ctx context.Context, exec *ScriptExecution) error {
for i, e := range s.executions {
if e.ID == exec.ID {
s.executions[i] = exec
return nil
}
}
return fmt.Errorf("execution %d not found", exec.ID)
}
func (s *InMemoryStore) GetExecution(ctx context.Context, id int64) (*ScriptExecution, error) {
for _, e := range s.executions {
if e.ID == id {
return e, nil
}
}
return nil, fmt.Errorf("execution %d not found", id)
}
func (s *InMemoryStore) ListExecutions(ctx context.Context, scriptName string, limit, offset uint32) ([]*ScriptExecution, error) {
var result []*ScriptExecution
for _, e := range s.executions {
if scriptName == "" || e.ScriptName == scriptName {
result = append(result, e)
}
}
// Simple pagination
start := int(offset)
if start >= len(result) {
return nil, nil
}
end := start + int(limit)
if end > len(result) {
end = len(result)
}
return result[start:end], nil
}
func (s *InMemoryStore) MarkRunning(ctx context.Context, scriptID, executionID int64) error {
script, err := s.GetScriptByID(ctx, scriptID)
if err != nil {
return err
}
s.running[scriptID] = &RunningScript{
ScriptID: scriptID,
ScriptName: script.Name,
ExecutionID: executionID,
StartedAt: time.Now(),
}
return nil
}
func (s *InMemoryStore) MarkStopped(ctx context.Context, scriptID int64) error {
delete(s.running, scriptID)
return nil
}
func (s *InMemoryStore) GetRunningScripts(ctx context.Context) ([]*RunningScript, error) {
result := make([]*RunningScript, 0, len(s.running))
for _, r := range s.running {
result = append(result, r)
}
return result, nil
}
func (s *InMemoryStore) IsScriptRunning(ctx context.Context, scriptID int64) (bool, error) {
_, exists := s.running[scriptID]
return exists, nil
}
func (s *InMemoryStore) Close() error {
return nil
}
// Store defines the interface for script storage.
type Store interface {
// CreateScript creates a new script.

View file

@ -496,6 +496,18 @@ func (g *LightningTerminal) start(ctx context.Context) error {
// to understand why this is necessary.
g.sessionRpcServer = newSessionRPCServer()
// Initialize the scripts service early. The full dependencies (like the
// macaroon baker) will be set after LND connects.
scriptStore := scripting.NewInMemoryStore()
scriptKVStore := scripting.NewInMemoryKVStore()
g.scriptManager = scripting.NewManager(
scriptStore,
scriptKVStore,
nil, // Macaroon baker will be set after LND connects
nil, // RPC caller not yet implemented
)
g.scriptRpcServer = scripting.NewRPCServer(g.scriptManager, scriptKVStore)
// Call the "real" main in a nested manner so the defers will properly
// be executed in the case of a graceful shutdown.
var (
@ -1058,18 +1070,14 @@ func (g *LightningTerminal) startInternalSubServers(ctx context.Context,
g.accountRpcServer.Start(g.accountService, superMacBaker)
// Initialize the scripts service with an in-memory KV store.
// TODO: Replace with persistent storage when BoltDB/SQL store is ready.
// Set the macaroon baker and LND clients for the scripts service now
// that LND is connected. The script manager was initialized early
// before LND setup.
log.Infof("Starting LiT scripts server")
scriptKVStore := scripting.NewInMemoryKVStore()
macBaker := scripting.NewLndMacaroonBaker(g.basicClient)
g.scriptManager = scripting.NewManager(
nil, // Store will be set when persistent storage is implemented
scriptKVStore,
macBaker,
nil, // RPC caller not yet implemented
)
g.scriptRpcServer = scripting.NewRPCServer(g.scriptManager, scriptKVStore)
g.scriptManager.SetMacaroonBaker(macBaker)
lndClients := scripting.NewLNDClientsFromServices(&g.lndClient.LndServices)
g.scriptManager.SetLNDClients(lndClients)
if !g.cfg.Autopilot.Disable {
withLndVersion := func(cfg *autopilotserver.Config) {