Skip to content
Open
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
71 changes: 71 additions & 0 deletions cmd/loop/liquidity.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"errors"
"fmt"
"os"
"strconv"
"strings"

Expand Down Expand Up @@ -390,6 +391,34 @@ var setParamsCommand = &cli.Command{
"save on chain fees. Not setting this flag " +
"therefore might result in a lower swap fees",
},
&cli.BoolFlag{
Name: "scriptautoloop",
Usage: "set to true to enable scriptable autoloop " +
"using Starlark scripts. This allows custom " +
"swap logic with variables, functions, loops, " +
"and sorting. Mutually exclusive with " +
"easyautoloop and threshold rules.",
Comment on lines +395 to +400
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The flag name 'scriptautoloop' is inconsistent with the naming pattern used elsewhere in the codebase and the PR. Throughout the protobuf definitions, Go structs, and documentation, the feature is called 'scriptable_autoloop' (with 'able'). The CLI flag should be '--scriptableautoloop' to match this convention and make the feature name consistent across the entire codebase.

Copilot uses AI. Check for mistakes.
},
&cli.StringFlag{
Name: "scriptfile",
Usage: "path to a Starlark script file for " +
"scriptable autoloop. The script must set " +
"a 'decisions' variable to a list of swaps. " +
"Recommended for production use as it allows " +
"version control and proper editing.",
},
&cli.StringFlag{
Name: "script",
Usage: "inline Starlark script for scriptable " +
"autoloop. For simple scripts only; use " +
"--scriptfile for complex scripts.",
},
&cli.Uint64Flag{
Name: "scripttickinterval",
Usage: "custom tick interval in seconds for " +
"scriptable autoloop. If not set, uses " +
"the default 20-minute interval.",
},
},
Action: setParams,
}
Expand Down Expand Up @@ -637,6 +666,48 @@ func setParams(ctx context.Context, cmd *cli.Command) error {
params.FastSwapPublication = true
}

// Handle scriptable autoloop flags.
if cmd.IsSet("scriptautoloop") {
params.ScriptableAutoloop = cmd.Bool("scriptautoloop")
flagSet = true
}

// Handle script content from file or inline.
scriptFileSet := cmd.IsSet("scriptfile")
scriptInlineSet := cmd.IsSet("script")

if scriptFileSet && scriptInlineSet {
return fmt.Errorf("cannot set both --scriptfile and --script; " +
"use one or the other")
}

if scriptFileSet {
scriptPath := cmd.String("scriptfile")
scriptBytes, err := os.ReadFile(scriptPath)
if err != nil {
return fmt.Errorf("failed to read script file %s: %w",
scriptPath, err)
}
params.ScriptableScript = string(scriptBytes)
flagSet = true
}

if scriptInlineSet {
params.ScriptableScript = cmd.String("script")
flagSet = true
}

if cmd.IsSet("scripttickinterval") {
params.ScriptableTickIntervalSec = cmd.Uint64("scripttickinterval")
flagSet = true
}

// Validate that scriptable autoloop is not used with easyautoloop.
if params.ScriptableAutoloop && params.EasyAutoloop {
return fmt.Errorf("scriptable autoloop cannot be used with " +
"easy autoloop; disable one before enabling the other")
}

if !flagSet {
return fmt.Errorf("at least one flag required to set params")
}
Expand Down
12 changes: 12 additions & 0 deletions docs/loop.1
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,18 @@ update the parameters set for the liquidity manager
.PP
\fB--minamt\fP="": the minimum amount in satoshis that the autoloop client will dispatch per-swap. (default: 0)

.PP
\fB--script\fP="": inline Starlark script for scriptable autoloop. For simple scripts only; use --scriptfile for complex scripts.

.PP
\fB--scriptautoloop\fP: set to true to enable scriptable autoloop using Starlark scripts. This allows custom swap logic with variables, functions, loops, and sorting. Mutually exclusive with easyautoloop and threshold rules.

.PP
\fB--scriptfile\fP="": path to a Starlark script file for scriptable autoloop. The script must set a 'decisions' variable to a list of swaps. Recommended for production use as it allows version control and proper editing.

.PP
\fB--scripttickinterval\fP="": custom tick interval in seconds for scriptable autoloop. If not set, uses the default 20-minute interval. (default: 0)

.PP
\fB--sweepconf\fP="": the number of blocks from htlc height that swap suggestion sweeps should target, used to estimate max miner fee. (default: 0)

Expand Down
4 changes: 4 additions & 0 deletions docs/loop.md
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,10 @@ The following flags are supported:
| `--asset_id="…"` | If set to a valid asset ID, the easyautoloop and localbalancesat flags will be set for the specified asset | string |
| `--asset_localbalance="…"` | the target size of total local balance in asset units, used by asset easy autoloop | uint | `0` |
| `--fast` | if set new swaps are expected to be published immediately, paying a potentially higher fee. If not set the swap server might choose to wait up to 30 minutes before publishing swap HTLCs on-chain, to save on chain fees. Not setting this flag therefore might result in a lower swap fees | bool | `false` |
| `--scriptautoloop` | set to true to enable scriptable autoloop using Starlark scripts. This allows custom swap logic with variables, functions, loops, and sorting. Mutually exclusive with easyautoloop and threshold rules | bool | `false` |
| `--scriptfile="…"` | path to a Starlark script file for scriptable autoloop. The script must set a 'decisions' variable to a list of swaps. Recommended for production use as it allows version control and proper editing | string |
| `--script="…"` | inline Starlark script for scriptable autoloop. For simple scripts only; use --scriptfile for complex scripts | string |
| `--scripttickinterval="…"` | custom tick interval in seconds for scriptable autoloop. If not set, uses the default 20-minute interval | uint | `0` |
| `--help` (`-h`) | show help | bool | `false` |

### `getinfo` command
Expand Down
7 changes: 4 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ require (
github.com/urfave/cli-docs/v3 v3.1.1-0.20251020101624-bec07369b4f6
github.com/urfave/cli/v3 v3.4.1
go.etcd.io/bbolt v1.4.3
go.starlark.net v0.0.0-20260102030733-3fee463870c9
golang.org/x/sync v0.18.0
google.golang.org/grpc v1.64.1
google.golang.org/grpc v1.65.0
google.golang.org/protobuf v1.36.11
gopkg.in/macaroon-bakery.v2 v2.3.0
gopkg.in/macaroon.v2 v2.1.0
Expand Down Expand Up @@ -193,8 +194,8 @@ require (
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.38.0 // indirect
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240730163845-b1a4ccb954bf // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 // indirect
gopkg.in/errgo.v1 v1.0.1 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
Expand Down
14 changes: 8 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1386,6 +1386,8 @@ go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI
go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.starlark.net v0.0.0-20260102030733-3fee463870c9 h1:nV1OyvU+0CYrp5eKfQ3rD03TpFYYhH08z31NK1HmtTk=
go.starlark.net v0.0.0-20260102030733-3fee463870c9/go.mod h1:YKMCv9b1WrfWmeqdV5MAuEHWsu5iC+fe6kYl2sQjdI8=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
Expand Down Expand Up @@ -2033,10 +2035,10 @@ google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOl
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y=
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s=
google.golang.org/genproto/googleapis/api v0.0.0-20240730163845-b1a4ccb954bf h1:GillM0Ef0pkZPIB+5iO6SDK+4T9pf6TpaYR6ICD5rVE=
google.golang.org/genproto/googleapis/api v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:OFMYQFHJ4TM3JRlWDZhJbZfra2uqc3WLBZiaaqP4DtU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf h1:liao9UHurZLtiEwBgT9LMOnKYsHze6eA6w1KQCMVN2Q=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 h1:YcyjlL1PRr2Q17/I0dPk2JmYS5CDXfcdb2Z3YRioEbw=
google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 h1:2035KHhUv+EpyB+hWgJnaWKJOdX1E95w2S8Rr4uWKTs=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
Expand Down Expand Up @@ -2078,8 +2080,8 @@ google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5v
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA=
google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/grpc/examples v0.0.0-20210424002626-9572fd6faeae/go.mod h1:Ly7ZA/ARzg8fnPU9TyZIxoz33sEUuWX7txiqs8lPTgE=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
Expand Down
17 changes: 15 additions & 2 deletions liquidity/liquidity.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,13 +293,24 @@ func (m *Manager) Run(ctx context.Context) error {
for {
select {
case <-m.cfg.AutoloopTicker.Ticks():
if m.params.EasyAutoloop {
// Check which autoloop mode to use.
// Priority: scriptable > easy > threshold rules.
switch {
case m.params.ScriptableAutoloop:
err := m.scriptableAutoLoop(ctx)
if err != nil {
log.Errorf("scriptable autoloop "+
"failed: %v", err)
}

case m.params.EasyAutoloop:
err := m.easyAutoLoop(ctx)
if err != nil {
log.Errorf("easy autoloop failed: %v",
err)
}
} else {

default:
err := m.autoloop(ctx)
switch err {
case ErrNoRules:
Expand Down Expand Up @@ -1495,6 +1506,8 @@ func (m *Manager) refreshAutoloopBudget(ctx context.Context) {

// dispatchStickyLoopOut attempts to dispatch a loop out swap that will
// automatically retry its execution with an amount based backoff.
//
//nolint:unparam
func (m *Manager) dispatchStickyLoopOut(ctx context.Context,
out loop.OutRequest, retryCount uint16, amountBackoff float64) {

Expand Down
43 changes: 41 additions & 2 deletions liquidity/parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,18 @@ type Parameters struct {
// swaps. If set to true, the deadline is set to immediate publication.
// If set to false, the deadline is set to 30 minutes.
FastSwapPublication bool

// ScriptableAutoloop enables Starlark-based scriptable autoloop mode.
// This mode is mutually exclusive with EasyAutoloop and threshold rules.
Comment on lines +132 to +133
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment on line 132 incorrectly states this enables "CEL-based scriptable autoloop" when the feature actually uses Starlark, not CEL. The PR description explicitly explains why Starlark was chosen over CEL. This comment should say "Starlark-based scriptable autoloop" to be consistent with the actual implementation and the rest of the codebase.

Copilot uses AI. Check for mistakes.
ScriptableAutoloop bool

// ScriptableScript is the Starlark script to evaluate on each tick.
// Required when ScriptableAutoloop is true.
Comment on lines +136 to +137
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment on line 136 incorrectly refers to "CEL expression" when the field actually contains a Starlark script. This should be updated to "Starlark script" to match the actual implementation and be consistent with the field name "ScriptableScript" and all other documentation in the PR.

Suggested change
// ScriptableScript is the Starlark script to evaluate on each tick.
// Required when ScriptableAutoloop is true.
// ScriptableScript is the Starlark script source to evaluate on each tick,
// and is required when ScriptableAutoloop is true.

Copilot uses AI. Check for mistakes.
ScriptableScript string

// ScriptableTickInterval overrides the default tick interval for
// scriptable mode. Zero means use DefaultAutoloopTicker.
ScriptableTickInterval time.Duration
Comment on lines +139 to +142
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ScriptableTickInterval parameter is defined and passed through RPC but is never actually used in the implementation. The scriptableAutoLoop function doesn't use a custom ticker based on this interval - it just uses the same ticker as other autoloop modes. Either implement the custom tick interval functionality by creating a separate ticker when ScriptableTickInterval is set, or remove this unused parameter from the API to avoid misleading users.

Suggested change
// ScriptableTickInterval overrides the default tick interval for
// scriptable mode. Zero means use DefaultAutoloopTicker.
ScriptableTickInterval time.Duration

Copilot uses AI. Check for mistakes.
}

// AssetParams define the asset specific autoloop parameters.
Expand Down Expand Up @@ -195,6 +207,27 @@ func (p Parameters) haveRules() bool {
func (p Parameters) validate(minConfs int32, openChans []lndclient.ChannelInfo,
server *Restrictions) error {

// Check scriptable autoloop constraints first.
if p.ScriptableAutoloop {
// Scriptable autoloop is mutually exclusive with easy autoloop.
if p.EasyAutoloop {
return errors.New("scriptable_autoloop and easy_autoloop " +
"are mutually exclusive")
}

// Scriptable autoloop is mutually exclusive with rules.
if len(p.ChannelRules) > 0 || len(p.PeerRules) > 0 {
return errors.New("scriptable_autoloop cannot be used " +
"with channel/peer rules")
}

// Script is required when scriptable autoloop is enabled.
if p.ScriptableScript == "" {
return errors.New("scriptable_script is required when " +
"scriptable_autoloop is enabled")
}
}

// First, we check that the rules on a per peer and per channel do not
// overlap, since this could lead to contractions.
for _, channel := range openChans {
Expand Down Expand Up @@ -475,8 +508,11 @@ func RpcToParameters(req *clientrpc.LiquidityParameters) (*Parameters,
EasyAutoloopTarget: btcutil.Amount(
req.EasyAutoloopLocalTargetSat,
),
AssetAutoloopParams: easyAssetParams,
FastSwapPublication: req.FastSwapPublication,
AssetAutoloopParams: easyAssetParams,
FastSwapPublication: req.FastSwapPublication,
ScriptableAutoloop: req.ScriptableAutoloop,
ScriptableScript: req.ScriptableScript,
ScriptableTickInterval: time.Duration(req.ScriptableTickIntervalSec) * time.Second,
}

if req.AutoloopBudgetRefreshPeriodSec != 0 {
Expand Down Expand Up @@ -621,6 +657,9 @@ func ParametersToRpc(cfg Parameters) (*clientrpc.LiquidityParameters,
AccountAddrType: addrType,
EasyAssetParams: easyAssetMap,
FastSwapPublication: cfg.FastSwapPublication,
ScriptableAutoloop: cfg.ScriptableAutoloop,
ScriptableScript: cfg.ScriptableScript,
ScriptableTickIntervalSec: uint64(cfg.ScriptableTickInterval / time.Second),
}
// Set excluded peers for easy autoloop.
rpcCfg.EasyAutoloopExcludedPeers = make(
Expand Down
Loading
Loading