Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 56 additions & 7 deletions cmd/loop/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -585,10 +582,35 @@ func logSwap(swap *looprpc.SwapStatus) {
}
}

if swap.State != looprpc.SwapState_INITIATED &&
swap.State != looprpc.SwapState_HTLC_PUBLISHED &&
swap.State != looprpc.SwapState_PREIMAGE_REVEALED {
showCost := true
if swap.Type == looprpc.SwapType_STATIC_LOOP_IN {
staticState := swap.GetStaticLoopInState()
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_UNKNOWN_STATIC_ADDRESS_SWAP_STATE:

loopState := swap.GetState()
showCost = loopState != looprpc.SwapState_INITIATED &&
loopState != looprpc.SwapState_HTLC_PUBLISHED &&
loopState != looprpc.SwapState_PREIMAGE_REVEALED
}
} else {
loopState := swap.GetState()
showCost = loopState != looprpc.SwapState_INITIATED &&
loopState != looprpc.SwapState_HTLC_PUBLISHED &&
loopState != looprpc.SwapState_PREIMAGE_REVEALED
}

if showCost {
fmt.Printf(" (cost: server %v, onchain %v, offchain %v)",
swap.CostServer, swap.CostOnchain, swap.CostOffchain,
)
Expand All @@ -597,6 +619,33 @@ func logSwap(swap *looprpc.SwapStatus) {
fmt.Println()
}

func monitorSwapState(swap *looprpc.SwapStatus) string {
if swap.Type != looprpc.SwapType_STATIC_LOOP_IN {
return genericMonitorSwapState(swap)
}

staticState := swap.GetStaticLoopInState()
if staticState != looprpc.
StaticAddressLoopInSwapState_UNKNOWN_STATIC_ADDRESS_SWAP_STATE {

return fmt.Sprintf("%v", staticState)
}

return genericMonitorSwapState(swap)
}

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) {
Expand Down
173 changes: 173 additions & 0 deletions cmd/loop/monitor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package main

import (
"bytes"
"io"
"os"
"strings"
"testing"
"time"

"github.com/lightninglabs/loop/looprpc"
"github.com/stretchr/testify/require"
)

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)
}

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))
}

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,
HtlcAddressP2Tr: "tb1p5cyxnuxmeuwuvkwfem96llyxf8duyshm56t8k8",
}

output := captureStdout(t, func() {
logSwap(swap)
})

require.Contains(
t, output, "STATIC_LOOP_IN SIGN_HTLC_TX 0.00050000 BTC",
)
require.Contains(
t, output, "P2TR: tb1p5cyxnuxmeuwuvkwfem96llyxf8duyshm56t8k8",
)
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())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
{
"metadata": {
"args": [
"loop",
"monitor",
"--network",
"regtest"
],
"env": {},
"version": "0.31.7-beta commit=static-monitor-replay commit_hash=staticmonitorreplay",
"run_error": "recv: rpc error: code = Canceled desc = context canceled",
"duration": 1000000000,
"clock_start_unix": 1769414443
},
"events": [
{
"time_ms": 1,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/Monitor",
"event": "send",
"message_type": "looprpc.MonitorRequest",
"payload": {}
}
},
{
"time_ms": 1,
"kind": "stdout",
"data": {
"lines": [
"Note: offchain cost may report as 0 after loopd restart during swap\n"
]
}
},
{
"time_ms": 2,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/Monitor",
"event": "recv",
"message_type": "looprpc.SwapStatus",
"payload": {
"amt": "50000",
"id": "3f4ec343ba4087eec3cdf501b4e19733d9685cc151db360bb67d2c924677ce71",
"id_bytes": "P07DQ7pAh+7DzfUBtOGXM9loXMFR2zYLtn0skkZ3znE=",
"type": "STATIC_LOOP_IN",
"state": "INITIATED",
"static_loop_in_state": "SIGN_HTLC_TX",
Comment on lines +47 to +48

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@hieblmi It is the thing that I'm mostly concerned about

"failure_reason": "FAILURE_REASON_NONE",
"initiation_time": "1769414443188873986",
"last_update_time": "1769414443188873986",
"htlc_address": "bcrt1ppuw9wtl65avd026t9nkxux8whz3gyhkjh6n9cryp8a5xz9jacf9qlgxsch",
"htlc_address_p2wsh": "",
"htlc_address_p2tr": "bcrt1ppuw9wtl65avd026t9nkxux8whz3gyhkjh6n9cryp8a5xz9jacf9qlgxsch",
"cost_server": "999",
"cost_onchain": "888",
"cost_offchain": "777",
"last_hop": "",
"outgoing_chan_set": [],
"label": "",
"asset_info": null
}
}
},
{
"time_ms": 2,
"kind": "stdout",
"data": {
"lines": [
"2026-01-26T03:00:43-05:00 STATIC_LOOP_IN SIGN_HTLC_TX 0.00050000 BTC - P2TR: bcrt1ppuw9wtl65avd026t9nkxux8whz3gyhkjh6n9cryp8a5xz9jacf9qlgxsch\n"
]
}
},
{
"time_ms": 1000,
"kind": "signal",
"data": {
"signal": "interrupt"
}
},
{
"time_ms": 1000,
"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": 1000,
"kind": "stderr",
"data": {
"lines": [
"[loop] recv: rpc error: code = Canceled desc = context canceled\n"
]
}
},
{
"time_ms": 1000,
"kind": "exit",
"data": {
"run_error": "recv: rpc error: code = Canceled desc = context canceled"
}
}
]
}
7 changes: 6 additions & 1 deletion interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -488,9 +489,13 @@ 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 TypeStaticAddressLoopIn.
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
Expand Down
Loading
Loading