Merge pull request #1165 from GustavoStingelin/feat/monitor-static-address
Some checks failed
CI / Commit Message (push) Has been cancelled
CI / RPC compilation check (push) Has been cancelled
CI / SQL compilation check (push) Has been cancelled
CI / go mod check (push) Has been cancelled
CI / build and lint code (push) Has been cancelled
CI / verify that auto-generated documentation is up-to-date (push) Has been cancelled
CI / run unit-test sqlite3 race (push) Has been cancelled
CI / run unit-test postgres race (push) Has been cancelled
CI / run LiT itests (push) Has been cancelled
CI / run LiT unit tests (push) Has been cancelled

multi: add static address loop in monitor
This commit is contained in:
Slyghtning 2026-07-24 15:19:11 +02:00 committed by GitHub
commit edad26b876
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1968 additions and 181 deletions

View file

@ -560,10 +560,7 @@ func parseAmt(text string) (btcutil.Amount, error) {
func logSwap(swap *looprpc.SwapStatus) {
// If our swap failed, we add our failure reason to the state.
swapState := fmt.Sprintf("%v", swap.State)
if swap.State == looprpc.SwapState_FAILED {
swapState = fmt.Sprintf("%v (%v)", swapState, swap.FailureReason)
}
swapState := monitorSwapState(swap)
if swap.Type == looprpc.SwapType_LOOP_OUT {
fmt.Printf("%v %v %v %v - %v",
@ -585,10 +582,32 @@ func logSwap(swap *looprpc.SwapStatus) {
}
}
if swap.State != looprpc.SwapState_INITIATED &&
swap.State != looprpc.SwapState_HTLC_PUBLISHED &&
swap.State != looprpc.SwapState_PREIMAGE_REVEALED {
showCost := shouldShowSwapCost(swap.GetState())
if swap.Type == looprpc.SwapType_STATIC_LOOP_IN {
staticState := swap.GetStaticLoopInState()
// Static loop-ins key cost visibility off the dedicated FSM state, not
// the generic SwapState lifecycle used by traditional swaps.
switch staticState {
case looprpc.StaticAddressLoopInSwapState_INIT_HTLC,
looprpc.StaticAddressLoopInSwapState_SIGN_HTLC_TX,
looprpc.StaticAddressLoopInSwapState_MONITOR_INVOICE_HTLC_TX,
looprpc.StaticAddressLoopInSwapState_SWEEP_STATIC_ADDRESS_HTLC_TIMEOUT,
looprpc.StaticAddressLoopInSwapState_MONITOR_HTLC_TIMEOUT_SWEEP,
looprpc.StaticAddressLoopInSwapState_UNLOCK_DEPOSITS:
showCost = false
case looprpc.StaticAddressLoopInSwapState_PAYMENT_RECEIVED,
looprpc.StaticAddressLoopInSwapState_HTLC_STATIC_ADDRESS_TIMEOUT_SWEPT,
looprpc.StaticAddressLoopInSwapState_SUCCEEDED,
looprpc.StaticAddressLoopInSwapState_SUCCEEDED_TRANSITIONING_FAILED,
looprpc.StaticAddressLoopInSwapState_FAILED_STATIC_ADDRESS_SWAP:
showCost = true
}
}
if showCost {
fmt.Printf(" (cost: server %v, onchain %v, offchain %v)",
swap.CostServer, swap.CostOnchain, swap.CostOffchain,
)
@ -597,6 +616,38 @@ func logSwap(swap *looprpc.SwapStatus) {
fmt.Println()
}
// monitorSwapState returns the static loop-in FSM label for static swaps and
// the shared swap-state label for all others.
func monitorSwapState(swap *looprpc.SwapStatus) string {
if swap.Type == looprpc.SwapType_STATIC_LOOP_IN {
return swap.GetStaticLoopInState().String()
}
return genericMonitorSwapState(swap)
}
// shouldShowSwapCost reports whether a swap's generic state is terminal enough
// to include the persisted cost summary in monitor output.
func shouldShowSwapCost(loopState looprpc.SwapState) bool {
return loopState != looprpc.SwapState_INITIATED &&
loopState != looprpc.SwapState_HTLC_PUBLISHED &&
loopState != looprpc.SwapState_PREIMAGE_REVEALED
}
// genericMonitorSwapState formats the shared swap-state label and failure
// reason used by non-static swaps.
func genericMonitorSwapState(swap *looprpc.SwapStatus) string {
loopState := swap.GetState()
swapState := fmt.Sprintf("%v", loopState)
if loopState == looprpc.SwapState_FAILED {
swapState = fmt.Sprintf(
"%v (%v)", swapState, swap.FailureReason,
)
}
return swapState
}
// getClientConn dials the loopd gRPC server with TLS and macaroon auth.
func getClientConn(address, tlsCertPath, macaroonPath string) (daemonConn,
func(), error) {

275
cmd/loop/monitor_test.go Normal file
View file

@ -0,0 +1,275 @@
package main
import (
"bytes"
"io"
"os"
"strings"
"testing"
"time"
"github.com/lightninglabs/loop/looprpc"
"github.com/stretchr/testify/require"
)
// TestMonitorSwapStateKeepsRegularLoopInFailureReason preserves the generic
// failure suffix for regular loop-in swaps so terminal errors stay visible.
func TestMonitorSwapStateKeepsRegularLoopInFailureReason(t *testing.T) {
swap := &looprpc.SwapStatus{
Type: looprpc.SwapType_LOOP_IN,
State: looprpc.SwapState_FAILED,
FailureReason: looprpc.
FailureReason_FAILURE_REASON_TIMEOUT,
}
got := monitorSwapState(swap)
require.Equal(t, "FAILED (FAILURE_REASON_TIMEOUT)", got)
}
// TestMonitorSwapStateLabelsStaticLoopInStages locks the precise static loop-in
// stage names so monitor output stays stable as the FSM evolves.
func TestMonitorSwapStateLabelsStaticLoopInStages(t *testing.T) {
tests := []struct {
name string
staticState looprpc.StaticAddressLoopInSwapState
want string
}{
{
name: "init htlc",
staticState: looprpc.StaticAddressLoopInSwapState_INIT_HTLC,
want: "INIT_HTLC",
},
{
name: "sign htlc",
staticState: looprpc.
StaticAddressLoopInSwapState_SIGN_HTLC_TX,
want: "SIGN_HTLC_TX",
},
{
name: "monitor invoice and htlc",
staticState: looprpc.
StaticAddressLoopInSwapState_MONITOR_INVOICE_HTLC_TX,
want: "MONITOR_INVOICE_HTLC_TX",
},
{
name: "unlock deposits",
staticState: looprpc.
StaticAddressLoopInSwapState_UNLOCK_DEPOSITS,
want: "UNLOCK_DEPOSITS",
},
{
name: "payment received",
staticState: looprpc.StaticAddressLoopInSwapState_PAYMENT_RECEIVED,
want: "PAYMENT_RECEIVED",
},
{
name: "timeout sweep",
staticState: looprpc.
StaticAddressLoopInSwapState_SWEEP_STATIC_ADDRESS_HTLC_TIMEOUT,
want: "SWEEP_STATIC_ADDRESS_HTLC_TIMEOUT",
},
{
name: "monitor timeout sweep",
staticState: looprpc.
StaticAddressLoopInSwapState_MONITOR_HTLC_TIMEOUT_SWEEP,
want: "MONITOR_HTLC_TIMEOUT_SWEEP",
},
{
name: "timeout swept",
staticState: looprpc.
StaticAddressLoopInSwapState_HTLC_STATIC_ADDRESS_TIMEOUT_SWEPT,
want: "HTLC_STATIC_ADDRESS_TIMEOUT_SWEPT",
},
{
name: "success",
staticState: looprpc.StaticAddressLoopInSwapState_SUCCEEDED,
want: "SUCCEEDED",
},
{
name: "succeeded transitioning failed",
staticState: looprpc.
StaticAddressLoopInSwapState_SUCCEEDED_TRANSITIONING_FAILED,
want: "SUCCEEDED_TRANSITIONING_FAILED",
},
{
name: "failed",
staticState: looprpc.
StaticAddressLoopInSwapState_FAILED_STATIC_ADDRESS_SWAP,
want: "FAILED_STATIC_ADDRESS_SWAP",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
swap := &looprpc.SwapStatus{
Type: looprpc.SwapType_STATIC_LOOP_IN,
StaticLoopInStateOptional: &looprpc.SwapStatus_StaticLoopInState{
StaticLoopInState: test.staticState,
},
}
got := monitorSwapState(swap)
require.Equal(t, test.want, got)
})
}
initSwap := &looprpc.SwapStatus{
Type: looprpc.SwapType_STATIC_LOOP_IN,
StaticLoopInStateOptional: &looprpc.SwapStatus_StaticLoopInState{
StaticLoopInState: looprpc.
StaticAddressLoopInSwapState_INIT_HTLC,
},
}
signSwap := &looprpc.SwapStatus{
Type: looprpc.SwapType_STATIC_LOOP_IN,
StaticLoopInStateOptional: &looprpc.SwapStatus_StaticLoopInState{
StaticLoopInState: looprpc.
StaticAddressLoopInSwapState_SIGN_HTLC_TX,
},
}
require.NotEqual(t, monitorSwapState(initSwap), monitorSwapState(signSwap))
}
// TestMonitorSwapStateLabelsUnknownStaticLoopIn ensures the absent static
// oneof maps to the UNKNOWN static loop-in label.
func TestMonitorSwapStateLabelsUnknownStaticLoopIn(t *testing.T) {
swap := &looprpc.SwapStatus{
Type: looprpc.SwapType_STATIC_LOOP_IN,
}
got := monitorSwapState(swap)
require.Equal(t, "UNKNOWN_STATIC_ADDRESS_SWAP_STATE", got)
}
// TestLogSwapHidesStaticLoopInCostForInFlightState proves in-flight static FSM
// state suppresses cost even with generic SUCCESS set deliberately.
func TestLogSwapHidesStaticLoopInCostForInFlightState(t *testing.T) {
swap := &looprpc.SwapStatus{
LastUpdateTime: time.Unix(1, 0).UnixNano(),
Type: looprpc.SwapType_STATIC_LOOP_IN,
State: looprpc.SwapState_SUCCESS,
StaticLoopInStateOptional: &looprpc.SwapStatus_StaticLoopInState{
StaticLoopInState: looprpc.
StaticAddressLoopInSwapState_INIT_HTLC,
},
Amt: 50_000,
CostServer: 11,
CostOnchain: 22,
CostOffchain: 33,
HtlcAddressP2Wsh: "bc1qstaticinflighttestaddress",
}
output := captureStdout(t, func() {
logSwap(swap)
})
require.Contains(t, output, "STATIC_LOOP_IN INIT_HTLC 0.00050000 BTC")
require.NotContains(t, output, "(cost:")
}
// TestLogSwapHidesCostForUnknownStaticLoopInState ensures unknown static
// states print the UNKNOWN label without a cost summary.
func TestLogSwapHidesCostForUnknownStaticLoopInState(t *testing.T) {
swap := &looprpc.SwapStatus{
LastUpdateTime: time.Unix(3, 0).UnixNano(),
Type: looprpc.SwapType_STATIC_LOOP_IN,
StaticLoopInStateOptional: &looprpc.SwapStatus_StaticLoopInState{
StaticLoopInState: looprpc.
StaticAddressLoopInSwapState_UNKNOWN_STATIC_ADDRESS_SWAP_STATE,
},
Amt: 50_000,
CostServer: 11,
CostOnchain: 22,
CostOffchain: 33,
HtlcAddressP2Wsh: "bc1qunknownstaticterminaltestaddress",
}
output := captureStdout(t, func() {
logSwap(swap)
})
require.Contains(
t, output, "STATIC_LOOP_IN UNKNOWN_STATIC_ADDRESS_SWAP_STATE 0.00050000 BTC",
)
require.NotContains(t, output, "(cost:")
}
// TestLogSwapShowsStaticLoopInCostForTerminalState preserves terminal cost
// output, including the P2WSH address, once the static loop-in is done.
func TestLogSwapShowsStaticLoopInCostForTerminalState(t *testing.T) {
swap := &looprpc.SwapStatus{
LastUpdateTime: time.Unix(2, 0).UnixNano(),
Type: looprpc.SwapType_STATIC_LOOP_IN,
State: looprpc.SwapState_INITIATED,
StaticLoopInStateOptional: &looprpc.SwapStatus_StaticLoopInState{
StaticLoopInState: looprpc.
StaticAddressLoopInSwapState_SUCCEEDED,
},
Amt: 50_000,
CostServer: 11,
CostOnchain: 22,
CostOffchain: 33,
HtlcAddressP2Wsh: "bc1qstaticterminaltestaddress",
}
output := captureStdout(t, func() {
logSwap(swap)
})
require.Contains(
t, output,
"STATIC_LOOP_IN SUCCEEDED 0.00050000 BTC - P2WSH: bc1qstaticterminaltestaddress",
)
require.Contains(
t, output,
"(cost: server 11, onchain 22, offchain 33)",
)
}
// TestLogSwapDisplaysStaticLoopInTypeAndStage ensures the monitor output names
// the static loop-in type and active FSM stage instead of collapsing them.
func TestLogSwapDisplaysStaticLoopInTypeAndStage(t *testing.T) {
swap := &looprpc.SwapStatus{
LastUpdateTime: time.Unix(1, 0).UnixNano(),
Type: looprpc.SwapType_STATIC_LOOP_IN,
StaticLoopInStateOptional: &looprpc.SwapStatus_StaticLoopInState{
StaticLoopInState: looprpc.
StaticAddressLoopInSwapState_SIGN_HTLC_TX,
},
Amt: 50_000,
HtlcAddressP2Wsh: "tb1q5cyxnuxmeuwuvkwfem96llyxf8duyshm56t8k8",
}
output := captureStdout(t, func() {
logSwap(swap)
})
require.Contains(
t, output, "STATIC_LOOP_IN SIGN_HTLC_TX 0.00050000 BTC",
)
require.Contains(
t, output, "P2WSH: tb1q5cyxnuxmeuwuvkwfem96llyxf8duyshm56t8k8",
)
require.NotContains(t, output, "STATIC_LOOP_IN INIT_HTLC")
}
func captureStdout(t *testing.T, fn func()) string {
t.Helper()
originalStdout := os.Stdout
reader, writer, err := os.Pipe()
require.NoError(t, err)
os.Stdout = writer
fn()
require.NoError(t, writer.Close())
os.Stdout = originalStdout
var buf bytes.Buffer
_, err = io.Copy(&buf, reader)
require.NoError(t, err)
require.NoError(t, reader.Close())
return strings.TrimSpace(buf.String())
}

View file

@ -15,9 +15,10 @@ import (
var listSwapsCommand = &cli.Command{
Name: "listswaps",
Usage: "list all swaps in the local database",
Description: "Allows the user to get a list of all swaps that are " +
"currently stored in the database",
Usage: "list traditional Loop In and Loop Out swaps",
Description: "Lists traditional Loop In and Loop Out swaps that are " +
"currently stored in the database. Static address loop-ins are not " +
"included; use `loop static listswaps` to view them.",
Action: listSwaps,
Flags: []cli.Flag{
&cli.BoolFlag{
@ -128,10 +129,12 @@ func listSwaps(ctx context.Context, cmd *cli.Command) error {
var swapInfoCommand = &cli.Command{
Name: "swapinfo",
Usage: "show the status of a swap",
Usage: "show the status of a traditional swap",
ArgsUsage: "id",
Description: "Allows the user to get the status of a single swap " +
"currently stored in the database",
Description: "Shows the status of a traditional Loop In or Loop Out " +
"swap currently stored in the database. Static address loop-ins " +
"must be viewed with `loop static listswaps`; there is no generic " +
"per-swap static lookup command.",
Flags: []cli.Flag{
&cli.Uint64Flag{
Name: "id",

View file

@ -0,0 +1,267 @@
{
"metadata": {
"args": [
"loop",
"monitor",
"--network",
"regtest"
],
"env": {},
"version": "0.33.3-beta commit= commit_hash=",
"run_error": "recv: rpc error: code = Canceled desc = context canceled",
"duration": 25407490958,
"clock_start_unix": 1784150305
},
"events": [
{
"time_ms": 13,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/Monitor",
"event": "send",
"message_type": "looprpc.MonitorRequest",
"payload": {}
}
},
{
"time_ms": 13,
"kind": "stdout",
"data": {
"lines": [
"Note: offchain cost may report as 0 after loopd restart during swap\n"
]
}
},
{
"time_ms": 16479,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/Monitor",
"event": "recv",
"message_type": "looprpc.SwapStatus",
"payload": {
"amt": "500000",
"id": "bec3777d45337f652d96136e5714ed90a5d4f7cda87b5842110ecc38c9a65826",
"id_bytes": "vsN3fUUzf2UtlhNuVxTtkKXU982oe1hCEQ7MOMmmWCY=",
"type": "STATIC_LOOP_IN",
"state": "INITIATED",
"static_loop_in_state": "INIT_HTLC",
"failure_reason": "FAILURE_REASON_NONE",
"initiation_time": "1784150320647961000",
"last_update_time": "1784150321494895000",
"htlc_address": "bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv",
"htlc_address_p2wsh": "bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv",
"htlc_address_p2tr": "",
"cost_server": "0",
"cost_onchain": "0",
"cost_offchain": "0",
"last_hop": "",
"outgoing_chan_set": [],
"label": "",
"asset_info": null
}
}
},
{
"time_ms": 16479,
"kind": "stdout",
"data": {
"lines": [
"2026-07-15T18:18:41-03:00 STATIC_LOOP_IN INIT_HTLC 0.00500000 BTC - P2WSH: bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv\n"
]
}
},
{
"time_ms": 16482,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/Monitor",
"event": "recv",
"message_type": "looprpc.SwapStatus",
"payload": {
"amt": "500000",
"id": "bec3777d45337f652d96136e5714ed90a5d4f7cda87b5842110ecc38c9a65826",
"id_bytes": "vsN3fUUzf2UtlhNuVxTtkKXU982oe1hCEQ7MOMmmWCY=",
"type": "STATIC_LOOP_IN",
"state": "INITIATED",
"static_loop_in_state": "SIGN_HTLC_TX",
"failure_reason": "FAILURE_REASON_NONE",
"initiation_time": "1784150320647961000",
"last_update_time": "1784150321499639000",
"htlc_address": "bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv",
"htlc_address_p2wsh": "bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv",
"htlc_address_p2tr": "",
"cost_server": "0",
"cost_onchain": "0",
"cost_offchain": "0",
"last_hop": "",
"outgoing_chan_set": [],
"label": "",
"asset_info": null
}
}
},
{
"time_ms": 16482,
"kind": "stdout",
"data": {
"lines": [
"2026-07-15T18:18:41-03:00 STATIC_LOOP_IN SIGN_HTLC_TX 0.00500000 BTC - P2WSH: bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv\n"
]
}
},
{
"time_ms": 16530,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/Monitor",
"event": "recv",
"message_type": "looprpc.SwapStatus",
"payload": {
"amt": "500000",
"id": "bec3777d45337f652d96136e5714ed90a5d4f7cda87b5842110ecc38c9a65826",
"id_bytes": "vsN3fUUzf2UtlhNuVxTtkKXU982oe1hCEQ7MOMmmWCY=",
"type": "STATIC_LOOP_IN",
"state": "INITIATED",
"static_loop_in_state": "MONITOR_INVOICE_HTLC_TX",
"failure_reason": "FAILURE_REASON_NONE",
"initiation_time": "1784150320647961000",
"last_update_time": "1784150321548450000",
"htlc_address": "bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv",
"htlc_address_p2wsh": "bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv",
"htlc_address_p2tr": "",
"cost_server": "0",
"cost_onchain": "0",
"cost_offchain": "0",
"last_hop": "",
"outgoing_chan_set": [],
"label": "",
"asset_info": null
}
}
},
{
"time_ms": 16530,
"kind": "stdout",
"data": {
"lines": [
"2026-07-15T18:18:41-03:00 STATIC_LOOP_IN MONITOR_INVOICE_HTLC_TX 0.00500000 BTC - P2WSH: bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv\n"
]
}
},
{
"time_ms": 17027,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/Monitor",
"event": "recv",
"message_type": "looprpc.SwapStatus",
"payload": {
"amt": "500000",
"id": "bec3777d45337f652d96136e5714ed90a5d4f7cda87b5842110ecc38c9a65826",
"id_bytes": "vsN3fUUzf2UtlhNuVxTtkKXU982oe1hCEQ7MOMmmWCY=",
"type": "STATIC_LOOP_IN",
"state": "INITIATED",
"static_loop_in_state": "PAYMENT_RECEIVED",
"failure_reason": "FAILURE_REASON_NONE",
"initiation_time": "1784150320647961000",
"last_update_time": "1784150322044521000",
"htlc_address": "bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv",
"htlc_address_p2wsh": "bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv",
"htlc_address_p2tr": "",
"cost_server": "1828",
"cost_onchain": "0",
"cost_offchain": "0",
"last_hop": "",
"outgoing_chan_set": [],
"label": "",
"asset_info": null
}
}
},
{
"time_ms": 17027,
"kind": "stdout",
"data": {
"lines": [
"2026-07-15T18:18:42-03:00 STATIC_LOOP_IN PAYMENT_RECEIVED 0.00500000 BTC - P2WSH: bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv (cost: server 1828, onchain 0, offchain 0)\n"
]
}
},
{
"time_ms": 17036,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/Monitor",
"event": "recv",
"message_type": "looprpc.SwapStatus",
"payload": {
"amt": "500000",
"id": "bec3777d45337f652d96136e5714ed90a5d4f7cda87b5842110ecc38c9a65826",
"id_bytes": "vsN3fUUzf2UtlhNuVxTtkKXU982oe1hCEQ7MOMmmWCY=",
"type": "STATIC_LOOP_IN",
"state": "INITIATED",
"static_loop_in_state": "SUCCEEDED",
"failure_reason": "FAILURE_REASON_NONE",
"initiation_time": "1784150320647961000",
"last_update_time": "1784150322052572000",
"htlc_address": "bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv",
"htlc_address_p2wsh": "bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv",
"htlc_address_p2tr": "",
"cost_server": "1828",
"cost_onchain": "0",
"cost_offchain": "0",
"last_hop": "",
"outgoing_chan_set": [],
"label": "",
"asset_info": null
}
}
},
{
"time_ms": 17036,
"kind": "stdout",
"data": {
"lines": [
"2026-07-15T18:18:42-03:00 STATIC_LOOP_IN SUCCEEDED 0.00500000 BTC - P2WSH: bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv (cost: server 1828, onchain 0, offchain 0)\n"
]
}
},
{
"time_ms": 25406,
"kind": "signal",
"data": {
"signal": "interrupt"
}
},
{
"time_ms": 25407,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/Monitor",
"event": "error",
"error": "rpc error: code = Canceled desc = context canceled",
"status": {
"code": 1,
"message": "context canceled"
}
}
},
{
"time_ms": 25407,
"kind": "stderr",
"data": {
"lines": [
"[loop] recv: rpc error: code = Canceled desc = context canceled\n"
]
}
},
{
"time_ms": 25407,
"kind": "exit",
"data": {
"run_error": "recv: rpc error: code = Canceled desc = context canceled"
}
}
]
}

View file

@ -203,7 +203,7 @@ fetches a new L402 authentication token from the server
\fB--help, -h\fP: show help
.SH listswaps
list all swaps in the local database
list traditional Loop In and Loop Out swaps
.PP
\fB--channel\fP="": the comma-separated list of short channel IDs of the channels to loop out
@ -233,7 +233,7 @@ list all swaps in the local database
\fB--start_time_ns\fP="": Unix timestamp in nanoseconds to select swaps initiated after this time (default: 0)
.SH swapinfo
show the status of a swap
show the status of a traditional swap
.PP
\fB--help, -h\fP: show help

View file

@ -235,9 +235,9 @@ The following flags are supported:
### `listswaps` command
list all swaps in the local database.
list traditional Loop In and Loop Out swaps.
Allows the user to get a list of all swaps that are currently stored in the database.
Lists traditional Loop In and Loop Out swaps that are currently stored in the database. Static address loop-ins are not included; use `loop static listswaps` to view them.
Usage:
@ -261,9 +261,9 @@ The following flags are supported:
### `swapinfo` command
show the status of a swap.
show the status of a traditional swap.
Allows the user to get the status of a single swap currently stored in the database.
Shows the status of a traditional Loop In or Loop Out swap currently stored in the database. Static address loop-ins must be viewed with `loop static listswaps`; there is no generic per-swap static lookup command.
Usage:

View file

@ -4,6 +4,7 @@ import (
"time"
"github.com/btcsuite/btcd/btcutil"
"github.com/lightninglabs/loop/fsm"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/swap"
"github.com/lightninglabs/taproot-assets/rfqmath"
@ -476,8 +477,12 @@ type SwapInfoKit struct {
LastUpdateTime time.Time
}
// SwapInfo exposes common info fields for loop in and loop out swaps.
// SwapInfo exposes common info fields for traditional swaps and static address
// loop-ins.
type SwapInfo struct {
// SwapStateData.State is authoritative for swap.TypeIn and swap.TypeOut.
// For swap.TypeStaticAddressLoopIn, StaticAddressLoopInState is
// authoritative and State remains its zero value, loopdb.StateInitiated.
loopdb.SwapStateData
loopdb.SwapContract
@ -488,9 +493,14 @@ type SwapInfo struct {
// SwapHash stores the swap preimage hash.
SwapHash lntypes.Hash
// SwapType describes whether this is a loop in or loop out swap.
// SwapType describes the kind of swap.
SwapType swap.Type
// StaticAddressLoopInState stores the precise static address loop-in FSM
// state when SwapType is swap.TypeStaticAddressLoopIn. For traditional
// swaps, it remains the fsm.StateType zero value, fsm.EmptyState.
StaticAddressLoopInState fsm.StateType
// HtlcAddressP2WSH stores the address of the P2WSH (native segwit)
// swap htlc. This is used for both loop-in and loop-out.
HtlcAddressP2WSH btcutil.Address

View file

@ -400,9 +400,16 @@ func rpcToFee(req *clientrpc.LiquidityParameters) (FeeLimit, error) {
// rpcToRule switches on rpc rule type to convert to our rule interface.
func rpcToRule(rule *clientrpc.LiquidityRule) (*SwapRule, error) {
swapType := swap.TypeOut
if rule.SwapType == clientrpc.SwapType_LOOP_IN {
var swapType swap.Type
switch rule.SwapType {
case clientrpc.SwapType_LOOP_OUT:
swapType = swap.TypeOut
case clientrpc.SwapType_LOOP_IN:
swapType = swap.TypeIn
default:
return nil, fmt.Errorf("unknown swap type: %v", rule.SwapType)
}
switch rule.Type {

View file

@ -3,9 +3,62 @@ package liquidity
import (
"testing"
clientrpc "github.com/lightninglabs/loop/looprpc"
"github.com/lightninglabs/loop/swap"
"github.com/stretchr/testify/require"
)
// TestRPCToRuleSwapType verifies RPC swap type conversion.
func TestRPCToRuleSwapType(t *testing.T) {
tests := []struct {
name string
swapType clientrpc.SwapType
wantType swap.Type
wantErr bool
}{
{
name: "loop out",
swapType: clientrpc.SwapType_LOOP_OUT,
wantType: swap.TypeOut,
},
{
name: "loop in",
swapType: clientrpc.SwapType_LOOP_IN,
wantType: swap.TypeIn,
},
{
name: "static loop in rejected",
swapType: clientrpc.SwapType_STATIC_LOOP_IN,
wantErr: true,
},
{
name: "unknown swap type rejected",
swapType: clientrpc.SwapType(99),
wantErr: true,
},
}
for _, testCase := range tests {
t.Run(testCase.name, func(t *testing.T) {
rpcRule := &clientrpc.LiquidityRule{
Type: clientrpc.LiquidityRuleType_THRESHOLD,
IncomingThreshold: 10,
OutgoingThreshold: 20,
SwapType: testCase.swapType,
}
got, err := rpcToRule(rpcRule)
if testCase.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, testCase.wantType, got.Type)
})
}
}
// TestValidateRestrictions tests validating client restrictions against a set
// of server restrictions.
func TestValidateRestrictions(t *testing.T) {

View file

@ -110,6 +110,10 @@ type Daemon struct {
macaroonService *lndclient.MacaroonService
}
// staticLoopInStatusChanBuffer keeps the shared swap-status fanout from
// stalling static loop-in FSM progress during transient subscriber gaps.
const staticLoopInStatusChanBuffer = 20
// New creates a new instance of the loop client daemon.
func New(config *Config, lisCfg *ListenerCfg) *Daemon {
return &Daemon{
@ -681,6 +685,7 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
LightningClient: d.lnd.Client,
}
openChannelManager = openchannel.NewManager(openChannelCfg)
statusChan := make(chan loop.SwapInfo, staticLoopInStatusChanBuffer)
// Run the deposit swap hash migration.
err = loopin.MigrateDepositSwapHash(
@ -701,6 +706,11 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
return err
}
statusUpdater := &staticLoopInStatusUpdater{
statusChan: statusChan,
mainCtx: d.mainCtx,
chainParams: d.lnd.ChainParams,
}
staticLoopInManager, err = loopin.NewManager(&loopin.Config{
Server: staticAddressClient,
@ -718,6 +728,7 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
ChainParams: d.lnd.ChainParams,
Signer: d.lnd.Signer,
ValidateLoopInContract: loop.ValidateLoopInContract,
SendUpdate: statusUpdater.sendUpdate,
MaxStaticAddrHtlcFeePercentage: d.cfg.MaxStaticAddrHtlcFeePercentage,
MaxStaticAddrHtlcBackupFeePercentage: d.cfg.MaxStaticAddrHtlcBackupFeePercentage,
}, blockHeight)
@ -783,7 +794,7 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
lnd: &d.lnd.LndServices,
swaps: make(map[lntypes.Hash]loop.SwapInfo),
subscribers: make(map[int]chan<- any),
statusChan: make(chan loop.SwapInfo),
statusChan: statusChan,
mainCtx: d.mainCtx,
reservationManager: reservationManager,
instantOutManager: instantOutManager,

View file

@ -0,0 +1,47 @@
package loopd
import (
"context"
"fmt"
"github.com/btcsuite/btcd/chaincfg"
"github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/staticaddr/loopin"
)
// staticLoopInStatusUpdater publishes static-address loop-in status updates to
// the client-facing swap stream.
type staticLoopInStatusUpdater struct {
// statusChan sends updates to the client-facing swap stream.
statusChan chan<- loop.SwapInfo
// mainCtx is canceled when loopd is shutting down.
mainCtx context.Context
// chainParams are used to reconstruct the static loop-in HTLC address.
chainParams *chaincfg.Params
}
// sendUpdate converts the persisted static-address loop-in into swap info and
// forwards it to the status stream unless either context is canceled.
func (u *staticLoopInStatusUpdater) sendUpdate(ctx context.Context,
swp *loopin.StaticAddressLoopIn) error {
info, err := staticAddressLoopInSwapInfoWithChainParams(
swp, u.chainParams,
)
if err != nil {
return fmt.Errorf("unable to notify static loop-in update: %w", err)
}
select {
case u.statusChan <- *info:
return nil
case <-ctx.Done():
return ctx.Err()
case <-u.mainCtx.Done():
return u.mainCtx.Err()
}
}

View file

@ -411,6 +411,8 @@ func (s *swapClientServer) marshallSwap(ctx context.Context,
}
var swapType looprpc.SwapType
staticLoopInState := looprpc.
StaticAddressLoopInSwapState_UNKNOWN_STATIC_ADDRESS_SWAP_STATE
var (
htlcAddress string
htlcAddressP2TR string
@ -437,6 +439,27 @@ func (s *swapClientServer) marshallSwap(ctx context.Context,
lastHop = loopSwap.LastHop[:]
}
case swap.TypeStaticAddressLoopIn:
// Static loop-ins surface their precise FSM state through the
// optional oneof and keep the reconstructed HTLC P2WSH address,
// not the reusable static address.
swapType = looprpc.SwapType_STATIC_LOOP_IN
staticLoopInState = toClientStaticAddressLoopInState(
loopSwap.StaticAddressLoopInState,
)
if loopSwap.HtlcAddressP2WSH == nil {
return nil, errors.New(
"missing static address loop-in P2WSH HTLC address",
)
}
htlcAddressP2WSH = loopSwap.HtlcAddressP2WSH.EncodeAddress()
htlcAddress = htlcAddressP2WSH
if loopSwap.LastHop != nil {
lastHop = loopSwap.LastHop[:]
}
case swap.TypeOut:
swapType = looprpc.SwapType_LOOP_OUT
if loopSwap.HtlcAddressP2WSH != nil {
@ -478,7 +501,7 @@ func (s *swapClientServer) marshallSwap(ctx context.Context,
return nil, errors.New("unknown swap type")
}
return &looprpc.SwapStatus{
rpcSwap := &looprpc.SwapStatus{
Amt: int64(loopSwap.AmountRequested),
Id: loopSwap.SwapHash.String(),
IdBytes: loopSwap.SwapHash[:],
@ -497,7 +520,15 @@ func (s *swapClientServer) marshallSwap(ctx context.Context,
LastHop: lastHop,
OutgoingChanSet: outGoingChanSet,
AssetInfo: assetInfo,
}, nil
}
if swapType == looprpc.SwapType_STATIC_LOOP_IN {
rpcSwap.StaticLoopInStateOptional =
&looprpc.SwapStatus_StaticLoopInState{
StaticLoopInState: staticLoopInState,
}
}
return rpcSwap, nil
}
// Monitor will return a stream of swap updates for currently active swaps.
@ -518,27 +549,28 @@ func (s *swapClientServer) Monitor(in *looprpc.MonitorRequest,
// Start a notification queue for this subscriber.
queue := queue.NewConcurrentQueue(20)
queue.Start()
ctx := server.Context()
// Add this subscriber to the global subscriber list. Also create a
// snapshot of all pending and completed swaps within the lock, to
// prevent subscribers from receiving duplicate updates.
s.swapsLock.Lock()
id := s.nextSubscriberID
s.nextSubscriberID++
s.subscribers[id] = queue.ChanIn()
var pendingSwaps, completedSwaps []loop.SwapInfo
for _, swap := range s.swaps {
if swap.State.Type() == loopdb.StateTypePending {
pendingSwaps = append(pendingSwaps, swap)
} else {
completedSwaps = append(completedSwaps, swap)
}
}
pendingSwaps, completedSwaps := s.monitorCachedSwaps()
s.swapsLock.Unlock()
err := s.appendStaticAddressLoopInMonitorSnapshot(
ctx, &pendingSwaps, &completedSwaps,
)
if err != nil {
s.swapsLock.Lock()
delete(s.subscribers, id)
s.swapsLock.Unlock()
queue.Stop()
return err
}
defer func() {
s.swapsLock.Lock()
delete(s.subscribers, id)
@ -568,6 +600,13 @@ func (s *swapClientServer) Monitor(in *looprpc.MonitorRequest,
)
})
// Static-address loop-in updates can arrive from both the initial snapshot
// and the live queue. Build a high-water mark from the snapshot so we can
// suppress stale duplicate snapshot items without dropping newer live ones.
staticSnapshotHighWater := staticAddressLoopInMonitorHighWater(
filteredSwaps,
)
// Return swaps to caller.
for _, swap := range filteredSwaps {
if err := send(swap); err != nil {
@ -585,6 +624,13 @@ func (s *swapClientServer) Monitor(in *looprpc.MonitorRequest,
}
swap := queueItem.(loop.SwapInfo)
if isInitialStaticAddressLoopInStale(
staticSnapshotHighWater, swap,
) {
continue
}
if err := send(swap); err != nil {
return err
}
@ -600,6 +646,102 @@ func (s *swapClientServer) Monitor(in *looprpc.MonitorRequest,
}
}
// staticAddressLoopInMonitorHighWater records the latest snapshot item for each
// static-address loop-in swap hash.
func staticAddressLoopInMonitorHighWater(
swaps []loop.SwapInfo) map[lntypes.Hash]staticAddressLoopInHighWater {
highWater := make(map[lntypes.Hash]staticAddressLoopInHighWater)
for _, swp := range swaps {
if swp.SwapType != swap.TypeStaticAddressLoopIn {
continue
}
current, ok := highWater[swp.SwapHash]
if !ok || swp.LastUpdate.After(current.lastUpdate) {
highWater[swp.SwapHash] = staticAddressLoopInHighWater{
lastUpdate: swp.LastUpdate,
staticState: swp.StaticAddressLoopInState,
}
}
}
return highWater
}
// staticAddressLoopInHighWater stores the most recent snapshot timestamp and
// state for one static-address loop-in swap.
type staticAddressLoopInHighWater struct {
lastUpdate time.Time
staticState fsm.StateType
}
// isInitialStaticAddressLoopInStale reports whether a live static-address
// loop-in update is older than the snapshot copy already sent, or equal to it
// with the same static FSM state.
func isInitialStaticAddressLoopInStale(
highWater map[lntypes.Hash]staticAddressLoopInHighWater,
swp loop.SwapInfo) bool {
if swp.SwapType != swap.TypeStaticAddressLoopIn {
return false
}
current, ok := highWater[swp.SwapHash]
if !ok {
return false
}
if swp.LastUpdate.Before(current.lastUpdate) {
return true
}
if swp.LastUpdate.After(current.lastUpdate) {
return false
}
// Equal timestamps can race the initial DB snapshot, so match state too.
return swp.StaticAddressLoopInState == current.staticState
}
// monitorCachedSwaps returns the current in-memory swaps split into pending and
// completed slices for monitor snapshot construction.
func (s *swapClientServer) monitorCachedSwaps() ([]loop.SwapInfo,
[]loop.SwapInfo) {
var pendingSwaps, completedSwaps []loop.SwapInfo
for _, swap := range s.swaps {
if swap.State.Type() == loopdb.StateTypePending {
pendingSwaps = append(pendingSwaps, swap)
} else {
completedSwaps = append(completedSwaps, swap)
}
}
return pendingSwaps, completedSwaps
}
// appendStaticAddressLoopInMonitorSnapshot appends the current static-address
// loop-in swaps to the monitor snapshot.
func (s *swapClientServer) appendStaticAddressLoopInMonitorSnapshot(
ctx context.Context, pendingSwaps, completedSwaps *[]loop.SwapInfo) error {
staticSwaps, err := s.staticAddressLoopInSwapInfos(ctx)
if err != nil {
return err
}
for _, swap := range staticSwaps {
if slices.Contains(
loopin.FinalStates, swap.StaticAddressLoopInState,
) {
*completedSwaps = append(*completedSwaps, *swap)
} else {
*pendingSwaps = append(*pendingSwaps, *swap)
}
}
return nil
}
// ListSwaps returns a list of all currently known swaps and their current
// status.
func (s *swapClientServer) ListSwaps(ctx context.Context,
@ -754,10 +896,13 @@ func (s *swapClientServer) SwapInfo(ctx context.Context,
// Just return the server's in-memory cache here too as we also want to
// return temporary failures to the client.
s.swapsLock.Lock()
swp, ok := s.swaps[swapHash]
s.swapsLock.Unlock()
if !ok {
return nil, fmt.Errorf("swap with hash %s not found", req.Id)
}
return s.marshallSwap(ctx, &swp)
}
@ -2089,6 +2234,8 @@ func (s *swapClientServer) ListStaticAddressSwaps(ctx context.Context,
}, nil
}
// staticAddressLoopInTimestamp converts a non-zero timestamp to Unix nano
// form and preserves zero timestamps as zero.
func staticAddressLoopInTimestamp(t time.Time) int64 {
if t.IsZero() {
return 0
@ -2114,6 +2261,141 @@ func staticAddressLoopInSwapServerCost(swp *loopin.StaticAddressLoopIn) int64 {
}
}
// staticAddressLoopInSwapInfos loads the static-address loop-in manager swaps
// and converts them to client-facing swap info records.
func (s *swapClientServer) staticAddressLoopInSwapInfos(
ctx context.Context) ([]*loop.SwapInfo, error) {
if s.staticLoopInManager == nil {
return nil, nil
}
staticSwaps, err := s.staticLoopInManager.GetAllSwaps(ctx)
if err != nil {
return nil, err
}
swapInfos := make([]*loop.SwapInfo, 0, len(staticSwaps))
for _, swp := range staticSwaps {
if swp == nil {
continue
}
swapInfo, err := s.staticAddressLoopInSwapInfo(ctx, swp)
if err != nil {
return nil, err
}
swapInfos = append(swapInfos, swapInfo)
}
return swapInfos, nil
}
// staticAddressLoopInSwapInfo converts one static-address loop-in into swap
// info using the daemon's current chain parameters.
func (s *swapClientServer) staticAddressLoopInSwapInfo(_ context.Context,
swp *loopin.StaticAddressLoopIn) (*loop.SwapInfo, error) {
chainParams, err := s.network.ChainParams()
if err != nil {
return nil, fmt.Errorf("error getting chain params")
}
return staticAddressLoopInSwapInfoWithChainParams(swp, chainParams)
}
// staticAddressLoopInSwapInfoWithChainParams converts one static-address
// loop-in into swap info, including its reconstructed V2 P2WSH HTLC address.
func staticAddressLoopInSwapInfoWithChainParams(
swp *loopin.StaticAddressLoopIn,
chainParams *chaincfg.Params) (*loop.SwapInfo, error) {
htlcAddress, err := staticAddressLoopInHtlcAddress(swp, chainParams)
if err != nil {
return nil, err
}
var lastHop *route.Vertex
if len(swp.LastHop) > 0 {
vertex, err := route.NewVertexFromBytes(swp.LastHop)
if err != nil {
return nil, err
}
lastHop = &vertex
}
amount := swp.TotalDepositAmount()
if swp.SelectedAmount > 0 {
amount = swp.SelectedAmount
}
lastUpdate := swp.LastUpdateTime
if lastUpdate.IsZero() {
lastUpdate = swp.InitiationTime
}
return &loop.SwapInfo{
SwapStateData: loopdb.SwapStateData{
// Mirror ListStaticAddressSwaps by reporting only the persisted
// client-visible server cost. On-chain and off-chain costs stay
// zero until static loop-ins persist real fee data.
Cost: loopdb.SwapCost{
Server: btcutil.Amount(
staticAddressLoopInSwapServerCost(swp),
),
},
},
SwapContract: loopdb.SwapContract{
AmountRequested: amount,
CltvExpiry: swp.HtlcCltvExpiry,
MaxSwapFee: swp.MaxSwapFee,
InitiationTime: swp.InitiationTime,
Label: swp.Label,
ProtocolVersion: loopdb.ProtocolVersion(
swp.ProtocolVersion,
),
},
LastUpdate: lastUpdate,
SwapHash: swp.SwapHash,
SwapType: swap.TypeStaticAddressLoopIn,
StaticAddressLoopInState: swp.GetState(),
HtlcAddressP2WSH: htlcAddress,
LastHop: lastHop,
}, nil
}
// staticAddressLoopInHtlcAddress reconstructs the V2 P2WSH HTLC address from
// the static-address loop-in's client and server keys.
func staticAddressLoopInHtlcAddress(swp *loopin.StaticAddressLoopIn,
chainParams *chaincfg.Params) (btcutil.Address, error) {
if swp.ClientPubkey == nil {
return nil, errors.New("missing static address loop-in client HTLC key")
}
if swp.ServerPubkey == nil {
return nil, errors.New("missing static address loop-in server HTLC key")
}
htlc, err := swap.NewHtlcV2(
swp.HtlcCltvExpiry, pubkeyTo33ByteSlice(swp.ClientPubkey),
pubkeyTo33ByteSlice(swp.ServerPubkey), swp.SwapHash, chainParams,
)
if err != nil {
return nil, fmt.Errorf("construct static address loop-in HTLC: %w", err)
}
return htlc.Address, nil
}
// pubkeyTo33ByteSlice converts a compressed public key to a fixed 33-byte
// array.
func pubkeyTo33ByteSlice(pubkey *btcec.PublicKey) [33]byte {
var pubkeyBytes [33]byte
copy(pubkeyBytes[:], pubkey.SerializeCompressed())
return pubkeyBytes
}
// GetStaticAddressSummary returns a summary of static address-related
// information. Amongst deposits and withdrawals and their total values, it also
// includes a list of detailed deposit information filtered by their state.
@ -2420,6 +2702,8 @@ func toClientDepositState(state fsm.StateType) looprpc.DepositState {
}
}
// toClientStaticAddressLoopInState maps the static-address loop-in FSM state
// to the RPC enum exposed to clients.
func toClientStaticAddressLoopInState(
state fsm.StateType) looprpc.StaticAddressLoopInSwapState {
@ -2572,7 +2856,12 @@ func (s *swapClientServer) processStatusUpdates(mainCtx context.Context) {
// subscribers about the changes.
case swp := <-s.statusChan:
s.swapsLock.Lock()
s.swaps[swp.SwapHash] = swp
// Static loop-ins are broadcast to monitor subscribers, but they
// stay out of the legacy swap cache so ListSwaps and SwapInfo remain
// traditional-swap views.
if swp.SwapType != swap.TypeStaticAddressLoopIn {
s.swaps[swp.SwapHash] = swp
}
for _, subscriber := range s.subscribers {
select {

View file

@ -7,6 +7,7 @@ import (
"testing"
"time"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
@ -25,6 +26,7 @@ import (
"github.com/lightninglabs/loop/staticaddr/script"
"github.com/lightninglabs/loop/swap"
mock_lnd "github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwallet"
@ -32,6 +34,7 @@ import (
"github.com/lightningnetwork/lnd/routing/route"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
@ -501,10 +504,554 @@ func TestListStaticAddressSwapsPopulatesTimingAndCosts(t *testing.T) {
)
}
// TestStaticAddressLoopInMarshallUsesStaticTypeAndP2WSH protects the RPC
// mapping invariant that static loop-ins expose their static type, static
// state, and P2WSH HTLC address without leaking a taproot HTLC address.
func TestStaticAddressLoopInMarshallUsesStaticTypeAndP2WSH(t *testing.T) {
server := &swapClientServer{}
loopSwap := &loop.SwapInfo{
SwapStateData: loopdb.SwapStateData{
State: loopdb.StateInitiated,
},
SwapContract: loopdb.SwapContract{
InitiationTime: time.Now(),
},
LastUpdate: time.Now(),
SwapHash: lntypes.Hash{1},
SwapType: swap.TypeStaticAddressLoopIn,
StaticAddressLoopInState: loopin.SignHtlcTx,
HtlcAddressP2WSH: testnetAddr,
}
rpcSwap, err := server.marshallSwap(t.Context(), loopSwap)
require.NoError(t, err)
require.Equal(t, looprpc.SwapType_STATIC_LOOP_IN, rpcSwap.Type)
require.Equal(
t, looprpc.StaticAddressLoopInSwapState_SIGN_HTLC_TX,
rpcSwap.GetStaticLoopInState(),
)
require.Equal(t, looprpc.SwapState_INITIATED, rpcSwap.State)
require.Equal(t, testnetAddr.EncodeAddress(), rpcSwap.HtlcAddressP2Wsh)
require.Empty(t, rpcSwap.HtlcAddressP2Tr)
}
// TestStaticAddressLoopInMarshallFailuresLeaveLegacyFieldsDefault asserts that
// static loop-in failures keep default legacy fields while preserving the
// precise static state.
func TestStaticAddressLoopInMarshallFailuresLeaveLegacyFieldsDefault(
t *testing.T) {
tests := []struct {
name string
state fsm.StateType
wantStaticState looprpc.StaticAddressLoopInSwapState
}{
{
name: "failed",
state: loopin.Failed,
wantStaticState: looprpc.
StaticAddressLoopInSwapState_FAILED_STATIC_ADDRESS_SWAP,
},
{
name: "succeeded transitioning failed",
state: loopin.SucceededTransitioningFailed,
wantStaticState: looprpc.
StaticAddressLoopInSwapState_SUCCEEDED_TRANSITIONING_FAILED,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
server, staticLoopIn := newGenericStaticLoopInServer(t)
staticLoopIn.SetState(test.state)
loopSwap, err := server.staticAddressLoopInSwapInfo(
t.Context(), staticLoopIn,
)
require.NoError(t, err)
rpcSwap, err := server.marshallSwap(t.Context(), loopSwap)
require.NoError(t, err)
require.Equal(t, looprpc.SwapState_INITIATED, rpcSwap.State)
require.Equal(
t, looprpc.FailureReason_FAILURE_REASON_NONE,
rpcSwap.FailureReason,
)
require.Equal(
t, test.wantStaticState,
rpcSwap.GetStaticLoopInState(),
)
})
}
}
// TestStaticAddressLoopInMarshallRejectsMissingHtlcAddress protects the
// fail-closed HTLC-address invariant for static loop-ins missing the P2WSH
// address required by the client-facing RPC representation.
func TestStaticAddressLoopInMarshallRejectsMissingHtlcAddress(t *testing.T) {
_, taprootAddress := newTestStaticAddressParams(t)
server := &swapClientServer{}
loopSwap := &loop.SwapInfo{
SwapStateData: loopdb.SwapStateData{
State: loopdb.StateInitiated,
},
SwapContract: loopdb.SwapContract{
InitiationTime: time.Now(),
},
LastUpdate: time.Now(),
SwapHash: lntypes.Hash{1},
SwapType: swap.TypeStaticAddressLoopIn,
StaticAddressLoopInState: loopin.SignHtlcTx,
HtlcAddressP2TR: taprootAddress,
}
_, err := server.marshallSwap(t.Context(), loopSwap)
require.ErrorContains(t, err, "missing static address loop-in P2WSH HTLC address")
}
// TestStaticAddressLoopInSwapInfoFailsClosedWhenHtlcKeysMissing protects the
// HTLC-address construction invariant that missing cooperative keys must not
// produce monitorable swap info.
func TestStaticAddressLoopInSwapInfoFailsClosedWhenHtlcKeysMissing(t *testing.T) {
server, staticLoopIn := newGenericStaticLoopInServer(t)
staticLoopIn.ClientPubkey = nil
_, err := server.staticAddressLoopInSwapInfo(t.Context(), staticLoopIn)
require.ErrorContains(
t, err, "missing static address loop-in client HTLC key",
)
}
// TestMonitorSnapshotIncludesStaticAddressLoopIns protects the monitor snapshot
// invariant that pending static loop-ins are included alongside cached generic
// swaps with their static state and swap-specific HTLC address.
func TestMonitorSnapshotIncludesStaticAddressLoopIns(t *testing.T) {
ctx := t.Context()
server, staticLoopIn := newGenericStaticLoopInServer(t)
pendingSwaps, completedSwaps := server.monitorCachedSwaps()
err := server.appendStaticAddressLoopInMonitorSnapshot(
ctx, &pendingSwaps, &completedSwaps,
)
require.NoError(t, err)
require.Empty(t, completedSwaps)
require.Len(t, pendingSwaps, 1)
require.Equal(t, staticLoopIn.SwapHash, pendingSwaps[0].SwapHash)
require.Equal(t, swap.TypeStaticAddressLoopIn, pendingSwaps[0].SwapType)
require.Equal(
t, staticLoopIn.GetState(),
pendingSwaps[0].StaticAddressLoopInState,
)
assertStaticLoopInUsesSwapHtlcAddress(t, staticLoopIn, pendingSwaps[0])
}
// TestMonitorSnapshotIncludesFinalStaticAddressLoopIns protects the monitor
// snapshot invariant that exact final static loop-in states are completed swaps.
func TestMonitorSnapshotIncludesFinalStaticAddressLoopIns(t *testing.T) {
server, staticLoopIn := newGenericStaticLoopInServer(t)
staticLoopIn.SetState(loopin.Succeeded)
pendingSwaps, completedSwaps := server.monitorCachedSwaps()
err := server.appendStaticAddressLoopInMonitorSnapshot(
t.Context(), &pendingSwaps, &completedSwaps,
)
require.NoError(t, err)
require.Empty(t, pendingSwaps)
require.Len(t, completedSwaps, 1)
}
// TestStaticLoopInStatusUpdaterUsesSwapHtlcAddress protects the live-update
// invariant that static loop-in status events derive the HTLC address from the
// swap, not from reusable static address parameters.
func TestStaticLoopInStatusUpdaterUsesSwapHtlcAddress(t *testing.T) {
ctx := t.Context()
_, staticLoopIn := newGenericStaticLoopInServer(t)
staticLoopIn.AddressParams = nil
statusChan := make(chan loop.SwapInfo, 1)
updater := &staticLoopInStatusUpdater{
statusChan: statusChan,
mainCtx: ctx,
chainParams: &chaincfg.TestNet3Params,
}
err := updater.sendUpdate(ctx, staticLoopIn)
require.NoError(t, err)
swapInfo := <-statusChan
assertStaticLoopInUsesSwapHtlcAddress(t, staticLoopIn, swapInfo)
}
// TestMonitorSuppressesStaticAddressLoopInSnapshotLiveDuplicate protects the
// monitor race invariant that live static loop-in updates arriving during the
// initial snapshot are deduplicated without dropping newer progress.
func TestMonitorSuppressesStaticAddressLoopInSnapshotLiveDuplicate(t *testing.T) {
logger := btclog.NewSLogger(
btclog.NewDefaultHandler(os.Stdout),
)
setLogger(logger.SubSystem(Subsystem))
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
mainCtx, mainCancel := context.WithCancel(t.Context())
defer mainCancel()
server, staticLoopIn, store := newGenericStaticLoopInServerWithStore(t)
server.statusChan = make(chan loop.SwapInfo)
server.subscribers = make(map[int]chan<- any)
server.mainCtx = mainCtx
snapshotStarted := make(chan struct{}, 1)
releaseSnapshot := make(chan struct{})
store.beforeGet = func() {
select {
case snapshotStarted <- struct{}{}:
default:
}
}
store.waitGet = releaseSnapshot
go server.processStatusUpdates(mainCtx)
monitorServer := &testMonitorServer{
ctx: ctx,
sent: make(chan *looprpc.SwapStatus, 3),
}
errChan := make(chan error, 1)
go func() {
errChan <- server.Monitor(&looprpc.MonitorRequest{}, monitorServer)
}()
select {
case <-snapshotStarted:
case <-ctx.Done():
t.Fatal(ctx.Err())
}
staticUpdate, err := server.staticAddressLoopInSwapInfo(ctx, staticLoopIn)
require.NoError(t, err)
staleUpdate := *staticUpdate
staleUpdate.State = loopdb.StateInitiated
staleUpdate.LastUpdate = staticUpdate.LastUpdate.Add(-time.Second)
server.statusChan <- staleUpdate
server.statusChan <- *staticUpdate
close(releaseSnapshot)
first := receiveMonitorUpdate(t, ctx, monitorServer.sent)
require.Equal(t, staticLoopIn.SwapHash[:], first.IdBytes)
require.Equal(t, looprpc.SwapType_STATIC_LOOP_IN, first.Type)
require.Equal(
t, looprpc.StaticAddressLoopInSwapState_PAYMENT_RECEIVED,
first.GetStaticLoopInState(),
)
nextUpdate := *staticUpdate
nextUpdate.State = loopdb.StateSuccess
nextUpdate.StaticAddressLoopInState = loopin.Succeeded
nextUpdate.LastUpdate = staticUpdate.LastUpdate.Add(time.Second)
server.statusChan <- nextUpdate
second := receiveMonitorUpdate(t, ctx, monitorServer.sent)
require.Equal(t, staticLoopIn.SwapHash[:], second.IdBytes)
require.Equal(t, looprpc.SwapType_STATIC_LOOP_IN, second.Type)
require.Equal(
t, looprpc.StaticAddressLoopInSwapState_SUCCEEDED,
second.GetStaticLoopInState(),
)
cancel()
require.NoError(t, <-errChan)
}
// TestStaticAddressLoopInHighWaterSuppressesExactDuplicate protects the
// high-water dedup invariant that an initial live update identical to the
// snapshot is treated as stale.
func TestStaticAddressLoopInHighWaterSuppressesExactDuplicate(t *testing.T) {
swapHash := lntypes.Hash{1, 2, 3}
lastUpdate := time.Unix(100, 0).UTC()
snapshot := loop.SwapInfo{
SwapHash: swapHash,
SwapType: swap.TypeStaticAddressLoopIn,
LastUpdate: lastUpdate,
StaticAddressLoopInState: loopin.PaymentReceived,
}
highWater := staticAddressLoopInMonitorHighWater([]loop.SwapInfo{
snapshot,
})
isStale := isInitialStaticAddressLoopInStale(highWater, snapshot)
require.True(t, isStale)
}
// TestStaticAddressLoopInHighWaterKeepsSameTimeDifferentState protects the
// high-water timing invariant that equal timestamps do not hide a distinct
// static loop-in state transition.
func TestStaticAddressLoopInHighWaterKeepsSameTimeDifferentState(t *testing.T) {
swapHash := lntypes.Hash{1, 2, 3}
lastUpdate := time.Unix(100, 0).UTC()
snapshot := loop.SwapInfo{
SwapHash: swapHash,
SwapType: swap.TypeStaticAddressLoopIn,
LastUpdate: lastUpdate,
StaticAddressLoopInState: loopin.PaymentReceived,
}
liveUpdate := snapshot
liveUpdate.StaticAddressLoopInState = loopin.Succeeded
highWater := staticAddressLoopInMonitorHighWater([]loop.SwapInfo{
snapshot,
})
isStale := isInitialStaticAddressLoopInStale(highWater, liveUpdate)
require.False(t, isStale)
}
// TestStaticAddressLoopInHighWaterSuppressesOlderStaticOnly protects the
// high-water cache invariant that stale suppression applies only to static
// loop-ins and cannot filter generic swap updates.
func TestStaticAddressLoopInHighWaterSuppressesOlderStaticOnly(t *testing.T) {
swapHash := lntypes.Hash{1, 2, 3}
lastUpdate := time.Unix(100, 0).UTC()
snapshot := loop.SwapInfo{
SwapHash: swapHash,
SwapType: swap.TypeStaticAddressLoopIn,
LastUpdate: lastUpdate,
StaticAddressLoopInState: loopin.PaymentReceived,
}
highWater := staticAddressLoopInMonitorHighWater([]loop.SwapInfo{
snapshot,
})
olderStatic := snapshot
olderStatic.LastUpdate = lastUpdate.Add(-time.Second)
olderStatic.StaticAddressLoopInState = loopin.Succeeded
nonStatic := olderStatic
nonStatic.SwapType = swap.TypeOut
staticStale := isInitialStaticAddressLoopInStale(highWater, olderStatic)
nonStaticStale := isInitialStaticAddressLoopInStale(
highWater, nonStatic,
)
require.True(t, staticStale)
require.False(t, nonStaticStale)
}
// TestStaticAddressLoopInStatusUpdateDoesNotEnterGenericSwapCache protects the
// cache isolation invariant that static loop-in live updates reach subscribers
// without entering the generic swap cache.
func TestStaticAddressLoopInStatusUpdateDoesNotEnterGenericSwapCache(t *testing.T) {
ctx := t.Context()
server, staticLoopIn := newGenericStaticLoopInServer(t)
server.statusChan = make(chan loop.SwapInfo)
updates := make(chan any, 1)
server.subscribers = map[int]chan<- any{0: updates}
mainCtx, cancel := context.WithCancel(ctx)
defer cancel()
go server.processStatusUpdates(mainCtx)
staticUpdate, err := server.staticAddressLoopInSwapInfo(ctx, staticLoopIn)
require.NoError(t, err)
server.statusChan <- *staticUpdate
select {
case update := <-updates:
require.Equal(t, staticLoopIn.SwapHash, update.(loop.SwapInfo).SwapHash)
case <-ctx.Done():
t.Fatal(ctx.Err())
}
server.swapsLock.Lock()
_, cached := server.swaps[staticLoopIn.SwapHash]
server.swapsLock.Unlock()
require.False(t, cached)
}
func newGenericStaticLoopInServer(t *testing.T) (*swapClientServer,
*loopin.StaticAddressLoopIn) {
server, staticLoopIn, _ := newGenericStaticLoopInServerWithStore(t)
return server, staticLoopIn
}
func newTestStaticAddressParams(t *testing.T) (*script.Parameters,
*btcutil.AddressTaproot) {
t.Helper()
const staticAddressExpiry = uint32(25)
_, staticClientPubkey := mock_lnd.CreateKey(12)
_, staticServerPubkey := mock_lnd.CreateKey(13)
staticAddress, err := script.NewStaticAddress(
input.MuSig2Version100RC2, int64(staticAddressExpiry),
staticClientPubkey, staticServerPubkey,
)
require.NoError(t, err)
staticPkScript, err := staticAddress.StaticAddressScript()
require.NoError(t, err)
taprootAddress, err := btcutil.NewAddressTaproot(
schnorr.SerializePubKey(staticAddress.TaprootKey),
&chaincfg.TestNet3Params,
)
require.NoError(t, err)
return &script.Parameters{
ClientPubkey: staticClientPubkey,
ServerPubkey: staticServerPubkey,
Expiry: staticAddressExpiry,
PkScript: staticPkScript,
}, taprootAddress
}
func newGenericStaticLoopInServerWithStore(t *testing.T) (*swapClientServer,
*loopin.StaticAddressLoopIn, *mockStaticAddressLoopInStore) {
t.Helper()
_, clientPubkey := mock_lnd.CreateKey(10)
_, serverPubkey := mock_lnd.CreateKey(11)
addressParams, _ := newTestStaticAddressParams(t)
depositOutpoint := wire.OutPoint{
Hash: chainhash.Hash{12, 13, 14},
Index: 2,
}
staticDeposit := &deposit.Deposit{
OutPoint: depositOutpoint,
Value: 51_000,
}
lastHop := route.Vertex{7, 8, 9}
staticLoopIn := &loopin.StaticAddressLoopIn{
SwapHash: lntypes.Hash{1, 2, 3},
HtlcCltvExpiry: 700,
InitiationTime: time.Unix(100, 0).UTC(),
LastUpdateTime: time.Unix(200, 0).UTC(),
Label: "static-loop-in",
ClientPubkey: clientPubkey,
ServerPubkey: serverPubkey,
LastHop: lastHop[:],
QuotedSwapFee: 1_111,
SelectedAmount: 50_000,
DepositOutpoints: []string{depositOutpoint.String()},
Deposits: []*deposit.Deposit{staticDeposit},
AddressParams: addressParams,
}
staticLoopIn.SetState(loopin.PaymentReceived)
depositStore := &mockDepositStore{
byOutpoint: map[string]*deposit.Deposit{
depositOutpoint.String(): staticDeposit,
},
}
loopInStore := &mockStaticAddressLoopInStore{
swaps: []*loopin.StaticAddressLoopIn{staticLoopIn},
}
staticLoopInManager, err := loopin.NewManager(&loopin.Config{
Store: loopInStore,
DepositManager: deposit.NewManager(&deposit.ManagerConfig{
Store: depositStore,
}),
}, 1)
require.NoError(t, err)
return &swapClientServer{
network: lndclient.NetworkTestnet,
swaps: make(map[lntypes.Hash]loop.SwapInfo),
staticLoopInManager: staticLoopInManager,
}, staticLoopIn, loopInStore
}
// assertStaticLoopInUsesSwapHtlcAddress verifies the static loop-in uses the
// swap HTLC P2WSH address expected by the fixture.
func assertStaticLoopInUsesSwapHtlcAddress(t *testing.T,
staticLoopIn *loopin.StaticAddressLoopIn, swapInfo loop.SwapInfo) {
t.Helper()
expectedAddress, err := staticAddressLoopInHtlcAddress(
staticLoopIn, &chaincfg.TestNet3Params,
)
require.NoError(t, err)
require.Nil(t, swapInfo.HtlcAddressP2TR)
require.NotNil(t, swapInfo.HtlcAddressP2WSH)
require.Equal(
t, expectedAddress.EncodeAddress(),
swapInfo.HtlcAddressP2WSH.EncodeAddress(),
)
}
// testMonitorServer implements the monitor stream interface for tests.
type testMonitorServer struct {
ctx context.Context
sent chan *looprpc.SwapStatus
}
// Send forwards monitor updates to the test channel until the context is canceled.
func (s *testMonitorServer) Send(swapStatus *looprpc.SwapStatus) error {
select {
case s.sent <- swapStatus:
return nil
case <-s.ctx.Done():
return s.ctx.Err()
}
}
// SetHeader is a no-op stub that satisfies the monitor stream interface in tests.
func (s *testMonitorServer) SetHeader(metadata.MD) error {
return nil
}
// SendHeader is a no-op stub that satisfies the monitor stream interface in tests.
func (s *testMonitorServer) SendHeader(metadata.MD) error {
return nil
}
// SetTrailer is a no-op stub that satisfies the monitor stream interface in tests.
func (s *testMonitorServer) SetTrailer(metadata.MD) {}
// Context returns the stream context used by the test monitor server.
func (s *testMonitorServer) Context() context.Context {
return s.ctx
}
// SendMsg is a no-op stub that satisfies the monitor stream interface in tests.
func (s *testMonitorServer) SendMsg(any) error {
return nil
}
// RecvMsg is a no-op stub that satisfies the monitor stream interface in tests.
func (s *testMonitorServer) RecvMsg(any) error {
return nil
}
// receiveMonitorUpdate waits for a monitor update or fails if the context is canceled.
func receiveMonitorUpdate(t *testing.T, ctx context.Context,
updates <-chan *looprpc.SwapStatus) *looprpc.SwapStatus {
t.Helper()
select {
case update := <-updates:
return update
case <-ctx.Done():
t.Fatal(ctx.Err())
}
return nil
}
// mockStaticAddressLoopInStore is a minimal in-memory loop-in store for RPC
// response mapping tests.
type mockStaticAddressLoopInStore struct {
swaps []*loopin.StaticAddressLoopIn
swaps []*loopin.StaticAddressLoopIn
beforeGet func()
waitGet <-chan struct{}
}
// CreateLoopIn satisfies the static loop-in store interface.
@ -523,9 +1070,20 @@ func (s *mockStaticAddressLoopInStore) UpdateLoopIn(_ context.Context,
// GetStaticAddressLoopInSwapsByStates returns the configured loop-ins.
func (s *mockStaticAddressLoopInStore) GetStaticAddressLoopInSwapsByStates(
_ context.Context, _ []fsm.StateType) ([]*loopin.StaticAddressLoopIn,
ctx context.Context, _ []fsm.StateType) ([]*loopin.StaticAddressLoopIn,
error) {
if s.beforeGet != nil {
s.beforeGet()
}
if s.waitGet != nil {
select {
case <-s.waitGet:
case <-ctx.Done():
return nil, ctx.Err()
}
}
return s.swaps, nil
}

View file

@ -80,6 +80,8 @@ const (
SwapType_LOOP_OUT SwapType = 0
// LOOP_IN indicates a loop in swap (on-chain to off-chain)
SwapType_LOOP_IN SwapType = 1
// STATIC_LOOP_IN indicates a static address loop in swap.
SwapType_STATIC_LOOP_IN SwapType = 2
)
// Enum value maps for SwapType.
@ -87,10 +89,12 @@ var (
SwapType_name = map[int32]string{
0: "LOOP_OUT",
1: "LOOP_IN",
2: "STATIC_LOOP_IN",
}
SwapType_value = map[string]int32{
"LOOP_OUT": 0,
"LOOP_IN": 1,
"LOOP_OUT": 0,
"LOOP_IN": 1,
"STATIC_LOOP_IN": 2,
}
)
@ -1474,8 +1478,12 @@ type SwapStatus struct {
IdBytes []byte `protobuf:"bytes,11,opt,name=id_bytes,json=idBytes,proto3" json:"id_bytes,omitempty"`
// The type of the swap.
Type SwapType `protobuf:"varint,3,opt,name=type,proto3,enum=looprpc.SwapType" json:"type,omitempty"`
// State the swap is currently in, see State enum.
// Generic loop-in/loop-out state for swaps.
State SwapState `protobuf:"varint,4,opt,name=state,proto3,enum=looprpc.SwapState" json:"state,omitempty"`
// Types that are valid to be assigned to StaticLoopInStateOptional:
//
// *SwapStatus_StaticLoopInState
StaticLoopInStateOptional isSwapStatus_StaticLoopInStateOptional `protobuf_oneof:"static_loop_in_state_optional"`
// A failure reason for the swap, only set if the swap has failed.
FailureReason FailureReason `protobuf:"varint,14,opt,name=failure_reason,json=failureReason,proto3,enum=looprpc.FailureReason" json:"failure_reason,omitempty"`
// Initiation time of the swap.
@ -1578,6 +1586,22 @@ func (x *SwapStatus) GetState() SwapState {
return SwapState_INITIATED
}
func (x *SwapStatus) GetStaticLoopInStateOptional() isSwapStatus_StaticLoopInStateOptional {
if x != nil {
return x.StaticLoopInStateOptional
}
return nil
}
func (x *SwapStatus) GetStaticLoopInState() StaticAddressLoopInSwapState {
if x != nil {
if x, ok := x.StaticLoopInStateOptional.(*SwapStatus_StaticLoopInState); ok {
return x.StaticLoopInState
}
}
return StaticAddressLoopInSwapState_UNKNOWN_STATIC_ADDRESS_SWAP_STATE
}
func (x *SwapStatus) GetFailureReason() FailureReason {
if x != nil {
return x.FailureReason
@ -1670,6 +1694,17 @@ func (x *SwapStatus) GetAssetInfo() *AssetLoopOutInfo {
return nil
}
type isSwapStatus_StaticLoopInStateOptional interface {
isSwapStatus_StaticLoopInStateOptional()
}
type SwapStatus_StaticLoopInState struct {
// Static address loop-in FSM state when type is STATIC_LOOP_IN.
StaticLoopInState StaticAddressLoopInSwapState `protobuf:"varint,20,opt,name=static_loop_in_state,json=staticLoopInState,proto3,enum=looprpc.StaticAddressLoopInSwapState,oneof"`
}
func (*SwapStatus_StaticLoopInState) isSwapStatus_StaticLoopInStateOptional() {}
type ListSwapsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Optional filter to only return swaps that match the filter.
@ -6704,14 +6739,15 @@ const file_client_proto_rawDesc = "" +
"\x12htlc_address_p2wsh\x18\x05 \x01(\tR\x10htlcAddressP2wsh\x12*\n" +
"\x11htlc_address_p2tr\x18\a \x01(\tR\x0fhtlcAddressP2tr\x12%\n" +
"\x0eserver_message\x18\x06 \x01(\tR\rserverMessageJ\x04\b\x04\x10\x05\"\x10\n" +
"\x0eMonitorRequest\"\xb1\x05\n" +
"\x0eMonitorRequest\"\xac\x06\n" +
"\n" +
"SwapStatus\x12\x10\n" +
"\x03amt\x18\x01 \x01(\x03R\x03amt\x12\x12\n" +
"\x02id\x18\x02 \x01(\tB\x02\x18\x01R\x02id\x12\x19\n" +
"\bid_bytes\x18\v \x01(\fR\aidBytes\x12%\n" +
"\x04type\x18\x03 \x01(\x0e2\x11.looprpc.SwapTypeR\x04type\x12(\n" +
"\x05state\x18\x04 \x01(\x0e2\x12.looprpc.SwapStateR\x05state\x12=\n" +
"\x05state\x18\x04 \x01(\x0e2\x12.looprpc.SwapStateR\x05state\x12X\n" +
"\x14static_loop_in_state\x18\x14 \x01(\x0e2%.looprpc.StaticAddressLoopInSwapStateH\x00R\x11staticLoopInState\x12=\n" +
"\x0efailure_reason\x18\x0e \x01(\x0e2\x16.looprpc.FailureReasonR\rfailureReason\x12'\n" +
"\x0finitiation_time\x18\x05 \x01(\x03R\x0einitiationTime\x12(\n" +
"\x10last_update_time\x18\x06 \x01(\x03R\x0elastUpdateTime\x12%\n" +
@ -6727,7 +6763,8 @@ const file_client_proto_rawDesc = "" +
"\x11outgoing_chan_set\x18\x11 \x03(\x04R\x0foutgoingChanSet\x12\x14\n" +
"\x05label\x18\x0f \x01(\tR\x05label\x128\n" +
"\n" +
"asset_info\x18\x13 \x01(\v2\x19.looprpc.AssetLoopOutInfoR\tassetInfo\"s\n" +
"asset_info\x18\x13 \x01(\v2\x19.looprpc.AssetLoopOutInfoR\tassetInfoB\x1f\n" +
"\x1dstatic_loop_in_state_optional\"s\n" +
"\x10ListSwapsRequest\x12B\n" +
"\x10list_swap_filter\x18\x01 \x01(\v2\x18.looprpc.ListSwapsFilterR\x0elistSwapFilter\x12\x1b\n" +
"\tmax_swaps\x18\x02 \x01(\x04R\bmaxSwaps\"\xf1\x02\n" +
@ -7088,10 +7125,11 @@ const file_client_proto_rawDesc = "" +
"\x13asset_cost_offchain\x18\x03 \x01(\x04R\x11assetCostOffchain*;\n" +
"\vAddressType\x12\x18\n" +
"\x14ADDRESS_TYPE_UNKNOWN\x10\x00\x12\x12\n" +
"\x0eTAPROOT_PUBKEY\x10\x01*%\n" +
"\x0eTAPROOT_PUBKEY\x10\x01*9\n" +
"\bSwapType\x12\f\n" +
"\bLOOP_OUT\x10\x00\x12\v\n" +
"\aLOOP_IN\x10\x01*s\n" +
"\aLOOP_IN\x10\x01\x12\x12\n" +
"\x0eSTATIC_LOOP_IN\x10\x02*s\n" +
"\tSwapState\x12\r\n" +
"\tINITIATED\x10\x00\x12\x15\n" +
"\x11PREIMAGE_REVEALED\x10\x01\x12\x12\n" +
@ -7321,121 +7359,122 @@ var file_client_proto_depIdxs = []int32{
91, // 4: looprpc.LoopInRequest.route_hints:type_name -> looprpc.RouteHint
1, // 5: looprpc.SwapStatus.type:type_name -> looprpc.SwapType
2, // 6: looprpc.SwapStatus.state:type_name -> looprpc.SwapState
3, // 7: looprpc.SwapStatus.failure_reason:type_name -> looprpc.FailureReason
88, // 8: looprpc.SwapStatus.asset_info:type_name -> looprpc.AssetLoopOutInfo
20, // 9: looprpc.ListSwapsRequest.list_swap_filter:type_name -> looprpc.ListSwapsFilter
9, // 10: looprpc.ListSwapsFilter.swap_type:type_name -> looprpc.ListSwapsFilter.SwapTypeFilter
18, // 11: looprpc.ListSwapsResponse.swaps:type_name -> looprpc.SwapStatus
24, // 12: looprpc.SweepHtlcResponse.not_requested:type_name -> looprpc.PublishNotRequested
25, // 13: looprpc.SweepHtlcResponse.published:type_name -> looprpc.PublishSucceeded
26, // 14: looprpc.SweepHtlcResponse.failed:type_name -> looprpc.PublishFailed
91, // 15: looprpc.QuoteRequest.loop_in_route_hints:type_name -> looprpc.RouteHint
85, // 16: looprpc.QuoteRequest.asset_info:type_name -> looprpc.AssetLoopOutRequest
86, // 17: looprpc.OutQuoteResponse.asset_rfq_info:type_name -> looprpc.AssetRfqInfo
91, // 18: looprpc.ProbeRequest.route_hints:type_name -> looprpc.RouteHint
40, // 19: looprpc.TokensResponse.tokens:type_name -> looprpc.L402Token
41, // 20: looprpc.GetInfoResponse.loop_out_stats:type_name -> looprpc.LoopStats
41, // 21: looprpc.GetInfoResponse.loop_in_stats:type_name -> looprpc.LoopStats
47, // 22: looprpc.LiquidityParameters.rules:type_name -> looprpc.LiquidityRule
0, // 23: looprpc.LiquidityParameters.account_addr_type:type_name -> looprpc.AddressType
89, // 24: looprpc.LiquidityParameters.easy_asset_params:type_name -> looprpc.LiquidityParameters.EasyAssetParamsEntry
4, // 25: looprpc.LiquidityParameters.loop_in_source:type_name -> looprpc.LoopInSource
1, // 26: looprpc.LiquidityRule.swap_type:type_name -> looprpc.SwapType
5, // 27: looprpc.LiquidityRule.type:type_name -> looprpc.LiquidityRuleType
45, // 28: looprpc.SetLiquidityParamsRequest.parameters:type_name -> looprpc.LiquidityParameters
6, // 29: looprpc.Disqualified.reason:type_name -> looprpc.AutoReason
14, // 30: looprpc.SuggestSwapsResponse.loop_out:type_name -> looprpc.LoopOutRequest
15, // 31: looprpc.SuggestSwapsResponse.loop_in:type_name -> looprpc.LoopInRequest
83, // 32: looprpc.SuggestSwapsResponse.static_loop_in:type_name -> looprpc.StaticAddressLoopInRequest
51, // 33: looprpc.SuggestSwapsResponse.disqualified:type_name -> looprpc.Disqualified
57, // 34: looprpc.ListReservationsResponse.reservations:type_name -> looprpc.ClientReservation
64, // 35: looprpc.ListInstantOutsResponse.swaps:type_name -> looprpc.InstantOut
69, // 36: looprpc.ListUnspentDepositsResponse.utxos:type_name -> looprpc.Utxo
92, // 37: looprpc.WithdrawDepositsRequest.outpoints:type_name -> lnrpc.OutPoint
7, // 38: looprpc.ListStaticAddressDepositsRequest.state_filter:type_name -> looprpc.DepositState
80, // 39: looprpc.ListStaticAddressDepositsResponse.filtered_deposits:type_name -> looprpc.Deposit
81, // 40: looprpc.ListStaticAddressWithdrawalResponse.withdrawals:type_name -> looprpc.StaticAddressWithdrawal
82, // 41: looprpc.ListStaticAddressSwapsResponse.swaps:type_name -> looprpc.StaticAddressLoopInSwap
7, // 42: looprpc.Deposit.state:type_name -> looprpc.DepositState
80, // 43: looprpc.StaticAddressWithdrawal.deposits:type_name -> looprpc.Deposit
8, // 44: looprpc.StaticAddressLoopInSwap.state:type_name -> looprpc.StaticAddressLoopInSwapState
80, // 45: looprpc.StaticAddressLoopInSwap.deposits:type_name -> looprpc.Deposit
91, // 46: looprpc.StaticAddressLoopInRequest.route_hints:type_name -> looprpc.RouteHint
80, // 47: looprpc.StaticAddressLoopInResponse.used_deposits:type_name -> looprpc.Deposit
87, // 48: looprpc.AssetRfqInfo.prepay_asset_rate:type_name -> looprpc.FixedPoint
87, // 49: looprpc.AssetRfqInfo.swap_asset_rate:type_name -> looprpc.FixedPoint
46, // 50: looprpc.LiquidityParameters.EasyAssetParamsEntry.value:type_name -> looprpc.EasyAssetAutoloopParams
14, // 51: looprpc.SwapClient.LoopOut:input_type -> looprpc.LoopOutRequest
15, // 52: looprpc.SwapClient.LoopIn:input_type -> looprpc.LoopInRequest
17, // 53: looprpc.SwapClient.Monitor:input_type -> looprpc.MonitorRequest
19, // 54: looprpc.SwapClient.ListSwaps:input_type -> looprpc.ListSwapsRequest
22, // 55: looprpc.SwapClient.SweepHtlc:input_type -> looprpc.SweepHtlcRequest
27, // 56: looprpc.SwapClient.SwapInfo:input_type -> looprpc.SwapInfoRequest
53, // 57: looprpc.SwapClient.AbandonSwap:input_type -> looprpc.AbandonSwapRequest
28, // 58: looprpc.SwapClient.LoopOutTerms:input_type -> looprpc.TermsRequest
31, // 59: looprpc.SwapClient.LoopOutQuote:input_type -> looprpc.QuoteRequest
28, // 60: looprpc.SwapClient.GetLoopInTerms:input_type -> looprpc.TermsRequest
31, // 61: looprpc.SwapClient.GetLoopInQuote:input_type -> looprpc.QuoteRequest
34, // 62: looprpc.SwapClient.Probe:input_type -> looprpc.ProbeRequest
36, // 63: looprpc.SwapClient.GetL402Tokens:input_type -> looprpc.TokensRequest
36, // 64: looprpc.SwapClient.GetLsatTokens:input_type -> looprpc.TokensRequest
38, // 65: looprpc.SwapClient.FetchL402Token:input_type -> looprpc.FetchL402TokenRequest
42, // 66: looprpc.SwapClient.GetInfo:input_type -> looprpc.GetInfoRequest
12, // 67: looprpc.SwapClient.StopDaemon:input_type -> looprpc.StopDaemonRequest
44, // 68: looprpc.SwapClient.GetLiquidityParams:input_type -> looprpc.GetLiquidityParamsRequest
48, // 69: looprpc.SwapClient.SetLiquidityParams:input_type -> looprpc.SetLiquidityParamsRequest
50, // 70: looprpc.SwapClient.SuggestSwaps:input_type -> looprpc.SuggestSwapsRequest
55, // 71: looprpc.SwapClient.ListReservations:input_type -> looprpc.ListReservationsRequest
58, // 72: looprpc.SwapClient.InstantOut:input_type -> looprpc.InstantOutRequest
60, // 73: looprpc.SwapClient.InstantOutQuote:input_type -> looprpc.InstantOutQuoteRequest
62, // 74: looprpc.SwapClient.ListInstantOuts:input_type -> looprpc.ListInstantOutsRequest
65, // 75: looprpc.SwapClient.NewStaticAddress:input_type -> looprpc.NewStaticAddressRequest
67, // 76: looprpc.SwapClient.ListUnspentDeposits:input_type -> looprpc.ListUnspentDepositsRequest
70, // 77: looprpc.SwapClient.WithdrawDeposits:input_type -> looprpc.WithdrawDepositsRequest
72, // 78: looprpc.SwapClient.ListStaticAddressDeposits:input_type -> looprpc.ListStaticAddressDepositsRequest
74, // 79: looprpc.SwapClient.ListStaticAddressWithdrawals:input_type -> looprpc.ListStaticAddressWithdrawalRequest
76, // 80: looprpc.SwapClient.ListStaticAddressSwaps:input_type -> looprpc.ListStaticAddressSwapsRequest
78, // 81: looprpc.SwapClient.GetStaticAddressSummary:input_type -> looprpc.StaticAddressSummaryRequest
83, // 82: looprpc.SwapClient.StaticAddressLoopIn:input_type -> looprpc.StaticAddressLoopInRequest
10, // 83: looprpc.SwapClient.StaticOpenChannel:input_type -> looprpc.StaticOpenChannelRequest
16, // 84: looprpc.SwapClient.LoopOut:output_type -> looprpc.SwapResponse
16, // 85: looprpc.SwapClient.LoopIn:output_type -> looprpc.SwapResponse
18, // 86: looprpc.SwapClient.Monitor:output_type -> looprpc.SwapStatus
21, // 87: looprpc.SwapClient.ListSwaps:output_type -> looprpc.ListSwapsResponse
23, // 88: looprpc.SwapClient.SweepHtlc:output_type -> looprpc.SweepHtlcResponse
18, // 89: looprpc.SwapClient.SwapInfo:output_type -> looprpc.SwapStatus
54, // 90: looprpc.SwapClient.AbandonSwap:output_type -> looprpc.AbandonSwapResponse
30, // 91: looprpc.SwapClient.LoopOutTerms:output_type -> looprpc.OutTermsResponse
33, // 92: looprpc.SwapClient.LoopOutQuote:output_type -> looprpc.OutQuoteResponse
29, // 93: looprpc.SwapClient.GetLoopInTerms:output_type -> looprpc.InTermsResponse
32, // 94: looprpc.SwapClient.GetLoopInQuote:output_type -> looprpc.InQuoteResponse
35, // 95: looprpc.SwapClient.Probe:output_type -> looprpc.ProbeResponse
37, // 96: looprpc.SwapClient.GetL402Tokens:output_type -> looprpc.TokensResponse
37, // 97: looprpc.SwapClient.GetLsatTokens:output_type -> looprpc.TokensResponse
39, // 98: looprpc.SwapClient.FetchL402Token:output_type -> looprpc.FetchL402TokenResponse
43, // 99: looprpc.SwapClient.GetInfo:output_type -> looprpc.GetInfoResponse
13, // 100: looprpc.SwapClient.StopDaemon:output_type -> looprpc.StopDaemonResponse
45, // 101: looprpc.SwapClient.GetLiquidityParams:output_type -> looprpc.LiquidityParameters
49, // 102: looprpc.SwapClient.SetLiquidityParams:output_type -> looprpc.SetLiquidityParamsResponse
52, // 103: looprpc.SwapClient.SuggestSwaps:output_type -> looprpc.SuggestSwapsResponse
56, // 104: looprpc.SwapClient.ListReservations:output_type -> looprpc.ListReservationsResponse
59, // 105: looprpc.SwapClient.InstantOut:output_type -> looprpc.InstantOutResponse
61, // 106: looprpc.SwapClient.InstantOutQuote:output_type -> looprpc.InstantOutQuoteResponse
63, // 107: looprpc.SwapClient.ListInstantOuts:output_type -> looprpc.ListInstantOutsResponse
66, // 108: looprpc.SwapClient.NewStaticAddress:output_type -> looprpc.NewStaticAddressResponse
68, // 109: looprpc.SwapClient.ListUnspentDeposits:output_type -> looprpc.ListUnspentDepositsResponse
71, // 110: looprpc.SwapClient.WithdrawDeposits:output_type -> looprpc.WithdrawDepositsResponse
73, // 111: looprpc.SwapClient.ListStaticAddressDeposits:output_type -> looprpc.ListStaticAddressDepositsResponse
75, // 112: looprpc.SwapClient.ListStaticAddressWithdrawals:output_type -> looprpc.ListStaticAddressWithdrawalResponse
77, // 113: looprpc.SwapClient.ListStaticAddressSwaps:output_type -> looprpc.ListStaticAddressSwapsResponse
79, // 114: looprpc.SwapClient.GetStaticAddressSummary:output_type -> looprpc.StaticAddressSummaryResponse
84, // 115: looprpc.SwapClient.StaticAddressLoopIn:output_type -> looprpc.StaticAddressLoopInResponse
11, // 116: looprpc.SwapClient.StaticOpenChannel:output_type -> looprpc.StaticOpenChannelResponse
84, // [84:117] is the sub-list for method output_type
51, // [51:84] is the sub-list for method input_type
51, // [51:51] is the sub-list for extension type_name
51, // [51:51] is the sub-list for extension extendee
0, // [0:51] is the sub-list for field type_name
8, // 7: looprpc.SwapStatus.static_loop_in_state:type_name -> looprpc.StaticAddressLoopInSwapState
3, // 8: looprpc.SwapStatus.failure_reason:type_name -> looprpc.FailureReason
88, // 9: looprpc.SwapStatus.asset_info:type_name -> looprpc.AssetLoopOutInfo
20, // 10: looprpc.ListSwapsRequest.list_swap_filter:type_name -> looprpc.ListSwapsFilter
9, // 11: looprpc.ListSwapsFilter.swap_type:type_name -> looprpc.ListSwapsFilter.SwapTypeFilter
18, // 12: looprpc.ListSwapsResponse.swaps:type_name -> looprpc.SwapStatus
24, // 13: looprpc.SweepHtlcResponse.not_requested:type_name -> looprpc.PublishNotRequested
25, // 14: looprpc.SweepHtlcResponse.published:type_name -> looprpc.PublishSucceeded
26, // 15: looprpc.SweepHtlcResponse.failed:type_name -> looprpc.PublishFailed
91, // 16: looprpc.QuoteRequest.loop_in_route_hints:type_name -> looprpc.RouteHint
85, // 17: looprpc.QuoteRequest.asset_info:type_name -> looprpc.AssetLoopOutRequest
86, // 18: looprpc.OutQuoteResponse.asset_rfq_info:type_name -> looprpc.AssetRfqInfo
91, // 19: looprpc.ProbeRequest.route_hints:type_name -> looprpc.RouteHint
40, // 20: looprpc.TokensResponse.tokens:type_name -> looprpc.L402Token
41, // 21: looprpc.GetInfoResponse.loop_out_stats:type_name -> looprpc.LoopStats
41, // 22: looprpc.GetInfoResponse.loop_in_stats:type_name -> looprpc.LoopStats
47, // 23: looprpc.LiquidityParameters.rules:type_name -> looprpc.LiquidityRule
0, // 24: looprpc.LiquidityParameters.account_addr_type:type_name -> looprpc.AddressType
89, // 25: looprpc.LiquidityParameters.easy_asset_params:type_name -> looprpc.LiquidityParameters.EasyAssetParamsEntry
4, // 26: looprpc.LiquidityParameters.loop_in_source:type_name -> looprpc.LoopInSource
1, // 27: looprpc.LiquidityRule.swap_type:type_name -> looprpc.SwapType
5, // 28: looprpc.LiquidityRule.type:type_name -> looprpc.LiquidityRuleType
45, // 29: looprpc.SetLiquidityParamsRequest.parameters:type_name -> looprpc.LiquidityParameters
6, // 30: looprpc.Disqualified.reason:type_name -> looprpc.AutoReason
14, // 31: looprpc.SuggestSwapsResponse.loop_out:type_name -> looprpc.LoopOutRequest
15, // 32: looprpc.SuggestSwapsResponse.loop_in:type_name -> looprpc.LoopInRequest
83, // 33: looprpc.SuggestSwapsResponse.static_loop_in:type_name -> looprpc.StaticAddressLoopInRequest
51, // 34: looprpc.SuggestSwapsResponse.disqualified:type_name -> looprpc.Disqualified
57, // 35: looprpc.ListReservationsResponse.reservations:type_name -> looprpc.ClientReservation
64, // 36: looprpc.ListInstantOutsResponse.swaps:type_name -> looprpc.InstantOut
69, // 37: looprpc.ListUnspentDepositsResponse.utxos:type_name -> looprpc.Utxo
92, // 38: looprpc.WithdrawDepositsRequest.outpoints:type_name -> lnrpc.OutPoint
7, // 39: looprpc.ListStaticAddressDepositsRequest.state_filter:type_name -> looprpc.DepositState
80, // 40: looprpc.ListStaticAddressDepositsResponse.filtered_deposits:type_name -> looprpc.Deposit
81, // 41: looprpc.ListStaticAddressWithdrawalResponse.withdrawals:type_name -> looprpc.StaticAddressWithdrawal
82, // 42: looprpc.ListStaticAddressSwapsResponse.swaps:type_name -> looprpc.StaticAddressLoopInSwap
7, // 43: looprpc.Deposit.state:type_name -> looprpc.DepositState
80, // 44: looprpc.StaticAddressWithdrawal.deposits:type_name -> looprpc.Deposit
8, // 45: looprpc.StaticAddressLoopInSwap.state:type_name -> looprpc.StaticAddressLoopInSwapState
80, // 46: looprpc.StaticAddressLoopInSwap.deposits:type_name -> looprpc.Deposit
91, // 47: looprpc.StaticAddressLoopInRequest.route_hints:type_name -> looprpc.RouteHint
80, // 48: looprpc.StaticAddressLoopInResponse.used_deposits:type_name -> looprpc.Deposit
87, // 49: looprpc.AssetRfqInfo.prepay_asset_rate:type_name -> looprpc.FixedPoint
87, // 50: looprpc.AssetRfqInfo.swap_asset_rate:type_name -> looprpc.FixedPoint
46, // 51: looprpc.LiquidityParameters.EasyAssetParamsEntry.value:type_name -> looprpc.EasyAssetAutoloopParams
14, // 52: looprpc.SwapClient.LoopOut:input_type -> looprpc.LoopOutRequest
15, // 53: looprpc.SwapClient.LoopIn:input_type -> looprpc.LoopInRequest
17, // 54: looprpc.SwapClient.Monitor:input_type -> looprpc.MonitorRequest
19, // 55: looprpc.SwapClient.ListSwaps:input_type -> looprpc.ListSwapsRequest
22, // 56: looprpc.SwapClient.SweepHtlc:input_type -> looprpc.SweepHtlcRequest
27, // 57: looprpc.SwapClient.SwapInfo:input_type -> looprpc.SwapInfoRequest
53, // 58: looprpc.SwapClient.AbandonSwap:input_type -> looprpc.AbandonSwapRequest
28, // 59: looprpc.SwapClient.LoopOutTerms:input_type -> looprpc.TermsRequest
31, // 60: looprpc.SwapClient.LoopOutQuote:input_type -> looprpc.QuoteRequest
28, // 61: looprpc.SwapClient.GetLoopInTerms:input_type -> looprpc.TermsRequest
31, // 62: looprpc.SwapClient.GetLoopInQuote:input_type -> looprpc.QuoteRequest
34, // 63: looprpc.SwapClient.Probe:input_type -> looprpc.ProbeRequest
36, // 64: looprpc.SwapClient.GetL402Tokens:input_type -> looprpc.TokensRequest
36, // 65: looprpc.SwapClient.GetLsatTokens:input_type -> looprpc.TokensRequest
38, // 66: looprpc.SwapClient.FetchL402Token:input_type -> looprpc.FetchL402TokenRequest
42, // 67: looprpc.SwapClient.GetInfo:input_type -> looprpc.GetInfoRequest
12, // 68: looprpc.SwapClient.StopDaemon:input_type -> looprpc.StopDaemonRequest
44, // 69: looprpc.SwapClient.GetLiquidityParams:input_type -> looprpc.GetLiquidityParamsRequest
48, // 70: looprpc.SwapClient.SetLiquidityParams:input_type -> looprpc.SetLiquidityParamsRequest
50, // 71: looprpc.SwapClient.SuggestSwaps:input_type -> looprpc.SuggestSwapsRequest
55, // 72: looprpc.SwapClient.ListReservations:input_type -> looprpc.ListReservationsRequest
58, // 73: looprpc.SwapClient.InstantOut:input_type -> looprpc.InstantOutRequest
60, // 74: looprpc.SwapClient.InstantOutQuote:input_type -> looprpc.InstantOutQuoteRequest
62, // 75: looprpc.SwapClient.ListInstantOuts:input_type -> looprpc.ListInstantOutsRequest
65, // 76: looprpc.SwapClient.NewStaticAddress:input_type -> looprpc.NewStaticAddressRequest
67, // 77: looprpc.SwapClient.ListUnspentDeposits:input_type -> looprpc.ListUnspentDepositsRequest
70, // 78: looprpc.SwapClient.WithdrawDeposits:input_type -> looprpc.WithdrawDepositsRequest
72, // 79: looprpc.SwapClient.ListStaticAddressDeposits:input_type -> looprpc.ListStaticAddressDepositsRequest
74, // 80: looprpc.SwapClient.ListStaticAddressWithdrawals:input_type -> looprpc.ListStaticAddressWithdrawalRequest
76, // 81: looprpc.SwapClient.ListStaticAddressSwaps:input_type -> looprpc.ListStaticAddressSwapsRequest
78, // 82: looprpc.SwapClient.GetStaticAddressSummary:input_type -> looprpc.StaticAddressSummaryRequest
83, // 83: looprpc.SwapClient.StaticAddressLoopIn:input_type -> looprpc.StaticAddressLoopInRequest
10, // 84: looprpc.SwapClient.StaticOpenChannel:input_type -> looprpc.StaticOpenChannelRequest
16, // 85: looprpc.SwapClient.LoopOut:output_type -> looprpc.SwapResponse
16, // 86: looprpc.SwapClient.LoopIn:output_type -> looprpc.SwapResponse
18, // 87: looprpc.SwapClient.Monitor:output_type -> looprpc.SwapStatus
21, // 88: looprpc.SwapClient.ListSwaps:output_type -> looprpc.ListSwapsResponse
23, // 89: looprpc.SwapClient.SweepHtlc:output_type -> looprpc.SweepHtlcResponse
18, // 90: looprpc.SwapClient.SwapInfo:output_type -> looprpc.SwapStatus
54, // 91: looprpc.SwapClient.AbandonSwap:output_type -> looprpc.AbandonSwapResponse
30, // 92: looprpc.SwapClient.LoopOutTerms:output_type -> looprpc.OutTermsResponse
33, // 93: looprpc.SwapClient.LoopOutQuote:output_type -> looprpc.OutQuoteResponse
29, // 94: looprpc.SwapClient.GetLoopInTerms:output_type -> looprpc.InTermsResponse
32, // 95: looprpc.SwapClient.GetLoopInQuote:output_type -> looprpc.InQuoteResponse
35, // 96: looprpc.SwapClient.Probe:output_type -> looprpc.ProbeResponse
37, // 97: looprpc.SwapClient.GetL402Tokens:output_type -> looprpc.TokensResponse
37, // 98: looprpc.SwapClient.GetLsatTokens:output_type -> looprpc.TokensResponse
39, // 99: looprpc.SwapClient.FetchL402Token:output_type -> looprpc.FetchL402TokenResponse
43, // 100: looprpc.SwapClient.GetInfo:output_type -> looprpc.GetInfoResponse
13, // 101: looprpc.SwapClient.StopDaemon:output_type -> looprpc.StopDaemonResponse
45, // 102: looprpc.SwapClient.GetLiquidityParams:output_type -> looprpc.LiquidityParameters
49, // 103: looprpc.SwapClient.SetLiquidityParams:output_type -> looprpc.SetLiquidityParamsResponse
52, // 104: looprpc.SwapClient.SuggestSwaps:output_type -> looprpc.SuggestSwapsResponse
56, // 105: looprpc.SwapClient.ListReservations:output_type -> looprpc.ListReservationsResponse
59, // 106: looprpc.SwapClient.InstantOut:output_type -> looprpc.InstantOutResponse
61, // 107: looprpc.SwapClient.InstantOutQuote:output_type -> looprpc.InstantOutQuoteResponse
63, // 108: looprpc.SwapClient.ListInstantOuts:output_type -> looprpc.ListInstantOutsResponse
66, // 109: looprpc.SwapClient.NewStaticAddress:output_type -> looprpc.NewStaticAddressResponse
68, // 110: looprpc.SwapClient.ListUnspentDeposits:output_type -> looprpc.ListUnspentDepositsResponse
71, // 111: looprpc.SwapClient.WithdrawDeposits:output_type -> looprpc.WithdrawDepositsResponse
73, // 112: looprpc.SwapClient.ListStaticAddressDeposits:output_type -> looprpc.ListStaticAddressDepositsResponse
75, // 113: looprpc.SwapClient.ListStaticAddressWithdrawals:output_type -> looprpc.ListStaticAddressWithdrawalResponse
77, // 114: looprpc.SwapClient.ListStaticAddressSwaps:output_type -> looprpc.ListStaticAddressSwapsResponse
79, // 115: looprpc.SwapClient.GetStaticAddressSummary:output_type -> looprpc.StaticAddressSummaryResponse
84, // 116: looprpc.SwapClient.StaticAddressLoopIn:output_type -> looprpc.StaticAddressLoopInResponse
11, // 117: looprpc.SwapClient.StaticOpenChannel:output_type -> looprpc.StaticOpenChannelResponse
85, // [85:118] is the sub-list for method output_type
52, // [52:85] is the sub-list for method input_type
52, // [52:52] is the sub-list for extension type_name
52, // [52:52] is the sub-list for extension extendee
0, // [0:52] is the sub-list for field type_name
}
func init() { file_client_proto_init() }
@ -7443,6 +7482,9 @@ func file_client_proto_init() {
if File_client_proto != nil {
return
}
file_client_proto_msgTypes[8].OneofWrappers = []any{
(*SwapStatus_StaticLoopInState)(nil),
}
file_client_proto_msgTypes[13].OneofWrappers = []any{
(*SweepHtlcResponse_NotRequested)(nil),
(*SweepHtlcResponse_Published)(nil),

View file

@ -546,10 +546,17 @@ message SwapStatus {
SwapType type = 3;
/*
State the swap is currently in, see State enum.
Generic loop-in/loop-out state for swaps.
*/
SwapState state = 4;
oneof static_loop_in_state_optional {
/*
Static address loop-in FSM state when type is STATIC_LOOP_IN.
*/
StaticAddressLoopInSwapState static_loop_in_state = 20;
}
/*
A failure reason for the swap, only set if the swap has failed.
*/
@ -608,6 +615,9 @@ enum SwapType {
// LOOP_IN indicates a loop in swap (on-chain to off-chain)
LOOP_IN = 1;
// STATIC_LOOP_IN indicates a static address loop in swap.
STATIC_LOOP_IN = 2;
}
enum SwapState {

View file

@ -3061,7 +3061,11 @@
},
"state": {
"$ref": "#/definitions/looprpcSwapState",
"description": "State the swap is currently in, see State enum."
"description": "Generic loop-in/loop-out state for swaps."
},
"static_loop_in_state": {
"$ref": "#/definitions/looprpcStaticAddressLoopInSwapState",
"description": "Static address loop-in FSM state when type is STATIC_LOOP_IN."
},
"failure_reason": {
"$ref": "#/definitions/looprpcFailureReason",
@ -3131,10 +3135,11 @@
"type": "string",
"enum": [
"LOOP_OUT",
"LOOP_IN"
"LOOP_IN",
"STATIC_LOOP_IN"
],
"default": "LOOP_OUT",
"title": "- LOOP_OUT: LOOP_OUT indicates an loop out swap (off-chain to on-chain)\n - LOOP_IN: LOOP_IN indicates a loop in swap (on-chain to off-chain)"
"description": " - LOOP_OUT: LOOP_OUT indicates an loop out swap (off-chain to on-chain)\n - LOOP_IN: LOOP_IN indicates a loop in swap (on-chain to off-chain)\n - STATIC_LOOP_IN: STATIC_LOOP_IN indicates a static address loop in swap."
},
"looprpcSweepHtlcRequest": {
"type": "object",

View file

@ -341,6 +341,11 @@ func (f *FSM) InitHtlcAction(ctx context.Context,
// Once the swap is stored, restart/recovery code owns invoice lifecycle.
invoiceNeedsCleanup = false
err = f.sendUpdate(ctx)
if err != nil {
f.Errorf("Error sending loop-in update: %v", err)
}
event = OnHtlcInitiated
return event

View file

@ -878,6 +878,68 @@ func TestCheckDepositsAvailableRejectsDivergentDepositOutpoints(
require.Empty(t, checker.outpoints)
}
// TestInitHtlcActionIgnoresSendUpdateErrorAfterPersistence protects the
// persistence-first invariant: once the loop-in is stored, a later status
// update failure must not roll back the action or state transition.
func TestInitHtlcActionIgnoresSendUpdateErrorAfterPersistence(t *testing.T) {
mockLnd := test.NewMockLnd()
_, serverKey := test.CreateKey(22)
server := &mockStaticAddressServer{
response: testStaticAddressLoopInResponse(
serverKey.SerializeCompressed(),
),
}
dep := &deposit.Deposit{
OutPoint: wire.OutPoint{
Hash: chainhash.Hash{2},
Index: 0,
},
Value: 500_000,
}
loopIn := &StaticAddressLoopIn{
Deposits: []*deposit.Deposit{dep},
DepositOutpoints: []string{dep.OutPoint.String()},
SelectedAmount: dep.Value,
QuotedSwapFee: 1_000,
InitiationHeight: uint32(mockLnd.Height),
InitiationTime: time.Now(),
PaymentTimeoutSeconds: 3_600,
}
sendUpdateErr := errors.New("status channel blocked")
sendUpdateCalled := false
f := &FSM{
StateMachine: &fsm.StateMachine{},
cfg: &Config{
Server: server,
DepositManager: &noopDepositManager{},
LndClient: mockLnd.Client,
WalletKit: mockLnd.WalletKit,
ChainParams: mockLnd.ChainParams,
Store: &mockStore{},
ValidateLoopInContract: testValidateLoopInContract,
MaxStaticAddrHtlcFeePercentage: 1,
MaxStaticAddrHtlcBackupFeePercentage: 1,
SendUpdate: func(context.Context,
*StaticAddressLoopIn) error {
sendUpdateCalled = true
return sendUpdateErr
},
},
loopIn: loopIn,
}
event := f.InitHtlcAction(t.Context(), nil)
require.Equal(t, OnHtlcInitiated, event)
require.Nil(t, f.LastActionError)
require.True(t, sendUpdateCalled)
}
// mockStaticAddressServer captures static-address loop-in requests in tests.
type mockStaticAddressServer struct {
swapserverrpc.StaticAddressServerClient

View file

@ -273,6 +273,22 @@ func (f *FSM) updateLoopIn(ctx context.Context, notification fsm.Notification) {
return
}
err = f.sendUpdate(ctx)
if err != nil {
f.Errorf("Error sending loop-in update: %v", err)
}
}
// sendUpdate publishes the latest loop-in state after it has been persisted.
// The callback must remain lightweight because it runs synchronously with FSM
// state transitions.
func (f *FSM) sendUpdate(ctx context.Context) error {
if f.cfg.SendUpdate == nil {
return nil
}
return f.cfg.SendUpdate(ctx, f.loopIn)
}
// isUpdateSkipped returns true if the loop-in should not be updated for the

View file

@ -98,6 +98,9 @@ type Config struct {
// request.
ValidateLoopInContract ValidateLoopInContract
// SendUpdate publishes a loop-in status update after it is persisted.
SendUpdate func(context.Context, *StaticAddressLoopIn) error
// MaxStaticAddrHtlcFeePercentage is the percentage of the swap amount
// that we allow the server to charge for the htlc transaction.
// Although highly unlikely, this is a defense against the server

View file

@ -245,6 +245,45 @@ func TestInitiateLoopInAllowsReservedAutoloopLabel(t *testing.T) {
require.Equal(t, selectedDeposit.Value, quoteGetter.amount)
}
// TestUpdateLoopInSendsUpdateAfterSuccessfulStoreUpdate protects the
// notification contract: after a successful database update, the manager must
// publish the stored loop-in state to listeners.
func TestUpdateLoopInSendsUpdateAfterSuccessfulStoreUpdate(t *testing.T) {
ctx := t.Context()
swapHash := lntypes.Hash{1, 2, 3}
updates := make(chan *StaticAddressLoopIn, 1)
loopIn := &StaticAddressLoopIn{SwapHash: swapHash}
loopIn.SetState(SignHtlcTx)
loopInFsm := &FSM{
cfg: &Config{
Store: &mockStore{stored: true},
SendUpdate: func(_ context.Context,
updated *StaticAddressLoopIn) error {
updates <- updated
return nil
},
},
loopIn: loopIn,
}
loopInFsm.updateLoopIn(ctx, fsm.Notification{
PreviousState: SignHtlcTx,
NextState: MonitorInvoiceAndHtlcTx,
})
select {
case updated := <-updates:
require.Equal(t, swapHash, updated.SwapHash)
require.Equal(t, MonitorInvoiceAndHtlcTx, updated.GetState())
case <-ctx.Done():
t.Fatal(ctx.Err())
}
}
// TestHandleLoopInSweepReqRejectsInvalidServerNonce ensures that a malformed
// MuSig2 nonce returned by the server is rejected before it reaches the signer.
func TestHandleLoopInSweepReqRejectsInvalidServerNonce(t *testing.T) {
@ -501,6 +540,7 @@ type mockStore struct {
swaps []*StaticAddressLoopIn
loopIns map[lntypes.Hash]*StaticAddressLoopIn
mapIDs map[lntypes.Hash][]deposit.ID
stored bool
}
func (s *mockStore) CreateLoopIn(_ context.Context,
@ -521,7 +561,7 @@ func (s *mockStore) GetStaticAddressLoopInSwapsByStates(_ context.Context,
return s.swaps, nil
}
func (s *mockStore) IsStored(_ context.Context, _ lntypes.Hash) (bool, error) {
return false, nil
return s.stored, nil
}
// RecordStaticAddressRiskDecision implements Store for manager tests.

View file

@ -5,6 +5,7 @@ import (
"database/sql"
"errors"
"strings"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
@ -23,6 +24,12 @@ import (
const OutpointSeparator = ";"
// sqlStoreUpdateTime returns the PostgreSQL-compatible timestamp precision used
// for persisted loop-in update metadata.
func sqlStoreUpdateTime(clock clock.Clock) time.Time {
return clock.Now().Truncate(time.Microsecond)
}
var (
// ErrInvalidOutpoint is returned when an outpoint contains the outpoint
// separator.
@ -288,13 +295,14 @@ func (s *SqlStore) CreateLoopIn(ctx context.Context,
Fast: loopIn.Fast,
}
updateTime := sqlStoreUpdateTime(s.clock)
updateArgs := sqlc.InsertStaticAddressMetaUpdateParams{
SwapHash: loopIn.SwapHash[:],
UpdateTimestamp: s.clock.Now(),
UpdateTimestamp: updateTime,
UpdateState: string(loopIn.GetState()),
}
return s.baseDB.ExecTx(ctx, loopdb.NewSqlWriteOpts(),
err := s.baseDB.ExecTx(ctx, loopdb.NewSqlWriteOpts(),
func(q Querier) error {
err := q.InsertSwap(ctx, swapArgs)
if err != nil {
@ -331,6 +339,13 @@ func (s *SqlStore) CreateLoopIn(ctx context.Context,
return q.InsertStaticAddressMetaUpdate(ctx, updateArgs)
},
)
if err != nil {
return err
}
loopIn.LastUpdateTime = updateTime
return nil
}
// UpdateLoopIn updates the loop-in in the database.
@ -351,13 +366,14 @@ func (s *SqlStore) UpdateLoopIn(ctx context.Context,
},
}
updateTime := sqlStoreUpdateTime(s.clock)
updateArgs := sqlc.InsertStaticAddressMetaUpdateParams{
SwapHash: loopIn.SwapHash[:],
UpdateState: string(loopIn.GetState()),
UpdateTimestamp: s.clock.Now(),
UpdateTimestamp: updateTime,
}
return s.baseDB.ExecTx(ctx, loopdb.NewSqlWriteOpts(),
err := s.baseDB.ExecTx(ctx, loopdb.NewSqlWriteOpts(),
func(q Querier) error {
err := q.UpdateStaticAddressLoopIn(ctx, updateParams)
if err != nil {
@ -367,6 +383,13 @@ func (s *SqlStore) UpdateLoopIn(ctx context.Context,
return q.InsertStaticAddressMetaUpdate(ctx, updateArgs)
},
)
if err != nil {
return err
}
loopIn.LastUpdateTime = updateTime
return nil
}
// RecordStaticAddressRiskDecision stores the server's confirmation-risk

View file

@ -252,7 +252,9 @@ func TestCreateLoopIn(t *testing.T) {
// Set up test context objects.
ctx := t.Context()
testDb := loopdb.NewTestDB(t)
testClock := clock.NewTestClock(time.Now())
createTime := time.Unix(1_717_171_717, 123_456_789).UTC()
expectedCreateTime := createTime.Truncate(time.Microsecond)
testClock := clock.NewTestClock(createTime)
defer testDb.Close()
depositStore := deposit.NewSqlStore(testDb.BaseDB)
@ -325,6 +327,7 @@ func TestCreateLoopIn(t *testing.T) {
err = swapStore.CreateLoopIn(ctx, &swapPending)
require.NoError(t, err)
require.Equal(t, expectedCreateTime, swapPending.LastUpdateTime)
depositIDs, err := swapStore.DepositIDsForSwapHash(
ctx, swapHashPending,
@ -349,6 +352,7 @@ func TestCreateLoopIn(t *testing.T) {
require.Equal(t, []string{d1.OutPoint.String(), d2.OutPoint.String()},
swap.DepositOutpoints)
require.Equal(t, SignHtlcTx, swap.GetState())
require.Equal(t, swapPending.LastUpdateTime, swap.LastUpdateTime)
require.Equal(
t, ConfirmationRiskDecisionNone,
swap.ConfirmationRiskDecision,
@ -445,14 +449,15 @@ func TestCreateLoopIn(t *testing.T) {
err = swapStore.UpdateLoopIn(ctx, &swapPending)
require.NoError(t, err)
require.Equal(
t, updateTime.Truncate(time.Microsecond),
swapPending.LastUpdateTime,
)
swap, err = swapStore.GetLoopInByHash(ctx, swapHashPending)
require.NoError(t, err)
require.Equal(t, Succeeded, swap.GetState())
require.WithinDuration(
t, updateTime.UTC(), swap.LastUpdateTime.UTC(),
time.Microsecond,
)
require.Equal(t, swapPending.LastUpdateTime, swap.LastUpdateTime)
}
// TestGetLoopInByHashOrdersDepositsBySnapshot ensures recovered deposits are

View file

@ -9,10 +9,13 @@ const (
// TypeOut is a loop out swap.
TypeOut
// TypeStaticAddressLoopIn is a static-address loop-in swap.
TypeStaticAddressLoopIn
)
// IsOut returns true if the swap is a loop out swap, false if it is a loop in
// swap.
// IsOut returns true only if the swap is TypeOut; TypeIn and
// TypeStaticAddressLoopIn both return false.
func (t Type) IsOut() bool {
return t == TypeOut
}
@ -23,6 +26,8 @@ func (t Type) String() string {
return "In"
case TypeOut:
return "Out"
case TypeStaticAddressLoopIn:
return "StaticAddressLoopIn"
default:
return "Unknown"
}