From 4a2bcb33747256bd68cf197c74030e7f009e61b3 Mon Sep 17 00:00:00 2001 From: Gustavo Stingelin Date: Wed, 8 Jul 2026 23:39:30 -0300 Subject: [PATCH] cmd/loop: display static loop-in monitor stages --- cmd/loop/main.go | 65 ++++- cmd/loop/monitor_test.go | 275 ++++++++++++++++++ .../04_loop-monitor-static-loop-in.json | 267 +++++++++++++++++ 3 files changed, 600 insertions(+), 7 deletions(-) create mode 100644 cmd/loop/monitor_test.go create mode 100644 cmd/loop/testdata/sessions/basic-swaps/04_loop-monitor-static-loop-in.json diff --git a/cmd/loop/main.go b/cmd/loop/main.go index 294095ca..127634d2 100644 --- a/cmd/loop/main.go +++ b/cmd/loop/main.go @@ -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) { diff --git a/cmd/loop/monitor_test.go b/cmd/loop/monitor_test.go new file mode 100644 index 00000000..9ed4841e --- /dev/null +++ b/cmd/loop/monitor_test.go @@ -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()) +} diff --git a/cmd/loop/testdata/sessions/basic-swaps/04_loop-monitor-static-loop-in.json b/cmd/loop/testdata/sessions/basic-swaps/04_loop-monitor-static-loop-in.json new file mode 100644 index 00000000..451d34f3 --- /dev/null +++ b/cmd/loop/testdata/sessions/basic-swaps/04_loop-monitor-static-loop-in.json @@ -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" + } + } + ] +}